diff --git a/Makefile b/Makefile index 1d77698..e0ddc1d 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/evidence/CB-EV-0018-the-table-you-can-read.md b/evidence/CB-EV-0018-the-table-you-can-read.md index cae0c35..2f3df58 100644 --- a/evidence/CB-EV-0018-the-table-you-can-read.md +++ b/evidence/CB-EV-0018-the-table-you-can-read.md @@ -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 3–4p with + either dataset. - **`ground-game` owes ten rulings** (U1–U10) and SOLVE's legality. - **Chaos: 3 of 12 in window 2, one override, changed nothing.** diff --git a/facts.toml b/facts.toml index bc2eac8..a02b304 100644 --- a/facts.toml +++ b/facts.toml @@ -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" diff --git a/games/ground/src/bot.rs b/games/ground/src/bot.rs index 551cad9..bedfe2c 100644 --- a/games/ground/src/bot.rs +++ b/games/ground/src/bot.rs @@ -153,6 +153,17 @@ pub fn legal_commands(state: &GroundState, seat: PlayerId) -> Vec 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 { + 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 = 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> { (0..n) .map(|i| -> Box { diff --git a/games/ground/src/lib.rs b/games/ground/src/lib.rs index 20fba88..4fbe666 100644 --- a/games/ground/src/lib.rs +++ b/games/ground/src/lib.rs @@ -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 = 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. /// diff --git a/research/CB-RES-0007-design-instrument.md b/research/CB-RES-0007-design-instrument.md new file mode 100644 index 0000000..b709f1d --- /dev/null +++ b/research/CB-RES-0007-design-instrument.md @@ -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 | +|---|---:|---| +| U1–U10 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. +- *U1–U10* — 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. diff --git a/scenarios/ground/gr-p05-solve-legality.yaml b/scenarios/ground/gr-p05-solve-legality.yaml new file mode 100644 index 0000000..42e35b1 --- /dev/null +++ b/scenarios/ground/gr-p05-solve-legality.yaml @@ -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 diff --git a/specs/GroundRules.md b/specs/GroundRules.md index 620b120..2ffad75 100644 --- a/specs/GroundRules.md +++ b/specs/GroundRules.md @@ -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) diff --git a/specs/MetricsAndScenarios.md b/specs/MetricsAndScenarios.md index 14d57fd..f13ffea 100644 --- a/specs/MetricsAndScenarios.md +++ b/specs/MetricsAndScenarios.md @@ -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 -`games/ground/src/lib.rs`. **Unmet**, target 58. The nine unlinked: +Measured 2026-08-04: **50 of 59** claimed rules are named in +`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 diff --git a/tools/design-baseline.py b/tools/design-baseline.py new file mode 100755 index 0000000..6e3f83c --- /dev/null +++ b/tools/design-baseline.py @@ -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") diff --git a/workplans/CB-WP-0021-import-the-edition.md b/workplans/CB-WP-0021-import-the-edition.md new file mode 100644 index 0000000..39c16a9 --- /dev/null +++ b/workplans/CB-WP-0021-import-the-edition.md @@ -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 | +| 3–4 | 3 | **6** | 7 | unreachable | +| 5–6 | 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.** diff --git a/workplans/CB-WP-0022-the-design-instrument.md b/workplans/CB-WP-0022-the-design-instrument.md new file mode 100644 index 0000000..d91ddd5 --- /dev/null +++ b/workplans/CB-WP-0022-the-design-instrument.md @@ -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 (U1–U10) | 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 U1–U10 +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.** diff --git a/workplans/CB-WP-0023-solve-legality.md b/workplans/CB-WP-0023-solve-legality.md new file mode 100644 index 0000000..845efe6 --- /dev/null +++ b/workplans/CB-WP-0023-solve-legality.md @@ -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.**