Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,13 +474,16 @@
"type": "string"
},
"merged": {
"type": "number"
"type": "number",
"nullable": true
},
"closed": {
"type": "number"
"type": "number",
"nullable": true
},
"reversed": {
"type": "number"
"type": "number",
"nullable": true
},
"accuracyPct": {
"type": "number",
Expand Down
10 changes: 5 additions & 5 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ export const PublicStatsSchema = z
accuracyPct: z.number().nullable(),
}),
),
/** Trailing weekly history of totals.accuracyPct's SAME formula (#4447) -- null accuracyPct on a week means
* too few decided (merged+closed) PRs that week to publish a meaningful percentage, not zero accuracy. */
/** Trailing weekly history of totals.accuracyPct's SAME formula (#4447) -- null counts/accuracyPct on a week means
* too few decided (merged+closed) PRs to publish meaningful or non-identifying details. */
accuracyTrend: z.array(
z.object({
weekStart: z.string(),
merged: z.number(),
closed: z.number(),
reversed: z.number(),
merged: z.number().nullable(),
closed: z.number().nullable(),
reversed: z.number().nullable(),
accuracyPct: z.number().nullable(),
}),
),
Expand Down
21 changes: 9 additions & 12 deletions src/services/public-accuracy-trend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export const MIN_ACCURACY_TREND_SAMPLE = 3;
export type PublicAccuracyTrendWeek = {
/** UTC Monday (YYYY-MM-DD) that starts the bucket. */
weekStart: string;
merged: number;
closed: number;
reversed: number;
merged: number | null;
closed: number | null;
reversed: number | null;
accuracyPct: number | null;
};

Expand All @@ -34,11 +34,11 @@ function roundPct(value: number): number {

/** Same formula as public-stats.ts's accuracyPct, reused so the trend and the live number can never drift
* apart into two competing definitions of "accuracy". */
function accuracyPctOf(merged: number, closed: number, reversed: number): number | null {
const decided = merged + closed;
if (decided < MIN_ACCURACY_TREND_SAMPLE) return null;
const reversalRate = Math.min(1, reversed / decided);
return roundPct(1 - reversalRate);
function publicBucketOf(bucket: { merged: number; closed: number; reversed: number }): Omit<PublicAccuracyTrendWeek, "weekStart"> {
const decided = bucket.merged + bucket.closed;
if (decided < MIN_ACCURACY_TREND_SAMPLE) return { merged: null, closed: null, reversed: null, accuracyPct: null };
const reversalRate = Math.min(1, bucket.reversed / decided);
return { merged: bucket.merged, closed: bucket.closed, reversed: bucket.reversed, accuracyPct: roundPct(1 - reversalRate) };
}

/** Fold day-granularity rows into `weeks` trailing UTC-Monday buckets ending in the week containing `nowMs`.
Expand All @@ -61,10 +61,7 @@ export function buildPublicAccuracyTrend(dayRows: DayRow[], nowMs: number, weeks

return buckets.map((bucket, offset) => ({
weekStart: isoWeekStart(oldestStartMs + offset * MS_PER_WEEK),
merged: bucket.merged,
closed: bucket.closed,
reversed: bucket.reversed,
accuracyPct: accuracyPctOf(bucket.merged, bucket.closed, bucket.reversed),
...publicBucketOf(bucket),
}));
}

Expand Down
2 changes: 1 addition & 1 deletion test/integration/public-stats-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe("GET /v1/public/stats (#1059)", () => {
totals: Record<string, number | null>;
weekly: { reviewed: number; merged: number };
byProject: Array<{ project: string; reviewed: number }>;
accuracyTrend: Array<{ weekStart: string; merged: number; closed: number; reversed: number; accuracyPct: number | null }>;
accuracyTrend: Array<{ weekStart: string; merged: number | null; closed: number | null; reversed: number | null; accuracyPct: number | null }>;
reuseRateTrend: Array<{ weekStart: string; hits: number; misses: number; reuseRatePct: number | null }>;
reviewVolumeTrend: Array<{ weekStart: string; reviewed: number; merged: number; filteredPct: number | null }>;
};
Expand Down
22 changes: 10 additions & 12 deletions test/unit/public-accuracy-trend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,22 @@ describe("buildPublicAccuracyTrend", () => {
it("REGRESSION: ignores day rows outside the trailing window instead of letting them corrupt the oldest bucket", () => {
const currentMonday = isoWeekStart(NOW);
const tooOld = isoWeekStart(NOW - 30 * 86_400_000);
const trend = buildPublicAccuracyTrend([{ day: tooOld, merged: 999, closed: 999, reversed: 999 }, { day: currentMonday, merged: 1, closed: 0, reversed: 0 }], NOW, 2);
expect(trend[0]).toMatchObject({ merged: 0, closed: 0, reversed: 0 });
expect(trend[1]).toMatchObject({ merged: 1, closed: 0, reversed: 0 });
const trend = buildPublicAccuracyTrend([{ day: tooOld, merged: 999, closed: 999, reversed: 999 }, { day: currentMonday, merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 }], NOW, 2);
expect(trend[0]).toMatchObject({ merged: null, closed: null, reversed: null });
expect(trend[1]).toMatchObject({ merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 });
});

it("ignores an unparseable day string rather than throwing or corrupting a bucket", () => {
const currentMonday = isoWeekStart(NOW);
const trend = buildPublicAccuracyTrend([{ day: "not-a-date", merged: 5, closed: 5, reversed: 5 }, { day: currentMonday, merged: 1, closed: 0, reversed: 0 }], NOW, 1);
const trend = buildPublicAccuracyTrend([{ day: "not-a-date", merged: 5, closed: 5, reversed: 5 }, { day: currentMonday, merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 }], NOW, 1);
expect(trend).toHaveLength(1);
expect(trend[0]).toMatchObject({ merged: 1, closed: 0, reversed: 0 });
expect(trend[0]).toMatchObject({ merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 });
});

it("returns null accuracyPct (not a misleading 0% or 100%) below MIN_ACCURACY_TREND_SAMPLE decided PRs", () => {
it("REGRESSION: redacts counts and accuracyPct below MIN_ACCURACY_TREND_SAMPLE decided PRs", () => {
const week = isoWeekStart(NOW);
const trend = buildPublicAccuracyTrend([{ day: week, merged: MIN_ACCURACY_TREND_SAMPLE - 1, closed: 0, reversed: 0 }], NOW, 1);
expect(trend[0]?.accuracyPct).toBeNull();
expect(trend[0]).toMatchObject({ merged: null, closed: null, reversed: null, accuracyPct: null });
});

it("returns a real percentage at exactly MIN_ACCURACY_TREND_SAMPLE decided PRs", () => {
Expand All @@ -83,7 +83,7 @@ describe("buildPublicAccuracyTrend", () => {
it("returns all-zero, null-accuracy buckets for an empty input (a brand-new / not-yet-enabled deployment)", () => {
const trend = buildPublicAccuracyTrend([], NOW, 3);
expect(trend).toHaveLength(3);
for (const week of trend) expect(week).toMatchObject({ merged: 0, closed: 0, reversed: 0, accuracyPct: null });
for (const week of trend) expect(week).toMatchObject({ merged: null, closed: null, reversed: null, accuracyPct: null });
});
});

Expand Down Expand Up @@ -130,7 +130,7 @@ describe("loadPublicAccuracyTrend — end-to-end over the real live tables", ()
expect(currentWeek?.reversed).toBe(1);
});

it("still reports the Orb-fleet side when GITTENSORY_PUBLIC_STATS_REPOS is empty (no own-ledger allowlist)", async () => {
it("redacts a sparse Orb-fleet week when GITTENSORY_PUBLIC_STATS_REPOS is empty (no own-ledger allowlist)", async () => {
const env = createTestEnv({ GITTENSORY_PUBLIC_STATS_REPOS: "" });
const thisMonday = isoWeekStart(NOW);
const thisWeekIso = `${thisMonday}T09:00:00.000Z`;
Expand All @@ -141,8 +141,6 @@ describe("loadPublicAccuracyTrend — end-to-end over the real live tables", ()

const trend = await loadPublicAccuracyTrend(env, NOW);
const currentWeek = trend[trend.length - 1];
expect(currentWeek?.closed).toBe(1);
expect(currentWeek?.merged).toBe(0);
expect(currentWeek?.reversed).toBe(0);
expect(currentWeek).toMatchObject({ merged: null, closed: null, reversed: null, accuracyPct: null });
});
});
Loading