UPDATE an entry
There is no code path that issues an UPDATE against ledger_entries. A wrong row stays wrong and a new row says so. This is the whole point of an append-only journal, and it is what makes the hash chain worth computing.
events in, never edits
DELETE an entry
Same. A deleted row takes its balance_after with it and breaks every replay that crosses it.
events in, never edits
Recompute a downstream hash
entries_rechained carries CHECK (entries_rechained = 0), so the claim is enforced rather than documented. The nightly verify independently confirms that no historical row_hash changed, including on days when corrections posted.
corrects the v2 workbench
Change a location in one row
A relocate without a movement_pair_id, a counterpart location and a leg fails its CHECK. The deferred trigger then refuses a pair that does not sum to zero. The 1,205 cross-location defect is not an attribution bug, it is 1,205 unrecorded transfers, and its structural cause is a ledger that permits a lone row to change a location.
L26
Post a zero-quantity correction
CHECK (qty_delta <> 0). A landed-cost revision moves value with zero quantity and belongs in cost_ledger, a second hash-chained journal. Weakening the constraint would mean every quantity aggregation in all fifteen modules has to remember to filter cost rows out, and forgetting once produces a silently wrong total.
L14
Write to the balance cache
No module 13 code path writes inventory_balance, and CI greps for it. The before-and-after figures on this page are reads. i2 stored a cached quantity and never compared it to anything, which is mechanically how sellable reached minus 140,753; the fix is not a better repair tool, it is having no repair tool at all.
L8
One more refusal that is a policy rather than a constraint: a bulk external reconciliation against a counted file or a supplier statement is a proposal set, not a posting. It opens one case per key, groups them by explanation, and presents the whole set as a single approval subject. INV1's equivalent posts a row per differing key with created_by = None, a constant note, and occurred_at hardcoded to one instant at year end, then rebuilds the balance table from scratch. 4,456 rows arrived that way and not one of them has a signature.