Skip to content

fix(fairness): commit only to a corpus a reader can actually download - #9966

Merged
JSONbored merged 1 commit into
mainfrom
fix/corpus-commitment-downloadable-9962
Jul 30, 2026
Merged

fix(fairness): commit only to a corpus a reader can actually download#9966
JSONbored merged 1 commit into
mainfrom
fix/corpus-commitment-downloadable-9962

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

The defect

api.loopover.ai publishes an eval-score record whose commitments.corpusChecksum the public verifier cannot rehash, and the Orb publishes seven of them. #9962 guessed the commitment path and the corpus route disagreed about the same rule. They do not — buildPublicCorpusCommitments is correct, and on api.loopover.ai the published checksum matches the served corpus exactly. There were two other defects, which happened to land on different deployments.

1. The verifier could not reach the corpus at all

loopover-verify fetches /v1/public/eval-corpus?rule_id=…. The route has only ever read ?ruleId=:

$ curl -s "https://api.loopover.ai/v1/public/eval-corpus?rule_id=ai_consensus_defect"
{"error":"rule_id_required"}          # 400

$ curl -s "https://api.loopover.ai/v1/public/eval-corpus?ruleId=ai_consensus_defect" | jq -r .checksum
470e234e8923461e8642807c46265f01393f6dc9e4e40e52363f41767b5c3c10   # == the published commitment

Every corpus fetch 400'd, nothing was ever rehashed, and the tool reported "no corresponding corpus is downloadable" against a deployment that was serving a complete, correct, byte-matching corpus. The rule_id_required error string is what made the snake_case spelling look right.

This survived because both sides were individually tested and individually correct. The route's tests called it with the spelling the route wanted; the CLI's tests stubbed fetch with a fixture map keyed on pathname, throwing the query string away. The one thing that was broken was the one thing neither suite looked at.

2. The commitment was unverifiable wherever reviews actually execute

precision.latestBacktestRun.corpusChecksum took precedence over the per-rule downloadable-corpus checksum. It is wrong three ways at once:

  • Not downloadable. It is checksumCases over the raw BacktestCase[]targetKey, the full metadata bag, full-precision timestamps — while the corpus a reader can fetch is the redacted one. Different inputs, so it could never match, no matter how healthy the deployment. Paired with trust.tier: "reproducible" it claims a reproducibility the artifact cannot support.
  • Global, not per rule. Selected with ORDER BY created_at DESC LIMIT 1 and no rule filter, so one rule's freeze point was stamped onto every record. eval-score-records.ts already warned this would have "every record but one committing to a different rule's cases", calling it latent "only because a single rule clears the publication floor". On a self-host ledger where seven clear it, it stopped being latent — which is exactly the seven-record symptom on the Orb.
  • Unwindowed. That query has no created_at >= ? bound, so the run can predate the windowStart/windowEnd the record goes on to declare.

The hosted Worker has review execution retired and never persists a run, so it never hit this path — which is why the surface looked fine there and broken on the Orb.

The fix

  • The route accepts rule_id as an alias for ruleId. Fixing only the client would leave every already-installed verifier reporting that same false negative against production forever, so the server meets them. The client now sends the canonical spelling, which works against every deployment old and new; the alias is for copies already in the wild, not for us.
  • The downloadable-corpus checksum is the sole commitment source. latestBacktestRun stays published in its own right as the rule-precision block's freeze point (and the fairness page still renders it) — it is only no longer misused as a public, re-derivable commitment. A rule with no downloadable corpus is now omitted rather than rescued into the surface with a commitment no reader can check.
  • loadPublicEvalCorpus reports readFailed. Fail-safe must keep meaning "never 500 an unauthenticated route", but it must stop also meaning "report the deployment's failure as a fact about the rule". A blip and a genuinely quiet rule were byte-identical before this.
  • The omission rule moves into isCommittableCorpus, one pure predicate both paths ask — the "ONE computation" the issue asks for. As an inline condition the read-failure arm was unreachable (a failed read is also an empty one) and therefore untestable; as a predicate over a plain value it can be driven with a degraded corpus that still carries cases, which is what proves the arm is load-bearing.

Tests

test/unit/loopover-verify-against-real-routes.test.ts deliberately owns no fixtures. It points fetch at the real Hono app over a real migrated D1 and lets the real runVerify drive it — its own URL construction, its own parameters, its own recomputation. The only oracle is "did the shipped tool verify the shipped API", so any future disagreement about a parameter name, a response field or a status code fails here.

All four guards were mutation-tested; each mutation fails, and the restored baseline is green:

Mutation Result
Client reverts to rule_id 1 failed
Route drops the rule_id alias 1 failed
isCommittableCorpus ignores readFailed 1 failed
latestBacktestRun precedence restored 6 failed

The first mutation is worth noting: the end-to-end claim still passes, because the route alias covers it. That is the alias working as intended, and it is why there is a separate explicit test that our client sends the canonical spelling — so nothing drifts onto a deprecated alias and quietly makes it load-bearing.

Acceptance criteria

  • A corpusChecksum is published only when the corpus route can serve the bytes it commits to, for the same rule and window.
  • Both paths derive that decision from one computation (isCommittableCorpus over loadPublicEvalCorpus).
  • A read failure is distinguishable from a genuinely empty corpus at the point the commitment is decided (readFailed).
  • loopover-verify reports the corpus claim as PASS — pinned by the real-routes suite. Against production it will pass on the next deploy; it already rehashes correctly today when asked with ?ruleId=, as shown above.

Verification

  • npx tsc --noEmit clean; full static checker sweep (44 checkers) green
  • npx vitest run test/unit1297 files, 25299 tests passed
  • ui:typecheck, ui:test, ui:openapi:check green; openapi.json regenerated and committed

Closes #9962

The published `corpusChecksum` pointed at bytes nobody could obtain, for two
independent reasons that happened to land on different deployments.

The verifier could not reach the corpus at all. `loopover-verify` fetched
`/v1/public/eval-corpus?rule_id=...` while the route has only ever read
`?ruleId=`, so every corpus fetch 400'd and the tool reported "no corresponding
corpus is downloadable" against a deployment serving a complete, correct,
byte-matching corpus. Both sides were individually tested and individually
correct: the route's tests used the route's spelling, and the CLI's tests
stubbed fetch with a fixture map keyed on pathname, which discarded the query
string. The one thing that was broken was the one thing neither suite looked at.

The commitment itself was unverifiable wherever reviews actually execute.
`precision.latestBacktestRun.corpusChecksum` took precedence over the per-rule
downloadable-corpus checksum, and it is wrong three ways: it hashes the RAW
corpus (targetKey, full metadata, full-precision timestamps) while readers can
only download the redacted one, so it could never match; it is selected with
`ORDER BY created_at DESC LIMIT 1` and no rule filter, so one rule's freeze
point was stamped onto every record; and that query has no window bound, so the
run could predate the window the record declares. This module already flagged
the cross-stamping as latent "only because a single rule clears the publication
floor" — on a self-host ledger where seven do, it stopped being latent.

Changes:

- The route accepts `rule_id` as an alias for `ruleId`. Fixing only the client
  would leave every already-published verifier reporting that same false
  negative against production forever, so the server meets them; the client now
  sends the canonical spelling, which works against every deployment.
- The downloadable-corpus checksum is the sole commitment source.
  `latestBacktestRun` remains published in its own right as the rule-precision
  block's freeze point, it is just no longer misused as a public commitment.
- `loadPublicEvalCorpus` reports `readFailed`, so a corpus that is empty because
  a read failed is distinguishable from one whose rule is genuinely quiet.
  Fail-safe must not also mean reporting a deployment's failure as a fact about
  the rule.
- The omission rule moves into `isCommittableCorpus`, one pure predicate both
  paths ask. As an inline condition the read-failure arm was unreachable (a
  failed read is also an empty one) and so untestable; as a predicate over a
  plain value it can be driven with a degraded corpus that still carries cases.

The new suite owns no fixtures: it points `fetch` at the real Hono app over a
real migrated D1 and lets the real `runVerify` drive it, so the only oracle is
whether the shipped tool verifies the shipped API. All four guards were
mutation-tested — reverting the client spelling, dropping the route alias,
ignoring `readFailed`, and restoring the run precedence each fail.

Closes #9962
@loopover-orb

loopover-orb Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 227d880 Commit Preview URL

Branch Preview URL
Jul 30 2026, 10:24 PM

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bundle Report

Changes will decrease total bundle size by 5.37kB (-0.07%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.89MB -5.37kB (-0.07%) ⬇️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-Cn3-k2Ep.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-CHldkZif.js (New) 955.89kB 955.89kB 100.0% 🚀
openapi.json 794 bytes 761.0kB 0.1%
assets/docs.fumadocs-spike-api-reference-BeHxn2p5.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-BcZzOV7o.js (New) 201.69kB 201.69kB 100.0% 🚀
assets/modal-CpC871Sy.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-BnuP5a6G.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-BjItOrfC.js (New) 78.97kB 78.97kB 100.0% 🚀
assets/verify-this-review-DXotzUZh.js (New) 61.77kB 61.77kB 100.0% 🚀
assets/routes-C6L2poST.js (New) 35.88kB 35.88kB 100.0% 🚀
assets/fairness-methodology-B2-axDCa.js (New) 35.63kB 35.63kB 100.0% 🚀
assets/owner-panel-DsLviz5z.js (New) 28.26kB 28.26kB 100.0% 🚀
assets/app-DnW0iQyn.js (New) 25.82kB 25.82kB 100.0% 🚀
assets/ui-vendor-CBfE6GKl.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/fairness-Kfx6qE-5.js (New) 23.13kB 23.13kB 100.0% 🚀
assets/miner-panel-BDUIcW8o.js (New) 20.49kB 20.49kB 100.0% 🚀
assets/api._op-FuSjSq8D.js (New) 17.51kB 17.51kB 100.0% 🚀
assets/self-hosting-docs-audit-B9hanakk.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs-client-loader-CrzJ4-Yg.js (New) 15.51kB 15.51kB 100.0% 🚀
assets/playground-panel-DWMpBLx8.js (New) 14.35kB 14.35kB 100.0% 🚀
assets/app.audit-CvNR1NG3.js (New) 10.22kB 10.22kB 100.0% 🚀
assets/app.config-generator-RjkSBiV2.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-y8m9r30b.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-D35PhzkZ.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-BbePlZQ8.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-114Ytqmn.js (New) 6.74kB 6.74kB 100.0% 🚀
assets/maintainer-workflow-C_MpkGpI.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-CN9Jf05U.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-DcQvU1PH.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-l7wqR6Et.js (New) 6.12kB 6.12kB 100.0% 🚀
assets/docs.index-DDW7arjJ.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/api.index-Chfn0gVQ.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-CbCfGIeT.js (New) 3.01kB 3.01kB 100.0% 🚀
assets/api-CjmrA7Pf.js (New) 2.66kB 2.66kB 100.0% 🚀
assets/docs-page-DM4lVeAK.js (New) 2.14kB 2.14kB 100.0% 🚀
assets/table-DITg3Eqo.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-U_goDqPS.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-CmPKHfj7.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-Co34ebc5.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-BiQBsbpX.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-BuXajLF1.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/docs._slug-Cbqzi6W3.js (New) 549 bytes 549 bytes 100.0% 🚀
assets/app.maintainer-CaH6mQjh.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-CZLRupDI.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-DfOYj5Gp.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-C27U7rWV.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-IPdFI5xm.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-D1xpP5To.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-CdZByVTk.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner--JHy7Fxi.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-CrFQY0tl.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-CGYLtDLO.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/git-pull-request-arrow-DNFwQQUQ.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-IGj9Ant4.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-Ck4X7t_-.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-C-MU-_2p.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-DFi_oL_O.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-CDe8vljg.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/play-C0J-s1-d.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-DVZTDLX4.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/add-scalar-classes-BsT3S3w6.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-B48flLiR.js (Deleted) -955.23kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-CNonBLhT.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-DsnwRhri.js (Deleted) -201.69kB 0 bytes -100.0% 🗑️
assets/modal-0QdFIdeU.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-Bfj9KYug.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-DlmG6PY_.js (Deleted) -78.97kB 0 bytes -100.0% 🗑️
assets/verify-this-review-C8lVDPER.js (Deleted) -65.28kB 0 bytes -100.0% 🗑️
assets/fairness-methodology-D4HZ__bh.js (Deleted) -38.95kB 0 bytes -100.0% 🗑️
assets/routes-BqQ8dqB5.js (Deleted) -35.88kB 0 bytes -100.0% 🗑️
assets/owner-panel-DcBt1J7h.js (Deleted) -28.26kB 0 bytes -100.0% 🗑️
assets/app-CiIkS4xV.js (Deleted) -25.82kB 0 bytes -100.0% 🗑️
assets/ui-vendor-D2ZWTxk3.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/fairness-Z5qAxEDB.js (Deleted) -23.13kB 0 bytes -100.0% 🗑️
assets/miner-panel-C-HN97Mp.js (Deleted) -20.49kB 0 bytes -100.0% 🗑️
assets/api._op-BwntBk01.js (Deleted) -17.51kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-CEnhwuNq.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs-client-loader-IcVy_wmn.js (Deleted) -15.51kB 0 bytes -100.0% 🗑️
assets/playground-panel-BMUY34AY.js (Deleted) -14.35kB 0 bytes -100.0% 🗑️
assets/app.audit-CM_RyKrW.js (Deleted) -10.22kB 0 bytes -100.0% 🗑️
assets/app.config-generator-2IqsOg1d.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-Dytuvh7t.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-C3wR_5rg.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-CX1YVvaQ.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-ApVdZUDD.js (Deleted) -6.74kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-BKo8wAsU.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-B8BV8WoV.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-DleNF0EL.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-DRnM5dOv.js (Deleted) -6.12kB 0 bytes -100.0% 🗑️
assets/docs.index-CPhrqiLE.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/api.index-gPQgnd7E.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BWsfhQZA.js (Deleted) -3.01kB 0 bytes -100.0% 🗑️
assets/api-Dup_2g2l.js (Deleted) -2.66kB 0 bytes -100.0% 🗑️
assets/docs-page-DYTfa3my.js (Deleted) -2.14kB 0 bytes -100.0% 🗑️
assets/table-CPxljitd.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-hju7pOzH.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-XLVGMNMX.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-8tZREoXq.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-DMi6gRrp.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-po9VXxMn.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/docs._slug-w98lxbWi.js (Deleted) -549 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-DhHrzpk9.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-Dq4bB_Zr.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-B6NWfN7K.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-Dq6OUMlg.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-BjmiJYVt.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-CttKMrG4.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-BuphpSed.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-Bcj7Itm4.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-BI8c86Jz.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-BnRXcoeS.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-CAiBHqgb.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-COcnWjQz.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-CdXOih8V.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-C-g_h_Si.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-Bd7Q5i7W.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-DYNQJdVK.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/play-GHwg8fpU.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-BfleiO-W.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️

@JSONbored
JSONbored merged commit 40133d7 into main Jul 30, 2026
7 of 8 checks passed
@JSONbored
JSONbored deleted the fix/corpus-commitment-downloadable-9962 branch July 30, 2026 22:33
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
25880 3 25877 21
View the top 3 failed test(s) by shortest run time
test/integration/public-eval-scores-route.test.ts > GET /v1/public/eval-scores (#9266, epic #8534, spec #9215) > filters by ?subject=
Stack Traces | 0.101s run time
AssertionError: expected [] to have a length of 1 but got +0

- Expected
+ Received

- 1
+ 0

 ❯ test/integration/public-eval-scores-route.test.ts:74:81
test/integration/public-eval-scores-route.test.ts > GET /v1/public/eval-scores (#9266, epic #8534, spec #9215) > filters by ?since= (an unparseable value excludes nothing, fail-open on a malformed optional filter)
Stack Traces | 0.121s run time
AssertionError: expected [] to have a length of 1 but got +0

- Expected
+ Received

- 1
+ 0

 ❯ test/integration/public-eval-scores-route.test.ts:88:82
test/integration/public-eval-scores-route.test.ts > GET /v1/public/eval-scores (#9266, epic #8534, spec #9215) > returns records whose recordDigest is independently recomputable from the record's own content
Stack Traces | 0.151s run time
AssertionError: expected [] to have a length of 1 but got +0

- Expected
+ Received

- 1
+ 0

 ❯ test/integration/public-eval-scores-route.test.ts:56:26

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

JSONbored added a commit that referenced this pull request Jul 31, 2026
…ires (#9974)

main has been red since #9966 (40133d7): every commit after it fails
test/integration/public-eval-scores-route.test.ts with "expected [] to have a
length of 1". Bisected -- the commit before it passes 9/9.

#9966 made the DOWNLOADABLE corpus the only commitment source, deliberately
dropping the persisted-backtest-run fallback: a commitment must name bytes a
reader can fetch and re-hash. Correct change, but this suite's fixture recorded
only the human OVERRIDES, and the corpus is built from rule-FIRED events joined
to them -- so it produced a 0-case corpus, no commitment, and no record.
Confirmed by probing loadPublicEvalCorpus directly: caseCount 0 with overrides
alone, 20 once the fired events are seeded too.

Seeding both halves restores what the fixture always meant to represent: 20
decided cases a reader can download. Changing the expectation to `records: []`
would have gone green while silently dropping the recordDigest-recomputability
check this suite exists for.

The commitment assertion moves off the literal "freeze-point-checksum" (the old
contract) and onto what /v1/public/eval-corpus actually serves, so the two
surfaces cannot drift apart while both still look correct in isolation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fairness: a corpusChecksum is published for a rule whose corpus is not downloadable

1 participant