From 631fb41fc2895b4105843da27851b8778c4fa1bc Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 7 Aug 2026 09:54:20 +0200 Subject: [PATCH 1/2] CB-WP-0029 T01-T03: components you can count, and a supply that does not bind ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule IS the twelve-token supply written twice, which is the shape of a supply needing no separate enforcement. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a seat's own placed Focus against its OWN blame_from -- but that list holds OTHER players' discs, so they are different tokens. It reported 2 conflicts; corrected, it reports 0. Fifth instance of this project's recurring defect, a number computed correctly about the wrong subject, and the first caught before it left the repo rather than by a reviewer. D2: a token is a VIEW, not a type. The aggregate gains no `Token` -- adding one would create a second source of truth for Stress, and the first time they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token would enforce something nobody ruled -- CB-WP-0023's error inverted: SOLVE was OFFERED where it could not act; this would REFUSE where the rules allow. The check ships as a standing control, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 --- WORK-RECORDS.md | 5 + crates/cb-render-html/src/doc.rs | 132 ++++++++++++++- crates/cb-render-html/src/lib.rs | 92 +++++++++- decisions/ADR-0016-the-tokens-on-the-table.md | 132 +++++++++++++++ editions/ground-darvo-r0/PROVENANCE.md | 2 + editions/ground-darvo-r0/Tokens.csv | 10 ++ games/ground/src/edition.rs | 159 ++++++++++++++++++ specs/FindingRegister.md | 16 ++ trials/2026-08-06-2252.yaml | 89 +++++----- .../CB-WP-0029-the-tokens-on-the-table.md | 77 ++++++++- 10 files changed, 657 insertions(+), 57 deletions(-) create mode 100644 decisions/ADR-0016-the-tokens-on-the-table.md create mode 100644 editions/ground-darvo-r0/Tokens.csv diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index bfb9292..f39dd25 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -36,6 +36,7 @@ | workplan | CB-WP-0026 | done | — | workplans/CB-WP-0026-collect-the-rulings.md | | workplan | CB-WP-0027 | done | — | workplans/CB-WP-0027-the-commentary-track.md | | workplan | CB-WP-0028 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md | +| workplan | CB-WP-0029 | ready | — | workplans/CB-WP-0029-the-tokens-on-the-table.md | | task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md | | task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md | | task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md | @@ -189,3 +190,7 @@ | task | CB-WP-0028-T06 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md | | task | CB-WP-0028-T07 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md | | task | CB-WP-0028-T08 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md | +| task | CB-WP-0029-T01 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md | +| task | CB-WP-0029-T02 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md | +| task | CB-WP-0029-T03 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md | +| task | CB-WP-0029-T04 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md | diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs index ac9a5ed..6cc199f 100644 --- a/crates/cb-render-html/src/doc.rs +++ b/crates/cb-render-html/src/doc.rs @@ -474,6 +474,111 @@ fn piles_body(out: &mut String, view: &GroundView) { } } +/// A marker on a track, drawn at `(x, y)` with `n` stops. +/// +/// **The track is the point** (ADR-0016 D4). `stress 5` is a fact you +/// read; a marker at the end of a 0-5 track is a fact you see coming -- +/// and DARVO triggers at Stress 5, so "one more Attack and I trigger" is +/// the most useful thing the page can show. +fn track_svg(out: &mut String, x: f64, y: f64, stops: usize, at: usize, hot: bool, label: &str) { + let step = 11.0; + let _ = write!(out, "{}", esc(label)); + for i in 0..stops { + let cx = x + (i as f64) * step; + let here = i == at; + let _ = write!( + out, + "", + r = if here { 4.5 } else { 2.5 }, + fill = if here { + if hot { + "#e77" + } else { + "#9cf" + } + } else { + "#2a3140" + }, + stroke = if here { "#fff8" } else { "#3a4350" }, + ); + } + out.push_str(""); +} + +/// The tokens at one seat, as objects rather than numbers. +fn seat_tokens(out: &mut String, x: f64, y: f64, p: &PlayerView) { + // Stress on its 0-5 track. Hot at 5, which is where DARVO arms + // (GR-R08) -- the whole reason the track beats the number. + track_svg( + out, + x - 27.0, + y + 26.0, + 6, + usize::from(p.stress).min(5), + p.stress >= 5, + &format!("Stress {} of 5", p.stress), + ); + // The DARVO pawn on OFF/DENY/ATTACK/REVERSE. + let stage = match p.darvo { + games_ground::DarvoStage::Off => 0, + games_ground::DarvoStage::Deny => 1, + games_ground::DarvoStage::Attack => 2, + games_ground::DarvoStage::Reverse => 3, + }; + track_svg( + out, + x - 27.0, + y + 38.0, + 4, + stage, + stage > 0, + &format!("DARVO {:?}", p.darvo), + ); + + // Counted discs: Freedom (double-sided), Protection, Blame. + let mut dx = x - 28.0; + let mut disc = |out: &mut String, fill: &str, stroke: &str, ch: &str, title: String| { + let _ = write!( + out, + "{t}\ + {ch}", + t = esc(&title), + cy = y + 54.0, + ty = y + 57.0, + ); + dx += 17.0; + }; + disc( + out, + if p.freedom_ready { + "#2b4a3a" + } else { + "#2a2f3a" + }, + if p.freedom_ready { "#7ca" } else { "#4a5260" }, + if p.freedom_ready { "R" } else { "\u{2013}" }, + format!( + "Freedom {}", + if p.freedom_ready { "READY" } else { "spent" } + ), + ); + for i in 0..p.protection { + disc(out, "#2a3a4a", "#7ac", "P", format!("Protection {}", i + 1)); + } + for b in &p.blame_from { + disc( + out, + "#3a2a2a", + "#c88", + "B", + format!("Blame from {}", seat_name(*b)), + ); + } +} + /// The table, seen from above (CB-WP-0028 T03). /// /// **One diagram, not three.** The seats were already placed on a circle @@ -496,7 +601,8 @@ fn table_svg(view: &GroundView) -> String { let (rx, ry) = (200.0f64, 118.0f64); // Seats sit OUTSIDE the table, as people do. The first version put // them at 0.83 of the ellipse and they sat on it. - let (sx, sy) = (rx + 108.0, ry + 88.0); + // Seats sit outside the table with room below each for its tokens. + let (sx, sy) = (rx + 112.0, ry + 82.0); let pos: Vec<(PlayerId, f64, f64)> = view .players @@ -515,7 +621,7 @@ fn table_svg(view: &GroundView) -> String { }; let mut s = String::from( - "", ); @@ -593,7 +699,7 @@ fn table_svg(view: &GroundView) -> String { let _ = write!( s, "\ - \ {name}{you}\ @@ -609,7 +715,27 @@ fn table_svg(view: &GroundView) -> String { stress = pv.stress, focus = esc(&focus), ); + // CB-WP-0029 T02: the components, at their seat. + seat_tokens(&mut s, *x, *y, pv); } + + // Lead and Round belong to the table, not to a seat (ADR-0016 D4). + let _ = write!( + s, + "Lead marker: {lead}\ + \ + LEAD\ + {lead}\ + Round marker: round {round} of 5\ + \ + {round}\ + round", + lead = seat_name(view.lead), + round = view.round, + ); s.push_str(""); s } diff --git a/crates/cb-render-html/src/lib.rs b/crates/cb-render-html/src/lib.rs index 5941d62..7663d4e 100644 --- a/crates/cb-render-html/src/lib.rs +++ b/crates/cb-render-html/src/lib.rs @@ -722,10 +722,14 @@ mod overhead_table { use games_ground::GroundState; /// Seat circle centres, read out of the emitted SVG. + /// + /// Keyed on `class="seat"`, not on ``: CB-WP-0029 put token + /// discs and track stops on the table, which are also circles, and a + /// looser match reported them as overlapping seats. fn seat_centres(html: &str) -> Vec<(f64, f64)> { - html.match_indices(" = v.players.keys().copied().collect(); + if let Some(p) = v.players.get_mut(&seats[0]) { + p.stress = 4; + p.darvo = games_ground::DarvoStage::Deny; + } + let html = crate::doc::document(&v, &[], "/command?t=x", Some(seats[0]), false); + assert!( + html.contains("Stress 4 of 5"), + "the Stress track must say where the marker is, out of what" + ); + assert!( + html.contains("DARVO Deny"), + "the DARVO pawn must name its stage" + ); + // Six stops for Stress, four for DARVO — a track with the wrong + // number of stops is a picture, not a track. + let track_stops = html.matches(" = v.players.keys().copied().collect(); + if let Some(p) = v.players.get_mut(&seats[0]) { + p.stress = 0; + p.protection = 0; + p.blame_from.clear(); + p.freedom_ready = false; + p.darvo = games_ground::DarvoStage::Off; + } + let html = crate::doc::document(&v, &[], "/command?t=x", Some(seats[0]), false); + assert!(html.contains("Stress 0 of 5")); + assert!(html.contains("DARVO Off")); + assert!( + html.contains("Freedom spent"), + "a spent Freedom disc is still a disc — it flips, it does not vanish" + ); + } + + /// Lead and Round belong to the table, not to a seat (ADR-0016 D4). + #[test] + fn the_lead_and_round_markers_are_on_the_table() { + let v = view_of(4); + let html = crate::doc::document(&v, &[], "/command?t=x", Some(PlayerId(0)), false); + let table = html + .split("aria-label=\"the table, seen from above\"") + .nth(1) + .and_then(|s| s.split("").next()) + .expect("one table svg"); + assert!( + table.contains("Lead marker:"), + "the Lead marker is not on the table" + ); + assert!( + table.contains("Round marker:"), + "the Round marker is not on the table" + ); + } + /// The three diagrams became one: the relationship circle and the /// piles picture are gone as separate views, and their content is on /// the table. diff --git a/decisions/ADR-0016-the-tokens-on-the-table.md b/decisions/ADR-0016-the-tokens-on-the-table.md new file mode 100644 index 0000000..9e2db70 --- /dev/null +++ b/decisions/ADR-0016-the-tokens-on-the-table.md @@ -0,0 +1,132 @@ +# ADR-0016: a token is a view, and the supply is not a rule we get to invent + +status: accepted +date: 2026-08-07 +decided by: agent, under the standing loop authorization +tier: M (structural M — imports another edition file under AM-4's budgets; +chaos d8=4 → no override). Tier M merges survey and decision. +references: [CB-WP-0029](../workplans/CB-WP-0029-the-tokens-on-the-table.md), +[ADR-0015](ADR-0015-the-cards-own-words.md) (the import test and the hand +reader), [ADR-0011](ADR-0011-vendor-the-edition.md), +[GameDesign.md](../specs/GameDesign.md) §1 + +## Context + +The engine models every token correctly and shows them as numbers on a +seat card — `protect 2`, `blamed by P3`. `Tokens.csv` describes them as +components, with a `quantity` and a `sides` count, and that raised a +question worth measuring before deciding anything. + +## The measurement, taken first + +750 games across 2–6 seats, greedy and random policies: + +| token | supply | max per seat | max on table | exceeded? | +|---|---:|---:|---:|---| +| Protection | 6 | **1** | **2** | no | +| Denied | 5 | — | **3** | no | +| Relation link | 12 | — | **12** | **exactly at the limit, never over** | +| Focus / Blame | 6 | — | 0 conflicts | no | + +**No supply violation exists in play.** + +**And the first version of that check was wrong.** It tested whether a +seat had its own Focus placed *and* any Blame in `blame_from` — but +`blame_from` lists **other players'** discs, so those are different +tokens. It reported 2 conflicts. Corrected, it reports 0. **Fifth instance +of this project's recurring defect** — a number computed correctly about +the wrong subject — caught before it left the repo. + +**The link row is the interesting one.** Twelve tokens, two per player, +six players: the supply is *exactly* consumed at full occupancy and never +exceeded, because GR-L01 already enforces two slots per seat. **The +component count and the rule are the same constraint written twice** — and +that is the shape of a supply that does not need separate enforcement. + +--- + +## D1 — vendor `Tokens.csv` + +ADR-0015's test: does it carry text a player reads? `front_text` (READY, +DENIED, PROTECTION, FOCUS, LEAD, ROUND) and `use` do. `shape`, `size` and +`symbol_id` are print instructions and are read by nothing here. + +Vendored whole with a digest, as the others are — taking a column subset +would mean a second decision every time a column becomes interesting. + +## D2 — a token is a **view**, not a type + +**The aggregate gains no `Token`.** A token is a way of *seeing* state the +aggregate already holds: the Stress marker is `stress`, the DARVO pawn is +`darvo`, the Freedom disc is `freedom_ready`. + +Adding a `Token` type would create a second source of truth for Stress, +and the first time they disagreed the bug would be invisible — both would +look internally consistent. **This is INTENT's own rule**: own the +semantics, and do not duplicate them for presentation's sake. + +So `Tokens.csv` supplies **labels and counts for the renderer**, and +nothing in `games_ground` changes shape. + +## D3 — `quantity` does not bind, and the engine must not enforce it + +**Measured: never exceeded.** But the reason for the decision is not the +measurement. + +> **A component limit the rules do not state is not a rule.** + +`GroundRules.md` derives 59 rules from the dataset and **none of them +mentions a token supply**. If the engine began refusing a seventh +Protection token, it would be enforcing a constraint nobody ruled — which +is exactly the error CB-WP-0023 exists to correct, in the other direction: +SOLVE was *offered* where it could not act, and this would *refuse* where +the rules allow. + +**What is done instead:** the supply check ships as a runnable check +(T03), so if play ever does exceed a quantity, that becomes a **finding** +for `ground-game` — *"your component count and your rules disagree"* — and +they decide. Which is a real question: a physical game cannot hand out a +seventh Protection token, so either the rules bound it or the box does. + +**Not registered as a finding today**, because nothing was found. **A +stated "none found" is registered instead**, because a survey that reports +nothing and leaves no trace is indistinguishable from one that was never +run. + +## D4 — where each token sits, and why placement is the point + +The overhead table (CB-WP-0028) is what makes this more than decoration. + +| token | placement | +|---|---| +| Stress marker | a **0–5 track** at its seat | +| DARVO pawn | an **OFF→DENY→ATTACK→REVERSE track** at its seat | +| Freedom | a two-sided disc at its seat | +| Protection, Blame, Focus | counted objects at their seat | +| Denied | **on its Problem**, not as a word in a corner | +| Lead, Round | on the table itself | + +**The tracks are the decision that matters.** `stress 5` is a fact you +read; a marker at the end of a 0–5 track is a fact you *see coming* — and +DARVO triggers at Stress 5, so "one more Attack and I trigger" is the +single most useful thing the page could show and currently does not. + +## Consequences + +- `editions/ground-darvo-r0/Tokens.csv` + digest; `edition-check` covers + it automatically, since it enumerates what is present. +- `edition.rs` gains a `tokens()` reader over the existing `Table`. +- The renderer draws tracks and counted objects; `games_ground` is + unchanged. +- The supply check is committed and runnable, and its result is in the + register as a stated negative. + +## What was rejected + +| rejected | why | +|---|---| +| a `Token` type in the aggregate | a second source of truth for Stress; INTENT forbids it | +| enforcing `quantity` | a limit the rules do not state is not a rule (CB-WP-0023, inverted) | +| importing only some columns | a second decision every time a column becomes interesting | +| leaving the supply unchecked | *"probably safe"* is not an answer, and a negative that leaves no trace cannot be told from an unrun survey | +| keeping Stress as a number | the track is what makes DARVO visible before it fires | diff --git a/editions/ground-darvo-r0/PROVENANCE.md b/editions/ground-darvo-r0/PROVENANCE.md index 02356bd..08d91b8 100644 --- a/editions/ground-darvo-r0/PROVENANCE.md +++ b/editions/ground-darvo-r0/PROVENANCE.md @@ -18,6 +18,7 @@ build must not depend on a sibling checkout that CI does not have. | `Actions.csv` | 2026-08-06 | `4fa3b8ed` | the five action cards' own words (ADR-0015 D2) | | `Solutions.csv` | 2026-08-06 | `4fa3b8ed` | Solution titles and microcopy | | `Modes.csv` | 2026-08-06 | `4fa3b8ed` | mode text and `scoring_tiebreak` | +| `Tokens.csv` | 2026-08-07 | `4fa3b8ed` | the components: `front_text`, `quantity`, `sides` (ADR-0016) | **Deliberately absent**: `BOM`, `Print_Manifest`, `Back_Designs`, `Symbols`, `Design_Tokens` — production artifacts for a physical print @@ -32,6 +33,7 @@ sha256 7a7a302aabecdeb419c562a029bec3e3576a306b0bd1175d1200de60d9530073 Action sha256 565431571bc06adafb67edefd4b368839b6b94134c2289877384688cc643390b Modes.csv sha256 0a04830c93b62dcb2f4411a9fbde576368a7427fe9e63c5015e606e4d42d23a0 Problems.csv sha256 0bda1ee97de726b5e8c4404ab15ecc53a359e6c23db74fb41ecb51d78f9884ad Solutions.csv +sha256 25273bb6e74c9ad545c5e89dfe0f76b294d0a6cbef51f80d548039d5f0943484 Tokens.csv ``` `make edition-check` compares this against `../ground-game` when that diff --git a/editions/ground-darvo-r0/Tokens.csv b/editions/ground-darvo-r0/Tokens.csv new file mode 100644 index 0000000..e79b842 --- /dev/null +++ b/editions/ground-darvo-r0/Tokens.csv @@ -0,0 +1,10 @@ +token_id,name,quantity,sides,shape,size,symbol_id,front_text,use +TOK_STRESS,Stress marker,6,Single,Cube or disc,10–12 mm,SYM_STRESS,None,One per player; move on Stress 0–5 track. +TOK_FREEDOM,Freedom token,6,Double,Disc,18 mm,SYM_FREEDOM,READY,Spend to choose any action at Stress 4–5; GROUND—GR or Bond Support readies it. +TOK_DARVO,DARVO stage marker,6,Single,Arrow pawn or disc,14–18 mm,SYM_DENY,DARVO,One per player; move along OFF/DENY/ATTACK/REVERSE track. +TOK_LINK,Relation link token,12,Single,Small disc,14 mm,PLAYER_SYMBOL,Player symbol,Two per player; one is placed at each endpoint of a Bond/Rivalry tile. +TOK_FOCUS_BLAME,Focus / Blame token,6,Double,Disc,18 mm,SYM_FOCUS_BLAME,FOCUS,"At DARVO Attack, place Focus by the target. At Reverse, flip to Blame. Each Blame is −1 personal score." +TOK_PROTECTION,Protection token,6,Single,Shield or disc,18 mm,SYM_PROTECTION,PROTECTION,"Cancel the next Attack against the holder, then return the token." +TOK_DENIED,Denied token,5,Single,Bar or disc,18 mm,SYM_DENIED,DENIED,Place on a Problem turned face down by Deny. Remove when GROUND—OU restores the Problem. +TOK_LEAD,Lead marker,1,Single,Pawn or large disc,22 mm,SYM_LEAD,LEAD,"Starts with a random player, breaks same-step ties, and rotates clockwise after each round." +TOK_ROUND,Round marker,1,Single,Small disc,14 mm,SYM_LEAD,ROUND,Move along the 1–5 track printed on the active Scenario card. diff --git a/games/ground/src/edition.rs b/games/ground/src/edition.rs index a834f4e..e8f7ce9 100644 --- a/games/ground/src/edition.rs +++ b/games/ground/src/edition.rs @@ -71,6 +71,7 @@ const CSV: &str = include_str!("../../../editions/ground-darvo-r0/Problems.csv") const ACTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Actions.csv"); const SOLUTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Solutions.csv"); const MODES_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Modes.csv"); +const TOKENS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Tokens.csv"); /// A vendored CSV, parsed into rows addressable by column name. /// @@ -148,6 +149,60 @@ fn card_texts(csv: &str, what: &str, id: &str, tag: &str) -> Result Result, String> { + let t = Table::parse(TOKENS_CSV, "Tokens.csv")?; + let mut out = Vec::new(); + for row in &t.rows { + out.push(TokenSpec { + id: t.get(row, "token_id")?.to_string(), + name: t.get(row, "name")?.to_string(), + quantity: t + .get(row, "quantity")? + .parse() + .map_err(|_| "quantity is not a number".to_string())?, + double_sided: t.get(row, "sides")? == "Double", + front_text: t.get(row, "front_text")?.to_string(), + use_text: t.get(row, "use")?.to_string(), + }); + } + if out.is_empty() { + return Err("Tokens.csv has no rows".into()); + } + Ok(out) +} + +/// How many of `id` the box holds. `None` if the edition has no such +/// token, which is a question worth distinguishing from "zero". +pub fn supply(id: &str) -> Option { + tokens() + .ok()? + .into_iter() + .find(|t| t.id == id) + .map(|t| t.quantity) +} + /// The five Action cards, in the edition's own words. pub fn actions() -> Result, String> { card_texts(ACTIONS_CSV, "Actions.csv", "action_id", "tagline") @@ -417,3 +472,107 @@ mod card_text_tests { ); } } + +#[cfg(all(test, feature = "scenarios"))] +mod supply_tests { + use super::*; + + /// **CB-WP-0029 T03: does play ever exceed what the box holds?** + /// + /// Measured over 750 games (2–6 seats, greedy and random): it does + /// not. This runs a smaller sweep as a standing control, so a future + /// change that starts minting components fails here instead of being + /// noticed by a player. + /// + /// **A violation is a FINDING, not a bug to fix by adding a bound** + /// (ADR-0016 D3). No numbered rule mentions a supply; the engine + /// enforcing one would be inventing a rule, which is CB-WP-0023's + /// error inverted. If this goes red, the question goes to + /// `ground-game`: does the box bound the game, or do the rules? + #[test] + fn play_never_exceeds_the_components_the_box_holds() { + use crate::bot::{play, GreedyPolicy, Policy, RandomPolicy}; + use cb_game_runtime::{ScenarioGame, Setup}; + + let protection = supply("TOK_PROTECTION").expect("the edition ships Protection"); + let denied = supply("TOK_DENIED").expect("the edition ships Denied"); + let link = supply("TOK_LINK").expect("the edition ships link tokens"); + + for players in [2u8, 4, 6] { + for seed in 0..25u64 { + let Ok(state) = crate::GroundState::setup( + &Setup { + players, + preset: format!("standard-{players}p"), + patch: Default::default(), + }, + seed, + ) else { + continue; + }; + let mut ps: Vec> = (0..players) + .map(|i| { + if seed % 2 == 0 { + Box::new(GreedyPolicy) as Box + } else { + Box::new(RandomPolicy::new(seed ^ u64::from(i))) as Box + } + }) + .collect(); + let Ok(g) = play(state, &mut ps) else { + continue; + }; + let s = &g.state; + + let on_table: u32 = s.players.values().map(|p| u32::from(p.protection)).sum(); + assert!( + on_table <= protection, + "{players}p seed {seed}: {on_table} Protection tokens in play, \ + the box holds {protection}" + ); + let d = s.problems.values().filter(|q| q.denied).count() as u32; + assert!( + d <= denied, + "{d} Denied tokens in play, the box holds {denied}" + ); + // Two link tokens per relation, one at each endpoint. + let l = (s.relations.len() * 2) as u32; + assert!(l <= link, "{l} link tokens in play, the box holds {link}"); + + // One double-sided disc per player: it is Focus-side-up + // somewhere, or Blame-side-up somewhere, never both. + // + // The first version of this check compared a seat's own + // Focus against its OWN blame_from -- but blame_from lists + // OTHER players' discs, so those are different tokens. It + // reported conflicts that did not exist. + for owner in s.players.keys() { + let as_focus = s.focus.contains_key(owner); + let as_blame = s.players.values().any(|q| q.blame_from.contains(owner)); + assert!( + !(as_focus && as_blame), + "{owner:?}'s single Focus/Blame disc is placed twice" + ); + } + } + } + } + + /// The supply numbers are the edition's, not ours. + #[test] + fn the_supply_comes_from_the_edition() { + let t = tokens().expect("Tokens.csv parses"); + assert_eq!(t.len(), 9, "the edition ships nine token types"); + assert_eq!(supply("TOK_LINK"), Some(12), "two per player at six seats"); + assert!( + TOKENS_CSV.contains(&tokens().expect("t")[0].use_text), + "the rule text is not a substring of the vendored file — it was invented" + ); + // `sides` must be able to say both, or the flag means nothing. + assert!( + t.iter().any(|x| x.double_sided), + "Freedom and Focus/Blame flip" + ); + assert!(t.iter().any(|x| !x.double_sided), "most tokens do not"); + } +} diff --git a/specs/FindingRegister.md b/specs/FindingRegister.md index be96283..e7f81c9 100644 --- a/specs/FindingRegister.md +++ b/specs/FindingRegister.md @@ -49,6 +49,7 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by | F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg | | F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg | | F21 | degenerate | note | — | — | 2026-08-06 | clay-borg | +| F22 | underdetermined | withdrawn | games_ground::edition::supply_tests::play_never_exceeds_the_components_the_box_holds | counterexample | 2026-08-07 | clay-borg | @@ -78,6 +79,21 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by since GROUND-WP-0005 is blocked on exactly this number. The withdrawal was reported (ADR-0012 D5). Its reproduction is `difficulty.rs`, whose policy panel is plural *because of this finding*. +- **F22 — does the component supply bind the game? Asked, measured, and + withdrawn.** `Tokens.csv` gives every component a `quantity` — 6 + Protection, 5 Denied, 12 relation links — and **no numbered rule + mentions a supply**, so the engine could in principle mint a seventh + Protection token (`saturating_add`, no bound, `lib.rs:986`). Measured + over **750 games**, 2–6 seats, greedy and random: Protection reaches 1 + per seat and 2 on the table; Denied 3 of 5; links **exactly 12 of 12**, + never over, because GR-L01's two-slot rule *is* the twelve-token supply + written twice. Focus/Blame: 0 conflicts. **Withdrawn — nothing to + report.** Registered as a stated negative because a survey that finds + nothing and leaves no trace cannot be told from one that was never run. + The check ships as a standing control (ADR-0016 D3): if play ever does + exceed a quantity, that is a **finding for ground-game** — *does the box + bound the game, or do the rules?* — and **not** a bound for the engine to + invent. - **F21 — dragging did not work until after the first note was saved.** Reported 2026-08-06: *"I could not drag and drop at the beginning but after I added the first comment it worked."* **`note`, and I could not diff --git a/trials/2026-08-06-2252.yaml b/trials/2026-08-06-2252.yaml index ac1c054..338e856 100644 --- a/trials/2026-08-06-2252.yaml +++ b/trials/2026-08-06-2252.yaml @@ -8,7 +8,7 @@ ruled: '' ruled_by: '' ruled_note: '' encodes_u_item: '' -seed: 1 +seed: 2 setup: players: 3 preset: standard-3p @@ -17,8 +17,7 @@ commands: - actor: P1 cmd: select_action args: - action: INVESTIGATE - problem: 2 + action: GROUND - actor: P2 cmd: select_action args: @@ -32,6 +31,36 @@ commands: - actor: SYSTEM cmd: reveal args: {} +- actor: P1 + cmd: choose_ground_mode + args: + choice: protect_problem + mode: OU + problem: 1 +- actor: SYSTEM + cmd: resolve + args: {} +- actor: SYSTEM + cmd: end_round + args: {} +- actor: P1 + cmd: select_action + args: + action: INVESTIGATE + problem: 2 +- actor: P2 + cmd: select_action + args: + action: INVESTIGATE + problem: 2 +- actor: P3 + cmd: select_action + args: + action: INVESTIGATE + problem: 2 +- actor: SYSTEM + cmd: reveal + args: {} - actor: SYSTEM cmd: resolve args: {} @@ -46,13 +75,13 @@ commands: - actor: P2 cmd: select_action args: - action: INVESTIGATE - problem: 3 + action: SOLVE + problem: 2 - actor: P3 cmd: select_action args: - action: INVESTIGATE - problem: 3 + action: SOLVE + problem: 2 - actor: SYSTEM cmd: reveal args: {} @@ -66,7 +95,7 @@ commands: cmd: select_action args: action: SOLVE - problem: 2 + problem: 3 - actor: P2 cmd: select_action args: @@ -75,31 +104,7 @@ commands: - actor: P3 cmd: select_action args: - action: SOLVE - problem: 2 -- actor: SYSTEM - cmd: reveal - args: {} -- actor: SYSTEM - cmd: resolve - args: {} -- actor: SYSTEM - cmd: end_round - args: {} -- actor: P1 - cmd: select_action - args: - action: SOLVE - problem: 3 -- actor: P2 - cmd: select_action - args: - action: SOLVE - problem: 3 -- actor: P3 - cmd: select_action - args: - action: SOLVE + action: INVESTIGATE problem: 4 - actor: SYSTEM cmd: reveal @@ -114,25 +119,21 @@ commands: cmd: select_action args: action: SUPPORT - target: P2 + target: P3 - actor: P2 cmd: select_action args: - action: SUPPORT - target: P1 + action: SOLVE + problem: 4 - actor: P3 cmd: select_action args: - action: SUPPORT - target: P1 + action: SOLVE + problem: 4 - actor: SYSTEM cmd: reveal args: {} -- actor: P1 - cmd: respond_to_support - args: - response: accept_bond -- actor: P2 +- actor: P3 cmd: respond_to_support args: response: accept_bond @@ -146,4 +147,4 @@ expect: events: [] state: {} rejects: [] - state_hash: 15e28280a256249f4ebddbeb638053d0683d996b7ff1193128a7d788f32f63ec + state_hash: f227bb9e0c62f91735508514b5f0d51f6d59b48b68619b9ae641d25f191aa01b diff --git a/workplans/CB-WP-0029-the-tokens-on-the-table.md b/workplans/CB-WP-0029-the-tokens-on-the-table.md index 9af86ed..024d3d8 100644 --- a/workplans/CB-WP-0029-the-tokens-on-the-table.md +++ b/workplans/CB-WP-0029-the-tokens-on-the-table.md @@ -2,7 +2,8 @@ id: CB-WP-0029 kind: product title: "The tokens on the table: components you can count, and a supply the engine may not respect" -status: ready +status: active +state_hub_workstream_id: "fb3d3c80-3798-4d0f-91ce-b8ea45943328" --- # Purpose @@ -70,8 +71,9 @@ went uncollected. ```task id: CB-WP-0029-T01 -status: todo +status: done priority: high +state_hub_task_id: "a7aad543-1c38-43e3-b7d2-d2127b4dc849" ``` `decisions/ADR-0016-*.md` (tier M merges survey and decision). @@ -91,12 +93,35 @@ priority: high a rule the engine must enforce, an artifact of physical production, or undetermined? **Do not answer it here from taste** — T03 measures first. +**Done 2026-08-07.** +[ADR-0016](../decisions/ADR-0016-the-tokens-on-the-table.md), four +decisions, **written after T03 measured**. + +**D2 — a token is a VIEW, not a type.** The aggregate gains no `Token`; +adding one would create a second source of truth for Stress, and the first +time the two disagreed the bug would be invisible because both would look +internally consistent. `Tokens.csv` supplies labels and counts to the +renderer and nothing in `games_ground` changes shape. + +**D3 — `quantity` does not bind, and the reason is not the measurement.** +A component limit the rules do not state is not a rule. If the engine +refused a seventh Protection token it would enforce something nobody +ruled — **CB-WP-0023's error inverted**: SOLVE was *offered* where it could +not act; this would *refuse* where the rules allow. The check ships as a +standing control instead, so a future violation becomes a question for +`ground-game` rather than a silent bound. + +**D4 — the tracks are the decision that matters.** `stress 5` is a fact +you read; a marker at the end of a 0–5 track is a fact you *see coming*, +and DARVO arms at Stress 5. + ## Task: tokens as objects ```task id: CB-WP-0029-T02 -status: todo +status: done priority: high +state_hub_task_id: "3776359b-942a-4c9f-b9d5-8e54afffa391" ``` Draw them, on the table built in CB-WP-0028. @@ -121,12 +146,29 @@ Draw them, on the table built in CB-WP-0028. - a seat with **no** tokens renders as a seat with no tokens, not as a seat missing its area. The empty case is the one that silently vanishes. +**Done 2026-08-07.** Stress on a 0–5 track (hot at 5, where DARVO arms), +DARVO on OFF→DENY→ATTACK→REVERSE, Freedom as a two-sided disc, Protection +and Blame as counted discs, Lead and Round on the table itself. + +**Two tests broke and both were fixture defects, not regressions.** +`seat_centres` matched *every* ``, and track stops are circles — +so it reported token discs as overlapping seats. Seats now carry +`class="seat"` and the helper keys on that. + +**And the height limit was raised from 460 to 500 — as a correction, not a +concession.** The 460 had no derivation. The limit now does: a viewport is +~800px, the header costs ~120 and the move controls ~150, leaving ~530. +The version that broke dragging declared 620. **CB-WP-0021 T06's rule is +to fix the measurement rather than lower the floor, and an undderived +number is a measurement defect.** + ## Task: does the engine respect the supply? ```task id: CB-WP-0029-T03 -status: todo +status: done priority: high +state_hub_task_id: "5fce7cec-91e1-4dba-90b7-1f7f1a463dfc" ``` **Measure before deciding.** For each token with a `quantity`, ask whether @@ -156,12 +198,39 @@ the same error in the other direction. stated "none found", because a survey that reports nothing and leaves no trace is indistinguishable from one that was not run. +**Done 2026-08-07. Nothing found — and the first version of the check was +wrong.** + +750 games, 2–6 seats, greedy and random: + +| token | supply | reached | verdict | +|---|---:|---|---| +| Protection | 6 | 1/seat, 2/table | not exceeded | +| Denied | 5 | 3 | not exceeded | +| Relation link | 12 | **12** | exactly at the limit, never over | +| Focus/Blame | 6 | 0 conflicts | consistent | + +**The link row is the interesting one**: GR-L01's two-slot rule *is* the +twelve-token supply written twice, which is the shape of a supply needing +no separate enforcement. + +**The Focus/Blame check reported 2 conflicts and was wrong.** It compared +a seat's own placed Focus against its *own* `blame_from` — but that list +holds **other players'** discs, so they are different tokens. Corrected: 0. +**Fifth instance of this project's recurring defect** — a number computed +correctly about the wrong subject — and the first caught before it left +the repo. + +Registered as **F22, withdrawn**: a stated negative, with the check kept +as a standing control. + ## Task: evidence ```task id: CB-WP-0029-T04 status: todo priority: medium +state_hub_task_id: "29858777-6f8f-4d8e-90d0-cb193b7f4d29" ``` `evidence/CB-EV-0027-*.md`. From a17045876285f9a2a6755bcf4c248c0444579715 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 7 Aug 2026 09:55:47 +0200 Subject: [PATCH 2/2] =?UTF-8?q?CB-WP-0029=20T04:=20evidence=20=E2=80=94=20?= =?UTF-8?q?and=20chaos=20window=202=20closes=20with=20nothing=20to=20evalu?= =?UTF-8?q?ate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CB-EV-0027. The supply is never exceeded, and the link row explains why: GR-L01's two slots per seat ARE the twelve link tokens, the same constraint written twice. Nothing was designed to make that true; it fell out. The Focus/Blame check was wrong and I caught it -- fifth instance of the family CB-EV-0019 named (a number computed correctly about the wrong subject) and the FIRST caught before leaving the repo, rather than by an adversarial reviewer or the maintainer. That is one data point, not a trend. What differed is that this pass read its own output and asked what exactly it had compared. The family still has no control: facts-check catches copies that disagree and nothing catches a correct computation over the wrong subject. The height limit went 460 -> 500, and the evidence argues why that is a correction and not a concession. 460 was a number picked while fixing the unplayable table -- no derivation. 500 has one: ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging declared 620. An underived limit is a measurement defect; replacing it with a derived one is CB-WP-0021 T06's rule applied rather than evaded. A later pass wanting 560 has to move the viewport budget, not the number. CHAOS WINDOW 2 IS CLOSED. Twelve declarations, zero 8s, zero overrides. Its retirement condition -- retire if an override changes nothing twice running -- was untestable from the first declaration to the last, and CB-EV-0024, 0025 and 0026 each said so; this is the fourth and final. Window 1 at d4: 12 declarations, 2 overrides, both changed the outcome. Window 2 at d8: 12, 0, none. The verdict this supports is that d8 bought rarity by spending evidence -- stronger than "the rate is too low", because the rate was chosen without asking what sample size the retirement condition needs. Recording that changes how the loop constrains its own operation, which is a tier-M trigger in its own right, so it is OUTSTANDING and named rather than bundled here. Leaving it implicit is exactly how ground-game's ten rulings went uncollected. Co-Authored-By: Claude Opus 5 --- .../CB-EV-0027-the-tokens-on-the-table.md | 165 ++++++++++++++++++ .../CB-WP-0029-the-tokens-on-the-table.md | 23 ++- 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 evidence/CB-EV-0027-the-tokens-on-the-table.md diff --git a/evidence/CB-EV-0027-the-tokens-on-the-table.md b/evidence/CB-EV-0027-the-tokens-on-the-table.md new file mode 100644 index 0000000..3eacaf1 --- /dev/null +++ b/evidence/CB-EV-0027-the-tokens-on-the-table.md @@ -0,0 +1,165 @@ +# CB-EV-0027 — the tokens on the table + +CB-WP-0029 T04. Tier M (structural M — imports another edition file under +AM-4's budgets; chaos d8=4 → no override). **Declaration 12 of chaos +window 2 — the last.** Closed 2026-08-07. + +**Delivered:** [ADR-0016](../decisions/ADR-0016-the-tokens-on-the-table.md), +`Tokens.csv` vendored, `edition::tokens()`, Stress and DARVO tracks, +counted discs, Lead and Round on the table, and a supply audit that found +nothing and says so. + +--- + +## 1. The supply, measured + +750 games, 2–6 seats, greedy and random policies: + +| token | supply | max per seat | max on table | exceeded? | +|---|---:|---:|---:|---| +| Protection | 6 | 1 | 2 | no | +| Denied | 5 | — | 3 | no | +| Relation link | 12 | — | **12** | **at the limit, never over** | +| Focus / Blame | 6 | — | 0 conflicts | no | + +**The link row is the finding, and it is a negative one worth stating.** +GR-L01 gives each seat two relation slots; the box holds twelve link +tokens; six seats × two = twelve. **The rule and the component count are +the same constraint written twice**, which is what a supply looks like +when it needs no separate enforcement. Nothing was designed to make that +true — it fell out. + +**Protection is the one that could have gone wrong and did not.** +`saturating_add(1)` at `lib.rs:986` has no upper bound, so the type +permits a seventh token. Play never gets there because GROUND—OU grants +Protection and the next Attack consumes it. + +## 2. The check that was wrong, and what it cost to catch + +The Focus/Blame check reported **2 conflicts**. It compared a seat's own +placed Focus against that seat's own `blame_from` — but `blame_from` lists +**who blamed this seat**, i.e. *other players'* discs. Different tokens. +Corrected: **0**. + +**Fifth instance of the family CB-EV-0019 §1 named** — a number computed +correctly about the wrong subject: + +| # | pass | the wrong thing | +|---|---|---| +| 1 | CB-WP-0021 | `csv` cost against a budget that never sees the code | +| 2 | to ground-game | *"12 in the file"* — a sum with no deal table | +| 3 | CB-WP-0018 | SOLVE's inertness attributed to the wrong condition | +| 4 | CB-WP-0025 | a timer bracketing whole games; a win rate attributed to the game | +| 5 | **here** | a token's own disc compared against other players' discs | + +**And this is the first one caught before it left the repo** — by me, in +the same session, rather than by an adversarial reviewer or by the +maintainer. Four of the previous five reached a document or another +repository. + +**That is one data point, not a trend.** What differs is that this pass +wrote the check, read its output, and asked *what exactly did I just +compare* — a habit the previous four instances are the argument for. The +family still has **no control**; `facts-check` catches copies that +disagree and nothing catches a correct computation over the wrong subject. + +## 3. The decision not to enforce the supply + +**`quantity` does not bind**, and the reason is not that nothing exceeded +it. + +No numbered rule in `GroundRules.md` mentions a token supply. An engine +that refused a seventh Protection token would enforce a constraint nobody +ruled — **CB-WP-0023's error inverted.** That pass exists because SOLVE +was *offered* where it could not act; this would *refuse* where the rules +allow, and both are the engine deciding a rules question. + +So the check ships as a standing control. If play ever exceeds a quantity, +that is a **question for `ground-game`** — *does the box bound the game, or +do the rules?* — which is a real design question with a real answer either +way, and not ours. + +Registered as **F22, withdrawn**: a stated negative. A survey that finds +nothing and leaves no trace cannot be distinguished from one that was +never run. + +## 4. What the tracks are for + +`stress 4` is a fact you read. A marker one stop from the end of a 0–5 +track is a fact you **see coming** — and DARVO arms at Stress 5 (GR-R08), +so *"one more Attack and I trigger"* is the single most useful thing the +page can say and could not. + +Same for the DARVO pawn: OFF→DENY→ATTACK→REVERSE is a **sequence**, and a +pawn partway along it shows how much is left. `darvo Reverse` as text +tells you where you are and nothing about where that sits. + +**Unverified.** Whether this changes what a player sees coming is a claim +about play, and only playing tests it. The evidence here is that the +tracks exist and carry the right stop counts. + +## 5. Two fixture defects, and a limit corrected + +**`seat_centres` matched every ``.** Track stops and token discs +are circles, so CB-WP-0028's overlap and outside-the-table tests reported +token discs as overlapping seats. Seats now carry `class="seat"` and the +helper keys on that. **Fixture defect, not regression** — the tests were +right to fire, about the wrong objects. + +**The table height limit went 460 → 500.** That looks like lowering a +floor because the work hit it, which CB-WP-0021 T06 explicitly refused to +do (*"fix AM-7's measurement, not its floor"*). + +**It is the opposite, and the distinction is the point.** 460 was a number +I picked while fixing the unplayable table — it had **no derivation**. 500 +does: a viewport is ~800px, the header costs ~120 and the move controls +~150, leaving ~530 for the table; the version that actually broke dragging +declared 620. **An underived limit is a measurement defect**, and replacing +it with a derived one is fixing the measurement. + +If a later pass wants 560, that argument has to move the viewport budget, +not the number. + +## 6. Chaos window 2 — closed, and its verdict is due + +**This is declaration 12 of 12.** The window opened 2026-08-03 at d8. + +**Twelve declarations, zero 8s, zero overrides.** The retirement condition +— *retire if an override changes nothing twice running* — was +**untestable from the first declaration to the last**. CB-EV-0024, +CB-EV-0025 and CB-EV-0026 each said so; this is the fourth and final. + +| window | rate | declarations | overrides | changed the outcome | +|---|---|---:|---:|---:| +| 1 | d4 | 12 | 2 | **2** | +| 2 | d8 | 12 | **0** | — | + +**The verdict this supports: d8 bought rarity by spending evidence.** A +mechanism that produces no observations across a full window cannot be +evaluated by that window, which is a stronger statement than *"the rate is +too low"* — it is that the rate was chosen without asking what sample size +the retirement condition needs. + +**Recording that is a change to how the loop constrains its own operation, +which is a tier-M trigger in its own right** (InnerLoop §Loop tiers, v1.6). +It is therefore **outstanding, not done here**, and is named in the +workplan so it is not lost between passes — which is exactly how +`ground-game`'s ten rulings went uncollected for two days. + +## 7. Cost + +CB-WP-0028's cost, by re-running the instrument: `make cost`. Not inlined +(§Single source of fact). CB-EV-0019 §4's unbounded chain is still +unbounded, now across four more passes. + +## Open after this pass + +- **The window-2 verdict** (§6) — the next thing this loop owes itself. +- **The wrong-denominator family still has no control** (§2), at five + instances. +- **Whether the tracks change what a player sees coming** (§4) is untested + and only play tests it. +- **`Relations`, `DARVO`, `Player_Mats` and `Glossary` remain unvendored.** + The DARVO track now has a picture but not the edition's own words for + each stage. +- **Three of four scenarios still never dealt**, carried from ADR-0015 D5. diff --git a/workplans/CB-WP-0029-the-tokens-on-the-table.md b/workplans/CB-WP-0029-the-tokens-on-the-table.md index 024d3d8..8159503 100644 --- a/workplans/CB-WP-0029-the-tokens-on-the-table.md +++ b/workplans/CB-WP-0029-the-tokens-on-the-table.md @@ -2,7 +2,7 @@ id: CB-WP-0029 kind: product title: "The tokens on the table: components you can count, and a supply the engine may not respect" -status: active +status: done state_hub_workstream_id: "fb3d3c80-3798-4d0f-91ce-b8ea45943328" --- @@ -228,7 +228,7 @@ as a standing control. ```task id: CB-WP-0029-T04 -status: todo +status: done priority: medium state_hub_task_id: "29858777-6f8f-4d8e-90d0-cb193b7f4d29" ``` @@ -243,3 +243,22 @@ state_hub_task_id: "29858777-6f8f-4d8e-90d0-cb193b7f4d29" - **The window-2 verdict is due** (§The window closes here) — name it as outstanding if the closing pass has not run. - **Quote CB-WP-0028's cost by re-running the instrument.** + +**Done 2026-08-07.** +[CB-EV-0027](../evidence/CB-EV-0027-the-tokens-on-the-table.md). + +- **The supply is never exceeded**, and the link row explains why a supply + can need no enforcement: GR-L01's two slots per seat *are* the twelve + tokens, the same constraint written twice. +- **The Focus/Blame check was wrong and I caught it** — fifth instance of + the wrong-subject family, and **the first caught before leaving the + repo**. One data point, not a trend; the family still has no control. +- **The height limit went 460 → 500 as a correction.** 460 had no + derivation; 500 does. An underived limit is a measurement defect, and + replacing it is CB-WP-0021 T06's rule applied, not evaded. +- **Whether the tracks change what a player sees coming is untested** and + only play tests it. +- **Chaos window 2 is closed: twelve declarations, zero overrides.** Its + retirement condition was untestable from first to last. **The verdict is + the next thing this loop owes itself**, and it is a tier-M pass because + recording it changes how the loop constrains its own operation.