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.
219 lines
9.7 KiB
Markdown
219 lines
9.7 KiB
Markdown
---
|
||
id: CB-WP-0011
|
||
kind: product
|
||
title: "Stage 1, first slice: an inspector that shows everything"
|
||
status: todo
|
||
state_hub_workstream_id: "1142442a-65f1-483a-800d-6ca8490c5f1a"
|
||
---
|
||
|
||
# Purpose
|
||
|
||
Tier **S** — and this is the **first time the chaos roll has changed
|
||
anything** in six declarations.
|
||
|
||
```
|
||
structural tier L (INTENT stage 1 creates a new capability port)
|
||
chaos d4 = 4 → override
|
||
override roll shuf -e S M L → S
|
||
declared tier S (structural L, chaos 4 → S)
|
||
```
|
||
|
||
Declaration 6 of 12 in the calibration window. The `CHAOS` gate in
|
||
`gates.toml` has `caught = []` and a `review_by` of 2026-09-30; this pass
|
||
is the first entry it can earn, whichever way the outcome falls.
|
||
|
||
## What tier S is allowed to mean here
|
||
|
||
A rolled-down tier relaxes *process weight*. It does not relax the hard
|
||
gate — **no implementation code for a capability exists before its ADR is
|
||
committed** — because that gate is stated unconditionally and the tier
|
||
table governs only "weight of steps 1–3", not whether step 3 happens.
|
||
|
||
The two rules meet head-on: stage 1's headline deliverable is a rendering
|
||
port (`cb-render-api` / `-null` / `-wgpu`, ArchitectureBlueprint §Port
|
||
pattern), a port needs an ADR, and tier S has no ADR.
|
||
|
||
**Resolution: split the declaration rather than lawyer either rule.**
|
||
This packet is the part of stage 1 that creates no port, adds no
|
||
dependency, and therefore trips no ADR gate. The GUI half gets its own
|
||
tier declaration and its own chaos roll later, informed by what this pass
|
||
learns. Two facts make that ordering the right one anyway:
|
||
|
||
- **AM-4a has 3,750 lines of headroom** (246,250 of 250,000 measured by
|
||
`make dep-weight`). Any 2D toolkit is a five- to six-figure line count.
|
||
The dependency decision needs a survey, and the roll just deleted the
|
||
survey — so the honest move is to not make the decision this pass.
|
||
- The second-use rule cuts the same way. A renderer that has never had to
|
||
show the whole of `GroundView` does not yet know what a view needs.
|
||
|
||
## The finding this packet acts on
|
||
|
||
`tools/cb-play/src/table.rs:113` already has
|
||
`render(view: &GroundView) -> String`. It is stage 1's renderer, written
|
||
in stage 0 to prompt a human, and **it drops roughly a third of what the
|
||
projection carries**:
|
||
|
||
| in `GroundView` | in `render` |
|
||
|---|---|
|
||
| `mode`, `focus`, `solution_discard` | absent |
|
||
| `ground_modes`, `ground_choices` | absent |
|
||
| `support_responses`, `darvo_targets` | absent |
|
||
| `protection`, `freedom_gate_lifted` | absent |
|
||
| `protected_this_round` on a Problem | absent |
|
||
| `personal`, `coalitions`, `mastery` on the outcome | absent |
|
||
|
||
Nothing is wrong: it was written to show a human their legal moves, and
|
||
it does. But "inspectable" is the word INTENT uses for stage 1, and an
|
||
inspector that silently omits the DARVO state machine and the GROUND
|
||
practice — the two mechanics this game exists to model — is not one.
|
||
|
||
The interesting part is that **no test could have caught this**, because
|
||
every assertion a renderer test naturally makes is satisfied by any
|
||
render. That is the harness-does-nothing class again, in its presentation
|
||
form.
|
||
|
||
## Task: the inspector shows everything, and a gate says so
|
||
|
||
```task
|
||
id: CB-WP-0011-T01
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "b8cb4c20-9d06-47a4-890c-bf8390015439"
|
||
```
|
||
|
||
Extend `render` to cover every field of `GroundView`, and — the load-
|
||
bearing half — add the check that keeps it covering them.
|
||
|
||
**The check.** Serialize the view to `serde_json::Value`, walk it for
|
||
every object key reachable in a populated state, and require each key to
|
||
be classified in exactly one of two lists in the test:
|
||
|
||
- `RENDERED` — the inspector shows it, asserted against a state where the
|
||
field holds a distinctive value that appears in the output;
|
||
- `OMITTED` — with a stated reason per entry (e.g. `hand_size`, which is
|
||
redundant when `hand` is shown to its owner).
|
||
|
||
A new field on `GroundView` that is in neither list **fails the test**.
|
||
That is the same shape as `not_control_gates` in `gates.toml`, for the
|
||
same reason: silence must cost something.
|
||
|
||
**Vacuity controls (M-D1-MUT).** The test must go red for a stated reason
|
||
when (a) a field is deleted from the render, (b) a field is moved from
|
||
`RENDERED` to `OMITTED` while still being rendered, and (c) the walk is
|
||
made to return an empty key set — the EXPECT-VACUOUS control, since a
|
||
coverage test over zero keys passes trivially and that is precisely how
|
||
this check would rot.
|
||
|
||
Build the fixture from a **mid-DARVO, mid-GROUND** state, not a fresh
|
||
deal: most of the missing fields are empty at deal time, and a coverage
|
||
test run against a state where the fields are absent is the same lie in a
|
||
different costume.
|
||
|
||
**Done 2026-08-02.** The renderer moved to `tools/cb-play/src/inspect.rs`
|
||
and now covers **42 of the 43 leaf paths** in a populated `GroundView`,
|
||
up from 24. The one omission is `players.*.hand` — `null` for a
|
||
non-viewer seat, where the absence is what `hand N card(s)` renders.
|
||
|
||
`every_view_field_is_classified` walks the serialized view for leaf
|
||
*paths* (not keys — `problem` occurs under a DARVO target, a GROUND
|
||
choice and a Selection, and a key-set walk would let one vouch for the
|
||
other two). Four controls, each run and each red for its stated reason:
|
||
|
||
| control | result |
|
||
|---|---|
|
||
| a field deleted from `render` | `players.*.protection is claimed rendered, but the output has no "protect 2"` |
|
||
| a field present in neither list | fired **for real on the first run** — `players.*.hand` |
|
||
| the walk returns no paths (EXPECT-VACUOUS) | `the walk found 0 leaf path(s) — it is not walking the view` |
|
||
| a classified path that no longer exists | `classified path(s) no longer exist in GroundView: ["outcome.nonexistent"]` |
|
||
|
||
The second is the one worth keeping: the gate caught a field I had missed
|
||
while writing the gate, before it had ever been committed.
|
||
|
||
## Task: `cb-play --inspect` walks a recorded game
|
||
|
||
```task
|
||
id: CB-WP-0011-T02
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "08f8a9c7-3dfe-4f72-b510-df9efa457370"
|
||
```
|
||
|
||
Stage 1 asks for a *debug inspector*. The inspector above renders one
|
||
state; this makes it walk a game.
|
||
|
||
Deliver `cb-play --inspect <bundle-or-scenario> [--as SEAT|spectator]`:
|
||
replay the recorded commands and print the full table after each step,
|
||
from the chosen viewpoint. A `.cbreplay` bundle and a scenario YAML are
|
||
both accepted — both already reconstruct a command sequence.
|
||
|
||
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.
|
||
It costs no dependency: replay, projection and render all exist.
|
||
|
||
**Acceptance.** Inspecting a recorded game must reproduce the end-state
|
||
hash the bundle records — an inspector that shows a state the replay did
|
||
not reach is worse than none. Assert the hash, and mutate the replay to
|
||
prove the assertion fires.
|
||
|
||
**Projection is not optional here.** `--as 2` must show P3's hand and
|
||
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
|
||
id: CB-WP-0011-T03
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "db722865-1233-4918-952d-0bf4761199e4"
|
||
```
|
||
|
||
Write `evidence/CB-EV-0009-inspectable-table.md` covering:
|
||
|
||
1. **Did tier S produce a worse outcome than tier L would have?** The
|
||
only honest way to answer is to name what the deleted survey would
|
||
have decided and check whether this pass needed it. State the answer
|
||
even if it is "no, and that is one data point against the tier table",
|
||
because that is the finding the calibration window is for.
|
||
2. The field-coverage gap as measured before and after T01.
|
||
3. Cost and session shape for the pass, per `make status`. The previous
|
||
pass opened at a **HARD** session-shape breach (338,852 mean context
|
||
against a 300,000 ceiling) and a **45%** trailing-3 meta share against
|
||
a soft 25%. This pass is `product`; report where both land.
|
||
4. Update `gates.toml`: `CHAOS` gets its first `caught` entry — the roll
|
||
fired, and what it changed.
|
||
|
||
**Do not update the ADR or INTENT.** Stage 1 is not shipped when this
|
||
lands; the port half is untouched and INTENT's stage-1 line stays open.
|
||
Marking a stage complete because part of it works is the failure mode
|
||
stage 0 avoided by leaving the CLI player open for three passes.
|