--- id: CB-WP-0033 kind: product title: "A game is the unit" status: done state_hub_workstream_id: "66c54551-38cd-465e-bb2f-9e1d83e14438" --- # Purpose ``` structural tier M (changes an artifact contract -- the recording path -- and the definition of a reported metric) chaos d8 = 7 → no override declared tier M ``` **Declaration 4 of chaos window 3.** ## The report Reviewing the maintainer's six trial notes, `make trials` said **positions unreachable: 4, target 0 — "the recording exists but the position moved."** **All four were false.** A recording holds exactly one hash, the final state; `reachability()` asked whether the note's hash was in that file. So a mid-game note could never match, and a post-game note from any but the last game could not either. **Instance 8 of the ADR-0018 family**, with an exact sensitivity: vary only *when* the note was written and the answer flips deterministically, with nothing having moved. ## The root cause was not the metric Three defects, one cause: **`play again` reused state that belongs to a game.** | reused | consequence | |---|---| | the recording path | **game 1's recording destroyed** — data loss | | the journal (never cleared) | game 2's log opened with game 1's commands | | the note's `after`, counted on that journal | indexed a recording without those commands | **Fixing `reachability()` alone would have turned the number green while a session still destroyed its own evidence.** [ADR-0019](../decisions/ADR-0019-a-game-is-the-unit.md). ## Task: a game is the unit ```task id: CB-WP-0033-T01 status: done priority: high state_hub_task_id: "b0b4b9f3-f99f-4b4d-8eed-ba2fa4f9ea98" ``` **Controls:** - **each fix fails on its own defect**, by mutation, or it is not tested; - **the metric can go red** — a check reporting 0 that cannot report anything else is decoration (ADR-0006 D3); - **old logs stay readable and are not called orphans**; - **the single-game invocation GameDesign §5 documents is unchanged.** **Done 2026-08-07.** All three mutation-proven: | mutation | what went red | |---|---| | `next_game` stops clearing the journal | *"game 2 opened holding game 1's commands"* | | notes not filtered by game | *"game 1's comment leaked into game 2's log"* | | driver stops re-pathing the recording | *"game 2 was never recorded separately"* | The third is the important one: it runs the **real two-game session**, so it covers the call site rather than the helper. The unit test proves `game_path` names files apart; only the integration test proves the driver uses it, and the driver was where the data loss lived. **Two defects were introduced and caught while writing this.** - `cfg.record` was re-derived from the *previous game's* path, so game 3 would have landed in `x-2-3.yaml`. Fixed by keeping the caller's base untouched, **and the compounding shape is now pinned by an assertion** so the fix cannot silently rot. - `Server::game()` was dead code, which `-D warnings` caught. Rather than `#[allow]`, the driver now *asks the server* which game it is — removing a second counter that could have disagreed with the one stamped on the notes. **The metric now reports honestly**: 0 unreachable, 6 legacy, and it says what legacy means. `trials.py` gained six controls, including the two that were previously **impossible to pass** — a mid-game note being reachable, and a note from game 2 being judged against game 2's recording. ## Not done here - **No hash per command in the recording** (ADR-0019 D3). A mid-game position is confirmed by replaying to it, not by string search. The trigger for revisiting is a reader who needs the position without a replay. - **The six existing notes stay legacy forever.** Their positions were never checkable; nothing is gained by inventing bindings for them.