# ADR-0019: a game is the unit, and `play again` was reusing what belongs to one status: accepted date: 2026-08-07 decided by: agent, under the standing loop authorization tier: M (structural M — changes an artifact contract, the recording path, and the definition of a reported metric). chaos d8 = 7 → no override. **Declaration 4 of chaos window 3.** references: [CB-WP-0033](../workplans/CB-WP-0033-a-game-is-the-unit.md), [ADR-0014](ADR-0014-a-second-channel.md) (the note channel), [ADR-0018](ADR-0018-a-number-that-does-not-move.md) (instance 8), [GameDesign.md](../specs/GameDesign.md) §5 ## Context `make trials` reported **positions unreachable: 4, target 0**, with the explanation *"the recording exists but the position moved."* **All four were false, and the explanation was false four times.** A recording carries exactly **one** hash — `expect.state_hash`, the final state. `reachability()` asked whether a note's hash appeared in that file. So the answer never depended on any position moving: | note | outcome | why | |---|---|---| | written mid-game | **always orphan** | no intermediate hash is in the file to match | | written after the last game | ok | it *is* the final hash | | written after an earlier game | orphan | `play again` overwrote the recording | Both Aug-7 sessions confirm it. In `2026-08-07-1612`, two post-game notes with identical round and step: game 1's is *orphan*, game 2's is *ok*, purely because the file holds game 2. **Instance 8 of the family in ADR-0018**, and the sensitivity is exact: vary only *when* a note was written and reachability flips deterministically, with nothing having moved. The computation (`hash in file`) was right; the subject — a file with one hash, read as a file of positions — was not. ## D1 — the root cause is not the metric Three defects, one cause: **`play again` reuses state that belongs to a game, not to a session.** | what | reused | consequence | |---|---|---| | the recording path | same file | **game 1's recording is destroyed** | | the journal | never cleared | game 2's log shows game 1's commands | | the note's position | counted against the shared journal | the index means nothing | The metric was reporting the symptom, in the wrong words. **Fixing only `reachability()` would have made the number go green while a session still destroyed its own evidence** — the failure the register calls `inert`. ## D2 — the recording path, and what stays unchanged Game 1 keeps the path it was given. Games 2, 3 … get `-2`, `-3` before the extension. **The single-game session is byte-identical to today**, which matters because GameDesign §5 documents exactly that invocation as the trial protocol, and this project has already shipped one gate that was written for a smaller world (CB-EV-0026 §4). A scheme that renamed *every* file would have made the spec wrong on the day it landed. **Nothing is overwritten.** A recording is evidence; `play again` silently deleting the previous game's evidence is a data-loss defect independent of notes. ## D3 — a note binds to a position by `(game, after)` `after` is the number of commands played when the note was written, which is exactly an index into the recording's own `commands:` list. **That is a binding a reader can act on**: replay the first `after` commands and you are standing where the player stood. The hash keeps a job — an **integrity check where it applies**. When `after` equals the command count, the note's hash must equal `expect.state_hash`, and a mismatch is a real finding. **Why not record a hash per command instead?** It would make every position matchable by string search, and it changes the recording format — which `record.rs`'s round trip, `edition-check` and any consumer in `ground-game` all read. Deferred, not refused: if a reader ever needs to confirm a mid-game position without replaying, that is the trigger. ## D4 — old logs stay readable, and are not called orphans The trial log gains `game` and `after`. Existing 5-column rows are parsed as **legacy** and reported as such. **They are not reported as orphans.** Their positions were never checkable by anything this repo can now run, and an orphan claim we cannot substantiate is precisely the false explanation this ADR exists to delete. A row that is neither 5 nor 7 columns raises (CB-WP-0032). ## Consequences - `trials.py`'s headline metric changes meaning; the old number was not measuring reachability and no comparison to it is valid. - The journal is cleared per game, so the log shows **this** game. - Notes from earlier games do not appear in the current game's log. - A multi-game session leaves several recordings where it left one. ## What was rejected | rejected | why | |---|---| | fixing `reachability()` alone | green metric, session still destroys its own recordings | | a hash per command in the recording | changes a format three consumers read, to save a replay | | renaming every recording to `-1`, `-2` | makes GameDesign §5's documented invocation wrong for the common case | | calling legacy rows orphans | an unsubstantiated claim, which is the defect being fixed | | clearing the notes per game | the trial log is the session's record; only the *log view* is per-game |