Reported three times across three sessions, two days apart, and it
survived a whole UI rebuild: "i cant see whos support i accept".
It was not styling. The move button's label was format!("{c:?}"), so the
player read `RespondToSupport { response: AcceptBond }` — Rust struct
syntax with no name in it. And the command does not carry the
counterparty, so nothing rendering it alone could have said who; it comes
off the view, as whoever played Support at this seat.
Four more seat-panel fields had the same defect, including `support
AcceptBond` — the one the report names. CB-WP-0020 fixed exactly this for
selections and left its four neighbours as they were.
command_label has no catch-all arm, and that earned its keep before any
test ran: GroundChoice::RejectReverse and SupportResponse::BreakRivalry
both failed to compile — two moves that would have shipped as struct
dumps. An offer the view cannot see is said to be unseen rather than given
an invented name.
The finding underneath: the coverage probe that exists to prove every view
field reaches the PLAYER was matching "player: Some(PlayerId(1))" and
"members: [PlayerId(1)". It was certifying the defect as coverage and
would have gone red had anyone fixed it. Second confirmation of
CB-WP-0024's finding, from the sharper side: a probe naming Debug output
does not merely fail to protect, it pins the defect in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.8 KiB
| id | kind | title | status | state_hub_workstream_id |
|---|---|---|---|---|
| CB-WP-0033 | product | A game is the unit | done | 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.
Task: a game is the unit
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.recordwas re-derived from the previous game's path, so game 3 would have landed inx-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 warningscaught. 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.