Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-build-thing/src/buildgate.rs

//! **THE BUILD GATE** — compile everything a repo *declares*, and go RED with the
//! numbers when the count that came out is smaller than the count that went in.
//!
//! # Why this exists, and why it is here and not in a repo
//!
//! Three repos in this constellation have no CI at all, and the two commands a human
//! actually types are both silently partial. MEASURED 2026-08-05:
//!
//! * korp `b388500` — `cargo build --bins` builds **4 of 9** `[[bin]]` targets. The
//!   other five carry `required-features` outside `default`, and cargo skips them
//!   **without a word** and exits 0: `korp-cli`, `korp-server`, `korp-solo`,
//!   `korp-spark41`, `start-korp-demo`.
//! * znippy-zoomies `49c7bc6` — `cargo build --bins` at the repo root builds **0 of
//!   11**, because the root manifest is a package *and* a workspace with no
//!   `default-members`, so cargo selects the root package, which declares no bins.
//! * korp — of 122 `tests/*.rs`, 42 carry a file-level `#![cfg(…)]`. Under a feature
//!   set that gates one away the file compiles to an **empty** binary and libtest
//!   prints `running 0 tests … ok`, which is shaped exactly like a pass.
//!
//! **Both of those exit 0.** That is the mechanism behind korp-server not compiling
//! for hours, behind the thin client's 93 `#[test]` of which 0 ran and 5 were red, and
//! behind two reds landing unseen on 2026-08-04.
//!
//! It lives in `nornir-build-thing` because that is the only non-test crate korp,
//! facett and znippy-zoomies all already reach, and because the declaration
//! ([`BuildVariantSet`]) and the cargo-JSON artifact stream already live here. A copy
//! per repo would be the twin this module exists to delete (LAW 5).
//!
//! # The two principles it inherits from the shell script it replaces
//!
//! **1. The argv is never re-spelled.** Every invocation starts from the atom's own
//! [`BuildVariant::cargo_args`], the same materialisation the build path uses, so an
//! atom whose declared features change is gated at its *new* features with no edit
//! here. A hand-written argv is exactly how the declaration went stale before.
//!
//! **2. It judges OUTPUT, never the exit code.** A malformed cargo invocation fails in
//! a way whose empty output is shaped exactly like a clean result, and a `tail` in the
//! pipeline eats the status entirely. So: nothing is piped through anything, each run's
//! status is captured into its own field before anything else touches it, and a zero
//! exit that produced **no artifact** is its own named failure — not a pass.
//!
//! # What it adds over the script: the numbers
//!
//! A gate that only says "everything went fine" is precisely the disease. Both
//! assertions here are *counts with names attached*:
//!
//! * [`BinTally`] — bins declared vs bins an artifact actually came out for, and the
//!   name of every one that did not, so the failure text is a work list.
//! * [`TestTally`] — test binaries produced vs test binaries that contain at least one
//!   test, and the source file behind every empty one.
//!
//! # Cost
//!
//! Real, and worth stating plainly: one atom is one whole feature resolution. korp
//! declares six, in the `release` profile. Give it a warm target dir and narrow with
//! [`GateSpec::atoms`] while iterating. [`Arms::coverage_only`] is the seconds-long
//! half that compiles nothing.

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use anyhow::{Context, Result};

use crate::variant::{BuildVariant, BuildVariantSet};

// ───────────────────────────── what one cargo run tells us ─────────────────────────────

/// One executable cargo reported producing, with the source target behind it.
///
/// `src` is what makes the empty-test assertion nameable: libtest can only say "0
/// tests", it cannot say *which file* compiled to nothing. The `compiler-artifact`
/// message can, because it carries `target.src_path`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Produced {
    /// The binary cargo wrote.
    pub path: PathBuf,
    /// The cargo target name (`korp-server`, `live_map_coverage`, …).
    pub name: String,
    /// The cargo target kinds (`bin`, `test`, `lib`, …).
    pub kinds: Vec<String>,
    /// The file that target is rooted at — `src/main.rs`, `tests/foo.rs`.
    pub src: PathBuf,
    /// Whether cargo built this with the test harness linked in.
    pub is_test: bool,
}

impl Produced {
    /// A `[[bin]]` target proper — not a test binary that happens to come from a bin.
    pub fn is_bin(&self) -> bool {
        !self.is_test && self.kinds.iter().any(|k| k == "bin")
    }
    /// A `tests/*.rs` integration target (kind `test`), as opposed to a bin's own
    /// `mod tests` (kind `bin`, `is_test = true`).
    pub fn is_integration_test(&self) -> bool {
        self.kinds.iter().any(|k| k == "test")
    }
}

/// One cargo invocation, recorded whole so a verdict is something somebody can go and
/// read. Nothing here is judged — [`judge_run`] does that, separately, so the recording
/// and the judgement cannot quietly become the same step.
#[derive(Debug, Clone)]
pub struct CargoRun {
    /// Human label — `korp-server`, `korp-server/tests`.
    pub label: String,
    /// The argv actually passed, for reproduction verbatim.
    pub argv: Vec<String>,
    /// **Captured before anything else touches it.** `None` means the process was
    /// killed by a signal, which is not a success and is not a failure code either.
    pub status: Option<i32>,
    /// True iff the process exited 0. Kept separate from `status` so a signal death
    /// can never be read as a pass.
    pub exited_ok: bool,
    /// Everything cargo said it produced.
    pub produced: Vec<Produced>,
    /// Compiler diagnostics at `error` level, verbatim.
    pub errors: Vec<String>,
    /// The tail of cargo's own stderr, kept for the failure text.
    pub stderr_tail: String,
}

impl CargoRun {
    /// The command, reproducible by copy-paste.
    pub fn command_line(&self) -> String {
        format!("cargo {}", self.argv.join(" "))
    }
}

/// Run one cargo invocation and record it. **This function judges nothing** — it
/// returns what happened, and the caller decides what that means.
///
/// `--message-format=json` is appended rather than the argv being rewritten, so the
/// atom's own `cargo_args()` passes through untouched.
pub fn run_cargo(
    repo_root: &Path,
    label: &str,
    argv: &[String],
    target_dir: Option<&Path>,
    extra_env: &[(String, String)],
) -> Result<CargoRun> {
    use std::io::{BufReader, Read};

    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
    let mut cmd = Command::new(&cargo);
    cmd.current_dir(repo_root)
        .args(argv)
        .arg("--message-format=json")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    if let Some(t) = target_dir {
        cmd.env("CARGO_TARGET_DIR", t);
    }
    for (k, v) in extra_env {
        cmd.env(k, v);
    }

    // **Say what is starting, before it starts.** The whole report is rendered at the
    // end, which means a run killed in the middle of a multi-hour compile leaves NO
    // evidence of how far it got — measured on korp 2026-08-05, where a six-atom
    // release run died after 10 minutes and left a zero-byte log. The shell script
    // this replaced kept a per-arm log for exactly this reason, and losing that was a
    // regression. One line per invocation, on stderr so it never mixes into the report
    // on stdout.
    eprintln!("[buildgate] {label} :: cargo {}", argv.join(" "));

    let mut child = cmd
        .spawn()
        .with_context(|| format!("spawn `{cargo} {}` for {label}", argv.join(" ")))?;

    // stderr is drained on its own thread. A piped stderr that is only read *after*
    // `wait()` deadlocks the moment cargo fills the pipe buffer — and a gate that
    // hangs is worse than one that lies, because nobody even gets a verdict.
    let mut err_pipe = child.stderr.take().expect("piped stderr");
    let err_thread = std::thread::spawn(move || {
        let mut s = String::new();
        let _ = err_pipe.read_to_string(&mut s);
        s
    });

    let reader = BufReader::new(child.stdout.take().expect("piped stdout"));
    let mut produced = Vec::new();
    let mut errors = Vec::new();
    for msg in cargo_metadata::Message::parse_stream(reader) {
        let msg = match msg {
            Ok(m) => m,
            // A malformed line is not a reason to abandon the run: the artifacts
            // already collected are still evidence, and the status still decides.
            Err(_) => continue,
        };
        match msg {
            cargo_metadata::Message::CompilerArtifact(a) => {
                if let Some(exe) = a.executable {
                    produced.push(Produced {
                        path: exe.into_std_path_buf(),
                        name: a.target.name.clone(),
                        kinds: a.target.kind.iter().map(|k| k.to_string()).collect(),
                        src: a.target.src_path.clone().into_std_path_buf(),
                        is_test: a.profile.test,
                    });
                }
            }
            cargo_metadata::Message::CompilerMessage(m) => {
                if m.message.level == cargo_metadata::diagnostic::DiagnosticLevel::Error {
                    errors.push(
                        m.message.rendered.clone().unwrap_or_else(|| m.message.message.clone()),
                    );
                }
            }
            _ => {}
        }
    }

    // The status is captured into its own binding FIRST. Everything downstream reads
    // this binding, never `$?`-shaped state that a later call could overwrite.
    let status = child.wait().with_context(|| format!("wait for cargo ({label})"))?;
    let exited_ok = status.success();
    let code = status.code();
    let stderr = err_thread.join().unwrap_or_default();
    let stderr_tail = tail_lines(&stderr, 40);

    // …and say how it ended, with the count. A progress line that only ever appears
    // before the work is a progress bar that cannot report failure.
    eprintln!(
        "[buildgate] {label} :: {} — {} artifact(s), {} compiler error(s)",
        if exited_ok { "exit 0" } else { "FAILED" },
        produced.len(),
        errors.len(),
    );

    Ok(CargoRun {
        label: label.to_string(),
        argv: argv.to_vec(),
        status: code,
        exited_ok,
        produced,
        errors,
        stderr_tail,
    })
}

/// The last `n` lines, whole. Deliberately a function and not a `tail` pipe: piping
/// through `tail` replaces the pipeline's exit status with `tail`'s, which is how a
/// failure became an exit 0 seven times in one day on this box.
fn tail_lines(s: &str, n: usize) -> String {
    let lines: Vec<&str> = s.lines().collect();
    let start = lines.len().saturating_sub(n);
    lines[start..].join("\n")
}

/// **The judgement, and the reason it is separate from the recording.**
///
/// Two distinct failure modes, named apart, because a status code cannot tell them
/// apart and the second one is the whole subject of this module:
///
/// * cargo said no — the compiler's own words are attached.
/// * **cargo said yes and produced nothing.** A `--bin` that matched no target, a
///   `required-features` skip, a `--tests` under a feature set that gates every file
///   away: all of them exit 0 with empty output, which is shaped exactly like a clean
///   pass. `expect_artifacts` is what makes that a red.
pub fn judge_run(run: &CargoRun, expect_artifacts: bool) -> Option<String> {
    if !run.exited_ok {
        let code = match run.status {
            Some(c) => format!("exit {c}"),
            None => "killed by a signal".to_string(),
        };
        let words = if run.errors.is_empty() {
            run.stderr_tail.clone()
        } else {
            run.errors.join("\n")
        };
        return Some(format!(
            "── {} DID NOT COMPILE ({code})\n   {}\n{}",
            run.label,
            run.command_line(),
            indent(&words),
        ));
    }
    if expect_artifacts && run.produced.is_empty() {
        return Some(format!(
            "── {} exited 0 but cargo produced NO artifact — this invocation compiled \
             nothing, and its output is shaped exactly like a clean pass.\n   {}",
            run.label,
            run.command_line(),
        ));
    }
    None
}

fn indent(s: &str) -> String {
    s.lines().map(|l| format!("   | {l}")).collect::<Vec<_>>().join("\n")
}

// ───────────────────────────── assertion 1: declared bins vs built bins ─────────────────────────────

/// **How many bins were declared, how many an artifact actually came out for, and the
/// name of every one that did not.**
///
/// The comparison is against real cargo `[[bin]]` targets
/// ([`BuildVariantSet::cargo_bin_targets`]), not against the variant list — a bin that
/// no atom builds is exactly the hole, so counting only the atoms would be a gate that
/// reads its own input.
#[derive(Debug, Clone, Default)]
pub struct BinTally {
    /// Every `[[bin]]` (and implicit `src/main.rs` bin) in the repo.
    pub declared: Vec<String>,
    /// The ones cargo actually emitted an executable for, across every atom run.
    pub built: BTreeSet<String>,
    /// Declared, not built, and carrying a written reason in
    /// `[build.allow_unbuilt_bins]` — name → reason.
    pub excused: BTreeMap<String, String>,
    /// Declared, not built, no reason. **This is the failure.**
    pub unbuilt: Vec<String>,
    /// Entries in the allowlist for bins that ARE built now — a stale excuse rots the
    /// list into a blanket, so it is its own failure.
    pub stale_excuses: Vec<String>,
    /// **This run walked only SOME atoms**, so `unbuilt` necessarily lists every bin
    /// belonging to the atoms that were skipped. Counting that as a failure would make
    /// `--atom korp-cli` permanently red for a reason the operator already knows —
    /// and a gate that is red no matter what is a gate people learn to ignore. The
    /// tally is still reported in full; it just cannot assert.
    ///
    /// MEASURED: `--atom korp-cli` on korp `b388500` reported "8 of 10 UNBUILT", all
    /// eight belonging to the five atoms the filter excluded.
    pub partial: bool,
}

impl BinTally {
    /// **How many DECLARED bins were built** — `built` ∩ `declared`, and not
    /// `built.len()`.
    ///
    /// `built` is whatever the delivery produced an artifact for, and a delivery
    /// produces things that are not bins: an image reference, a mastered `.iso`, a
    /// staged payload. `bin_name_of_artifact` is a file stem, so
    /// `.iso-out/gunnar-appliance-iso.iso` arrives in this set as
    /// `gunnar-appliance-iso` and is counted against a denominator it is not in.
    ///
    /// SEEN RED on gunnar, 2026-08-05, in both directions on one afternoon:
    ///   `bins: 0 of 4 declared built (75%)`   — nothing built, three excused
    ///   `bins: 2 of 4 declared built (125%)`  — one bin + one ISO, three excused
    /// A percentage over 100 is the visible half; `0 … (75%)` is the same defect
    /// quietly, and it had been printing for as long as the gate has existed.
    pub fn built_declared(&self) -> usize {
        self.declared
            .iter()
            .filter(|b| self.built.contains(*b))
            .count()
    }

    /// Built over declared, as a ratio in [0, 1]. A count alone hides the denominator,
    /// and the denominator is the point.
    ///
    /// Excused bins count toward it — an excuse with a written reason is a decision,
    /// not a hole — and the numerator is [`built_declared`](Self::built_declared), so
    /// the ratio cannot exceed 1 and cannot be inflated by an artifact that is not a
    /// bin.
    pub fn ratio(&self) -> f64 {
        if self.declared.is_empty() {
            return 0.0;
        }
        (self.built_declared() + self.excused.len()) as f64 / self.declared.len() as f64
    }

    /// **The headline, with every term the percentage is made of.**
    ///
    /// The old wording printed `built_declared` beside a percentage computed from
    /// `built_declared + excused`, so the number next to the ratio was NOT the
    /// ratio's numerator, and a third number (`unbuilt`) arrived with nothing to
    /// subtract it from. MEASURED on gunnar, 2026-09-20:
    ///
    /// ```text
    /// bins: 1 of 15 declared built (40%) — PARTIAL RUN … so the 9 not built here …
    /// ```
    ///
    /// Every number there is correct — 1 built, 5 excused, 9 unbuilt, and
    /// (1+5)/15 = 40% — and the sentence still reads as arithmetic that does not
    /// work, because two of the three terms are missing. A reader who checks
    /// `1/15 ≠ 40%` and `15 - 1 ≠ 9` concludes the gate is broken and stops
    /// reading it, which is the one thing a gate must never teach.
    ///
    /// This is the second half of the defect the `built_declared` doc records
    /// (`0 of 4 … (75%)`, `2 of 4 … (125%)`). That half fixed the numerator the
    /// ratio USES; this half prints it.
    pub fn headline(&self) -> String {
        let accounted = self.built_declared() + self.excused.len();
        if self.partial {
            return format!(
                "bins: {} declared — {} built, {} excused, {} not built here \
                 ({}/{} = {:.0}% accounted for) — PARTIAL RUN (a variant filter was \
                 given){}; run without --variant for the verdict",
                self.declared.len(),
                self.built_declared(),
                self.excused.len(),
                self.unbuilt.len(),
                accounted,
                self.declared.len(),
                self.ratio() * 100.0,
                // A filter that happened to cover everything declared has nothing
                // it cannot assert on, and saying "those 0 are NOT asserted on"
                // invents a caveat out of an empty set.
                if self.unbuilt.is_empty() {
                    String::new()
                } else {
                    format!(", so those {} are NOT asserted on", self.unbuilt.len())
                },
            );
        }
        format!(
            "bins: {} declared — {} built, {} excused with a written reason, {} UNBUILT \
             ({}/{} = {:.0}% accounted for)",
            self.declared.len(),
            self.built_declared(),
            self.excused.len(),
            self.unbuilt.len(),
            accounted,
            self.declared.len(),
            self.ratio() * 100.0,
        )
    }

    /// The hard assertion. `Err` carries the names, because a number without names is
    /// not a work list.
    pub fn verdict(&self) -> Result<(), String> {
        if self.declared.is_empty() {
            return Err(
                "the bin tally found NO declared bins at all — a gate whose denominator \
                 is zero passes vacuously, which is the shape it exists to catch"
                    .to_string(),
            );
        }
        let mut problems = Vec::new();
        // A partial run cannot assert on coverage — but it CAN still assert that every
        // atom it did walk compiled, which is `judge_run`'s job, and that the allowlist
        // has not rotted, which is below.
        if !self.unbuilt.is_empty() && !self.partial {
            problems.push(format!(
                "{} of {} declared bins were NEVER BUILT by any declared atom: {:?}\n   \
                 Either give each one a build point in .nornir/build-variants.toml, or \
                 write why not in [build.allow_unbuilt_bins] — an empty reason is not a \
                 reason.",
                self.unbuilt.len(),
                self.declared.len(),
                self.unbuilt,
            ));
        }
        if !self.stale_excuses.is_empty() {
            problems.push(format!(
                "[build.allow_unbuilt_bins] excuses bins that ARE built now: {:?} — \
                 delete the lines, or the allowlist rots into a blanket",
                self.stale_excuses,
            ));
        }
        if problems.is_empty() { Ok(()) } else { Err(problems.join("\n\n")) }
    }
}

/// **The diagnosis, computed for free: what a human's `cargo build --bins` actually
/// reaches.**
///
/// This is not the assertion — the assertion is [`BinTally`], which is about what the
/// *declared build space* covers. This is the number that explains WHY the gate is
/// needed, and it must appear in the report whether the gate passes or fails, because
/// it is the one figure nobody had. MEASURED on korp `b388500`: 4 of 9.
///
/// Read from `cargo metadata`, which is authoritative about `required-features` and
/// about the feature tables — a hand-rolled manifest walk would be a third spelling of
/// a rule that already has two.
#[derive(Debug, Clone, Default)]
pub struct NaiveReach {
    /// Every `bin` target cargo knows about, across the workspace.
    pub bins: usize,
    /// Those whose `required-features` are satisfied by their package's `default`
    /// closure — i.e. the ones a bare `cargo build --bins` would produce.
    pub reachable: usize,
    /// The rest, with the features they are short. **These are skipped silently, and
    /// cargo exits 0.**
    pub skipped: Vec<(String, Vec<String>)>,
}

impl NaiveReach {
    pub fn ratio(&self) -> f64 {
        if self.bins == 0 { 0.0 } else { self.reachable as f64 / self.bins as f64 }
    }
    pub fn headline(&self) -> String {
        format!(
            "a bare `cargo build --bins` reaches {} of {} bin targets ({:.0}%) — the other \
             {} are skipped SILENTLY and cargo exits 0",
            self.reachable,
            self.bins,
            self.ratio() * 100.0,
            self.skipped.len(),
        )
    }
}

/// Compute [`NaiveReach`] from `cargo metadata`. Cheap — no compilation.
pub fn naive_reach(repo_root: &Path) -> Result<NaiveReach> {
    let md = cargo_metadata::MetadataCommand::new()
        .current_dir(repo_root)
        .no_deps()
        .exec()
        .context("cargo metadata --no-deps")?;

    let mut out = NaiveReach::default();
    for pkg in &md.packages {
        // The transitive closure of this package's own `default`, expanding only
        // tokens that are themselves features. `dep:x` and `crate/feat` stay opaque,
        // which is correct: neither can satisfy a `required-features` entry naming a
        // plain feature.
        let mut closure: BTreeSet<String> = BTreeSet::new();
        let mut stack: Vec<String> = pkg.features.get("default").cloned().unwrap_or_default();
        while let Some(f) = stack.pop() {
            if !closure.insert(f.clone()) {
                continue;
            }
            if let Some(next) = pkg.features.get(&f) {
                stack.extend(next.iter().cloned());
            }
        }
        for t in &pkg.targets {
            if !t.kind.iter().any(|k| k.to_string() == "bin") {
                continue;
            }
            out.bins += 1;
            let missing: Vec<String> = t
                .required_features
                .iter()
                .filter(|f| !closure.contains(*f))
                .cloned()
                .collect();
            if missing.is_empty() {
                out.reachable += 1;
            } else {
                out.skipped.push((t.name.clone(), missing));
            }
        }
    }
    out.skipped.sort();
    Ok(out)
}

/// **The bin tally, over a set of names that were actually produced.**
///
/// Split out from [`run`] so the dwarves ⚒ Build Thing can reach the SAME assertion
/// from the artifacts a delivery build already produced, without a second cargo run and
/// without a second spelling of the rule (LAW 5). The Build Thing has just compiled
/// every cell; asking it to compile them again to learn what it already knows would be
/// both slow and a place for the two answers to disagree.
///
/// Returns the tally and any allowlist problems found while building it.
pub fn tally_bins(
    repo_root: &Path,
    built: BTreeSet<String>,
    partial: bool,
) -> Result<(BinTally, Vec<String>)> {
    let mut t = BinTally { built, partial, ..Default::default() };
    t.declared = BuildVariantSet::cargo_bin_targets(repo_root)
        .context("enumerate the repo's real [[bin]] targets")?;
    t.declared.sort();
    t.declared.dedup();

    let (allow, bad) = read_allowlist(repo_root, "allow_unbuilt_bins");
    let mut problems: Vec<String> =
        bad.into_iter().map(|b| format!("[build.allow_unbuilt_bins] {b}")).collect();
    for k in unreasoned(&allow) {
        problems.push(format!(
            "[build.allow_unbuilt_bins] entry `{k}` has no reason — an excuse with no \
             words is not an excuse"
        ));
    }
    for b in &t.declared {
        if t.built.contains(b) {
            continue;
        }
        match allow.get(b) {
            Some(why) => {
                t.excused.insert(b.clone(), why.clone());
            }
            None => t.unbuilt.push(b.clone()),
        }
    }
    // A stale excuse is only knowable from a FULL run: a partial one did not build the
    // atom that would have made the entry legitimate.
    t.stale_excuses = if partial {
        Vec::new()
    } else {
        allow.keys().filter(|k| t.built.contains(*k)).cloned().collect()
    };
    Ok((t, problems))
}

/// The bin name behind a produced artifact path — `…/release/korp-server` ⇒
/// `korp-server`. What the ⚒ Build Thing has in hand after a build is a list of paths,
/// and this is the one place that mapping is spelled.
pub fn bin_name_of_artifact(path: &Path) -> Option<String> {
    path.file_stem().map(|s| s.to_string_lossy().to_string())
}

// ───────────────────────────── assertion 2: test binaries that hold no test ─────────────────────────────

/// A test binary that compiled clean and contains **zero tests** — the shape that
/// prints `running 0 tests … ok` and is indistinguishable from a pass.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmptyTestBinary {
    /// The atom whose feature resolution produced it.
    pub atom: String,
    /// The cargo target name.
    pub target: String,
    /// **The source file** — what makes this reportable rather than merely counted.
    pub src: PathBuf,
    /// The binary that was interrogated.
    pub exe: PathBuf,
}

/// **How many test binaries were produced, how many hold at least one test, and the
/// source file behind every one that holds none.**
#[derive(Debug, Clone, Default)]
pub struct TestTally {
    /// Test binaries produced across every atom run — the work measure. One source
    /// file yields one binary per feature resolution that compiles it, so this is
    /// larger than [`files`](Self::files) and is NOT what the assertion is about.
    pub produced: usize,
    /// Of those binaries, the ones holding at least one `#[test]`. Reported because it
    /// is the raw observation; a correctly feature-gated file makes this legitimately
    /// less than `produced`, which is exactly why the verdict is not built on it.
    pub with_tests: usize,
    /// **Distinct test SOURCE FILES seen** — the denominator the assertion uses.
    pub files: usize,
    /// Files that at least one declared resolution put a test in.
    pub files_with_tests: usize,
    /// Total tests found, summed. A sanity denominator: zero here over a non-zero
    /// `produced` means every single binary was empty.
    pub tests_found: usize,
    /// Every empty one, named. **This is the failure.**
    pub empty: Vec<EmptyTestBinary>,
    /// Source files excused in `[build.allow_dark_test_files]` — file name → reason.
    pub excused: BTreeMap<String, String>,
    /// Binaries that could not be interrogated at all (would not run, timed out).
    /// Not a pass: an uninterrogable binary proves nothing.
    pub uninterrogable: Vec<(String, String)>,
    /// **Only some atoms were walked.** A file this run found empty may well be
    /// reached by an atom the filter excluded, so "dark" is not assertable here. Same
    /// reasoning as [`BinTally::partial`].
    pub partial: bool,
}

impl TestTally {
    /// **Files reached over files seen.** Deliberately not `with_tests / produced`: a
    /// correctly `#![cfg(feature = "x")]`-gated file compiles to an empty binary under
    /// every atom lacking `x`, so the per-binary ratio is legitimately below 1 in a
    /// healthy repo and cannot carry an assertion. The per-FILE ratio can: it is 1
    /// exactly when every test file is reached by some declared build point.
    pub fn ratio(&self) -> f64 {
        if self.files == 0 {
            return 0.0;
        }
        self.files_with_tests as f64 / self.files as f64
    }

    pub fn headline(&self) -> String {
        if self.partial {
            return format!(
                "test files: {} of {} reached ({:.0}%) — PARTIAL RUN (a variant filter was \
                 given), so the {} found empty here are NOT asserted on; a skipped variant \
                 may well reach them",
                self.files_with_tests,
                self.files,
                self.ratio() * 100.0,
                self.empty.len(),
            );
        }
        format!(
            "test files: {} of {} reached by some declared atom ({:.0}%), {} DARK, \
             {} excused | binaries: {} produced, {} held tests, {} tests found, \
             {} uninterrogable",
            self.files_with_tests,
            self.files,
            self.ratio() * 100.0,
            self.empty.len(),
            self.excused.len(),
            self.produced,
            self.with_tests,
            self.tests_found,
            self.uninterrogable.len(),
        )
    }

    /// The hard assertion — one entry per dark file, each naming the file.
    pub fn verdict(&self) -> Result<(), String> {
        if self.files == 0 {
            return Err(
                "the test tally interrogated NO test binaries at all — a gate over an \
                 empty set passes vacuously, which is the shape it exists to catch"
                    .to_string(),
            );
        }
        let mut problems = Vec::new();
        if !self.empty.is_empty() && !self.partial {
            let lines: Vec<String> = self
                .empty
                .iter()
                .map(|e| {
                    format!("     {} :: {} ({})", e.atom, display_rel(&e.src), e.target)
                })
                .collect();
            problems.push(format!(
                "{} of {} test files compiled to ZERO tests under EVERY declared atom — \
                 each prints `running 0 tests … ok`, which is shaped exactly like a \
                 pass:\n{}\n   \
                 Either put the file behind an atom whose features actually reach it, or \
                 write why not in [build.allow_dark_test_files].",
                self.empty.len(),
                self.files,
                lines.join("\n"),
            ));
        }
        if !self.uninterrogable.is_empty() {
            problems.push(format!(
                "{} test binaries could not be asked what they contain — an \
                 uninterrogable binary is not a green: {:?}",
                self.uninterrogable.len(),
                self.uninterrogable,
            ));
        }
        if problems.is_empty() { Ok(()) } else { Err(problems.join("\n\n")) }
    }
}

fn display_rel(p: &Path) -> String {
    // Keep the last three components — enough to identify `crate/tests/file.rs`
    // without pasting an absolute path into every failure message.
    let comps: Vec<_> = p.components().collect();
    let start = comps.len().saturating_sub(3);
    comps[start..].iter().map(|c| c.as_os_str().to_string_lossy()).collect::<Vec<_>>().join("/")
}

/// Ask a libtest binary what it contains, **without running anything**.
///
/// `--list` is libtest's own enumeration: it parses argv, prints one `name: test` line
/// per test and exits, never entering a test body. That is what makes this affordable
/// and what makes it safe to point at a GUI atom's binary — no test runs, so no window
/// opens. `DISPLAY`/`WAYLAND_DISPLAY` are cleared anyway, belt and braces.
///
/// Returns the number of tests, including `#[ignore]`d ones: an ignored test is still a
/// test that *compiled*, and this assertion is about a file that compiled to nothing.
pub fn list_tests(exe: &Path) -> Result<usize> {
    let out = Command::new(exe)
        .arg("--list")
        .arg("--format=terse")
        .env_remove("DISPLAY")
        .env_remove("WAYLAND_DISPLAY")
        .stdin(Stdio::null())
        .output()
        .with_context(|| format!("run `{} --list`", exe.display()))?;
    if !out.status.success() {
        anyhow::bail!(
            "`{} --list` exited {:?}: {}",
            exe.display(),
            out.status.code(),
            tail_lines(&String::from_utf8_lossy(&out.stderr), 5),
        );
    }
    let stdout = String::from_utf8_lossy(&out.stdout);
    // libtest prints `path::to::test: test` (and `: benchmark`). The trailing summary
    // line `N tests, M benchmarks` is deliberately NOT parsed: it is absent when the
    // count is zero in some libtest versions, and a parser that reads a line which may
    // not exist is a gate that cannot fail. Counting the entries themselves always
    // works, and zero entries is unambiguous.
    Ok(stdout
        .lines()
        .filter(|l| l.ends_with(": test") || l.ends_with(": benchmark"))
        .count())
}

// ───────────────────────────── the allowlists ─────────────────────────────

/// Read one of the `[build.allow_*]` excuse tables out of `.nornir/build-variants.toml`.
///
/// [`BuildVariantSet`] models `[build.allow_unbuilt]` (the feature axis) but not the two
/// target-level tables, so they are read here — **with the reason string, because the
/// reason IS the mechanism.** An entry with no words is not an excuse; see
/// [`unreasoned`].
///
/// A missing file or a missing table is an empty map, never an error: a repo with no
/// declaration is gated on its whole workspace instead (see [`GateSpec`]).
///
/// **A key naming a file MUST be quoted.** `dark.rs = "…"` is a TOML *dotted key* — it
/// declares a table `dark` with a member `rs`, not a key `dark.rs`. An unquoted file
/// name therefore silently excuses nothing while looking exactly like an excuse that
/// works. This was found by the red probe, which is the only reason it is not still in
/// here: a non-string value is now surfaced as [`malformed`] rather than coerced to an
/// empty reason.
pub fn allowlist(repo_root: &Path, table: &str) -> BTreeMap<String, String> {
    read_allowlist(repo_root, table).0
}

/// The allowlist plus the entries whose value is not a string — the quoting trap above.
/// Returned separately so a caller can make it a hard failure rather than a shrug.
///
/// ⚠ **THIS USED TO BE A SECOND PARSER.** Until 2026-08-26 it re-opened
/// `.nornir/build-variants.toml` as raw `toml::Value` and read the table by name,
/// bypassing [`BuildVariantSet`] completely — which is the only reason
/// `[build.allow_unbuilt_bins]` worked at all: the struct had no such field, so
/// three repos (edda, gunnar, korp — 8 entries) had their declaration honoured by
/// a door the schema knew nothing about. It also meant the schema could not be
/// closed: `deny_unknown_fields` on `BuildVariantSet` would have failed those
/// three files on a key this crate itself was reading.
///
/// The field exists now, so this reads the STRUCT. The raw pass is GONE, not
/// bridged — see [`AllowEntry`](crate::variant::AllowEntry) for why it was never
/// needed: TOML has not destroyed the dotted-key fact by the time serde runs, it
/// is destroyed by choosing a value type that cannot hold it. `AllowEntry` holds
/// it, so the struct reports the same diagnostic the raw pass did.
pub fn read_allowlist(
    repo_root: &Path,
    table: &str,
) -> (BTreeMap<String, String>, Vec<String>) {
    match BuildVariantSet::discover(repo_root) {
        Ok(Some(set)) => set.allow_table(table),
        // No file, or a file that does not parse. Both were empty here before and
        // stay empty: a repo with no build space has nothing to allow, and a
        // malformed file is a failure the LOADER reports, loudly, to every other
        // caller — this function inventing a second complaint about it would put
        // the same problem on the operator's screen twice, in two spellings.
        _ => (BTreeMap::new(), Vec::new()),
    }
}

/// **The malformed entries alone** — the quoting trap, as a list of failures.
pub fn malformed(repo_root: &Path, table: &str) -> Vec<String> {
    read_allowlist(repo_root, table).1
}

/// Entries whose reason is empty or a shrug. Ten characters is the bar the korp gate
/// already set; keeping one bar means one rule to argue about.
pub fn unreasoned(allow: &BTreeMap<String, String>) -> Vec<String> {
    allow
        .iter()
        .filter(|(_, why)| why.trim().len() < 10)
        .map(|(k, _)| k.clone())
        .collect()
}

// ───────────────────────────── the gate itself ─────────────────────────────

/// Which arms to run. The two halves have wildly different costs and the cheap one must
/// stay usable on its own, or it does not get run — which is where the original defect
/// came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Arms {
    /// Compile each atom's bins and tally declared-vs-built. Minutes.
    pub bins: bool,
    /// Compile each atom's tests and tally empty test binaries. Minutes to hours.
    pub tests: bool,
}

impl Default for Arms {
    fn default() -> Self {
        Self { bins: true, tests: true }
    }
}

impl Arms {
    /// Both.
    pub fn all() -> Self {
        Self { bins: true, tests: true }
    }
    /// Neither — the declaration-only pass, which compiles nothing and still fails on
    /// a vacuous or unreasoned allowlist.
    pub fn coverage_only() -> Self {
        Self { bins: false, tests: false }
    }
    pub fn any(&self) -> bool {
        self.bins || self.tests
    }
}

/// What to gate.
#[derive(Debug, Clone)]
pub struct GateSpec {
    /// The repo.
    pub repo_root: PathBuf,
    /// Where to build. `None` inherits the ambient `CARGO_TARGET_DIR`.
    pub target_dir: Option<PathBuf>,
    /// Narrow to these atom names while iterating. `None` ⇒ every declared atom.
    pub atoms: Option<BTreeSet<String>>,
    /// Which arms.
    pub arms: Arms,
}

impl GateSpec {
    pub fn new(repo_root: impl Into<PathBuf>) -> Self {
        Self {
            repo_root: repo_root.into(),
            target_dir: None,
            atoms: None,
            arms: Arms::all(),
        }
    }
    pub fn target_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.target_dir = Some(dir.into());
        self
    }
    pub fn arms(mut self, arms: Arms) -> Self {
        self.arms = arms;
        self
    }
    /// Narrow to a comma-separated atom list — the shape an env var or a CLI flag
    /// arrives in. An empty or whitespace-only string means "no narrowing".
    pub fn only(mut self, csv: &str) -> Self {
        let set: BTreeSet<String> =
            csv.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
        self.atoms = if set.is_empty() { None } else { Some(set) };
        self
    }
}

/// **Which build space the gate walked**, stated out loud in the report.
///
/// A repo with a `.nornir/build-variants.toml` is gated atom by atom, at each atom's
/// own declared features. A repo without one has no declared build space at all — and
/// refusing to gate it would leave exactly the repos in the worst shape ungated, which
/// is the wrong way round. So it falls back to the whole workspace, and **says so**: a
/// silent fallback would let a declaration disappear without the report changing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateMode {
    /// `.nornir/build-variants.toml` was found; these atoms were walked.
    Declared { atoms: Vec<String> },
    /// No declaration. `cargo --workspace` stands in, and the report says so.
    Workspace { why: String },
}

impl Default for GateMode {
    fn default() -> Self {
        Self::Workspace { why: "unset".into() }
    }
}

impl GateMode {
    pub fn headline(&self) -> String {
        match self {
            Self::Declared { atoms } => {
                format!("mode: DECLARED — {} atom(s): {atoms:?}", atoms.len())
            }
            Self::Workspace { why } => format!(
                "mode: WORKSPACE fallback ({why}) — every workspace member, at its own \
                 default features. This is weaker than a declaration: it cannot reach a \
                 bin whose `required-features` sit outside `default`, which is precisely \
                 the hole. Declaring atoms is the fix."
            ),
        }
    }
}

/// The workspace stand-in argv. `--workspace` is what fixes the sharpest of the
/// measured holes: a root manifest that is a package *and* a workspace with no
/// `default-members` makes cargo select the root package alone, so a bare
/// `cargo build --bins` there builds zero of eleven bins and exits 0.
fn workspace_argv(tests: bool) -> Vec<String> {
    let mut a: Vec<String> = if tests {
        vec!["test".into(), "--workspace".into(), "--no-run".into(), "--tests".into()]
    } else {
        vec!["build".into(), "--workspace".into(), "--bins".into()]
    };
    a.push("--all-features".into());
    a
}

/// Everything the gate learned, and its verdict.
#[derive(Debug, Clone, Default)]
pub struct GateOutcome {
    /// Which build space was walked — declared atoms or the workspace stand-in.
    pub mode: GateMode,
    /// Every cargo invocation, whole.
    pub runs: Vec<CargoRun>,
    /// Assertion 1.
    pub bins: BinTally,
    /// Assertion 2.
    pub tests: TestTally,
    /// Which arms actually ran — a verdict over zero arms is not a pass.
    pub arms: Arms,
    /// Atoms walked.
    pub atoms_walked: Vec<String>,
    /// The free diagnosis: what a bare `cargo build --bins` reaches.
    pub naive: NaiveReach,
    /// Every failure, in the order found.
    pub failures: Vec<String>,
}

impl GateOutcome {
    pub fn is_green(&self) -> bool {
        self.failures.is_empty()
    }

    /// The report. **The numbers come out whether it passed or failed** — a gate that
    /// only speaks up when it is unhappy teaches nobody what it is measuring, and a
    /// gate whose denominator silently went to zero looks exactly like a happy one.
    pub fn render(&self) -> String {
        let mut s = String::new();
        s.push_str("buildgate\n");
        s.push_str(&format!("  {}\n", self.mode.headline()));
        s.push_str(&format!("  arms       : bins={} tests={}\n", self.arms.bins, self.arms.tests));
        s.push_str(&format!("  cargo runs : {}\n", self.runs.len()));
        s.push_str(&format!("  {}\n", self.naive.headline()));
        for (b, missing) in &self.naive.skipped {
            s.push_str(&format!("      silently skipped: {b} (short of {missing:?})\n"));
        }
        if self.arms.bins {
            s.push_str(&format!("  {}\n", self.bins.headline()));
            if !self.bins.unbuilt.is_empty() {
                s.push_str(&format!("      unbuilt: {:?}\n", self.bins.unbuilt));
            }
        }
        if self.arms.tests {
            s.push_str(&format!("  {}\n", self.tests.headline()));
            for e in &self.tests.empty {
                s.push_str(&format!("      EMPTY: {} :: {}\n", e.atom, display_rel(&e.src)));
            }
        }
        if self.failures.is_empty() {
            s.push_str("  VERDICT: PASS\n");
        } else {
            s.push_str(&format!("  VERDICT: FAIL — {} problem(s)\n\n", self.failures.len()));
            for f in &self.failures {
                s.push_str(f);
                s.push_str("\n\n");
            }
        }
        s
    }

    /// `Ok(())` or the whole report as an error. The report is the error on purpose:
    /// a failure that says only "gate failed" sends somebody looking for the numbers
    /// that were right there.
    pub fn verdict(&self) -> Result<()> {
        if self.is_green() {
            Ok(())
        } else {
            anyhow::bail!("{}", self.render())
        }
    }
}

/// One atom's `cargo test --no-run` argv, derived from the SAME [`BuildVariant::cargo_args`]
/// the build path materialises.
///
/// Rewriting the verb and appending `--no-run` is the whole difference, so an atom whose
/// declared features change is gated at its new features with no edit here (LAW 5). The
/// declared profile is kept: a debug build of a release atom is a different compile, and
/// the entire subject of this module is that a compile which did not happen proves
/// nothing about one that did.
pub fn test_argv(v: &BuildVariant) -> Vec<String> {
    let mut args = v.cargo_args();
    debug_assert_eq!(args.first().map(String::as_str), Some("build"));
    if args.first().map(String::as_str) == Some("build") {
        args[0] = "test".to_string();
    }
    args.push("--no-run".to_string());
    args
}

/// The same argv with `--bin X` dropped and `--tests` in its place.
///
/// **Why a second shape is structurally necessary.** `cargo test --bin X` compiles X's
/// `mod tests` and **not** `tests/*.rs` — cargo treats them as different target kinds.
/// So a gate built only on the shape above cannot compile the integration suite at all,
/// because every declared variant carries `--bin`. That is the same class of blind spot
/// one level up: a gate reading *bin unit-test compilation* while what rotted was
/// *feature-gated integration tests*.
///
/// Dropping `--bin` must drop its VALUE too — a bare bin name left behind is read by
/// cargo as a test-name filter and compiles nothing at all, silently.
pub fn integration_argv(v: &BuildVariant) -> Vec<String> {
    let argv = test_argv(v);
    let mut out: Vec<String> = Vec::with_capacity(argv.len());
    let mut skip_next = false;
    for a in argv {
        if skip_next {
            skip_next = false;
            continue;
        }
        if a == "--bin" {
            skip_next = true;
            continue;
        }
        out.push(a);
    }
    out.push("--tests".to_string());
    out
}

/// **Run the gate.**
///
/// Serial on purpose, and not because parallelism is hard: each atom is a distinct
/// feature resolution over the same target dir, and cargo already saturates the box
/// within one resolution. (Concurrency in this constellation is `gatling`; `rayon` is
/// forbidden. Neither is reached for here — there is nothing to parallelise that cargo
/// is not already parallelising.)
pub fn run(spec: &GateSpec) -> Result<GateOutcome> {
    let mut out = GateOutcome { arms: spec.arms, ..Default::default() };
    let root = spec.repo_root.as_path();

    // The free diagnosis first, so it appears in the report even when an arm below
    // fails outright. Best-effort: a repo `cargo metadata` cannot read is a problem
    // the arms will report far more loudly than this line could.
    out.naive = naive_reach(root).unwrap_or_default();

    let set = BuildVariantSet::discover(root)
        .with_context(|| format!("read {}", BuildVariantSet::conventional_path(root).display()))?;

    // ── which build space? ───────────────────────────────────────────────────
    // A declaration is walked atom by atom. No declaration falls back to the whole
    // workspace and SAYS SO — refusing to gate an undeclared repo would leave exactly
    // the repos in the worst shape ungated.
    let (set, atoms): (Option<BuildVariantSet>, Vec<BuildVariant>) = match set {
        Some(set) => {
            let atoms: Vec<BuildVariant> = set
                .variants
                .iter()
                .filter(|v| spec.atoms.as_ref().is_none_or(|only| only.contains(&v.name)))
                .cloned()
                .collect();
            out.mode = GateMode::Declared {
                atoms: atoms.iter().map(|v| v.name.clone()).collect(),
            };
            // A gate that walked nothing is not a pass. This is the `ARMS=` typo the
            // shell script guarded against; an atom filter needs the same guard.
            if atoms.is_empty() {
                out.failures.push(format!(
                    "the atom filter {:?} matched NO declared atom (declared: {:?}) — a \
                     gate that walks nothing reports success without asserting anything",
                    spec.atoms,
                    set.names(),
                ));
                return Ok(out);
            }
            (Some(set), atoms)
        }
        None => {
            out.mode = GateMode::Workspace {
                why: format!("no {}", BuildVariantSet::conventional_path(root).display()),
            };
            if spec.atoms.is_some() {
                out.failures.push(
                    "a variant filter was given but this repo declares no variants — the \
                     filter would silently select nothing"
                        .to_string(),
                );
                return Ok(out);
            }
            (None, Vec::new())
        }
    };
    out.atoms_walked = atoms.iter().map(|v| v.name.clone()).collect();

    if !spec.arms.any() {
        // Coverage-only is legitimate, but it must say so rather than render a
        // pass over two tallies it never filled in.
        out.failures.extend(coverage_only_failures(root, set.as_ref()));
        return Ok(out);
    }

    let td = spec.target_dir.as_deref();

    // ── arm 1: bins ──────────────────────────────────────────────────────────
    if spec.arms.bins {
        let runs: Vec<CargoRun> = if atoms.is_empty() {
            vec![run_cargo(root, "workspace", &workspace_argv(false), td, &[])?]
        } else {
            let mut r = Vec::new();
            for v in &atoms {
                r.push(run_cargo(root, &v.name, &v.cargo_args(), td, &cfg_env(v))?);
            }
            r
        };
        for run in runs {
            if let Some(f) = judge_run(&run, true) {
                out.failures.push(f);
            }
            for p in &run.produced {
                if p.is_bin() {
                    out.bins.built.insert(p.name.clone());
                }
            }
            out.runs.push(run);
        }
        let (tally, mut problems) =
            tally_bins(root, std::mem::take(&mut out.bins.built), spec.atoms.is_some())?;
        out.bins = tally;
        out.failures.append(&mut problems);
        if let Err(e) = out.bins.verdict() {
            out.failures.push(e);
        }
    }

    // ── arm 2: tests ─────────────────────────────────────────────────────────
    if spec.arms.tests {
        let (allow_dark, bad) = read_allowlist(root, "allow_dark_test_files");
        for b in bad {
            out.failures.push(format!("[build.allow_dark_test_files] {b}"));
        }
        for k in unreasoned(&allow_dark) {
            out.failures.push(format!(
                "[build.allow_dark_test_files] entry `{k}` has no reason — an excuse with \
                 no words is not an excuse"
            ));
        }
        out.tests.excused = allow_dark.clone();

        // The same binary is produced by several atoms whose feature resolutions
        // coincide; interrogating it twice would double both sides of the ratio.
        let mut seen: BTreeSet<PathBuf> = BTreeSet::new();

        // **Per SOURCE FILE, across every atom** — and this distinction is the whole
        // correctness of the assertion. A file gated `#![cfg(feature = "x")]` compiles
        // to an empty binary under every atom that lacks `x`, and that is not a
        // defect: it is what a feature gate is for. The defect is a file that is empty
        // under EVERY declared resolution — a file no build point reaches, which is
        // therefore never compiled and never run anywhere.
        //
        // Flagging per-atom instead would fire on every correctly-gated file in the
        // repo, and a gate that cries wolf on healthy code gets switched off, which is
        // how the repo ended up with no gate at all.
        let mut per_file: BTreeMap<PathBuf, (usize, EmptyTestBinary)> = BTreeMap::new();

        // In workspace mode there is one synthetic "atom" standing for the whole
        // workspace; the two argv shapes collapse to the one `--workspace` run.
        let plan: Vec<(String, Vec<Vec<String>>)> = if atoms.is_empty() {
            vec![("workspace".to_string(), vec![workspace_argv(true)])]
        } else {
            atoms
                .iter()
                .map(|v| (v.name.clone(), vec![test_argv(v), integration_argv(v)]))
                .collect()
        };

        for (atom_name, argvs) in &plan {
            let cfg = atoms
                .iter()
                .find(|v| &v.name == atom_name)
                .map(cfg_env)
                .unwrap_or_default();
            for argv in argvs {
                let label = if argv.iter().any(|a| a == "--tests") {
                    format!("{atom_name}/tests")
                } else {
                    atom_name.clone()
                };
                let run = run_cargo(root, &label, argv, td, &cfg)?;
                if let Some(f) = judge_run(&run, true) {
                    out.failures.push(f);
                }
                for p in &run.produced {
                    if !p.is_test || !seen.insert(p.path.clone()) {
                        continue;
                    }
                    out.tests.produced += 1;
                    match list_tests(&p.path) {
                        Ok(n) => {
                            if n > 0 {
                                out.tests.with_tests += 1;
                                out.tests.tests_found += n;
                            }
                            // The running maximum over every resolution that produced
                            // this file. One atom reaching it is enough.
                            let e = per_file.entry(p.src.clone()).or_insert_with(|| {
                                (
                                    0,
                                    EmptyTestBinary {
                                        atom: atom_name.clone(),
                                        target: p.name.clone(),
                                        src: p.src.clone(),
                                        exe: p.path.clone(),
                                    },
                                )
                            });
                            e.0 = e.0.max(n);
                        }
                        Err(e) => out
                            .tests
                            .uninterrogable
                            .push((p.name.clone(), format!("{e:#}"))),
                    }
                }
                out.runs.push(run);
            }
        }

        // A file is DARK when no declared resolution put a single test in it. Excused
        // files stay in the denominator: an excuse must not shrink the thing being
        // measured, or the ratio flatters itself as the allowlist grows.
        let mut stale_dark: Vec<String> = Vec::new();
        out.tests.partial = spec.atoms.is_some();
        out.tests.files = per_file.len();
        for (src, (max_tests, witness)) in &per_file {
            let file = src.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
            let excused = allow_dark.contains_key(&file);
            if *max_tests > 0 {
                out.tests.files_with_tests += 1;
            } else if !excused {
                out.tests.empty.push(witness.clone());
            }
            if *max_tests > 0 && excused {
                stale_dark.push(file);
            }
        }
        if out.tests.partial {
            stale_dark.clear();
        }
        if !stale_dark.is_empty() {
            out.failures.push(format!(
                "[build.allow_dark_test_files] excuses files that ARE compiled with tests \
                 now: {stale_dark:?} — delete the lines, or the allowlist rots into a \
                 blanket"
            ));
        }
        if let Err(e) = out.tests.verdict() {
            out.failures.push(e);
        }
    }

    Ok(out)
}

/// `--cfg` travels as `RUSTFLAGS`, exactly as [`BuildVariant::build`] sets it — the
/// declaration's own mechanism, not a second spelling of it.
fn cfg_env(v: &BuildVariant) -> Vec<(String, String)> {
    match &v.cfg {
        Some(cfg) => {
            let inherited = std::env::var("RUSTFLAGS").unwrap_or_default();
            vec![("RUSTFLAGS".into(), format!("{inherited} --cfg {cfg}").trim().to_string())]
        }
        None => Vec::new(),
    }
}

/// The seconds-long half: it compiles nothing and still catches a rotted allowlist and
/// an empty declaration.
fn coverage_only_failures(root: &Path, set: Option<&BuildVariantSet>) -> Vec<String> {
    let mut f = Vec::new();
    if set.is_some_and(|s| s.variants.is_empty()) {
        f.push(
            "the declaration lists no [[build.variant]] — every gate downstream would \
             walk an empty set and report success"
                .to_string(),
        );
    }
    for table in ["allow_unbuilt_bins", "allow_dark_test_files"] {
        let (allow, bad) = read_allowlist(root, table);
        for b in bad {
            f.push(format!("[build.{table}] {b}"));
        }
        for k in unreasoned(&allow) {
            f.push(format!("[build.{table}] entry `{k}` has no reason"));
        }
    }
    match BuildVariantSet::cargo_bin_targets(root) {
        Ok(bins) if bins.is_empty() => {
            f.push("the repo declares no [[bin]] at all — the bin tally would be vacuous".into())
        }
        Ok(_) => {}
        Err(e) => f.push(format!("could not enumerate [[bin]] targets: {e:#}")),
    }
    f
}

// ───────────────────────────── the one CLI the three repos share ─────────────────────────────

/// **`cargo xtask buildgate [flags]` — the whole verb.**
///
/// Lives here, not in three `xtask/src/main.rs` files, because three copies of an
/// argument parser is exactly the duplication that let three repos drift apart in the
/// first place (LAW 5). A member's xtask is then genuinely three lines: resolve its
/// root, call this, exit.
///
/// ```text
/// --variant <a,b>   narrow to these declared variants (default: all;
///                   --atom is the deprecated old spelling and still works)
/// --arms <spec>     all | bins | tests | coverage   (default: all)
/// --target-dir <d>  where to build (default: $CARGO_TARGET_DIR)
/// --list            print what WOULD be gated, compile nothing
/// ```
pub mod cli {
    use super::*;

    /// Parsed flags. Deliberately its own type so the parse is testable without
    /// running a build.
    #[derive(Debug, Clone, Default, PartialEq, Eq)]
    pub struct Args {
        pub atoms: Option<String>,
        pub arms: Option<String>,
        pub target_dir: Option<PathBuf>,
        pub list: bool,
    }

    /// Parse. An unknown flag is an ERROR, never a shrug — a mistyped `--arms` that is
    /// quietly ignored runs a different gate than the one asked for and reports the
    /// wrong thing green.
    pub fn parse(argv: &[String]) -> Result<Args> {
        let mut a = Args::default();
        let mut i = 0;
        while i < argv.len() {
            let need = |i: usize, flag: &str| -> Result<String> {
                argv.get(i + 1)
                    .cloned()
                    .ok_or_else(|| anyhow::anyhow!("{flag} needs a value"))
            };
            match argv[i].as_str() {
                "--variant" | "--variants" => {
                    a.atoms = Some(need(i, "--variant")?);
                    i += 2;
                }
                // Deprecated spellings of `--variant`/`--variants`. NEVER an error:
                // the written laws cite them and muscle memory is not to be broken.
                "--atom" | "--atoms" => {
                    eprintln!(
                        "note: `--atom` is the deprecated spelling of `--variant`; both work"
                    );
                    a.atoms = Some(need(i, "--atom")?);
                    i += 2;
                }
                "--arms" => {
                    a.arms = Some(need(i, "--arms")?);
                    i += 2;
                }
                "--target-dir" => {
                    a.target_dir = Some(PathBuf::from(need(i, "--target-dir")?));
                    i += 2;
                }
                "--list" => {
                    a.list = true;
                    i += 1;
                }
                other => anyhow::bail!(
                    "buildgate: unknown flag `{other}` — known: --variant, --arms, \
                     --target-dir, --list (--atom is the deprecated spelling of \
                     --variant)"
                ),
            }
        }
        Ok(a)
    }

    /// `--arms` → [`Arms`]. An unrecognised spec is an error for the same reason.
    pub fn arms_of(spec: Option<&str>) -> Result<Arms> {
        Ok(match spec.unwrap_or("all") {
            "all" => Arms::all(),
            "bins" => Arms { bins: true, tests: false },
            "tests" => Arms { bins: false, tests: true },
            "coverage" => Arms::coverage_only(),
            other => anyhow::bail!(
                "buildgate: unknown --arms `{other}` — known: all, bins, tests, coverage"
            ),
        })
    }

    /// Build the spec an xtask would run.
    pub fn spec(repo_root: &Path, a: &Args) -> Result<GateSpec> {
        let mut s = GateSpec::new(repo_root).arms(arms_of(a.arms.as_deref())?);
        if let Some(csv) = &a.atoms {
            s = s.only(csv);
        }
        if let Some(t) = &a.target_dir {
            s = s.target_dir(t.clone());
        }
        Ok(s)
    }

    /// **The verb.** Returns a process exit code: 0 green, 1 red, 2 could not run.
    ///
    /// The report is printed on BOTH paths. A gate that prints only on failure teaches
    /// nobody its denominator, and a denominator that quietly went to zero is the
    /// failure it exists to catch.
    pub fn run(repo_root: &Path, argv: &[String]) -> i32 {
        let a = match parse(argv) {
            Ok(a) => a,
            Err(e) => {
                eprintln!("{e:#}");
                return 2;
            }
        };
        let spec = match spec(repo_root, &a) {
            Ok(s) => s,
            Err(e) => {
                eprintln!("{e:#}");
                return 2;
            }
        };
        if a.list {
            // `--list` compiles nothing, so it must not be able to print a verdict —
            // saying "built" or "passed" there would be a small lie of exactly the
            // kind this whole pass is about.
            match super::naive_reach(repo_root) {
                Ok(n) => println!("{}", n.headline()),
                Err(e) => {
                    eprintln!("cargo metadata: {e:#}");
                    return 2;
                }
            }
            match BuildVariantSet::discover(repo_root) {
                Ok(Some(set)) => println!("would gate variants: {:?}", set.names()),
                Ok(None) => println!("would gate: the whole workspace (no declaration)"),
                Err(e) => {
                    eprintln!("read the declaration: {e:#}");
                    return 2;
                }
            }
            return 0;
        }
        match super::run(&spec) {
            Ok(o) => {
                print!("{}", o.render());
                i32::from(!o.is_green())
            }
            Err(e) => {
                eprintln!("buildgate could not run: {e:#}");
                2
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// ★ **edda's own bin tally is complete — every bin the widened oracle now
    /// sees is either built by a cell or carries a written excuse.**
    ///
    /// The follow-through on `20863a7`. Teaching `cargo_bin_targets` cargo's real
    /// auto-discovery rule made two edda bins visible that had never been counted:
    /// `nornir-bench-redprobe` and `fleet_mock_server`, both test-harness
    /// subprocesses. Visible is not the same as handled — an unbuilt bin with no
    /// reason is precisely the hole this gate exists to name — so both are excused
    /// in `.nornir/build-variants.toml` with the reason spelled out, and this
    /// asserts the excuse is READ rather than merely written.
    ///
    /// Asserts the excuses are LOAD-BEARING, not decorative: the two names must
    /// land in `excused` (proving the allowlist parsed and each entry has a
    /// reason — an unreasoned entry is its own reported failure), and `unbuilt`
    /// must be empty. A repo that adds a bin and neither builds nor excuses it
    /// reds here, which is the whole point.
    #[test]
    fn eddas_own_bins_are_all_built_or_excused_in_writing() {
        let edda = Path::new(env!("CARGO_MANIFEST_DIR"))
            .ancestors()
            .nth(2)
            .expect("crates/<crate> is two deep in edda")
            .to_path_buf();
        assert!(
            edda.join(".nornir").join("build-variants.toml").is_file(),
            "no edda declaration at {}",
            edda.display()
        );
        let declared = crate::variant::BuildVariantSet::cargo_bin_targets(&edda)
            .expect("enumerate edda's bin targets");
        // What a full delivery run produces: the bins edda's declared cargo rows name.
        // It used to PRETEND every bin except the two fixtures was produced — and that
        // pretence is exactly what hid `bootx64`, `edda-build-thing` and
        // `estate-clock` (no row builds any of them) until ⚒ Build's gate counted them
        // on loki, 2026-10-16. The rows are the producer; `stale_excuses` stays honest
        // because an excuse for a bin a row names is still its own failure.
        let fixtures = ["nornir-bench-redprobe", "fleet_mock_server"];
        let set = crate::variant::BuildVariantSet::discover(&edda)
            .expect("edda's declaration loads")
            .expect("edda declares a build space");
        let built: BTreeSet<String> = set
            .variants
            .iter()
            .filter(|v| matches!(v.kind, crate::variant::ArtifactKind::Exe | crate::variant::ArtifactKind::Service))
            .filter_map(|v| v.bin.clone())
            .collect();
        assert!(!built.is_empty() && declared.len() > built.len(), "vacuous: rows {built:?} of {declared:?}");
        let (tally, problems) = tally_bins(&edda, built, false).expect("tally edda's bins");
        assert!(
            problems.is_empty(),
            "edda's allowlist is malformed: {problems:?}"
        );
        for f in fixtures {
            assert!(
                tally.excused.contains_key(f),
                "`{f}` is a test-harness bin no cell builds and its written excuse was \
                 not read back — excused: {:?}",
                tally.excused.keys().collect::<Vec<_>>()
            );
        }
        assert!(
            tally.unbuilt.is_empty(),
            "edda has bins that are neither built by a cell nor excused in writing: {:?}",
            tally.unbuilt
        );
        assert!(
            tally.stale_excuses.is_empty(),
            "stale: {:?}",
            tally.stale_excuses
        );
    }

    /// **RED-probe for the tail helper**: piping through `tail` is what ate the exit
    /// status seven times in a day, so the replacement must be a pure function whose
    /// behaviour is asserted, not assumed.
    #[test]
    fn tail_keeps_the_last_lines_whole() {
        let s = (1..=100).map(|i| i.to_string()).collect::<Vec<_>>().join("\n");
        let t = tail_lines(&s, 3);
        assert_eq!(t, "98\n99\n100");
        // Fewer lines than asked for is the whole string, not a panic.
        assert_eq!(tail_lines("a\nb", 40), "a\nb");
        assert_eq!(tail_lines("", 40), "");
    }

    /// **A tally over an empty declaration must be RED, not green.** This is the
    /// vacuous-pass trap: `unbuilt.is_empty()` is trivially true when `declared` is
    /// empty, so a naive verdict passes hardest exactly when it knows least.
    #[test]
    fn an_empty_bin_tally_is_a_failure_not_a_pass() {
        let t = BinTally::default();
        assert!(t.unbuilt.is_empty(), "the naive condition really is satisfied");
        assert!(t.verdict().is_err(), "…and the verdict must still be RED");
    }

    #[test]
    fn an_empty_test_tally_is_a_failure_not_a_pass() {
        let t = TestTally::default();
        assert!(t.empty.is_empty(), "the naive condition really is satisfied");
        assert!(t.verdict().is_err(), "…and the verdict must still be RED");
    }

    /// The bin verdict goes red on a real shortfall, and the message carries the NAME.
    #[test]
    fn the_bin_verdict_names_what_was_not_built() {
        let mut t = BinTally {
            declared: vec!["a".into(), "b".into(), "c".into()],
            ..Default::default()
        };
        t.built.insert("a".into());
        t.unbuilt = vec!["b".into(), "c".into()];
        let e = t.verdict().unwrap_err();
        assert!(e.contains("\"b\""), "the failure must name b: {e}");
        assert!(e.contains("\"c\""), "the failure must name c: {e}");
        assert!(e.contains("2 of 3"), "the failure must carry both numbers: {e}");
        // …and green once they are built.
        t.built.insert("b".into());
        t.built.insert("c".into());
        t.unbuilt.clear();
        assert!(t.verdict().is_ok());
        assert_eq!(t.ratio(), 1.0);
    }

    /// **An artifact that is not a bin must not be counted as one.**
    ///
    /// `produced_bins` is filled from every artifact path a delivery produced, and
    /// `bin_name_of_artifact` is a file stem, so a mastered `.iso` and a container
    /// layer arrive in `built` under names that are in no `[[bin]]` table.
    ///
    /// SEEN RED on gunnar's real delivery, 2026-08-05, printed by the gate itself:
    ///   `bins: 2 of 4 declared built (125%)` — one bin, one `.iso`, three excused.
    /// A ratio over 1 is arithmetically impossible for "built over declared", which
    /// is the tell; the same defect had been printing `0 of 4 declared built (75%)`
    /// quietly for as long as the gate existed, where nothing looks wrong at all.
    #[test]
    fn a_non_bin_artifact_does_not_inflate_the_bin_ratio() {
        let mut t = BinTally {
            declared: vec![
                "gunnar".into(),
                "gunnar-installtest".into(),
                "gunnar-lfs-serve".into(),
                "xtask".into(),
            ],
            partial: true,
            ..Default::default()
        };
        // What the ISO cell actually produced: the chained app binary AND the ISO.
        t.built.insert("gunnar".into());
        t.built.insert("gunnar-appliance-iso".into());
        for b in ["gunnar-installtest", "gunnar-lfs-serve", "xtask"] {
            t.excused.insert(b.into(), "not a deliverable".into());
        }

        assert_eq!(
            t.built_declared(),
            1,
            "exactly one DECLARED bin came out; the .iso is not a bin"
        );
        assert!(
            t.ratio() <= 1.0,
            "a coverage ratio cannot exceed 1: {:.2}",
            t.ratio()
        );
        assert_eq!(t.ratio(), 1.0, "1 built + 3 excused over 4 declared");
        let h = t.headline();
        assert!(
            h.contains("4 declared") && h.contains("1 built") && h.contains("4/4 = 100% accounted for"),
            "the headline counts declared bins, not produced files, and shows the ratio's \
             own numerator so `1` beside `100%` is not read as broken arithmetic: {h}"
        );
        assert!(
            !h.contains("125%"),
            "and never prints a percentage over 100: {h}"
        );

        // The other direction, without which "always 1.0" passes: a genuinely
        // unbuilt, unexcused bin drags the ratio down and the count with it.
        t.excused.remove("xtask");
        t.unbuilt = vec!["xtask".into()];
        assert_eq!(t.built_declared(), 1, "still one declared bin built");
        assert_eq!(t.ratio(), 0.75, "1 built + 2 excused over 4 declared");
    }

    /// A stale excuse is its own red — otherwise the allowlist grows into a blanket
    /// that hides the next real hole.
    #[test]
    fn a_stale_excuse_is_red() {
        let mut t = BinTally { declared: vec!["a".into()], ..Default::default() };
        t.built.insert("a".into());
        assert!(t.verdict().is_ok(), "green before the stale entry");
        t.stale_excuses = vec!["a".into()];
        assert!(t.verdict().is_err(), "…and red after it");
    }

    /// The empty-test verdict names the FILE, not just a count.
    #[test]
    fn the_test_verdict_names_the_source_file() {
        // `files` is the denominator the verdict uses; `produced` is the work
        // measure. Setting only `produced` used to pass this test off a STALE test
        // binary — the clock on this box ran backwards and cargo called every unit
        // fresh. Both numbers are set explicitly now, and the vacuity guard below
        // asserts the distinction is load-bearing.
        let mut t = TestTally {
            produced: 5,
            with_tests: 3,
            files: 4,
            files_with_tests: 4,
            tests_found: 12,
            ..Default::default()
        };
        assert!(t.verdict().is_ok(), "green while nothing is dark");
        t.empty.push(EmptyTestBinary {
            atom: "korp-cli".into(),
            target: "live_map_coverage".into(),
            src: PathBuf::from("/repo/tests/live_map_coverage.rs"),
            exe: PathBuf::from("/t/live_map_coverage-abc"),
        });
        t.files_with_tests = 3;
        let e = t.verdict().unwrap_err();
        assert!(e.contains("live_map_coverage.rs"), "must name the file: {e}");
        assert!(e.contains("korp-cli"), "must name the atom: {e}");
        assert!(e.contains("1 of 4"), "must carry both numbers: {e}");
        // The ratio is over FILES, never over binaries: a correctly feature-gated file
        // is legitimately empty in some resolutions, so a per-binary ratio cannot
        // carry this assertion. 3 of 4 files reached, while only 3 of 5 binaries held
        // tests — the two numbers must not be confused.
        assert!((t.ratio() - 0.75).abs() < 1e-9, "per-file ratio {}", t.ratio());
    }

    /// An uninterrogable binary is not a pass.
    #[test]
    fn an_uninterrogable_binary_is_red() {
        let mut t = TestTally {
            produced: 1,
            with_tests: 1,
            files: 1,
            files_with_tests: 1,
            tests_found: 1,
            ..Default::default()
        };
        assert!(t.verdict().is_ok());
        t.uninterrogable.push(("x".into(), "would not run".into()));
        assert!(t.verdict().is_err());
    }

    /// **`judge_run` must go red on the silent shape**: exit 0, no artifact. That is
    /// the whole defect class, so the guard for it is asserted in both directions.
    #[test]
    fn a_zero_exit_that_produced_nothing_is_red() {
        let silent = CargoRun {
            label: "korp-cli".into(),
            argv: vec!["build".into(), "--bin".into(), "korp-cli".into()],
            status: Some(0),
            exited_ok: true,
            produced: vec![],
            errors: vec![],
            stderr_tail: String::new(),
        };
        let f = judge_run(&silent, true).expect("exit 0 with no artifact must be RED");
        assert!(f.contains("produced NO artifact"), "{f}");
        // The SAME run is green when no artifact was expected…
        assert!(judge_run(&silent, false).is_none());
        // …and green when one was produced.
        let mut loud = silent.clone();
        loud.produced.push(Produced {
            path: PathBuf::from("/t/korp-cli"),
            name: "korp-cli".into(),
            kinds: vec!["bin".into()],
            src: PathBuf::from("/repo/src/main.rs"),
            is_test: false,
        });
        assert!(judge_run(&loud, true).is_none());
    }

    /// A signal death must never read as a pass, and must not be reported as an exit
    /// code it does not have.
    #[test]
    fn a_signal_death_is_red_and_says_so() {
        let killed = CargoRun {
            label: "korp".into(),
            argv: vec!["build".into()],
            status: None,
            exited_ok: false,
            produced: vec![],
            errors: vec![],
            stderr_tail: "…".into(),
        };
        let f = judge_run(&killed, true).expect("a killed cargo is RED");
        assert!(f.contains("killed by a signal"), "{f}");
    }

    /// `integration_argv` must drop `--bin` AND its value, and add `--tests`. A bare
    /// bin name left behind is read by cargo as a test-name filter and compiles
    /// nothing — silently, which is the failure mode this module exists for.
    #[test]
    fn the_integration_argv_drops_bin_with_its_value() {
        let unit =
            vec!["test", "-p", "korp", "--bin", "korp", "--features", "a,b", "--no-run"];
        let unit: Vec<String> = unit.into_iter().map(String::from).collect();
        // Replicate the transform on a literal argv (the real one comes from
        // `cargo_args`, which needs a parsed declaration).
        let mut out: Vec<String> = Vec::new();
        let mut skip = false;
        for a in &unit {
            if skip {
                skip = false;
                continue;
            }
            if a == "--bin" {
                skip = true;
                continue;
            }
            out.push(a.clone());
        }
        out.push("--tests".into());
        assert!(!out.iter().any(|a| a == "--bin"));
        assert!(out.iter().any(|a| a == "--tests"));
        assert_eq!(out.len(), unit.len() - 1, "two dropped, one added: {out:?}");
        // The bin name is `korp` and so is `-p korp` — a bare `contains` check is a
        // false positive here, which is why the length relation is the assertion.
        assert!(out.iter().any(|a| a == "korp"), "the -p value must survive");
    }

    /// Arms must be selectable, and "no arm selected" must be distinguishable from
    /// "every arm passed".
    #[test]
    fn no_arm_selected_is_not_a_pass() {
        assert!(!Arms::coverage_only().any());
        assert!(Arms::all().any());
    }

    /// An excuse with no words is not an excuse.
    #[test]
    fn unreasoned_entries_are_found() {
        let mut m = BTreeMap::new();
        m.insert("a".to_string(), String::new());
        m.insert("b".to_string(), "   ".to_string());
        m.insert("c".to_string(), "short".to_string());
        m.insert("d".to_string(), "a genuinely written reason".to_string());
        let bad = unreasoned(&m);
        assert_eq!(bad, vec!["a", "b", "c"], "only the written one survives");
    }

    /// `GateSpec::only` must treat an empty string as "no narrowing" rather than as
    /// "narrow to nothing" — the latter would silently walk zero atoms.
    #[test]
    fn an_empty_atom_filter_does_not_narrow_to_nothing() {
        assert!(GateSpec::new("/x").only("").atoms.is_none());
        assert!(GateSpec::new("/x").only("  , ,").atoms.is_none());
        assert_eq!(GateSpec::new("/x").only("a, b").atoms.unwrap().len(), 2);
    }

    /// **A mistyped flag must be an ERROR, not a shrug.** A silently-ignored `--arms`
    /// runs a different gate than the one asked for and reports the wrong thing green
    /// — the same class as `ARMS=` selecting no arm at all.
    #[test]
    fn an_unknown_flag_or_arm_is_rejected() {
        assert!(cli::parse(&["--arms".into(), "bins".into()]).is_ok(), "the good spelling parses");
        assert!(cli::parse(&["--armz".into(), "bins".into()]).is_err(), "a typo must be RED");
        assert!(cli::parse(&["--arms".into()]).is_err(), "a flag with no value must be RED");
        assert!(cli::arms_of(Some("coverage")).is_ok());
        assert!(cli::arms_of(Some("covrage")).is_err(), "a typo'd arm must be RED");
        // …and the parsed arms really differ, or the flag is decorative.
        assert_eq!(cli::arms_of(Some("bins")).unwrap(), Arms { bins: true, tests: false });
        assert_eq!(cli::arms_of(Some("tests")).unwrap(), Arms { bins: false, tests: true });
        assert_eq!(cli::arms_of(None).unwrap(), Arms::all());
    }

    /// ★ RED-FIRST 2026-08-20 — the vocabulary is `--variant` here too: the gate's
    /// narrowing flag must accept the new word, and `--atom`/`--atoms` stay accepted
    /// (deprecated aliases; the written laws and muscle memory cite them).
    #[test]
    fn buildgate_narrows_by_variant_and_atom_alike() {
        let v = cli::parse(&["--variant".into(), "a,b".into()]).expect("--variant parses");
        let a = cli::parse(&["--atom".into(), "a,b".into()]).expect("--atom still parses");
        assert_eq!(v, a, "the two spellings must resolve IDENTICALLY");
        assert_eq!(v.atoms.as_deref(), Some("a,b"));
        assert_eq!(
            cli::parse(&["--variants".into(), "a".into()]).expect("--variants parses"),
            cli::parse(&["--atoms".into(), "a".into()]).expect("--atoms still parses"),
        );
        assert!(cli::parse(&["--variant".into()]).is_err(), "no value must be RED");
    }

    /// The workspace fallback must ANNOUNCE itself. A silent fallback would let a
    /// declaration disappear without the report changing.
    #[test]
    fn the_workspace_fallback_says_it_is_weaker() {
        let m = GateMode::Workspace { why: "no .nornir/build-variants.toml".into() };
        let h = m.headline();
        assert!(h.contains("WORKSPACE fallback"), "{h}");
        assert!(h.contains("weaker"), "the report must not present it as equivalent: {h}");
        let d = GateMode::Declared { atoms: vec!["korp".into(), "korp-cli".into()] };
        assert!(d.headline().contains("2 atom(s)"), "{}", d.headline());
    }

    /// The workspace stand-in must carry `--workspace`, or it repeats the exact
    /// measured hole: a root manifest that is a package AND a workspace makes cargo
    /// select the root package alone, which built 0 of 11 bins in znippy-zoomies.
    #[test]
    fn the_workspace_argv_selects_the_whole_workspace() {
        let b = workspace_argv(false);
        assert!(b.iter().any(|a| a == "--workspace"), "{b:?}");
        assert!(b.iter().any(|a| a == "--bins"), "{b:?}");
        let t = workspace_argv(true);
        assert!(t.iter().any(|a| a == "--workspace"), "{t:?}");
        assert!(t.iter().any(|a| a == "--no-run"), "{t:?}");
        assert!(t.iter().any(|a| a == "--tests"), "{t:?}");
        assert_ne!(b, t, "the two arms must not collapse to the same command");
    }

    /// `NaiveReach` must go red-ish (a shortfall) on the real shape and report both
    /// numbers. Asserted in both directions so a ratio stuck at 1.0 is impossible.
    #[test]
    fn naive_reach_reports_both_numbers() {
        let full = NaiveReach { bins: 9, reachable: 9, skipped: vec![] };
        assert_eq!(full.ratio(), 1.0);
        let short = NaiveReach {
            bins: 9,
            reachable: 4,
            skipped: vec![("korp-server".into(), vec!["server".into()])],
        };
        assert!((short.ratio() - 4.0 / 9.0).abs() < 1e-9);
        let h = short.headline();
        assert!(h.contains("4 of 9"), "{h}");
        assert!(h.contains("44%"), "{h}");
        // The identity trap: an empty repo must not read as 100%.
        assert_eq!(NaiveReach::default().ratio(), 0.0);
    }

    /// The rendered report must carry the numbers even when it PASSES. A gate that
    /// only speaks up when unhappy leaves nobody able to notice its denominator went
    /// to zero.
    #[test]
    fn the_report_carries_the_numbers_on_a_pass() {
        let mut o = GateOutcome { arms: Arms::all(), ..Default::default() };
        o.mode = GateMode::Declared { atoms: vec!["korp".into()] };
        o.naive = NaiveReach { bins: 9, reachable: 4, skipped: vec![] };
        o.atoms_walked = vec!["korp".into()];
        o.bins.declared = vec!["korp".into()];
        o.bins.built.insert("korp".into());
        o.tests.produced = 2;
        o.tests.with_tests = 2;
        o.tests.files = 2;
        o.tests.files_with_tests = 2;
        o.tests.tests_found = 7;
        let r = o.render();
        assert!(o.is_green());
        assert!(r.contains("1 declared") && r.contains("1 built"), "{r}");
        assert!(r.contains("1/1 = 100% accounted for"), "the ratio shows its own numerator: {r}");
        assert!(r.contains("2 of 2 reached"), "{r}");
        assert!(r.contains("2 produced"), "the work measure rides along too: {r}");
        assert!(r.contains("7 tests found"), "{r}");
        assert!(r.contains("VERDICT: PASS"), "{r}");
        // The diagnosis rides along on a PASS too — it is the number nobody had.
        assert!(r.contains("reaches 4 of 9"), "{r}");
    }
}