CB-WP-0048 T02/T03/T04: Configuration replaces Variant in the state
Some checks failed
ci / check (push) Has been cancelled
Some checks failed
ci / check (push) Has been cancelled
The three-armed enum could not express a configuration carrying two
modules. It does now: --variant h1 expands to problem_stress.flat_any_open
AND attack_relief.self_soothe_ge4, and scoped_plus_attack_soothe plays.
Legacy names alias forever via serde(alias="variant") plus a
scalar-or-map deserialiser. serde(default) alone would have been a silent
migration bug -- every H1/H2 recording would have come back as baseline.
All 26 scenarios pass unchanged; none pins a state hash.
Three things shipped broken first, all caught by gates rather than by
reading:
1. `profile` was in the state hash. with_config(select("ground-darvo-r0"))
sets profile=Some("baseline") where setup alone leaves None, so two
states at THE SAME POINT IN ASPECT SPACE hashed differently and a
replay bundle stopped reproducing its own initial state. Now
serde(skip): a hash covers what determines play. This was the open
judgement from T01 and it did not survive contact with the replay path.
2. A YAML parse inside the event loop. rules() -> resolve() -> catalog()
re-parsed catalog.yaml per rule check; AM-6 fell to 9,345 events/s
against a 100,000 target. OnceLock, and aspect validation moved to
where a configuration is BUILT.
Then I nearly optimised a phantom: 470k still looked like a 3x
regression against the "~1.7M on bnt-lap001" reference in the gate's
own message. Making rules() free measured 491k -- this machine's
ceiling. Before optimising against a reference, measure the ceiling
with the suspect code removed.
3. The refusal did not fire on the path a player takes.
`--module problem_deal.pressure_deck` played a full baseline game and
reported success, because with_config is a builder and fell back to
the printed rules -- the silent no-op ADR-0022 exists to refuse. Every
unit test of resolve() passed. The helper was tested and the driver
was not, which is CB-WP-0033's finding verbatim. The new test asserts
refusal BY NAME and that no game was played.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
704b99975b
commit
48a7bce04d
12 changed files with 538 additions and 67 deletions
|
|
@ -110,7 +110,7 @@ could not express.
|
|||
|
||||
```task
|
||||
id: CB-WP-0048-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "3232a451-b60a-42f6-8200-b8115d4b35b9"
|
||||
```
|
||||
|
|
@ -127,11 +127,23 @@ Per ADR-0022 D2, through the catalog's own `legacy_experiment_id`.
|
|||
- **the expansion comes from the catalog**, not a table in our source: a
|
||||
second copy of ground-game's mapping is F25's shape.
|
||||
|
||||
**Done 2026-08-08.** `GroundState.variant: Variant` became
|
||||
`config: Configuration`, with `#[serde(alias = "variant")]` plus a
|
||||
scalar-or-map deserialiser so a recording written before this loads by
|
||||
its old name and old spelling. **`serde(default)` alone would have been a
|
||||
silent migration bug**: every recording played under H1 or H2 would have
|
||||
come back as the baseline.
|
||||
|
||||
All 26 scenarios pass unchanged — none pins a state hash. `--variant h2`,
|
||||
`--profile h2` and `--module problem_stress.scoped` all resolve to the
|
||||
same point in aspect space, and `--variant h1` expands to **two** modules
|
||||
on two aspects, which is the decomposition schema 2 exists for.
|
||||
|
||||
## Task: the resolved configuration is recorded
|
||||
|
||||
```task
|
||||
id: CB-WP-0048-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5be13b74-bcc9-4697-96bb-91f1966c760b"
|
||||
```
|
||||
|
|
@ -147,11 +159,58 @@ logs and panel cells carry the **resolved** module list.
|
|||
- **a bare field write cannot leave a module inert** — the H2 defect had
|
||||
three call sites and this is the generalisation of it.
|
||||
|
||||
**Done 2026-08-08**, and the interesting part is what shipped broken
|
||||
first.
|
||||
|
||||
### `profile` was in the hash, and it should not have been
|
||||
|
||||
The replay walk caught it within the hour: `with_config(select("ground-darvo-r0"))`
|
||||
sets `profile = Some("baseline")` while `setup` alone leaves it `None`,
|
||||
so **two states at the same point in aspect space hashed differently** and
|
||||
a bundle stopped reproducing its own initial state. `profile` is now
|
||||
`#[serde(skip)]`. A state hash should cover what determines play;
|
||||
provenance of what was *requested* belongs in the recording's manifest.
|
||||
|
||||
This was the open judgement flagged when T01 landed. It did not survive
|
||||
contact with the replay path, which is the right way for it to have been
|
||||
settled.
|
||||
|
||||
### A YAML parse inside the event loop
|
||||
|
||||
`rules()` called `resolve()` called `catalog()`, which re-parsed
|
||||
`catalog.yaml` **per rule check**. AM-6 throughput fell to **9,345
|
||||
events/s** against a 100,000 target — a spec gate, caught the same run.
|
||||
The catalog is an `include_str!` constant, so parsing it more than once
|
||||
was never doing anything but work; it is a `OnceLock` now, and the aspect
|
||||
validation moved out of `resolve` to where a configuration is *built*.
|
||||
|
||||
**And then I nearly optimised a phantom.** 470k events/s still looked
|
||||
like a 3x regression against the *"~1.7M on bnt-lap001"* reference in the
|
||||
gate's own message. Making `rules()` free and re-measuring gave **491k**
|
||||
— this machine's ceiling. The reference is another machine, and the only
|
||||
real regression was the one already fixed. The control is cheap and worth
|
||||
naming: **before optimising against a reference, measure the ceiling with
|
||||
the suspect code removed.**
|
||||
|
||||
### The refusal did not fire on the path a player takes
|
||||
|
||||
`--module problem_deal.pressure_deck` **played a full baseline game and
|
||||
reported success.** `with_config` is a builder, cannot return an error,
|
||||
and fell back to the printed rules — the silent no-op ADR-0022 exists to
|
||||
refuse. Every unit test of `resolve()` passed throughout.
|
||||
|
||||
**The helper was tested and the driver was not**, which is exactly
|
||||
CB-WP-0033's finding: *"the unit test proves the helper; only the
|
||||
integration test proves the driver, and the driver was where the data
|
||||
loss lived."* The driver resolves before dealing now, and the new test
|
||||
asserts both that it refuses **by name** and that **no game was played** —
|
||||
a refusal that still deals is the same no-op wearing an error message.
|
||||
|
||||
## Task: the page and the panels speak aspects
|
||||
|
||||
```task
|
||||
id: CB-WP-0048-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: normal
|
||||
state_hub_task_id: "ff135f86-2265-4db5-b412-23fcb68df489"
|
||||
```
|
||||
|
|
@ -164,6 +223,18 @@ state_hub_task_id: "ff135f86-2265-4db5-b412-23fcb68df489"
|
|||
reconstructs;
|
||||
- **a module nothing measured is reported unmeasured**, not absent.
|
||||
|
||||
**Done 2026-08-08.** The page names every live module —
|
||||
`attack_relief.self_soothe_ge4 + problem_stress.scoped` where `h2` could
|
||||
only ever have said one thing — and names the baseline by id when none
|
||||
are live, because "no modules" and "we could not read the catalog" must
|
||||
not look the same on the line a player uses to tell rules sets apart.
|
||||
|
||||
The four aspects sitting at their defaults are **declared omitted** from
|
||||
the player page with a reason: an aspect at its default *is* the printed
|
||||
game, and naming all four on every page is the wall of rules a player
|
||||
stops reading. `cb-play inspect` prints all four, because a maintainer
|
||||
diffing two states is asking a different question (ADR-0022 D3).
|
||||
|
||||
## Not done here
|
||||
|
||||
- **No policy reads the configuration.** F27 already records that the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue