From d4bac919c10632345d283c85b133eaad80ebdc8a Mon Sep 17 00:00:00 2001 From: shin-core <153108882+shin-core@users.noreply.github.com> Date: Fri, 31 Jul 2026 20:04:44 +0900 Subject: [PATCH] fix(review): derive the proof-page accuracy from real reversals and surface ledger exclusions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The public proof page published two figures that could never be anything but perfect. 1. accuracy was always 1. confirmed was SUM(action IN (merge,close) AND reason_code NOT LIKE 'reversal%'), but decision_records.reason_code is written once at decision time from two derivations, neither of which can produce a 'reversal…' value, and nothing updates it — reversals are separate audit_events rows. So confirmed === decided for every repo and buildProofAccuracy published accuracy = round3(confirmed/decided) = 1 with a Wilson interval hugging 1, the exact denominator-choice failure the codebase already diagnosed once on public-stats. Count confirmed as merge/close decisions with NO reversal_reverted/reversal_reopened/reversal_superseded audit row for their #, in its OWN section: a failing read degrades to insufficient_data (null confirmed) rather than asserting confirmed === decided. 2. a ledger with declared waivers or pruned preimages still badged plain 'verified'. verifyDecisionLedger reports prunedRecords / waivedContentMismatches / waivedUnchainedRecords, but the proof page's verifyLedger dependency dropped them, so buildProofLedgerStatus could only ever render 'verified · anchored'. Carry the three counts onto the verified status; buildProofBadgeMessage now returns 'verified · N excluded' (N = their sum) with the neutral colour when the sum is > 0, and today's exact strings and colours when it is 0. Unchanged: PROOF_MIN_DECISIONS, PROOF_SAMPLE_RECORDS, round3, buildProofAccuracy's published/insufficient_data shape, the anchor status, the empty/broken/unavailable ledger states, and the allowlist-by-name privacy discipline. Closes #10012 --- src/review/proof-summary.ts | 73 ++++++++++++++++++----- test/unit/proof-summary.test.ts | 102 ++++++++++++++++++++++++++------ 2 files changed, 143 insertions(+), 32 deletions(-) diff --git a/src/review/proof-summary.ts b/src/review/proof-summary.ts index f1a36b13d0..20e5e88fb1 100644 --- a/src/review/proof-summary.ts +++ b/src/review/proof-summary.ts @@ -36,7 +36,7 @@ export const PROOF_MIN_DECISIONS = 20; export const PROOF_SAMPLE_RECORDS = 5; export type ProofLedgerStatus = - | { state: "verified"; tipSeq: number; totalCount: number; checkedAt: string } + | { state: "verified"; tipSeq: number; totalCount: number; checkedAt: string; prunedRecords: number; waivedContentMismatches: number; waivedUnchainedRecords: number } /** `brokenKind` travels alongside the position because the KIND of break is the actionable half: a * pruned-preimage short tail and a row-hash mismatch are very different claims about this operator. */ | { state: "broken"; tipSeq: number; totalCount: number; checkedAt: string; brokenAtSeq: number; brokenKind: string } @@ -83,7 +83,11 @@ function round3(value: number): number { * PURE. `confirmed`/`decided` come from the already-public precision block; nothing new is computed here * and no new SQL surface exists for this page. */ -export function buildProofAccuracy(decided: number, confirmed: number, minimumDecisions: number = PROOF_MIN_DECISIONS): ProofAccuracy { +export function buildProofAccuracy(decided: number, confirmed: number | null, minimumDecisions: number = PROOF_MIN_DECISIONS): ProofAccuracy { + // #10012: a null `confirmed` means the reversal read (its own section in loadProofSummary) FAILED. A rate + // must never be asserted on a failed read -- degrade to insufficient_data rather than treating the missing + // count as zero (which would publish a fabricated accuracy of 0). + if (confirmed === null) return { state: "insufficient_data", decided: Math.max(0, decided), minimumDecisions }; // ONE guard for both reasons a rate is unpublishable, and both arms are reachable: `wilsonInterval` // returns null exactly when there are no trials, which IS the "nothing decided" case, so checking it // here rather than pre-empting it with a separate `decided <= 0` test avoids a branch no input can take. @@ -115,12 +119,15 @@ export function buildProofAnchorStatus(anchors: readonly PublicLedgerAnchor[]): /** Project a ledger-verify result onto the page's status. An empty ledger is `empty`, not `verified`: * "nothing has been decided yet" and "everything checks out" are different claims. */ export function buildProofLedgerStatus( - verify: { ok: boolean; tipSeq: number; totalCount: number; break?: { kind: string; atSeq: number } | undefined } | null, + verify: { ok: boolean; tipSeq: number; totalCount: number; prunedRecords: number; waivedContentMismatches: number; waivedUnchainedRecords: number; break?: { kind: string; atSeq: number } | undefined } | null, checkedAt: string, ): ProofLedgerStatus { if (!verify) return { state: "unavailable", checkedAt }; if (verify.totalCount === 0) return { state: "empty", checkedAt }; - if (verify.ok) return { state: "verified", tipSeq: verify.tipSeq, totalCount: verify.totalCount, checkedAt }; + // #10012: carry the declared exclusions (pruned preimages + content/unchained waivers) so the badge can say + // "verified · N excluded" instead of a bare "verified" that hides them -- an internal verifier that reports + // waivers must not render as unqualified "verified" on the most public surface. + if (verify.ok) return { state: "verified", tipSeq: verify.tipSeq, totalCount: verify.totalCount, checkedAt, prunedRecords: verify.prunedRecords, waivedContentMismatches: verify.waivedContentMismatches, waivedUnchainedRecords: verify.waivedUnchainedRecords }; return { state: "broken", tipSeq: verify.tipSeq, @@ -144,8 +151,8 @@ export function buildProofSummary(input: { repoFullName: string; decisionCount: number; decided: number; - confirmed: number; - verify: { ok: boolean; tipSeq: number; totalCount: number; break?: { kind: string; atSeq: number } | undefined } | null; + confirmed: number | null; + verify: { ok: boolean; tipSeq: number; totalCount: number; prunedRecords: number; waivedContentMismatches: number; waivedUnchainedRecords: number; break?: { kind: string; atSeq: number } | undefined } | null; anchors: readonly PublicLedgerAnchor[]; records: ReadonlyArray<{ pullNumber: number; action: string; reasonCode: string; decidedAt: string; recordDigest: string }>; checkedAt: string; @@ -174,8 +181,13 @@ export function buildProofSummary(input: { * scalar this module exists to avoid. */ export function buildProofBadgeMessage(summary: ProofSummary): string { switch (summary.ledger.state) { - case "verified": + case "verified": { + // #10012: a clean chain that nonetheless carries declared exclusions (pruned preimages or content/ + // unchained waivers) is "verified · N excluded", never a bare "verified · anchored" that hides them. + const excluded = summary.ledger.prunedRecords + summary.ledger.waivedContentMismatches + summary.ledger.waivedUnchainedRecords; + if (excluded > 0) return `verified · ${excluded} excluded`; return summary.anchor.state === "anchored" ? "verified · anchored" : "verified"; + } case "broken": return "chain broken"; case "empty": @@ -187,8 +199,13 @@ export function buildProofBadgeMessage(summary: ProofSummary): string { export function buildProofBadgeColor(summary: ProofSummary): string { switch (summary.ledger.state) { - case "verified": + case "verified": { + // #10012: a "verified · N excluded" badge is neutral grey, not the confident green — the exclusions are + // exactly what the green would over-claim past. + const excluded = summary.ledger.prunedRecords + summary.ledger.waivedContentMismatches + summary.ledger.waivedUnchainedRecords; + if (excluded > 0) return "#9e9e9e"; return summary.anchor.state === "anchored" ? "#3fb950" : "#2da44e"; + } case "broken": return "#f85149"; // Neutral, not alarming: a repo with nothing decided yet has not failed anything. @@ -282,7 +299,7 @@ export async function loadProofSummary( env: Env, repoFullName: string, deps: { - verifyLedger: (env: Env) => Promise<{ ok: boolean; tipSeq: number; totalCount: number; break?: { kind: string; atSeq: number } | undefined }>; + verifyLedger: (env: Env) => Promise<{ ok: boolean; tipSeq: number; totalCount: number; prunedRecords: number; waivedContentMismatches: number; waivedUnchainedRecords: number; break?: { kind: string; atSeq: number } | undefined }>; loadAnchors: (env: Env) => Promise<{ anchors: PublicLedgerAnchor[] }>; now?: () => string; }, @@ -305,12 +322,36 @@ export async function loadProofSummary( () => env.DB.prepare( `SELECT COUNT(*) AS decisionCount, - SUM(CASE WHEN action IN ('merge', 'close') THEN 1 ELSE 0 END) AS decided, - SUM(CASE WHEN action IN ('merge', 'close') AND reason_code NOT LIKE 'reversal%' THEN 1 ELSE 0 END) AS confirmed + SUM(CASE WHEN action IN ('merge', 'close') THEN 1 ELSE 0 END) AS decided FROM decision_records WHERE repo_full_name = ?`, ) .bind(repoFullName) - .first<{ decisionCount: number | null; decided: number | null; confirmed: number | null }>(), + .first<{ decisionCount: number | null; decided: number | null }>(), + null, + ); + + // #10012: `confirmed` counts merge/close decisions this repo made that were NOT later reversed. Reversals + // are recorded as separate audit_events rows (reversal_reverted / reversal_reopened / reversal_superseded, + // per public-rule-precision's own treatment of the same events), NOT as a reason_code on the decision row -- + // decision_records.reason_code is written once at decision time and can never be 'reversal…', so the old + // `reason_code NOT LIKE 'reversal%'` made confirmed === decided for every repo, publishing accuracy = 1 by + // construction. Anti-join against the reversal audit rows keyed by `#`. Its OWN section so a + // failing read degrades toward NOT asserting a rate (buildProofSummary reads a null `confirmed` as + // insufficient_data) rather than fabricating one. + const confirmedRow = await section( + () => + env.DB.prepare( + `SELECT COUNT(*) AS confirmed + FROM decision_records d + WHERE d.repo_full_name = ? AND d.action IN ('merge', 'close') + AND NOT EXISTS ( + SELECT 1 FROM audit_events a + WHERE a.event_type IN ('reversal_reverted', 'reversal_reopened', 'reversal_superseded') + AND a.target_key = d.repo_full_name || '#' || d.pull_number + )`, + ) + .bind(repoFullName) + .first<{ confirmed: number | null }>(), null, ); @@ -336,7 +377,11 @@ export async function loadProofSummary( // degrade to 0, which renders as the honest "no decisions yet" state rather than a fabricated rate. decisionCount: counts?.decisionCount ?? 0, decided: counts?.decided ?? 0, - confirmed: counts?.confirmed ?? 0, + // #10012: null (not 0) when the reversal read FAILED (section returned null), so buildProofAccuracy + // degrades to insufficient_data rather than publishing a fabricated rate. A successful read always has a + // numeric COUNT(*) (never SQL NULL), so the inner `?? 0` is a defensive floor no input reaches. + /* v8 ignore next -- COUNT(*) is never NULL on a successful read; the `?? 0` is a defensive floor */ + confirmed: confirmedRow ? confirmedRow.confirmed ?? 0 : null, verify, anchors, records, @@ -348,7 +393,7 @@ export async function loadProofSummary( * bind failing ones, which is what makes the unavailable path a tested outcome rather than a hoped-for one. */ export type ProofPageDeps = { loadManifest: (env: Env, repoFullName: string) => Promise<{ publicProof: { present: boolean; enabled: boolean } } | null>; - verifyLedger: (env: Env) => Promise<{ ok: boolean; tipSeq: number; totalCount: number; break?: { kind: string; atSeq: number } | undefined }>; + verifyLedger: (env: Env) => Promise<{ ok: boolean; tipSeq: number; totalCount: number; prunedRecords: number; waivedContentMismatches: number; waivedUnchainedRecords: number; break?: { kind: string; atSeq: number } | undefined }>; loadAnchors: (env: Env) => Promise<{ anchors: PublicLedgerAnchor[] }>; now?: (() => string) | undefined; }; diff --git a/test/unit/proof-summary.test.ts b/test/unit/proof-summary.test.ts index c148734750..d8632f031f 100644 --- a/test/unit/proof-summary.test.ts +++ b/test/unit/proof-summary.test.ts @@ -16,6 +16,7 @@ import { import { renderProofBadgeSvg } from "../../src/api/proof-badge"; import { createApp } from "../../src/api/routes"; import { appendDecisionLedger, persistDecisionRecord } from "../../src/review/decision-record"; +import { recordAuditEvent } from "../../src/db/repositories"; import { loadPublicLedgerAnchors, recordLedgerAnchorAttempt } from "../../src/review/ledger-anchor-persistence"; import { createTestEnv } from "../helpers/d1"; import { loadProofPageRepoOverride, resolveProofPage, type ProofPageDeps } from "../../src/review/proof-summary"; @@ -89,18 +90,18 @@ describe("buildProofAnchorStatus (#9569)", () => { describe("buildProofLedgerStatus — honest boundary states (#9569)", () => { it("REGRESSION: an EMPTY ledger is `empty`, not `verified` — different claims", () => { - expect(buildProofLedgerStatus({ ok: true, tipSeq: 0, totalCount: 0 }, CHECKED_AT)).toEqual({ state: "empty", checkedAt: CHECKED_AT }); + expect(buildProofLedgerStatus({ ok: true, tipSeq: 0, totalCount: 0, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, CHECKED_AT)).toEqual({ state: "empty", checkedAt: CHECKED_AT }); }); it("verified, broken (with kind and position), and unavailable each render distinctly", () => { - expect(buildProofLedgerStatus({ ok: true, tipSeq: 9, totalCount: 9 }, CHECKED_AT)).toEqual({ - state: "verified", tipSeq: 9, totalCount: 9, checkedAt: CHECKED_AT, + expect(buildProofLedgerStatus({ ok: true, tipSeq: 9, totalCount: 9, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, CHECKED_AT)).toEqual({ + state: "verified", tipSeq: 9, totalCount: 9, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0, }); - expect(buildProofLedgerStatus({ ok: false, tipSeq: 9, totalCount: 9, break: { kind: "row_hash_mismatch", atSeq: 4 } }, CHECKED_AT)).toEqual({ + expect(buildProofLedgerStatus({ ok: false, tipSeq: 9, totalCount: 9, break: { kind: "row_hash_mismatch", atSeq: 4 }, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, CHECKED_AT)).toEqual({ state: "broken", tipSeq: 9, totalCount: 9, checkedAt: CHECKED_AT, brokenAtSeq: 4, brokenKind: "row_hash_mismatch", }); // A break with no detail is still broken, marked unknown rather than silently claiming seq 0. - expect(buildProofLedgerStatus({ ok: false, tipSeq: 9, totalCount: 9 }, CHECKED_AT)).toMatchObject({ brokenAtSeq: -1, brokenKind: "unknown" }); + expect(buildProofLedgerStatus({ ok: false, tipSeq: 9, totalCount: 9, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, CHECKED_AT)).toMatchObject({ brokenAtSeq: -1, brokenKind: "unknown" }); // A failed read is `unavailable` — not "broken", which would accuse the operator of tampering. expect(buildProofLedgerStatus(null, CHECKED_AT)).toEqual({ state: "unavailable", checkedAt: CHECKED_AT }); }); @@ -113,7 +114,7 @@ describe("buildProofSummary — the structural privacy boundary (#9569)", () => decisionCount: 30, decided: 30, confirmed: 29, - verify: { ok: true, tipSeq: 30, totalCount: 30 }, + verify: { ok: true, tipSeq: 30, totalCount: 30, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, anchors: [anchor()], records: [ { @@ -139,7 +140,7 @@ describe("buildProofSummary — the structural privacy boundary (#9569)", () => })); const summary = buildProofSummary({ repoFullName: "o/r", decisionCount: 25, decided: 25, confirmed: 25, - verify: { ok: true, tipSeq: 25, totalCount: 25 }, anchors: [], records: many, checkedAt: CHECKED_AT, + verify: { ok: true, tipSeq: 25, totalCount: 25, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, anchors: [], records: many, checkedAt: CHECKED_AT, }); expect(summary.sampleRecords).toHaveLength(PROOF_SAMPLE_RECORDS); // The caveat travels IN the payload, so a screenshot or embed cannot shed it the way a footer can. @@ -153,8 +154,8 @@ describe("proof badge (#9569)", () => { ({ ledger, anchor: anchored ? { state: "anchored" } : { state: "not_yet_anchored" } }) as Parameters[0]; it("reports the LEDGER state, never a bare accuracy percentage", () => { - expect(buildProofBadgeMessage(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT }, true))).toBe("verified · anchored"); - expect(buildProofBadgeMessage(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT }, false))).toBe("verified"); + expect(buildProofBadgeMessage(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, true))).toBe("verified · anchored"); + expect(buildProofBadgeMessage(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, false))).toBe("verified"); expect(buildProofBadgeMessage(summaryWith({ state: "broken", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, brokenAtSeq: 1, brokenKind: "k" }, false))).toBe("chain broken"); expect(buildProofBadgeMessage(summaryWith({ state: "empty", checkedAt: CHECKED_AT }, false))).toBe("no decisions yet"); expect(buildProofBadgeMessage(summaryWith({ state: "unavailable", checkedAt: CHECKED_AT }, false))).toBe("unavailable"); @@ -164,12 +165,12 @@ describe("proof badge (#9569)", () => { expect(buildProofBadgeColor(summaryWith({ state: "empty", checkedAt: CHECKED_AT }, false))).toBe("#9e9e9e"); expect(buildProofBadgeColor(summaryWith({ state: "unavailable", checkedAt: CHECKED_AT }, false))).toBe("#9e9e9e"); expect(buildProofBadgeColor(summaryWith({ state: "broken", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, brokenAtSeq: 1, brokenKind: "k" }, false))).toBe("#f85149"); - expect(buildProofBadgeColor(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT }, true))).toBe("#3fb950"); - expect(buildProofBadgeColor(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT }, false))).toBe("#2da44e"); + expect(buildProofBadgeColor(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, true))).toBe("#3fb950"); + expect(buildProofBadgeColor(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, false))).toBe("#2da44e"); }); it("renders valid SVG for both the summary and the null (unavailable) case, escaping its text", () => { - const svg = renderProofBadgeSvg(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT }, true)); + const svg = renderProofBadgeSvg(summaryWith({ state: "verified", tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, true)); expect(svg.startsWith(" { it("composes from the real tables and degrades per section rather than failing the page", async () => { const env = await seeded(); const summary = await loadProofSummary(env, "o/r", { - verifyLedger: async () => ({ ok: true, tipSeq: 3, totalCount: 3 }), + verifyLedger: async () => ({ ok: true, tipSeq: 3, totalCount: 3, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }), // A failing anchor read must degrade to not_yet_anchored, not blow up the page. loadAnchors: async () => { throw new Error("d1 down"); }, now: () => CHECKED_AT, @@ -252,7 +253,7 @@ describe("loadProofSummary + routes (#9569)", () => { throw new Error("d1 down"); }; const summary = await loadProofSummary(env, "o/r", { - verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0 }), + verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }), loadAnchors: async () => ({ anchors: [] }), now: () => CHECKED_AT, }); @@ -271,7 +272,7 @@ describe("loadProofSummary + routes (#9569)", () => { }), }); const summary = await loadProofSummary(env, "o/r", { - verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0 }), + verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }), loadAnchors: async () => ({ anchors: [] }), now: () => CHECKED_AT, }); @@ -282,7 +283,7 @@ describe("loadProofSummary + routes (#9569)", () => { it("an unknown repo yields the honest empty page rather than a 404 or a fabricated rate", async () => { const env = await seeded(); const summary = await loadProofSummary(env, "nobody/nothing", { - verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0 }), + verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }), loadAnchors: async () => ({ anchors: [] }), now: () => CHECKED_AT, }); @@ -322,7 +323,7 @@ describe("loadProofSummary + routes (#9569)", () => { signature: "sig", keyId: "k1", backend: "rekor", status: "ok", backendRef: { uuid: "u" }, proofR2Key: null, }); const summary = await loadProofSummary(env, "o/r", { - verifyLedger: async () => ({ ok: true, tipSeq: 3, totalCount: 3 }), + verifyLedger: async () => ({ ok: true, tipSeq: 3, totalCount: 3, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }), loadAnchors: (target) => loadPublicLedgerAnchors(target, {}), now: () => CHECKED_AT, }); @@ -375,7 +376,7 @@ describe("loadProofSummary + routes (#9569)", () => { const env = await seeded(); const deps: ProofPageDeps = { loadManifest: async () => null, - verifyLedger: async () => ({ ok: true, tipSeq: 3, totalCount: 3 }), + verifyLedger: async () => ({ ok: true, tipSeq: 3, totalCount: 3, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }), loadAnchors: async () => ({ anchors: [] }), now: () => CHECKED_AT, }; @@ -441,4 +442,69 @@ describe("loadProofSummary + routes (#9569)", () => { expect((await app.request("/v1/public/repos/o/r/proof", {}, broken)).status).toBe(200); expect((await app.request("/v1/public/repos/o/r/proof-badge.svg", {}, broken)).status).toBe(200); }); +}); + +describe("#10012: accuracy is derived from real reversals, and the badge surfaces declared exclusions", () => { + const okVerify = (over: Partial<{ prunedRecords: number; waivedContentMismatches: number; waivedUnchainedRecords: number }> = {}) => + async () => ({ ok: true as const, tipSeq: 25, totalCount: 25, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0, ...over }); + const noAnchors = async () => ({ anchors: [] }); + + async function seedDecisions(env: ReturnType, count: number): Promise { + for (let index = 1; index <= count; index += 1) { + await persistDecisionRecord( + env, + { + schemaVersion: "5", repoFullName: "o/r", pullNumber: index, headSha: `sha${index}`, baseSha: null, + action: "merge", reasonCode: "clean", configDigest: "c", settingsDigest: "s", gatePack: "oss-anti-slop", + ciState: "success", modelIds: null, promptDigest: null, aiConfidence: null, aiAgreement: null, + salvageability: null, divertedByHoldout: false, decidedAt: CHECKED_AT, + } as never, + String(index).padStart(64, "0"), + ); + } + } + + const reverse = (env: ReturnType, pullNumber: number) => + recordAuditEvent(env, { eventType: "reversal_reverted", actor: null, targetKey: `o/r#${pullNumber}`, outcome: "completed", detail: "reverted", metadata: { repoFullName: "o/r", pullNumber } }); + + it("REGRESSION: the published accuracy is derived from real reversals, not from reason_code — 25 decisions, 2 reversed → 0.92", async () => { + const env = createTestEnv({ LOOPOVER_PUBLIC_PROOF: "true" }); + await seedDecisions(env, 25); + await reverse(env, 3); + await reverse(env, 17); + const summary = await loadProofSummary(env, "o/r", { verifyLedger: okVerify(), loadAnchors: noAnchors, now: () => CHECKED_AT }); + expect(summary.accuracy).toMatchObject({ state: "published", decided: 25, confirmed: 23, accuracy: 0.92 }); + }); + + it("25 decisions with ZERO reversals still publishes accuracy 1 — the perfect case stays expressible", async () => { + const env = createTestEnv({ LOOPOVER_PUBLIC_PROOF: "true" }); + await seedDecisions(env, 25); + const summary = await loadProofSummary(env, "o/r", { verifyLedger: okVerify(), loadAnchors: noAnchors, now: () => CHECKED_AT }); + expect(summary.accuracy).toMatchObject({ state: "published", decided: 25, confirmed: 25, accuracy: 1 }); + }); + + it("a failing reversal read degrades to insufficient_data, never publishing a fabricated rate", async () => { + const env = createTestEnv({ LOOPOVER_PUBLIC_PROOF: "true" }); + await seedDecisions(env, 25); + // Make only the reversal anti-join read throw; the decided/count read and the rest still compose. + const realPrepare = env.DB.prepare.bind(env.DB); + env.DB.prepare = ((sql: string) => { + if (sql.includes("reversal_reverted")) throw new Error("d1 down"); + return realPrepare(sql); + }) as never; + const summary = await loadProofSummary(env, "o/r", { verifyLedger: okVerify(), loadAnchors: noAnchors, now: () => CHECKED_AT }); + expect(summary.accuracy.state).toBe("insufficient_data"); + }); + + it("the badge renders 'verified · N excluded' + neutral colour when the ledger carries declared exclusions", () => { + const summary = { ledger: { state: "verified" as const, tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 231 }, anchor: { state: "anchored" as const } } as Parameters[0]; + expect(buildProofBadgeMessage(summary)).toBe("verified · 231 excluded"); + expect(buildProofBadgeColor(summary)).toBe("#9e9e9e"); + }); + + it("a clean ledger with zero exclusions keeps today's exact badge strings and colours", () => { + const anchored = { ledger: { state: "verified" as const, tipSeq: 1, totalCount: 1, checkedAt: CHECKED_AT, prunedRecords: 0, waivedContentMismatches: 0, waivedUnchainedRecords: 0 }, anchor: { state: "anchored" as const } } as Parameters[0]; + expect(buildProofBadgeMessage(anchored)).toBe("verified · anchored"); + expect(buildProofBadgeColor(anchored)).toBe("#3fb950"); + }); }); \ No newline at end of file