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 [ + ``, + `${labelText}: ${messageText}`, + ``, + ``, + ``, + ``, + `${labelText}`, + `${messageText}`, + ``, + ].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("]*>[^<]*[<>][^<]*<\/text>/); + }); +}); + +describe("the opt-OUT gate and its recorded default (#9569 requirement 6)", () => { + it("the operator flag is OFF by default and truthy-string parsed", () => { + expect(isPublicProofPageEnabled({})).toBe(false); + expect(isPublicProofPageEnabled({ LOOPOVER_PUBLIC_PROOF: "" })).toBe(false); + expect(isPublicProofPageEnabled({ LOOPOVER_PUBLIC_PROOF: "false" })).toBe(false); + for (const on of ["1", "true", "yes", "on", "TRUE"]) { + expect(isPublicProofPageEnabled({ LOOPOVER_PUBLIC_PROOF: on })).toBe(true); + } + }); + + it("REGRESSION: a repo defaults ON once the operator opts in, and can opt OUT — but never opt IN alone", () => { + const on = { LOOPOVER_PUBLIC_PROOF: "true" }; + // Default ON: the data is already public, so a page over it needs no second opt-in. + expect(isProofPageEnabledForRepo(on)).toBe(true); + expect(isProofPageEnabledForRepo(on, { present: false, enabled: false })).toBe(true); + // The repo's opt-out is honored. + expect(isProofPageEnabledForRepo(on, { present: true, enabled: false })).toBe(false); + expect(isProofPageEnabledForRepo(on, { present: true, enabled: true })).toBe(true); + // The operator flag wins outright — a repo cannot opt INTO a surface the fleet has not enabled, which + // is what keeps the fleet-wide switch a real switch. + expect(isProofPageEnabledForRepo({}, { present: true, enabled: true })).toBe(false); + }); +}); + +describe("loadProofSummary + routes (#9569)", () => { + async function seeded() { + const env = createTestEnv({ LOOPOVER_PUBLIC_PROOF: "true" }); + for (let index = 1; index <= 3; 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, + `${index}`.repeat(64), + ); + await appendDecisionLedger(env, `record:o/r#${index}@sha${index}`, `${index}`.repeat(64)); + } + return env; + } + + 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 }), + // 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, + }); + expect(summary.decisionCount).toBe(3); + expect(summary.anchor).toEqual({ state: "not_yet_anchored" }); + expect(summary.ledger).toMatchObject({ state: "verified", totalCount: 3 }); + // Three decisions is under the floor — no rate is claimed. + expect(summary.accuracy.state).toBe("insufficient_data"); + expect(summary.sampleRecords.length).toBeGreaterThan(0); + // A failing LEDGER read likewise degrades to unavailable rather than throwing. + const degraded = await loadProofSummary(env, "o/r", { + verifyLedger: async () => { throw new Error("d1 down"); }, + loadAnchors: async () => ({ anchors: [] }), + now: () => CHECKED_AT, + }); + expect(degraded.ledger).toEqual({ state: "unavailable", checkedAt: CHECKED_AT }); + }); + + it("INVARIANT: every DB section degrades independently — a failing read never fabricates a figure", async () => { + const env = createTestEnv({ LOOPOVER_PUBLIC_PROOF: "true" }); + // Both decision-record reads throw. The page must still compose, reporting zero decisions and no rate, + // rather than surfacing a partial or invented number on an unauthenticated marketing surface. + (env.DB as unknown as { prepare: () => never }).prepare = () => { + throw new Error("d1 down"); + }; + const summary = await loadProofSummary(env, "o/r", { + verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0 }), + loadAnchors: async () => ({ anchors: [] }), + now: () => CHECKED_AT, + }); + expect(summary).toMatchObject({ decisionCount: 0, sampleRecords: [], ledger: { state: "empty" } }); + expect(summary.accuracy).toMatchObject({ state: "insufficient_data", decided: 0 }); + }); + + it("a driver returning no `results` array degrades to an empty sample rather than throwing", async () => { + const env = createTestEnv({ LOOPOVER_PUBLIC_PROOF: "true" }); + // A D1 driver that resolves without a `results` key — the `?? []` arm, which a real outage or a driver + // version change can produce and which must not take the page down. + (env.DB as unknown as { prepare: (sql: string) => unknown }).prepare = () => ({ + bind: () => ({ + first: async () => null, + all: async () => ({}), + }), + }); + const summary = await loadProofSummary(env, "o/r", { + verifyLedger: async () => ({ ok: true, tipSeq: 0, totalCount: 0 }), + loadAnchors: async () => ({ anchors: [] }), + now: () => CHECKED_AT, + }); + expect(summary.sampleRecords).toEqual([]); + expect(summary.decisionCount).toBe(0); + }); + + 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 }), + loadAnchors: async () => ({ anchors: [] }), + now: () => CHECKED_AT, + }); + expect(summary).toMatchObject({ decisionCount: 0, ledger: { state: "empty" }, anchor: { state: "not_yet_anchored" } }); + expect(summary.accuracy).toMatchObject({ state: "insufficient_data", decided: 0 }); + expect(summary.sampleRecords).toEqual([]); + }); + + it("REGRESSION: both routes 404 while the flag is OFF, and serve once it is on", async () => { + const app = createApp(); + const off = createTestEnv(); + expect((await app.request("/v1/public/repos/o/r/proof", {}, off)).status).toBe(404); + const badgeOff = await app.request("/v1/public/repos/o/r/proof-badge.svg", {}, off); + expect(badgeOff.status).toBe(404); + // Even the 404 renders a real badge — a broken image in a README is worse than an honest neutral one. + expect(await badgeOff.text()).toContain("unavailable"); + expect(badgeOff.headers.get("content-type")).toContain("image/svg+xml"); + + const on = await seeded(); + const proof = await app.request("/v1/public/repos/o/r/proof", {}, on); + expect(proof.status).toBe(200); + const body = (await proof.json()) as { repoFullName: string; boundary: string; decisionCount: number }; + expect(body).toMatchObject({ repoFullName: "o/r", decisionCount: 3 }); + expect(body.boundary).toBe(PROOF_BOUNDARY_STATEMENT); + expect(proof.headers.get("cache-control")).toContain("max-age=60"); + + const badge = await app.request("/v1/public/repos/o/r/proof-badge.svg", {}, on); + expect(badge.status).toBe(200); + expect(await badge.text()).toContain(" { + const env = await seeded(); + await recordLedgerAnchorAttempt(env, { + payload: { v: 1, ledger: "loopover.decision_ledger", seq: 3, rowHash: "a".repeat(64), totalCount: 3, at: CHECKED_AT }, + 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 }), + loadAnchors: (target) => loadPublicLedgerAnchors(target, {}), + now: () => CHECKED_AT, + }); + expect(summary.anchor).toMatchObject({ state: "anchored", backend: "rekor", seq: 3 }); + }); +}); From 26f90ca582de973668a67fd9178da3688b723a23 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:29:33 -0700 Subject: [PATCH 2/6] fix(proof): actually wire the per-repo opt-out the routes only claimed to honor (#9569) Review caught the real defect: both handlers called isProofPageEnabledForRepo(c.env) with no second argument, so the ProofPageRepoOverride documented at length in proof-summary.ts and in the PR body was never loaded or passed. Every repo was effectively opt-out-less once the fleet flag was on -- a gate that is described, typed, and unit-tested as a pure function, but never reachable from the surface it governs. That is the registered-but-unreachable class, and the long comment made it worse rather than better by making it look done. - Adds a real `publicProof:` focus-manifest block (engine parser + toJson + loader snapshot), mirroring `publicStats:`/`ops:`. Precedence is deliberately the opposite of those two: read from the TARGET repo's manifest rather than the operator's self-repo, because the thing being opted out of is that repo's own page. - loadProofPageRepoOverride resolves it, degrading a failed manifest load to "no override" -- a broken manifest never takes a page DOWN, which is the failure direction worth accepting here and is now stated in the doc comment rather than left implicit. - Both routes load the override BEFORE anything else, so a repo that turned its page off does not have its decision records queried to build a summary that will be discarded. - Documents the block in .loopover.yml.example, including the precedence and the opt-out default. Tests that would have caught it: a repo opting out in its manifest now gets 404 from BOTH routes with the fleet flag on, while a different repo in the same fleet still serves 200 (the opt-out is per repo, not a kill switch); explicit opt-in and no-block-at-all both serve; and the resolver is covered across absent/explicit/failing loads. --- .loopover.yml.example | 14 ++++++ .../loopover-engine/src/focus-manifest.ts | 44 +++++++++++++++++++ src/api/routes.ts | 13 ++++-- src/review/proof-summary.ts | 23 ++++++++++ src/signals/focus-manifest-loader.ts | 3 +- src/signals/focus-manifest.ts | 1 + test/unit/focus-manifest.test.ts | 1 + test/unit/proof-summary.test.ts | 41 ++++++++++++++++- 8 files changed, 134 insertions(+), 6 deletions(-) diff --git a/.loopover.yml.example b/.loopover.yml.example index 05b93477c..f0aa1a82d 100644 --- a/.loopover.yml.example +++ b/.loopover.yml.example @@ -1493,3 +1493,17 @@ settings: # # To stop trusting a peer, remove its key -- that is the whole revocation story, by design. # peerKeys: # - 0000000000000000000000000000000000000000000000000000000000000000 + +# Public proof page (#9569) — the shareable, unauthenticated per-repo verification page +# (`/proof//`) and its README badge. +# +# OPT-OUT, not opt-in. Every figure the page renders is ALREADY publicly fetchable through the +# ledger-verify, anchors and decision-record endpoints, so gating a page over data anyone can already +# curl would add friction without adding privacy. This block exists because a page is nonetheless a +# different artifact from an API: it is discoverable, linkable, and it markets this repo's numbers. +# +# Precedence: the operator's fleet-wide LOOPOVER_PUBLIC_PROOF flag must be on first. This block can turn +# THIS repo's page OFF; it cannot turn one ON that the operator has not enabled. Omit the block entirely +# to keep the page on once the operator enables it. +# publicProof: +# enabled: false # Bool. Default when the block is absent: true (opt-out). diff --git a/packages/loopover-engine/src/focus-manifest.ts b/packages/loopover-engine/src/focus-manifest.ts index c2a545c67..6274a62e0 100644 --- a/packages/loopover-engine/src/focus-manifest.ts +++ b/packages/loopover-engine/src/focus-manifest.ts @@ -440,6 +440,21 @@ export type FocusManifestPublicStatsConfig = { enabled: boolean; }; +/** + * Per-repo opt-OUT for the public proof page and its badge (#9569), declared under `publicProof:`. + * + * Shape matches `publicStats:`/`ops:`, but the PRECEDENCE is deliberately different: those are fleet-wide + * and read from the operator's self-repo manifest, whereas this is read from the TARGET repo's own manifest, + * because the thing being opted out of is that repo's own page. Not present ⇒ enabled, once the operator's + * LOOPOVER_PUBLIC_PROOF flag is on — opt-OUT, since every figure the page renders is already publicly + * fetchable through the ledger-verify / anchors / decision-record endpoints. A repo can turn its page off; + * it cannot turn one on that the operator has not enabled. + */ +export type FocusManifestPublicProofConfig = { + present: boolean; + enabled: boolean; +}; + /** * Config-as-code override for the internal, bearer-gated contributor-trust-profile / fairness-analytics * surface (LOOPOVER_FAIRNESS_ANALYTICS, #fairness-analytics), declared under `fairnessAnalytics:`. Same @@ -1249,6 +1264,7 @@ export type FocusManifest = { maintainerRecap: FocusManifestMaintainerRecapConfig; ops: FocusManifestOpsConfig; publicStats: FocusManifestPublicStatsConfig; + publicProof: FocusManifestPublicProofConfig; fairnessAnalytics: FocusManifestFairnessAnalyticsConfig; draftFlow: FocusManifestDraftFlowConfig; upstreamDriftIssues: FocusManifestUpstreamDriftIssuesConfig; @@ -1413,6 +1429,13 @@ const EMPTY_PUBLIC_STATS_CONFIG: FocusManifestPublicStatsConfig = { enabled: false, }; +/** #9569: absent means ENABLED at the resolver (opt-out), so `enabled:false` here is only the shape's + * default — `present:false` is what the resolver actually keys on. */ +const EMPTY_PUBLIC_PROOF_CONFIG: FocusManifestPublicProofConfig = { + present: false, + enabled: false, +}; + const EMPTY_FAIRNESS_ANALYTICS_CONFIG: FocusManifestFairnessAnalyticsConfig = { present: false, enabled: false, @@ -1478,6 +1501,7 @@ const EMPTY_MANIFEST: FocusManifest = { maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG }, ops: { ...EMPTY_OPS_CONFIG }, publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG }, + publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG }, fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG }, draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG }, upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG }, @@ -1520,6 +1544,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG }, ops: { ...EMPTY_OPS_CONFIG }, publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG }, + publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG }, fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG }, draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG }, upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG }, @@ -2356,6 +2381,24 @@ export function publicStatsConfigToJson(config: FocusManifestPublicStatsConfig): return { enabled: config.enabled }; } +/** Parse the optional `publicProof:` mapping (#9569). Mirrors {@link parsePublicStatsConfig} exactly. */ +function parsePublicProofConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestPublicProofConfig { + if (value === undefined || value === null) return { ...EMPTY_PUBLIC_PROOF_CONFIG }; + if (typeof value !== "object" || Array.isArray(value)) { + warnings.push('Manifest field "publicProof" must be a mapping; ignoring it.'); + return { ...EMPTY_PUBLIC_PROOF_CONFIG }; + } + const record = value as Record; + const enabled = normalizeOptionalBoolean(record.enabled, "publicProof.enabled", warnings) ?? false; + return { present: true, enabled }; +} + +/** Serialize a publicProof config so a cached snapshot round-trips through {@link parsePublicProofConfig}. */ +export function publicProofConfigToJson(config: FocusManifestPublicProofConfig): JsonValue { + if (!config.present) return null; + return { enabled: config.enabled }; +} + /** Parse the optional `fairnessAnalytics:` mapping (#fairness-analytics). Mirrors {@link parsePublicStatsConfig} * exactly -- the only field is `enabled`, no DB layer to overlay onto. */ function parseFairnessAnalyticsConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestFairnessAnalyticsConfig { @@ -4326,6 +4369,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource): maintainerRecap: parseMaintainerRecapConfig(record.maintainerRecap, warnings), ops: parseOpsConfig(record.ops, warnings), publicStats: parsePublicStatsConfig(record.publicStats, warnings), + publicProof: parsePublicProofConfig(record.publicProof, warnings), fairnessAnalytics: parseFairnessAnalyticsConfig(record.fairnessAnalytics, warnings), draftFlow: parseDraftFlowConfig(record.draftFlow, warnings), upstreamDriftIssues: parseUpstreamDriftIssuesConfig(record.upstreamDriftIssues, warnings), diff --git a/src/api/routes.ts b/src/api/routes.ts index 5bbd86179..05a26f1c9 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -306,7 +306,7 @@ 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 { isProofPageEnabledForRepo, loadProofPageRepoOverride, 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"; @@ -1357,8 +1357,11 @@ export function createApp() { // 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")}`; + // The repo's OWN opt-out, loaded before anything else is read: a repo that turned its page off must not + // have its decision records queried to build a summary that will be discarded. + const override = await loadProofPageRepoOverride(c.env, repoFullName, loadRepoFocusManifest); + if (!isProofPageEnabledForRepo(c.env, override)) return c.json({ error: "not_found" }, 404); try { const summary = await loadProofSummary(c.env, repoFullName, { verifyLedger: (env) => verifyDecisionLedger(env), @@ -1376,12 +1379,14 @@ export function createApp() { // 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)) { + const badgeRepoFullName = `${c.req.param("owner")}/${c.req.param("repo")}`; + const badgeOverride = await loadProofPageRepoOverride(c.env, badgeRepoFullName, loadRepoFocusManifest); + if (!isProofPageEnabledForRepo(c.env, badgeOverride)) { 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")}`, { + const summary = await loadProofSummary(c.env, badgeRepoFullName, { verifyLedger: (env) => verifyDecisionLedger(env), loadAnchors: (env) => loadPublicLedgerAnchors(env, { limit: 20 }), }); diff --git a/src/review/proof-summary.ts b/src/review/proof-summary.ts index 204869222..29e2b72e0 100644 --- a/src/review/proof-summary.ts +++ b/src/review/proof-summary.ts @@ -217,6 +217,29 @@ export function buildProofBadgeColor(summary: ProofSummary): string { export type ProofPageRepoOverride = { present: boolean; enabled: boolean }; +/** + * Load ONE repo's `publicProof:` opt-out from its own focus manifest. + * + * Read from the TARGET repo's manifest, not the operator's self-repo, because the thing being opted out of + * is that repo's own page -- the opposite precedence from `publicStats:`/`ops:`, which are fleet-wide. + * + * A manifest load failure degrades to `{ present: false }`, i.e. exactly as if no override existed, so a + * network blip or malformed YAML can never accidentally EXPOSE a page the maintainer turned off... which is + * the wrong direction, and is why the caller must treat a failed load as the operator default rather than + * this function pretending to know. Documented here because the failure direction is the interesting part: + * we accept "a broken manifest leaves the page on" in exchange for "a broken manifest never takes a page + * down", matching how every other resolveX accessor in this codebase degrades. + */ +export async function loadProofPageRepoOverride( + env: Env, + repoFullName: string, + loadManifest: (env: Env, repoFullName: string) => Promise<{ publicProof: { present: boolean; enabled: boolean } } | null>, +): Promise { + const manifest = await loadManifest(env, repoFullName).catch(() => null); + if (!manifest?.publicProof?.present) return { present: false, enabled: false }; + return { present: true, enabled: manifest.publicProof.enabled }; +} + /** 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 ?? ""); diff --git a/src/signals/focus-manifest-loader.ts b/src/signals/focus-manifest-loader.ts index 021ac9f51..36b529848 100644 --- a/src/signals/focus-manifest-loader.ts +++ b/src/signals/focus-manifest-loader.ts @@ -2,7 +2,7 @@ import { listSignalSnapshots, persistSignalSnapshot } from "../db/repositories"; import { mapWithConcurrency } from "../queue/map-with-concurrency"; import type { JsonValue } from "../types"; import { nowIso } from "../utils/json"; -import { contentLaneConfigToJson, experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, opsConfigToJson, publicStatsConfigToJson, fairnessAnalyticsConfigToJson, draftFlowConfigToJson, upstreamDriftIssuesConfigToJson, sweepWatchdogConfigToJson, prReconciliationConfigToJson, activeReviewReconciliationConfigToJson, loopEscalationConfigToJson, federatedIntelligenceConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest"; +import { contentLaneConfigToJson, experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, opsConfigToJson, publicStatsConfigToJson, publicProofConfigToJson, fairnessAnalyticsConfigToJson, draftFlowConfigToJson, upstreamDriftIssuesConfigToJson, sweepWatchdogConfigToJson, prReconciliationConfigToJson, activeReviewReconciliationConfigToJson, loopEscalationConfigToJson, federatedIntelligenceConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest"; import { LOOPOVER_REPO_FOCUS_MANIFEST_YAML, resolveLoopOverSelfRepoFullName } from "../config/loopover-repo-focus-manifest"; import type { LocalManifestLoadResult } from "../selfhost/private-config"; @@ -337,6 +337,7 @@ function manifestToJson(manifest: FocusManifest): Record { maintainerRecap: maintainerRecapConfigToJson(manifest.maintainerRecap), ops: opsConfigToJson(manifest.ops), publicStats: publicStatsConfigToJson(manifest.publicStats), + publicProof: publicProofConfigToJson(manifest.publicProof), fairnessAnalytics: fairnessAnalyticsConfigToJson(manifest.fairnessAnalytics), draftFlow: draftFlowConfigToJson(manifest.draftFlow), upstreamDriftIssues: upstreamDriftIssuesConfigToJson(manifest.upstreamDriftIssues), diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index 4f5e81c3f..4026164e7 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -37,6 +37,7 @@ export { maintainerRecapConfigToJson, opsConfigToJson, publicStatsConfigToJson, + publicProofConfigToJson, fairnessAnalyticsConfigToJson, draftFlowConfigToJson, upstreamDriftIssuesConfigToJson, diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 5aab97263..e831bfd1e 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -971,6 +971,7 @@ describe("compileFocusManifestPolicy", () => { maintainerRecap: { present: false, enabled: false, cadence: "weekly", channel: "discord" }, ops: { present: false, enabled: false }, publicStats: { present: false, enabled: false }, + publicProof: { present: false, enabled: false }, fairnessAnalytics: { present: false, enabled: false }, draftFlow: { present: false, enabled: false }, upstreamDriftIssues: { present: false, enabled: false }, diff --git a/test/unit/proof-summary.test.ts b/test/unit/proof-summary.test.ts index b1bc398a7..51a88f32e 100644 --- a/test/unit/proof-summary.test.ts +++ b/test/unit/proof-summary.test.ts @@ -18,6 +18,8 @@ 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 { loadProofPageRepoOverride } from "../../src/review/proof-summary"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; 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 @@ -326,4 +328,41 @@ describe("loadProofSummary + routes (#9569)", () => { }); expect(summary.anchor).toMatchObject({ state: "anchored", backend: "rekor", seq: 3 }); }); -}); + + it("REGRESSION: a repo that opts OUT in its manifest gets 404 from BOTH routes, even with the fleet flag on", async () => { + // The defect this test exists for: both handlers called isProofPageEnabledForRepo(c.env) with no + // override, so the documented per-repo opt-out was never loaded and every repo was effectively + // opt-out-less once the operator flag was on. + const app = createApp(); + const env = await seeded(); + await upsertRepoFocusManifest(env, "o/r", { publicProof: { enabled: false } } as never); + + expect((await app.request("/v1/public/repos/o/r/proof", {}, env)).status).toBe(404); + const badge = await app.request("/v1/public/repos/o/r/proof-badge.svg", {}, env); + expect(badge.status).toBe(404); + expect(await badge.text()).toContain("unavailable"); + + // A DIFFERENT repo in the same fleet is unaffected — the opt-out is per repo, not a kill switch. + expect((await app.request("/v1/public/repos/other/repo/proof", {}, env)).status).toBe(200); + }); + + it("an explicit manifest opt-IN serves, and so does a repo with no manifest block at all (opt-out default)", async () => { + const app = createApp(); + const env = await seeded(); + await upsertRepoFocusManifest(env, "o/r", { publicProof: { enabled: true } } as never); + expect((await app.request("/v1/public/repos/o/r/proof", {}, env)).status).toBe(200); + // No block at all: the page is on, because the data is already public and this is opt-OUT. + const bare = await seeded(); + expect((await app.request("/v1/public/repos/o/r/proof", {}, bare)).status).toBe(200); + }); + + it("loadProofPageRepoOverride: absent block, explicit values, and a failing load all resolve honestly", async () => { + const env = createTestEnv(); + expect(await loadProofPageRepoOverride(env, "o/r", async () => null)).toEqual({ present: false, enabled: false }); + expect(await loadProofPageRepoOverride(env, "o/r", async () => ({ publicProof: { present: false, enabled: false } }))).toEqual({ present: false, enabled: false }); + expect(await loadProofPageRepoOverride(env, "o/r", async () => ({ publicProof: { present: true, enabled: false } }))).toEqual({ present: true, enabled: false }); + expect(await loadProofPageRepoOverride(env, "o/r", async () => ({ publicProof: { present: true, enabled: true } }))).toEqual({ present: true, enabled: true }); + // A failing manifest load degrades to "no override" -- a broken manifest never takes a page DOWN. + expect(await loadProofPageRepoOverride(env, "o/r", async () => { throw new Error("network down"); })).toEqual({ present: false, enabled: false }); + }); +}); \ No newline at end of file From d27e945aa6960542bdd45c24a8133d081bbcdf43 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:42:25 -0700 Subject: [PATCH 3/6] fix(build): build @loopover/contract in ui:build, unbreaking the Cloudflare Workers build The Workers build for loopover-ui has been failing on every PR since #9521 (merged as #9590) made src/openapi/schemas.ts import @loopover/contract/public-api: Cannot find module '.../node_modules/@loopover/contract/dist/public-api.js' imported from /opt/buildhome/repo/src/openapi/schemas.ts ui:build builds ui-kit and engine, then runs ui:openapi -- but never builds the contract package, so the import resolves to a dist/ that does not exist. CI did not catch it because the GitHub workflow has its own separate "Build contract package" step (ci.yml:361) before the drift checks; the Cloudflare build runs npm run build:cloudflare -> ui:build directly and gets no such step. The two paths had silently diverged. Add @loopover/contract to the same turbo invocation that already builds the engine, so the one script both paths share produces everything ui:openapi imports. Reproduced locally by deleting packages/loopover-contract/dist and running ui:openapi (identical ERR_MODULE_NOT_FOUND), then confirmed the fixed chain builds the package and writes the spec with no drift. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c24722a91..8d3faee24 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "lint:composite-actions": "node --experimental-strip-types scripts/lint-composite-actions.ts", "ui:dev": "npm run ui:preview", "ui:kit:build": "npm run build --workspace @loopover/ui-kit", - "ui:build": "npm run ui:kit:build && turbo run build --filter=@loopover/engine && npm run ui:openapi && npm --workspace @loopover/ui run build && npm --workspace @loopover/ui-miner run build", + "ui:build": "npm run ui:kit:build && turbo run build --filter=@loopover/engine --filter=@loopover/contract && npm run ui:openapi && npm --workspace @loopover/ui run build && npm --workspace @loopover/ui-miner run build", "ui:preview": "npm run ui:build && wrangler dev --config apps/loopover-ui/dist/server/wrangler.json --ip 127.0.0.1 --port 4173 --local", "ui:lint": "npm run ui:kit:build && npm --workspace @loopover/ui-kit run format:check && npm --workspace @loopover/ui run format:check && npm --workspace @loopover/ui run lint && npm --workspace @loopover/ui-miner run format:check && npm --workspace @loopover/ui-miner run lint", "ui:typecheck": "npm run ui:kit:build && npm --workspace @loopover/ui run typecheck && npm --workspace @loopover/ui-miner run typecheck", From 64696cbf715d97bd04233e21da43d9ac4240372e Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:49:37 -0700 Subject: [PATCH 4/6] fix(manifest): register publicProof as a known top-level field, and sync the example template Two failures from the #9569 manifest block, both mine. 1. The unknown-top-level-field validator never learned about `publicProof`, so every manifest carrying it warned "Manifest contains unknown top-level field: publicProof." That was invisible on the first pass and appeared on every LATER one, because the first pass parses a manifest with no such key while later passes reload the persisted snapshot -- which my loader change now serializes the field into. The warning lands in the published review comment, so an unchanged PR got a fresh comment PATCH on every regate sweep: exactly the #3379 churn that test exists to prevent, reintroduced by a field the writer knew about and the reader did not. Found by instrumenting the test's PATCH interception to diff the two comment bodies rather than guessing at the cause; the added line named itself. 2. config/examples/loopover.full.yml must mirror .loopover.yml.example from "WHERE IT LIVES" onward, and I documented the block in only one of the two. Verified against origin/main first to confirm both were regressions from this branch rather than pre-existing. --- config/examples/loopover.full.yml | 14 ++++++++++++++ packages/loopover-engine/src/focus-manifest.ts | 1 + 2 files changed, 15 insertions(+) diff --git a/config/examples/loopover.full.yml b/config/examples/loopover.full.yml index 416a73317..13096355f 100644 --- a/config/examples/loopover.full.yml +++ b/config/examples/loopover.full.yml @@ -1507,3 +1507,17 @@ settings: # # To stop trusting a peer, remove its key -- that is the whole revocation story, by design. # peerKeys: # - 0000000000000000000000000000000000000000000000000000000000000000 + +# Public proof page (#9569) — the shareable, unauthenticated per-repo verification page +# (`/proof//`) and its README badge. +# +# OPT-OUT, not opt-in. Every figure the page renders is ALREADY publicly fetchable through the +# ledger-verify, anchors and decision-record endpoints, so gating a page over data anyone can already +# curl would add friction without adding privacy. This block exists because a page is nonetheless a +# different artifact from an API: it is discoverable, linkable, and it markets this repo's numbers. +# +# Precedence: the operator's fleet-wide LOOPOVER_PUBLIC_PROOF flag must be on first. This block can turn +# THIS repo's page OFF; it cannot turn one ON that the operator has not enabled. Omit the block entirely +# to keep the page on once the operator enables it. +# publicProof: +# enabled: false # Bool. Default when the block is absent: true (opt-out). diff --git a/packages/loopover-engine/src/focus-manifest.ts b/packages/loopover-engine/src/focus-manifest.ts index 6274a62e0..7986668d9 100644 --- a/packages/loopover-engine/src/focus-manifest.ts +++ b/packages/loopover-engine/src/focus-manifest.ts @@ -4290,6 +4290,7 @@ export const FOCUS_MANIFEST_TOP_LEVEL_FIELDS = [ "maintainerRecap", "ops", "publicStats", + "publicProof", "draftFlow", "upstreamDriftIssues", "sweepWatchdog", From bc191182b759685022b187d89c6b641ad0896e72 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 28 Jul 2026 23:22:33 -0700 Subject: [PATCH 5/6] test(proof): close the patch-coverage gaps, and fix a second sync-throw the gap exposed Codecov flagged 8 uncovered changed lines across focus-manifest.ts and routes.ts. I had measured coverage on proof-summary.ts and proof-badge.ts only, and never on the two files the manifest block and the routes actually touched -- so the gap was in my own verification, not just the tests. Closing it turned up a real defect rather than only missing assertions: loadProofPageRepoOverride used `.catch()` on the injected manifest loader, so a loader throwing SYNCHRONOUSLY (a driver-level failure before it ever returns a promise) skipped the handler entirely and would have escaped to the route -- 503ing a public page over a manifest read that is supposed to be optional. That is the same defect this file already had in loadProofSummary's section reads, which I fixed there and then reintroduced here. Now a real try/catch, with a regression test using a synchronously-throwing loader. Coverage: - parsePublicProofConfig / publicProofConfigToJson: explicit on/off, a present-but-empty block (present-but-false, which the resolver keys on), absence, three non-mapping shapes warning rather than throwing, and a snapshot round-trip. - A regression test asserting publicProof is a KNOWN top-level field, so the writer/reader split behind the #3379 regate churn cannot return. - The two route 503 arms are unreachable today (every inner read is individually fail-safe), so they are excluded with the house v8 pragma and a note on why they are kept: a future unguarded read should degrade to 503 rather than 500 on an unauthenticated public route. The badge arm uses ignore start/stop -- `next 2` miscounts across a multi-line comment and left the return uncovered. All three changed files now report zero uncovered changed lines. --- src/api/routes.ts | 8 +++++++ src/review/proof-summary.ts | 11 ++++++++- test/unit/focus-manifest.test.ts | 38 ++++++++++++++++++++++++++++++++ test/unit/proof-summary.test.ts | 5 +++++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index 05a26f1c9..62477733a 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1370,6 +1370,9 @@ export function createApp() { c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=300"); return c.json(summary); } catch { + /* v8 ignore next -- defense in depth: loadProofSummary wraps every section (and the override loader + its own read) in try/catch, so nothing inside currently throws. Kept so a future read added there + without that discipline degrades to 503 rather than a 500 on an unauthenticated public route. */ return c.json({ error: "unavailable" }, 503); } }); @@ -1393,8 +1396,13 @@ export function createApp() { c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400"); return c.body(renderProofBadgeSvg(summary)); } catch { + // Same defense-in-depth arm as the proof route above: a README embed gets the neutral badge rather + // than a broken image if a future unguarded read is added. Unreachable today -- every inner read is + // individually fail-safe -- so the whole arm is excluded rather than left as a coverage hole. + /* v8 ignore start */ c.header("Cache-Control", "public, max-age=300"); return c.body(renderProofBadgeSvg(null), 503); + /* v8 ignore stop */ } }); diff --git a/src/review/proof-summary.ts b/src/review/proof-summary.ts index 29e2b72e0..e0085134f 100644 --- a/src/review/proof-summary.ts +++ b/src/review/proof-summary.ts @@ -235,7 +235,16 @@ export async function loadProofPageRepoOverride( repoFullName: string, loadManifest: (env: Env, repoFullName: string) => Promise<{ publicProof: { present: boolean; enabled: boolean } } | null>, ): Promise { - const manifest = await loadManifest(env, repoFullName).catch(() => null); + // try/catch, NOT `.catch()`: a loader that throws SYNCHRONOUSLY (a driver-level failure before it ever + // returns a promise) skips a promise handler entirely, and the throw would escape to the route and 503 a + // public page over a manifest read that is supposed to be optional. Same defect this file already had in + // loadProofSummary's section reads. + let manifest: { publicProof: { present: boolean; enabled: boolean } } | null = null; + try { + manifest = await loadManifest(env, repoFullName); + } catch { + return { present: false, enabled: false }; + } if (!manifest?.publicProof?.present) return { present: false, enabled: false }; return { present: true, enabled: manifest.publicProof.enabled }; } diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index e831bfd1e..03bbe5f32 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -83,6 +83,44 @@ const FULL_MANIFEST = { publicNotes: ["Prefer small, focused PRs."], }; +describe("publicProof manifest block (#9569)", () => { + it("parses an explicit block, defaults `enabled` to false, and warns on a non-mapping instead of throwing", async () => { + const { parseFocusManifest, publicProofConfigToJson } = await import("../../src/signals/focus-manifest"); + + expect(parseFocusManifest({ publicProof: { enabled: false } }).publicProof).toEqual({ present: true, enabled: false }); + expect(parseFocusManifest({ publicProof: { enabled: true } }).publicProof).toEqual({ present: true, enabled: true }); + // A present block with no `enabled` key is present-but-false, not absent -- the resolver keys on + // `present`, so the distinction is load-bearing. + expect(parseFocusManifest({ publicProof: {} }).publicProof).toEqual({ present: true, enabled: false }); + // Absent entirely. + expect(parseFocusManifest({ wantedPaths: ["src/**"] }).publicProof).toEqual({ present: false, enabled: false }); + + // A scalar or a list is a config mistake: warned about and ignored, never a thrown parse. + for (const bad of ["nonsense", ["a"], 42]) { + const parsed = parseFocusManifest({ publicProof: bad }); + expect(parsed.publicProof).toEqual({ present: false, enabled: false }); + expect(parsed.warnings.some((warning) => warning.includes('Manifest field "publicProof" must be a mapping'))).toBe(true); + } + + // Round-trips through the snapshot serializer, so a cached manifest re-parses identically. + expect(publicProofConfigToJson({ present: false, enabled: false })).toBeNull(); + expect(publicProofConfigToJson({ present: true, enabled: false })).toEqual({ enabled: false }); + expect(publicProofConfigToJson({ present: true, enabled: true })).toEqual({ enabled: true }); + const roundTripped = parseFocusManifest({ publicProof: publicProofConfigToJson({ present: true, enabled: false }) }); + expect(roundTripped.publicProof).toEqual({ present: true, enabled: false }); + }); + + it("REGRESSION: publicProof is a KNOWN top-level field — otherwise its warning rewrites the review comment", async () => { + const { parseFocusManifest } = await import("../../src/signals/focus-manifest"); + // The writer/reader split behind #9569's regate churn: the loader serializes this field into the + // persisted snapshot, so a reader that did not recognize it warned on every reload, and that warning + // rewrote the published comment on every regate sweep (the #3379 class). + const parsed = parseFocusManifest({ publicProof: { enabled: false } }); + expect(parsed.warnings.some((warning) => warning.includes("unknown top-level field"))).toBe(false); + }); +}); + + describe("parseFocusManifest", () => { it("normalizes a fully specified manifest", () => { const manifest = parseFocusManifest(FULL_MANIFEST); diff --git a/test/unit/proof-summary.test.ts b/test/unit/proof-summary.test.ts index 51a88f32e..5fce6e9bf 100644 --- a/test/unit/proof-summary.test.ts +++ b/test/unit/proof-summary.test.ts @@ -364,5 +364,10 @@ describe("loadProofSummary + routes (#9569)", () => { expect(await loadProofPageRepoOverride(env, "o/r", async () => ({ publicProof: { present: true, enabled: true } }))).toEqual({ present: true, enabled: true }); // A failing manifest load degrades to "no override" -- a broken manifest never takes a page DOWN. expect(await loadProofPageRepoOverride(env, "o/r", async () => { throw new Error("network down"); })).toEqual({ present: false, enabled: false }); + // REGRESSION: a loader that throws SYNCHRONOUSLY (a driver-level failure before it returns a promise) + // skips a `.catch()` entirely -- the earlier implementation let that escape to the route and 503 a + // public page over an optional manifest read. + const syncThrow = (() => { throw new Error("d1 down"); }) as unknown as Parameters[2]; + expect(await loadProofPageRepoOverride(env, "o/r", syncThrow)).toEqual({ present: false, enabled: false }); }); }); \ No newline at end of file From b9700760feb4a23fe9d76e052a566c56b22ffe06 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 28 Jul 2026 23:38:09 -0700 Subject: [PATCH 6/6] refactor(proof): one shared resolver for both surfaces, and delete the unreachable arms Replaces the coverage pragmas with the fix they were papering over. The gate, the read and the outcome now live in ONE resolver (resolveProofPage) that both handlers render. That is not tidiness: the gate previously lived inline in both route bodies and exactly one of them was wired to the per-repo opt-out, which is the defect review caught. A shared resolver makes "the page and the badge agree about whether this repo is published" true by construction instead of by two call sites remembering the same thing. With that in place the two 503 arms were provably unreachable, because loadProofSummary is TOTAL -- every read is wrapped per section, so a failing ledger/anchor/record read degrades to that section's honest neutral state and the page still composes. Rather than excluding dead branches from coverage, the outcome is gone from the type: ProofPageResult is `ok | disabled`. A test asserts the totality directly -- every dependency failing at once, including a DB binding that throws on property access, still resolves to a rendered page in its neutral states. Same treatment for buildProofAccuracy's `!interval` guard: wilsonInterval returns null exactly when there are no trials, which IS the nothing-decided case, so one reachable guard covers both reasons a rate is unpublishable instead of a dead branch behind a pragma. Net: no `v8 ignore` pragmas anywhere in the #9569 code, and zero uncovered changed lines or branches across proof-summary.ts, routes.ts and focus-manifest.ts. --- src/api/routes.ts | 65 +++++++++++------------------ src/review/proof-summary.ts | 53 ++++++++++++++++++++++-- test/unit/proof-summary.test.ts | 73 ++++++++++++++++++++++++++++++++- 3 files changed, 144 insertions(+), 47 deletions(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index 62477733a..637d5e9da 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -306,7 +306,7 @@ 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, loadProofPageRepoOverride, loadProofSummary } from "../review/proof-summary"; +import { resolveProofPage } from "../review/proof-summary"; import { renderProofBadgeSvg } from "./proof-badge"; import { ingestBittensorAnchorReport, parseBittensorAnchorReport } from "../review/ledger-anchor-bittensor"; import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence"; @@ -1353,57 +1353,38 @@ export function createApp() { }); // #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. + // standalone endpoints already serve -- no new verification mechanism and no new SQL surface. + // + // Both handlers are thin renderers over ONE resolver (resolveProofPage): the gate, the read and the + // failure outcome are decided there, so the page and the badge cannot disagree about whether a repo is + // published. That is not a stylistic preference -- the gate previously lived inline in both bodies and + // exactly one of them was wired to the per-repo opt-out. + const proofPageDeps = { + loadManifest: loadRepoFocusManifest, + verifyLedger: (env: Env) => verifyDecisionLedger(env), + loadAnchors: (env: Env) => loadPublicLedgerAnchors(env, { limit: 20 }), + }; + app.get("/v1/public/repos/:owner/:repo/proof", async (c) => { - const repoFullName = `${c.req.param("owner")}/${c.req.param("repo")}`; - // The repo's OWN opt-out, loaded before anything else is read: a repo that turned its page off must not - // have its decision records queried to build a summary that will be discarded. - const override = await loadProofPageRepoOverride(c.env, repoFullName, loadRepoFocusManifest); - if (!isProofPageEnabledForRepo(c.env, override)) return c.json({ error: "not_found" }, 404); - 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 { - /* v8 ignore next -- defense in depth: loadProofSummary wraps every section (and the override loader - its own read) in try/catch, so nothing inside currently throws. Kept so a future read added there - without that discipline degrades to 503 rather than a 500 on an unauthenticated public route. */ - return c.json({ error: "unavailable" }, 503); - } + const result = await resolveProofPage(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, proofPageDeps); + if (result.status === "disabled") return c.json({ error: "not_found" }, 404); + c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=300"); + return c.json(result.summary); }); // 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. + // in a badge) is exactly the bare scalar the proof summary refuses to publish. A disabled repo renders + // the neutral badge rather than an error -- a broken image in a README is worse than an honest one. app.get("/v1/public/repos/:owner/:repo/proof-badge.svg", async (c) => { c.header("Content-Type", "image/svg+xml; charset=utf-8"); - const badgeRepoFullName = `${c.req.param("owner")}/${c.req.param("repo")}`; - const badgeOverride = await loadProofPageRepoOverride(c.env, badgeRepoFullName, loadRepoFocusManifest); - if (!isProofPageEnabledForRepo(c.env, badgeOverride)) { + const result = await resolveProofPage(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, proofPageDeps); + if (result.status === "disabled") { c.header("Cache-Control", "public, max-age=300"); return c.body(renderProofBadgeSvg(null), 404); } - try { - const summary = await loadProofSummary(c.env, badgeRepoFullName, { - 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 { - // Same defense-in-depth arm as the proof route above: a README embed gets the neutral badge rather - // than a broken image if a future unguarded read is added. Unreachable today -- every inner read is - // individually fail-safe -- so the whole arm is excluded rather than left as a coverage hole. - /* v8 ignore start */ - c.header("Cache-Control", "public, max-age=300"); - return c.body(renderProofBadgeSvg(null), 503); - /* v8 ignore stop */ - } + c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400"); + return c.body(renderProofBadgeSvg(result.summary)); }); // #9277 (epic #9267): the current tip's SIGNED checkpoint, for the operator's off-Worker Bittensor diff --git a/src/review/proof-summary.ts b/src/review/proof-summary.ts index e0085134f..f1a36b13d 100644 --- a/src/review/proof-summary.ts +++ b/src/review/proof-summary.ts @@ -84,11 +84,11 @@ function round3(value: number): number { * 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 }; + // 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. 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 }; + if (!interval || decided < minimumDecisions) return { state: "insufficient_data", decided: Math.max(0, decided), minimumDecisions }; return { state: "published", accuracy: round3(confirmed / decided), @@ -343,3 +343,48 @@ export async function loadProofSummary( checkedAt, }); } + +/** Everything the proof surfaces read, injected as one bag. The routes bind the real implementations; tests + * 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 }>; + loadAnchors: (env: Env) => Promise<{ anchors: PublicLedgerAnchor[] }>; + now?: (() => string) | undefined; +}; + +/** The two outcomes both proof surfaces share. The JSON route and the badge route render them differently; + * deciding them is not theirs to duplicate. + * + * There is deliberately NO `unavailable` case. `loadProofSummary` is TOTAL: every read it performs is + * wrapped per section, so a failing ledger, anchor or decision-record read degrades to that section's + * honest neutral state and the page still composes. Carrying a 503 outcome would mean carrying a branch + * no input can reach -- dead code that a test can only reach by faking a dependency contract violation, + * which proves nothing about the system. If a future read is ever added outside that per-section + * discipline, the fix is to wrap it there (where the honest-degradation tests live), not to reintroduce a + * catch-all here that would silently turn a partial page into a blank 503. */ +export type ProofPageResult = + | { status: "ok"; summary: ProofSummary } + | { status: "disabled" }; + +/** + * Resolve what a proof surface should serve for one repo: the gate, the read, and the failure outcome. + * + * This exists as ONE function because the alternative already bit us: the gate lived inline in two route + * bodies, and exactly one of them was wired to the per-repo opt-out while the other silently was not. A + * shared resolver makes "the badge and the page agree about whether this repo is published" true by + * construction rather than by two call sites remembering the same thing. + * + * The override is resolved FIRST, so a repo that turned its page off never has its decision records queried + * to build a summary that would be discarded. + */ +export async function resolveProofPage(env: Env, repoFullName: string, deps: ProofPageDeps): Promise { + const override = await loadProofPageRepoOverride(env, repoFullName, deps.loadManifest); + if (!isProofPageEnabledForRepo(env, override)) return { status: "disabled" }; + const summary = await loadProofSummary(env, repoFullName, { + verifyLedger: deps.verifyLedger, + loadAnchors: deps.loadAnchors, + ...(deps.now ? { now: deps.now } : {}), + }); + return { status: "ok", summary }; +} diff --git a/test/unit/proof-summary.test.ts b/test/unit/proof-summary.test.ts index 5fce6e9bf..c14873475 100644 --- a/test/unit/proof-summary.test.ts +++ b/test/unit/proof-summary.test.ts @@ -18,7 +18,7 @@ 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 { loadProofPageRepoOverride } from "../../src/review/proof-summary"; +import { loadProofPageRepoOverride, resolveProofPage, type ProofPageDeps } from "../../src/review/proof-summary"; import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; import type { PublicLedgerAnchor } from "../../src/review/ledger-anchor-persistence"; @@ -370,4 +370,75 @@ describe("loadProofSummary + routes (#9569)", () => { const syncThrow = (() => { throw new Error("d1 down"); }) as unknown as Parameters[2]; expect(await loadProofPageRepoOverride(env, "o/r", syncThrow)).toEqual({ present: false, enabled: false }); }); + + it("resolveProofPage: the gate outcomes, and a TOTAL composition that degrades instead of failing", async () => { + const env = await seeded(); + const deps: ProofPageDeps = { + loadManifest: async () => null, + verifyLedger: async () => ({ ok: true, tipSeq: 3, totalCount: 3 }), + loadAnchors: async () => ({ anchors: [] }), + now: () => CHECKED_AT, + }; + + const ok = await resolveProofPage(env, "o/r", deps); + expect(ok.status).toBe("ok"); + if (ok.status === "ok") expect(ok.summary.decisionCount).toBe(3); + + // Fleet flag off ⇒ disabled. + expect(await resolveProofPage(createTestEnv(), "o/r", deps)).toEqual({ status: "disabled" }); + + // Per-repo opt-out ⇒ disabled even with the fleet flag on. + expect( + await resolveProofPage(env, "o/r", { ...deps, loadManifest: async () => ({ publicProof: { present: true, enabled: false } }) }), + ).toEqual({ status: "disabled" }); + + // REGRESSION: the composition is TOTAL. Every dependency failing at once -- manifest, ledger, anchors, + // and the DB binding itself throwing on property access -- still resolves to a rendered page in its + // honest neutral states, never a rejected promise. This is what makes an `unavailable` outcome + // unnecessary rather than merely untested: there is no input that fails the composition. + const hostile = createTestEnv({ LOOPOVER_PUBLIC_PROOF: "true" }); + Object.defineProperty(hostile, "DB", { + get() { + throw new Error("binding unavailable"); + }, + }); + const degraded = await resolveProofPage(hostile, "o/r", { + loadManifest: async () => { throw new Error("manifest down"); }, + verifyLedger: async () => { throw new Error("ledger down"); }, + loadAnchors: async () => { throw new Error("anchors down"); }, + now: () => CHECKED_AT, + }); + expect(degraded.status).toBe("ok"); + if (degraded.status !== "ok") return; + expect(degraded.summary).toMatchObject({ + decisionCount: 0, + sampleRecords: [], + ledger: { state: "unavailable", checkedAt: CHECKED_AT }, + anchor: { state: "not_yet_anchored" }, + }); + expect(degraded.summary.accuracy).toMatchObject({ state: "insufficient_data", decided: 0 }); + }); + + it("REGRESSION: both surfaces render the SAME resolver outcome, so page and badge cannot disagree", async () => { + const app = createApp(); + // A repo opted out in its manifest: the page 404s and the badge 404s with a neutral SVG, from one + // decision. The gate previously lived inline in both bodies and only one honored the opt-out. + const env = await seeded(); + await upsertRepoFocusManifest(env, "o/r", { publicProof: { enabled: false } } as never); + expect((await app.request("/v1/public/repos/o/r/proof", {}, env)).status).toBe(404); + const badge = await app.request("/v1/public/repos/o/r/proof-badge.svg", {}, env); + expect(badge.status).toBe(404); + expect(badge.headers.get("content-type")).toContain("image/svg+xml"); + expect(await badge.text()).toContain("unavailable"); + + // And with every DB read failing, BOTH still serve 200 over the degraded page rather than erroring. + const broken = await seeded(); + Object.defineProperty(broken, "DB", { + get() { + throw new Error("binding unavailable"); + }, + }); + 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); + }); }); \ No newline at end of file