Skip to content

Commit baaedcd

Browse files
committed
fix(stats): bound the accuracy denominator to the window its numerator survives in
#9718 gated accuracy on whether a reversal is observable at all, and fixed the weekly trend's Orb-folded denominator. It did not fix the third asymmetry in #9676: `merged`/`closed` come from `github_app.pr_public_surface_published`, the single retention-EXEMPT audit event type, so they are lifetime and never shrink -- while `reversal_*` rows prune with the rest of audit_events at 90 days. Pairing an immortal denominator with a 90-day numerator makes `1 - reversed/decided` drift toward 100% as the ledger ages, independent of real reversal behavior. Confirmed live after #9718 deployed: byProject still published 100% for all three repos on 2377/602/508 reviewed with 0 reversals, because the reversal signal IS observable on that deployment -- the gate passed and the ratio was still meaningless. The accuracy denominator is now a second, retention-windowed disposition query that shares BOTH of the numerator's bounds: own-ledger only, and inside audit_events' retention window. `reviewed`/`merged`/`closed` keep publishing lifetime volume, which is measured and correct. That display-vs-denominator split is the same shape #7449 established for the Orb fold and #9718 carried into the weekly trend; the #7449 snapshot it supersedes is removed rather than left dead, with its reasoning folded into the new comment. Boundary case, stated rather than hidden: a PR published before the window but reversed inside it contributes to the numerator and not the denominator. accuracyPct's existing clamp already handles that (it is the same shape as the reopened-auto-close case its comment describes), and the alternative -- an unbounded denominator -- is the bug being fixed. Refs #9676
1 parent e5cc2cd commit baaedcd

2 files changed

Lines changed: 75 additions & 10 deletions

File tree

src/review/public-stats.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,10 @@ export async function getPublicStats(
356356
// The own-ledger side needs at least one allowlisted project to query; an empty allowlist skips these three
357357
// queries entirely (own-ledger totals stay zero) but still lets the Orb aggregate below run.
358358
const inList = projects.map(() => "?").join(", ");
359-
const [dispositions, reversalRows, weeklyRows, effortRows] = projects.length === 0
359+
const [dispositions, windowedDispositions, reversalRows, weeklyRows, effortRows] = projects.length === 0
360360
? await Promise.all([
361361
Promise.resolve<DispositionRow[]>([]),
362+
Promise.resolve<{ project: string; merged: number; closed: number }[]>([]),
362363
Promise.resolve<{ project: string; reversed: number }[]>([]),
363364
Promise.resolve<{ reviewed: number; merged: number }[]>([]),
364365
Promise.resolve<{ totalMinutes: number | null }[]>([]),
@@ -377,6 +378,25 @@ export async function getPublicStats(
377378
GROUP BY ev.repo`,
378379
...projects,
379380
),
381+
// The ACCURACY denominator, bounded to the same window its numerator can survive in. `reviewed`/
382+
// `merged`/`closed` above are lifetime: `github_app.pr_public_surface_published` is the single
383+
// retention-EXEMPT audit event type (src/db/retention.ts's DURABLE_AUDIT_EVENT_TYPES), so those
384+
// counts never shrink. `reversal_*` rows are pruned with the rest of audit_events at 90 days. Pairing
385+
// an immortal denominator with a 90-day numerator makes `1 - reversed/decided` drift toward 100% as
386+
// the ledger ages, independent of real reversal behavior -- the same shape #7449 fixed for the
387+
// Orb-folded denominator, and confirmed live (2377/602/508 reviewed, 0 reversals, 100% each).
388+
safeAll<{ project: string; merged: number; closed: number }>(
389+
env,
390+
`SELECT ev.repo AS project,
391+
SUM(CASE WHEN pr.merged_at IS NOT NULL THEN 1 ELSE 0 END) AS merged,
392+
SUM(CASE WHEN pr.state = 'closed' AND pr.merged_at IS NULL THEN 1 ELSE 0 END) AS closed
393+
FROM (SELECT DISTINCT repo, number FROM (${PUBLISHED_PR_KEYS}) WHERE created_at >= ?) ev
394+
LEFT JOIN pull_requests pr ON pr.repo_full_name = ev.repo AND pr.number = ev.number
395+
WHERE LOWER(ev.repo) IN (${inList})
396+
GROUP BY ev.repo`,
397+
retentionCutoffIsoForTable("audit_events"),
398+
...projects,
399+
),
380400
safeAll<{ project: string; reversed: number }>(
381401
env,
382402
// A "reversal" = a human overturning a terminal engine auto-action, already detected and recorded by
@@ -466,6 +486,10 @@ export async function getPublicStats(
466486
// question about the same deployment, rather than one of them silently disagreeing with the others.
467487
const totalReversals = [...reversedByProject.values()].reduce((sum, n) => sum + n, 0);
468488
const reversalObservable = await loadReversalObservability(env, totalReversals);
489+
// project -> the retention-windowed merged/closed pairing for `reversed`. See the query's own comment.
490+
const windowedByProject = new Map(windowedDispositions.map((row) => [String(row.project).toLowerCase(), row]));
491+
let windowedMerged = 0;
492+
let windowedClosed = 0;
469493
const byProject = dispositions
470494
.map((d) => {
471495
const merged = d.merged ?? 0;
@@ -474,6 +498,11 @@ export async function getPublicStats(
474498
const reversed =
475499
reversedByProject.get(String(d.project).toLowerCase()) ?? 0;
476500
const reviewed = merged + closed + inReview;
501+
const windowed = windowedByProject.get(String(d.project).toLowerCase());
502+
const windowedRepoMerged = windowed?.merged ?? 0;
503+
const windowedRepoClosed = windowed?.closed ?? 0;
504+
windowedMerged += windowedRepoMerged;
505+
windowedClosed += windowedRepoClosed;
477506
totals.handled += reviewed;
478507
totals.merged += merged;
479508
totals.closed += closed;
@@ -485,7 +514,7 @@ export async function getPublicStats(
485514
reviewed,
486515
merged,
487516
closed,
488-
accuracyPct: accuracyPct(merged, closed, reversed, reversalObservable),
517+
accuracyPct: accuracyPct(windowedRepoMerged, windowedRepoClosed, reversed, reversalObservable),
489518
};
490519
})
491520
.filter((r) => r.reviewed > 0)
@@ -501,13 +530,13 @@ export async function getPublicStats(
501530
// Snapshot before Orb merge: effort SQL only covers allowlisted own-ledger publishes, while `reviewed`
502531
// below includes Orb fleet outcomes folded into totals.merged/closed.
503532
const ownLedgerReviewed = reviewedOf(totals);
504-
// #7449: also snapshot the pre-fold own-ledger merged/closed. totals.reversed stays own-ledger-only (the Orb
505-
// aggregate has no reversal concept), so the published global accuracyPct below is computed from THESE, not the
506-
// fleet-folded totals.merged/closed -- otherwise the denominator would grow with every newly registered install
507-
// while the numerator stayed own-ledger-scoped, trending the percentage toward 100 independent of real reversal
508-
// behavior. The fleet fold still (correctly) inflates reviewed/handled/minutesSaved, which have no such pairing.
509-
const ownLedgerMerged = totals.merged;
510-
const ownLedgerClosed = totals.closed;
533+
// #7449 (superseded, same reasoning): the published global accuracyPct is NOT computed from
534+
// totals.merged/closed. Those are lifetime AND fleet-folded, while totals.reversed is own-ledger-only and
535+
// 90-day-pruned, so pairing them lets the denominator grow -- with every newly registered install, and with
536+
// every additional day of immortal publish events -- while the numerator cannot, trending the percentage
537+
// toward 100 independent of real reversal behavior. windowedMerged/windowedClosed (accumulated above) is the
538+
// pairing that shares BOTH of the numerator's bounds: own-ledger only, and inside audit_events' retention
539+
// window. The fleet fold still correctly inflates reviewed/handled/minutesSaved, which have no such pairing.
511540
// Fleet accuracy (bugfix, #fairness-analytics): independent of the own-ledger allowlist above, so it's fetched
512541
// unconditionally alongside the Orb global fold, matching that fold's own unscoped-regardless-of-allowlist
513542
// behavior (see the "skips the own-ledger queries but still queries the Orb aggregate" test).
@@ -613,7 +642,7 @@ export async function getPublicStats(
613642
// Option 1 of #7449: compute the global accuracy from the OWN-LEDGER merged/closed snapshot (not the
614643
// fleet-folded totals.merged/closed), so its numerator (own-ledger reversed) and denominator are drawn
615644
// from the same population. See the ownLedgerMerged/ownLedgerClosed snapshot above the Orb fold for why.
616-
accuracyPct: accuracyPct(ownLedgerMerged, ownLedgerClosed, totals.reversed, reversalObservable),
645+
accuracyPct: accuracyPct(windowedMerged, windowedClosed, totals.reversed, reversalObservable),
617646
minutesSaved,
618647
},
619648
weekly: { reviewed: w.reviewed ?? 0, merged: w.merged ?? 0 },

test/unit/public-stats.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,42 @@ describe("getPublicStats — live aggregate over the review ledger", () => {
737737
expect(out.byProject.map((row) => row.accuracyPct)).toEqual([null]);
738738
});
739739

740+
it("REGRESSION: the accuracy denominator is bounded to audit_events' retention window, not lifetime (real D1)", async () => {
741+
// `github_app.pr_public_surface_published` is the ONE retention-exempt event type, so reviewed/merged/
742+
// closed are immortal, while `reversal_*` rows prune at 90 days. Pairing them made 1 - reversed/decided
743+
// drift toward 100% as the ledger aged -- live: 2377/602/508 reviewed, 0 reversals, 100% each.
744+
const env = createTestEnv({ LOOPOVER_PUBLIC_STATS_REPOS: "JSONbored/loopover" });
745+
const old = new Date(NOW - 200 * 86_400_000).toISOString(); // outside the 90-day window
746+
const recent = new Date(NOW - 5 * 86_400_000).toISOString();
747+
await upsertRepositoryFromGitHub(env, { name: "loopover", full_name: "JSONbored/loopover", private: false, owner: { login: "JSONbored" } }, 1);
748+
749+
// Four ancient merged PRs -- they keep counting toward `reviewed`, but must NOT pad the accuracy
750+
// denominator, because a reversal of any of them would long since have been pruned.
751+
for (const number of [1, 2, 3, 4]) {
752+
await upsertPullRequestFromGitHub(env, "JSONbored/loopover", { number, title: `old ${number}`, state: "closed", merged_at: old, user: { login: "a" }, head: { sha: `s${number}` }, labels: [] });
753+
await recordAuditEvent(env, { eventType: "github_app.pr_public_surface_published", targetKey: `JSONbored/loopover#${number}`, outcome: "completed", createdAt: old });
754+
}
755+
// One recent auto-closed PR, reversed by a human.
756+
await upsertPullRequestFromGitHub(env, "JSONbored/loopover", { number: 5, title: "recent", state: "open", user: { login: "b" }, head: { sha: "s5" }, labels: [] });
757+
await recordAuditEvent(env, { eventType: "github_app.pr_public_surface_published", targetKey: "JSONbored/loopover#5", outcome: "completed", createdAt: recent });
758+
await recordAuditEvent(env, { eventType: "agent.action.close", targetKey: "JSONbored/loopover#5", outcome: "completed", createdAt: recent });
759+
await recordAuditEvent(env, { eventType: "reversal_reopened", targetKey: "JSONbored/loopover#5", outcome: "completed", createdAt: recent });
760+
// ...and one recent merged PR that stands, so the windowed denominator is 1 merged + 0 closed... plus
761+
// PR#5 which is currently `open` and so counts as inReview, not decided.
762+
await upsertPullRequestFromGitHub(env, "JSONbored/loopover", { number: 6, title: "recent ok", state: "closed", merged_at: recent, user: { login: "c" }, head: { sha: "s6" }, labels: [] });
763+
await recordAuditEvent(env, { eventType: "github_app.pr_public_surface_published", targetKey: "JSONbored/loopover#6", outcome: "completed", createdAt: recent });
764+
765+
const out = await getPublicStats(env, NOW);
766+
// Lifetime volume still reports every PR ever published -- that part is measured and unaffected.
767+
expect(out.totals.reviewed).toBe(6);
768+
expect(out.totals.merged).toBe(5);
769+
expect(out.totals.reversed).toBe(1);
770+
// Accuracy divides by the WINDOWED denominator (PR#6 merged inside the window), not the lifetime 5.
771+
// 1 - 1/1 = 0%, not the 1 - 1/5 = 80% the old lifetime pairing would have published.
772+
expect(out.byProject[0]?.accuracyPct).toBe(0);
773+
expect(out.totals.accuracyPct).toBe(0);
774+
});
775+
740776
it("publishes a real accuracy once the deployment records the auto-actions a reversal attaches to (real D1)", async () => {
741777
const env = createTestEnv({ LOOPOVER_PUBLIC_STATS_REPOS: "JSONbored/loopover" });
742778
await upsertRepositoryFromGitHub(env, { name: "loopover", full_name: "JSONbored/loopover", private: false, owner: { login: "JSONbored" } }, 1);

0 commit comments

Comments
 (0)