diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json
index eb4d35a72e..058ceb7fdb 100644
--- a/apps/gittensory-ui/public/openapi.json
+++ b/apps/gittensory-ui/public/openapi.json
@@ -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": [
@@ -531,7 +558,8 @@
"weekly",
"byProject",
"accuracyTrend",
- "reuseRateTrend"
+ "reuseRateTrend",
+ "reviewVolumeTrend"
]
},
"PublicQualityMetrics": {
diff --git a/apps/gittensory-ui/src/components/site/proof-of-power-stats-model.ts b/apps/gittensory-ui/src/components/site/proof-of-power-stats-model.ts
index a081836b53..0c9c18dc92 100644
--- a/apps/gittensory-ui/src/components/site/proof-of-power-stats-model.ts
+++ b/apps/gittensory-ui/src/components/site/proof-of-power-stats-model.ts
@@ -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). */
diff --git a/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx b/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx
index 9f930e61b8..7f0e05b914 100644
--- a/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx
+++ b/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx
@@ -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) {
@@ -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();
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 () => {
diff --git a/apps/gittensory-ui/src/components/site/proof-of-power-stats.tsx b/apps/gittensory-ui/src/components/site/proof-of-power-stats.tsx
index c0cb76dcce..9692fc9bd8 100644
--- a/apps/gittensory-ui/src/components/site/proof-of-power-stats.tsx
+++ b/apps/gittensory-ui/src/components/site/proof-of-power-stats.tsx
@@ -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 =
@@ -123,11 +126,13 @@ export function ProofOfPowerStats({ className }: { className?: string }) {
label="PRs reviewed"
value={}
hint={`${intFmt.format(totals.merged)} merged across ${repoCount} repo${repoCount === 1 ? "" : "s"}${weekly.reviewed > 0 ? ` · +${intFmt.format(weekly.reviewed)} this week` : ""}`}
+ trend={}
/>
}
/>
{
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);
}
diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts
index 6ffda995ec..599297a7f9 100644
--- a/src/openapi/schemas.ts
+++ b/src/openapi/schemas.ts
@@ -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");
diff --git a/src/services/public-accuracy-trend.ts b/src/services/public-accuracy-trend.ts
index a9e4834bcd..0ba7f5286b 100644
--- a/src/services/public-accuracy-trend.ts
+++ b/src/services/public-accuracy-trend.ts
@@ -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