Skip to content

Commit 7ffb51e

Browse files
feat(proof): public per-repo proof summary, endpoint and README badge (#9569) (#9608)
* 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. * 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. * 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. * 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. * 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. * 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. --------- Co-authored-by: loopover-orb[bot] <296761690+loopover-orb[bot]@users.noreply.github.com>
1 parent e95afc0 commit 7ffb51e

14 files changed

Lines changed: 1136 additions & 1 deletion

File tree

.loopover.yml.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,3 +1493,17 @@ settings:
14931493
# # To stop trusting a peer, remove its key -- that is the whole revocation story, by design.
14941494
# peerKeys:
14951495
# - 0000000000000000000000000000000000000000000000000000000000000000
1496+
1497+
# Public proof page (#9569) — the shareable, unauthenticated per-repo verification page
1498+
# (`/proof/<owner>/<repo>`) and its README badge.
1499+
#
1500+
# OPT-OUT, not opt-in. Every figure the page renders is ALREADY publicly fetchable through the
1501+
# ledger-verify, anchors and decision-record endpoints, so gating a page over data anyone can already
1502+
# curl would add friction without adding privacy. This block exists because a page is nonetheless a
1503+
# different artifact from an API: it is discoverable, linkable, and it markets this repo's numbers.
1504+
#
1505+
# Precedence: the operator's fleet-wide LOOPOVER_PUBLIC_PROOF flag must be on first. This block can turn
1506+
# THIS repo's page OFF; it cannot turn one ON that the operator has not enabled. Omit the block entirely
1507+
# to keep the page on once the operator enables it.
1508+
# publicProof:
1509+
# enabled: false # Bool. Default when the block is absent: true (opt-out).

apps/loopover-ui/public/openapi.json

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26299,6 +26299,82 @@
2629926299
}
2630026300
}
2630126301
}
26302+
},
26303+
"/v1/public/repos/{owner}/{repo}/proof": {
26304+
"get": {
26305+
"operationId": "getPublicRepoProof",
26306+
"tags": [
26307+
"Public"
26308+
],
26309+
"summary": "Public proof summary for one repo — ledger status, anchor, calibration with coverage and interval, sample records",
26310+
"parameters": [
26311+
{
26312+
"schema": {
26313+
"type": "string"
26314+
},
26315+
"required": true,
26316+
"name": "owner",
26317+
"in": "path"
26318+
},
26319+
{
26320+
"schema": {
26321+
"type": "string"
26322+
},
26323+
"required": true,
26324+
"name": "repo",
26325+
"in": "path"
26326+
}
26327+
],
26328+
"responses": {
26329+
"200": {
26330+
"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"
26331+
},
26332+
"404": {
26333+
"description": "The proof page is disabled fleet-wide, or this repo has opted out"
26334+
},
26335+
"503": {
26336+
"description": "Composition failed — no partial or fabricated summary is served"
26337+
}
26338+
}
26339+
}
26340+
},
26341+
"/v1/public/repos/{owner}/{repo}/proof-badge.svg": {
26342+
"get": {
26343+
"operationId": "getPublicRepoProofBadge",
26344+
"tags": [
26345+
"Public"
26346+
],
26347+
"summary": "README badge for the proof page — reports the ledger's state, never a bare accuracy percentage",
26348+
"parameters": [
26349+
{
26350+
"schema": {
26351+
"type": "string"
26352+
},
26353+
"required": true,
26354+
"name": "owner",
26355+
"in": "path"
26356+
},
26357+
{
26358+
"schema": {
26359+
"type": "string"
26360+
},
26361+
"required": true,
26362+
"name": "repo",
26363+
"in": "path"
26364+
}
26365+
],
26366+
"responses": {
26367+
"200": {
26368+
"description": "SVG badge"
26369+
},
26370+
"404": {
26371+
"description": "Disabled or opted out — still an SVG (a neutral 'unavailable' badge), so a README never shows a broken image"
26372+
},
26373+
"503": {
26374+
"description": "Same neutral SVG on an internal error"
26375+
}
26376+
}
26377+
}
2630226378
}
2630326379
},
2630426380
"servers": [

config/examples/loopover.full.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,3 +1507,17 @@ settings:
15071507
# # To stop trusting a peer, remove its key -- that is the whole revocation story, by design.
15081508
# peerKeys:
15091509
# - 0000000000000000000000000000000000000000000000000000000000000000
1510+
1511+
# Public proof page (#9569) — the shareable, unauthenticated per-repo verification page
1512+
# (`/proof/<owner>/<repo>`) and its README badge.
1513+
#
1514+
# OPT-OUT, not opt-in. Every figure the page renders is ALREADY publicly fetchable through the
1515+
# ledger-verify, anchors and decision-record endpoints, so gating a page over data anyone can already
1516+
# curl would add friction without adding privacy. This block exists because a page is nonetheless a
1517+
# different artifact from an API: it is discoverable, linkable, and it markets this repo's numbers.
1518+
#
1519+
# Precedence: the operator's fleet-wide LOOPOVER_PUBLIC_PROOF flag must be on first. This block can turn
1520+
# THIS repo's page OFF; it cannot turn one ON that the operator has not enabled. Omit the block entirely
1521+
# to keep the page on once the operator enables it.
1522+
# publicProof:
1523+
# enabled: false # Bool. Default when the block is absent: true (opt-out).

packages/loopover-engine/src/focus-manifest.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,21 @@ export type FocusManifestPublicStatsConfig = {
440440
enabled: boolean;
441441
};
442442

443+
/**
444+
* Per-repo opt-OUT for the public proof page and its badge (#9569), declared under `publicProof:`.
445+
*
446+
* Shape matches `publicStats:`/`ops:`, but the PRECEDENCE is deliberately different: those are fleet-wide
447+
* and read from the operator's self-repo manifest, whereas this is read from the TARGET repo's own manifest,
448+
* because the thing being opted out of is that repo's own page. Not present ⇒ enabled, once the operator's
449+
* LOOPOVER_PUBLIC_PROOF flag is on — opt-OUT, since every figure the page renders is already publicly
450+
* fetchable through the ledger-verify / anchors / decision-record endpoints. A repo can turn its page off;
451+
* it cannot turn one on that the operator has not enabled.
452+
*/
453+
export type FocusManifestPublicProofConfig = {
454+
present: boolean;
455+
enabled: boolean;
456+
};
457+
443458
/**
444459
* Config-as-code override for the internal, bearer-gated contributor-trust-profile / fairness-analytics
445460
* surface (LOOPOVER_FAIRNESS_ANALYTICS, #fairness-analytics), declared under `fairnessAnalytics:`. Same
@@ -1249,6 +1264,7 @@ export type FocusManifest = {
12491264
maintainerRecap: FocusManifestMaintainerRecapConfig;
12501265
ops: FocusManifestOpsConfig;
12511266
publicStats: FocusManifestPublicStatsConfig;
1267+
publicProof: FocusManifestPublicProofConfig;
12521268
fairnessAnalytics: FocusManifestFairnessAnalyticsConfig;
12531269
draftFlow: FocusManifestDraftFlowConfig;
12541270
upstreamDriftIssues: FocusManifestUpstreamDriftIssuesConfig;
@@ -1413,6 +1429,13 @@ const EMPTY_PUBLIC_STATS_CONFIG: FocusManifestPublicStatsConfig = {
14131429
enabled: false,
14141430
};
14151431

1432+
/** #9569: absent means ENABLED at the resolver (opt-out), so `enabled:false` here is only the shape's
1433+
* default — `present:false` is what the resolver actually keys on. */
1434+
const EMPTY_PUBLIC_PROOF_CONFIG: FocusManifestPublicProofConfig = {
1435+
present: false,
1436+
enabled: false,
1437+
};
1438+
14161439
const EMPTY_FAIRNESS_ANALYTICS_CONFIG: FocusManifestFairnessAnalyticsConfig = {
14171440
present: false,
14181441
enabled: false,
@@ -1478,6 +1501,7 @@ const EMPTY_MANIFEST: FocusManifest = {
14781501
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
14791502
ops: { ...EMPTY_OPS_CONFIG },
14801503
publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG },
1504+
publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG },
14811505
fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG },
14821506
draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG },
14831507
upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG },
@@ -1520,6 +1544,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo
15201544
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
15211545
ops: { ...EMPTY_OPS_CONFIG },
15221546
publicStats: { ...EMPTY_PUBLIC_STATS_CONFIG },
1547+
publicProof: { ...EMPTY_PUBLIC_PROOF_CONFIG },
15231548
fairnessAnalytics: { ...EMPTY_FAIRNESS_ANALYTICS_CONFIG },
15241549
draftFlow: { ...EMPTY_DRAFT_FLOW_CONFIG },
15251550
upstreamDriftIssues: { ...EMPTY_UPSTREAM_DRIFT_ISSUES_CONFIG },
@@ -2356,6 +2381,24 @@ export function publicStatsConfigToJson(config: FocusManifestPublicStatsConfig):
23562381
return { enabled: config.enabled };
23572382
}
23582383

2384+
/** Parse the optional `publicProof:` mapping (#9569). Mirrors {@link parsePublicStatsConfig} exactly. */
2385+
function parsePublicProofConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestPublicProofConfig {
2386+
if (value === undefined || value === null) return { ...EMPTY_PUBLIC_PROOF_CONFIG };
2387+
if (typeof value !== "object" || Array.isArray(value)) {
2388+
warnings.push('Manifest field "publicProof" must be a mapping; ignoring it.');
2389+
return { ...EMPTY_PUBLIC_PROOF_CONFIG };
2390+
}
2391+
const record = value as Record<string, JsonValue>;
2392+
const enabled = normalizeOptionalBoolean(record.enabled, "publicProof.enabled", warnings) ?? false;
2393+
return { present: true, enabled };
2394+
}
2395+
2396+
/** Serialize a publicProof config so a cached snapshot round-trips through {@link parsePublicProofConfig}. */
2397+
export function publicProofConfigToJson(config: FocusManifestPublicProofConfig): JsonValue {
2398+
if (!config.present) return null;
2399+
return { enabled: config.enabled };
2400+
}
2401+
23592402
/** Parse the optional `fairnessAnalytics:` mapping (#fairness-analytics). Mirrors {@link parsePublicStatsConfig}
23602403
* exactly -- the only field is `enabled`, no DB layer to overlay onto. */
23612404
function parseFairnessAnalyticsConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestFairnessAnalyticsConfig {
@@ -4247,6 +4290,7 @@ export const FOCUS_MANIFEST_TOP_LEVEL_FIELDS = [
42474290
"maintainerRecap",
42484291
"ops",
42494292
"publicStats",
4293+
"publicProof",
42504294
"draftFlow",
42514295
"upstreamDriftIssues",
42524296
"sweepWatchdog",
@@ -4326,6 +4370,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
43264370
maintainerRecap: parseMaintainerRecapConfig(record.maintainerRecap, warnings),
43274371
ops: parseOpsConfig(record.ops, warnings),
43284372
publicStats: parsePublicStatsConfig(record.publicStats, warnings),
4373+
publicProof: parsePublicProofConfig(record.publicProof, warnings),
43294374
fairnessAnalytics: parseFairnessAnalyticsConfig(record.fairnessAnalytics, warnings),
43304375
draftFlow: parseDraftFlowConfig(record.draftFlow, warnings),
43314376
upstreamDriftIssues: parseUpstreamDriftIssuesConfig(record.upstreamDriftIssues, warnings),

src/api/proof-badge.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// README badge for the public proof page (#9569). Renders through the SAME flat-badge primitive and the
2+
// SAME XML escaping as the repo-quality badge (`./badge.ts`), so an unauthenticated, embeddable surface
3+
// cannot become an injection vector and the two badges cannot drift apart visually.
4+
import { escapeXml } from "./badge";
5+
import { buildProofBadgeColor, buildProofBadgeMessage, type ProofSummary } from "../review/proof-summary";
6+
7+
export const PROOF_BADGE_LABEL = "loopover proof";
8+
const UNAVAILABLE_COLOR = "#9e9e9e";
9+
10+
/** `null` renders the neutral unavailable badge — used for both the flag-off 404 and the error 503, since
11+
* from a README's point of view those are the same thing: no claim is being made right now. */
12+
export function renderProofBadgeSvg(summary: ProofSummary | null): string {
13+
const message = summary ? buildProofBadgeMessage(summary) : "unavailable";
14+
const color = summary ? buildProofBadgeColor(summary) : UNAVAILABLE_COLOR;
15+
return renderFlatBadge(PROOF_BADGE_LABEL, message, color);
16+
}
17+
18+
function renderFlatBadge(label: string, message: string, color: string): string {
19+
const labelText = escapeXml(label);
20+
const messageText = escapeXml(message);
21+
const labelWidth = textWidth(label);
22+
const messageWidth = textWidth(message);
23+
const totalWidth = labelWidth + messageWidth;
24+
return [
25+
`<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${labelText}: ${messageText}">`,
26+
`<title>${labelText}: ${messageText}</title>`,
27+
`<rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>`,
28+
`<rect width="${labelWidth}" height="20" rx="3" fill="#24292f"/>`,
29+
`<rect x="${labelWidth}" width="${messageWidth}" height="20" rx="3" fill="${escapeXml(color)}"/>`,
30+
`<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">`,
31+
`<text x="${labelWidth / 2}" y="14">${labelText}</text>`,
32+
`<text x="${labelWidth + messageWidth / 2}" y="14">${messageText}</text>`,
33+
`</g></svg>`,
34+
].join("");
35+
}
36+
37+
function textWidth(text: string): number {
38+
return Math.max(40, Math.round(text.length * 6.5) + 10);
39+
}

src/api/routes.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ import { isRagEnabled } from "../review/rag-wire";
306306
import { loadDecisionLedgerTip, loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record";
307307
import { buildEvalScoreRecordsFromRulePrecision, filterEvalScoreRecords } from "../review/eval-score-records";
308308
import { anchorSigningInput, buildLedgerAnchorPayload, currentAnchorKey, parseAnchorPublicKeys, publicAnchorStatus, signLedgerAnchorPayload } from "../review/ledger-anchor";
309+
import { resolveProofPage } from "../review/proof-summary";
310+
import { renderProofBadgeSvg } from "./proof-badge";
309311
import { ingestBittensorAnchorReport, parseBittensorAnchorReport } from "../review/ledger-anchor-bittensor";
310312
import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence";
311313
import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats";
@@ -1366,6 +1368,41 @@ export function createApp() {
13661368
});
13671369
});
13681370

1371+
// #9569: the public proof page's data, and its README badge. Read-only over the SAME public sources the
1372+
// standalone endpoints already serve -- no new verification mechanism and no new SQL surface.
1373+
//
1374+
// Both handlers are thin renderers over ONE resolver (resolveProofPage): the gate, the read and the
1375+
// failure outcome are decided there, so the page and the badge cannot disagree about whether a repo is
1376+
// published. That is not a stylistic preference -- the gate previously lived inline in both bodies and
1377+
// exactly one of them was wired to the per-repo opt-out.
1378+
const proofPageDeps = {
1379+
loadManifest: loadRepoFocusManifest,
1380+
verifyLedger: (env: Env) => verifyDecisionLedger(env),
1381+
loadAnchors: (env: Env) => loadPublicLedgerAnchors(env, { limit: 20 }),
1382+
};
1383+
1384+
app.get("/v1/public/repos/:owner/:repo/proof", async (c) => {
1385+
const result = await resolveProofPage(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, proofPageDeps);
1386+
if (result.status === "disabled") return c.json({ error: "not_found" }, 404);
1387+
c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=300");
1388+
return c.json(result.summary);
1389+
});
1390+
1391+
// The badge deliberately reports the LEDGER's state rather than an accuracy percentage: a badge is a
1392+
// one-glance claim, and an accuracy number without the interval that makes it honest (which does not fit
1393+
// in a badge) is exactly the bare scalar the proof summary refuses to publish. A disabled repo renders
1394+
// the neutral badge rather than an error -- a broken image in a README is worse than an honest one.
1395+
app.get("/v1/public/repos/:owner/:repo/proof-badge.svg", async (c) => {
1396+
c.header("Content-Type", "image/svg+xml; charset=utf-8");
1397+
const result = await resolveProofPage(c.env, `${c.req.param("owner")}/${c.req.param("repo")}`, proofPageDeps);
1398+
if (result.status === "disabled") {
1399+
c.header("Cache-Control", "public, max-age=300");
1400+
return c.body(renderProofBadgeSvg(null), 404);
1401+
}
1402+
c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400");
1403+
return c.body(renderProofBadgeSvg(result.summary));
1404+
});
1405+
13691406
// #9277 (epic #9267): the current tip's SIGNED checkpoint, for the operator's off-Worker Bittensor
13701407
// commitment submitter to fetch and commit on-chain (sha256 of `signingInput` is the exact 32 bytes
13711408
// `Data::Sha256` holds). Unauthenticated like every /v1/public/* sibling: it is the same payload the

src/auth/route-auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export function requiresApiToken(path: string): boolean {
4848
if (path === "/v1/public/decision-ledger/anchor-key") return false;
4949
// #9271: the public anchor-attempt listing, added in the SAME PR as its route.
5050
if (path === "/v1/public/decision-ledger/anchors") return false;
51+
// #9569: the public proof page's data and its README badge. Unauthenticated by design -- every figure
52+
// they render is already served unauthenticated by the ledger-verify / anchors / decision-record routes
53+
// above; this is a composition, not a new disclosure. Added in the SAME PR as the routes, per #9120.
54+
if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/proof$/.test(path)) return false;
55+
if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/proof-badge\.svg$/.test(path)) return false;
5156
// #9277: the current tip's signed checkpoint, for the operator's off-Worker Bittensor submitter (and
5257
// anyone else — it is the same payload the Rekor/git backends already publish externally). Added in the
5358
// SAME PR as its route, per the #9120 lesson.

src/env.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,10 @@ declare global {
609609
* worker is byte-identical to today. Exposes review-disposition counts + a reversal-grounded accuracy
610610
* percentage + an estimated-time-saved figure ONLY — never PR content, authors, scores, or reward internals.
611611
* See review/public-stats.ts. */
612+
/** #9569: fleet-wide switch for the public proof page (`/proof/:owner/:repo`) and its badge. Default
613+
* OFF like every sibling public surface. A repo can opt OUT via its own manifest, but cannot opt IN
614+
* when this is off -- see isProofPageEnabledForRepo's recorded decision in review/proof-summary.ts. */
615+
LOOPOVER_PUBLIC_PROOF?: string;
612616
LOOPOVER_PUBLIC_STATS?: string;
613617
/** Proof of Power (#1059): comma-separated allowlist of repo full-names ("owner/repo") whose OWN historical
614618
* review ledger (audit_events "published a review surface" + pull_requests terminal state) counts toward

src/openapi/spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,32 @@ export function buildOpenApiSpec() {
19091909
200: { description: "{ anchors: [{ id, seq, rowHash, keyId, backend, backendRef, status, error, createdAt }], nextBefore, status } — a failed attempt is returned identically to a successful one, never filtered out or reshaped. The top-level `status` (anchored | empty_ledger | unconfigured | pending) says why the list looks as it does, so an empty list cannot be mistaken for a healthy one; it is omitted when a backend/before filter is applied, where empty just means none matched" },
19101910
},
19111911
});
1912+
registry.registerPath({
1913+
method: "get",
1914+
path: "/v1/public/repos/{owner}/{repo}/proof",
1915+
operationId: "getPublicRepoProof",
1916+
tags: ["Public"],
1917+
summary: "Public proof summary for one repo — ledger status, anchor, calibration with coverage and interval, sample records",
1918+
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
1919+
responses: {
1920+
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" },
1921+
404: { description: "The proof page is disabled fleet-wide, or this repo has opted out" },
1922+
503: { description: "Composition failed — no partial or fabricated summary is served" },
1923+
},
1924+
});
1925+
registry.registerPath({
1926+
method: "get",
1927+
path: "/v1/public/repos/{owner}/{repo}/proof-badge.svg",
1928+
operationId: "getPublicRepoProofBadge",
1929+
tags: ["Public"],
1930+
summary: "README badge for the proof page — reports the ledger's state, never a bare accuracy percentage",
1931+
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
1932+
responses: {
1933+
200: { description: "SVG badge" },
1934+
404: { description: "Disabled or opted out — still an SVG (a neutral 'unavailable' badge), so a README never shows a broken image" },
1935+
503: { description: "Same neutral SVG on an internal error" },
1936+
},
1937+
});
19121938
registry.registerPath({
19131939
method: "get",
19141940
path: "/v1/public/decision-ledger/anchor-payload",

0 commit comments

Comments
 (0)