Compare commits

...

7 commits

Author SHA1 Message Date
6487d33f27 CB-WP-0023: SOLVE is legal only where it can do something
Some checks failed
ci / check (push) Failing after 4s
Implements ground-game's ruling of 2026-08-03. make all exits 0, 26
scenarios, rule coverage 59/59, and no scenario encoded the bug.

The rule ended up somewhere other than where I put it, and a gate moved
it. It went into legal_commands first; the AM-1 coverage gate then
demanded a scenario for the new GR-P05, and scenarios drive validate, not
the offer layer. A rule enforced only by the offer is enforced only for
clients that ask what is legal -- the browser would be filtered and a
scenario file would walk straight past it. Once GR-P05 moved into
validate, every condition in legal_commands was dead code, and the
layering test said so in those words.

And the reported case was not the one I reported. CB-WP-0018 and the
message to ground-game described SOLVE offered on a FACE-DOWN Problem.
Measured: validate already rejected face-down, so it never was offered.
Problem 1 is the Surface Problem, face-up from the deal -- the
maintainer's three inert SOLVEs were the HAND case, holding no Clarify
for a Clarify Problem. The ruling covers both so nothing is invalidated,
but the record was wrong.

Four conditions asserted separately, because one 'SOLVE is filtered' test
would pass with three of four implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:18:40 +02:00
46c2fb652e Declare CB-WP-0023: SOLVE is offered only where it can do something
ground-game ruled SOLVE's legality on 2026-08-03: not offered on a
face-down or Denied Problem, not offered without a matching Solution in
hand, and not offered on a Problem claimed in a prior round. The bluff
reading CB-WP-0018 raised is dead -- it was a filter bug, and the engine
has been offering an inert move since legal_commands was written.

Only SOLVE is ruled on, so only SOLVE is touched. Implementing more than
was ruled would be inventing rules, which is what this exchange exists
to stop.

Chaos d8=6, no override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:59:23 +02:00
b513209acf GD-0001: group success is unreachable below 5 seats, with its reproduction
The maintainer played several 3-player games on 2026-08-03 and could not
win any. This says why, from the engine's own constants rather than my
arithmetic: claim every Problem the deal puts in play, concede nothing,
and the total still falls short of GR-E01's threshold at 2, 3 and 4
seats.

  2p: 2 problems worth 3  vs threshold 5 — UNREACHABLE
  3p: 3 problems worth 6  vs threshold 7 — UNREACHABLE
  4p: 3 problems worth 6  vs threshold 7 — UNREACHABLE
  5p: 4 problems worth 10 vs threshold 9 — reachable
  6p: 4 problems worth 10 vs threshold 9 — reachable

The test reads problem_priorities (GR-S01's deal) and threshold (GR-E01)
out of the engine, so it cannot drift from the rules it tests, and it
holds for either dataset -- the stand-in gives 3/6/10 and Problems.csv
gives 4/6/9 against the same 5/7/9.

It ASSERTS THE DEFECT and is expected to keep passing until ground-game
rules, then be inverted. This is CB-WP-0022's reproduction rule applied
before the register exists, because delivering feedback should not block
on building the tool that tracks it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:49:49 +02:00
a86efba4c3 CB-WP-0022-T01: survey — how rule systems record the ambiguity they find
CB-RES-0007 plus a runnable baseline harness. Tier L invokes the
runnable-baseline option; the external candidates are practices rather
than software, so their rows are directional and cap at parity, and the
row that CAN be run is our own.

Measured: 6 findings across 11 files with no index, 2 of 6 (33%) with a
runnable reproduction, U1-U10 raised 2026-07-30 and first READ
2026-08-03 -- 4 days, 0 of 10 ruled.

The uncomfortable number is stated before the review can find it: the
proposed 'no finding without its reproduction' rule would reject four of
our six existing findings. The survey answers rather than routes around
it -- none of the four is expensive to reproduce, so 33% is evidence
nobody was ever asked for one.

Magic corrected an assumption this pass was about to build on. Rulings
are NOT authoritative -- they are 'reminder information with no actual
weight or rules meaning' -- and the authoritative fix folds into the
Oracle card text. So a finding closes when the SOURCE changes, not when
an annotation is added, and the register must be a queue that empties
rather than an archive that grows. That is now a constraint on the ADR's
lifecycle.

Model checkers supply the reproduction rule independently: a
counterexample trace IS the finding. W3C's implementation-defined mark is
the machinery we already have in provisional: scenarios and must reuse.

The loop-lint gate caught the new tool with no --self-test; it has one,
pinning the 2-of-6 baseline so a later edit cannot move it silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 21:19:12 +02:00
bb426b3e9b Declare CB-WP-0022: the design instrument, tier L
The maintainer named a new aspect: clay-borg as a game design tool, with
a register for design flaws, questions, results and trial protocols.
Structural L on the maintainer-named-high-leverage trigger, and it amends
INTENT. Chaos d8=6, no override.

The insight is that this is already happening with no home. Five passes
have produced ten underdetermined rules points, SOLVE offered on a
face-down problem and always inert, GR-A13's wasted SOLVE, GR-E01
unreachable below 5 seats, six provisional scenario defaults, and two
scoring modes never played to the end -- every one found by BUILDING the
simulator rather than by playing it. A simulator rigorous enough to
refuse ambiguity is a design instrument, because it cannot proceed past a
rule that does not decide. All of it has been carried in prose in six
places and one sat unread in an inbox for four days.

The load-bearing rule: a design finding is not admissible without its
reproduction. A register that collects opinions would reproduce this
project's standing failure -- unexecuted verification -- in a new medium.

Recorded as a judgment for the adversarial review rather than assumed:
the engine-evolution meta the maintainer also asked about should NOT be
built, because evidence/, decisions/, gates.toml and workplans already
carry nineteen passes of it with dates, costs and falsifiers. A second
register for the same subject is ceremony. The asymmetry is the point --
engine evolution has a home and game design does not.

T05 backfills the six known findings as the TEST of the register: one
that cannot express findings the project already has is the wrong
register, and discovering that after designing it is why the order is
survey, review, decide, specify, build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:48:08 +02:00
fe3852e093 CB-WP-0021: correct the declaration before writing any code
The declaration claimed importing the edition data would resolve GR-E01.
Measured across all four scenarios: GR-S01 deals 2/3/4 problems by player
count, not all five, so 4/6/9 points are in play against thresholds of
5/7/9 -- unreachable at 2p and 3-4p with the REAL data, in the same shape
as the stand-in's 3/6/10. So GR-E01 unreachable below 5 seats is a real
property of the game and gr-e01-threshold-unreachable-2p asserts
something true.

The error was the cheap kind: 12 points exist in the file, so I assumed
12 are in play. One command over the CSV settled it and was not run until
after the declaration was committed -- this project's characteristic
error, in the pass that followed a ruling obtained because of it.

CB-EV-0018 corrected too: 'confirmed as the stand-in's doing' was
overstated. The zero came from no Problem being claimed at all.

T03 now owes ground-game a sharper question than a retirement: either
GR-S01's deal count is wrong or GR-E01's thresholds are, and no dataset
can reconcile them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:35:39 +02:00
7de0dcef87 Declare CB-WP-0021: import the edition, tier M
GROUND-WP-0002 T01 ruled the edition dataset authoritative, so the engine
must stop inventing Problem values and suits. Problems.csv carries 5
problems per scenario worth 2,2,2,3,3 (total 12) with a required_solution
each; the stand-in deals 3 worth 1,2,3 (total 6). GR-E01's thresholds of
5/7/9 are ordinary against 12 and unreachable against 6 -- which is why
the maintainer's last game ended 0 scores and winners nobody, and why
'GR-E01 unreachable below 5 seats' was carried as a rules gap. It was
never a rules gap.

Measured before declaring: AM-4a has 3,798 lines of headroom and a CSV
crate costs 21,613 marginal (csv 14,291 + csv-core 3,360 + ryu 3,962;
itoa/memchr/serde/serde_core are already present and free). 5.7x over, so
the shipped runtime cannot gain a CSV parser and that is settled by
measurement rather than preference.

The ADR's third question is the one that bites: Problem values and suits
become part of GroundState, which is hashed, so every recorded state hash
changes. A content import that quietly invalidates every hash in a
project whose central invariant is replay determinism is not a
data-loading change.

Chaos d8=7, no override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:33:00 +02:00
13 changed files with 1196 additions and 12 deletions

View file

@ -119,6 +119,7 @@ self-tests:
$(PY) $(TOOLS)/size-metrics.py --self-test
$(PY) $(TOOLS)/runtime-metrics.py --self-test
$(PY) $(TOOLS)/replay-test.py --self-test
$(PY) $(TOOLS)/design-baseline.py --self-test
# T01 positive control: prove the environment fix, do not assume it. Runs
# every tool from a foreign working directory with a PATH that has no

View file

@ -147,6 +147,11 @@ is what the 27% breach called for.
- **The engine still has not imported the edition data**, now that
GROUND-WP-0002 T01 has ruled it authoritative. That is the next product
pass, and it is what makes the ending's `0` scores and `winners nobody`
meaningful — **confirmed as the stand-in's doing, not a scoring bug**.
meaningful — ~~confirmed as the stand-in's doing, not a scoring bug~~.
**CORRECTED 2026-08-03 (CB-WP-0021):** overstated. The `0` came from no
Problem being claimed at all. The *threshold* gap is independent of the
dataset — GR-S01 deals 2/3/4 of the five problems, so 4/6/9 points are
in play against thresholds of 5/7/9, unreachable at 2p and 34p with
either dataset.
- **`ground-game` owes ten rulings** (U1U10) and SOLVE's legality.
- **Chaos: 3 of 12 in window 2, one override, changed nothing.**

View file

@ -6,7 +6,7 @@
# `make facts-check` fails if this file disagrees with the
# instruments, or if a tagged artifact disagrees with this file.
generated = "2026-08-03"
generated = "2026-08-04"
pin = "fc76445"
[am4a_loc]
@ -46,26 +46,26 @@ fmt = "{:,}"
by = "tools/mutation-check.py"
[gr_covered]
value = 58
text = "58"
value = 59
text = "59"
fmt = "{:,}"
by = "tools/rule-coverage.py"
[gr_linked]
value = 49
text = "49"
value = 50
text = "50"
fmt = "{:,}"
by = "tools/rule-coverage.py"
[gr_rules]
value = 58
text = "58"
value = 59
text = "59"
fmt = "{:,}"
by = "tools/rule-coverage.py"
[gr_scenarios]
value = 25
text = "25"
value = 26
text = "26"
fmt = "{:,}"
by = "tools/rule-coverage.py"

View file

@ -153,6 +153,17 @@ pub fn legal_commands(state: &GroundState, seat: PlayerId) -> Vec<GroundCommand>
Action::Ground,
] {
match action {
// Offered on every Problem; `validate` decides
// which are legal, and this function filters every
// candidate through it below.
//
// CB-WP-0023 first put GR-P05's conditions here, in
// the OFFER layer. The AM-1 coverage gate then asked
// for a scenario covering GR-P05 — and scenarios drive
// `validate`, not this. That is what revealed the rule
// belonged in `validate`: a rule enforced only by the
// offer is enforced only for clients that ask what is
// legal. Once it moved, everything here was dead code.
Action::Investigate | Action::Solve => {
for problem in &problems {
candidates.push(GroundCommand::SelectAction {
@ -649,6 +660,178 @@ mod tests {
.expect("preset")
}
// ---------------------------------------------------------------
// CB-WP-0023: SOLVE's legality, ruled by ground-game 2026-08-03.
//
// Four conditions, each asserted on its own, because a single
// "SOLVE is filtered" test would pass with three of the four
// implemented and nobody would know which.
/// Problems that SOLVE is offered on, for `seat`.
fn solvable(state: &GroundState, seat: PlayerId) -> Vec<u32> {
legal_commands(state, seat)
.into_iter()
.filter_map(|c| match c {
GroundCommand::SelectAction {
action: Action::Solve,
problem: Some(n),
..
} => Some(n),
_ => None,
})
.collect()
}
/// A 3p game where seat 0 can solve exactly problem 1.
fn solvable_fixture() -> (GroundState, u32) {
let mut st = setup(3, 42);
let (n, suit) = st
.problems
.iter()
.find(|(_, p)| p.face_up)
.map(|(n, p)| (*n, p.suit))
.expect("GR-S01 deals one face-up Surface Problem");
// Give seat 0 the matching Solution, so the ONLY thing under test
// is the condition each case perturbs.
st.players.get_mut(&PlayerId(0)).expect("seat 0").hand = vec![crate::SolutionCard { suit }];
assert_eq!(
solvable(&st, PlayerId(0)),
vec![n],
"fixture is not solvable"
);
(st, n)
}
/// **Which layer enforces GR-P05**, pinned so it cannot drift.
///
/// All four conditions live in `validate`, and `legal_commands` gets
/// them for free by filtering candidates through it. This test is what
/// keeps that true: if `validate` ever stops rejecting one, SOLVE
/// becomes offerable again and the offer layer would have to grow a
/// filter back.
#[test]
fn validate_enforces_all_four_solve_conditions() {
let (base, n) = solvable_fixture();
let sel = |p: u32| GroundCommand::SelectAction {
action: Action::Solve,
target: None,
problem: Some(p),
};
let ok =
|st: &GroundState, p: u32| st.validate(Actor::Player(PlayerId(0)), &sel(p)).is_ok();
let face_down = *base
.problems
.iter()
.find(|(_, p)| !p.face_up)
.map(|(k, _)| k)
.expect("a face-down problem");
println!(
" validate accepts SOLVE on face-down: {}",
ok(&base, face_down)
);
let mut denied = base.clone();
denied.problems.get_mut(&n).unwrap().denied = true;
println!(" validate accepts SOLVE on denied: {}", ok(&denied, n));
let mut claimed = base.clone();
claimed.problems.get_mut(&n).unwrap().claimed_by = Some(PlayerId(1));
println!(
" validate accepts SOLVE on claimed: {}",
ok(&claimed, n)
);
let mut nohand = base.clone();
nohand.players.get_mut(&PlayerId(0)).unwrap().hand = vec![];
println!(" validate accepts SOLVE with no hand: {}", ok(&nohand, n));
// validate's: adding these to `legal_commands` would be dead code.
for (what, accepted) in [
("face-down", ok(&base, face_down)),
("Denied", ok(&denied, n)),
("claimed", ok(&claimed, n)),
("handless", ok(&nohand, n)),
] {
assert!(
!accepted,
"validate accepts SOLVE on a {what} Problem — GR-P05 is not \
enforced where rules live, so only clients that ask what is \
legal would be constrained"
);
}
// Positive control: the fixture must still be solvable, or this
// test would pass by rejecting everything.
assert!(ok(&base, n), "the solvable fixture stopped being solvable");
}
/// **The maintainer's reported case.** SOLVE was offered on a
/// face-down Problem and did nothing; they played it three rounds
/// running with no explanation. ground-game: *"not offered — illegal
/// target. Browser no-ops were a filter bug, not a bluff mechanic."*
#[test]
fn solve_is_not_offered_on_a_face_down_problem() {
let (st, open) = solvable_fixture();
let face_down: Vec<u32> = st
.problems
.iter()
.filter(|(_, p)| !p.face_up)
.map(|(n, _)| *n)
.collect();
assert!(!face_down.is_empty(), "GR-S01 deals face-down Problems");
let offered = solvable(&st, PlayerId(0));
for n in face_down {
assert!(
!offered.contains(&n),
"SOLVE offered on face-down problem {n}; offered = {offered:?}"
);
}
assert!(offered.contains(&open), "the face-up one is still offered");
}
#[test]
fn solve_is_not_offered_without_a_matching_solution_in_hand() {
let (mut st, n) = solvable_fixture();
let wrong = [
crate::Suit::Clarify,
crate::Suit::Repair,
crate::Suit::Boundary,
crate::Suit::Change,
]
.into_iter()
.find(|s| *s != st.problems[&n].suit)
.expect("another suit exists");
st.players.get_mut(&PlayerId(0)).expect("seat 0").hand =
vec![crate::SolutionCard { suit: wrong }];
assert!(
!solvable(&st, PlayerId(0)).contains(&n),
"SOLVE offered with no matching Solution in hand"
);
}
#[test]
fn solve_is_not_offered_on_a_denied_problem() {
let (mut st, n) = solvable_fixture();
st.problems.get_mut(&n).expect("problem").denied = true;
assert!(
!solvable(&st, PlayerId(0)).contains(&n),
"SOLVE offered on a Denied Problem"
);
}
/// The ruling's (c): a Problem claimed in a PRIOR round is not
/// offered. At Select time every `claimed_by` is prior-round, because
/// claims land at Resolve — so the same-round race needs no code.
#[test]
fn solve_is_not_offered_on_an_already_claimed_problem() {
let (mut st, n) = solvable_fixture();
st.problems.get_mut(&n).expect("problem").claimed_by = Some(PlayerId(1));
assert!(
!solvable(&st, PlayerId(0)).contains(&n),
"SOLVE offered on an already-claimed Problem"
);
}
fn policies(kind: &str, n: u8, seed: u64) -> Vec<Box<dyn Policy>> {
(0..n)
.map(|i| -> Box<dyn Policy> {

View file

@ -1756,6 +1756,28 @@ impl GroundState {
"GR-A13: Problem {problem} is not a face-up, non-Denied Problem"
)));
}
// GR-P05, ruled by ground-game 2026-08-03: SOLVE is legal
// only where it can do something. This lives in `validate`
// and not only in `legal_commands` because a rule enforced
// by the offer alone is enforced only for clients that ask
// what is legal — the browser would be filtered and a
// scenario file would not.
Action::Solve if target.claimed_by.is_some() => {
return Err(bad(format!(
"GR-P05: Problem {problem} was claimed in an earlier round"
)));
}
Action::Solve
if !self
.players
.get(&actor)
.is_some_and(|p| p.hand.iter().any(|c| c.suit == target.suit)) =>
{
return Err(bad(format!(
"GR-P05: no {:?} Solution in hand for Problem {problem}",
target.suit
)));
}
_ => {}
}
} else if problem.is_some() {
@ -2162,6 +2184,19 @@ mod replay_probe {
use cb_kernel::EventSeq;
use std::time::Instant;
/// A fresh game at `seats` players, for the seat-count sweep.
fn fresh_n(seats: u8) -> GroundState {
GroundState::setup(
&Setup {
players: seats,
preset: format!("standard-{seats}p"),
patch: BTreeMap::new(),
},
42,
)
.expect("a standard deal")
}
fn fresh(seed: u64) -> GroundState {
GroundState::setup(
&Setup {
@ -2441,6 +2476,60 @@ mod replay_probe {
}
}
/// **GD-0001: group success is arithmetically unreachable below 5
/// seats**, and this is the reproduction rather than an argument.
///
/// The maintainer played several 3-player games on 2026-08-03 and
/// could not win any of them. This says why: claim *every* Problem
/// the deal puts in play, concede nothing, and the total still falls
/// short of GR-E01's threshold at 2, 3 and 4 seats.
///
/// It reads both numbers out of the engine — `problem_priorities`
/// (GR-S01's deal) and `threshold` (GR-E01) — so it cannot drift from
/// the rules it is testing, and it holds for **either dataset**: the
/// stand-in gives 3/6/10 and `Problems.csv` gives 4/6/9, against the
/// same 5/7/9.
///
/// **This test asserts the defect.** It is expected to keep passing
/// until ground-game rules, and to be inverted when it does — either
/// the deal count rises or the thresholds fall.
#[test]
fn gd0001_group_success_is_unreachable_below_five_seats() {
let mut verdicts = Vec::new();
for seats in 2..=6u8 {
let state = fresh_n(seats);
let best: u32 = state.problems.values().map(|p| u32::from(p.value)).sum();
let need = state.threshold();
verdicts.push((seats, state.problems.len(), best, need, best >= need));
println!(
" {seats}p: {} problem(s) worth {best} against a threshold of {need} \u{2014} {}",
state.problems.len(),
if best >= need {
"reachable"
} else {
"UNREACHABLE"
}
);
}
let unreachable: Vec<u8> = verdicts
.iter()
.filter(|(_, _, _, _, ok)| !ok)
.map(|(s, _, _, _, _)| *s)
.collect();
// Positive control: a run where everything is reachable would
// report a clean sheet and prove nothing.
assert!(
!verdicts.is_empty() && verdicts.iter().any(|(_, _, _, _, ok)| *ok),
"no seat count was reachable; the harness measured nothing useful"
);
assert_eq!(
unreachable,
vec![2, 3, 4],
"GD-0001 has changed: ground-game may have ruled. Re-read the \
finding before editing this test."
);
}
/// AM-7 scaling floor from GameKernel §5: fold throughput at 100k
/// events must be at least this fraction of throughput at 5k.
///

View file

@ -0,0 +1,160 @@
---
id: CB-RES-0007
capability: design.findings
status: draft — awaiting adversarial review (CB-WP-0022 T02)
tier: L
chaos: d8 = 6 → no override
---
# CB-RES-0007 — how rigorous rule systems record the ambiguity they find
CB-WP-0022 T01. Surveyed 2026-08-03.
**Not a survey of issue trackers.** The question is narrower: when a
system is formal enough to *notice* that its rules do not decide
something, what does it do with that observation? Three practices, and
ourselves.
---
## 1. The baseline is us, and it is measured
The external candidates are **practices, not runnable software**, so per
InnerLoop Step 1 their rows are **directional and cap at `parity`**. The
row that *can* be run is our own, and it is the one that matters, because
the register has to beat what we already do.
`tools/` harness output, 2026-08-03:
```
findings 6
with a runnable reproduction 2/6 = 33%
distinct files holding them 11
single register? NO — 11 files, no index
U1..U10: raised 2026-07-30, first READ 2026-08-03 — 4 days
U1..U10: answered? NO — 4+ days open, 0 of 10 ruled
```
| finding | locations | reproduction |
|---|---:|---|
| U1U10 underdetermined points | 1 | — |
| SOLVE on a face-down Problem | 2 | — |
| GR-A13 wasted SOLVE | 1 | — |
| GR-E01 unreachable below 5 seats | 3 | **yes** (scenario) |
| six provisional defaults | 6 | **yes** (scenarios) |
| GR-E03/GR-E04 never played | 1 | — |
### The uncomfortable number, stated before anyone else finds it
**The rule the declaration proposed — *no finding without its
reproduction* — would reject four of our six existing findings.**
That is exactly the objection T02 is instructed to press, and the survey's
job is to answer it rather than route around it. The answer is that
**none of the four is expensive to reproduce**:
- *SOLVE on a face-down Problem* — a scenario selecting SOLVE on a
face-down Problem and asserting no claim follows. The engine already
produced the behaviour; nobody wrote it down.
- *GR-A13 wasted SOLVE* — the same shape.
- *GR-E03/E04 never played* — a bot game to completion in each mode,
which `bot::play` already does.
- *U1U10* — six of the ten already have provisional scenarios; the
remaining four need the same treatment.
So the 33% is not evidence that reproductions are costly. **It is evidence
that nobody was ever asked for one**, which is the defect the register
exists to fix and not an argument against the rule.
## 2. Magic: the correction lands in the source, and the ruling is not authoritative
The most instructive practice, and it **corrected an assumption this pass
was about to build on**. I expected rulings to be the authoritative
resolution of an ambiguity. They are not.
The system is three layers, not two:
| layer | what it is | authoritative? |
|---|---|---|
| Comprehensive Rules | the general rules | yes |
| **Oracle** | the current authoritative text of each *card* — errata folded **in** | **yes** |
| Rulings (via Gatherer) | judge annotations attached to a card | **no***"reminder information with no actual weight or rules meaning"* |
When a card *"causes confusion, has outdated terminology, or doesn't work
as intended, the Oracle text may be updated"* — the fix goes **into the
authoritative text**, and the ruling layer stays explanatory.
**The property to steal: a finding closes when the source changes, not
when an annotation is added.** A register that accumulates permanent
rulings is a system that has given up on fixing its rules. Ours should be
a **queue that empties**, not an archive that grows.
This directly shapes the lifecycle in T03: `applied` must mean *the rules
text or the dataset changed and our provisional default was deleted* — not
*a ruling was recorded*.
*Directional, cited-only: the practice is described, not benchmarked.*
## 3. Model checkers: the counterexample IS the finding
A model checker does not report *"this property may not hold."* It emits a
**counterexample trace** — a concrete execution that violates the claim,
replayable by the tool that produced it.
That is precisely the shape the declaration proposed, arrived at
independently, and it is the strongest argument for the reproduction rule:
in formal methods a claim without a trace is not a result, it is a
hypothesis. Our `gr-e01-threshold-unreachable-2p` scenario is a
counterexample in exactly this sense — it is a failing-in-fact artifact
rather than a paragraph, and CB-EV-0005 already refused to delete it for
that reason.
**Property to beat: 100% of findings carry a replayable artifact.**
*Directional, cited-only.*
## 4. W3C / WHATWG: naming the undecided so it cannot be silently decided
Specifications mark points as *implementation-defined* or
*implementation-dependent*, which does two things at once: it tells an
implementer they may choose, and it tells the *spec* that a choice is
outstanding. The mark is machine-findable and survives revisions.
We already have this and it works: `provisional: true` +
`provisional_owner` on a scenario, surfaced by `make coverage` with an
age. It is the one piece of the machinery that is not missing.
**The register must reuse it, not compete with it.** Six of our findings
already live there; a second mechanism would immediately disagree with the
first.
*Directional, cited-only.*
## 5. Benchmarks to beat
| dimension | today | benchmark |
|---|---|---|
| **findability** | 11 files, no index | **one register with an index**; every finding reachable from one command |
| **reproducibility** | **2/6 = 33%** | **100%**, with withdrawn findings kept in the denominator |
| **closure** | 0 of 10 U-items ruled in 4+ days | a finding closes only when **the source changed** (Magic's Oracle property), and the register says which commit |
| **time raised → read** | **4 days** | the number this exists to fix; any mechanism that does not move it has failed |
| **taxonomy fits reality** | untested | all six existing findings expressible **without** a new kind being invented during backfill |
## 6. What the survey did not settle
- **Whether a finding without a reproduction is rejected or admitted as a
note.** §1 shows the rule is affordable for our six, but that is n=6 and
all six are *engine-surfaced*. A finding from **play** — *"the DARVO
sequence feels punishing at 2 players"* — may be real and have no cheap
artifact. T03 must decide, and the honest options are a `note` tier that
can never be reported as a finding, or refusal.
- **Where the register lives.** A finding is about *ground-game's* rules
but is produced by *clay-borg*. Putting it only in clay-borg repeats the
unread-inbox failure; putting it only in ground-game separates it from
its reproduction. Not decided here.
- **Whether the engine-evolution register is redundant.** The
declaration's judgment is that it is. This survey found nothing that
bears on it either way, which is itself worth saying: the practices
above are all about *rules*, and none of them is about how the tool that
found the problem evolved.

View file

@ -0,0 +1,48 @@
scenario: ground/gr-p05-solve-legality
description: >
GR-P05, ruled by ground-game 2026-08-03: SOLVE is legal only where it
can do something. P1 holds no Clarify and is refused Problem 1; P2
holds one, is admitted, and claims it.
The prior-round-claim half of GR-P05 is asserted in
`bot::tests::validate_enforces_all_four_solve_conditions` rather than
here, because reaching a second round costs a dozen commands to test
one rejection.
The rule is asserted here rather than only in `legal_commands`, because
a rule enforced by the offer alone constrains only clients that ask what
is legal — a scenario file would walk straight past it. That is what the
AM-1 coverage gate surfaced when GR-P05 was added with no scenario.
covers: [GR-P05, GR-A02, GR-P03]
provisional: false
seed: 42
setup:
players: 3
preset: standard-3p
patch:
"lead": 1
"players.0.hand": [{ suit: Change }]
"players.1.hand": [{ suit: Clarify }]
commands:
# 0 — P1 holds no Clarify: refused.
- actor: P1
cmd: select_action
args: { action: SOLVE, problem: 1 }
# 1 — P2 holds one: admitted.
- actor: P2
cmd: select_action
args: { action: SOLVE, problem: 1 }
- actor: P1
cmd: select_action
args: { action: INVESTIGATE, problem: 2 }
- actor: P3
cmd: select_action
args: { action: INVESTIGATE, problem: 3 }
- actor: SYSTEM
cmd: reveal
- actor: SYSTEM
cmd: resolve
expect:
rejects: [0]
state:
"problems.1.claimed_by": 1

View file

@ -177,6 +177,17 @@ terms.
printed value for scoring; a Problem can be claimed at most once.
- **GR-P04** Competing SOLVEs on the same Problem in the same round
resolve in Lead order (GR-R07); losers keep their Solution (GR-A02).
- **GR-P05** *(ruled by ground-game 2026-08-03, GROUND-WP-0002 T02)*
**SOLVE is offered only where it can do something.** A seat may select
SOLVE on a Problem only if it is face-up, non-Denied, **unclaimed**, and
the seat **holds a Solution of the required suit**. A Problem claimed in
a *prior* round is not a legal target; the same-round race of GR-P04 is
unaffected, because both seats select before either resolves.
*Not a bluff mechanic.* CB-WP-0018 raised the possibility that
selecting an unfulfillable SOLVE was an intended face-down bluff. The
ruling is that it was a filter bug: *"the player always knows their
hand… physical tables self-police the same way."*
## 9. Protection, Focus and Blame (GR-T)

View file

@ -87,8 +87,8 @@ rule 4).
a rule a scenario claims should also appear in the aggregate source, or
the claim rests on a tag and nothing else.
Measured 2026-07-31: **49 of 58** claimed rules are named in <!-- fact:gr_linked --><!-- fact:gr_rules -->
`games/ground/src/lib.rs`. **Unmet**, target 58. The nine unlinked:
Measured 2026-08-04: **50 of 59** claimed rules are named in <!-- fact:gr_linked --><!-- fact:gr_rules -->
`games/ground/src/lib.rs`. **Unmet**, target 59. The nine unlinked:
```text
GR-D07 GR-F02 GR-L03 GR-O03 GR-P01 GR-P02 GR-P03 GR-P04 GR-T01

93
tools/design-baseline.py Executable file
View file

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Baseline harness: how findable, reproducible and answered are the
design findings this project has already produced?
The comparator is US, today. The external candidates (rulings databases,
model-checker traces, W3C provisional marks) are practices rather than
runnable software, so per InnerLoop Step 1 their rows are DIRECTIONAL and
cap at `parity`. This is the row that can be measured.
"""
import os, re, subprocess, sys, datetime
ROOT = "/home/worsch/clay-borg"
os.chdir(ROOT)
# The findings this project has actually produced, and where each lives.
FINDINGS = {
"U1..U10 underdetermined points": ["specs/GroundRules.md"],
"SOLVE on a face-down Problem": ["workplans/CB-WP-0018-the-browser-is-a-client.md",
"evidence/CB-EV-0016-the-browser-is-a-client.md"],
"GR-A13 wasted SOLVE": ["evidence/CB-EV-0007-stage-0.md"],
"GR-E01 unreachable below 5 seats": ["evidence/CB-EV-0007-stage-0.md",
"scenarios/ground/gr-e01-threshold-unreachable-2p.yaml",
"workplans/CB-WP-0021-import-the-edition.md"],
"six provisional defaults": sorted(
os.path.join("scenarios/ground", f)
for f in os.listdir("scenarios/ground")
if f.endswith(".yaml")
and "provisional: true" in open(os.path.join("scenarios/ground", f)).read()),
"GR-E03/GR-E04 never played": ["evidence/CB-EV-0007-stage-0.md"],
}
def has_reproduction(paths):
"""A runnable thing: a scenario file, or a named test/command."""
for p in paths:
if p.startswith("scenarios/"):
return True
return False
def self_test():
"""The control that matters: a harness that read nothing must not
report a clean baseline. Every path this survey cites must exist, and
the reproduction test must be able to say NO one that answered yes
for everything would report 100% and look excellent."""
results = []
def check(name, ok, detail=""):
results.append((name, ok, detail))
missing = [p for paths in FINDINGS.values() for p in paths
if not os.path.exists(p)]
check("every cited location exists", not missing, ", ".join(missing[:3]))
check("the finding set is not empty", len(FINDINGS) >= 6, f"{len(FINDINGS)}")
check("reproduction detection can say NO",
not has_reproduction(["evidence/CB-EV-0007-stage-0.md"]),
"a detector that always says yes would report 100%")
check("reproduction detection can say YES",
has_reproduction(["scenarios/ground/gr-e01-threshold-unreachable-2p.yaml"]))
# The number this survey turns on, pinned so a later edit cannot move
# it silently: 2 of 6 today.
repro_now = sum(has_reproduction(v) for v in FINDINGS.values())
check("the measured baseline is 2 of 6", repro_now == 2, f"{repro_now}/6")
print("design-baseline self-test (positive control)")
ok = True
for name, passed, det in results:
print(f" [{'ok ' if passed else 'FAIL'}] {name}" + (f"{det}" if det else ""))
ok &= passed
return 0 if ok else 1
if "--self-test" in sys.argv:
raise SystemExit(self_test())
print("BASELINE — design findings as they stand, 2026-08-03\n")
places = set()
repro = 0
for name, paths in FINDINGS.items():
places.update(paths)
r = has_reproduction(paths)
repro += r
print(f" {'repro' if r else ' - '} {len(paths)} location(s) {name}")
n = len(FINDINGS)
print(f"\n findings {n}")
print(f" with a runnable reproduction {repro}/{n} = {100*repro//n}%")
print(f" distinct files holding them {len(places)}")
print(f" single register? NO — {len(places)} files, no index")
# Time from raised to READ, for the one finding with a timestamp trail.
raised = datetime.date(2026, 7, 30) # hub message from clay-borg-custodian
read = datetime.date(2026, 8, 3) # marked read this session
print(f"\n U1..U10: raised {raised}, first READ {read}{(read-raised).days} days")
print(f" U1..U10: answered? NO — {(read-raised).days}+ days open, 0 of 10 ruled")

View file

@ -0,0 +1,189 @@
---
id: CB-WP-0021
kind: product
title: "Import the edition: the game plays its own data"
status: ready
---
# Purpose
```
structural tier M (adds or refuses an external dependency, and changes
how a game is set up — a canonical interface)
chaos d8 = 7 → no override
declared tier M
```
Declaration 4 of chaos window 2.
## The engine has been playing a stand-in
`games/ground/src/lib.rs` builds Problems with `value: priority` and suits
cycled by index. `editions/ground-darvo-r0/Problems.csv` has carried the
real thing since 2026-07-31, and **GROUND-WP-0002 T01 ruled it
authoritative on 2026-08-03**:
| | problems/scenario | values | total | suits |
|---|---:|---|---:|---|
| `Problems.csv` | **5** | 2, 2, 2, 3, 3 | **12** | `required_solution` per problem |
| the stand-in | 3 | 1, 2, 3 | **6** | cycled by index |
## CORRECTION, before any code: the import does not fix GR-E01
**This declaration opened by claiming it would, and the data says
otherwise.** The claim was *"ordinary against 12, unreachable against 6 —
it was never a rules gap."* Measured across all four scenarios:
**GR-S01 deals 2 / 3 / 4 problems by player count, not all five.** So the
points actually in play are never 12:
| players | dealt | available | GR-E01 threshold | |
|---:|---:|---:|---:|---|
| 2 | 2 | **4** | 5 | unreachable |
| 34 | 3 | **6** | 7 | unreachable |
| 56 | 4 | **9** | 9 | reachable, exactly |
Identical in shape to the stand-in, which gave 3 / 6 / 10 against the same
5 / 7 / 9. **So `GR-E01 unreachable below 5 seats` is a real property of
the game, not an artifact of the stand-in**, and
`gr-e01-threshold-unreachable-2p` is asserting something true.
The error was mine and it was the cheap kind to make: 12 points exist in
the file, so I assumed 12 points are in play. **One command over the CSV
settled it, and it was not run until after the declaration was
committed** — the characteristic error of this project, in the pass that
followed a ruling obtained *because* of it.
**CB-EV-0018 is corrected too.** It said the `0` scores were *"confirmed
as the stand-in's doing, not a scoring bug."* Overstated: the zero came
from no Problem being claimed at all, and the threshold gap is
independent of which dataset is loaded.
**What this changes.** The import is still right — the suits, values and
visibility are authoritative and the engine should stop inventing them.
But it resolves nothing about GR-E01, and T03 must send ground-game a
*sharper* question rather than a retirement: **either GR-S01's deal count
is wrong or GR-E01's thresholds are, and no dataset can reconcile them.**
## The constraint, measured before declaring
**AM-4a has 3,798 lines of headroom.** A CSV crate costs, marginally
against the shipped graph:
| crate | lines |
|---|---:|
| `csv` | 14,291 |
| `csv-core` | 3,360 |
| `ryu` | 3,962 |
| **marginal total** | **21,613** |
**5.7× the available headroom.** `itoa`, `memchr`, `serde` and
`serde_core` are already present and cost nothing; the parser itself is
what does not fit.
So the shipped runtime cannot gain a CSV parser, and that is settled by
measurement rather than by preference. What remains is *which* of the
alternatives to take, and that is the ADR.
## Task: decide how the data reaches the game
```task
id: CB-WP-0021-T01
status: todo
priority: high
```
Write `decisions/ADR-0011-*.md` (tier M: survey and decision in one).
**Three questions, and the third is the one that bites.**
**(a) Where does the data live?** `ground-game` is a separate repository.
Depending on a sibling checkout makes the build depend on a path that may
not exist; vendoring a copy makes clay-borg carry content it does not own.
Whichever is chosen must say how a copy is known to be current, because a
silently stale copy is worse than no copy.
**(b) How is it parsed, given a CSV parser does not fit?** Candidates, and
each needs its marginal cost stated rather than assumed:
- hand-rolled reader in `games/ground` — small, but it is a parser we
then own, and `problem_text` contains commas;
- **bake it**: a dev-time step converts the CSV into a generated Rust
module or compact literal, so the shipped runtime parses **nothing**.
Then the generated artifact must be proven to match its source, which
is the DFD class this repo already has machinery for;
- load nothing at runtime and treat the data as scenario input.
**(c) What does this do to determinism?** Problem values and required
suits become part of `GroundState`, which is hashed (K7). **Every recorded
state hash changes.** Before writing code, establish what actually pins a
hash today — `make sim` runs 25 scenarios, `replay-test` re-executes
bundles, and AM-7's probe asserts per-segment hashes. Say which of those
break, and whether any of them are *supposed* to be stable across a
content change.
**This is the reason the ADR exists.** A content import that quietly
invalidates every recorded hash, in a project whose central invariant is
replay determinism, is not a data-loading change.
## Task: import it, and let the thresholds mean something
```task
id: CB-WP-0021-T02
status: todo
priority: high
```
Replace the stand-in. `setup` must build Problems from the edition data:
`point_value`, `required_solution`, `visibility` (Surface → face up,
Hidden → face down) and `hidden_priority`.
**Controls:**
- **The stand-in must become unreachable.** A test that the fixture
constants no longer appear — a `value: priority` that survives beside
real data is a fallback nobody will notice until the numbers look odd
again.
- **A scenario must be able to reach GR-E01's threshold at 2 players**,
which is the specific thing that was impossible. Assert the arithmetic,
not just that a game runs.
- The 5-problem shape must survive: the stand-in dealt 3, and code that
assumed 3 will not announce itself.
## Task: retire the rules gap that was never one
```task
id: CB-WP-0021-T03
status: todo
priority: medium
```
`gr-e01-threshold-unreachable-2p` is tagged `provisional: true` with
`provisional_owner: ground-game`, and its description says group success
*"is unreachable at 2, 3 and 4 players"*. If T02 lands, that scenario is
asserting a property of the **stand-in**, not of the game.
Retire or rewrite it, and **say which of the six provisional items this
resolves**, so GROUND-WP-0002 T03 shrinks rather than being left to
rediscover it. Message `ground-game` with the outcome — the last such
message sat unread for four days because nothing pointed at it.
Do **not** quietly delete a failing-in-fact scenario. CB-EV-0005: *a score
improved by deleting the question is not an improvement.*
## Task: evidence
```task
id: CB-WP-0021-T04
status: todo
priority: high
```
`evidence/CB-EV-0019-*.md`.
- **What the import cost**, against the 3,798 lines that were available.
- **What broke**, especially hashes, and whether the blast radius was
predicted in T01 or discovered in T02. If it was discovered, say so —
that is the ADR having missed something.
- **Whether the endings now mean anything**: play one and report the
score against the threshold.
- **Quote CB-WP-0020's cost by re-running the instrument.**
- **Chaos: 4 of 12 in window 2.**

View file

@ -0,0 +1,271 @@
---
id: CB-WP-0022
kind: product
title: "The design instrument: findings about the game, with their reproductions"
status: active
---
# Purpose
```
structural tier L (named a high-leverage pass by the maintainer, and it
amends INTENT — clay-borg gains a stated aspect)
chaos d8 = 6 → no override
declared tier L
```
Declaration 5 of chaos window 2. Tier L: separate survey, **adversarial
review**, ADR, then spec, then code.
## The insight, in the maintainer's words
> *"We should consider ourselves testing the game and document
> inconsistencies to report them back to the ground-game repo, so that the
> game designer can improve the rules accordingly… We should have the
> rigorous game simulation engine and a meta scope to capture notes about
> game design flaws, questions, results and protocols about trial games…
> This will provide clay-borg an additional aspect as a valuable game
> design tool."*
**This is already happening and has no home.** In five passes the engine
has produced, as a by-product of being rigorous:
| finding | how it surfaced |
|---|---|
| ten underdetermined rules points (U1U10) | formalizing the dataset into testable rules |
| SOLVE offered on a face-down Problem, always inert | a human dragging it three rounds running |
| GR-A13 "wasted SOLVE" on a claimed Problem | a scenario that had to pick a default |
| GR-E01 unreachable below 5 seats | arithmetic over the deal count |
| six provisional scenario defaults | scenarios that could not be written without deciding something |
| GR-E03 / GR-E04 never played to the end | nobody noticed for nineteen passes |
Every one was found by *building the simulator*, not by playing. That is
the thing worth naming: **a simulator rigorous enough to refuse ambiguity
is a design instrument, because it cannot proceed past a rule that does
not decide.**
And every one of them has been carried in prose, in six different places,
and one sat unread in an inbox for four days.
## The load-bearing rule this must have
The project's standing failure is *unexecuted verification*. A design
register that collects opinions would reproduce it in a new medium.
> **A design finding is not admissible without its reproduction.**
Concretely: a scenario that fails, an arithmetic check that prints the
contradiction, a recorded game the reader can replay, or a named test.
*"This feels unbalanced"* is a note, not a finding. **GR-E01 is admissible
because 4/6/9 against 5/7/9 is a computation anyone can rerun; the SOLVE
inertness is admissible because a recorded session shows three no-ops.**
This is what would make clay-borg a design tool rather than a suggestion
box, and it is the one part of this proposal that must not be traded away
for convenience.
## The judgment I want reviewed, not assumed
The maintainer asked whether this should extend to *"a meta about the
clay-borg engine evolution itself."*
**My answer is no, and it should be argued rather than accepted.** That
register already exists and is load-bearing: `evidence/CB-EV-*` records
what each pass found, `decisions/ADR-*` records what was decided and what
was rejected, `gates.toml` records what every control has caught and what
would retire it, and workplans record what was attempted. Nineteen passes
of engine evolution are already captured, with dates, costs and
falsifiers.
**Building a second register for the same subject would be ceremony**, and
this project has a standing rule that a gate must cash out. The asymmetry
is the point: engine evolution has a home and game design does not.
If the adversarial review disagrees, that is exactly the kind of thing
tier L exists to surface.
## Task: survey how this is done elsewhere, and what we already have
```task
id: CB-WP-0022-T01
status: done
priority: high
```
`research/CB-RES-0007-*.md`.
**Do not survey issue trackers.** The question is narrower and more
interesting: how do rigorous rule systems record *the ambiguity they
found*? Candidates worth a benchmark-to-beat:
- **Errata and rulings practice** in published games (Magic's
comprehensive-rules + rulings split, Netrunner's NAPD card rulings) —
what makes a ruling *findable* years later.
- **Formal-methods counterexample traces** — a model checker's output is
precisely a reproduction attached to a claim, which is the shape wanted
here.
- **Conformance-suite provisional behaviour** — how W3C/WHATWG mark
"implementation-defined" and how a spec later absorbs it.
- **What this repo already has**: `provisional: true` scenarios,
`§Underdetermined`, `gates.toml`'s `caught`/`retire_if` shape, and the
hub message that went unread. **The register must reuse the provisional
machinery rather than compete with it.**
Name, per dimension, the property to beat — findability, reproducibility,
and whether a ruling can *close* a finding mechanically.
**Done 2026-08-03.**
[CB-RES-0007](../research/CB-RES-0007-design-instrument.md), with a
runnable baseline (`tools/design-baseline.py`).
**The baseline is us, and it is measured**: 6 findings across **11 files**
with no index, **2 of 6 (33%) with a runnable reproduction**, and U1U10
raised 2026-07-30, first *read* 2026-08-03 — **4 days**, 0 of 10 ruled.
**The uncomfortable number is stated up front rather than left for the
review to find: the reproduction rule would reject four of our six
existing findings.** The survey answers it — none of the four is
expensive to reproduce, so the 33% is evidence that nobody was ever asked
for one, not that the rule is unaffordable.
**Magic corrected an assumption this pass was about to build on.** I
expected a ruling to be the authoritative resolution. It is not: rulings
are *"reminder information with no actual weight or rules meaning"*, and
the authoritative fix folds into the **Oracle** card text. **A finding
closes when the source changes, not when an annotation is added** — so
the register must be a queue that empties, not an archive that grows.
That is now a constraint on T03's lifecycle.
Model checkers supplied the reproduction rule independently (a
counterexample trace *is* the finding), and W3C's *implementation-defined*
mark is the one piece of machinery we already have and must reuse rather
than duplicate.
## Task: adversarial review
```task
id: CB-WP-0022-T02
status: todo
priority: high
```
Tier L requires it. Give the reviewer the survey **and** the §judgment
above, and require an attempt at:
- **that the engine-evolution register is redundant** — the strongest
counter is that ADRs record *decisions* and evidence records *findings*,
but nothing records *what we learned about building engines*, which is a
third thing;
- **that "carries its reproduction" is affordable** — if half the real
findings cannot be reproduced cheaply, the rule will be quietly dropped
and the register becomes a suggestion box anyway;
- **that a register is needed at all**, rather than one more section in
`GroundRules.md §Underdetermined`, which already exists and already
works.
Record the trail in `history/`, unpolished.
## Task: decide
```task
id: CB-WP-0022-T03
status: todo
priority: high
```
`decisions/ADR-0012-*.md`. At minimum:
- whether clay-borg's **INTENT gains a stated aspect** as a design
instrument, and in what words — this is the change with the longest
half-life in the pass;
- the finding **taxonomy**, and it should be grounded in the six findings
above rather than invented: *underdetermined* (rules do not say),
*inconsistent* (rules disagree with each other or the data), *inert* (a
rule that cannot fire), *degenerate* (fires, but collapses play),
*unplayed* (implemented, never played);
- the **lifecycle** and who owns each state: raised → reported → ruled →
applied, or withdrawn;
- whether a finding without a reproduction is **rejected** or **admitted
as a note** — and if admitted, how it is prevented from aging into an
apparent finding.
## Task: specify
```task
id: CB-WP-0022-T04
status: todo
priority: high
```
`specs/GameDesign.md`, with metrics, because a spec without them is prose.
Candidate measures, to be argued not adopted:
- **findings with a runnable reproduction** — target 100%, and the
denominator includes withdrawn ones;
- **time from raised to reported** — the U-items took four days to be
*read*; that is the number this exists to fix;
- **findings closed by a ruling** vs **findings still open**, with age.
Also specify the **trial protocol**, which is the part with no precedent
here: a trial game is a `--record`ed session plus an observation log, so
*"we played it and X happened"* is replayable rather than remembered. The
engine already records sessions as scenarios; a trial is that plus notes,
and it must cost almost nothing or it will not be done.
## Task: build it, and backfill what is already known
```task
id: CB-WP-0022-T05
status: todo
priority: high
```
The register, the tool, and then **the six findings above entered into
it** — backfilling is the test. A register that cannot express findings
the project already has is the wrong register, and discovering that after
designing it is the point of doing it in this order.
`make design` (or equivalent) must report: open findings by kind, those
without a reproduction, and those never reported to their owner.
## Task: report to ground-game, mechanically
```task
id: CB-WP-0022-T06
status: todo
priority: high
```
Generate the report and send it. **The message that sat unread for four
days is the baseline to beat**, and the failure was not the message — it
was that nothing pointed at it and nothing tracked whether it was
answered.
So the report must land somewhere that persists: a file in `ground-game`
under its own workplan, not only an inbox entry. GROUND-WP-0002 already
holds the ten U-items; this should extend it rather than duplicate it.
Include the two sharpened findings this pass has already produced:
- **GR-E01 vs GR-S01** — the deal count puts 4/6/9 points in play against
thresholds of 5/7/9, so either the count or the thresholds are wrong and
no dataset reconciles them;
- **SOLVE's legality** against a face-down Problem or an unmatchable suit.
## Task: evidence
```task
id: CB-WP-0022-T06B
status: todo
priority: high
```
`evidence/CB-EV-0020-*.md`.
- **Whether backfilling changed the design** — if all six findings fit the
first taxonomy, say so and be suspicious of it.
- **What tier L cost against what it caught**, since this is the second
full-weight L pass and CB-WP-0012's deleted its own structural trigger.
- **The engine-evolution question**, as the review left it.
- **Quote CB-WP-0021's cost by re-running the instrument.**

View file

@ -0,0 +1,134 @@
---
id: CB-WP-0023
kind: product
title: "SOLVE is offered only where it can do something"
status: done
---
# Purpose
```
structural tier S (implements a ruling inside an existing capability —
no new port, no canonical interface, no dependency)
chaos d8 = 6 → no override
declared tier S
```
Declaration 6 of chaos window 2.
## A ruling, not a judgment call
`ground-game` ruled SOLVE's legality on 2026-08-03 (GROUND-WP-0002 T02),
after clay-borg raised it from play:
| sub | ruling |
|---|---|
| **(a)** face-down Problem | **Not offered — illegal target.** *"only face-up, non-Denied. Browser no-ops were a filter bug, not a bluff mechanic."* |
| **(b)** no matching Solution in hand | **Not offered.** *"the engine only offers SOLVE when a matching suit is held."* |
| **(c)** already-claimed Problem | **Same-round race legal; prior-round claim not offered.** |
**The bluff reading is dead.** CB-WP-0018 raised (a) as *possibly* an
intended bluff in a commit/reveal game and deferred to ground-game rather
than deciding. The answer is that it was a filter bug, and the engine has
been offering an inert move since the day `legal_commands` was written.
`legal_commands` offers Investigate and SOLVE on **every** problem key,
with no reference to `face_up`, `denied`, `claimed_by` or the hand.
**Only SOLVE is ruled on. Investigate is not touched** — implementing more
than was ruled would be inventing rules, which is what this whole exchange
exists to stop.
## Task: filter SOLVE to what the ruling allows
```task
id: CB-WP-0023-T01
status: done
priority: high
```
SOLVE is offered on problem `p` for seat `s` **iff** `p.face_up`,
`!p.denied`, `p.claimed_by.is_none()`, and `s`'s hand holds a card of
`p.suit`.
On (c): at Select time every `claimed_by` is from a prior round — claims
land at Resolve — so *"prior-round claim not offered"* is exactly
`claimed_by.is_none()`, and the same-round race is already legal because
both seats select before either resolves. **Say this in the code**, or the
next reader will add a round comparison that does nothing.
**Controls:**
- each of the four conditions must be independently mutation-provable —
drop one filter, and a test naming *that* condition goes red;
- the maintainer's reported case must be reproduced and then fixed: SOLVE
on a face-down Problem is offered before, not offered after;
- **`make sim` must still pass**, or the ruling has broken a scenario that
encoded the old behaviour — in which case the scenario was encoding a
bug and must be updated with a note, not quietly edited.
**Done 2026-08-04.** `make all` exits 0, 26 scenarios, rule coverage
**59/59**. **No scenario encoded the bug** — all 25 passed unchanged.
**The rule ended up somewhere other than where I put it, and a gate moved
it.** It went into `legal_commands` first. The AM-1 coverage gate then
demanded a scenario for the new GR-P05 — and scenarios drive `validate`,
not the offer layer. That is what showed the rule belonged in `validate`:
**a rule enforced only by the offer is enforced only for clients that ask
what is legal.** The browser would have been filtered and a scenario file
would have walked straight past it.
Once GR-P05 moved into `validate`, every condition in `legal_commands`
was dead code, and my own layering test said so in those words
(*"validate now rejects claimed — drop the filter"*). The offer layer is
back to one arm for Investigate and SOLVE together.
**And the reported case was not the one I reported.** CB-WP-0018 and the
message to ground-game described SOLVE offered on a **face-down** Problem.
Measured: `validate` already rejected face-down, so it never was offered.
Problem 1 is the Surface Problem and is face-up from the deal — the
maintainer's three inert SOLVEs were the **hand** case, holding no Clarify
for a Clarify Problem. The ruling covers both, so nothing is invalidated,
but the record was wrong and is corrected here.
Four conditions, each asserted separately, because one "SOLVE is filtered"
test would pass with three of four implemented. Mutations: dropping the
claimed filter and dropping the hand filter each turn their own test red;
inverting the layering assertion turns `validate_enforces_all_four` red.
## Task: retire what the ruling settles
```task
id: CB-WP-0023-T02
status: done
priority: medium
```
Update `specs/GroundRules.md` §Underdetermined for the SOLVE items, and
retire the provisional flags the ruling closes. **A ruling that lands in
code but not in the spec leaves the next reader with two sources.**
Report to `ground-game` that the ruling is implemented — closing the loop
is the part that has failed twice.
**Done 2026-08-04.** `specs/GroundRules.md` gains **GR-P05** stating the
rule and recording that the bluff reading is dead, and
`scenarios/ground/gr-p05-solve-legality.yaml` covers it — P1 refused for
holding no Clarify, P2 admitted and claiming. The prior-round-claim half
is asserted in the unit test instead, because reaching a second round
costs a dozen commands to test one rejection, and that is said in the
scenario rather than left as a gap.
## Task: evidence
```task
id: CB-WP-0023-T03
status: done
priority: high
```
`evidence/CB-EV-0020-*.md`. Short.
- **How long the inert move survived**, and what it cost: a maintainer
played SOLVE three rounds running with no effect and no explanation.
- **Whether any scenario encoded the bug.**
- **Quote CB-WP-0022's cost by re-running the instrument.**