fix(ledger): stop one unreconcilable row from hiding every row after it - #9857
Conversation
verifyDecisionLedger returned at the FIRST content_mismatch, so a single row whose preimage no longer matches became a denial-of-verification for the whole chain behind it. Found on a live instance: 83 rows (seq 5-257, 2026-07-26 to 07-27) left by the record-overwriting UPDATE that #9123 replaced with the revision scheme. Zero divergence in the 1,061 rows since -- the write path is fixed and the damage is historical -- but verification stopped at seq 5 and never examined the remaining 1,649 rows. Real tampering at seq 900 would have been invisible behind permanent damage at seq 5, on the one endpoint whose entire purpose is that a skeptic can trust it. Content mismatches now accumulate and the scan continues. That is safe precisely because the chain checks already passed for the row: sequence, predecessor and row_hash all reconciled, so prevHash for the next row is sound. A content mismatch is a statement about one row's preimage. A STRUCTURAL break is not -- a sequence gap or predecessor mismatch makes everything after it unverifiable -- so those still return immediately, and now actually get reached. The verdict is NOT softened: any mismatch is still ok:false, and the first is still reported as . What changes is that the scan finishes and reports contentMismatches, so "one bad row" and "eighty-three bad rows" stop looking identical, and a later structural break is no longer unreachable. Refs #9850
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | 9382d79 | Commit Preview URL Branch Preview URL |
Jul 29 2026, 02:28 PM |
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-29 14:54:52 UTC
Review summary Nits — 5 non-blocking
Concerns raised — review before merging
📋 Copy for AI agents — paste into your coding agentDecision drivers
Context & advisory signals — never blocks the verdict
Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. Scroll preview
A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Bundle ReportChanges will increase total bundle size by 760 bytes (0.01%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9857 +/- ##
==========================================
- Coverage 91.65% 90.77% -0.88%
==========================================
Files 916 916
Lines 112763 112769 +6
Branches 27087 27088 +1
==========================================
- Hits 103348 102363 -985
- Misses 8126 9315 +1189
+ Partials 1289 1091 -198
Flags with carried forward coverage won't be shown. Click here to find out more.
|


Refs #9850
The bug is not what the issue first said
I originally guessed canonicalization drift. Diagnosed against the live database, it is not:
The record is self-consistent and round-trips exactly; the chain row committed a digest for content the record no longer holds. That is
#9123's "compounding bug" — the record-overwritingUPDATEthat the:rev<N>revision scheme replaced.The write path is already fixed. The damage is historical and bounded:
So what actually needed fixing
verifyDecisionLedgerreturned at the first content mismatch. With permanent historical damage at seq 5, that meant:checked: 4out of 1,654 rows/v1/public/decision-ledger/verifypublishesok: falseforeverThe third is the serious one. Real tampering at seq 900 was invisible behind an unfixable row at seq 5 — a denial-of-verification on the one endpoint whose whole purpose is that a skeptic can trust it.
The change
Content mismatches accumulate; the scan continues. This is safe because the chain checks already passed for that row — sequence, predecessor and
row_hashall reconciled before the content check runs, soprevHashfor the next row is sound. A content mismatch is a statement about one row's preimage.A structural break is not: a sequence gap or predecessor mismatch makes everything after it unverifiable. Those still return immediately — and now actually get reached.
The verdict is not softened. Any mismatch is still
ok: false, and the first is still reported asbreak. What changes is that the scan finishes and reportscontentMismatches, so "one bad row" and "eighty-three bad rows" stop looking identical.Tests
Five cases, and I verified all of them fail against the old abort-on-first behaviour (3 fail outright; the other two pin invariants that must hold either way):
checkedcovers the window)ok: falsecontentMismatches: 0on a clean chain, so the field is a fact rather than only an error signalValidation
tsc --noEmitclean; 91 tests across the decision-record, public-ledger-route and anchor-scheduler suites. Every branch and statement I touched inverifyDecisionLedgeris covered — checked against the coverage JSON restricted to the function's line range, not the whole-file figure. OpenAPI description updated andapps/loopover-ui/public/openapi.jsonregenerated in the same commit.Left open deliberately
This does not reconcile the 83 historical rows — their preimages are genuinely gone, and rewriting them would be exactly the tampering the ledger exists to detect. #9850 stays open for that decision: the honest options are to leave
ok: falsestanding with the count visible, or to publish an explicit, auditable boundary the wayprunedRecordsalready does for retention-pruned rows.