Split out of #9940, which turned out to be a separate (topological) problem.
The defect
api.loopover.ai publishes an eval-score record for ai_consensus_defect carrying a commitments.corpusChecksum, but /v1/public/eval-corpus?rule_id=ai_consensus_defect serves nothing usable for that rule. The commitment therefore points at bytes no reader can obtain — which is exactly what a commitment must never do.
Reproducible with the public verifier:
$ npx -p @loopover/mcp loopover-verify
SKIP Every corpusChecksum matches a downloadable corpus, and no scored record commits to an empty one
1 commitment(s) published, but no corresponding corpus is downloadable -- nothing could be rehashed
Why this should not be possible
buildPublicCorpusCommitments (src/review/public-eval-corpus.ts) already implements #9805's rule and looks correct on inspection — it omits a rule whose corpus is empty or truncated:
const corpus = await loadPublicEvalCorpus(env, ruleId, nowMs);
if (corpus.caseCount === 0 || corpus.truncated) continue;
commitments.set(ruleId, corpus.checksum);
So the commitment path and the serving route disagree about the same rule. Candidates:
- The two read different windows or different
nowMs, so the builder sees cases the route no longer does.
- The route applies a filter the builder does not (or vice versa) — the provenance exclusion and the rule-history read bound are both places they could diverge.
- The route is failing for a reason that degrades to empty (
loadPublicEvalCorpus is fail-safe by design, so a read error is indistinguishable from a genuinely empty corpus at the call site).
Candidate 3 is the most likely and the most concerning: a failed read silently produces the same result as "no cases", which means a transient D1 problem could publish a commitment while the corpus route serves nothing — the exact state observed.
Acceptance
- A
corpusChecksum is published only when the corpus route can actually serve the bytes it commits to, for the same rule and window.
- The two paths derive that decision from ONE computation rather than two that can disagree.
- If a read failure is why a corpus is empty, that is distinguishable from a genuinely empty corpus at the point the commitment is decided.
loopover-verify reports the corpus claim as PASS against production.
Split out of #9940, which turned out to be a separate (topological) problem.
The defect
api.loopover.aipublishes an eval-score record forai_consensus_defectcarrying acommitments.corpusChecksum, but/v1/public/eval-corpus?rule_id=ai_consensus_defectserves nothing usable for that rule. The commitment therefore points at bytes no reader can obtain — which is exactly what a commitment must never do.Reproducible with the public verifier:
Why this should not be possible
buildPublicCorpusCommitments(src/review/public-eval-corpus.ts) already implements #9805's rule and looks correct on inspection — it omits a rule whose corpus is empty or truncated:So the commitment path and the serving route disagree about the same rule. Candidates:
nowMs, so the builder sees cases the route no longer does.loadPublicEvalCorpusis fail-safe by design, so a read error is indistinguishable from a genuinely empty corpus at the call site).Candidate 3 is the most likely and the most concerning: a failed read silently produces the same result as "no cases", which means a transient D1 problem could publish a commitment while the corpus route serves nothing — the exact state observed.
Acceptance
corpusChecksumis published only when the corpus route can actually serve the bytes it commits to, for the same rule and window.loopover-verifyreports the corpus claim as PASS against production.