clay-borg/decisions/ADR-0019-a-game-is-the-unit.md
tegwick edab11c0e4
Some checks failed
ci / check (push) Failing after 3s
CB-WP-0033: a game is the unit
make trials reported "positions unreachable: 4, target 0 — the recording
exists but the position moved". All four were false. A recording holds one
hash, the final state, and 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: vary only WHEN a note was written and the answer flips, with
nothing having moved.

The root cause was not the metric. play again reused state belonging to a
game: it overwrote the previous game's recording (data loss), never
cleared the journal (game 2's log opened with game 1's commands), and so a
note's command index pointed into a recording without those commands.
Fixing reachability alone would have gone green while a session still
destroyed its own evidence.

A note now binds by (game, after) — an index into the recording's own
commands list, which a reader can replay to. The hash keeps a job as the
integrity check at the end of a game, where it can actually fail. Game 1
keeps the path it was given, so GameDesign §5's documented invocation is
unchanged; later games get -2, -3 and nothing is overwritten. Legacy
5-column logs stay readable and are reported as legacy, never as orphans —
an unsubstantiated orphan claim is the defect being fixed.

All three fixes mutation-proven, including at the call site via a real
two-game session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:41:11 +02:00

114 lines
5.2 KiB
Markdown

# 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 |