Skip to content
Merged
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
30 changes: 29 additions & 1 deletion apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,33 @@
"reuseRatePct"
]
}
},
"reviewVolumeTrend": {
"type": "array",
"items": {
"type": "object",
"properties": {
"weekStart": {
"type": "string"
},
"reviewed": {
"type": "number"
},
"merged": {
"type": "number"
},
"filteredPct": {
"type": "number",
"nullable": true
}
},
"required": [
"weekStart",
"reviewed",
"merged",
"filteredPct"
]
}
}
},
"required": [
Expand All @@ -531,7 +558,8 @@
"weekly",
"byProject",
"accuracyTrend",
"reuseRateTrend"
"reuseRateTrend",
"reviewVolumeTrend"
]
},
"PublicQualityMetrics": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ export type PublicStats = {
misses: number;
reuseRatePct: number | null;
}>;
/** Trailing weekly review-volume/filtered-rate trend (#4445 follow-up) -- each week is the cohort of PRs
* first published that week; `merged` reflects their CURRENT disposition, not necessarily merged that
* same week. */
reviewVolumeTrend: Array<{
weekStart: string;
reviewed: number;
merged: number;
filteredPct: number | null;
}>;
};

/** Relative "updated Ns ago" label from the payload's updatedAt (mirrors MetaStrip's freshness logic). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ const PAYLOAD: PublicStats = {
{ weekStart: "2026-06-15", hits: 80, misses: 20, reuseRatePct: 80 },
{ weekStart: "2026-06-22", hits: 83, misses: 17, reuseRatePct: 83 },
],
reviewVolumeTrend: [
{ weekStart: "2026-05-04", reviewed: 2, merged: 1, filteredPct: null },
{ weekStart: "2026-05-11", reviewed: 300, merged: 150, filteredPct: 50 },
{ weekStart: "2026-05-18", reviewed: 310, merged: 160, filteredPct: 48.4 },
{ weekStart: "2026-05-25", reviewed: 320, merged: 165, filteredPct: 48.4 },
{ weekStart: "2026-06-01", reviewed: 330, merged: 170, filteredPct: 48.5 },
{ weekStart: "2026-06-08", reviewed: 340, merged: 175, filteredPct: 48.5 },
{ weekStart: "2026-06-15", reviewed: 350, merged: 180, filteredPct: 48.6 },
{ weekStart: "2026-06-22", reviewed: 358, merged: 185, filteredPct: 48.3 },
],
};

function renderWithClient(ui: ReactNode) {
Expand Down Expand Up @@ -146,12 +156,13 @@ describe("ProofOfPowerStats", () => {
expect(screen.getByText("avoided redoing prior AI work")).toBeTruthy();
});

it("renders a sparkline beside accuracy and reuse-rate, each labeled by its own week count", async () => {
it("renders a sparkline beside all four trend-backed tiles, each labeled by its own week count", async () => {
apiFetch.mockResolvedValue({ ok: true, status: 200, durationMs: 1, data: PAYLOAD });
renderWithClient(<ProofOfPowerStats />);
await screen.findByText("Decision accuracy");
const sparklines = screen.getAllByRole("img", { name: "Trend over the last 8 weeks" });
expect(sparklines).toHaveLength(2); // accuracy + reuse-rate, both 8-week payloads
// PRs reviewed + Filtered without merge + Decision accuracy + AI work reused, all 8-week payloads.
expect(sparklines).toHaveLength(4);
});

it("settles the count-up on the real reviewed total (not stuck at 0 when rAF never fires)", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,12 @@ export function ProofOfPowerStats({ className }: { className?: string }) {
const { totals, weekly, byProject } = data;
const repoCount = byProject.length;
const timeSaved = formatTimeSaved(totals.minutesSaved);
// #4447/#4448: 8-week sparklines riding beside the two tiles that have a weekly trend to show. The other
// tiles (PRs reviewed, filtered %, time saved) have no persisted weekly series, only a lifetime total plus a
// single "this week" delta -- nothing for a sparkline to plot yet.
// #4447/#4448/#4445-follow-up: 8-week sparklines riding beside every tile that has a weekly trend to show.
// "Maintainer time saved" is the one tile left without one -- it's a fixed multiple of PRs-reviewed
// (minutesSaved = reviewed × ~20min), so its own trend line would just be a rescaled copy of the reviewed
// sparkline, not new information.
const reviewedSparkline = toTrendPoints(data.reviewVolumeTrend, (week) => week.reviewed);
const filteredSparkline = toTrendPoints(data.reviewVolumeTrend, (week) => week.filteredPct);
const accuracySparkline = toTrendPoints(data.accuracyTrend, (week) => week.accuracyPct);
const reuseRateSparkline = toTrendPoints(data.reuseRateTrend, (week) => week.reuseRatePct);
const latestReuseRatePct =
Expand All @@ -123,11 +126,13 @@ export function ProofOfPowerStats({ className }: { className?: string }) {
label="PRs reviewed"
value={<Num value={totals.reviewed} />}
hint={`${intFmt.format(totals.merged)} merged across ${repoCount} repo${repoCount === 1 ? "" : "s"}${weekly.reviewed > 0 ? ` · +${intFmt.format(weekly.reviewed)} this week` : ""}`}
trend={<Sparkline points={reviewedSparkline} color="var(--chart-3)" />}
/>
<Stat
label="Filtered without merge"
value={totals.filteredPct == null ? "—" : `${totals.filteredPct}%`}
hint={`${intFmt.format(totals.reviewed - totals.merged)} closed, advised, or escalated`}
trend={<Sparkline points={filteredSparkline} color="var(--chart-4)" />}
/>
<Stat
label="Maintainer time saved"
Expand Down
10 changes: 8 additions & 2 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ import { isRagEnabled } from "../review/rag-wire";
import { getPublicStats, isPublicStatsEnabled } from "../review/public-stats";
import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend";
import { loadPublicReuseRateTrend } from "../services/public-reuse-rate-trend";
import { loadPublicReviewVolumeTrend } from "../services/public-review-volume-trend";
import { buildMaintainerQualityDashboard, isMaintainerQualityDataStale } from "../services/maintainer-quality-dashboard";
import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics";
import { compileFocusManifestPolicy, MAX_FOCUS_MANIFEST_BYTES, normalizeReadinessGateMode } from "../signals/focus-manifest";
Expand Down Expand Up @@ -960,9 +961,14 @@ export function createApp() {
app.get("/v1/public/stats", async (c) => {
if (!isPublicStatsEnabled(c.env)) return c.json({ error: "not_found" }, 404);
try {
const [stats, accuracyTrend, reuseRateTrend] = await Promise.all([getPublicStats(c.env), loadPublicAccuracyTrend(c.env), loadPublicReuseRateTrend(c.env)]);
const [stats, accuracyTrend, reuseRateTrend, reviewVolumeTrend] = await Promise.all([
getPublicStats(c.env),
loadPublicAccuracyTrend(c.env),
loadPublicReuseRateTrend(c.env),
loadPublicReviewVolumeTrend(c.env),
]);
c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=300");
return c.json({ ...stats, accuracyTrend, reuseRateTrend });
return c.json({ ...stats, accuracyTrend, reuseRateTrend, reviewVolumeTrend });
} catch {
return c.json({ error: "public_stats_unavailable" }, 503);
}
Expand Down
13 changes: 13 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ export const PublicStatsSchema = z
reuseRatePct: z.number().nullable(),
}),
),
/** Trailing weekly PR-review-volume/filtered-rate trend (#4445 follow-up) -- each week is the COHORT of PRs
* first published that week, `merged` reflects their CURRENT disposition (not necessarily merged the same
* week), and null filteredPct means too few reviewed PRs that week to publish a meaningful percentage. The
* most recent 1-2 weeks can read a lower filteredPct than they'll eventually settle at, since some of that
* cohort may still be in flight. */
reviewVolumeTrend: z.array(
z.object({
weekStart: z.string(),
reviewed: z.number(),
merged: z.number(),
filteredPct: z.number().nullable(),
}),
),
})
.openapi("PublicStats");

Expand Down
5 changes: 3 additions & 2 deletions src/services/public-accuracy-trend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ async function loadReversalDayRows(env: Env, projects: string[], sinceIso: strin

/** Day-bucketed Orb-fleet merged/closed, matching getOrbGlobalStats (orb/outcomes.ts) exactly except for the
* added `GROUP BY day`. No excludeAccount here, mirroring getPublicStats's own choice not to exclude any
* account from the homepage total (see public-stats.ts's file header). */
async function loadOrbDayRows(env: Env, sinceIso: string): Promise<Map<string, { merged: number; closed: number }>> {
* account from the homepage total (see public-stats.ts's file header). Exported for reuse by the sibling
* review-volume trend (#4445 follow-up), which needs the SAME per-day Orb split for its own "reviewed" total. */
export async function loadOrbDayRows(env: Env, sinceIso: string): Promise<Map<string, { merged: number; closed: number }>> {
const map = new Map<string, { merged: number; closed: number }>();
const rows = await safeAll<{ day: string; merged: number; closed: number }>(
env,
Expand Down
134 changes: 134 additions & 0 deletions src/services/public-review-volume-trend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Public "PRs reviewed" / "Filtered without merge %" weekly trend (#4445 follow-up, sibling to #4447's
// accuracy trend and #4448's reuse-rate trend). The homepage already shows LIVE lifetime totals.reviewed and
// totals.filteredPct (public-stats.ts's own formula: (reviewed - merged) / reviewed) but no history.
//
// DELIBERATELY a per-week COHORT, not an independent per-day event count (unlike accuracyTrend, which buckets
// merged/closed by their OWN respective event dates as two independent series): filteredPct only means anything
// evaluated against a FIXED set of reviewed PRs, so each week's bucket is "of the PRs first published THAT
// week, how many are (as of now) merged" -- mirrors getPublicStats's own weeklyRows subquery (own-ledger side),
// just grouped by day instead of filtered by a single `sinceIso` threshold. A side effect worth knowing: the
// most recent 1-2 weeks' cohorts include PRs still in flight (not yet merged or closed), so their filteredPct
// can read lower than it will once those PRs resolve -- an honest "not enough time has passed yet" artifact,
// not a bug.
//
// DELIBERATELY NOT a persisted/cron rollup, mirroring #4447/#4448's own design: audit_events and pull_requests
// are already durable, so a live weekly re-bucketing of the SAME rows can recompute any historical week
// correctly on every request -- no cron-miss gap risk, no second copy of the number to keep in sync, and the
// SAME formula as the live figure by construction.
import { PUBLISHED_PR_KEYS, publicStatsProjects, safeAll } from "../review/public-stats";
import { isoWeekStart } from "./public-quality-metrics";
import { loadOrbDayRows } from "./public-accuracy-trend";

export const PUBLIC_REVIEW_VOLUME_TREND_WEEKS = 8;
/** Below this many reviewed PRs in a week, that week's filteredPct is too noisy to publish (the raw `reviewed`
* count itself is always shown -- a count needs no sample-size guard the way a ratio does). */
export const MIN_REVIEW_VOLUME_TREND_SAMPLE = 3;

export type PublicReviewVolumeTrendWeek = {
/** UTC Monday (YYYY-MM-DD) that starts the bucket. */
weekStart: string;
reviewed: number;
merged: number;
filteredPct: number | null;
};

type DayRow = { day: string; reviewed: number; merged: number };

const MS_PER_WEEK = 7 * 86_400_000;

function roundPct(value: number): number {
return Math.round(value * 1000) / 10;
}

/** Same formula as public-stats.ts's filteredPct, reused so the trend and the live number can never drift
* apart into two competing definitions of "filtered". */
function filteredPctOf(reviewed: number, merged: number): number | null {
if (reviewed < MIN_REVIEW_VOLUME_TREND_SAMPLE) return null;
return roundPct((reviewed - merged) / reviewed);
}

/** Fold day-granularity rows into `weeks` trailing UTC-Monday buckets ending in the week containing `nowMs`.
* Pure -- mirrors buildPublicAccuracyTrend's own bucketing shape (public-accuracy-trend.ts, #4447). */
export function buildPublicReviewVolumeTrend(dayRows: DayRow[], nowMs: number, weeks: number = PUBLIC_REVIEW_VOLUME_TREND_WEEKS): PublicReviewVolumeTrendWeek[] {
const currentStartMs = Date.parse(isoWeekStart(nowMs));
const oldestStartMs = currentStartMs - (weeks - 1) * MS_PER_WEEK;
const buckets = Array.from({ length: weeks }, () => ({ reviewed: 0, merged: 0 }));

for (const row of dayRows) {
const dayMs = Date.parse(`${row.day}T00:00:00.000Z`);
if (!Number.isFinite(dayMs)) continue;
const weekOffset = Math.floor((dayMs - oldestStartMs) / MS_PER_WEEK);
if (weekOffset < 0 || weekOffset >= weeks) continue;
const bucket = buckets[weekOffset]!;
bucket.reviewed += row.reviewed;
bucket.merged += row.merged;
}

return buckets.map((bucket, offset) => ({
weekStart: isoWeekStart(oldestStartMs + offset * MS_PER_WEEK),
reviewed: bucket.reviewed,
merged: bucket.merged,
filteredPct: filteredPctOf(bucket.reviewed, bucket.merged),
}));
}

/** Day-bucketed own-ledger reviewed/merged COHORTS: for each PR first published on a given day, `reviewed`
* credits that day and `merged` credits it too IF the PR is (as of now) merged -- regardless of which day the
* merge itself happened on. Matches getPublicStats's own weeklyRows subquery (same MIN(created_at)/
* MAX(merged_at) shape, same GROUP BY ev.repo, ev.number), just grouped by day and scoped by a HAVING clause
* instead of a single sinceIso threshold. */
async function loadOwnLedgerDayRows(env: Env, projects: string[], sinceIso: string): Promise<Map<string, { reviewed: number; merged: number }>> {
const map = new Map<string, { reviewed: number; merged: number }>();
if (projects.length === 0) return map;
const inList = projects.map(() => "?").join(", ");
const rows = await safeAll<{ day: string; reviewed: number; merged: number }>(
env,
`SELECT date(first_seen) AS day,
COUNT(*) AS reviewed,
SUM(CASE WHEN merged_at IS NOT NULL THEN 1 ELSE 0 END) AS merged
FROM (
SELECT ev.repo, ev.number, MIN(ev.created_at) AS first_seen, MAX(pr.merged_at) AS merged_at
FROM (${PUBLISHED_PR_KEYS}) ev
LEFT JOIN pull_requests pr ON pr.repo_full_name = ev.repo AND pr.number = ev.number
WHERE LOWER(ev.repo) IN (${inList})
GROUP BY ev.repo, ev.number
)
GROUP BY day
HAVING date(first_seen) >= date(?)`,
...projects,
sinceIso,
);
/* v8 ignore next -- SUM(CASE WHEN ... THEN 1 ELSE 0 END) over an existing GROUP BY day always yields a
* defined integer (0 or more), never SQL NULL, so the ?? 0 fallback can't currently be exercised; kept for
* defense against a future query-shape change (mirrors public-accuracy-trend.ts's identical guard). */
for (const row of rows) map.set(row.day, { reviewed: row.reviewed ?? 0, merged: row.merged ?? 0 });
return map;
}

/** Assemble the public review-volume trend from the SAME live tables getPublicStats already reads, folding the
* Orb fleet's per-day merged+closed into `reviewed`/`merged` exactly as getPublicStats folds orb.total/
* orb.merged into totals.handled/totals.merged for the lifetime figure. Fail-safe: each underlying query
* degrades to [] on error (safeAll), so a single bad query yields under-counted weeks rather than throwing the
* whole public stats payload. */
export async function loadPublicReviewVolumeTrend(env: Env, nowMs: number = Date.now()): Promise<PublicReviewVolumeTrendWeek[]> {
const projects = publicStatsProjects(env);
const sinceIso = new Date(Date.parse(isoWeekStart(nowMs)) - (PUBLIC_REVIEW_VOLUME_TREND_WEEKS - 1) * MS_PER_WEEK).toISOString();

const [ownLedger, orb] = await Promise.all([
loadOwnLedgerDayRows(env, projects, sinceIso),
loadOrbDayRows(env, sinceIso),
]);

const days = new Set([...ownLedger.keys(), ...orb.keys()]);
const dayRows: DayRow[] = [...days].map((day) => {
const orbDay = orb.get(day);
const orbReviewed = orbDay ? orbDay.merged + orbDay.closed : 0;
return {
day,
reviewed: (ownLedger.get(day)?.reviewed ?? 0) + orbReviewed,
merged: (ownLedger.get(day)?.merged ?? 0) + (orbDay?.merged ?? 0),
};
});

return buildPublicReviewVolumeTrend(dayRows, nowMs);
}
10 changes: 10 additions & 0 deletions test/integration/public-stats-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createApp } from "../../src/api/routes";
import { createTestEnv } from "../helpers/d1";
import { PUBLIC_ACCURACY_TREND_WEEKS } from "../../src/services/public-accuracy-trend";
import { PUBLIC_REUSE_RATE_TREND_WEEKS } from "../../src/services/public-reuse-rate-trend";
import { PUBLIC_REVIEW_VOLUME_TREND_WEEKS } from "../../src/services/public-review-volume-trend";

/** Seed the LIVE ledger: a published-review surface per reviewed PR (audit_events) + each PR's terminal
* disposition (pull_requests state/merged_at), plus one live reversal (an engine close on a now-reopened PR). */
Expand Down Expand Up @@ -64,6 +65,7 @@ describe("GET /v1/public/stats (#1059)", () => {
byProject: Array<{ project: string; reviewed: number }>;
accuracyTrend: Array<{ weekStart: string; merged: number; closed: number; reversed: number; 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 }>;
};
expect(body.totals.handled).toBe(5); // distinct reviewed PRs
expect(body.totals.merged).toBe(3);
Expand All @@ -86,5 +88,13 @@ describe("GET /v1/public/stats (#1059)", () => {
// #4448: the weekly AI-work reuse-rate trend rides along on the SAME response too.
expect(body.reuseRateTrend).toHaveLength(PUBLIC_REUSE_RATE_TREND_WEEKS);
for (const week of body.reuseRateTrend) expect(typeof week.weekStart).toBe("string");
// #4445 follow-up: the weekly review-volume/filtered-rate trend rides along on the SAME response too.
expect(body.reviewVolumeTrend).toHaveLength(PUBLIC_REVIEW_VOLUME_TREND_WEEKS);
for (const week of body.reviewVolumeTrend) expect(typeof week.weekStart).toBe("string");
// All 5 seeded PRs were published "now" (no explicit created_at in the seed), so the whole cohort lands in
// the current week: reviewed 5, merged 3 -- the SAME totals as the lifetime totals.reviewed/merged above.
const currentWeek = body.reviewVolumeTrend[body.reviewVolumeTrend.length - 1];
expect(currentWeek?.reviewed).toBe(5);
expect(currentWeek?.merged).toBe(3);
});
});
Loading
Loading