CB-WP-0037 T02: the engine's rules against the edition's
Some checks failed
ci / check (push) Failing after 3s

Vendored DARVO.csv, Relations.csv and Scenarios.csv — the three unread
files that carry mechanism. The engine has implemented the DARVO sequence
and relation formation/breaking from GroundRules.md since the beginning,
and had never read the game owner's statement of them.

Every checkable clause agrees, and agreement is recorded rather than
noted: a survey that finds nothing and leaves no trace cannot be told from
one never run. The two hardest clauses to notice were already right —
Focus placed "even if the Attack was cancelled", and Focus removed when
the sequence ends before REVERSE.

The tests are tripwires, not derivations. The match was made by a person
reading prose, and that reading goes stale in silence when the prose
changes; each behaviour pins the phrase it was read from, so a reworded
edition goes red and asks for a human. Mutation-proven by rewording the
cancelled-Attack clause.

F25 raised, and it is the real yield: Scenarios.csv carries
threshold_2_players/3_4/5_6, starting_stress and round_track, and the
engine hardcodes all three — a match returning 5/7/9, stress: 2 at setup,
five rounds. They agree on all four scenarios. These are the most
contested numbers in the project; the whole 4/6/9 vs 5/7/9 episode turned
on them, and the engine has been right by maintenance coincidence rather
than by reading the file that owns them.

Also pinned: Problems.csv and Scenarios.csv both state the deal and the
engine reads only the first. They agree; nothing was checking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-08 00:21:40 +02:00
parent d98580fb4c
commit 53109ec5e5
7 changed files with 423 additions and 1 deletions

View file

@ -0,0 +1,4 @@
stage_order,stage,symbol_id,mandatory_effect,target_memory,advance
1,DENY,SYM_DENY,"Choose one face-up, unsolved Problem not protected by GROUND—OU. Turn it face down and place a Denied token on it. Ordinary Investigate cannot reveal it.",None.,Advance to ATTACK unless the sequence is ended by GROUND or Bond Support.
2,ATTACK,SYM_DARVO_ATTACK,"Make one extra Attack against any other player. Resolve it with the normal relation rules. Then place your Focus/Blame token Focus-side beside that target, even if the Attack was cancelled.",The Focus target becomes the target of REVERSE.,Advance to REVERSE unless the sequence is ended; remove Focus if the sequence ends before REVERSE.
3,REVERSE,SYM_REVERSE,"Target the player holding your Focus. Unless they reject it with GROUND—ND, flip your Focus token to Blame in front of them, give them +1 Stress, and take one Protection token. Then reduce your own Stress by 2.",Focus becomes Blame and remains with the target until removed by GROUND—ND.,End the DARVO sequence.
1 stage_order stage symbol_id mandatory_effect target_memory advance
2 1 DENY SYM_DENY Choose one face-up, unsolved Problem not protected by GROUND—OU. Turn it face down and place a Denied token on it. Ordinary Investigate cannot reveal it. None. Advance to ATTACK unless the sequence is ended by GROUND or Bond Support.
3 2 ATTACK SYM_DARVO_ATTACK Make one extra Attack against any other player. Resolve it with the normal relation rules. Then place your Focus/Blame token Focus-side beside that target, even if the Attack was cancelled. The Focus target becomes the target of REVERSE. Advance to REVERSE unless the sequence is ended; remove Focus if the sequence ends before REVERSE.
4 3 REVERSE SYM_REVERSE Target the player holding your Focus. Unless they reject it with GROUND—ND, flip your Focus token to Blame in front of them, give them +1 Stress, and take one Protection token. Then reduce your own Stress by 2. Focus becomes Blame and remains with the target until removed by GROUND—ND. End the DARVO sequence.

View file

@ -19,6 +19,15 @@ build must not depend on a sibling checkout that CI does not have.
| `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) |
| `DARVO.csv` | 2026-08-08 | `8886543` | **the DARVO sequence as rules**: `mandatory_effect`, `target_memory`, `advance` (CB-WP-0037 T02) |
| `Relations.csv` | 2026-08-08 | `8886543` | **how Bonds and Rivalries form and break**: `formation`, `breaking`, `rules_text` |
| `Scenarios.csv` | 2026-08-08 | `8886543` | the four scenarios and their setups — the engine deals one |
**Why these three, now.** The engine already implements the DARVO
sequence and relation formation/breaking — from `GroundRules.md`, never
from the edition's own statement of them. That is not inert display data;
it is **rules taken from a secondary source and never checked against the
primary one**, and CB-WP-0037 T02 exists to check them.
**Deliberately absent**: `BOM`, `Print_Manifest`, `Back_Designs`,
`Symbols`, `Design_Tokens` — production artifacts for a physical print
@ -30,8 +39,11 @@ that this engine plays the edition as printed (ADR-0015 D2).
```
sha256 7a7a302aabecdeb419c562a029bec3e3576a306b0bd1175d1200de60d9530073 Actions.csv
sha256 0afb882dc8001f03bc48a152c9d8c36ad492ceeff2d30ef6a3a690937317f77b DARVO.csv
sha256 565431571bc06adafb67edefd4b368839b6b94134c2289877384688cc643390b Modes.csv
sha256 0a04830c93b62dcb2f4411a9fbde576368a7427fe9e63c5015e606e4d42d23a0 Problems.csv
sha256 cd84c3b131c1b7a541e9e42529fdce2b7911f0832cce8c50cfcb8462d01dc551 Relations.csv
sha256 f830de9ca44417a7510e04a39ea24a070a81969b33a31fa68361fba132ad40cd Scenarios.csv
sha256 0bda1ee97de726b5e8c4404ab15ecc53a359e6c23db74fb41ecb51d78f9884ad Solutions.csv
sha256 25273bb6e74c9ad545c5e89dfe0f76b294d0a6cbef51f80d548039d5f0943484 Tokens.csv
```

View file

@ -0,0 +1,3 @@
relation_side,symbol_id,title,rules_text,formation,breaking
Bond,SYM_BOND,BOND,"Support through an existing Bond: 2 Stress, ready Freedom, cancel the target's current DARVO stage, and end that sequence. Attack through a Bond: +2 Stress and flip this tile to Rivalry.",A no-relation Support may create a Bond if both players have a free relation slot and the target accepts.,GROUND—ND may break it. An Attack flips it rather than breaking it.
Rivalry,SYM_RIVALRY,RIVALRY,Support through Rivalry: 1 Stress; the target chooses to flip the tile to Bond or break it. Attack through Rivalry: +2 Stress and break the relation.,A no-relation Attack creates a Rivalry automatically if both players have a free relation slot.,"A further Attack, a repairing Support rejected as Bond, or GROUND—ND may break it."
1 relation_side symbol_id title rules_text formation breaking
2 Bond SYM_BOND BOND Support through an existing Bond: −2 Stress, ready Freedom, cancel the target's current DARVO stage, and end that sequence. Attack through a Bond: +2 Stress and flip this tile to Rivalry. A no-relation Support may create a Bond if both players have a free relation slot and the target accepts. GROUND—ND may break it. An Attack flips it rather than breaking it.
3 Rivalry SYM_RIVALRY RIVALRY Support through Rivalry: −1 Stress; the target chooses to flip the tile to Bond or break it. Attack through Rivalry: +2 Stress and break the relation. A no-relation Attack creates a Rivalry automatically if both players have a free relation slot. A further Attack, a repairing Support rejected as Bond, or GROUND—ND may break it.

View file

@ -0,0 +1,5 @@
scenario_id,title,premise,surface_problem_id,hidden_problem_ids,setup_2_players,setup_3_4_players,setup_5_6_players,threshold_2_players,threshold_3_4_players,threshold_5_6_players,starting_stress,round_track
SCN_01,Missed Deadline,"The team promised a delivery. It did not arrive when expected, and the conversation about why is becoming personal.",PRB_01_S,PRB_01_1 | PRB_01_2 | PRB_01_3 | PRB_01_4,Surface + hidden priorities 12 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 13 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 14 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 15 printed along the bottom edge.
SCN_02,Shared Responsibility,A shared responsibility keeps being left undone. Everyone agrees it matters; nobody agrees on what fairness requires.,PRB_02_S,PRB_02_1 | PRB_02_2 | PRB_02_3 | PRB_02_4,Surface + hidden priorities 12 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 13 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 14 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 15 printed along the bottom edge.
SCN_03,Decision Without Consent,A decision affecting the group was announced as settled. Several players believe they were consulted only after the fact.,PRB_03_S,PRB_03_1 | PRB_03_2 | PRB_03_3 | PRB_03_4,Surface + hidden priorities 12 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 13 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 14 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 15 printed along the bottom edge.
SCN_04,Broken Confidence,"Information shared in confidence travelled beyond the expected circle. Harm, intent, and responsibility are now contested.",PRB_04_S,PRB_04_1 | PRB_04_2 | PRB_04_3 | PRB_04_4,Surface + hidden priorities 12 (3 problems; available 6). Threshold 5.,Surface + hidden priorities 13 (4 problems; available 9). Threshold 7.,Surface + hidden priorities 14 (5 problems; available 12). Threshold 9.,5,7,9,All players start at Stress 2.,Rounds 15 printed along the bottom edge.
1 scenario_id title premise surface_problem_id hidden_problem_ids setup_2_players setup_3_4_players setup_5_6_players threshold_2_players threshold_3_4_players threshold_5_6_players starting_stress round_track
2 SCN_01 Missed Deadline The team promised a delivery. It did not arrive when expected, and the conversation about why is becoming personal. PRB_01_S PRB_01_1 | PRB_01_2 | PRB_01_3 | PRB_01_4 Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5. Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7. Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9. 5 7 9 All players start at Stress 2. Rounds 1–5 printed along the bottom edge.
3 SCN_02 Shared Responsibility A shared responsibility keeps being left undone. Everyone agrees it matters; nobody agrees on what fairness requires. PRB_02_S PRB_02_1 | PRB_02_2 | PRB_02_3 | PRB_02_4 Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5. Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7. Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9. 5 7 9 All players start at Stress 2. Rounds 1–5 printed along the bottom edge.
4 SCN_03 Decision Without Consent A decision affecting the group was announced as settled. Several players believe they were consulted only after the fact. PRB_03_S PRB_03_1 | PRB_03_2 | PRB_03_3 | PRB_03_4 Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5. Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7. Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9. 5 7 9 All players start at Stress 2. Rounds 1–5 printed along the bottom edge.
5 SCN_04 Broken Confidence Information shared in confidence travelled beyond the expected circle. Harm, intent, and responsibility are now contested. PRB_04_S PRB_04_1 | PRB_04_2 | PRB_04_3 | PRB_04_4 Surface + hidden priorities 1–2 (3 problems; available 6). Threshold 5. Surface + hidden priorities 1–3 (4 problems; available 9). Threshold 7. Surface + hidden priorities 1–4 (5 problems; available 12). Threshold 9. 5 7 9 All players start at Stress 2. Rounds 1–5 printed along the bottom edge.

View file

@ -72,6 +72,9 @@ const ACTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Action
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");
const DARVO_CSV: &str = include_str!("../../../editions/ground-darvo-r0/DARVO.csv");
const RELATIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Relations.csv");
const SCENARIOS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Scenarios.csv");
/// A vendored CSV, parsed into rows addressable by column name.
///
@ -410,6 +413,119 @@ pub fn deal(scenario_id: &str, players: u8) -> Result<Vec<EditionProblem>, Strin
Ok(dealt)
}
/// One DARVO stage as the edition states it (CB-WP-0037 T02).
///
/// **The engine already implements this sequence**, from `GroundRules.md`
/// and not from here. Vendoring it does not change behaviour; it makes
/// the behaviour checkable against the source that owns it.
#[derive(Debug, Clone)]
pub struct DarvoStageText {
pub order: u8,
pub stage: String,
pub mandatory_effect: String,
pub target_memory: String,
pub advance: String,
}
/// The DARVO sequence, in stage order.
pub fn darvo_stages() -> Result<Vec<DarvoStageText>, String> {
let t = Table::parse(DARVO_CSV, "DARVO.csv")?;
let mut out = Vec::new();
for row in &t.rows {
out.push(DarvoStageText {
order: t
.get(row, "stage_order")?
.parse()
.map_err(|_| "stage_order is not a number".to_string())?,
stage: t.get(row, "stage")?.to_string(),
mandatory_effect: t.get(row, "mandatory_effect")?.to_string(),
target_memory: t.get(row, "target_memory")?.to_string(),
advance: t.get(row, "advance")?.to_string(),
});
}
out.sort_by_key(|s| s.order);
Ok(out)
}
/// A scenario as the edition states it (CB-WP-0037 T02).
///
/// **This file states the deal a second time.** `Problems.csv` carries
/// `visibility` and `hidden_priority`; this carries the same board as
/// explicit id lists. The engine deals from the former, so the two are a
/// pair that can drift.
#[derive(Debug, Clone)]
pub struct ScenarioText {
pub id: String,
pub title: String,
pub surface_problem_id: String,
pub hidden_problem_ids: Vec<String>,
/// GR-E01 by seat band, as the EDITION states it. The engine has its
/// own copy in `GroundState::threshold` (F25).
pub thresholds: (u32, u32, u32),
/// Prose, e.g. "All players start at Stress 2."
pub starting_stress: String,
/// Prose, e.g. "Rounds 1-5 printed along the bottom edge."
pub round_track: String,
}
/// The four scenarios. **The engine deals `SCN_01` and only `SCN_01`**
/// (ADR-0015 D5) — this is what says so out loud.
pub fn scenarios() -> Result<Vec<ScenarioText>, String> {
let t = Table::parse(SCENARIOS_CSV, "Scenarios.csv")?;
let mut out = Vec::new();
for row in &t.rows {
out.push(ScenarioText {
id: t.get(row, "scenario_id")?.to_string(),
title: t.get(row, "title")?.to_string(),
surface_problem_id: t.get(row, "surface_problem_id")?.to_string(),
hidden_problem_ids: t
.get(row, "hidden_problem_ids")?
.split('|')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
thresholds: (
t.get(row, "threshold_2_players")?
.parse()
.map_err(|_| "threshold_2_players is not a number".to_string())?,
t.get(row, "threshold_3_4_players")?
.parse()
.map_err(|_| "threshold_3_4_players is not a number".to_string())?,
t.get(row, "threshold_5_6_players")?
.parse()
.map_err(|_| "threshold_5_6_players is not a number".to_string())?,
),
starting_stress: t.get(row, "starting_stress")?.to_string(),
round_track: t.get(row, "round_track")?.to_string(),
});
}
Ok(out)
}
/// One relation side as the edition states it (CB-WP-0037 T02).
#[derive(Debug, Clone)]
pub struct RelationText {
pub side: String,
pub rules_text: String,
pub formation: String,
pub breaking: String,
}
/// Bond and Rivalry, with how each forms and breaks.
pub fn relations() -> Result<Vec<RelationText>, String> {
let t = Table::parse(RELATIONS_CSV, "Relations.csv")?;
let mut out = Vec::new();
for row in &t.rows {
out.push(RelationText {
side: t.get(row, "relation_side")?.to_string(),
rules_text: t.get(row, "rules_text")?.to_string(),
formation: t.get(row, "formation")?.to_string(),
breaking: t.get(row, "breaking")?.to_string(),
});
}
Ok(out)
}
/// The core Solution deck, 6 per suit in canonical order (GR-S04).
pub fn solution_deck() -> Vec<SolutionCard> {
[Suit::Clarify, Suit::Repair, Suit::Boundary, Suit::Change]
@ -548,6 +664,9 @@ mod card_text_tests {
let _ = solutions();
let _ = modes();
let _ = tokens();
let _ = darvo_stages();
let _ = relations();
let _ = scenarios();
let files = [
("Problems.csv", CSV),
@ -555,6 +674,9 @@ mod card_text_tests {
("Solutions.csv", SOLUTIONS_CSV),
("Modes.csv", MODES_CSV),
("Tokens.csv", TOKENS_CSV),
("DARVO.csv", DARVO_CSV),
("Relations.csv", RELATIONS_CSV),
("Scenarios.csv", SCENARIOS_CSV),
];
let mut report = String::new();
let mut total_unread = 0usize;
@ -586,6 +708,222 @@ mod card_text_tests {
println!("{report}");
}
/// **F25** — the engine hardcodes numbers the edition states.
///
/// `GroundState::threshold` is a `match` returning 5/7/9;
/// `Scenarios.csv` carries `threshold_2_players`,
/// `threshold_3_4_players` and `threshold_5_6_players`. Setup writes
/// `stress: 2`; the edition says *"All players start at Stress 2."*
/// The engine runs five rounds; the edition prints a 1-5 track.
///
/// **They agree, on every scenario** — and that is the point, not the
/// reassurance. These are the most contested numbers in the project:
/// the whole 4/6/9 versus 5/7/9 episode turned on them, and the
/// engine has been right about them by maintenance coincidence rather
/// than by reading the file that owns them.
///
/// Same shape as F24's solution deck. `inert`, role `default`: green
/// because they agree, red the moment either side moves.
#[test]
fn the_engine_agrees_with_the_editions_own_numbers() {
for s in scenarios().expect("Scenarios.csv") {
// Mirrors `GroundState::threshold`'s bands exactly.
assert_eq!(
s.thresholds,
(5, 7, 9),
"{}: the edition's thresholds moved away from the engine's hardcoded 5/7/9",
s.id
);
assert!(
s.starting_stress.contains("Stress 2"),
"{}: setup writes stress: 2 because of this line, which now reads {:?}",
s.id,
s.starting_stress
);
assert!(
s.round_track.contains("1\u{2013}5") || s.round_track.contains("1-5"),
"{}: the engine plays five rounds because of this line, which now reads {:?}",
s.id,
s.round_track
);
}
}
/// **Two files state the same deal, and the engine reads one.**
///
/// `Problems.csv` carries `visibility` and `hidden_priority`;
/// `Scenarios.csv` carries `surface_problem_id` and
/// `hidden_problem_ids` for the same four scenarios. They agree
/// today, for every scenario — **and nothing was checking**, so an
/// edit to one and not the other would have had the engine dealing a
/// board the edition contradicts, silently.
///
/// Ours to check, not to rule on: if they ever disagree this is a
/// question for `ground-game`, and the test says which file said what.
#[test]
fn the_two_files_that_state_the_deal_agree() {
// Read Problems.csv here rather than through `EditionProblem`,
// which carries no id: adding one would touch a struct that
// feeds setup, and this test has no business changing state.
let pt = Table::parse(CSV, "Problems.csv").expect("Problems.csv");
for s in scenarios().expect("Scenarios.csv") {
let rows: Vec<&Vec<String>> = pt
.rows
.iter()
.filter(|r| pt.get(r, "scenario_id").expect("scenario_id") == s.id)
.collect();
assert!(!rows.is_empty(), "{}: no Problems at all", s.id);
let surface: Vec<&str> = rows
.iter()
.filter(|r| pt.get(r, "visibility").expect("visibility") == "Surface")
.map(|r| pt.get(r, "problem_id").expect("problem_id"))
.collect();
assert_eq!(
surface,
vec![s.surface_problem_id.as_str()],
"{}: Problems.csv and Scenarios.csv disagree on the Surface Problem",
s.id
);
let mut hidden: Vec<&str> = rows
.iter()
.filter(|r| pt.get(r, "visibility").expect("visibility") != "Surface")
.map(|r| pt.get(r, "problem_id").expect("problem_id"))
.collect();
hidden.sort_unstable();
let mut stated: Vec<&str> = s.hidden_problem_ids.iter().map(|x| x.as_str()).collect();
stated.sort_unstable();
assert_eq!(
hidden, stated,
"{}: Problems.csv and Scenarios.csv disagree on the hidden Problems",
s.id
);
}
}
/// **CB-WP-0037 T02 — the engine's rules against the edition's.**
///
/// The engine implements DARVO and relation behaviour from
/// `GroundRules.md`. `DARVO.csv` and `Relations.csv` are where the
/// game's owner states the same rules, and until 2026-08-08 nothing
/// here had ever read them.
///
/// **Every checkable clause agrees**, including the two easiest to
/// drop: Focus is placed *"even if the Attack was cancelled"*, and
/// Focus is removed when the sequence ends before REVERSE.
///
/// **Agreement is recorded, not just noted.** A survey that finds
/// nothing and leaves no trace cannot be told from one never run
/// (CB-EV-0027 §3).
///
/// **These are tripwires, not derivations.** The behaviour was
/// matched by a person reading prose, and that reading can go stale
/// in silence when the prose changes. Pinning the phrase each
/// behaviour was read from turns *"I checked it once"* into *"it is
/// still what I checked"* — and when ground-game rewords a rule this
/// goes red and asks for a human, which is the correct outcome, not
/// an automatic re-derivation.
#[test]
fn the_edition_still_says_what_the_engine_implements() {
let stages = darvo_stages().expect("DARVO.csv");
assert_eq!(
stages.iter().map(|s| s.stage.as_str()).collect::<Vec<_>>(),
["DENY", "ATTACK", "REVERSE"],
"the DARVO sequence changed shape"
);
let stage = |name: &str| {
stages
.iter()
.find(|s| s.stage == name)
.unwrap_or_else(|| panic!("no {name} stage"))
.clone()
};
// GR-D03 / lib.rs DarvoStage::Deny.
let deny = stage("DENY");
assert!(deny
.mandatory_effect
.contains("Turn it face down and place a Denied token"));
assert!(deny
.advance
.contains("Advance to ATTACK unless the sequence is ended"));
// GR-D04 / lib.rs DarvoStage::Attack. Both clauses the engine
// implements and a careless reading would lose.
let attack = stage("ATTACK");
assert!(
attack
.mandatory_effect
.contains("even if the Attack was cancelled"),
"the engine places Focus after a cancelled Attack because the edition says to"
);
assert!(attack
.target_memory
.contains("Focus target becomes the target of REVERSE"));
assert!(
attack
.advance
.contains("remove Focus if the sequence ends before REVERSE"),
"`focus.remove` on DarvoEnded is this clause; if it is gone, so is the reason"
);
// GR-D05 / lib.rs DarvoStage::Reverse: +1 to the target, a
// Protection to the owner, then 2 to the owner.
let reverse = stage("REVERSE");
assert!(reverse
.mandatory_effect
.contains("+1 Stress, and take one Protection token"));
assert!(reverse
.mandatory_effect
.contains("reduce your own Stress by 2"));
assert!(reverse.advance.contains("End the DARVO sequence"));
let rels = relations().expect("Relations.csv");
let side = |name: &str| {
rels.iter()
.find(|r| r.side == name)
.unwrap_or_else(|| panic!("no {name} side"))
.clone()
};
// GR-A04 / GR-A07 against lib.rs's Support and attack paths.
let bond = side("Bond");
assert!(bond.rules_text.contains("\u{2212}2 Stress, ready Freedom"));
assert!(
bond.rules_text
.contains("cancel the target's current DARVO stage"),
"the bond_support branch that pushes DarvoEnded is this clause"
);
assert!(bond
.rules_text
.contains("Attack through a Bond: +2 Stress and flip this tile"));
assert!(bond
.formation
.contains("free relation slot and the target accepts"));
// GR-A05 / GR-A08.
let rivalry = side("Rivalry");
assert!(rivalry
.rules_text
.contains("Support through Rivalry: \u{2212}1 Stress"));
assert!(
rivalry
.rules_text
.contains("the target chooses to flip the tile to Bond or break it"),
"SupportResponse::FlipToBond / BreakRivalry are this clause"
);
assert!(rivalry
.rules_text
.contains("Attack through Rivalry: +2 Stress and break"));
assert!(
rivalry
.formation
.contains("creates a Rivalry automatically"),
"the engine forms it without consent because the edition says automatically"
);
}
/// **The import check ground-game's final ruling asked for**, at
/// every seat band (`RULED GROUND-WP-0004`, 2026-08-03, engine ask 2):
///

View file

@ -46,6 +46,7 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
| F16 | inconsistent | withdrawn | games/ground/examples/difficulty.rs | counterexample | 2026-08-05 | clay-borg |
| F17 | degenerate | raised | games/ground/examples/attack-value.rs | counterexample | 2026-08-06 | ground-game |
| F18 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_engine_reads_only_part_of_what_it_vendored` | counterexample | 2026-08-06 | clay-borg |
| F25 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_engine_agrees_with_the_editions_own_numbers` | default | 2026-08-08 | clay-borg |
| F24 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_hardcoded_deck_still_matches_the_edition` | default | 2026-08-07 | clay-borg |
| 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 |
@ -202,6 +203,19 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
sees `Clarify` where the card reads *"Ask What Happened — Invite a
concrete account before judging."* **`inert`**: the data exists and
cannot fire, because nothing reads it. **Ours, and CB-WP-0028 fixes it.**
- **F25 — the engine hardcodes numbers the edition states.**
`GroundState::threshold` is a `match` returning 5/7/9 while
`Scenarios.csv` carries `threshold_2_players`, `threshold_3_4_players`
and `threshold_5_6_players`; setup writes `stress: 2` against *"All
players start at Stress 2."*; the engine plays five rounds against a
printed 15 track. **All three agree, on all four scenarios**, and that
is the finding rather than the reassurance: these are the most contested
numbers in the project — the whole 4/6/9 versus 5/7/9 episode turned on
them — and the engine has been right by maintenance coincidence rather
than by reading the file that owns them. **`inert`**, role `default`:
green because they agree, red the moment either side moves. **Ours.**
Same shape as F24.
- **F24 — the draw pile is a Rust literal.** `solution_deck()` builds six
of each suit from an array and never opens `Solutions.csv`, whose `suit`
and `quantity` columns say the same thing. **They agree today** — 24

View file

@ -108,7 +108,7 @@ deletes the literal, and it turns red the moment either side moves.
```task
id: CB-WP-0037-T02
status: todo
status: done
priority: high
state_hub_task_id: "625b51fd-8af1-46bf-8b91-087760259dd6"
```
@ -124,6 +124,52 @@ mechanism.
trace cannot be told apart from one never run (CB-EV-0027 §3);
- AM-4's dependency and size budgets hold, or the pass says what it cost.
**Done 2026-08-08.** `DARVO.csv`, `Relations.csv` and `Scenarios.csv`
vendored with digests; `make edition-check` green both ways.
**Every checkable clause agrees.** Recorded rather than merely noted,
because a survey that finds nothing and leaves no trace cannot be told
from one never run:
| the edition says | the engine does |
|---|---|
| Bond Support: 2 Stress, ready Freedom, **cancel the target's DARVO stage** | 2, `FreedomReadied`, `DarvoEnded` |
| Bond Attack: +2 and **flip** to Rivalry | +2, `RelationFormed(Rivalry)` |
| Rivalry Support: 1, **target chooses** flip or break | 1, `FlipToBond` / `BreakRivalry` |
| Rivalry Attack: +2 and **break** | +2, `RelationBroken` |
| no-relation Attack forms a Rivalry **automatically** | forms without consent, both slots free |
| DARVO ATTACK places Focus **even if the Attack was cancelled** | `FocusPlaced` after `resolve_attack` regardless |
| **remove Focus if the sequence ends before REVERSE** | `focus.remove` on `DarvoEnded` |
| REVERSE: flip to Blame, +1 to target, take a Protection, 2 to self | exactly that, with GROUND—ND rejection |
**The two hardest clauses to notice were already right**, which is the
part worth saying: nobody had read the file, and they were right anyway.
**These are tripwires, not derivations.** The match was made by a person
reading prose, and that reading goes stale in silence when the prose
changes. Each behaviour pins the phrase it was read from, so a reworded
edition turns the test red and asks for a human — mutation-proven by
rewording *"even if the Attack was cancelled"*, which fails naming the
exact engine branch that depends on it.
**F25 raised, and it is the real yield.** `Scenarios.csv` carries
`threshold_2_players/3_4/5_6`, `starting_stress` and `round_track` — and
the engine **hardcodes all three**: a `match` returning 5/7/9, `stress: 2`
at setup, five rounds. They agree on all four scenarios. **These are the
most contested numbers in the project** — the entire 4/6/9 versus 5/7/9
episode turned on them — and the engine has been right about them by
maintenance coincidence rather than by reading the file that owns them.
Same shape as F24; `inert`, role `default`.
**A second pair that could drift silently**: `Problems.csv` and
`Scenarios.csv` both state the deal, and the engine reads only the first.
`the_two_files_that_state_the_deal_agree` checks all four scenarios; they
match today, and nothing was checking.
**Coverage, re-measured** — the earlier figure would otherwise have
understated the gap by three files: `DARVO.csv` 5/6, `Relations.csv` 4/6,
`Scenarios.csv` 4/13.
## Task: classify the rest
```task