diff --git a/editions/ground-darvo-r0/PROVENANCE.md b/editions/ground-darvo-r0/PROVENANCE.md index 4ae96da..0ef55a6 100644 --- a/editions/ground-darvo-r0/PROVENANCE.md +++ b/editions/ground-darvo-r0/PROVENANCE.md @@ -46,6 +46,27 @@ falsifiers rather than merely skipped the designer placed *outside* the core; importing it would break the claim that this engine plays the edition as printed (ADR-0015 D2). +## The catalog and the H1 experiment package + +Vendored 2026-08-08 from `ground-game` `8886543` (CB-WP-0038). These are +**outside this directory** — `editions/catalog.yaml` and +`editions/experiments/h1-problem-stress/` — because the catalog selects +*between* packages and does not belong inside one. + +**Digests were claimed for these by CB-WP-0038 T01 and not recorded**; +the adversarial review ([CB-REV-0001](../../reviews/CB-REV-0001-h1.md)) +reported it unverified and it was a real gap. + +``` +sha256 eb21fa3237637790fef601fe6668a190a549715e9a78b7dfe47b40cd069b648e ../catalog.yaml +sha256 f58e81f84ea2b0d16e39932261eb3f3d9890345cdf37ad6f0b3abc00636840be ../experiments/h1-problem-stress/rules_delta.yaml +sha256 7b1cc0149122b855e827bc930576ed165bf7dd8d62707e845a9e514ce3521f8e ../experiments/h1-problem-stress/Actions.csv +``` + +**`rules_delta.yaml` is the load-bearing one**: it is the executable +statement of H1, and the kernel implements it. A silent change there is a +silent change to what we measured. + ## Digests ``` diff --git a/evidence/CB-EV-0030-h1-measured.md b/evidence/CB-EV-0030-h1-measured.md index 35bf00d..5e1ff70 100644 --- a/evidence/CB-EV-0030-h1-measured.md +++ b/evidence/CB-EV-0030-h1-measured.md @@ -20,10 +20,18 @@ than trusted, and the ATTACK panel re-run for both rule sets. `ground-game`'s success criteria (design note §3.2), answered one by one. **Three of four fail.** +> **CORRECTED 2026-08-08 after adversarial review** +> ([`CB-REV-0001`](../reviews/CB-REV-0001-h1.md)). Criteria 1 and 2 were +> rendered as flat failures **on the strength of the greedy column +> alone**, while this pass's own harness printed 31–1000 DARVO arms per +> cell in the rank-75 and rank-95 columns. That is the selective-column +> move this project says it exists to prevent, committed in the file that +> says so. The verdicts below are restated per column. + | # | criterion | verdict | |---|---|---| -| 1 | DARVO arm rate non-trivial, "not 0/500" | **fails** — still **0** under greedy at every seat count | -| 2 | ATTACK selection rises for some subpopulation | **fails** — greedy still attacks **0** times; and for the rank-75 policy ATTACK *fell*, 604 → 180 at 2p | +| 1 | DARVO arm rate non-trivial, "not 0/500" | **not a flat fail.** **0** under greedy; **81–1000 per cell** under rank-75 and rank-95, baseline *and* H1. It fails for a seat that regulates and passes for one that does not — see [CB-EV-0031](CB-EV-0031-a-seat-that-does-not-regulate.md) | +| 2 | ATTACK selection rises for some subpopulation | **mixed.** Greedy attacks **0** times; rank-75's ATTACK *fell* 604 → 180 at 2p; the unregulated seat's rose 0 → 726 | | 3 | group success does not collapse | **fails hardest** — greedy's wins go **165 → 0** (3p), **190 → 0** (4p), **200 → 0** (6p) | | 4 | Bond/GROUND stay better than DARVO | holds, but **vacuously**: DARVO never fires | diff --git a/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md b/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md index e7fa5be..c41d27e 100644 --- a/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md +++ b/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md @@ -4,9 +4,17 @@ CB-WP-0039. Tier M (adds a policy to the panel every future measurement is reported against; chaos d8 = 2 → no override). **Declaration 11 of chaos window 3.** Closed 2026-08-08. -> Like [CB-EV-0030](CB-EV-0030-h1-measured.md), **this has not had the -> tier-L adversarial review that pass owes, and none of it may reach -> `ground-game` until that review has run.** +> **CORRECTED 2026-08-08 after adversarial review, and most of the +> original file was wrong.** The review is in +> [`CB-REV-0001`](../reviews/CB-REV-0001-h1.md). The policy this pass +> called *"greedy with one preference changed"* differed in **five** +> places — including `SpendFreedom`, which it ranked 95 unconditionally +> where greedy ranks it 0 unless the gate bites, so the seat burned its +> Freedom token in round one of every game. **Every number below was +> measuring that bug.** They have been re-derived against a policy that +> delegates to `GreedyPolicy::rank` and overrides exactly one arm. +> +> Sections 1 and 2 are rewritten. **Section 3 is withdrawn entirely.** **Delivered:** the `reactive` policy, and the measurement CB-EV-0030 §4 named as the one it had not run. @@ -24,24 +32,27 @@ unreachable **by construction**. So "H1 does nothing" was really "H1 does nothing *to a seat that already manages its Stress*" — and H1 was written for the seat that does not. -`reactive` is greedy with **one preference changed**: GROUND demoted below -ATTACK. SHARED GROUND, 200 games per cell, all 200 ran in every cell: +`reactive` now **delegates to `GreedyPolicy::rank` and overrides one arm** +(`Ground if gated`), so "one preference differs" is structurally true +rather than a claim. SHARED GROUND, 200 games per cell, all 200 ran, and +**`peak` is now Stress held, not Stress assigned** (§2): | | | greedy | | | | reactive | | | |---|---:|---:|---:|---:|---:|---:|---:|---:| | **seats** | won | atk | darvo | peak | won | atk | darvo | peak | -| 2p | 68 | 0 | 0 | 4 | **0** | 1200 | **400** | **5** | -| 3p | 0 | 0 | 0 | 4 | **0** | 1800 | **400** | **5** | -| 4p | 0 | 0 | 0 | 4 | **0** | 2400 | **400** | **5** | -| 6p | 0 | 0 | 0 | 4 | **0** | 3600 | **400** | **5** | +| 2p | 68 | 0 | 0 | 4 | **66** | 726 | 363 | **5** | +| 3p | 0 | 0 | 0 | 4 | 0 | 1200 | 600 | **5** | +| 4p | 0 | 0 | 0 | 4 | 0 | 1600 | 800 | **5** | +| 6p | 0 | 0 | 0 | 4 | 0 | 2400 | 1200 | **5** | -**H1's mechanism works.** Criterion 1 (DARVO arm rate non-trivial) and -criterion 2 (ATTACK selection rises for some subpopulation) are **met** — -for the subpopulation H1 was aimed at. CB-EV-0030 reported both as -failures, and that report was about greedy. +**H1's mechanism does reach the unregulated seat.** DARVO arms **2 per +seat per game**, scaling with the table — where the earlier figure was a +flat 400 in every cell, which was an artifact of the broken policy. -**Criterion 3 still fails, and now worse.** Reactive wins **nothing**, at -any seat count, under H1. +**Criterion 3's verdict is corrected.** The original said reactive *"wins +nothing, at any seat count"*. **False.** At 2p it wins **66 against +greedy's 68** — indistinguishable. It wins nothing at 3p and above, which +is also where greedy wins nothing. ## 2. The baseline finding, which is larger than H1 @@ -49,40 +60,55 @@ Look at the baseline half of the same run. Greedy and reactive are **identical** — 132/165/190/200, zero attacks, zero DARVO, in all four bands. -**Peak Stress across 3,200 baseline games was 1.** Starting Stress is 2, -so no seat's Stress was ever *set* above 1: it only ever went down. +**Peak Stress held across the 1,600 baseline games was 2 — the starting +value, never exceeded.** Stress only ever went down. -> **The baseline's entire Stress economy is unreachable under this panel.** -> The gate at 4, the DARVO arm at 5, the Freedom token that buys past the -> gate — none of it can engage, and a policy built to be reckless with -> Stress plays identically to one built to husband it. +**Three corrections to what this section originally said.** -That is a deeper account of F17 than F17 has. ATTACK is not merely -unrewarding; **the pressure it is supposed to answer never exists.** H1 is -an attempt to fix exactly this, and it is aimed at the right thing. +1. It reported **1**, because `peak` was a maximum over `StressSet` event + *payloads*. Starting Stress is written by `setup`, never by an event, + so a table sitting at 2 all game reported 1 — and a table with no + `StressSet` at all would have reported 0. **Wrong subject**: the metric + answered *"highest value ever assigned"* while the prose claimed + *"highest Stress reached"*. +2. It said **3,200 baseline games**. The run is 3,200 *total*; the + baseline half is **1,600**. +3. It generalised to *"a policy built to be reckless with Stress plays + identically to one built to husband it"*. **False, and refuted by this + repo's own sibling harness**: `attack-value.rs`'s rank-95 policy is + exactly that policy, and under **baseline** it drives Stress to 5 and + arms DARVO 400–1000 times per cell. -## 3. What H1-B actually does, which is not what it was for +**What survives, stated narrowly.** Under both policies measured here — +neither of which ever selects ATTACK under baseline — **no Stress is ever +added at all**. The baseline's Stress economy is reachable only through +ATTACK, so a table that does not attack never meets the gate, the arm, or +the Freedom token. That is still a real observation about F17, and it is +**about ATTACK being the sole inbound pressure**, not about the economy +being unreachable in general. -`darvo` is **exactly 400 in every cell** while `atk` scales cleanly with -seats (1200 / 1800 / 2400 / 3600 = 3 per seat per game). A constant across -seat counts is an artifact smell, so it was measured rather than reported. +## 3. WITHDRAWN — "H1-B suppresses DARVO in the attacker" -Six-player final Stress, every seed: **`[5, 5, 4, 4, 4, 4]`**. Exactly two -seats arm, whatever the table size. +The original §3 argued that H1-B holds the attacker at Stress 4, below the +arm, and pushes its targets to 5 — inferred from a constant `darvo = 400` +and a final-Stress signature of `[5, 5, 4, 4, 4, 4]`. -**The self-soothe is doing it.** An attacker at Stress ≥ 4 takes −1 for a -landed Attack, which holds it at 4 — *below the arm* — while its targets -take +1 or +2 and climb to 5. **H1-B suppresses DARVO in the seat that -attacks and concentrates it in the seat attacked.** +**Withdrawn on two independent grounds, both found by the review.** -The hypothesis wanted ATTACK to be rational at high Stress. It is. The -side effect is that the aggressor is the one seat protected from arming. +1. **The constant was the broken policy.** With the one-arm-override + policy, `darvo` scales with seats (363/600/800/1200). There is no + constant to explain. +2. **Disabling H1-B changes the arm count by exactly zero**, in every + cell, under the corrected policy. The effect attributed to H1-B's + arithmetic was an interaction with the undeclared preference deltas. -**Stated as a limitation, not a finding about H1:** `reactive` picks its -Attack target by first-legal, not by any judgement, so the *concentration* -on two seats is partly an artifact of crude targeting. The direction — -attackers held at 4, targets pushed to 5 — follows from H1-B's arithmetic -and does not depend on the targeting. **The number 2 does.** +**And the inference was invalid even where the numbers held.** Final +Stress does not record who armed: `DarvoEnded` resets the stage to `Off` +and REVERSE applies −2 to its owner, so a seat can arm and finish below 5. +The review demonstrated six seats arming behind the same `[5,5,4,4,4,4]` +signature. **The original hedged the wrong variable** — it disclaimed "the +number 2" and defended "the direction", and the direction is the part that +failed. ## 4. What this does not say diff --git a/games/ground/examples/regulation.rs b/games/ground/examples/regulation.rs index 10def52..3884227 100644 --- a/games/ground/examples/regulation.rs +++ b/games/ground/examples/regulation.rs @@ -40,31 +40,22 @@ impl Policy for Reactive { .players .get(&seat) .is_some_and(|p| p.stress >= 4 && !p.freedom_gate_lifted); + // ONE ARM OVERRIDDEN, THE REST DELEGATED. + // + // The first version re-typed greedy's ranking and changed one + // line of the copy. It differed in five places, and the review + // found it: `SpendFreedom` ranked 95 unconditionally instead of + // greedy's `95 if gated else 0`, so this seat spent its Freedom + // token in round one of every game — a second change to the very + // mechanism under study. Delegating makes "exactly one preference + // differs" structurally true instead of a claim in a comment. let rank = |c: &GroundCommand| -> i32 { match c { GroundCommand::SelectAction { - action, problem, .. - } => match action { - // THE ONE LINE. Greedy has `Ground if gated => 100`. - Action::Ground if gated => 5, - Action::Solve - if problem - .and_then(|p| state.problems.get(&p)) - .is_some_and(|p| p.claimed_by.is_some()) => - { - 20 - } - Action::Solve => 90, - Action::Investigate => 80, - Action::Support => 70, - Action::Ground => 5, - Action::Attack => 10, - }, - GroundCommand::SpendFreedom => 95, - GroundCommand::ChooseGroundMode { .. } => 50, - GroundCommand::RespondToSupport { .. } => 90, - GroundCommand::ChooseDarvoTarget { .. } => 50, - GroundCommand::Reveal | GroundCommand::Resolve | GroundCommand::EndRound => -1, + action: Action::Ground, + .. + } if gated => 5, + other => GreedyPolicy::rank(state, seat, other), } }; let mut best = 0; @@ -77,6 +68,10 @@ impl Policy for Reactive { } } +/// `Scenarios.csv`: "All players start at Stress 2." Named rather than +/// inlined so the peak metric cannot silently disagree with setup. +const START_STRESS: u8 = 2; + struct Cell { games: u32, won: u32, @@ -136,17 +131,28 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce c.atk += 1; } } + // Peak Stress **held**, not peak Stress *assigned*. + // + // The first version took a maximum over `StressSet` PAYLOADS. + // Starting Stress is 2 and is written by `setup`, never by an + // event, so a table that sat at 2 all game reported **1**, and a + // table with no `StressSet` at all would report 0. CB-EV-0031 §2 + // built its headline claim on that number. Wrong subject: the + // metric answered "highest value ever assigned", the prose said + // "highest Stress reached". + let mut held: std::collections::BTreeMap = + g.state.players.keys().map(|s| (*s, START_STRESS)).collect(); + c.peak_stress = c.peak_stress.max(START_STRESS); for e in &g.events { if matches!(e, games_ground::GroundEvent::DarvoTriggered { .. }) { c.darvo += 1; } - // How close does Stress actually get to the arm at 5? That is - // the number H1's whole mechanism turns on, and CB-EV-0030 - // had to infer it from a single game. - if let games_ground::GroundEvent::StressSet { stress, .. } = e { + if let games_ground::GroundEvent::StressSet { player, stress } = e { + held.insert(*player, *stress); c.peak_stress = c.peak_stress.max(*stress); } } + let _ = held; } c } diff --git a/games/ground/src/bot.rs b/games/ground/src/bot.rs index 9e4fd3f..879fc6a 100644 --- a/games/ground/src/bot.rs +++ b/games/ground/src/bot.rs @@ -323,7 +323,20 @@ impl Policy for RandomPolicy { pub struct GreedyPolicy; impl GreedyPolicy { - fn rank(state: &GroundState, seat: PlayerId, cmd: &GroundCommand) -> i32 { + /// The ranking, **public so a variant policy can override exactly one + /// arm and inherit the rest** (CB-WP-0039, after review). + /// + /// It was private, so `regulation.rs` re-typed an abridged copy and + /// called it "greedy with one preference changed". It differed in + /// five places — including `SpendFreedom`, which the copy ranked 95 + /// unconditionally where this ranks it 0 unless the gate is biting, + /// so the "reactive" seat burned its Freedom token in round one of + /// every game. **A second change to the exact mechanism the pass was + /// studying**, and every number in CB-EV-0031 was measuring it. + /// + /// Making this callable removes the possibility rather than testing + /// for it: a caller that delegates cannot drift. + pub fn rank(state: &GroundState, seat: PlayerId, cmd: &GroundCommand) -> i32 { let gated = state .players .get(&seat) diff --git a/games/ground/src/lib.rs b/games/ground/src/lib.rs index 0feb7e9..c6f2423 100644 --- a/games/ground/src/lib.rs +++ b/games/ground/src/lib.rs @@ -2092,14 +2092,40 @@ mod tests { s } - /// **The load-bearing control.** A variant system that perturbs - /// the baseline invalidates every measurement this repo has. + /// **The load-bearing control**, rewritten after review found the + /// first version inert. + /// + /// It compared two states built by the *same* `setup` call, both + /// then assigned `Variant::Baseline` — bitwise identical by + /// construction, so the assertion could only fail if hashing were + /// nondeterministic. It carried no information about variants at + /// all. Adding `#[serde(skip)]` to `variant` — which is exactly + /// the "a scenario replayed under the wrong variant diverges + /// silently" failure the workplan named — left all 57 tests green. + /// + /// This asserts the two properties that were claimed: + /// **the variant reaches the hash**, and **selecting the baseline + /// is not a change**. #[test] - fn baseline_is_bit_for_bit_what_it_was() { + fn the_variant_reaches_the_hash_and_baseline_is_not_a_change() { for players in [2u8, 3, 6] { for seed in 0..8u64 { let base = setup(players, Variant::Baseline, seed); - let mut default_built = GroundState::setup( + let h1 = setup(players, Variant::H1ProblemStress, seed); + + // 1. The variant is IN the hash. `#[serde(skip)]` on + // the field makes these equal, which is the whole + // silent-divergence failure. + assert_ne!( + cb_events::state_hash_hex(&base), + cb_events::state_hash_hex(&h1), + "{players}p seed {seed}: the variant does not reach the state hash, \ + so a recording cannot say which rules it was played under" + ); + + // 2. And selecting the baseline explicitly is not a + // change from selecting nothing. + let untouched = GroundState::setup( &Setup { players, preset: format!("standard-{players}p"), @@ -2108,12 +2134,10 @@ mod tests { seed, ) .expect("setup"); - // Untouched: whatever `setup` produces IS baseline. - assert_eq!(default_built.variant, Variant::Baseline); - default_built.variant = Variant::Baseline; + assert_eq!(untouched.variant, Variant::Baseline, "the default moved"); assert_eq!( cb_events::state_hash_hex(&base), - cb_events::state_hash_hex(&default_built), + cb_events::state_hash_hex(&untouched), "{players}p seed {seed}: selecting the baseline changed it" ); } @@ -2226,6 +2250,68 @@ mod tests { ); } + /// **H1-A lands before the DARVO arm check** (review M5). + /// + /// The delta orders it "+1 Stress, then clamp, then DARVO arm + /// check as today". CB-WP-0038 claimed this was got right and + /// nothing tested it: moving the pressure after the arm check + /// left every test green. + /// + /// A seat at 4 with a Problem unclaimed must arm **in the same + /// round end** — pressure takes it to 5, and the check sees it. + #[test] + fn h1a_pressure_arms_darvo_in_the_same_round_end() { + let mut s = setup(3, Variant::H1ProblemStress, 7); + for p in s.players.values_mut() { + p.stress = 4; + p.darvo = DarvoStage::Off; + } + assert!(s.problems.values().any(|p| p.claimed_by.is_none())); + let events = s.end_round_events(); + assert!( + events + .iter() + .any(|e| matches!(e, GroundEvent::DarvoTriggered { .. })), + "pressure took the seat to 5 but the arm check did not see it, \ + so H1-A is ordered after it" + ); + // And the order is visible in the event stream: the Stress + // must be set before the trigger, not after. + let first_stress = events + .iter() + .position(|e| matches!(e, GroundEvent::StressSet { .. })); + let first_trigger = events + .iter() + .position(|e| matches!(e, GroundEvent::DarvoTriggered { .. })); + assert!( + first_stress < first_trigger, + "the arm check precedes the pressure in the event stream" + ); + } + + /// **H1-B must not fire on a GROUND—OU cancellation** (review M11). + /// + /// `resolve_attack` has two cancel paths and the existing test + /// exercised only Protection — it passed `&Default::default()` + /// for `ou_cancels`, so the other path was never reached. + #[test] + fn h1b_does_not_soothe_an_ou_cancelled_attack() { + let mut s = setup(3, Variant::H1ProblemStress, 3); + let seats: Vec = s.players.keys().copied().collect(); + let (a, t) = (seats[0], seats[1]); + s.players.get_mut(&a).expect("a").stress = 4; + let mut ou = std::collections::BTreeSet::new(); + ou.insert((a, t)); + let mut events = Vec::new(); + s.resolve_attack(a, t, &ou, &mut events); + assert!( + !events + .iter() + .any(|e| matches!(e, GroundEvent::StressSet { player, .. } if *player == a)), + "an Attack cancelled by GROUND—OU still soothed the attacker" + ); + } + /// **`rules_delta.yaml`'s `unchanged:` list is ground-game's claim /// about their own experiment, and it is checkable.** /// @@ -2260,6 +2346,61 @@ mod tests { base.solution_deck, h1.solution_deck, "{players}p: deck moved" ); + + // solve_legality / support / ground_modes: the SAME menu + // in the SAME position. Comparing setup fields could not + // see a delta that forbids an action -- the review made + // SOLVE illegal under H1 and this test stayed green. + let seats: Vec = h1.players.keys().copied().collect(); + for seat in &seats { + let mut lb: Vec = crate::bot::legal_commands(&base, *seat) + .iter() + .map(|c| format!("{c:?}")) + .collect(); + let mut lh: Vec = crate::bot::legal_commands(&h1, *seat) + .iter() + .map(|c| format!("{c:?}")) + .collect(); + lb.sort(); + lh.sort(); + assert_eq!( + lb, lh, + "{players}p {seat}: H1 changed which commands are legal" + ); + } + + // relation_slots: 2, asserted at the BOUNDARY. The old + // check asked `has_free_slot` of a seat with no relations, + // which holds for any capacity >= 1. + if seats.len() >= 3 { + let mut s = h1.clone(); + s.relations + .insert(Pair::new(seats[0], seats[1]), Relation::Bond); + assert!(s.has_free_slot(seats[0]), "one relation leaves a slot"); + s.relations + .insert(Pair::new(seats[0], seats[2]), Relation::Bond); + assert!( + !s.has_free_slot(seats[0]), + "{players}p: two relations must fill both slots -- capacity moved" + ); + } + + // darvo_stage_table: the arm sits at Stress 5. Every + // Problem claimed, so H1-A cannot add pressure and the + // only question is where the threshold is. + let mut s = h1.clone(); + for p in s.players.values_mut() { + p.stress = 4; + } + for p in s.problems.values_mut() { + p.claimed_by = Some(seats[0]); + } + assert!( + !s.end_round_events() + .iter() + .any(|e| matches!(e, GroundEvent::DarvoTriggered { .. })), + "{players}p: DARVO armed below Stress 5 -- the stage table moved" + ); } } } diff --git a/reviews/CB-REV-0001-h1.md b/reviews/CB-REV-0001-h1.md new file mode 100644 index 0000000..a8051be --- /dev/null +++ b/reviews/CB-REV-0001-h1.md @@ -0,0 +1,129 @@ +# CB-REV-0001 — adversarial review of the H1 measurement + +The tier-L review CB-WP-0038 owed (InnerLoop Step 2). One round: challenge, +then response. Run 2026-08-08 by a separate agent against +CB-WP-0038/CB-EV-0030 and CB-WP-0039/CB-EV-0031. + +> **Verdict: not approvable as submitted.** Thirteen challenges, five +> rated FATAL. **Every FATAL is conceded.** Nothing from either evidence +> file had reached `ground-game`, which is the only reason this is a +> correction rather than a retraction. + +The reviewer reproduced every number, re-derived on **three** samples +(seeds 0..200, 1000..1200, 5000..5500) and ran **14 kernel mutations**. + +--- + +## The five that were fatal + +### 1. `Reactive` was not "greedy with one preference changed" — it differed in five + +**Conceded, and it is the worst thing in the pass.** The policy re-typed +an abridged copy of greedy's ranking and changed one line *of the copy*, +under a comment reading `// THE ONE LINE`. It also differed in +`SpendFreedom` (95 unconditional against greedy's `95 if gated else 0`), +`Solve` on a claimed Problem, `ChooseGroundMode` and `RespondToSupport`. + +**The `SpendFreedom` difference is a second change to the exact mechanism +under study**: the seat burned its Freedom token in round one of every +game, ungated. Measured by the reviewer: 1200 spends against greedy's 0. + +**This pass claimed ADR-0018's one-varying-parameter discipline in its own +workplan while violating it.** That is worse than not claiming it. + +**Fixed structurally rather than by testing:** `GreedyPolicy::rank` is now +`pub`, and `Reactive` delegates to it and overrides a single match arm. A +caller that delegates cannot drift. The reviewer's `StrictReactive` +numbers reproduce exactly. + +### 2. The H1-B suppression mechanism does not survive the corrected policy + +**Conceded and withdrawn.** The reviewer disabled H1-B and re-ran: +`darvo` is **identical in every cell**. The effect CB-EV-0031 §3 attributed +to "H1-B's arithmetic" was an interaction with challenge 1's bugs. + +**The hedge was on the wrong variable.** §3 disclaimed *"the number 2"* +and defended *"the direction"*. The direction is what failed. + +### 3. `[5,5,4,4,4,4]` does not show that two seats armed + +**Conceded.** `DarvoEnded` resets the stage and REVERSE gives its owner +−2, so a seat can arm and end below 5. The reviewer exhibited **six** +seats arming behind the same signature. A terminal snapshot was used to +prove a path property. + +### 4. Criterion 1 was failed on the greedy column alone + +**Conceded.** CB-EV-0030 rendered *"DARVO arm rate still 0"* as a flat +failure while **its own printed table** showed 31–1000 arms per cell in +the rank-75 and rank-95 columns. **The selective-column move, in the file +that names selective reporting as the thing to avoid.** Restated per +column. + +### 5. "Peak Stress was 1" was a wrong-subject error, with two more defects + +**Conceded, all three.** `peak` maximised over `StressSet` event +*payloads*; starting Stress is written by `setup` and never by an event, +so a table sitting at 2 reported 1. True peak held: **2**. The baseline +game count was **1,600**, not 3,200. And the generalisation — *"a reckless +policy plays identically to a careful one"* — is refuted by this repo's +own rank-95 policy, which under **baseline** drives Stress to 5. + +**What survives is narrower and is now stated that way**: under the two +policies measured, neither of which selects ATTACK under baseline, no +Stress is ever added. ATTACK is the sole inbound pressure. + +--- + +## The serious ones + +| # | challenge | outcome | +|---|---|---| +| 6 | `baseline_is_bit_for_bit_what_it_was` compared two identically-constructed states — inert against its own threat; `#[serde(skip)]` on `variant` left 57/57 green | **conceded.** Replaced by `the_variant_reaches_the_hash_and_baseline_is_not_a_change`, which asserts baseline and H1 hash **differently**. The reviewer's M6 now goes red | +| 7 | the `unchanged:` test checked **3 of 7** entries; making SOLVE illegal under H1 left it green | **conceded.** Now compares `legal_commands` per seat (catches M9), checks relation slots at the **boundary** rather than on an empty seat, and asserts the DARVO arm is at 5 (catches M7). Both verified red | +| 8 | "criteria met" rests on a forced move — `reactive` ranks ATTACK at 10 and picks it only when the gate leaves nothing else | **conceded as a limitation, recorded, not fixed.** It is true that the instrument cannot show a null result once H1-A reaches Stress 4. That is a real weakness of the measurement and is now stated in CB-EV-0031 §4 | +| 9 | three claimed properties had no failing test: H1-A's ordering, H1-B on the DARVO extra Attack, H1-B on an OU-cancelled Attack | **two fixed** — `h1a_pressure_arms_darvo_in_the_same_round_end` and `h1b_does_not_soothe_an_ou_cancelled_attack`, both mutation-verified. **The DARVO extra-Attack path remains untested** and is carried as open | + +## The rest + +- **10 — H1-B's `after_target_and_relation_effects` ordering is vacuous + here.** Conceded: nothing between the two positions touches the + attacker's Stress, so the clause cannot be checked in this kernel. The + honest statement replaces the claim that it was got right. +- **11 — `regulation.rs` still skips setup failures silently.** Conceded; + the credit CB-EV-0030 §3 took was half-earned. The `play` path was + instrumented, the `setup` path was not. +- **13 — round-5 DARVO arms are counted but can never act**, and + `end_round_events` scores from `self` rather than `work`, so round-5 H1-A + Stress is invisible to the CommonProblem tiebreak. **Accepted as a real + defect in the other two modes**, which CB-EV-0030 reports on. Carried + open. +- **12 — reproduction and sample robustness: no problem found.** Every + number reproduced; no conclusion was seed-specific. **The failures were + of construction and interpretation, not sampling.** + +## What the reviewer could not check, and it is recorded rather than glossed + +The design note itself (criteria quoted from CB-EV-0030, not read from +source); the catalog digest, which the reviewer could not find and +reported as **unverified rather than absent**; `--variant` on the CLI; +`make cost`; and the two non-SHARED modes under `reactive`. + +**The catalog digest is a real gap.** CB-WP-0038 T01's control said *"the +catalog is vendored with a digest, like every other borrowed file"*, and +`PROVENANCE.md` records digests for the ten CSVs and **not** for +`catalog.yaml` or the H1 package. The control was claimed and not met. + +--- + +## What this cost, and what it bought + +The review found **five fatal defects in numbers that were one step from +another repository's design decision**, and four of the five were errors +of the exact class this project has been cataloguing since ADR-0018 — +correct computation, wrong subject. + +**The instrument caught the instrument's author.** CB-WP-0039 was itself +written to correct CB-EV-0030, and it introduced worse errors than the +ones it fixed. **The lesson is not "review works" — it is that a pass +written to correct a previous pass inherits none of its caution.** diff --git a/tools/edition-check.py b/tools/edition-check.py index 79fd599..75c359a 100755 --- a/tools/edition-check.py +++ b/tools/edition-check.py @@ -50,13 +50,29 @@ def vendored_files(): return sorted(f for f in os.listdir(d) if f.endswith(".csv")) +# CB-WP-0038 vendored two things that are NOT inside the edition +# directory: `editions/catalog.yaml`, which selects between packages and +# so belongs above them, and the `h1-problem-stress` experiment package. +# +# Their digests were CLAIMED by that pass and never recorded. The +# adversarial review (CB-REV-0001) could not find them and reported the +# control unverified — correctly. Recorded relative to `editions/`. +SIBLINGS = "../" + + +def sibling_files(): + """Recorded paths that live beside the edition rather than in it.""" + return sorted(f for f in recorded() if f.startswith(SIBLINGS)) + + def check(): want = recorded() print("edition-check — vendored data against its provenance") rc = 0 - present = vendored_files() - undocumented = [f for f in present if f not in want] + siblings = sibling_files() + present = vendored_files() + siblings + undocumented = [f for f in vendored_files() if f not in want] if undocumented: print(f" [FAIL] vendored with no recorded digest: {', '.join(undocumented)}") rc = 1 @@ -69,6 +85,7 @@ def check(): if name not in want: continue have = digest(os.path.join(ROOT, EDITION, name)) + # `../x` resolves out of the edition dir, which is the point. if have != want[name]: print(f" [FAIL] {name} does not match its recorded digest") print(f" recorded {want[name]}\n actual {have}") @@ -125,11 +142,18 @@ def self_test(): chk("every vendored file has a recorded digest", all(f in want for f in present), "a file added without a digest must fail, not be skipped") + # `present` is the edition's own CSVs plus the sibling paths recorded + # for the catalog and the experiment package (CB-WP-0038). + everything = present + sibling_files() chk("every digest names a file that is here", - all(f in present for f in want), + all(f in everything for f in want), "a stale digest is a lie with a filename") chk("digests match the real files", - all(digest(os.path.join(ROOT, EDITION, f)) == want[f] for f in present)) + all(digest(os.path.join(ROOT, EDITION, f)) == want[f] for f in everything)) + chk("the sibling packages are covered", + len(sibling_files()) >= 3, + "catalog.yaml and rules_delta.yaml decide WHAT WE MEASURED; " + "CB-WP-0038 claimed digests for them and recorded none") # The control that matters: a changed byte must be detected. import tempfile diff --git a/workplans/CB-WP-0039-a-seat-that-does-not-regulate.md b/workplans/CB-WP-0039-a-seat-that-does-not-regulate.md index ac231ac..06d304a 100644 --- a/workplans/CB-WP-0039-a-seat-that-does-not-regulate.md +++ b/workplans/CB-WP-0039-a-seat-that-does-not-regulate.md @@ -64,27 +64,47 @@ state_hub_task_id: "2c509cdb-aedb-4dce-a21f-f16bcdf17d80" - **the answer may be that H1 still fails**, and that is reported as plainly as the other outcome. -**Done 2026-08-08.** +**Done 2026-08-08, then CORRECTED after adversarial review** +([CB-REV-0001](../reviews/CB-REV-0001-h1.md)). [CB-EV-0031](../evidence/CB-EV-0031-a-seat-that-does-not-regulate.md). -**It changed CB-EV-0030's verdict.** H1's criteria 1 and 2 are **met** for -the seat H1 was aimed at: DARVO arms 400 times per cell and ATTACK is -selected 3 times per seat per game. The earlier "H1 does nothing" was a -statement about greedy, which grounds the moment the gate bites. +> **This task's own control — "exactly one preference differs from +> greedy" — was false when it was marked done.** The policy differed in +> five places, including `SpendFreedom`, which made the seat burn its +> Freedom token in round one of every game: a second change to the exact +> mechanism under study. **Every number below was measuring that.** +> +> `GreedyPolicy::rank` is now public and the policy delegates to it, +> overriding one match arm — so the control is structurally true rather +> than asserted in a comment. -**Criterion 3 fails harder**: reactive wins nothing, at any seat count. +**It changed CB-EV-0030's verdict**, and that part survives: H1 does reach +the unregulated seat. DARVO arms **2 per seat per game**, scaling with the +table — the earlier flat "400 in every cell" was the broken policy. -**The larger finding is about the baseline, not H1.** Greedy and reactive -play *identically* under baseline — same wins, zero attacks, zero DARVO — -and **peak Stress across 3,200 baseline games was 1**, against a starting -value of 2. The gate at 4, the arm at 5 and the Freedom token are all -unreachable. That is a deeper account of F17 than F17 has, and it is -**not raised as a finding here** because it wants the plural panel first. +**Criterion 3's verdict is corrected and was wrong**: reactive wins +**66 at 2p against greedy's 68**, not "nothing at any seat count". It wins +nothing at 3p and above, and so does greedy. -**A constant was investigated rather than reported.** `darvo` was exactly -400 in every cell while `atk` scaled properly with seats. Six-player final -Stress is `[5, 5, 4, 4, 4, 4]` every seed: **H1-B holds the attacker at 4, -below the arm, and pushes its targets to 5** — the self-soothe suppresses -DARVO in the aggressor and concentrates it in the attacked. The direction -follows from H1-B's arithmetic; **the number 2 is partly an artifact of -`reactive`'s first-legal targeting**, and is labelled as such. +**The baseline observation survives only in a narrower form.** Peak Stress +*held* across the **1,600** baseline games was **2** — the starting value, +never exceeded. The original said **1** because the metric maximised over +`StressSet` event payloads and the starting value is written by `setup`, +never by an event: **wrong subject**, in the pass that was written to +correct a wrong-subject error. + +And the generalisation was refuted by this repo's own harness: the rank-95 +policy *is* a reckless policy and it drives Stress to 5 under **baseline**. +What survives: under the two policies here, neither of which selects ATTACK +under baseline, **no Stress is ever added** — ATTACK is the sole inbound +pressure. + +**The H1-B suppression claim is WITHDRAWN.** It was inferred from a +constant that the broken policy produced, and disabling H1-B under the +corrected policy changes the arm count by **exactly zero** in every cell. +The inference was also invalid on its own terms: final Stress does not +record who armed, because `DarvoEnded` resets the stage and REVERSE gives +its owner −2. + +**The pass hedged the wrong variable** — it disclaimed "the number 2" and +defended "the direction", and the direction is what failed.