From ca666b8fdc80e9deada7f7f7b24535f090b94a11 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Tue, 28 Jul 2026 16:58:56 -0700
Subject: [PATCH 1/6] feat(proof): public per-repo proof summary, endpoint and
README badge (#9569)
The shareable, unauthenticated twin of the in-app trust panel. One
composition serves both, so the public page and #9193's panel cannot
disagree about a figure -- which is the property the page exists to
demonstrate.
THE PRIVACY BOUNDARY IS STRUCTURAL. Every field is built by NAMING it,
never by filtering a wider object. A blocklist has to anticipate every
field a future upstream type might grow and silently leaks the one it did
not; an allowlisted shape cannot leak a field nobody wrote down. Tested by
feeding hostile records carrying hotkey/wallet/reward/trust-score/private-
rank and asserting none of it reaches the serialized page -- while the
named fields do, so the test proves allowlisting rather than an empty
object.
NEVER A BARE SCALAR. Any accuracy figure carries its coverage and a Wilson
interval; below a 20-decision floor there is no rate at all, only an
explicit insufficient_data state that still publishes the count. A perfect
record over 19 decisions must not render as 100%. Wilson rather than Wald
because a gate metric lives near p->1, exactly where Wald claims
impossible certainty.
HONEST BOUNDARY STATES. An empty ledger is `empty`, not `verified` --
different claims. A failed read is `unavailable`, not `broken`, which
would accuse the operator of tampering. A FAILED anchor attempt is not an
anchor: the public attempt log is where failures are legible, and
presenting one here would claim corroboration that does not exist. The
verification-contract boundary statement travels IN the payload, so a
screenshot or embed cannot shed it the way a footer caption can.
The badge reports the LEDGER's state rather than an accuracy percentage: a
badge is a one-glance claim, and an accuracy number without the interval
that makes it honest does not fit in one. Disabled and errored both render
a neutral SVG -- a broken image in a README is worse than an honest
"unavailable".
DECISION (requirement 6), recorded beside the code that implements it: the
page is opt-OUT per repo, default ON once the operator's fleet-wide flag
(default OFF) is on. Every figure is already publicly fetchable through
the ledger-verify / anchors / decision-record endpoints, so gating a page
over it would add friction without privacy. The per-repo switch still
exists because a page is a different artifact from an API -- discoverable,
linkable, and it markets a repo's numbers whether or not the maintainer
wants that. A repo can opt out but cannot opt IN when the operator has
not, which keeps the fleet switch a real switch.
Found and fixed while testing: `DB.prepare()` throws SYNCHRONOUSLY on a
driver-level failure, so the `.catch()` chain never ran and a D1 outage
would have 503'd the whole public page instead of degrading. Each section
is now a real try/catch, which is the difference between the
fail-safe-per-section contract being documented and being true.
Backend half of #9569; the /proof/:owner/:repo UI route renders this
payload and lands separately.
---
apps/loopover-ui/public/openapi.json | 76 +++++++
src/api/proof-badge.ts | 39 ++++
src/api/routes.ts | 43 ++++
src/auth/route-auth.ts | 5 +
src/env.d.ts | 4 +
src/openapi/spec.ts | 26 +++
src/review/proof-summary.ts | 313 +++++++++++++++++++++++++
test/unit/proof-summary.test.ts | 329 +++++++++++++++++++++++++++
8 files changed, 835 insertions(+)
create mode 100644 src/api/proof-badge.ts
create mode 100644 src/review/proof-summary.ts
create mode 100644 test/unit/proof-summary.test.ts
diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json
index a84890ed1..adac8935a 100644
--- a/apps/loopover-ui/public/openapi.json
+++ b/apps/loopover-ui/public/openapi.json
@@ -26252,6 +26252,82 @@
}
}
}
+ },
+ "/v1/public/repos/{owner}/{repo}/proof": {
+ "get": {
+ "operationId": "getPublicRepoProof",
+ "tags": [
+ "Public"
+ ],
+ "summary": "Public proof summary for one repo — ledger status, anchor, calibration with coverage and interval, sample records",
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "ProofSummary. Any accuracy figure carries its coverage AND a Wilson confidence interval; below the sample floor it is an explicit `insufficient_data` state, never a bare percentage. Carries the verification-boundary statement in the payload"
+ },
+ "404": {
+ "description": "The proof page is disabled fleet-wide, or this repo has opted out"
+ },
+ "503": {
+ "description": "Composition failed — no partial or fabricated summary is served"
+ }
+ }
+ }
+ },
+ "/v1/public/repos/{owner}/{repo}/proof-badge.svg": {
+ "get": {
+ "operationId": "getPublicRepoProofBadge",
+ "tags": [
+ "Public"
+ ],
+ "summary": "README badge for the proof page — reports the ledger's state, never a bare accuracy percentage",
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "SVG badge"
+ },
+ "404": {
+ "description": "Disabled or opted out — still an SVG (a neutral 'unavailable' badge), so a README never shows a broken image"
+ },
+ "503": {
+ "description": "Same neutral SVG on an internal error"
+ }
+ }
+ }
}
},
"servers": [
diff --git a/src/api/proof-badge.ts b/src/api/proof-badge.ts
new file mode 100644
index 000000000..549712bfa
--- /dev/null
+++ b/src/api/proof-badge.ts
@@ -0,0 +1,39 @@
+// README badge for the public proof page (#9569). Renders through the SAME flat-badge primitive and the
+// SAME XML escaping as the repo-quality badge (`./badge.ts`), so an unauthenticated, embeddable surface
+// cannot become an injection vector and the two badges cannot drift apart visually.
+import { escapeXml } from "./badge";
+import { buildProofBadgeColor, buildProofBadgeMessage, type ProofSummary } from "../review/proof-summary";
+
+export const PROOF_BADGE_LABEL = "loopover proof";
+const UNAVAILABLE_COLOR = "#9e9e9e";
+
+/** `null` renders the neutral unavailable badge — used for both the flag-off 404 and the error 503, since
+ * from a README's point of view those are the same thing: no claim is being made right now. */
+export function renderProofBadgeSvg(summary: ProofSummary | null): string {
+ const message = summary ? buildProofBadgeMessage(summary) : "unavailable";
+ const color = summary ? buildProofBadgeColor(summary) : UNAVAILABLE_COLOR;
+ return renderFlatBadge(PROOF_BADGE_LABEL, message, color);
+}
+
+function renderFlatBadge(label: string, message: string, color: string): string {
+ const labelText = escapeXml(label);
+ const messageText = escapeXml(message);
+ const labelWidth = textWidth(label);
+ const messageWidth = textWidth(message);
+ const totalWidth = labelWidth + messageWidth;
+ return [
+ ``,
+ ].join("");
+}
+
+function textWidth(text: string): number {
+ return Math.max(40, Math.round(text.length * 6.5) + 10);
+}
diff --git a/src/api/routes.ts b/src/api/routes.ts
index d555c395a..47fea56ed 100644
--- a/src/api/routes.ts
+++ b/src/api/routes.ts
@@ -308,6 +308,8 @@ import { isRagEnabled } from "../review/rag-wire";
import { loadDecisionLedgerTip, loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
import { buildEvalScoreRecordsFromRulePrecision, filterEvalScoreRecords } from "../review/eval-score-records";
import { anchorSigningInput, buildLedgerAnchorPayload, currentAnchorKey, parseAnchorPublicKeys, signLedgerAnchorPayload } from "../review/ledger-anchor";
+import { isProofPageEnabledForRepo, loadProofSummary } from "../review/proof-summary";
+import { renderProofBadgeSvg } from "./proof-badge";
import { ingestBittensorAnchorReport, parseBittensorAnchorReport } from "../review/ledger-anchor-bittensor";
import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence";
import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats";
@@ -1352,6 +1354,47 @@ export function createApp() {
return c.json(result);
});
+ // #9569: the public proof page's data, and its README badge. Read-only over the SAME public sources the
+ // standalone endpoints already serve -- no new verification mechanism and no new SQL surface. Two gates
+ // must allow (see isProofPageEnabledForRepo's recorded opt-out decision): the operator's fleet-wide flag,
+ // default OFF like every sibling public surface, and the repo's own opt-out.
+ app.get("/v1/public/repos/:owner/:repo/proof", async (c) => {
+ if (!isProofPageEnabledForRepo(c.env)) return c.json({ error: "not_found" }, 404);
+ const repoFullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
+ try {
+ const summary = await loadProofSummary(c.env, repoFullName, {
+ verifyLedger: (env) => verifyDecisionLedger(env),
+ loadAnchors: (env) => loadPublicLedgerAnchors(env, { limit: 20 }),
+ });
+ c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=300");
+ return c.json(summary);
+ } catch {
+ return c.json({ error: "unavailable" }, 503);
+ }
+ });
+
+ // The badge deliberately reports the LEDGER's state rather than an accuracy percentage: a badge is a
+ // one-glance claim, and an accuracy number without the interval that makes it honest (which does not fit
+ // in a badge) is exactly the bare scalar the proof summary refuses to publish.
+ app.get("/v1/public/repos/:owner/:repo/proof-badge.svg", async (c) => {
+ c.header("Content-Type", "image/svg+xml; charset=utf-8");
+ if (!isProofPageEnabledForRepo(c.env)) {
+ c.header("Cache-Control", "public, max-age=300");
+ return c.body(renderProofBadgeSvg(null), 404);
+ }
+ try {
+ const summary = await loadProofSummary(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, {
+ verifyLedger: (env) => verifyDecisionLedger(env),
+ loadAnchors: (env) => loadPublicLedgerAnchors(env, { limit: 20 }),
+ });
+ c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400");
+ return c.body(renderProofBadgeSvg(summary));
+ } catch {
+ c.header("Cache-Control", "public, max-age=300");
+ return c.body(renderProofBadgeSvg(null), 503);
+ }
+ });
+
// #9277 (epic #9267): the current tip's SIGNED checkpoint, for the operator's off-Worker Bittensor
// commitment submitter to fetch and commit on-chain (sha256 of `signingInput` is the exact 32 bytes
// `Data::Sha256` holds). Unauthenticated like every /v1/public/* sibling: it is the same payload the
diff --git a/src/auth/route-auth.ts b/src/auth/route-auth.ts
index 429036fea..d19456c76 100644
--- a/src/auth/route-auth.ts
+++ b/src/auth/route-auth.ts
@@ -48,6 +48,11 @@ export function requiresApiToken(path: string): boolean {
if (path === "/v1/public/decision-ledger/anchor-key") return false;
// #9271: the public anchor-attempt listing, added in the SAME PR as its route.
if (path === "/v1/public/decision-ledger/anchors") return false;
+ // #9569: the public proof page's data and its README badge. Unauthenticated by design -- every figure
+ // they render is already served unauthenticated by the ledger-verify / anchors / decision-record routes
+ // above; this is a composition, not a new disclosure. Added in the SAME PR as the routes, per #9120.
+ if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/proof$/.test(path)) return false;
+ if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/proof-badge\.svg$/.test(path)) return false;
// #9277: the current tip's signed checkpoint, for the operator's off-Worker Bittensor submitter (and
// anyone else — it is the same payload the Rekor/git backends already publish externally). Added in the
// SAME PR as its route, per the #9120 lesson.
diff --git a/src/env.d.ts b/src/env.d.ts
index 0a59667e9..81a11e0c3 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -601,6 +601,10 @@ declare global {
* worker is byte-identical to today. Exposes review-disposition counts + a reversal-grounded accuracy
* percentage + an estimated-time-saved figure ONLY — never PR content, authors, scores, or reward internals.
* See review/public-stats.ts. */
+ /** #9569: fleet-wide switch for the public proof page (`/proof/:owner/:repo`) and its badge. Default
+ * OFF like every sibling public surface. A repo can opt OUT via its own manifest, but cannot opt IN
+ * when this is off -- see isProofPageEnabledForRepo's recorded decision in review/proof-summary.ts. */
+ LOOPOVER_PUBLIC_PROOF?: string;
LOOPOVER_PUBLIC_STATS?: string;
/** Proof of Power (#1059): comma-separated allowlist of repo full-names ("owner/repo") whose OWN historical
* review ledger (audit_events "published a review surface" + pull_requests terminal state) counts toward
diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts
index 83f03ed76..358aecc76 100644
--- a/src/openapi/spec.ts
+++ b/src/openapi/spec.ts
@@ -1906,6 +1906,32 @@ export function buildOpenApiSpec() {
200: { description: "{ anchors: [{ id, seq, rowHash, keyId, backend, backendRef, status, error, createdAt }], nextBefore } — a failed attempt is returned identically to a successful one, never filtered out or reshaped" },
},
});
+ registry.registerPath({
+ method: "get",
+ path: "/v1/public/repos/{owner}/{repo}/proof",
+ operationId: "getPublicRepoProof",
+ tags: ["Public"],
+ summary: "Public proof summary for one repo — ledger status, anchor, calibration with coverage and interval, sample records",
+ request: { params: z.object({ owner: z.string(), repo: z.string() }) },
+ responses: {
+ 200: { description: "ProofSummary. Any accuracy figure carries its coverage AND a Wilson confidence interval; below the sample floor it is an explicit `insufficient_data` state, never a bare percentage. Carries the verification-boundary statement in the payload" },
+ 404: { description: "The proof page is disabled fleet-wide, or this repo has opted out" },
+ 503: { description: "Composition failed — no partial or fabricated summary is served" },
+ },
+ });
+ registry.registerPath({
+ method: "get",
+ path: "/v1/public/repos/{owner}/{repo}/proof-badge.svg",
+ operationId: "getPublicRepoProofBadge",
+ tags: ["Public"],
+ summary: "README badge for the proof page — reports the ledger's state, never a bare accuracy percentage",
+ request: { params: z.object({ owner: z.string(), repo: z.string() }) },
+ responses: {
+ 200: { description: "SVG badge" },
+ 404: { description: "Disabled or opted out — still an SVG (a neutral 'unavailable' badge), so a README never shows a broken image" },
+ 503: { description: "Same neutral SVG on an internal error" },
+ },
+ });
registry.registerPath({
method: "get",
path: "/v1/public/decision-ledger/anchor-payload",
diff --git a/src/review/proof-summary.ts b/src/review/proof-summary.ts
new file mode 100644
index 000000000..204869222
--- /dev/null
+++ b/src/review/proof-summary.ts
@@ -0,0 +1,313 @@
+// Public proof-page summary (#9569) — the shareable, unauthenticated twin of the in-app trust panel.
+//
+// ONE IMPLEMENTATION, TWO RENDERINGS. This module is the single composition both surfaces read: the public
+// proof page here and the in-app per-repo trust panel (#9193) render the SAME object, so the two can never
+// disagree about a figure. A number that appears in one and not the other, or differs between them, would
+// undermine the exact property this page exists to demonstrate.
+//
+// ── THE PRIVACY BOUNDARY IS STRUCTURAL ───────────────────────────────────────────────────────────────
+// Every field below is built by NAMING it, never by filtering a wider object. That distinction is the whole
+// control: a blocklist has to anticipate every field a future upstream type might grow, and silently leaks
+// the one it did not anticipate. An allowlisted shape cannot leak a field nobody wrote down, so wallet,
+// hotkey, reward, trust-score and private-ranking data are unreachable here by construction rather than by
+// vigilance. `buildProofSummary` takes only already-public inputs and copies named scalars out of them.
+//
+// ── NEVER A BARE SCALAR ──────────────────────────────────────────────────────────────────────────────
+// Any accuracy figure travels with its COVERAGE (how many decisions it is computed over) and a Wilson
+// confidence INTERVAL. A "97% accurate" with no denominator is marketing; the same number over 31 decisions
+// with a [0.84, 0.99] interval is a claim someone can argue with. Wilson rather than Wald because a gate
+// metric lives near p→1, exactly where Wald claims impossible certainty (see `wilsonInterval`'s own note).
+// Below the sample floor the accuracy is `null` — no data must render as no claim, never as a fabricated
+// zero or a bare percentage.
+//
+// ── HONEST BOUNDARY STATES ───────────────────────────────────────────────────────────────────────────
+// Not-yet-anchored and no-published-records are NEUTRAL states carrying the verification contract's own
+// language, not errors and not blanks. A repo that has not been anchored yet is not a failing repo; a page
+// that renders that as an error would be lying in the more damaging direction.
+import { wilsonInterval } from "../orb/analytics";
+import type { PublicLedgerAnchor } from "./ledger-anchor-persistence";
+
+/** The floor below which an accuracy figure is not published at all. Mirrors the public precision block's
+ * own discipline: a percentage over a handful of decisions is noise wearing a number's clothes. */
+export const PROOF_MIN_DECISIONS = 20;
+
+/** How many published decision records the page samples. Enough to be checkable by hand, small enough that
+ * the page stays a summary rather than a dump. */
+export const PROOF_SAMPLE_RECORDS = 5;
+
+export type ProofLedgerStatus =
+ | { state: "verified"; tipSeq: number; totalCount: number; checkedAt: string }
+ /** `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 }
+ | { state: "empty"; checkedAt: string }
+ | { state: "unavailable"; checkedAt: string };
+
+export type ProofAnchorStatus =
+ | { state: "anchored"; backend: string; seq: number; rowHash: string; at: string }
+ | { state: "not_yet_anchored" };
+
+export type ProofAccuracy =
+ | { state: "published"; accuracy: number; decided: number; confirmed: number; interval: { lo: number; hi: number } }
+ /** Below the floor. `decided` is still published — "we have 7 decisions, too few to claim a rate" is a
+ * more honest statement than hiding the count along with the figure. */
+ | { state: "insufficient_data"; decided: number; minimumDecisions: number };
+
+export type ProofSampleRecord = { pullNumber: number; action: string; reasonCode: string; decidedAt: string; recordDigest: string };
+
+export type ProofSummary = {
+ schemaVersion: 1;
+ repoFullName: string;
+ decisionCount: number;
+ accuracy: ProofAccuracy;
+ ledger: ProofLedgerStatus;
+ anchor: ProofAnchorStatus;
+ sampleRecords: ProofSampleRecord[];
+ /** The plain-language boundary statement — what this page does NOT prove. Carried IN the payload so a
+ * screenshot or an embed cannot shed it the way a footer caption can. */
+ boundary: string;
+};
+
+/** The verification contract's own language for the limit of what anchoring proves (#9420's framing). */
+export const PROOF_BOUNDARY_STATEMENT =
+ "Anchoring bounds how far back an undetected rewrite could reach; it does not make every row checkable in real time. A rewrite made since the last checkpoint, followed by ordinary appends, is absorbed into future anchors.";
+
+/** Round to 3dp — the precision the rest of the public surface publishes at. */
+function round3(value: number): number {
+ return Math.round(value * 1000) / 1000;
+}
+
+/**
+ * Fold decision counts into a publishable accuracy claim, or into an explicit insufficient-data state.
+ *
+ * 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 {
+ if (decided < minimumDecisions || decided <= 0) return { state: "insufficient_data", decided: Math.max(0, decided), minimumDecisions };
+ const interval = wilsonInterval(confirmed, decided);
+ /* v8 ignore next -- wilsonInterval returns null only for trials <= 0, which the guard above already
+ excluded; the branch exists so a future change to that helper cannot produce a NaN interval here. */
+ if (!interval) return { state: "insufficient_data", decided, minimumDecisions };
+ return {
+ state: "published",
+ accuracy: round3(confirmed / decided),
+ decided,
+ confirmed,
+ interval: { lo: round3(interval.lo), hi: round3(interval.hi) },
+ };
+}
+
+/** Project the newest SUCCESSFUL anchor onto the page's anchor state. A failed attempt is not an anchor:
+ * the public attempt log (#9271) is where failures are legible, and presenting one here as "anchored"
+ * would claim corroboration that does not exist. */
+export function buildProofAnchorStatus(anchors: readonly PublicLedgerAnchor[]): ProofAnchorStatus {
+ const succeeded = anchors.filter((anchor) => anchor.status === "ok");
+ // The list arrives newest-first; take the newest successful one without assuming the caller sorted.
+ let newest: PublicLedgerAnchor | undefined;
+ for (const anchor of succeeded) {
+ if (!newest || Date.parse(anchor.createdAt) > Date.parse(newest.createdAt)) newest = anchor;
+ }
+ if (!newest) return { state: "not_yet_anchored" };
+ return { state: "anchored", backend: newest.backend, seq: newest.seq, rowHash: newest.rowHash, at: newest.createdAt };
+}
+
+/** 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,
+ 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 };
+ return {
+ state: "broken",
+ tipSeq: verify.tipSeq,
+ totalCount: verify.totalCount,
+ checkedAt,
+ // A break with no position still renders as broken; -1 marks "broken, position unknown" rather than
+ // silently claiming seq 0, which is a real position. Same for an unnamed kind.
+ brokenAtSeq: verify.break?.atSeq ?? -1,
+ brokenKind: verify.break?.kind ?? "unknown",
+ };
+}
+
+/**
+ * Compose the public proof summary from already-public inputs.
+ *
+ * Every field is copied out BY NAME (see the header's privacy note), so this function cannot leak a field
+ * a future upstream type grows. `sampleRecords` is bounded and each entry is likewise rebuilt field by
+ * field rather than spread from a wider record.
+ */
+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;
+ anchors: readonly PublicLedgerAnchor[];
+ records: ReadonlyArray<{ pullNumber: number; action: string; reasonCode: string; decidedAt: string; recordDigest: string }>;
+ checkedAt: string;
+}): ProofSummary {
+ return {
+ schemaVersion: 1,
+ repoFullName: input.repoFullName,
+ decisionCount: input.decisionCount,
+ accuracy: buildProofAccuracy(input.decided, input.confirmed),
+ ledger: buildProofLedgerStatus(input.verify, input.checkedAt),
+ anchor: buildProofAnchorStatus(input.anchors),
+ sampleRecords: input.records.slice(0, PROOF_SAMPLE_RECORDS).map((record) => ({
+ pullNumber: record.pullNumber,
+ action: record.action,
+ reasonCode: record.reasonCode,
+ decidedAt: record.decidedAt,
+ recordDigest: record.recordDigest,
+ })),
+ boundary: PROOF_BOUNDARY_STATEMENT,
+ };
+}
+
+/** The proof badge's message — deliberately the LEDGER's state rather than an accuracy percentage. A badge
+ * is a one-glance claim, and "verified" is a claim this system can actually stand behind at a glance;
+ * an accuracy number without its interval (which does not fit in a badge) would be exactly the bare
+ * scalar this module exists to avoid. */
+export function buildProofBadgeMessage(summary: ProofSummary): string {
+ switch (summary.ledger.state) {
+ case "verified":
+ return summary.anchor.state === "anchored" ? "verified · anchored" : "verified";
+ case "broken":
+ return "chain broken";
+ case "empty":
+ return "no decisions yet";
+ default:
+ return "unavailable";
+ }
+}
+
+export function buildProofBadgeColor(summary: ProofSummary): string {
+ switch (summary.ledger.state) {
+ case "verified":
+ return summary.anchor.state === "anchored" ? "#3fb950" : "#2da44e";
+ case "broken":
+ return "#f85149";
+ // Neutral, not alarming: a repo with nothing decided yet has not failed anything.
+ default:
+ return "#9e9e9e";
+ }
+}
+
+// ── DECISION: THE PROOF PAGE IS OPT-**OUT**, PER REPO, DEFAULT ON WHEN THE OPERATOR ENABLES IT ────────
+// (#9569 requirement 6 asks for this to be decided and recorded; here it is, next to the code that
+// implements it, rather than only in an issue comment that the code could drift away from.)
+//
+// Opt-OUT rather than opt-in, because every figure this page renders is ALREADY publicly fetchable today:
+// `/v1/public/decision-ledger/verify`, `/v1/public/decision-ledger/anchors` and
+// `/v1/public/decision-records/...` are unauthenticated by design and were argued as public-safe when they
+// shipped. Gating a PAGE over data anyone can already curl would add friction without adding privacy --
+// security theater, and the kind that makes a verification story look less confident than it is.
+//
+// A per-repo opt-out still exists, because a page is a genuinely different artifact from an API: it is
+// discoverable, linkable, indexable, and it markets a repo's numbers whether or not the maintainer wants
+// them marketed. "Anyone could assemble this" and "we assembled it for you and gave it a URL" are not the
+// same act, so the maintainer keeps a switch.
+//
+// Two gates, both of which must allow: the operator's fleet-wide flag (default OFF, like every sibling
+// public surface), and the repo's own manifest setting (default ON once the operator has opted in).
+
+export type ProofPageRepoOverride = { present: boolean; enabled: boolean };
+
+/** Fleet-wide operator flag -- truthy-string, default OFF, matching isPublicStatsEnabled's convention. */
+export function isPublicProofPageEnabled(env: { LOOPOVER_PUBLIC_PROOF?: string | undefined }): boolean {
+ return /^(1|true|yes|on)$/i.test(env.LOOPOVER_PUBLIC_PROOF ?? "");
+}
+
+/**
+ * The effective gate for ONE repo: the operator flag AND the repo's own opt-out.
+ *
+ * An absent repo override means ON (opt-out, per the decision above). An operator flag that is off wins
+ * outright -- a repo cannot opt INTO a surface the operator has not enabled, which keeps the fleet-wide
+ * switch a real switch rather than a default a repo can override.
+ */
+export function isProofPageEnabledForRepo(
+ env: { LOOPOVER_PUBLIC_PROOF?: string | undefined },
+ repoOverride?: ProofPageRepoOverride | undefined,
+): boolean {
+ if (!isPublicProofPageEnabled(env)) return false;
+ return repoOverride?.present ? repoOverride.enabled : true;
+}
+
+/**
+ * Load one repo's proof summary from the SAME public sources the standalone endpoints serve.
+ *
+ * No new SQL surface and no new verification mechanism (#9569's own boundary): the decision counts come
+ * from `decision_records`, the chain status from the shared `verifyDecisionLedger`, the anchor from the
+ * public attempt log, and the samples from records already published individually. Every read is wrapped
+ * so one failing section degrades to its honest neutral state rather than failing the whole page --
+ * matching `loadPublicRulePrecision`'s own fail-safe-per-section contract.
+ */
+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 }>;
+ loadAnchors: (env: Env) => Promise<{ anchors: PublicLedgerAnchor[] }>;
+ now?: () => string;
+ },
+): Promise {
+ const checkedAt = (deps.now ?? (() => new Date().toISOString()))();
+
+ // `.catch()` alone is NOT enough here: `DB.prepare()` throws SYNCHRONOUSLY on a driver-level failure, so
+ // the rejection never reaches a promise handler and the whole page 503s instead of degrading. Each
+ // section is therefore wrapped in a real try/catch. That is the difference between the fail-safe-per-
+ // section contract being documented and it being true.
+ const section = async (read: () => Promise, fallback: T): Promise => {
+ try {
+ return await read();
+ } catch {
+ return fallback;
+ }
+ };
+
+ const counts = await section(
+ () =>
+ 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
+ FROM decision_records WHERE repo_full_name = ?`,
+ )
+ .bind(repoFullName)
+ .first<{ decisionCount: number | null; decided: number | null; confirmed: number | null }>(),
+ null,
+ );
+
+ const records = await section(
+ async () =>
+ (
+ await env.DB.prepare(
+ `SELECT pull_number AS pullNumber, action, reason_code AS reasonCode, created_at AS decidedAt, record_digest AS recordDigest
+ FROM decision_records WHERE repo_full_name = ? ORDER BY created_at DESC LIMIT ?`,
+ )
+ .bind(repoFullName, PROOF_SAMPLE_RECORDS)
+ .all<{ pullNumber: number; action: string; reasonCode: string; decidedAt: string; recordDigest: string }>()
+ ).results ?? [],
+ [] as Array<{ pullNumber: number; action: string; reasonCode: string; decidedAt: string; recordDigest: string }>,
+ );
+
+ const verify = await section(() => deps.verifyLedger(env), null);
+ const anchors = await section(async () => (await deps.loadAnchors(env)).anchors, [] as PublicLedgerAnchor[]);
+
+ return buildProofSummary({
+ repoFullName,
+ // SUM/COUNT over an empty table yield NULL/0 respectively -- both nullish arms are real and both
+ // 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,
+ verify,
+ anchors,
+ records,
+ checkedAt,
+ });
+}
diff --git a/test/unit/proof-summary.test.ts b/test/unit/proof-summary.test.ts
new file mode 100644
index 000000000..b1bc398a7
--- /dev/null
+++ b/test/unit/proof-summary.test.ts
@@ -0,0 +1,329 @@
+import { describe, expect, it } from "vitest";
+import {
+ buildProofAccuracy,
+ buildProofAnchorStatus,
+ buildProofBadgeColor,
+ buildProofBadgeMessage,
+ buildProofLedgerStatus,
+ buildProofSummary,
+ isProofPageEnabledForRepo,
+ isPublicProofPageEnabled,
+ loadProofSummary,
+ PROOF_BOUNDARY_STATEMENT,
+ PROOF_MIN_DECISIONS,
+ PROOF_SAMPLE_RECORDS,
+} from "../../src/review/proof-summary";
+import { renderProofBadgeSvg } from "../../src/api/proof-badge";
+import { createApp } from "../../src/api/routes";
+import { appendDecisionLedger, persistDecisionRecord } from "../../src/review/decision-record";
+import { loadPublicLedgerAnchors, recordLedgerAnchorAttempt } from "../../src/review/ledger-anchor-persistence";
+import { createTestEnv } from "../helpers/d1";
+import type { PublicLedgerAnchor } from "../../src/review/ledger-anchor-persistence";
+
+// #9569: the public, shareable twin of the in-app trust panel. The properties that matter here are the ones
+// that would quietly turn a verification page into a marketing page: an accuracy figure without its
+// denominator and interval, a failed anchor attempt presented as an anchor, an empty ledger presented as
+// "verified", or a field nobody wrote down leaking through a spread.
+
+const CHECKED_AT = "2026-07-28T12:00:00.000Z";
+
+function anchor(overrides: Partial = {}): PublicLedgerAnchor {
+ return {
+ id: "a1",
+ seq: 10,
+ rowHash: "f".repeat(64),
+ keyId: "k1",
+ backend: "rekor",
+ backendRef: { uuid: "u" },
+ status: "ok",
+ error: null,
+ createdAt: "2026-07-20T00:00:00.000Z",
+ ...overrides,
+ };
+}
+
+describe("buildProofAccuracy — never a bare scalar (#9569)", () => {
+ it("publishes accuracy ONLY with its coverage and a Wilson interval", () => {
+ const accuracy = buildProofAccuracy(60, 59);
+ expect(accuracy.state).toBe("published");
+ if (accuracy.state !== "published") return;
+ expect(accuracy).toMatchObject({ accuracy: 0.983, decided: 60, confirmed: 59 });
+ // Wilson, not Wald: at 59/60 Wald would claim an upper bound of ~1.017 (impossible) and a lower bound
+ // that overstates certainty. Wilson stays inside [0,1] and keeps an honest lower bound.
+ expect(accuracy.interval.lo).toBeGreaterThan(0.9);
+ expect(accuracy.interval.lo).toBeLessThan(accuracy.accuracy);
+ expect(accuracy.interval.hi).toBeLessThanOrEqual(1);
+ });
+
+ it("REGRESSION: below the sample floor there is NO rate — but the count is still published", () => {
+ const thin = buildProofAccuracy(PROOF_MIN_DECISIONS - 1, PROOF_MIN_DECISIONS - 1);
+ expect(thin).toEqual({ state: "insufficient_data", decided: PROOF_MIN_DECISIONS - 1, minimumDecisions: PROOF_MIN_DECISIONS });
+ // A perfect record over 19 decisions must NOT render as 100% — that is the exact bare-scalar claim
+ // this module exists to refuse.
+ expect(JSON.stringify(thin)).not.toContain("accuracy");
+ // Zero and negative both degrade to a non-negative count rather than a fabricated rate.
+ expect(buildProofAccuracy(0, 0)).toMatchObject({ state: "insufficient_data", decided: 0 });
+ expect(buildProofAccuracy(-5, 0)).toMatchObject({ state: "insufficient_data", decided: 0 });
+ // Exactly at the floor publishes.
+ expect(buildProofAccuracy(PROOF_MIN_DECISIONS, PROOF_MIN_DECISIONS).state).toBe("published");
+ });
+});
+
+describe("buildProofAnchorStatus (#9569)", () => {
+ it("REGRESSION: a FAILED attempt is not an anchor — presenting one would claim corroboration that does not exist", () => {
+ expect(buildProofAnchorStatus([anchor({ status: "failed", error: "rekor 429" })])).toEqual({ state: "not_yet_anchored" });
+ expect(buildProofAnchorStatus([])).toEqual({ state: "not_yet_anchored" });
+ });
+
+ it("picks the NEWEST successful anchor regardless of list order", () => {
+ const older = anchor({ id: "old", seq: 5, createdAt: "2026-07-01T00:00:00.000Z" });
+ const newer = anchor({ id: "new", seq: 12, createdAt: "2026-07-25T00:00:00.000Z", backend: "git" });
+ const failedNewest = anchor({ id: "bad", seq: 13, createdAt: "2026-07-27T00:00:00.000Z", status: "failed", error: "boom" });
+ for (const order of [[older, newer, failedNewest], [failedNewest, newer, older]]) {
+ expect(buildProofAnchorStatus(order)).toEqual({ state: "anchored", backend: "git", seq: 12, rowHash: "f".repeat(64), at: "2026-07-25T00:00:00.000Z" });
+ }
+ });
+});
+
+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 });
+ });
+
+ 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: false, tipSeq: 9, totalCount: 9, break: { kind: "row_hash_mismatch", atSeq: 4 } }, 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" });
+ // 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 });
+ });
+});
+
+describe("buildProofSummary — the structural privacy boundary (#9569)", () => {
+ it("REGRESSION: fields nobody named cannot travel, even when the inputs carry them", () => {
+ const summary = buildProofSummary({
+ repoFullName: "o/r",
+ decisionCount: 30,
+ decided: 30,
+ confirmed: 29,
+ verify: { ok: true, tipSeq: 30, totalCount: 30 },
+ anchors: [anchor()],
+ records: [
+ {
+ pullNumber: 1, action: "merge", reasonCode: "clean", decidedAt: CHECKED_AT, recordDigest: "d".repeat(64),
+ // Hostile extras — exactly the classes the privacy boundary names. A spread-based composition
+ // would carry every one of these onto an unauthenticated page.
+ hotkey: "5FHneW46xGXgs5mUiveU4sbTyGBz", walletAddress: "0xdead", rewardTao: 42, trustScore: 0.9, privateRank: 3,
+ } as never,
+ ],
+ checkedAt: CHECKED_AT,
+ });
+ const serialized = JSON.stringify(summary);
+ for (const forbidden of ["hotkey", "walletAddress", "rewardTao", "trustScore", "privateRank", "0xdead", "5FHneW46"]) {
+ expect(serialized).not.toContain(forbidden);
+ }
+ // The named fields DID come through, so the test is proving allowlisting rather than an empty object.
+ expect(summary.sampleRecords[0]).toEqual({ pullNumber: 1, action: "merge", reasonCode: "clean", decidedAt: CHECKED_AT, recordDigest: "d".repeat(64) });
+ });
+
+ it("bounds the sample and carries the boundary statement IN the payload", () => {
+ const many = Array.from({ length: 25 }, (_, index) => ({
+ pullNumber: index, action: "merge", reasonCode: "clean", decidedAt: CHECKED_AT, recordDigest: String(index).repeat(2),
+ }));
+ 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,
+ });
+ 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.
+ expect(summary.boundary).toBe(PROOF_BOUNDARY_STATEMENT);
+ expect(summary.boundary).toContain("does not make every row checkable in real time");
+ });
+});
+
+describe("proof badge (#9569)", () => {
+ const summaryWith = (ledger: Parameters[0]["ledger"], anchored: boolean) =>
+ ({ 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: "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");
+ });
+
+ it("colors a not-yet-decided repo NEUTRALLY — it has not failed anything", () => {
+ 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");
+ });
+
+ 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));
+ expect(svg.startsWith("