From 5aa9ba1ba510d8ba8d4b2bceb093d8079f28b433 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 30 Aug 2026 21:25:06 +0700 Subject: [PATCH 1/2] fix(tri): the meta-gate over the ledgers had gone stale by my own addition `tri ledgers audit` plants a false entry in each ledger and demands the gate fail. Its list of ledgers was hardcoded at four. Two passes ago I ADDED a ledger -- `docs/reports/orphan_modules.json`, the orphan ceilings -- and did not add it to the audit. A guard written as a list, gone stale by addition, and this time the addition was mine. Adding it needed the audit to learn two shapes: * a gate that is a `tri` subcommand rather than a python script, and * a plant that leaves the file VALID. Appending a line to a JSON ledger makes the gate fail because the file no longer PARSES -- a catch for the wrong reason, which is a control reporting success without measuring anything. The planted entry is a ceiling for a crate the workspace does not declare: valid JSON, false claim. Planting that exposed a second live defect. `mods orphan --gate` iterates the crates and looks up each one's ceiling, so a ceiling for a crate that does NOT exist was never visited and sat in the ledger unmentioned. The ledger's own rule is "exact match, not an upper bound"; a ghost entry is slack in the other direction, and slack is where the next one hides. The gate now fails on it. Historical control -- revert only the gate fix, keep the ledger in the audit: MISSED docs/reports/orphan_modules.json <- a stale entry here exits 0 stale entry MISSED 1 exit 1 and with the fix, `caught ... by tri mods orphan --gate`, exit 0. SELF-CORRECTION, and the bigger half. I opened this saying the audit covered four of seven ledgers. Counting from DISK says fifteen: nine `tools/*baseline*.txt` and six `docs/reports/*.json`. My seven was a sample taken from memory while chasing something else -- this repository's own lesson about counts, applied to me. So the audit no longer holds a list of what exists. It enumerates ledger-shaped files from the tree: ledger-shaped files on disk 15 planted into 5, excused 2, unclassified 8 The two excused carry the measurement that excuses them (the corpus ratchet is too slow to plant into before a commit; a sha1-keyed ledger cannot be given a planted entry that is false by construction). The eight are named as a work list, not a verdict -- a meta-gate that prints only what it covers reads as coverage. An enumeration read from the tree cannot go stale by addition, which is the defect this meta-gate exists to catch, in the meta-gate. cargo test -p tri 391 passed, 0 failed (was 389; two new) cargo clippy 120 warnings, same as master Refs #2864 --- cli/tri/src/ledgers.rs | 303 ++++++++++++++++-- cli/tri/src/modreach.rs | 16 + ...e-ledgers-had-gone-stale-by-my-own-addi.md | 10 + 3 files changed, 298 insertions(+), 31 deletions(-) create mode 100644 docs/now/2026-08-30-the-meta-gate-over-the-ledgers-had-gone-stale-by-my-own-addi.md diff --git a/cli/tri/src/ledgers.rs b/cli/tri/src/ledgers.rs index 47fffbd09..0430900e3 100644 --- a/cli/tri/src/ledgers.rs +++ b/cli/tri/src/ledgers.rs @@ -43,30 +43,112 @@ pub enum LedgersCmd { /// is. struct Ledger { path: &'static str, - gate: &'static str, - stale_line: &'static str, + gate: Gate, + plant: Plant, +} + +/// What runs the ledger's claims. +enum Gate { + /// A python checker, by path. + Script(&'static str), + /// A `tri` subcommand. `docs/reports/orphan_modules.json` is gated by + /// `tri mods orphan --gate`, not by a script, and a meta-gate that only + /// knows about scripts is a meta-gate with a shape for a population. + Tri(&'static [&'static str]), +} + +/// How to make one entry FALSE without making the file unreadable. +enum Plant { + /// Append a line. `{spec}` becomes a spec that passes today. + Line(&'static str), + /// Add a ceiling for a crate the workspace does not declare. + /// + /// Appending a line to a JSON ledger would make the gate fail because the + /// file no longer PARSES -- a catch for the wrong reason, which is a + /// control that reports success without measuring anything. The planted + /// entry has to stay valid and be false. + GhostCeiling, } const LEDGERS: &[Ledger] = &[ Ledger { path: "tools/specs_generate_baseline.txt", - gate: "tools/check_specs_generate.py", - stale_line: "{spec} | planted by `tri ledgers audit`", + gate: Gate::Script("tools/check_specs_generate.py"), + plant: Plant::Line("{spec} | planted by `tri ledgers audit`"), }, Ledger { path: "tools/seal_baseline.txt", - gate: "tools/check_seal_coverage.py", - stale_line: "PlantedByAudit.json | dangling | {spec}", + gate: Gate::Script("tools/check_seal_coverage.py"), + plant: Plant::Line("PlantedByAudit.json | dangling | {spec}"), }, Ledger { path: "tools/conflict_markers_baseline.txt", - gate: "tools/check_conflict_markers.py", - stale_line: "{spec} | planted by `tri ledgers audit`", + gate: Gate::Script("tools/check_conflict_markers.py"), + plant: Plant::Line("{spec} | planted by `tri ledgers audit`"), }, Ledger { path: "tools/verilog_width_baseline.txt", - gate: "tools/check_verilog_widths.py", - stale_line: "{spec} | planted by `tri ledgers audit`", + gate: Gate::Script("tools/check_verilog_widths.py"), + plant: Plant::Line("{spec} | planted by `tri ledgers audit`"), + }, + Ledger { + path: "docs/reports/orphan_modules.json", + gate: Gate::Tri(&["mods", "orphan", "--gate"]), + plant: Plant::GhostCeiling, + }, +]; + +/// Every file in this repository shaped like a ledger, found by walking rather +/// than written down. +/// +/// The hand-written list said four and I extended it to five. Counting from +/// DISK says fifteen: nine `tools/*baseline*.txt` and six +/// `docs/reports/*.json`. The four-of-seven I started from was a sample taken +/// while chasing something else -- this repository's own lesson about counts, +/// applied to me. +/// +/// Enumerating from disk is the only version that cannot go stale by addition, +/// which is the defect this meta-gate exists to catch, in the meta-gate. +fn ledger_shaped(root: &Path) -> Vec { + let mut out = Vec::new(); + for (dir, want_baseline) in [("tools", true), ("docs/reports", false)] { + let Ok(rd) = std::fs::read_dir(root.join(dir)) else { + continue; + }; + for e in rd.flatten() { + let name = e.file_name().to_string_lossy().to_string(); + let keep = if want_baseline { + name.contains("baseline") && name.ends_with(".txt") + } else { + name.ends_with(".json") + }; + if keep { + out.push(format!("{dir}/{name}")); + } + } + } + out.sort(); + out +} + +/// A ledger this audit does NOT plant into, and the measurement behind it. +struct Unaudited { + path: &'static str, + why: &'static str, +} + +const UNAUDITED: [Unaudited; 2] = [ + Unaudited { + path: "docs/reports/suite_expectations.json", + why: "its gate is the corpus ratchet, which compiles every spec in the corpus. \ + Planting into it costs minutes per run and would make this audit too slow \ + to run before a commit -- which is when a meta-gate has to be cheap.", + }, + Unaudited { + path: "tools/withdrawn_live_baseline.txt", + why: "entries are keyed by the sha1 of the line they excuse, so a planted line \ + would need a hash that matches text elsewhere in the tree. A planted entry \ + that cannot be false by construction proves nothing.", }, ]; @@ -91,15 +173,56 @@ fn a_passing_spec(root: &Path, t27c: &Path) -> Option { .map(|s| s.to_string()) } -fn run_gate(root: &Path, gate: &str) -> Option { - let out = std::process::Command::new("python3") - .arg(gate) - .current_dir(root) - .output() - .ok()?; +fn run_gate(root: &Path, gate: &Gate) -> Option { + let out = match gate { + Gate::Script(path) => std::process::Command::new("python3") + .arg(path) + .current_dir(root) + .output() + .ok()?, + Gate::Tri(args) => std::process::Command::new(std::env::current_exe().ok()?) + .args(*args) + .current_dir(root) + .output() + .ok()?, + }; Some(out.status.success()) } +/// A name for the gate, for messages and for asking whether two are the same. +fn gate_name(gate: &Gate) -> String { + match gate { + Gate::Script(p) => (*p).to_string(), + Gate::Tri(args) => format!("tri {}", args.join(" ")), + } +} + +/// Does the gate's file exist? A `tri` subcommand is this binary, always here. +fn gate_present(root: &Path, gate: &Gate) -> bool { + match gate { + Gate::Script(p) => root.join(p).is_file(), + Gate::Tri(_) => true, + } +} + +/// The planted text: false by construction, and still readable by the gate. +fn plant_text(before: &str, plant: &Plant, spec: &str) -> Option { + match plant { + Plant::Line(t) => Some(format!("{before}{}\n", t.replace("{spec}", spec))), + Plant::GhostCeiling => { + // Textual, so the file's formatting survives: insert one key into + // the `ceilings` object rather than re-serialising the document. + let at = before.find("\"ceilings\"")?; + let brace = before[at..].find('{')? + at + 1; + Some(format!( + "{}\n \"cli/planted-by-ledgers-audit\": 0,{}", + &before[..brace], + &before[brace..] + )) + } + } +} + pub fn run(cmd: &LedgersCmd, root: PathBuf) -> Result<()> { let LedgersCmd::Audit = cmd; let t27c = ["target/release/t27c", "target/debug/t27c"] @@ -139,13 +262,12 @@ pub fn run(cmd: &LedgersCmd, root: PathBuf) -> Result<()> { let (mut caught, mut missed, mut skipped) = (0usize, 0usize, 0usize); for l in LEDGERS { let path = root.join(l.path); - let gate = root.join(l.gate); - if !path.is_file() || !gate.is_file() { + if !path.is_file() || !gate_present(&root, &l.gate) { println!(" SKIP {} (ledger or gate absent)", l.path); skipped += 1; continue; } - let Some(clean) = run_gate(&root, l.gate) else { + let Some(clean) = run_gate(&root, &l.gate) else { println!(" SKIP {} (gate did not run)", l.path); skipped += 1; continue; @@ -162,20 +284,30 @@ pub fn run(cmd: &LedgersCmd, root: PathBuf) -> Result<()> { skipped += 1; continue; }; - let planted = format!("{}{}\n", before, l.stale_line.replace("{spec}", &spec)); + let Some(planted) = plant_text(&before, &l.plant, &spec) else { + println!(" SKIP {} (nothing to plant into)", l.path); + skipped += 1; + continue; + }; if std::fs::write(&path, &planted).is_err() { skipped += 1; continue; } - let verdict = run_gate(&root, l.gate); + let verdict = run_gate(&root, &l.gate); let _ = std::fs::write(&path, &before); match verdict { Some(false) => { - println!(" caught {}", l.path); + // Name the gate too: "caught" without it says a stale entry + // fails SOMETHING, and which one is the next reader's question. + println!(" caught {:<38} by {}", l.path, gate_name(&l.gate)); caught += 1; } _ => { - println!(" MISSED {} <- a stale entry here exits 0", l.path); + println!( + " MISSED {:<38} by {} <- a stale entry here exits 0", + l.path, + gate_name(&l.gate) + ); missed += 1; } } @@ -197,7 +329,43 @@ pub fn run(cmd: &LedgersCmd, root: PathBuf) -> Result<()> { "{missed} ledger(s) do not catch a stale entry" )); } - println!(" Every ledger fails when one of its entries stops being true."); + println!(" Every ledger this audit plants into fails when an entry stops being true."); + println!(); + + let shaped = ledger_shaped(&root); + let known: Vec<&str> = LEDGERS + .iter() + .map(|l| l.path) + .chain(UNAUDITED.iter().map(|u| u.path)) + .collect(); + let loose: Vec<&String> = shaped + .iter() + .filter(|f| !known.contains(&f.as_str())) + .collect(); + + println!( + " ledger-shaped files on disk {} planted into {}, excused {}, unclassified {}", + shaped.len(), + LEDGERS.len(), + UNAUDITED.len(), + loose.len() + ); + println!(); + for u in &UNAUDITED { + println!(" excused {}", u.path); + for chunk in u.why.split_whitespace().collect::>().chunks(11) { + println!(" {}", chunk.join(" ")); + } + } + if !loose.is_empty() { + println!(); + println!(" NOT YET CLASSIFIED -- neither planted into nor measured and"); + println!(" excused. A work list, not a verdict: a meta-gate that prints"); + println!(" only what it covers reads as coverage."); + for f in &loose { + println!(" {f}"); + } + } Ok(()) } @@ -205,23 +373,78 @@ pub fn run(cmd: &LedgersCmd, root: PathBuf) -> Result<()> { mod tests { use super::*; - // The planted line must name the spec, or the audit tests nothing. + /// A ledger is planted into or measured and excused, never both. + /// + /// The same rule the census audit keeps: an exclusion is a measurement, so + /// it carries the reading that produced it. "too hard" is not one. #[test] - fn every_template_carries_the_spec_placeholder() { + fn no_ledger_is_both_planted_into_and_excused() { + for u in &UNAUDITED { + assert!( + !LEDGERS.iter().any(|l| l.path == u.path), + "{} is excused and also planted into", + u.path + ); + assert!( + u.why.len() > 60, + "{}: an exclusion is a measurement, not a shrug -- {:?}", + u.path, + u.why + ); + } + } + + /// The enumeration reads the tree, so it cannot go stale by addition. + /// + /// The hand-written list said four. Disk says fifteen. If this ever finds + /// fewer than the files this repository is known to carry, the walk is + /// broken and every "unclassified 0" it prints would be a silence. + #[test] + fn the_enumeration_reads_the_tree() { + let root = std::process::Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| PathBuf::from(String::from_utf8_lossy(&o.stdout).trim().to_string())); + let Some(root) = root else { + return; // not in a checkout; nothing to read + }; + let found = ledger_shaped(&root); + assert!( + found.len() >= 10, + "the walk found {} ledger-shaped files, which is fewer than this \ + repository carries -- a broken walk prints `unclassified 0`", + found.len() + ); for l in LEDGERS { assert!( - l.stale_line.contains("{spec}"), - "template names no spec: {}", + found.iter().any(|f| f == l.path), + "{} is planted into but the walk does not see it", l.path ); } } + // The planted line must name the spec, or the audit tests nothing. + #[test] + fn every_template_carries_the_spec_placeholder() { + for l in LEDGERS { + if let Plant::Line(t) = &l.plant { + assert!(t.contains("{spec}"), "template names no spec: {}", l.path); + } + } + } + #[test] fn every_ledger_has_a_distinct_gate() { let mut seen = std::collections::BTreeSet::new(); for l in LEDGERS { - assert!(seen.insert(l.gate), "two ledgers share a gate: {}", l.gate); + let name = gate_name(&l.gate); + assert!( + seen.insert(name.clone()), + "two ledgers share a gate: {name}" + ); } } @@ -230,8 +453,26 @@ mod tests { #[test] fn substitution_keeps_the_path() { for l in LEDGERS { - let line = l.stale_line.replace("{spec}", "specs/x/y.t27"); - assert!(line.contains("specs/x/y.t27"), "{}", l.path); + match &l.plant { + Plant::Line(_) => { + let planted = plant_text("", &l.plant, "specs/x/y.t27").expect("planted"); + assert!(planted.contains("specs/x/y.t27"), "{}", l.path); + } + // A JSON ledger is falsified by an entry, not by a spec name. + // The planted text must still PARSE -- appending junk makes the + // gate fail because the file is unreadable, which is a catch + // for the wrong reason. + Plant::GhostCeiling => { + let before = "{\n \"ceilings\": {\n \"a\": 1\n }\n}\n"; + let planted = plant_text(before, &l.plant, "unused").expect("planted"); + assert!(planted.contains("planted-by-ledgers-audit"), "{}", l.path); + assert!( + serde_json::from_str::(&planted).is_ok(), + "{}: the planted ledger must still parse", + l.path + ); + } + } } } } diff --git a/cli/tri/src/modreach.rs b/cli/tri/src/modreach.rs index ce4d48bc3..6fc593976 100644 --- a/cli/tri/src/modreach.rs +++ b/cli/tri/src/modreach.rs @@ -384,6 +384,22 @@ pub fn run(gate: bool) -> Result<()> { println!(" {:<52} {:>5} lines{}", p.display(), lines, t); } } + // A ceiling for a crate the workspace does not declare is never visited by + // the loop above, so it sat in the ledger unmentioned. The ledger's own rule + // is "exact match, not an upper bound"; a ghost entry is slack in the other + // direction, and slack is where the next one hides. + if let Some(cmap) = &ceil { + for named in cmap.keys() { + if !crates.iter().any(|c| c == named) { + breaches.push(format!( + "{named}: has a ceiling but is not a workspace member. A ledger entry for \ + a crate that does not exist is watched by nothing and hides nothing -- \ + remove it, or add the crate to Cargo.toml." + )); + } + } + } + println!(); println!( " {total_orphans} of {total_files} files, carrying {total_tests} test(s) that do not exist \ diff --git a/docs/now/2026-08-30-the-meta-gate-over-the-ledgers-had-gone-stale-by-my-own-addi.md b/docs/now/2026-08-30-the-meta-gate-over-the-ledgers-had-gone-stale-by-my-own-addi.md new file mode 100644 index 000000000..4fcaec908 --- /dev/null +++ b/docs/now/2026-08-30-the-meta-gate-over-the-ledgers-had-gone-stale-by-my-own-addi.md @@ -0,0 +1,10 @@ +# NOW -- The meta-gate over the ledgers had gone stale by my own addition (2026-08-30) + +## The meta-gate over the ledgers had gone stale by my own addition (Refs #2864) + +- `tri ledgers audit` plants a false entry in each ledger and demands the gate fail. Its list of ledgers was hardcoded at four. Two passes ago I ADDED a ledger -- docs/reports/orphan_modules.json, the orphan ceilings -- and did not add it to the audit. The guard written as a list, gone stale by addition, and this time the addition was mine. +- Adding it needed the audit to learn two shapes: a gate that is a tri subcommand rather than a python script, and a plant that keeps the file VALID. Appending a line to a JSON ledger makes the gate fail because the file no longer parses -- a catch for the wrong reason, which is a control reporting success without measuring. +- Planting a ghost ceiling exposed a second live defect: `mods orphan --gate` iterates crates and looks up their ceilings, so a ceiling for a crate that does not exist was never visited. The ledger's own rule is exact match, not an upper bound; a ghost entry is slack in the other direction. The gate now fails on it. +- Historical control: revert only the gate fix and the meta-gate reports MISSED docs/reports/orphan_modules.json, exit 1. With it, caught. +- Self-correction: I opened this saying the audit covered 4 of 7 ledgers. Counting from disk says FIFTEEN -- nine tools/*baseline*.txt and six docs/reports/*.json. My seven was a sample taken from memory while chasing something else, which is this repository's own lesson about counts, applied to me. +- So the audit now enumerates ledger-shaped files from DISK rather than from a list: 15 on disk, 5 planted into, 2 measured and excused, 8 named as not yet classified. An enumeration read from the tree cannot go stale by addition, which is the defect this meta-gate exists to catch, in the meta-gate. From 4551e3d2a27c4c99f5b121fa5966130ce1e6fdc5 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 30 Aug 2026 21:26:01 +0700 Subject: [PATCH 2/2] skill(ci-gates) 384-386: my own addition staled the list; a catch for the wrong reason; a remembered count is a sample (Refs #2864) --- .claude/skills/ci-gates/SKILL.md | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/.claude/skills/ci-gates/SKILL.md b/.claude/skills/ci-gates/SKILL.md index c08e04fa0..efbc7e2c4 100644 --- a/.claude/skills/ci-gates/SKILL.md +++ b/.claude/skills/ci-gates/SKILL.md @@ -9624,3 +9624,61 @@ lists, and refuses a reason shorter than sixty characters. `"too hard"` fails. The distinction being preserved is the one this whole document keeps circling: a reader must be able to tell **"looked and could not"** from **"never looked"**, and a blank space says the second while meaning the first. + +## 384. The list went stale by MY addition, into the gate that watches lists + +`tri ledgers audit` exists to catch a ledger whose entries have stopped being +true. Its own list of ledgers was hardcoded at four. Two passes earlier I added +a fifth ledger -- `docs/reports/orphan_modules.json` -- and did not add it here. + +§377 already says a guard written as a list goes stale by addition. This is that +sentence coming back with my name on it, inside the meta-gate whose whole subject +is stale lists. + +**Whenever you ADD a ledger, a ceiling, a baseline or an allowlist, the same +commit adds it to whatever audits that kind of thing.** If you cannot name the +audit, that is the finding. + +The repair is not a fifth entry. It is enumerating from the tree: + +``` +ledger-shaped files on disk 15 planted into 5, excused 2, unclassified 8 +``` + +**An enumeration read from disk cannot go stale by addition.** A list written +down always can, and writing it down is what feels like being thorough. + +## 385. A catch for the wrong reason is not a catch + +Adding a JSON ledger to an audit that plants a line into text files looked like +one line of code. Appending a line to JSON makes the file unparseable, so the +gate goes red -- and the audit records `caught`. + +It caught nothing. The gate failed on a **syntax error**, not on the stale +entry, and the audit would have gone on reporting that ledger as protected while +its actual staleness check was never exercised. + +The planted falsehood has to be **valid and false**: for a ceiling ledger, a +ceiling naming a crate the workspace does not declare. Which promptly found a +second defect -- the gate iterated crates and looked up their ceilings, so a +ceiling for a crate that does not exist was never visited at all. + +**When a control reports a catch, ask which of the two possible reasons the +subject failed for.** If the planted input breaks the reader rather than the +claim, every downstream "caught" is a green light with nothing behind it -- the +same family as a gate that prints FAILED and exits 0, one level in. + +## 386. The count you carry in your head is a sample + +I opened that pass writing "the audit covers four of seven ledgers". Seven was +from memory, assembled while working on something else. Counting the tree: +**fifteen** -- nine `tools/*baseline*.txt` and six `docs/reports/*.json`. + +§362 says the same thing about a note written while chasing another problem, and +it was my note being corrected then too. The habit that fails is not the +counting; it is *not re-counting* when the number becomes load-bearing. + +**Before a number decides scope -- what to cover, what to exclude, what to call +done -- re-derive it from the tree in the same breath you use it.** It costs one +command. Here it changed "we cover most of them" into "we cover a third", and +the honest print of that ratio is worth more than the five rows above it.