# Two reads of one file are not one read of one state _2026-09-22 · booth_ **The one finding across both U4 panels that changed code rather than prose, and it came from Hulda (Codex) on the CONTRACT-paraphrase round — before any code existed.** The contract specified the hold check as: is_held(marks_for(child), read_error(child)) Two reads of `.marks.json`, presented as one answer. They are not. A write or a repair landing between them yields a pair that described the booth at **no instant**, and the losing pair is `([], None)` — no marks, no error — which is **exactly the pair that deletes**. A lenient reader plus a strict reader, each correct on its own, compose into a fail-open delete. The fix is `booth.marks.hold_read(booth) -> (marks, error)`: ONE strict read answering both questions. `sweep_once` now does one read per booth per tick instead of two. And because `_read_raw_strict` **raises rather than dropping an entry**, a non-raising strict read returns exactly what the lenient read would — so the index uses that same one read for its badge too and falls back to `marks_for` only on the error path, where leniency is the point. Better than the original in both correctness and cost. **The generalisable class, in heid's words: a two-read seam presented as one answer is a TOCTOU race even when nothing on the page looks concurrent.** Worth looking for anywhere two reader functions with different strictness feed one decision — especially when that decision ends in `rmtree`. Related: `[[2026-09-21-marks-write-wiped-judgment]]` is the same reads-lenient/writes-strict asymmetry; U4 extends it to the reaper with "deletes strict", whose scope is **the sweeper only** — a hand delete is never strict, which is what gives an unreadable-marks hold an exit at all.