clay-borg/workplans/CB-WP-0033-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

3.7 KiB

id kind title status
CB-WP-0033 product A game is the unit done

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.

Task: a game is the unit

id: CB-WP-0033-T01
status: done
priority: high

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.