diff --git a/editions/ground-darvo-r0/DARVO.csv b/editions/ground-darvo-r0/DARVO.csv new file mode 100644 index 0000000..740e533 --- /dev/null +++ b/editions/ground-darvo-r0/DARVO.csv @@ -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. diff --git a/editions/ground-darvo-r0/PROVENANCE.md b/editions/ground-darvo-r0/PROVENANCE.md index 08d91b8..8cb861d 100644 --- a/editions/ground-darvo-r0/PROVENANCE.md +++ b/editions/ground-darvo-r0/PROVENANCE.md @@ -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 ``` diff --git a/editions/ground-darvo-r0/Relations.csv b/editions/ground-darvo-r0/Relations.csv new file mode 100644 index 0000000..e339c93 --- /dev/null +++ b/editions/ground-darvo-r0/Relations.csv @@ -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." diff --git a/editions/ground-darvo-r0/Scenarios.csv b/editions/ground-darvo-r0/Scenarios.csv new file mode 100644 index 0000000..ac2ad99 --- /dev/null +++ b/editions/ground-darvo-r0/Scenarios.csv @@ -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 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. +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. +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. +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. diff --git a/games/ground/src/edition.rs b/games/ground/src/edition.rs index 97c3818..d893dff 100644 --- a/games/ground/src/edition.rs +++ b/games/ground/src/edition.rs @@ -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, 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, 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, + /// 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, 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, 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 { [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> = 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::>(), + ["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): /// diff --git a/specs/FindingRegister.md b/specs/FindingRegister.md index 0656253..bc68d5d 100644 --- a/specs/FindingRegister.md +++ b/specs/FindingRegister.md @@ -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 1–5 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 diff --git a/workplans/CB-WP-0037-the-fourteen-unread-files.md b/workplans/CB-WP-0037-the-fourteen-unread-files.md index a91446c..855e691 100644 --- a/workplans/CB-WP-0037-the-fourteen-unread-files.md +++ b/workplans/CB-WP-0037-the-fourteen-unread-files.md @@ -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