CB-WP-0011-T02: cb-play --inspect walks a recorded game

Renders the table after every step of a .cbreplay bundle or a scenario
YAML, from any seat's projection or a spectator's. This is the first
thing in the project that answers 'what did the table look like when it
went wrong?' without adding a dbg! and re-running.

INTERFACE CHANGE (flagged per InnerLoop chaos limits -- this is a
tier-S pass that touched a runtime crate): cb-game-runtime gains
replay::open, extracted out of replay::replay. Dev-only, behind the
scenarios feature, no type changed. The point of the extraction is that
the inspector and the replay gate share one bundle reader, controls
included, so the inspector cannot show a state a replay never reached.

Three M-D1-MUT controls, each red for its stated reason. The
load-bearing one asserts one rendered table per step: without it, a
walk that rendered nothing would still report a matching hash.
This commit is contained in:
tegwick 2026-08-02 02:45:55 +02:00
parent d2ca1046c0
commit b11fc91fd4
4 changed files with 478 additions and 22 deletions

View file

@ -133,7 +133,7 @@ while writing the gate, before it had ever been committed.
```task
id: CB-WP-0011-T02
status: todo
status: done
priority: high
state_hub_task_id: "08f8a9c7-3dfe-4f72-b510-df9efa457370"
```
@ -160,6 +160,35 @@ must not show anyone else's. The stage-0 test that checked this was
vacuous twice before it held (CB-EV-0007); seat the assertion where the
hidden thing is actually hidden.
**Done 2026-08-02.** `cb-play --inspect PATH [--as SEAT|spectator]`
walks a `.cbreplay` bundle or a scenario YAML and renders the table after
every step.
The design decision worth recording: `replay::replay` was split, and its
bundle reader extracted as `replay::open`, so **the inspector walks
through the same reader the replay gate uses** — including the control
that makes the recorded seed load-bearing. A second reader would let the
inspector show states a replay never reached, and it would be a
duplicated fact in the one place where being wrong is silent. The hash is
then asserted anyway, because a structural argument that is never
executed is the class of claim this project keeps finding to be wrong.
The two modes take **disjoint** flags in both directions
(`--inspect --seed 9` and a bare `--as 2` are both refused, exit 64): a
flag accepted and ignored is how a user comes to believe they inspected
seed 9 when they inspected whatever the recording holds.
Three controls, each red for its stated reason:
| control | result |
|---|---|
| the hash comparison removed | `a_walk_that_does_not_reproduce_fails` fails — the tampered bundle walks clean |
| `Eyes::Seat` resolved to `Viewer::Spectator` | `P3 shows a hand in every table and nobody else does` |
| the per-step render dropped | `one table per step plus the opening one` |
The third is the one that matters: without it a walk that rendered
nothing at all would still have reported a matching hash and passed.
## Task: evidence, and what the chaos roll cost
```task