Skip to content

Commit d1ca88e

Browse files
committed
fix(review): stop publishing a reproducibility freeze point that commits to an empty corpus
`/v1/public/stats` reported a `latestBacktestRun.corpusChecksum` of 4f53cda1…, which is `checksumCases([])` — SHA-256 over the canonicalized empty case list. The fairness page rendered it as a "Reproducibility freeze point", and `/v1/public/eval-scores` published two records committed to it with `trust.tier: "reproducible"` alongside decided=460/confirmed=287. A hash over zero cases is the same 32 bytes for every rule, every window and every deployment, so it points at nothing a skeptic can re-derive anything from. Treat it the same way as a missing run: `loadPublicRulePrecision` now reports `latestBacktestRun: null` for it, which clears the freeze point from the fairness page and, through the builder's existing guard, empties the eval-scores response. The record builder additionally refuses the value on its own, since it is the code that stamps the `reproducible` tier and should not depend on its caller to be honest. The scores are unaffected — they come from a different dataset (`signal.human_override:*` audit events), so an empty corpus never means the numbers are zero, only that they are uncommitted, which is exactly the state the record spec says must not be published. Docs: the walkthrough's step 4 referenced a bare `/v1/public/stats`, which resolves against loopover.ai and 404s — only the API host serves it. Fetch it absolutely, matching the convention in what-you-can-verify.mdx. Step 1 and the track-record command shell out to `wrangler d1 execute --remote` against the deployment's own database, so they need that deployment's Cloudflare credentials and are not runnable by a third party; each step is now marked with who can actually run it instead of the page claiming nothing needs a key. The freeze point covers whichever single rule the latest run backtested, not every rule on the report, and is described that way now.
1 parent 61ab43e commit d1ca88e

6 files changed

Lines changed: 131 additions & 19 deletions

File tree

apps/loopover-ui/content/docs/verify-this-review.mdx

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,27 @@ website only build trust if a skeptic can check them without asking anyone's per
1111
page is the end-to-end walkthrough: export the same corpus snapshot the numbers come from, verify
1212
its checksum, replay the same scorer over it, and compare what you get against what is published.
1313

14-
Everything below runs read-only against a database export and pure functions from
15-
`@loopover/engine`. Nothing posts anywhere, nothing needs an API key.
14+
Everything below runs read-only against a corpus export and pure functions from `@loopover/engine`.
15+
Nothing posts anywhere, and nothing needs a LoopOver API key.
16+
17+
<Callout variant="warn">
18+
**Step 1 is not yet runnable by a stranger.** Exporting the corpus reads the deployment's own
19+
database — with `--remote` that means the operator's Cloudflare D1, which needs *their* Cloudflare
20+
credentials, not a LoopOver key. So today steps 1–3 are reproducible by an operator or by anyone
21+
running a self-host deployment against their own data, and step 4 is the part a third party can
22+
check unauthenticated. No anonymous corpus download exists yet; until one does, this page marks
23+
each step with who can actually run it rather than implying everyone can run all of them.
24+
</Callout>
1625

1726
For the wider contract — every claim, its artifact, and its trust assumption, including the ones you
1827
cannot check — see [what you can verify](/docs/what-you-can-verify).
1928

20-
## 1. Export the corpus snapshot
29+
## 1. Export the corpus snapshot *(operator / self-host)*
2130

2231
Every rule's fired/override history exports as a versioned, checksummed JSON snapshot
23-
([backtest &amp; calibration](/docs/backtest-calibration) explains how that history is recorded):
32+
([backtest &amp; calibration](/docs/backtest-calibration) explains how that history is recorded).
33+
`--remote` shells out to `wrangler d1 execute … --remote`, so it reads the deployed database and
34+
requires that deployment's own Cloudflare credentials:
2435

2536
```bash
2637
npx tsx scripts/backtest-corpus-export.ts --rule-id linked_issue_scope_mismatch --output corpus.json --remote
@@ -34,10 +45,12 @@ npx tsx scripts/backtest-corpus-export.ts --rule-id linked_issue_scope_mismatch
3445

3546
The snapshot's `checksum` field is a SHA-256 over the canonicalized cases (keys sorted, so
3647
property order can never change the hash). The fairness report's *reproducibility freeze point*
37-
shows the checksum of the corpus behind the latest persisted backtest run — an export of the same
38-
window reproduces the same checksum, byte for byte.
48+
shows that checksum for the **most recent persisted backtest run**, whichever single rule that run
49+
covered — re-exporting that rule over the same window reproduces the same checksum, byte for byte.
50+
It is one run's freeze point, not a per-rule commitment for every rule on the report, so compare it
51+
against an export of the rule the run actually covered.
3952

40-
## 2. Verify the checksum
53+
## 2. Verify the checksum *(anyone, given a snapshot)*
4154

4255
The manifest is self-verifying: recompute the hash over its own `cases` array and compare it to
4356
the recorded `checksum`. The canonicalization lives in `scripts/backtest-corpus-export-core.ts`
@@ -53,7 +66,7 @@ console.log(recomputed.checksum === saved.checksum ? "checksum OK" : "CHECKSUM M
5366
'
5467
```
5568

56-
## 3. Replay the scorer
69+
## 3. Replay the scorer *(anyone, given a snapshot)*
5770

5871
The published precision comes from the same pure functions any Node script can import:
5972
`scoreBacktest` replays a classifier over the labeled cases, and `compareBacktestScores` applies
@@ -75,19 +88,42 @@ console.log(report);
7588
- **`null` is never `0`.** Precision and recall stay `null` below the decided-sample floor;
7689
the fairness report renders that as *insufficient data*, never as a zero.
7790

78-
## 4. Compare against the published numbers
91+
## 4. Compare against the published numbers *(anyone)*
92+
93+
The [fairness report](/fairness) renders each rule's decided-case count and measured precision from
94+
the public stats endpoint's `rulePrecision` block. That endpoint is served by the API host, not by
95+
this site, so fetch it absolutely — a bare `/v1/public/stats` resolves against `loopover.ai` and
96+
404s:
97+
98+
```bash
99+
curl -s "https://api.loopover.ai/v1/public/stats" | jq '.rulePrecision'
100+
```
101+
102+
The same per-rule numbers are also published as digest-committed
103+
[EvalScoreRecords](/docs/what-you-can-verify), each independently re-derivable without trusting the
104+
transport — recompute `recordDigest` over the record's own remaining fields and compare:
105+
106+
```bash
107+
curl -s "https://api.loopover.ai/v1/public/eval-scores" | jq '.records'
108+
```
109+
110+
That array is empty whenever the latest backtest run's corpus is empty. A checksum over zero cases
111+
is byte-identical for every rule and every window, so it commits to nothing a reader could re-derive
112+
the scores from — publishing a record against it would assert a reproducibility that does not exist.
113+
An empty `records` array therefore means *the numbers are not currently committed to a corpus*, never
114+
that the numbers are zero.
79115

80-
The [fairness report](/fairness) renders each rule's decided-case count and measured precision
81-
from the public stats endpoint (`/v1/public/stats`, the `rulePrecision` block). The aggregated
82-
run history is also readable directly:
116+
The aggregated run history is readable directly too, again against the deployment's own database
117+
(operator / self-host):
83118

84119
```bash
85120
npx tsx scripts/backtest-track-record.ts --db loopover --remote
86121
```
87122

88123
Your replayed `confirmed / decided` for a rule should match the published precision for the same
89-
window; the freeze-point checksum ties the published numbers to the exact corpus you just
90-
verified.
124+
window. The freeze-point checksum ties the published numbers to the corpus you just verified only
125+
for the rule the latest run actually backtested — for any other rule it is a timestamped pointer to
126+
a different run, not a commitment to that rule's own cases.
91127

92128
## 5. Verify an attested run (when a run carries one)
93129

apps/loopover-ui/content/docs/what-you-can-verify.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,10 @@ replayable history — not hand-entered numbers.
176176

177177
This is the walkthrough in [Verify this review](/docs/verify-this-review): export the checksummed
178178
corpus, verify its checksum, re-run the same public scoring functions from `@loopover/engine`, and
179-
compare. **Anyone** can do this for public repositories.
179+
compare. The checksum verification and the scorer replay are pure functions **anyone** can run over
180+
a snapshot they hold, and the published numbers themselves are fetchable unauthenticated — but the
181+
export step that produces the snapshot reads the deployment's own database and needs that
182+
deployment's credentials, so it is an operator / self-host step today rather than an anonymous one.
180183

181184
<Callout variant="warn">
182185
**Private repositories are the exception.** A hosted tenant's review history cannot be published

src/review/eval-score-records.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// It adds no new scoring and no new trust -- the numbers are the same ones `/v1/public/stats` already
55
// publishes, just committed to a corpus checksum and made independently re-derivable per-record.
66
import { canonicalJson, contentDigest } from "./decision-record";
7-
import type { PublicRulePrecision } from "./public-rule-precision";
7+
import { EMPTY_CORPUS_CHECKSUM, type PublicRulePrecision } from "./public-rule-precision";
88

99
export const EVAL_SCORE_RECORD_SCHEMA_VERSION = 1 as const;
1010

@@ -76,6 +76,14 @@ async function finalizeRecord(input: EvalScoreRecordDigestInput): Promise<EvalSc
7676
* a record whose commitments cannot be independently re-derived (no corpus checksum to point at) is not
7777
* publishable, so this deliberately emits nothing rather than a record with a placeholder commitment.
7878
*
79+
* Also returns an empty array when the run's checksum is {@link EMPTY_CORPUS_CHECKSUM}. A hash over zero
80+
* cases is the same 32 bytes for every rule, every window, and every deployment, so it points at nothing a
81+
* consumer could re-derive the scores from -- it is a placeholder commitment wearing a real hash's clothes,
82+
* and pairing it with a `reproducible` trust tier claims a reproducibility the artifact cannot support. The
83+
* scores themselves come from a different dataset (live human-override events) and are unaffected by whether
84+
* a corpus was exported, so an empty corpus never means the numbers are zero -- it means they are
85+
* uncommitted, which is exactly the state #9215 says must not be published.
86+
*
7987
* `recall` and `abstained` do not apply to this work-unit kind: ORB's gate rules fire deterministically (no
8088
* agent choosing to abstain) and this data measures precision, not a false-negative rate. `recall` is
8189
* `null` (genuinely inapplicable, never a misleading `0`); `abstained` is `0` (there is no abstention
@@ -85,6 +93,7 @@ async function finalizeRecord(input: EvalScoreRecordDigestInput): Promise<EvalSc
8593
export async function buildEvalScoreRecordsFromRulePrecision(precision: PublicRulePrecision, issuedAt: string): Promise<EvalScoreRecord[]> {
8694
if (!precision.latestBacktestRun) return [];
8795
const { corpusChecksum } = precision.latestBacktestRun;
96+
if (corpusChecksum === EMPTY_CORPUS_CHECKSUM) return [];
8897
const windowStart = new Date(Date.parse(issuedAt) - precision.windowDays * 24 * 60 * 60 * 1000).toISOString();
8998

9099
const records = await Promise.all(
@@ -145,3 +154,4 @@ export async function verifyEvalScoreRecordDigest(record: EvalScoreRecord): Prom
145154
// Re-exported so callers that only import this module never need a second import from decision-record.ts
146155
// just to canonicalize something alongside a record (e.g. logging, or a future signed-bundle wrapper).
147156
export { canonicalJson, contentDigest };
157+
export { EMPTY_CORPUS_CHECKSUM };

src/review/public-rule-precision.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ export const PUBLIC_PRECISION_MIN_DECIDED = 10;
2323
// duplication rule-calibration-trend.ts documents for its identical queries.
2424
const HUMAN_OVERRIDE_EVENT_TYPE_PREFIX = "signal.human_override:";
2525

26+
/** `checksumCases([])` — SHA-256 over the canonicalized empty case list (the two-byte string `"[]"`), i.e. what
27+
* a corpus export produces for a rule with no labeled cases at all. A hash over zero cases is the same 32
28+
* bytes for every rule, every window and every deployment, so it is not the "independently-verifiable freeze
29+
* point" {@link PublicRulePrecision.latestBacktestRun} claims to be — it points at nothing a skeptic could
30+
* re-derive anything from. Hard-coded because the canonicalization that produces it
31+
* (`scripts/backtest-corpus-export-core.ts`) runs on `node:crypto` and is unimportable from the Workers
32+
* runtime; this module's own test re-derives it from `sha256Hex("[]")` so it can never drift. */
33+
export const EMPTY_CORPUS_CHECKSUM = "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945";
34+
2635
export type PublicRulePrecisionRow = {
2736
ruleId: string;
2837
decided: number;
@@ -40,7 +49,9 @@ export type PublicRulePrecision = {
4049
/** All three reversal shapes counted over the window — the "counted against ourselves" number. */
4150
reversals: { reopened: number; reverted: number; superseded: number };
4251
/** The latest persisted backtest run carrying a corpus checksum — the independently-verifiable freeze
43-
* point — or null when no run has been recorded yet. */
52+
* point — or null when no run has been recorded yet, or when the latest run's corpus was empty (see
53+
* {@link EMPTY_CORPUS_CHECKSUM}: a commitment to nothing is not a freeze point, so it is reported as
54+
* absent rather than published as though it were verifiable). */
4455
latestBacktestRun: { corpusChecksum: string; at: string } | null;
4556
};
4657

@@ -102,6 +113,9 @@ export async function loadPublicRulePrecision(env: Env, nowMs: number = Date.now
102113
reverted: reversalCount("reversal_reverted"),
103114
superseded: reversalCount("reversal_superseded"),
104115
},
105-
latestBacktestRun: latest && typeof latest.checksum === "string" && latest.checksum !== "" ? { corpusChecksum: latest.checksum, at: latest.created_at } : null,
116+
latestBacktestRun:
117+
latest && typeof latest.checksum === "string" && latest.checksum !== "" && latest.checksum !== EMPTY_CORPUS_CHECKSUM
118+
? { corpusChecksum: latest.checksum, at: latest.created_at }
119+
: null,
106120
};
107121
}

test/unit/eval-score-records.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
ORB_GATE_SUBJECT_ID,
77
OUTCOME_CONFIRMED_PRECISION_SCORING_RULE_VERSION,
88
verifyEvalScoreRecordDigest,
9+
EMPTY_CORPUS_CHECKSUM,
910
type EvalScoreRecord,
1011
} from "../../src/review/eval-score-records";
11-
import { contentDigest } from "../../src/review/decision-record";
12+
import { contentDigest, sha256Hex } from "../../src/review/decision-record";
1213
import type { PublicRulePrecision } from "../../src/review/public-rule-precision";
1314

1415
const ISSUED_AT = "2026-07-27T12:00:00.000Z";
@@ -29,6 +30,25 @@ describe("buildEvalScoreRecordsFromRulePrecision (#9266)", () => {
2930
expect(records).toEqual([]);
3031
});
3132

33+
it("refuses to publish records whose freeze point commits to an empty corpus", async () => {
34+
// Regression: production published decided=460/confirmed=287 alongside sha256("[]") -- a hash that is
35+
// byte-identical for every rule and every window, so it committed to nothing a consumer could re-derive.
36+
const records = await buildEvalScoreRecordsFromRulePrecision(
37+
{ ...PRECISION_WITH_FREEZE_POINT, latestBacktestRun: { corpusChecksum: EMPTY_CORPUS_CHECKSUM, at: "2026-07-27T10:00:00.000Z" } },
38+
ISSUED_AT,
39+
);
40+
expect(records).toEqual([]);
41+
});
42+
43+
it("EMPTY_CORPUS_CHECKSUM is the exporter's own checksum over zero cases", async () => {
44+
// scripts/backtest-corpus-export-core.ts hashes `JSON.stringify(cases.map(canonicalizeCase))`, which for
45+
// an empty list is the two-byte string "[]" -- re-derived here so the hard-coded constant cannot drift
46+
// from the exporter that produces the value it guards against. (That exporter's canonicalization and
47+
// canonicalJson coincide ONLY on the empty case, so this hashes the literal preimage rather than
48+
// round-tripping [] through either one.)
49+
expect(EMPTY_CORPUS_CHECKSUM).toBe(await sha256Hex("[]"));
50+
});
51+
3252
it("builds one record per rule, committed to the freeze point's corpus checksum", async () => {
3353
const records = await buildEvalScoreRecordsFromRulePrecision(PRECISION_WITH_FREEZE_POINT, ISSUED_AT);
3454
expect(records).toHaveLength(2);

test/unit/public-rule-precision.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
loadPublicRulePrecision,
44
PUBLIC_PRECISION_MIN_DECIDED,
55
PUBLIC_PRECISION_WINDOW_DAYS,
6+
EMPTY_CORPUS_CHECKSUM,
67
} from "../../src/review/public-rule-precision";
8+
import { sha256Hex } from "../../src/review/decision-record";
79
import { recordAuditEvent } from "../../src/db/repositories";
810
import { createSignalStore } from "../../src/review/signal-tracking-wire";
911
import { createTestEnv } from "../helpers/d1";
@@ -95,6 +97,33 @@ describe("loadPublicRulePrecision (#8230)", () => {
9597
expect(block.latestBacktestRun).toEqual({ corpusChecksum: "newest111", at: new Date(NOW - 30_000).toISOString() });
9698
});
9799

100+
it("reports no freeze point when the latest run's corpus was empty", async () => {
101+
// Regression: production's latest run carried sha256("[]") -- the checksum of ZERO cases -- and the
102+
// fairness page rendered it as a "reproducibility freeze point" while /v1/public/eval-scores published
103+
// records committed to it. A hash over no cases is identical everywhere, so it verifies nothing.
104+
const env = createTestEnv();
105+
await seedVerdicts(env, "ai_consensus_defect", 15, 5);
106+
await recordAuditEvent(env, {
107+
eventType: "calibration.logic_backtest_run",
108+
targetKey: "rule",
109+
outcome: "completed",
110+
metadata: { corpusChecksum: EMPTY_CORPUS_CHECKSUM, comparison: {} },
111+
createdAt: new Date(NOW - 1000).toISOString(),
112+
});
113+
114+
const block = await loadPublicRulePrecision(env, NOW);
115+
expect(block.latestBacktestRun).toBeNull();
116+
// The scores come from a different dataset (human-override events) and are unaffected -- an empty corpus
117+
// means the numbers are uncommitted, never that they are zero.
118+
expect(block.rules).toEqual([{ ruleId: "ai_consensus_defect", decided: 20, confirmed: 15, precision: 0.75 }]);
119+
});
120+
121+
it("EMPTY_CORPUS_CHECKSUM is the exporter's own checksum over zero cases", async () => {
122+
// scripts/backtest-corpus-export-core.ts hashes `JSON.stringify(cases.map(canonicalizeCase))`, which for
123+
// an empty list is the two-byte string "[]" -- re-derived here so the constant cannot drift from it.
124+
expect(EMPTY_CORPUS_CHECKSUM).toBe(await sha256Hex("[]"));
125+
});
126+
98127
it("degrades fail-safe on a broken store and reports null freeze point on a fresh ledger", async () => {
99128
const empty = await loadPublicRulePrecision(createTestEnv(), NOW);
100129
expect(empty).toEqual({

0 commit comments

Comments
 (0)