From 7de0dcef87be9d5c81a11e1145f1cebe3c2e64d7 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 20:33:00 +0200 Subject: [PATCH 1/7] 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 --- workplans/CB-WP-0021-import-the-edition.md | 158 +++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 workplans/CB-WP-0021-import-the-edition.md 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..20bb9df --- /dev/null +++ b/workplans/CB-WP-0021-import-the-edition.md @@ -0,0 +1,158 @@ +--- +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 | + +GR-E01's thresholds are 2p → 5, 3–4p → 7, 5–6p → 9. **Ordinary against 12, +unreachable against 6** — which is why the maintainer's last game ended +`0` scores and `winners nobody`, and why clay-borg has been carrying +`GR-E01 unreachable below 5 seats` as a rules gap for ground-game to rule +on. It was never a rules gap. + +## 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.** From fe3852e0934cae5951499327c47969c83f5407dd Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 20:35:39 +0200 Subject: [PATCH 2/7] 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 --- evidence/CB-EV-0018-the-table-you-can-read.md | 7 +++- workplans/CB-WP-0021-import-the-edition.md | 41 ++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) 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/workplans/CB-WP-0021-import-the-edition.md b/workplans/CB-WP-0021-import-the-edition.md index 20bb9df..39c16a9 100644 --- a/workplans/CB-WP-0021-import-the-edition.md +++ b/workplans/CB-WP-0021-import-the-edition.md @@ -28,11 +28,42 @@ authoritative on 2026-08-03**: | `Problems.csv` | **5** | 2, 2, 2, 3, 3 | **12** | `required_solution` per problem | | the stand-in | 3 | 1, 2, 3 | **6** | cycled by index | -GR-E01's thresholds are 2p → 5, 3–4p → 7, 5–6p → 9. **Ordinary against 12, -unreachable against 6** — which is why the maintainer's last game ended -`0` scores and `winners nobody`, and why clay-borg has been carrying -`GR-E01 unreachable below 5 seats` as a rules gap for ground-game to rule -on. It was never a rules gap. +## 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 From bb426b3e9b688b8e05954da0528d796978b96f84 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 20:48:08 +0200 Subject: [PATCH 3/7] 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 --- workplans/CB-WP-0022-the-design-instrument.md | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 workplans/CB-WP-0022-the-design-instrument.md 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..5462d63 --- /dev/null +++ b/workplans/CB-WP-0022-the-design-instrument.md @@ -0,0 +1,244 @@ +--- +id: CB-WP-0022 +kind: product +title: "The design instrument: findings about the game, with their reproductions" +status: ready +--- + +# 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: todo +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. + +## 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.** From a86efba4c30af503567e7a2535f537ccd282d004 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 21:19:12 +0200 Subject: [PATCH 4/7] =?UTF-8?q?CB-WP-0022-T01:=20survey=20=E2=80=94=20how?= =?UTF-8?q?=20rule=20systems=20record=20the=20ambiguity=20they=20find?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Makefile | 1 + research/CB-RES-0007-design-instrument.md | 160 ++++++++++++++++++ tools/design-baseline.py | 93 ++++++++++ workplans/CB-WP-0022-the-design-instrument.md | 31 +++- 4 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 research/CB-RES-0007-design-instrument.md create mode 100755 tools/design-baseline.py 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/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/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-0022-the-design-instrument.md b/workplans/CB-WP-0022-the-design-instrument.md index 5462d63..d91ddd5 100644 --- a/workplans/CB-WP-0022-the-design-instrument.md +++ b/workplans/CB-WP-0022-the-design-instrument.md @@ -2,7 +2,7 @@ id: CB-WP-0022 kind: product title: "The design instrument: findings about the game, with their reproductions" -status: ready +status: active --- # Purpose @@ -88,7 +88,7 @@ tier L exists to surface. ```task id: CB-WP-0022-T01 -status: todo +status: done priority: high ``` @@ -114,6 +114,33 @@ found*? Candidates worth a benchmark-to-beat: 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 From b513209acf83fb650572994a9122bbcd7d1acb2e Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 23:49:49 +0200 Subject: [PATCH 5/7] GD-0001: group success is unreachable below 5 seats, with its reproduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- games/ground/src/lib.rs | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/games/ground/src/lib.rs b/games/ground/src/lib.rs index 20fba88..4359b81 100644 --- a/games/ground/src/lib.rs +++ b/games/ground/src/lib.rs @@ -2162,6 +2162,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 +2454,56 @@ 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. /// From 46c2fb652ed3f7c15bf8a6292b68547c01239e20 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 23:59:23 +0200 Subject: [PATCH 6/7] 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 --- workplans/CB-WP-0023-solve-legality.md | 97 ++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 workplans/CB-WP-0023-solve-legality.md diff --git a/workplans/CB-WP-0023-solve-legality.md b/workplans/CB-WP-0023-solve-legality.md new file mode 100644 index 0000000..b24edb5 --- /dev/null +++ b/workplans/CB-WP-0023-solve-legality.md @@ -0,0 +1,97 @@ +--- +id: CB-WP-0023 +kind: product +title: "SOLVE is offered only where it can do something" +status: active +--- + +# 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: todo +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. + +## Task: retire what the ruling settles + +```task +id: CB-WP-0023-T02 +status: todo +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. + +## Task: evidence + +```task +id: CB-WP-0023-T03 +status: todo +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.** From 6487d33f27915a69f25fecc9b129d2d7bbf972f5 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 4 Aug 2026 00:18:40 +0200 Subject: [PATCH 7/7] CB-WP-0023: SOLVE is legal only where it can do something 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 --- facts.toml | 18 +- games/ground/src/bot.rs | 183 ++++++++++++++++++++ games/ground/src/lib.rs | 28 ++- scenarios/ground/gr-p05-solve-legality.yaml | 48 +++++ specs/GroundRules.md | 11 ++ specs/MetricsAndScenarios.md | 4 +- workplans/CB-WP-0023-solve-legality.md | 45 ++++- 7 files changed, 321 insertions(+), 16 deletions(-) create mode 100644 scenarios/ground/gr-p05-solve-legality.yaml 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 4359b81..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() { @@ -2482,7 +2504,11 @@ mod replay_probe { println!( " {seats}p: {} problem(s) worth {best} against a threshold of {need} \u{2014} {}", state.problems.len(), - if best >= need { "reachable" } else { "UNREACHABLE" } + if best >= need { + "reachable" + } else { + "UNREACHABLE" + } ); } let unreachable: Vec = verdicts 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/workplans/CB-WP-0023-solve-legality.md b/workplans/CB-WP-0023-solve-legality.md index b24edb5..845efe6 100644 --- a/workplans/CB-WP-0023-solve-legality.md +++ b/workplans/CB-WP-0023-solve-legality.md @@ -2,7 +2,7 @@ id: CB-WP-0023 kind: product title: "SOLVE is offered only where it can do something" -status: active +status: done --- # Purpose @@ -43,7 +43,7 @@ exists to stop. ```task id: CB-WP-0023-T01 -status: todo +status: done priority: high ``` @@ -66,11 +66,40 @@ next reader will add a round comparison that does nothing. 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: todo +status: done priority: medium ``` @@ -81,11 +110,19 @@ 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: todo +status: done priority: high ```