clay-borg/workplans/CB-WP-0049-a-seat-that-plays-its-own-objective.md
tegwick 82b9e7df31 Sync hub state for CB-WP-0049
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 22:47:45 +02:00

8.2 KiB
Raw Blame History

id kind title status state_hub_workstream_id
CB-WP-0049 product A seat that plays its own objective in_progress 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 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.

Task: a policy is bound by what its seat can see

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

id: CB-WP-0049-T02
status: todo
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).

Task: a seat that plays it, and the F27 re-measurement

id: CB-WP-0049-T03
status: todo
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.

Task: module-aware evaluation

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).

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.