Some checks failed
ci / check (push) Failing after 3s
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>
114 lines
5.2 KiB
Markdown
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 |
|