clay-borg/decisions/ADR-0023-a-policy-is-bound-by-what-its-seat-can-see.md
tegwick c6607e8233
Some checks failed
ci / check (push) Has been cancelled
ADR-0023 + CB-WP-0049 T01: a policy is bound by what its seat can see
Policy::choose takes the whole GroundState -- every face-down Problem's
suit and value, every seat's hand -- which is exactly what project()
exists to withhold. No shipped policy reads it, but the first thing a
competitive policy must do is VALUE a Problem, and value is the hidden
field. The trap goes live on the first line of the F27 work.

Established behaviourally rather than by narrowing 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. The mirror of ADR-0013 D1 -- same kernel, two
searches, opposite permissions, discriminated by WHEN the question is
asked; a policy plays from inside an information set, so retrospective
permission would be strategy fusion.

Running the control found two defects IN THE CONTROL:

1. The rearrangements rotated hidden values 2<->3 together, leaving max
   invariant -- 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.
2. It accused `random` of peeking, because it reused one policy instance
   and compared a first call against a fourth. It takes a constructor
   now, so every variant is judged from identical policy state.

Both are a difference in output read as evidence about hidden state --
the wrong-subject family, found twice inside a control written to detect
wrong subjects.

Three mutations, three red. The control is proven against a deliberate
violator before being trusted about compliant policies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 22:45:55 +02:00

4.3 KiB

ADR-0023: a policy is bound by what its seat can see

status: accepted date: 2026-08-08 decided by: agent, under the standing loop authorization tier: S (states a rule that already held in practice, and makes it checkable; no interface moved) references: ADR-0013 (why the retrospective search may see everything), CB-RES-0009 (information sets), specs/GameDesign.md §1.4 (sensitivity), F27

Context

Policy::choose takes &GroundState. GroundState carries every face-down Problem's suit and value and every seat's hand. GroundState::project exists precisely to withhold those: ProblemView::FaceDown carries nothing but the fact that a Problem is there.

So the trait hands a bot exactly the information the projection was written to hide. No shipped policy reads it — GreedyPolicy::rank touches only stress, claimed_by and the legal set — but nothing prevents it, and nothing would notice.

This stops being latent immediately. CB-WP-0049 writes policies whose job is to value Problems in order to test F27, and a Problem's value is the field the projection hides. A seat that ranks a claim by the value of a face-down card is playing a game nobody can play, and every number measured with it would be about that game.

D1 — a policy may not depend on what its seat cannot see

A measurement made with a peeking policy is not a measurement of GROUND.

This is the mirror of ADR-0013 D1, and the pairing is the point. The retrospective search is allowed to see everything, because after the game there is one world and a line found in it was executable in the only world there was. A policy plays during the game, from inside an information set, so the same permission would be a different claim entirely — the classic strategy-fusion error.

Same repository, two searches over the same kernel, opposite permissions, and the discriminator is when the question is asked.

D2 — checked behaviourally, not by the type system

The obvious enforcement is to change choose to take &GroundView. We do not do that first, because a behavioural control catches more:

Vary only what the seat cannot see, and assert the policy's choice does not move.

  • It binds every policy, including ones written later and ones written outside this crate, without their cooperation.
  • It survives a policy that gets the view and reconstructs hidden state by other means.
  • It is the sensitivity discipline the project already runs on (GameDesign §1.4): name the variable, vary it, watch the number.

A type change makes one route unavailable; the control makes the property false-if-violated. The type change is still worth doing and is not refused here — it is simply not the thing that establishes the property.

D3 — the control must be shown to fail

A peek control that no policy can fail is decoration (ADR-0006 D3). The test carries a deliberately peeking policy — one that ranks by the value of face-down Problems — and asserts that policy is caught. A control proven only against compliant policies has not been proven at all.

Consequences

  • Every policy is covered by the peek control, and a new one that peeks goes red on arrival rather than on review.
  • Hidden-information variation becomes a fixture: the same helper that proves policies blind can measure how much a line depended on what was hidden.
  • Policies that must reason about unseen Problems have to do it as inference from what is visible (a face-down Problem exists, and the scenario states the deal), which is the honest form of the same reasoning.
  • Bots remain unable to test claims about mechanisms they do not attend to — an orthogonal limit, recorded as F27, and not addressed by this.

What was rejected

rejected why
changing choose to &GroundView first closes one route; the property still would not be checked, and outside policies could still peek
trusting review four reviews on this repo found five, three, four and two FATAL items; peeking is invisible in a diff that looks like ranking
allowing peeking for "stronger" bots a stronger bot at a game nobody can play measures that game; ADR-0013 D1's permission is retrospective and does not transfer