diff --git a/Makefile b/Makefile index 8d8355e..4ab7814 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ PORT ?= 0 # Every cargo recipe runs at the repo root; the shell does not persist cd. IN_REPO := cd $(REPO) && -.PHONY: help ground check test sim bench bench-test coverage dep-weight cost cost-test cost-pin cost-budget shape-budget cost-mix loop-lint self-tests env-test task-done status facts-check facts-gen mutation-check size-metrics runtime-metrics build-time am6 am7 am8 edition-check replay-test loc play gate-review all \ +.PHONY: help ground check test sim bench bench-test coverage dep-weight cost cost-test cost-pin cost-budget shape-budget cost-mix loop-lint self-tests env-test task-done status facts-check facts-gen mutation-check size-metrics runtime-metrics build-time am6 am7 am8 edition-check replay-test loc play gate-review panels all \ design difficulty trials # `design`, `difficulty` and `trials` were added by CB-WP-0022, CB-WP-0025 # and CB-WP-0027 and none was declared here. Only `trials` revealed it, by @@ -228,6 +228,16 @@ trials: difficulty: @cargo run --release -q -p games-ground --example difficulty +# CB-REV-0002 #7: the H1 measurement harnesses were run by NO gate. Every +# number in CB-EV-0030 and CB-EV-0031 came from a manual invocation of an +# ungated binary -- so the assertion added to catch short cells was +# unreachable from `make`, and "what would the harness report if the work +# silently stopped" answered: green, and nothing else. +## the variant panels: ATTACK's value, and regulation under H1 +panels: + @cargo run --release -q -p games-ground --example attack-value + @cargo run --release -q -p games-ground --example regulation + # CB-WP-0022 T05: the design-finding register, reported over # specs/GroundRules.md. Shows the QUEUE by default; the log of closed # findings is a line, not a listing, because a default view that mixes diff --git a/editions/ground-darvo-r0/PROVENANCE.md b/editions/ground-darvo-r0/PROVENANCE.md index 0ef55a6..f3ff8f0 100644 --- a/editions/ground-darvo-r0/PROVENANCE.md +++ b/editions/ground-darvo-r0/PROVENANCE.md @@ -61,6 +61,7 @@ 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 +sha256 62785f5e7e245c60171624d15de2f40187a44fec54f93c7d9705cf52584b1078 ../experiments/h1-problem-stress/Rules_Text.csv ``` **`rules_delta.yaml` is the load-bearing one**: it is the executable diff --git a/evidence/CB-EV-0030-h1-measured.md b/evidence/CB-EV-0030-h1-measured.md index 5e1ff70..64e3eb2 100644 --- a/evidence/CB-EV-0030-h1-measured.md +++ b/evidence/CB-EV-0030-h1-measured.md @@ -20,18 +20,33 @@ 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. +> **CORRECTED TWICE.** Round 1 found criteria 1 and 2 rendered as flat +> failures **on the greedy column alone**, while this pass's own harness +> printed non-zero DARVO arms in the rank-75 and rank-95 columns — the +> selective-column move, in the file that names it. +> +> **Round 2 found the correction was itself selective, and its numbers +> wrong** ([`CB-REV-0002`](../reviews/CB-REV-0002-h1.md) #2): it quoted +> "81–1000 per cell … baseline *and* H1" and, twelve lines away, +> "31–1000". Both ranges came from the **baseline** rank-75 row. Under +> **H1**, rank-75 arms are `2p 59, 3p 0, 4p 0, 6p 0` — three cells at zero +> and the fourth below the stated floor. +> +> **Every cell is now given**, because two attempts at summarising this +> into a range have now produced three wrong numbers. + +**DARVO arms per 200-game cell, SHARED GROUND** (all three modes agree on +arms; they differ only in who wins): + +| | greedy | rank-75 | rank-95 | +|---|---:|---:|---:| +| **baseline** 2p / 3p / 4p / 6p | 0 / 0 / 0 / 0 | 81 / 125 / 218 / 31 | 400 / 805 / 1000 / 1000 | +| **H1** 2p / 3p / 4p / 6p | 0 / 0 / 0 / 0 | **59 / 0 / 0 / 0** | 600 / 870 / 941 / 970 | | # | criterion | verdict | |---|---|---| -| 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 | +| 1 | DARVO arm rate non-trivial, "not 0/500" | **depends entirely on the policy, and H1 does not improve it for the middle one.** Zero under greedy in both. Under rank-75, H1 **reduces** arms to zero at 3p and above. Only rank-95 — a seat that always attacks — arms freely, and it does so **under the baseline too** | +| 2 | ATTACK selection rises for some subpopulation | **no, for the policy the criterion is about.** Greedy attacks 0 times under both. rank-75's attacks fall 604 → 180 at 2p and **592/923/389 → 0** at 3p/4p/6p. Only the deliberately unregulated seat of CB-EV-0031 rises, and it never wins | | 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 7807651..62e5c76 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 @@ -45,9 +45,10 @@ rather than a claim. SHARED GROUND, 200 games per cell, all 200 ran, and | 4p | 0 | 0 | 0 | 4 | 0 | 1600 | 800 | **5** | | 6p | 0 | 0 | 0 | 4 | 0 | 2400 | 1200 | **5** | -**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. +**H1's mechanism does reach the unregulated seat.** DARVO arms **exactly +1 per seat per game** — 600/(200×3), 800/(200×4), 1200/(200×6) — and 0.91 +at 2p. The first correction said **2** per seat per game, which is wrong +by a factor of two and was caught by round 2 (#3). **Criterion 3's verdict is corrected.** The original said reactive *"wins nothing, at any seat count"*. **False.** At 2p it wins **66 against @@ -95,9 +96,11 @@ and a final-Stress signature of `[5, 5, 4, 4, 4, 4]`. **Withdrawn on two independent grounds, both found by the review.** -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. +1. **The constant was the broken policy** — but a constant remains, and + saying otherwise was wrong (round 2, #12). With the one-arm-override + policy `darvo` is **exactly `seats × games`** at 3p and above. The + constant moved from 400 to `n·games`; it did not dissolve, and whatever + forces exactly one arm per seat per game is still unexplained. 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. @@ -132,13 +135,20 @@ CB-WP-0038 asserted it because the code shares `resolve_attack`; nothing tested it, and the withdrawn §3 ran straight through this path. `h1b_soothes_the_darvo_stage_attack_too` now covers it, mutation-verified. -**Round-5 pressure did not reach the score — a real defect, not a -reporting one.** `end_round_events` scored from `self` while H1-A's -pressure went into `work`, and `score` reads Stress for the GR-E03 and -GR-E04 tiebreaks. The final round's pressure was invisible to **the two -modes CB-EV-0030 reports on**. Fixed, and the test uses the case that -actually bites: uniform pressure preserves an ordering, so it takes the -**clamp** at 5 to collapse a gap and change who wins. +**Round-5 pressure did not reach the score.** `end_round_events` scored +from `self` while H1-A's pressure went into `work`, and `score` reads +Stress for the GR-E03 and GR-E04 tiebreaks. Fixed, and the test uses the +case that actually bites: uniform pressure preserves an ordering, so it +takes the **clamp** at 5 to collapse a gap and change who wins. + +**Its impact was claimed and never measured, and the measurement is +zero** (round 2, #5). This file said the pressure was invisible to *"the +two modes CB-EV-0030 reports on"* without checking. The reviewer ran +~72,000 games across all three modes with a divergence detector and found +**no divergence at all**; `attack-value`'s output is byte-identical with +the fix and without it. **The defect is real in principle and its only +witness is a hand-constructed board.** It is worth fixing and it changed +nothing that has been reported. **Inert arms, and a second wrong-subject error caught on the way.** A DARVO arm at the End of Round 5 can never advance a stage — `GameEnded` @@ -156,8 +166,15 @@ follows immediately — and ground-game's criterion 1 is about DARVO every cell**, because it tested `g.rounds >= 5` — a property of the *game*, not of the *event*. Every arm in every completed game was marked inert, which briefly looked like "criterion 1 is not met after all". An -arm is inert when no `RoundEnded` follows it; that figure matches the -reviewer's independent 29-of-363. +arm is inert when no `RoundEnded` follows it. + +**29-of-363 was independently confirmed twice**, and the citation for it +was wrong the first time (round 2, #6): this file credited +`CB-REV-0001`, which does not contain the figure. Round 1's reviewer *did* +report it — 29 of 363 at 2p, 8% — but only in its message, and it was +never transcribed into the review file. **The record is now corrected +there.** Round 2 re-derived it independently, by a different definition +(no later DARVO event names that player), and agrees in every cell. **Criterion 1 stands as met**: 92% of arms at 2p and all of them above are live. diff --git a/games/ground/examples/regulation.rs b/games/ground/examples/regulation.rs index fceecfb..59ce515 100644 --- a/games/ground/examples/regulation.rs +++ b/games/ground/examples/regulation.rs @@ -20,9 +20,12 @@ use games_ground::{Action, GroundCommand, GroundState, ScoringMode, Variant}; /// Greedy, with GROUND demoted below ATTACK. Nothing else differs. /// -/// **The copy is deliberate.** `GreedyPolicy::rank` is private, and the -/// point is to change one line of it while holding the rest identical — -/// calling into it would make that impossible to demonstrate. +/// **It delegates.** An earlier version re-typed an abridged copy of +/// greedy's ranking and called changing one line of the copy "one +/// preference"; it differed in five, and the seat burned its Freedom +/// token in round one of every game. This comment used to argue for the +/// copy, which gave a future maintainer written cover to restore it +/// (CB-REV-0002 #11). struct Reactive; impl Policy for Reactive { @@ -72,6 +75,10 @@ impl Policy for Reactive { /// inlined so the peak metric cannot silently disagree with setup. const START_STRESS: u8 = 2; +/// Games per cell. Named once so the assertion and the banner cannot +/// disagree — the banner said "200 games per cell" over 196-game columns. +const GAMES: u32 = 200; + struct Cell { games: u32, won: u32, @@ -99,7 +106,7 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce setup_fails: 0, inert_arms: 0, }; - for seed in 0..200u64 { + for seed in 0..GAMES as u64 { let Ok(mut st) = GroundState::setup( &Setup { players, @@ -174,20 +181,31 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce } if let games_ground::GroundEvent::StressSet { player, stress } = e { held.insert(*player, *stress); - c.peak_stress = c.peak_stress.max(*stress); } + // Read from `held`, which is the point. The first correction + // BUILT `held`, then took the max over event payloads anyway + // and silenced the unused binding with `let _ = held;` — so + // the comment described code that did not exist (CB-REV-0002 + // #10). The published figures were right only because the + // START_STRESS floor made the two agree. + c.peak_stress = c + .peak_stress + .max(held.values().copied().max().unwrap_or(START_STRESS)); } - let _ = held; } - // Reported as a REFUSAL, not left for a reader to notice a short - // column. CB-REV-0001 #11: `games != 200` was printed, never asserted. + // **`games`, not `games + setup_fails`** (CB-REV-0002 #1). + // + // The first version asserted the SUM — and a refused setup increments + // `setup_fails` while skipping `games`, so the sum is invariant under + // exactly the failure it claimed to catch. It could only ever fire on + // a `play` error, the path that was already instrumented. Verified by + // injecting setup failures: green, exit 0, and a full table printed + // over 196-game columns under a banner reading "200 games per cell". assert_eq!( - c.games + c.setup_fails, - 200, - "{players}p {variant:?}: {} games ran and {} setups failed — the cell is short, \ - so every number in it is over a sample nobody chose", - c.games, - c.setup_fails + c.games, GAMES, + "{players}p {variant:?}: only {} of {GAMES} games ran ({} setups refused) — \ + the cell is short, so every number in it is over a sample nobody chose", + c.games, c.setup_fails ); if c.setup_fails > 0 { eprintln!( @@ -202,7 +220,7 @@ fn main() { println!("CB-WP-0039 — does H1 reach a seat that does not regulate?\n"); println!("`reactive` is greedy with ONE preference changed: GROUND is"); println!("demoted below ATTACK, so the seat never grounds to shed Stress."); - println!("SHARED GROUND; `won` is group success; 200 games per cell.\n"); + println!("SHARED GROUND; `won` is group success; {GAMES} games per cell.\n"); for (vlabel, variant) in [ ("BASELINE ground-darvo-r0", Variant::Baseline), diff --git a/games/ground/src/lib.rs b/games/ground/src/lib.rs index 818de67..0ec93fd 100644 --- a/games/ground/src/lib.rs +++ b/games/ground/src/lib.rs @@ -2409,12 +2409,21 @@ mod tests { panic!("round 5 did not end the game"); }; assert!(outcome.group_success, "the fixture must qualify"); + + // **The oracle names the expected winners** (CB-REV-0002 #4). + // The first version asserted only that the set CHANGED, so + // reversing GR-E03's tiebreak — making HIGHER Stress win — + // left it green: the set still changed, just wrongly. let pre = s.score(); - assert_ne!( - outcome.winners, pre.winners, - "the outcome ignores the final round's pressure — scored from `self`. \ - pre={:?} post={:?}", - pre.winners, outcome.winners + assert_eq!( + pre.winners, + vec![seats[1]], + "before the pressure, the seat at Stress 4 wins on the tiebreak" + ); + assert_eq!( + outcome.winners, + vec![seats[0], seats[1]], + "after it both are clamped to 5, so they share" ); } diff --git a/gates.toml b/gates.toml index a806b36..fc1784b 100644 --- a/gates.toml +++ b/gates.toml @@ -163,3 +163,17 @@ caught = [ "CB-WP-0013: forced SH-3's re-justification and then its retirement. `make gate-review` had reported SH-3 as a standing breach for seven passes with zero actions taken, which is this registry's own ritual test (ADR-0006 D4); asking what it had ever caught is what exposed that the metric could not read above ~0% in the window it was gated on (ADR-0008 D1)", ] retire_if = "it has retired, tightened, or forced the re-justification of nothing by its review date — then it is a ritual, and ADR-0006 D4 says rituals cash out or go" + +[[gate]] +id = "CB-REV-0002/7" +name = "variant panels" +target = "panels" +checks = "the H1 measurement harnesses actually run, and a short cell fails rather than printing a number a reader must notice" +notes = """ +Registered because the second adversarial review asked what the harness +would report if the work silently stopped, and the answer was "green, and +nothing else": `attack-value` and `regulation` were wired into no target, +so every figure in CB-EV-0030 and CB-EV-0031 came from a manual run of an +ungated binary -- including the assertion added to catch short cells, +which was unreachable from `make`. +""" diff --git a/reviews/CB-REV-0001-h1.md b/reviews/CB-REV-0001-h1.md index 123e05f..b9e2c61 100644 --- a/reviews/CB-REV-0001-h1.md +++ b/reviews/CB-REV-0001-h1.md @@ -97,7 +97,10 @@ Stress is ever added. ATTACK is the sole inbound pressure. - **11 — `regulation.rs` still skips setup failures silently.** Conceded and **fixed**: counted, and a short cell now fails an assertion rather than printing a number a reader must notice. -- **13 — round-5 arms, and scoring from `self`.** Both **fixed**, and the +- **13 — round-5 arms, and scoring from `self`.** *(Round 1's reviewer + reported the arm figure as **29 of 363 at 2p, 8%**, under its + `StrictReactive`. That number was omitted when this file was written and + later cited to it — see CB-REV-0002 #6. Recorded here now.)* Both **fixed**, and the second was a real defect rather than a reporting one: `score` reads Stress for the GR-E03/GR-E04 tiebreaks, so the final round's pressure was invisible to the two modes CB-EV-0030 reports on. Inert arms are now diff --git a/reviews/CB-REV-0002-h1.md b/reviews/CB-REV-0002-h1.md new file mode 100644 index 0000000..98aec51 --- /dev/null +++ b/reviews/CB-REV-0002-h1.md @@ -0,0 +1,120 @@ +# CB-REV-0002 — re-review of the H1 corrections + +Round 2 of the tier-L review. A **fresh** agent, not the round-1 reviewer, +because a re-review by the same reviewer inherits its own sampling. Run +2026-08-08 against the corrections made after +[`CB-REV-0001`](CB-REV-0001-h1.md). + +> **Verdict: not approvable.** Three FATAL, five SERIOUS. +> +> **The substance of round 1's corrections held. The claims about them did +> not**, and two of the defects were introduced *by* the corrections. + +The reviewer was briefed on the specific risk — **the author of the +corrections is the author of the errors** — and re-derived on five +independent seed windows. + +--- + +## What it could not break, after trying + +Stated first, because it is what round 1's corrections were for. + +- **`Reactive` is genuinely one arm different.** The reviewer built a + `PureDelegate` with the same loop shape and *no* override, hashed every + command of every game, and got **identical output to `GreedyPolicy` in + 8/8 cells over 500 seeds**, both variants. Round 1's worst finding is + properly closed. *Residual: nothing asserts it; it is true by + construction, not by test.* +- **All five replacement controls are non-inert**, re-verified by + mutation, and the SOLVE control is red on nine alternate seeds. +- **`h1b_soothes_the_darvo_stage_attack_too` really covers that path** — + disabling H1-B *only* for the DARVO extra Attack reddens that test alone. +- **The inert-arm metric is right**, agreeing exactly with an independent + definition in every cell of every window. +- **Every number in CB-EV-0031 §1 reproduces** and is stable across five + windows. +- **`score()` from `work` is correct per the delta and changes no baseline + path.** + +## The three fatal ones + +### 1. The fix for round 1's #11 did not fix it + +The assertion was `games + setup_fails == 200`. **A refused setup +increments `setup_fails` and skips `games`, so the sum is invariant under +exactly the failure it claimed to catch.** Injecting setup failures gave +exit 0 and a full table over 196-game columns, under a banner reading +"200 games per cell". + +**Conceded.** Now `assert_eq!(c.games, GAMES)`, verified to exit 101 under +injected failures, with `GAMES` named once so the banner and the assertion +cannot disagree. + +### 2. The correction to the selective-column FATAL was itself selective + +CB-EV-0030's correction quoted *"81–1000 per cell … baseline **and** H1"* +and, twelve lines away, *"31–1000"*. **Both came from the baseline +rank-75 row.** Under H1, rank-75 arms are `2p 59, 3p 0, 4p 0, 6p 0`. + +**Conceded, and it is the same error twice.** The remedy for choosing a +favourable column chose a favourable column. **Every cell is now printed** +rather than summarised, because two attempts at a range produced three +wrong numbers — and the corrected verdict is the opposite of the one it +replaced: under rank-75, **H1 reduces DARVO arms to zero** at 3p and above. + +### 3. "DARVO arms 2 per seat per game" is off by a factor of two + +It is **1 per seat per game**, exactly, at every band: 600/(200×3), +800/(200×4), 1200/(200×6). Deterministic across five windows. + +**Conceded.** A headline number, wrong, in a corrected section. + +## The serious ones + +| # | challenge | outcome | +|---|---|---| +| 4 | the tiebreak test's oracle was `assert_ne!` — "the set changed", not "changed correctly". Reversing GR-E03's tiebreak left it green | **conceded, fixed.** The oracle now names the expected winners before and after; the reviewer's M7 goes red | +| 5 | the #13 "real defect" was claimed to affect the reported modes and never measured | **conceded.** ~72,000 games across three modes: **zero divergences**, `attack-value` byte-identical. Real in principle, and its only witness is a constructed board. Now stated | +| 6 | "matches the reviewer's independent 29-of-363" cites a file that does not contain it | **conceded, with a correction to the correction.** Round 1's reviewer *did* report 29 of 363 — in its message, never transcribed into the review file. So the figure is real and the **record** was not. Recorded in CB-REV-0001 now | +| 7 | `regulation.rs` and `attack-value.rs` are run by **no gate** | **conceded, and it is the sharpest finding.** Every number in both evidence files came from a manual run of an ungated binary — including the assertion added for #1, unreachable from `make`. Now `make panels`, registered in `gates.toml` | +| 8 | `edition-check`'s sibling handling was self-certifying: it read the file list *out of PROVENANCE*, crashed instead of failing on an absent file, and "coverage" counted Markdown lines | **conceded — a defect introduced by the correction.** Siblings are now discovered on disk; an absent recorded file FAILs; undocumented siblings FAIL. **It found a real gap on its first run**: `Rules_Text.csv` was vendored with no digest | + +## The minor ones + +- **9 — the CSV falsifier ran over YAML.** Fixed; it now filters to `.csv`. +- **10 — "peak Stress held" was dead code.** `held` was built, never read, + and `let _ = held;` kept clippy quiet; the metric was still max over + payloads plus a floor. **The published numbers were right by + coincidence.** Now actually read from `held`, and the figures are + unchanged — which is how it stayed hidden. +- **11 — `Reactive`'s doc comment still argued for the copy** that round 1 + ruled fatal, giving cover to restore it. Rewritten. +- **12 — §3's withdrawal ground was wrong.** *"There is no constant to + explain"* — there is: `darvo == seats × games`, exactly. The constant + moved from 400 to `n·games` rather than dissolving, and **what forces + exactly one arm per seat per game is still unexplained.** +- **13 — assorted**, including that the delta's DARVO clause is + conditional (*"if they share the same path"*) where the test's comment + says "outright". + +## What the reviewer could not check + +Digest freshness against upstream (`edition-check` reports +`upstream not checked out` when the sibling repo is absent — it did not +have it); several untouched modules; ground-game's design note; felt-play. + +--- + +## The pattern, now visible across two rounds + +Round 1 found five fatal errors of the form *correct computation, wrong +subject*. **Round 2 found three more, in the corrections to them** — an +assertion invariant under its own target, a selective fix for a selective +report, and a rate wrong by a factor of two. + +> **A correction is new work and deserves the scrutiny of new work.** It +> arrives feeling like a conclusion, and that is exactly when it is least +> examined. + +**Round 3 is owed.** Same reason as round 2. diff --git a/tools/edition-check.py b/tools/edition-check.py index 75c359a..c31034b 100755 --- a/tools/edition-check.py +++ b/tools/edition-check.py @@ -59,10 +59,27 @@ def vendored_files(): # control unverified — correctly. Recorded relative to `editions/`. SIBLINGS = "../" +# Discovered ON DISK, not read out of PROVENANCE (CB-REV-0002 #8). The +# first version listed siblings by reading the digest block, which made +# three controls vacuous at once: a recorded-but-absent file could never +# be reported missing (it was in `present` by construction), an absent one +# raised FileNotFoundError from `digest` instead of failing with the +# designed message, and "the sibling packages are covered" counted lines +# in a Markdown file -- it passed with all three files deleted. +SIBLING_GLOBS = ("catalog.yaml", "experiments/*/rules_delta.yaml", "experiments/*/*.csv") + 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)) + """Sibling packages that are on disk, as `../`-relative paths.""" + import glob as _glob + + root = os.path.join(ROOT, EDITION, SIBLINGS) + out = [] + for pattern in SIBLING_GLOBS: + for hit in _glob.glob(os.path.join(root, pattern)): + rel = os.path.relpath(hit, os.path.join(ROOT, EDITION)) + out.append(rel.replace(os.sep, "/")) + return sorted(out) def check(): @@ -72,7 +89,7 @@ def check(): siblings = sibling_files() present = vendored_files() + siblings - undocumented = [f for f in vendored_files() if f not in want] + undocumented = [f for f in vendored_files() + siblings if f not in want] if undocumented: print(f" [FAIL] vendored with no recorded digest: {', '.join(undocumented)}") rc = 1 @@ -81,10 +98,15 @@ def check(): print(f" [FAIL] a digest is recorded for a file that is not here: {', '.join(missing)}") rc = 1 - for name in present: + for name in [f for f in present if f.endswith(".csv")]: if name not in want: continue - have = digest(os.path.join(ROOT, EDITION, name)) + path = os.path.join(ROOT, EDITION, name) + if not os.path.exists(path): + print(f" [FAIL] {name} is recorded but not on disk") + rc = 1 + continue + have = digest(path) # `../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") @@ -96,7 +118,7 @@ def check(): # ADR-0015 D3's falsifier, checked rather than asserted: the hand # reader handles commas inside quotes and NOTHING ELSE. A doubled # quote or an embedded newline means `csv` is the answer after all. - for name in present: + for name in [f for f in present if f.endswith(".csv")]: raw = open(os.path.join(ROOT, EDITION, name), encoding="utf-8-sig").read() if '""' in raw: print(f" [FAIL] {name} contains a doubled quote — ADR-0011's revisit") @@ -116,7 +138,7 @@ def check(): print(" [----] upstream not checked out — freshness UNVERIFIED") print(f" expected {UPSTREAM_DIR}") return rc - for name in present: + for name in [f for f in present if f.endswith(".csv")]: up = os.path.join(UPSTREAM_DIR, name) if not os.path.exists(up): print(f" [FAIL] {name} is not in upstream — where did it come from?") @@ -151,9 +173,9 @@ def self_test(): chk("digests match the real files", 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") + all(f in want for f in sibling_files()) and len(sibling_files()) >= 3, + "catalog.yaml and rules_delta.yaml decide WHAT WE MEASURED; the first " + "version counted lines in PROVENANCE and passed with the files deleted") # The control that matters: a changed byte must be detected. import tempfile