--- id: CB-WP-0049 kind: product title: "A seat that plays its own objective" status: active state_hub_workstream_id: "92b98076-65c2-4db3-8ab0-6f0139d4f893" --- # Purpose ``` structural tier M (adds a measurement instrument whose output changes what published panels mean) declared tier M ``` Closes the gap [CB-WP-0048](CB-WP-0048-a-configuration-not-a-variant.md) opened: configurations are now selectable and **nothing can judge them**. ## The finding this exists to answer **F27.** `scenario-panel` measured 4 scenarios × 3 modes × 3 seat bands and found group success *exactly* equal across all three scoring modes in all 36 cells. The arithmetic is right and the reason is simple: `GreedyPolicy` never reads `state.mode`. The same games are played and only the winner set is carved differently. So **the two competitive modes are scoring lenses over cooperative play**, and the same will be true of every aspect module: a bot that does not attend to a mechanism cannot test a claim about that mechanism's incentives. CB-RES-0010 §5.4's last bullet is F27 restated from the design side. This is now the binding constraint on the whole programme. `scoped_plus_attack_soothe` is selectable today (CB-WP-0048 T01) and no instrument in this repo can say whether it is any good. ## The prerequisite nobody had noticed `Policy::choose` takes `&GroundState`, which carries **every face-down Problem's suit and value and every seat's hand**. `project` exists to withhold exactly those. No shipped policy reads them. But the first thing a competitive policy must do is **value a Problem**, and value is the field the projection hides — so the trap becomes live on the first line of the work this workplan is about. [ADR-0023](../decisions/ADR-0023-a-policy-is-bound-by-what-its-seat-can-see.md). ## Task: a policy is bound by what its seat can see ```task id: CB-WP-0049-T01 status: done priority: high state_hub_task_id: "2b84e2d1-a26a-4cfa-bc96-21b40cb487df" ``` Per ADR-0023: established **behaviourally**, not by changing the trait — vary only what the seat cannot see and the choice must not move. That binds every policy, including ones written later and outside this crate, without their cooperation. **Done 2026-08-08.** `bot::blindness` provides `hidden_rearrangements` and `is_blind`; `GreedyPolicy` and `RandomPolicy` are proven blind across three seeds × three seat bands × every seat. ### Two defects in the control itself, both caught by running it **1. The first rearrangement was invariant under what a violator reads.** It rotated every hidden value 2↔3 *together*, which leaves `max` unchanged and (at equal counts) `sum` too — so the deliberate peeker, which ranks by the largest hidden value, **was not caught**. A control whose variation is invariant under the statistic a violator reads is not a control. The family now forces all-2, all-3 and two alternating splits, so any symmetric function of the hidden values moves across it. **2. It accused `random` of peeking.** `is_blind` reused one policy instance, so it compared a first call against a fourth — and `RandomPolicy` advances its own stream. It takes a **constructor** now, so every variant is judged from an identical starting policy and the only difference between two runs is the state. Both are the same mistake in different clothes: **a difference in the output was read as evidence about the hidden state**, when the first was insensitive to it and the second was caused by something else entirely. That is the wrong-subject family, found twice inside one control written to detect wrong subjects. | mutation | what went red | |---|---| | rearrangements leave the multiset invariant | *"a policy reading face-down values must be caught"* | | rearrangements vary nothing at all | *"no rearrangement changed a hidden card"* + the peeker escapes | | one policy instance reused across variants | *"random chose Command(5) and then Command(2)"* | **The control is proven against a deliberate violator** before being trusted about compliant policies (ADR-0023 D3) — a peek control nothing can fail is decoration. ## Task: an objective, taken from the game's own scoring ```task id: CB-WP-0049-T02 status: done priority: high state_hub_task_id: "7e3611bf-4cfb-412c-8e33-695778c7980b" ``` What a seat is trying to maximise, per `ScoringMode`: | mode | objective | |---|---| | SHARED GROUND | the group total | | COMMON PROBLEM | own claimed value − own Blame | | BONDED COALITIONS | own coalition's summed personal score | **Derived from `GroundState::score`, never reimplemented.** All three are already computed there for the outcome; a second copy in the bot would be a second definition of winning, and the two would disagree the first time ground-game rules on F28. **Controls:** - **the objective agrees with `Outcome` on a finished game**, for every mode — asserted against the real scoring rather than a fixture; - **a seat's objective is not the group's** under the competitive modes, shown on a concrete board rather than argued; - **the objective is blind** (T01's control applies to anything that reaches a policy). **Done 2026-08-08.** `GroundState::score` is now public and `bot::objective` reads it. Nothing about winning is restated in the bot. Because `score` is a pure function of the state it answers mid-game too — *"what would this position score if it stopped now"* — which is what a policy needs and what a second implementation would have had to invent. ## Task: a seat that plays it, and the F27 re-measurement ```task id: CB-WP-0049-T03 status: done priority: high state_hub_task_id: "a842c0a3-9374-4f08-8e2b-1571b8f241ef" ``` A policy that maximises its own objective, delegating to `GreedyPolicy::rank` wherever the objective is silent — **delegating, not copying**, which CB-WP-0039 learned the hard way when a re-typed "greedy with one preference changed" differed in five places and burned the Freedom token in round one of every game. Then re-run `scenario-panel`. **The result is not predicted here.** Either the modes now diverge — F27 resolves, and the competitive modes are real — or they still do not, and that is a much more interesting finding about GROUND: that the game's scoring modes do not reach its decisions. Both outcomes are publishable and the workplan must not be written as though one is expected. **Controls:** - **the panel's group-success column moves, or it is reported as not moving** — a null result stated as a null result; - **the competitive policy is blind** (T01); - **greedy's numbers are unchanged**, so the new policy is an addition and not a silent edit of the published baseline. **Done 2026-08-08.** `ObjectivePolicy` delegates to `GreedyPolicy::rank` and overrides two arms. ### Where the modes can differ at all Working this out was most of the task, and it bounds the result: **SOLVE always claims for the actor**, so a seat maximising its own score and one maximising the group's want the same SOLVE in nearly every position. That is a fact about GROUND's action set, not a shortcoming of the bot. Two places the objective genuinely diverges, both readable off the table: - **SUPPORT regulates someone else** — worth less when the beneficiary is a rival (COMMON PROBLEM), worth *more* when a Bond merges them into my coalition and my score is the coalition's sum (BONDED COALITIONS). - **SOLVE's value is the card's value** — greedy ranks every legal SOLVE at 90 regardless, and under a competitive objective the difference between a 2 and a 3 is the margin. ### The result — F27 splits in two The panel now prints **both policies side by side**. That was a correction mid-task: the first version printed only the new policy's `win/g` and I compared it against a figure remembered from CB-WP-0047, which is a comparison against a board nobody re-ran. | | | |---|---| | **group success** | **unchanged** — 34 of 36 cells identical; SCN_03 at 4p goes 99→100 in all three modes, which is the SOLVE-by-value refinement, not a mode effect | | **who wins** | **moves** — BONDED COALITIONS at 4p: 2.04 → 2.98, 2.12 → 3.29, 2.05 → 3.01 winning seats per game. COMMON PROBLEM at 6p: 1.10 → 1.17 | **So the original claim was too strong.** The modes are not "scoring lenses over cooperative play" — they reach decisions. The sharper statement: **GROUND's scoring modes change who wins, not whether the group succeeds.** **And the effect is seat-band dependent**: 2p shows no divergence in any mode, 4p the largest, 6p none under BONDED COALITIONS. A candidate explanation is that two relation slots per seat cap network growth, so at 6p the incentive exists and cannot be acted on. **That is untested** and is the next variable to move. **Under SHARED GROUND the two policies agree** at all but ≤2 decision points across 12 boards — the control that separates "attends to the objective" from "is simply a different bot". Without it, every number above could have been the new policy being different rather than being mode-aware. ## Task: module-aware evaluation ```task id: CB-WP-0049-T04 status: todo priority: normal state_hub_task_id: "1b26c7e9-e049-49d0-aecd-63830c7c8a37" ``` A seat under `problem_stress.scoped` should prefer clearing a Problem whose scope includes it; under `attack_relief.self_soothe_ge4`, ATTACK becomes a Stress tool at the gate. Both are **visible** to the seat (`ProblemMarker` is in the projection), so neither needs hidden state. **The composition risk is the same one schema 2 exists to retire.** One policy per module is `H2AwarePolicy` — the blob at a new layer. Evaluation should compose per aspect the way a configuration does, and if that is not achievable cheaply, the honest move is one policy that reads the resolved `Rules` rather than several that each assume one. **Controls:** - **a module-aware seat differs from greedy ONLY where the module is live** — under the baseline the two must be identical, which is the control that separates "attends to the module" from "plays differently"; - **blind** (T01). ## Two things this pass fixed that were not the task **A test keyed on a word another repo owns.** T01's controls found their example module by searching the catalog for `status: proposed` — and ground-game renamed those to `ready-for-implement` mid-session, so two tests failed over a string neither was about. The property they actually need is *"a module the catalog has that this kernel cannot run"*, which `resolve()` determines and which does not move when someone edits their vocabulary. Same lesson as matching a rules passage by heading rather than row number (CB-WP-0046) and reading a module's `aspect:` field rather than splitting its id on the dot. **The mirror was re-vendored by hand three times in one session.** `make vendor` now does it and regenerates the digest block by **walking** `editions/`, never by typing it. It reports files only one side has rather than resolving them — deleting a file upstream dropped, or copying one it added, are decisions (ADR-0011), not a sync. Its self-test caught a defect in itself on first run: it read *both* digest blocks in `PROVENANCE.md` and reported ten edition CSVs as "recorded but absent", when they are absent only from the *sibling* walk — a different question from absent. Mutation-proved: touch a vendored file without re-running and *"every recorded digest is the file's current bytes"* goes red. ## Not done here - **The trait still takes `&GroundState`.** ADR-0023 D2 chose the behavioural control first, on the grounds that it binds policies the type system cannot reach. Narrowing `choose` to `&GroundView` remains worth doing and is not refused — it is simply not what establishes the property. - **Opponent modelling is out of scope.** These policies play their own objective; none of them models another seat playing theirs. A competitive mode where nobody anticipates a rival is still a weak test of that mode, and saying so is part of T03's result. - **No lookahead.** `search.rs` is retrospective and **may not be reused here**: ADR-0013 D1 permits it to see everything precisely because after the game there is one world. A policy plays from inside an information set, so the same permission would be strategy fusion. Same kernel, two searches, opposite permissions, and the discriminator is when the question is asked.