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

nordisk/monetize

xtask/src/score.rs

//! `cargo xtask score [--out <file>]`: A22's six areas, measured from the tree.
//!
//! The scorecard of the v2.0 plan (P5: "score re-taken; the target is at least 4 of 5
//! in every area"), as a program instead of a reading. It is STATIC: it reads files
//! under the repository and its sibling checkouts and nothing else. No network, no
//! cargo, no git command, no live estate. It prints each area with its evidence and
//! writes `score.json` (default `target/score.json`). It is not a CI gate.
//!
//! # The rules
//!
//! Each area has four bars, taken from the targets of A22 (§3 of both reviews) and
//! A24 (§5b, §8.4). An area scores **1 + the number of bars it meets**: 1 is absent,
//! 3 is present with known holes, 5 is top-notch, as on A22's scale. The total is out
//! of 30; A25 §4 measured 16 by hand on 2026-10-24 ([`BASELINE`]).
//!
//! Words used below. **Test code** is a file under a `tests/`, `benches/` or
//! `examples/` directory, a file named `tests.rs`, a module declared under a test
//! `#[cfg(..)]`, or an item under one. **Tooling** is a crate that only tests or builds
//! ([`TOOLING`]); `xtask/` is not scanned at all. **Runtime code** is every other
//! line of Rust that is not blank and not a comment.
//!
//! | Area | Bar (id) | Met when |
//! |---|---|---|
//! | tests | `tests.suite` | at least [`MIN_TESTS`] test functions, and every runtime crate has one |
//! | | `tests.no-ignore` | there are tests, and no `#[ignore]` |
//! | | `tests.conformance` | the three trait suites of A24 (product guest, Cloud, Payment: `src/conformance.rs` in [`SUITES`]) exist, and test code in the tree runs each |
//! | | `tests.production-code` | no test code names a native product plugin or `Linked::commercial()` ([`NATIVE`]; the owner's rule) |
//! | robustness | `robustness.no-spawn` | runtime code has no `Command::new`, except [`ALLOWED_SPAWN`] (the owner's rule) |
//! | | `robustness.no-swallowed-save` | no `let _ =` in runtime code calls a save, store, money or custody function ([`SENSITIVE`]) |
//! | | `robustness.unwrap` | at most [`MAX_UNWRAP_PER_KLOC`] `.unwrap()`/`.expect(` per 1,000 runtime lines |
//! | | `robustness.threads` | at most [`MAX_THREAD_SPAWNS`] raw `thread::spawn` in runtime code (long work is a job) |
//! | security | `security.sandbox` | the product guest host names fuel, a deadline, a memory cap, a body cap and the host allow-list ([`SANDBOX_TOKENS`]) |
//! | | `security.signed` | runtime code verifies guests against `guest_trust_roots` with a `.p7s` |
//! | | `security.no-dead-grants` | the vocabulary is split by guest kind: the product kind's own set (`grants::PRODUCT`) holds none of [`DEAD_GRANTS`] (A22 §3.3, A24: product grants 6 → 0), and the host's whole vocabulary (`grants::VOCABULARY`) has no `env` door (owner decision 6, 2026-10-26) |
//! | | `security.rule-tests` | every door crate present ([`DOOR_CRATES`]) has a test file named `*security*` |
//! | observability | `observability.why` | at least [`MIN_NAMED_REFUSALS`] distinct `REFUSED [code]` in runtime code |
//! | | `observability.alerts` | at least [`MIN_ALERT_LINES`] runtime lines carry `ALERT` |
//! | | `observability.alert-table` | a doc has a table with a `code` and a `severity` column |
//! | | `observability.counters` | runtime code has a per-op counter type (`…Counter(s)`/`…Metrics`) or a metrics crate |
//! | docs | `docs.guide` | [`GUIDE`] has at least [`MIN_GUIDE_LINES`] lines, and `README.md` exists |
//! | | `docs.op-table` | the guide's op table names exactly the ops of `ops::ALL` |
//! | | `docs.no-stale` | none of the known stale claims ([`STALE`]) is in the tree |
//! | | `docs.crates` | every runtime crate has a `README.md` or a `//!` crate doc |
//! | release | `release.ci` | `cargo xtask ci`'s plan ([`CI_PLAN_FILES`]) carries the lock check and the workspace tests |
//! | | `release.lock-drift` | every path package of every lock is at its manifest's version, and no refused crate is in a lock twice |
//! | | `release.checklist` | a doc has a "release checklist" heading |
//! | | `release.locks` | at most [`MAX_LOCKS`] `Cargo.lock` files outside `xtask/` (A28 M1: root, box side, fast test, build thing, kvm guest) |

use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Component, Path, PathBuf};
use std::process::ExitCode;

use nornir_xtask_support::lockcheck;

/// `tests.suite`: the least number of test functions.
pub const MIN_TESTS: usize = 1000;
/// `robustness.unwrap`: the most `.unwrap()`/`.expect(` per 1,000 runtime lines.
pub const MAX_UNWRAP_PER_KLOC: u64 = 1;
/// `robustness.threads`: the most raw `thread::spawn` in runtime code.
pub const MAX_THREAD_SPAWNS: usize = 5;
/// `observability.why`: the least number of distinct named refusals.
pub const MIN_NAMED_REFUSALS: usize = 50;
/// `observability.alerts`: the least number of `ALERT` lines.
pub const MIN_ALERT_LINES: usize = 50;
/// `docs.guide`: the least length of the guide.
pub const MIN_GUIDE_LINES: usize = 300;
/// `release.locks`: the most lock files outside `xtask/`. A28 M1 (T14): the root and the four
/// detached workspaces outside xtask that keep a reason (monetize-admin, monetize-fast,
/// monetize-build-thing, the kvm guest; xtask/tests/detached_locks.rs says why).
pub const MAX_LOCKS: usize = 5;
/// `release.ci`: where `cargo xtask ci`'s plan is written, read together.
pub const CI_PLAN_FILES: &[&str] = &["xtask/src/ci.rs", "xtask/src/main.rs"];
/// The STE manual.
pub const GUIDE: &str = "docs/integration-guide.md";

/// Crates that only test or build: not runtime code. `xtask/` is not scanned at all.
pub const TOOLING: &[&str] = &[
    "uitest",
    "crates/mock-upcloud",
    "crates/mock-scaleway",
    "crates/monetize-fast",
    "crates/fast-edge",
    "crates/lake-product",
    "crates/monetize-build-thing",
    "guests/lake",
    "guests/toy",
    "guests/toy-vendor",
    "guests/toy-cloud",
    "guests/toy-dns",
    "guests/toy-signup",
];

/// Runtime files that may spawn: monetize-admin (PID 1) supervises its own server
/// child (owner, 2026-10-24: the rule forbids spawning tools, not supervising).
pub const ALLOWED_SPAWN: &[&str] = &["crates/monetize-admin/src/supervise.rs"];

/// The trait conformance suites of A24 §8.4: (name, crate dir, crate ident).
pub const SUITES: &[(&str, &str, &str)] = &[
    ("product guest", "crates/monetize-product-guest", "monetize_product_guest"),
    ("cloud", "crates/monetize-cloud", "monetize_cloud"),
    ("payment", "crates/monetize-pay", "monetize_pay"),
];

/// What a test must not open (the owner's rule: tests test the code that runs).
pub const NATIVE: &[&str] = &["monetize_product_gunnar", "monetize_product_holger", "Linked::commercial"];

/// Identifier stems that make a `let _ =` a swallowed save, money or custody error.
pub const SENSITIVE: &[&str] = &[
    "save", "store", "persist", "commit", "capture", "refund", "charge", "custody", "journal", "ledger", "settle",
];

/// What the product guest host must name (A22 §3.3: fuel, deadline, memory, body caps,
/// host allow-list).
pub const SANDBOX_TOKENS: &[&str] = &["fuel", "deadline", "max_memory", "BODY", "http_hosts"];

/// Grants that exist for a path that never runs (A22 §3.3; A25 §4).
pub const DEAD_GRANTS: &[&str] = &["GRPC", "SIGN", "ENV", "STATE"];

/// The crates that hold a door, a key or the guest sandbox.
pub const DOOR_CRATES: &[&str] = &[
    "crates/monetize-server",
    "crates/monetize-admin",
    "crates/monetize-front",
    "crates/monetize-product-wasm",
    "crates/monetize-cloud-wasm",
    "crates/monetize-site-buy",
];

/// Known stale claims (phrase, where it was named).
pub const STALE: &[(&str, &str)] = &[
    ("Your guest must still answer them", "A22 §3.5, A25 §4: the guide's push-op claim"),
    ("Holger's are keep-only", "A22 §3.5: F-U-16"),
    ("takes the update first", "A28 H17: the front drives the update and is upgraded last (wire.rs front_role)"),
    ("guardian.redb", "A28 M20: the fleet store is fleet.redb"),
    ("its usage (`state.redb`)", "A28 M20: usage is usage.redb; state.redb is the console's passkeys and sessions"),
    ("`VendorGuest`, 19 ops", "monetize v2 step 4: VendorGuest has the ten Payment ops (monetize-pay-guest ops::VENDOR)"),
    ("`VendorGuest`, 20 ops", "monetize v2 step 4: VendorGuest has the ten Payment ops (monetize-pay-guest ops::VENDOR)"),
    ("`STARTER-2xCPU-4GB` main", "A28 M24: Holger's main is STARTER-1xCPU-2GB (holger-monetize-guest estate.rs)"),
];

/// A25 §4's hand-measured score on 2026-10-24 (main cf36e8ed): 16 of 30.
pub const BASELINE: [(Area, u8); 6] = [
    (Area::Tests, 2),
    (Area::Robustness, 2),
    (Area::Security, 3),
    (Area::Observability, 3),
    (Area::Docs, 3),
    (Area::Release, 3),
];

/// How many evidence items a list shows before "and N more".
const SHOW: usize = 12;

/// One of A22's six areas.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Area {
    Tests,
    Robustness,
    Security,
    Observability,
    Docs,
    Release,
}

impl Area {
    pub fn name(self) -> &'static str {
        match self {
            Area::Tests => "tests",
            Area::Robustness => "robustness",
            Area::Security => "security",
            Area::Observability => "observability",
            Area::Docs => "docs",
            Area::Release => "release",
        }
    }
    pub fn baseline(self) -> u8 {
        BASELINE.iter().find(|(a, _)| *a == self).map(|(_, b)| *b).unwrap_or(0)
    }
}

/// One bar, met or not, with what was found.
#[derive(Debug, Clone)]
pub struct Check {
    pub id: &'static str,
    pub bar: String,
    pub pass: bool,
    pub evidence: Vec<String>,
}

/// One area: 1 + the bars it meets.
#[derive(Debug, Clone)]
pub struct AreaScore {
    pub area: Area,
    pub score: u8,
    pub checks: Vec<Check>,
}

/// The whole card.
#[derive(Debug, Clone)]
pub struct Card {
    pub root: PathBuf,
    /// HEAD's commit, read from `.git` when it is there.
    pub commit: Option<String>,
    pub areas: Vec<AreaScore>,
    /// The counts behind the bars, by name.
    pub facts: Vec<(String, u64)>,
}

impl Card {
    pub fn total(&self) -> u8 {
        self.areas.iter().map(|a| a.score).sum()
    }
    pub fn max(&self) -> u8 {
        (self.areas.len() * 5) as u8
    }
    pub fn baseline_total(&self) -> u8 {
        BASELINE.iter().map(|(_, b)| *b).sum()
    }

    /// The card as text: each area, each bar, its evidence.
    pub fn render(&self) -> String {
        let mut s = format!(
            "monetize scorecard (static; A22's six areas, 4 bars each, score = 1 + bars met)\n  tree   {}\n  commit {}\n\n",
            self.root.display(),
            self.commit.as_deref().unwrap_or("(no .git)")
        );
        for a in &self.areas {
            let b = a.area.baseline();
            let delta = a.score as i32 - b as i32;
            s.push_str(&format!(
                "{:<14} {} / 5   (baseline {b}, {})\n",
                a.area.name(),
                a.score,
                if delta == 0 { "same".to_string() } else { format!("{delta:+}") }
            ));
            for c in &a.checks {
                s.push_str(&format!("  [{}] {:<30} {}\n", if c.pass { "x" } else { " " }, c.id, c.bar));
                for e in &c.evidence {
                    s.push_str(&format!("        {e}\n"));
                }
            }
            s.push('\n');
        }
        s.push_str(&format!(
            "TOTAL {} / {}   (baseline {} / 30, A25 §4; target ≥ 24 with 4 in every area)\n",
            self.total(),
            self.max(),
            self.baseline_total()
        ));
        s
    }

    /// The card as JSON (hand-written: the xtask carries no serde).
    pub fn to_json(&self) -> String {
        let mut s = String::from("{\n");
        s.push_str("  \"schema\": 1,\n");
        s.push_str(&format!("  \"tree\": {},\n", json_string(&self.root.display().to_string())));
        s.push_str(&format!(
            "  \"commit\": {},\n",
            self.commit.as_deref().map(json_string).unwrap_or_else(|| "null".into())
        ));
        s.push_str(&format!("  \"total\": {},\n", self.total()));
        s.push_str(&format!("  \"max\": {},\n", self.max()));
        s.push_str(&format!("  \"baseline_total\": {},\n", self.baseline_total()));
        s.push_str("  \"baseline_source\": \"A25 §4 (2026-10-24, main cf36e8ed)\",\n");
        s.push_str("  \"rule\": \"area score = 1 + bars met (4 bars per area)\",\n");
        s.push_str("  \"areas\": [\n");
        for (i, a) in self.areas.iter().enumerate() {
            s.push_str("    {\n");
            s.push_str(&format!("      \"area\": {},\n", json_string(a.area.name())));
            s.push_str(&format!("      \"score\": {},\n", a.score));
            s.push_str(&format!("      \"baseline\": {},\n", a.area.baseline()));
            s.push_str("      \"checks\": [\n");
            for (j, c) in a.checks.iter().enumerate() {
                let ev: Vec<String> = c.evidence.iter().map(|e| json_string(e)).collect();
                s.push_str(&format!(
                    "        {{ \"id\": {}, \"pass\": {}, \"bar\": {}, \"evidence\": [{}] }}{}\n",
                    json_string(c.id),
                    c.pass,
                    json_string(&c.bar),
                    ev.join(", "),
                    if j + 1 < a.checks.len() { "," } else { "" }
                ));
            }
            s.push_str("      ]\n");
            s.push_str(&format!("    }}{}\n", if i + 1 < self.areas.len() { "," } else { "" }));
        }
        s.push_str("  ],\n");
        s.push_str("  \"facts\": {\n");
        for (i, (k, v)) in self.facts.iter().enumerate() {
            s.push_str(&format!(
                "    {}: {v}{}\n",
                json_string(k),
                if i + 1 < self.facts.len() { "," } else { "" }
            ));
        }
        s.push_str("  }\n}\n");
        s
    }
}

/// A JSON string literal of `s`.
pub fn json_string(s: &str) -> String {
    let mut o = String::with_capacity(s.len() + 2);
    o.push('"');
    for c in s.chars() {
        match c {
            '"' => o.push_str("\\\""),
            '\\' => o.push_str("\\\\"),
            '\n' => o.push_str("\\n"),
            '\r' => o.push_str("\\r"),
            '\t' => o.push_str("\\t"),
            c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)),
            c => o.push(c),
        }
    }
    o.push('"');
    o
}

/// Write the card's JSON to `out`, making its directory.
pub fn write_json(card: &Card, out: &Path) -> Result<(), String> {
    if let Some(dir) = out.parent() {
        fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
    }
    fs::write(out, card.to_json()).map_err(|e| format!("write {}: {e}", out.display()))
}

/// `cargo xtask score [--out <file>]`: print the card, write the JSON. Always exits 0
/// once it has written the card: the score is a reading, not a gate.
pub fn main(root: &Path, args: &[String]) -> ExitCode {
    let mut out = root.join("target/score.json");
    let mut it = args.iter();
    while let Some(a) = it.next() {
        match a.as_str() {
            "--out" => match it.next() {
                Some(p) => out = PathBuf::from(p),
                None => {
                    eprintln!("xtask score: `--out` needs a file");
                    return ExitCode::FAILURE;
                }
            },
            other => {
                eprintln!("xtask score: unknown argument `{other}` (usage: cargo xtask score [--out <file>])");
                return ExitCode::FAILURE;
            }
        }
    }
    let card = score(root);
    print!("{}", card.render());
    match write_json(&card, &out) {
        Ok(()) => {
            println!("\nscore.json: {}", out.display());
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("xtask score: {e}");
            ExitCode::FAILURE
        }
    }
}

// ───────────────────────────── the tree ─────────────────────────────

/// One Rust file, its lines and which of them are test code.
struct RsFile {
    rel: String,
    lines: Vec<String>,
    /// Per line: test code?
    test: Vec<bool>,
    /// The crate directory it belongs to (relative), if any.
    krate: Option<String>,
    tooling: bool,
}

impl RsFile {
    fn runtime(&self) -> bool {
        !self.tooling && self.krate.is_some() && !self.rel.ends_with("/build.rs") && self.rel != "build.rs"
    }
    /// Runtime code lines: (1-based line number, text).
    fn runtime_lines(&self) -> impl Iterator<Item = (usize, &str)> {
        let rt = self.runtime();
        self.lines
            .iter()
            .enumerate()
            .filter(move |(i, l)| rt && !self.test[*i] && is_code(l))
            .map(|(i, l)| (i + 1, l.as_str()))
    }
    fn test_lines(&self) -> impl Iterator<Item = (usize, &str)> {
        self.lines.iter().enumerate().filter(|(i, _)| self.test[*i]).map(|(i, l)| (i + 1, l.as_str()))
    }
}

struct Tree {
    root: PathBuf,
    rs: Vec<RsFile>,
    /// Other text files (`.md`, `.toml`), relative path → text.
    text: BTreeMap<String, String>,
    /// Crate directories (relative) → package name.
    crates: BTreeMap<String, String>,
}

fn is_code(l: &str) -> bool {
    let t = l.trim();
    !t.is_empty() && !t.starts_with("//") && !t.starts_with("/*") && !t.starts_with('*')
}

fn skip_dir(name: &str) -> bool {
    name.starts_with('.') || name == "target" || name.starts_with("target-") || name == "node_modules"
}

fn rel_of(root: &Path, p: &Path) -> String {
    p.strip_prefix(root).unwrap_or(p).to_string_lossy().replace('\\', "/")
}

fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
    let Ok(rd) = fs::read_dir(dir) else { return };
    let mut es: Vec<PathBuf> = rd.flatten().map(|e| e.path()).collect();
    es.sort();
    for p in es {
        let name = p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
        if p.is_dir() {
            if !skip_dir(&name) {
                walk(&p, out);
            }
        } else {
            out.push(p);
        }
    }
}

fn under(rel: &str, prefix: &str) -> bool {
    rel == prefix || rel.starts_with(&format!("{prefix}/"))
}

fn is_tooling(krate: &str) -> bool {
    TOOLING.iter().any(|t| under(krate, t))
}

/// A path that is test code by where it is.
fn test_path(rel: &str) -> bool {
    let parts: Vec<&str> = rel.split('/').collect();
    let (dirs, file) = parts.split_at(parts.len().saturating_sub(1));
    dirs.iter().any(|d| matches!(*d, "tests" | "benches" | "examples")) || file.first() == Some(&"tests.rs")
}

/// `#[cfg(...)]` that switches on under test (`test`, `any(test, …)`), not `not(test)`.
fn is_test_cfg(t: &str) -> bool {
    if !(t.starts_with("#[cfg(") || t.starts_with("#![cfg(")) || t.contains("not(test") {
        return false;
    }
    t.split(|c: char| !(c.is_alphanumeric() || c == '_')).any(|w| w == "test")
}

/// `mod name;` → `name`.
fn mod_decl(t: &str) -> Option<String> {
    let t = t.strip_prefix("pub(crate) ").or_else(|| t.strip_prefix("pub ")).unwrap_or(t);
    let rest = t.strip_prefix("mod ")?;
    let name = rest.strip_suffix(';')?.trim();
    (!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')).then(|| name.to_string())
}

/// The last line of the item that starts on `start`: braces counted outside strings,
/// raw strings (`r#"…"#`, R5a: their quotes are text), char literals and line comments;
/// an item with no brace ends at its `;`.
fn item_end(lines: &[String], start: usize) -> usize {
    let (mut depth, mut opened, mut in_str) = (0i64, false, false);
    // Inside a raw string: the number of `#` that close it.
    let mut in_raw: Option<usize> = None;
    for (k, line) in lines.iter().enumerate().skip(start) {
        let cs: Vec<char> = line.chars().collect();
        let mut i = 0;
        while i < cs.len() {
            let c = cs[i];
            if let Some(hashes) = in_raw {
                if c == '"' && (1..=hashes).all(|h| cs.get(i + h) == Some(&'#')) {
                    in_raw = None;
                    i += 1 + hashes;
                } else {
                    i += 1;
                }
                continue;
            }
            if in_str {
                if c == '\\' {
                    i += 2;
                    continue;
                }
                if c == '"' {
                    in_str = false;
                }
                i += 1;
                continue;
            }
            if let Some((hashes, len)) = raw_start(&cs, i) {
                in_raw = Some(hashes);
                i += len;
                continue;
            }
            match c {
                '/' if cs.get(i + 1) == Some(&'/') => break,
                '"' => in_str = true,
                '\'' if cs.get(i + 2) == Some(&'\'') => {
                    i += 3;
                    continue;
                }
                '\'' if cs.get(i + 1) == Some(&'\\') && cs.get(i + 3) == Some(&'\'') => {
                    i += 4;
                    continue;
                }
                '{' => {
                    depth += 1;
                    opened = true;
                }
                '}' => {
                    depth -= 1;
                    if opened && depth <= 0 {
                        return k;
                    }
                }
                ';' if !opened => return k,
                _ => {}
            }
            i += 1;
        }
    }
    lines.len().saturating_sub(1)
}

/// A raw string literal opening at `i` (`r"`, `r#"`, `br##"`, not inside a name such
/// as `for"`): its `#` count and the length of the opener.
fn raw_start(cs: &[char], i: usize) -> Option<(usize, usize)> {
    let r = match cs.get(i) {
        Some('r') => i,
        Some('b') if cs.get(i + 1) == Some(&'r') => i + 1,
        _ => return None,
    };
    if i > 0 && cs.get(i - 1).is_some_and(|p| p.is_alphanumeric() || *p == '_') {
        return None;
    }
    let hashes = cs[r + 1..].iter().take_while(|c| **c == '#').count();
    (cs.get(r + 1 + hashes) == Some(&'"')).then_some((hashes, r - i + 2 + hashes))
}

/// Which lines of a file are under a test `#[cfg]`, and the module files it gates.
fn test_mask(lines: &[String]) -> (Vec<bool>, Vec<String>) {
    let n = lines.len();
    let mut mask = vec![false; n];
    let mut gated = Vec::new();
    let mut i = 0;
    while i < n {
        let t = lines[i].trim();
        if t.starts_with("#![cfg(") && is_test_cfg(t) {
            return (vec![true; n], gated);
        }
        if !is_test_cfg(t) {
            i += 1;
            continue;
        }
        let mut j = i + 1;
        while j < n {
            let u = lines[j].trim();
            if u.is_empty() || u.starts_with("#[") || u.starts_with("//") {
                j += 1;
            } else {
                break;
            }
        }
        if j >= n {
            mask[i..].iter_mut().for_each(|m| *m = true);
            break;
        }
        let end = match mod_decl(lines[j].trim()) {
            Some(name) => {
                gated.push(name);
                j
            }
            None => item_end(lines, j),
        };
        mask[i..=end].iter_mut().for_each(|m| *m = true);
        i = end + 1;
    }
    (mask, gated)
}

/// `[package]` name and version of a manifest (`None` version when it is inherited
/// and no `[workspace.package]` above it says it).
fn manifest_package(path: &Path, stop: &Path) -> Option<(String, Option<String>)> {
    let text = fs::read_to_string(path).ok()?;
    let (mut section, mut name, mut version, mut inherit) = (String::new(), None, None, false);
    for l in text.lines() {
        let t = l.trim();
        if t.starts_with('[') {
            section = t.trim_matches(|c| c == '[' || c == ']').trim().to_string();
            continue;
        }
        if section != "package" {
            continue;
        }
        let Some((k, v)) = t.split_once('=') else { continue };
        let (k, v) = (k.trim(), v.trim());
        match k {
            "name" => name = Some(v.trim_matches('"').to_string()),
            "version" if v.starts_with('"') => version = Some(v.trim_matches('"').to_string()),
            "version" if v.contains("workspace") => inherit = true,
            "version.workspace" => inherit = true,
            _ => {}
        }
    }
    let name = name?;
    if inherit && version.is_none() {
        version = workspace_version(path.parent()?, stop);
    }
    Some((name, version))
}

/// The `[workspace.package] version` of the nearest manifest at or above `dir`.
fn workspace_version(dir: &Path, stop: &Path) -> Option<String> {
    let mut d = Some(dir);
    while let Some(cur) = d {
        if let Ok(text) = fs::read_to_string(cur.join("Cargo.toml")) {
            let mut section = String::new();
            for l in text.lines() {
                let t = l.trim();
                if t.starts_with('[') {
                    section = t.trim_matches(|c| c == '[' || c == ']').trim().to_string();
                    continue;
                }
                if section == "workspace.package" {
                    if let Some((k, v)) = t.split_once('=') {
                        if k.trim() == "version" {
                            return Some(v.trim().trim_matches('"').to_string());
                        }
                    }
                }
            }
        }
        if cur == stop {
            break;
        }
        d = cur.parent();
    }
    None
}

/// Every `path = "…"` in a manifest or a cargo config, resolved against `base`.
fn path_deps(text: &str, base: &Path) -> Vec<PathBuf> {
    let mut out = Vec::new();
    for part in text.split("path").skip(1) {
        let rest = part.trim_start();
        let Some(rest) = rest.strip_prefix('=') else { continue };
        let rest = rest.trim_start();
        let Some(rest) = rest.strip_prefix('"') else { continue };
        let Some(p) = rest.split('"').next() else { continue };
        out.push(normalize(&base.join(p)));
    }
    out
}

fn normalize(p: &Path) -> PathBuf {
    let mut out = PathBuf::new();
    for c in p.components() {
        match c {
            Component::ParentDir => {
                out.pop();
            }
            Component::CurDir => {}
            c => out.push(c.as_os_str()),
        }
    }
    out
}

impl Tree {
    fn read(root: &Path) -> Tree {
        let mut files = Vec::new();
        walk(root, &mut files);
        let mut crates = BTreeMap::new();
        let mut rs_paths = Vec::new();
        let mut text = BTreeMap::new();
        for p in files {
            let rel = rel_of(root, &p);
            if under(&rel, "xtask") {
                continue;
            }
            let name = p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
            if name == "Cargo.toml" {
                if let Some((pkg, _)) = manifest_package(&p, root) {
                    let dir = rel.strip_suffix("Cargo.toml").unwrap_or("").trim_end_matches('/').to_string();
                    crates.insert(dir, pkg);
                }
            }
            if name.ends_with(".rs") {
                rs_paths.push((rel, p));
            } else if name.ends_with(".md") || name.ends_with(".toml") {
                if let Ok(t) = fs::read_to_string(&p) {
                    text.insert(rel, t);
                }
            }
        }
        // Pass 1: masks, and the module files a test cfg gates.
        let mut gated: BTreeSet<String> = BTreeSet::new();
        let mut parsed = Vec::new();
        for (rel, p) in rs_paths {
            let Ok(t) = fs::read_to_string(&p) else { continue };
            let lines: Vec<String> = t.lines().map(str::to_string).collect();
            let (mask, mods) = test_mask(&lines);
            let (dir, file) = rel.rsplit_once('/').unwrap_or(("", rel.as_str()));
            let base = if matches!(file, "lib.rs" | "main.rs" | "mod.rs") {
                dir.to_string()
            } else {
                let stem = file.trim_end_matches(".rs");
                if dir.is_empty() {
                    stem.to_string()
                } else {
                    format!("{dir}/{stem}")
                }
            };
            for m in mods {
                let g = if base.is_empty() { m } else { format!("{base}/{m}") };
                gated.insert(format!("{g}.rs"));
                gated.insert(g);
            }
            parsed.push((rel, lines, mask));
        }
        // Pass 2: classify.
        let rs = parsed
            .into_iter()
            .map(|(rel, lines, mask)| {
                let whole = test_path(&rel) || gated.iter().any(|g| under(&rel, g));
                let test = if whole { vec![true; lines.len()] } else { mask };
                let krate = crates.keys().filter(|d| d.is_empty() || under(&rel, d)).max_by_key(|d| d.len()).cloned();
                let tooling = krate.as_deref().map(is_tooling).unwrap_or(false);
                RsFile { rel, lines, test, krate, tooling }
            })
            .collect();
        Tree { root: root.to_path_buf(), rs, text, crates }
    }

    fn runtime_crates(&self) -> Vec<&String> {
        self.crates.keys().filter(|d| !d.is_empty() && !is_tooling(d)).collect()
    }

    fn rs(&self, rel: &str) -> Option<&RsFile> {
        self.rs.iter().find(|f| f.rel == rel)
    }

    /// The docs: `docs/**.md` and the top-level `.md` files.
    fn docs(&self) -> impl Iterator<Item = (&String, &String)> {
        self.text.iter().filter(|(k, _)| k.ends_with(".md") && (under(k, "docs") || !k.contains('/')))
    }
}

fn capped(mut items: Vec<String>) -> Vec<String> {
    if items.len() > SHOW {
        let more = items.len() - SHOW;
        items.truncate(SHOW);
        items.push(format!("… and {more} more"));
    }
    items
}

fn thousands(n: u64) -> String {
    let s = n.to_string();
    let mut o = String::new();
    for (i, c) in s.chars().enumerate() {
        if i > 0 && (s.len() - i) % 3 == 0 {
            o.push(',');
        }
        o.push(c);
    }
    o
}

fn check(id: &'static str, bar: &str, pass: bool, evidence: Vec<String>) -> Check {
    Check { id, bar: bar.to_string(), pass, evidence }
}

fn listed(head: String, items: &[String]) -> Vec<String> {
    let mut ev = vec![head];
    ev.extend(capped(items.iter().map(|e| format!("  {e}")).collect()));
    ev
}

// ───────────────────────────── the scorer ─────────────────────────────

/// What one pass over the runtime code counts.
#[derive(Default)]
struct Runtime {
    lines: u64,
    unwraps: u64,
    /// file → (first line, count)
    unwrap_files: BTreeMap<String, (usize, u64)>,
    unwrap_or_default: u64,
    spawns: Vec<String>,
    threads: Vec<String>,
    let_all: u64,
    let_save: Vec<String>,
    refusals: BTreeSet<String>,
    alerts: usize,
    counters: Vec<String>,
    trust_roots: Vec<String>,
    p7s: Vec<String>,
}

fn count_runtime(tree: &Tree) -> Runtime {
    let mut r = Runtime::default();
    for f in &tree.rs {
        for (n, l) in f.runtime_lines() {
            r.lines += 1;
            let u = (l.matches(".unwrap()").count() + l.matches(".expect(").count()) as u64;
            if u > 0 {
                r.unwraps += u;
                r.unwrap_files.entry(f.rel.clone()).or_insert((n, 0)).1 += u;
            }
            r.unwrap_or_default += l.matches("unwrap_or_default()").count() as u64;
            if l.contains("Command::new(") && !ALLOWED_SPAWN.contains(&f.rel.as_str()) {
                r.spawns.push(format!("{}:{n}", f.rel));
            }
            for _ in 0..l.matches("thread::spawn(").count() {
                r.threads.push(format!("{}:{n}", f.rel));
            }
            for rhs in l.split("let _ =").skip(1) {
                r.let_all += 1;
                // Only a call can swallow an error: `let _ = (set, journal);`, `let _ = &x;`
                // and `let _ = x;` mark a name as used.
                let rhs = rhs.trim_start();
                let call = rhs.starts_with(|c: char| c.is_alphabetic() || c == '_' || c == ':') && rhs.contains('(');
                let sensitive = call
                    && rhs
                        .split(|c: char| !(c.is_alphanumeric() || c == '_'))
                        .any(|w| SENSITIVE.iter().any(|s| w.to_lowercase().starts_with(s)));
                if sensitive {
                    r.let_save.push(format!("{}:{n}  {}", f.rel, l.trim()));
                }
            }
            for after in l.split("REFUSED [").skip(1) {
                if let Some(code) = after.split(']').next() {
                    if !code.is_empty() && code.chars().all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.')) {
                        r.refusals.insert(code.to_string());
                    }
                }
            }
            if l.contains("ALERT") {
                r.alerts += 1;
            }
            let ctr = l
                .split("struct ")
                .skip(1)
                .filter_map(|s| s.split(|c: char| !(c.is_alphanumeric() || c == '_')).next())
                .any(|id| id.ends_with("Counter") || id.ends_with("Counters") || id.ends_with("Metrics"));
            if ctr || l.contains("metrics::counter!") || l.contains("prometheus::") {
                r.counters.push(format!("{}:{n}", f.rel));
            }
            if l.contains("guest_trust_roots") {
                r.trust_roots.push(format!("{}:{n}", f.rel));
            }
            if l.contains(".p7s") {
                r.p7s.push(format!("{}:{n}", f.rel));
            }
        }
    }
    r
}

/// Score the tree at `root`.
pub fn score(root: &Path) -> Card {
    let tree = Tree::read(root);
    let rt = count_runtime(&tree);
    let rt_crates = tree.runtime_crates();
    let mut facts: Vec<(String, u64)> = Vec::new();

    let tests = score_tests(&tree, &rt_crates, &mut facts);
    let robustness = score_robustness(&rt, &mut facts);
    let security = score_security(&tree, &rt);
    let observability = score_observability(&tree, &rt, &mut facts);
    let docs = score_docs(&tree, &rt_crates, &mut facts);
    let release = score_release(root, &tree, &mut facts);

    Card {
        root: root.to_path_buf(),
        commit: head_commit(root),
        areas: vec![
            area(Area::Tests, tests),
            area(Area::Robustness, robustness),
            area(Area::Security, security),
            area(Area::Observability, observability),
            area(Area::Docs, docs),
            area(Area::Release, release),
        ],
        facts,
    }
}

fn fact(facts: &mut Vec<(String, u64)>, k: &str, v: u64) {
    facts.push((k.to_string(), v));
}

fn score_tests(tree: &Tree, rt_crates: &[&String], facts: &mut Vec<(String, u64)>) -> Vec<Check> {
    let mut test_fns = 0usize;
    let mut test_files = BTreeSet::new();
    let mut crates_with_tests = BTreeSet::new();
    let mut ignored = Vec::new();
    let mut native = Vec::new();
    for f in &tree.rs {
        for (n, l) in f.lines.iter().enumerate() {
            let t = l.trim();
            if t.starts_with("#[test]") || t.starts_with("#[tokio::test") {
                test_fns += 1;
                test_files.insert(f.rel.clone());
                if let Some(k) = &f.krate {
                    crates_with_tests.insert(k.clone());
                }
            }
            if t.starts_with("#[ignore") {
                ignored.push(format!("{}:{}", f.rel, n + 1));
            }
        }
        if f.test_lines().any(|(_, l)| NATIVE.iter().any(|w| l.contains(w))) {
            native.push(f.rel.clone());
        }
    }
    fact(facts, "test_fns", test_fns as u64);
    fact(facts, "test_files", test_files.len() as u64);
    fact(facts, "ignored", ignored.len() as u64);
    fact(facts, "native_plugin_test_files", native.len() as u64);

    let untested: Vec<String> = rt_crates.iter().filter(|c| !crates_with_tests.contains(**c)).map(|c| c.to_string()).collect();
    let head = format!(
        "{} test functions in {} files (bar ≥ {})",
        thousands(test_fns as u64),
        test_files.len(),
        thousands(MIN_TESTS as u64)
    );
    let mut ev = vec![head];
    if untested.is_empty() {
        ev.push(format!("every runtime crate ({}) has a test", rt_crates.len()));
    } else {
        ev.extend(listed(format!("{} of {} runtime crates have no test:", untested.len(), rt_crates.len()), &untested));
    }
    let suite = check(
        "tests.suite",
        &format!("≥ {} test functions, and every runtime crate has one", thousands(MIN_TESTS as u64)),
        test_fns >= MIN_TESTS && untested.is_empty(),
        ev,
    );

    let mut ev = listed(format!("{} #[ignore]", ignored.len()), &ignored);
    if test_fns == 0 {
        ev.push("no tests at all".into());
    }
    let ignore = check("tests.no-ignore", "there are tests, and none is #[ignore]", test_fns > 0 && ignored.is_empty(), ev);

    let mut ev = Vec::new();
    let (mut present, mut run) = (0u64, 0u64);
    for (name, dir, ident) in SUITES {
        let candidates = [format!("{dir}/src/conformance.rs"), format!("{dir}/src/conformance/mod.rs")];
        let Some(file) = candidates.iter().find(|c| tree.root.join(c).is_file()) else {
            ev.push(format!("{name}: missing ({})", candidates[0]));
            continue;
        };
        present += 1;
        let own = format!("{dir}/src/conformance");
        let qualified = format!("{ident}::conformance");
        let runners: Vec<&str> = tree
            .rs
            .iter()
            .filter(|f| !under(&f.rel, &own) && f.rel != *file)
            .filter(|f| {
                f.test_lines()
                    .any(|(_, l)| l.contains(&qualified) || (f.krate.as_deref() == Some(*dir) && l.contains("conformance::")))
            })
            .map(|f| f.rel.as_str())
            .collect();
        if !runners.is_empty() {
            run += 1;
        }
        let who = if runners.is_empty() { String::new() } else { format!(": {}", runners.join(", ")) };
        ev.push(format!("{name}: present ({file}), {} test file(s) run it{who}", runners.len()));
    }
    fact(facts, "conformance_suites_present", present);
    fact(facts, "conformance_suites_run", run);
    let n = SUITES.len() as u64;
    let conf = check(
        "tests.conformance",
        "the product guest, Cloud and Payment conformance suites exist, and a test runs each",
        present == n && run == n,
        ev,
    );

    let prod = check(
        "tests.production-code",
        "no test opens a native product plugin or Linked::commercial() (tests test what runs)",
        native.is_empty(),
        listed(format!("{} test file(s) open a native product plugin or Linked::commercial()", native.len()), &native),
    );
    vec![suite, ignore, conf, prod]
}

fn score_robustness(rt: &Runtime, facts: &mut Vec<(String, u64)>) -> Vec<Check> {
    fact(facts, "runtime_lines", rt.lines);
    fact(facts, "unwrap_expect", rt.unwraps);
    fact(facts, "unwrap_or_default", rt.unwrap_or_default);
    fact(facts, "let_underscore", rt.let_all);
    fact(facts, "let_underscore_on_save", rt.let_save.len() as u64);
    fact(facts, "thread_spawns", rt.threads.len() as u64);
    fact(facts, "process_spawns", rt.spawns.len() as u64);

    let spawn = check(
        "robustness.no-spawn",
        "runtime code spawns no process (the supervisor excepted)",
        rt.spawns.is_empty(),
        listed(format!("{} process spawn(s) in runtime code (allowed: {})", rt.spawns.len(), ALLOWED_SPAWN.join(", ")), &rt.spawns),
    );
    let swallowed = check(
        "robustness.no-swallowed-save",
        "no `let _ =` swallows a save, store, money or custody error",
        rt.let_save.is_empty(),
        listed(
            format!("{} `let _ =` in runtime code; {} on a save, store, money or custody call", rt.let_all, rt.let_save.len()),
            &rt.let_save,
        ),
    );
    let per_k = if rt.lines == 0 { 0.0 } else { rt.unwraps as f64 * 1000.0 / rt.lines as f64 };
    let mut by: Vec<(&String, &(usize, u64))> = rt.unwrap_files.iter().collect();
    by.sort_by(|a, b| b.1 .1.cmp(&a.1 .1).then(a.0.cmp(b.0)));
    let worst: Vec<String> = by.iter().map(|(f, (first, n))| format!("{f}:{first} ({n})")).collect();
    let unwrap = check(
        "robustness.unwrap",
        &format!("≤ {MAX_UNWRAP_PER_KLOC} unwrap()/expect( per 1,000 runtime lines"),
        rt.unwraps * 1000 <= rt.lines * MAX_UNWRAP_PER_KLOC,
        listed(
            format!(
                "{} unwrap()/expect( in {} runtime lines: {per_k:.2} per 1,000 (bar ≤ {MAX_UNWRAP_PER_KLOC}); by file, first line (count):",
                thousands(rt.unwraps),
                thousands(rt.lines)
            ),
            &worst,
        ),
    );
    let threads = check(
        "robustness.threads",
        &format!("≤ {MAX_THREAD_SPAWNS} raw thread::spawn in runtime code (long work is a job)"),
        rt.threads.len() <= MAX_THREAD_SPAWNS,
        listed(
            format!(
                "{} raw thread::spawn in runtime code (bar ≤ {MAX_THREAD_SPAWNS}); {} unwrap_or_default() besides",
                rt.threads.len(),
                rt.unwrap_or_default
            ),
            &rt.threads,
        ),
    );
    vec![spawn, swallowed, unwrap, threads]
}

fn score_security(tree: &Tree, rt: &Runtime) -> Vec<Check> {
    let host: String = tree
        .rs
        .iter()
        .filter(|f| under(&f.rel, "crates/monetize-product-wasm/src"))
        .flat_map(|f| f.runtime_lines().map(|(_, l)| l.to_string()).collect::<Vec<_>>())
        .collect::<Vec<_>>()
        .join("\n");
    let missing: Vec<&str> = SANDBOX_TOKENS.iter().filter(|t| !host.contains(**t)).copied().collect();
    let sandbox = check(
        "security.sandbox",
        "the guest host sets fuel, a deadline, a memory cap, a body cap and the host allow-list",
        missing.is_empty() && !host.is_empty(),
        vec![if host.is_empty() {
            "crates/monetize-product-wasm/src: no runtime code".into()
        } else if missing.is_empty() {
            format!("crates/monetize-product-wasm/src names {}", SANDBOX_TOKENS.join(", "))
        } else {
            format!("crates/monetize-product-wasm/src lacks {}", missing.join(", "))
        }],
    );
    let first = |v: &[String]| v.first().map(|e| format!(", first {e}")).unwrap_or_default();
    let signed = check(
        "security.signed",
        "guests are verified against guest_trust_roots with a .p7s",
        !rt.trust_roots.is_empty() && !rt.p7s.is_empty(),
        vec![
            format!("guest_trust_roots: {} runtime line(s){}", rt.trust_roots.len(), first(&rt.trust_roots)),
            format!(".p7s: {} runtime line(s){}", rt.p7s.len(), first(&rt.p7s)),
        ],
    );
    let sdk = "crates/monetize-product-guest/src/lib.rs";
    // Owner decision 6 (2026-10-26): the vocabulary is split by guest kind, so the bar reads the
    // PRODUCT kind's own set, never the host's whole vocabulary; and `env` is no generic door, so
    // the vocabulary itself must not carry it for any kind.
    let is_dead = |x: &String| DEAD_GRANTS.iter().any(|d| d.eq_ignore_ascii_case(x));
    let (pass, ev) = match tree.rs(sdk) {
        None => (false, vec![format!("{sdk} is missing")]),
        Some(f) if !f.lines.iter().any(|l| l.trim_start().starts_with("pub mod grants")) => (true, vec![format!("{sdk} has no grants table: no grant")]),
        Some(f) => match const_list(&f.lines, "pub mod grants", "pub const PRODUCT") {
            None => (false, vec![format!("{sdk}: the grants are not split by guest kind (no grants::PRODUCT, the product kind's own set)")]),
            Some(g) => {
                let dead: Vec<String> = g.iter().filter(|x| is_dead(x)).map(|x| x.to_uppercase()).collect();
                let tail = if dead.is_empty() { String::new() } else { format!("; dead: {} (granted for a path that never runs)", dead.join(", ")) };
                let mut ev = vec![format!("{sdk}: grants::PRODUCT = [{}]{tail}", g.join(", "))];
                let env_door = match const_list(&f.lines, "pub mod grants", "pub const VOCABULARY") {
                    Some(v) => {
                        let env = v.iter().any(|x| x.eq_ignore_ascii_case("env"));
                        ev.push(format!(
                            "{sdk}: grants::VOCABULARY = [{}]{}",
                            v.join(", "),
                            if env { "; ENV is still a generic door (owner decision 6)" } else { "" }
                        ));
                        env
                    }
                    None => false,
                };
                (dead.is_empty() && !env_door, ev)
            }
        },
    };
    let grants = check(
        "security.no-dead-grants",
        "the grants are split by guest kind: the product kind grants none of grpc, sign, env, state, and no kind has an env door",
        pass,
        ev,
    );

    let doors: Vec<&str> = DOOR_CRATES.iter().copied().filter(|d| tree.crates.contains_key(*d)).collect();
    let (mut have, mut lack) = (Vec::new(), Vec::new());
    for d in &doors {
        let files: Vec<&str> = tree
            .rs
            .iter()
            .filter(|f| f.krate.as_deref() == Some(*d) && f.test.first().copied().unwrap_or(false))
            .filter(|f| f.rel.rsplit('/').next().map(|n| n.contains("security")).unwrap_or(false))
            .map(|f| f.rel.as_str())
            .collect();
        if files.is_empty() {
            lack.push(d.to_string());
        } else {
            have.push(format!("  {d}: {}", files.join(", ")));
        }
    }
    let mut ev = vec![format!("{} of {} door crate(s) have a security test file", have.len(), doors.len())];
    ev.extend(have);
    if !lack.is_empty() {
        ev.push(format!("  without: {}", lack.join(", ")));
    }
    let rules = check(
        "security.rule-tests",
        "every door crate has a security test file that names its rules",
        !doors.is_empty() && lack.is_empty(),
        ev,
    );
    vec![sandbox, signed, grants, rules]
}

fn score_observability(tree: &Tree, rt: &Runtime, facts: &mut Vec<(String, u64)>) -> Vec<Check> {
    fact(facts, "named_refusals", rt.refusals.len() as u64);
    fact(facts, "alert_lines", rt.alerts as u64);
    let why = check(
        "observability.why",
        &format!("≥ {MIN_NAMED_REFUSALS} distinct named refusals REFUSED [code]"),
        rt.refusals.len() >= MIN_NAMED_REFUSALS,
        vec![format!("{} distinct REFUSED [code] in runtime code", rt.refusals.len())],
    );
    let alerts = check(
        "observability.alerts",
        &format!("≥ {MIN_ALERT_LINES} ALERT lines in runtime code"),
        rt.alerts >= MIN_ALERT_LINES,
        vec![format!("{} runtime line(s) carry ALERT", rt.alerts)],
    );
    let tables: Vec<String> = tree
        .docs()
        .flat_map(|(rel, t)| {
            t.lines()
                .enumerate()
                .filter(|(_, l)| {
                    let l = l.trim().to_lowercase();
                    let cells: Vec<&str> = l.split('|').map(str::trim).collect();
                    l.starts_with('|') && cells.iter().any(|c| c.contains("code")) && cells.iter().any(|c| c.contains("severity"))
                })
                .map(|(n, _)| format!("{rel}:{}", n + 1))
                .collect::<Vec<_>>()
        })
        .collect();
    let table = check(
        "observability.alert-table",
        "a doc has an alert table (code, severity, who)",
        !tables.is_empty(),
        vec![if tables.is_empty() {
            "no table with a code and a severity column in docs/ or the top-level .md".into()
        } else {
            format!("alert table at {}", tables.join(", "))
        }],
    );
    let counters = check(
        "observability.counters",
        "a counter per guest op and per vendor call",
        !rt.counters.is_empty(),
        vec![if rt.counters.is_empty() {
            "no …Counter(s)/…Metrics type and no metrics crate in runtime code".into()
        } else {
            format!("counters at {}", rt.counters.join(", "))
        }],
    );
    vec![why, alerts, table, counters]
}

fn score_docs(tree: &Tree, rt_crates: &[&String], facts: &mut Vec<(String, u64)>) -> Vec<Check> {
    let guide = tree.text.get(GUIDE);
    let guide_lines = guide.map(|g| g.lines().count()).unwrap_or(0);
    let readme = tree.text.contains_key("README.md");
    let present = check(
        "docs.guide",
        &format!("{GUIDE} (≥ {MIN_GUIDE_LINES} lines) and README.md exist"),
        guide_lines >= MIN_GUIDE_LINES && readme,
        vec![
            if guide.is_some() { format!("{GUIDE}: {guide_lines} lines") } else { format!("{GUIDE}: missing") },
            format!("README.md: {}", if readme { "present" } else { "missing" }),
        ],
    );

    let code_ops = tree
        .rs("crates/monetize-product-guest/src/lib.rs")
        .and_then(|f| const_list(&f.lines, "pub mod ops", "pub const ALL"))
        .unwrap_or_default();
    let guide_ops = guide.map(|g| guide_op_names(g)).unwrap_or_default();
    let code_set: BTreeSet<&str> = code_ops.iter().map(String::as_str).collect();
    let guide_set: BTreeSet<&str> = guide_ops.iter().map(String::as_str).collect();
    let not_in_guide: Vec<&str> = code_set.difference(&guide_set).copied().collect();
    let not_in_code: Vec<&str> = guide_set.difference(&code_set).copied().collect();
    let mut ev = vec![format!("ops::ALL has {} op(s); the guide's op table names {}", code_set.len(), guide_set.len())];
    if !not_in_guide.is_empty() {
        ev.push(format!("  not in the guide: {}", not_in_guide.join(", ")));
    }
    if !not_in_code.is_empty() {
        ev.push(format!("  not in ops::ALL: {}", not_in_code.join(", ")));
    }
    let ops = check(
        "docs.op-table",
        "the guide's op table names exactly ops::ALL",
        !code_set.is_empty() && not_in_guide.is_empty() && not_in_code.is_empty(),
        ev,
    );

    let mut stale = Vec::new();
    let texts = tree.rs.iter().map(|f| (&f.rel, f.lines.join("\n"))).chain(tree.text.iter().map(|(k, v)| (k, v.clone())));
    for (rel, t) in texts {
        for (n, l) in t.lines().enumerate() {
            for (phrase, why) in STALE {
                if l.contains(phrase) {
                    stale.push(format!("{rel}:{}  \"{phrase}\" ({why})", n + 1));
                }
            }
        }
    }
    fact(facts, "stale_claims", stale.len() as u64);
    let no_stale = check(
        "docs.no-stale",
        "no known stale claim is in the tree",
        stale.is_empty(),
        listed(format!("{} hit(s) of {} known stale claim(s)", stale.len(), STALE.len()), &stale),
    );

    let undoc: Vec<String> = rt_crates
        .iter()
        .filter(|c| {
            let readme = tree.text.contains_key(&format!("{c}/README.md"));
            let doc = ["src/lib.rs", "src/main.rs"]
                .iter()
                .filter_map(|f| tree.rs(&format!("{c}/{f}")))
                .any(|f| f.lines.iter().any(|l| l.trim_start().starts_with("//!")));
            !readme && !doc
        })
        .map(|c| c.to_string())
        .collect();
    fact(facts, "runtime_crates", rt_crates.len() as u64);
    fact(facts, "undocumented_crates", undoc.len() as u64);
    let head = format!("{} of {} runtime crates have a README.md or a //! crate doc", rt_crates.len() - undoc.len(), rt_crates.len());
    let crates = check(
        "docs.crates",
        "every runtime crate has a README.md or a //! crate doc",
        undoc.is_empty() && !rt_crates.is_empty(),
        if undoc.is_empty() { vec![head] } else { listed(head + "; without:", &undoc) },
    );
    vec![present, ops, no_stale, crates]
}

fn score_release(root: &Path, tree: &Tree, facts: &mut Vec<(String, u64)>) -> Vec<Check> {
    // The plan lives in xtask/src/ci.rs (since 2026-10-24) or, in an older tree, main.rs.
    let read: Vec<&str> = CI_PLAN_FILES.iter().copied().filter(|f| root.join(f).is_file()).collect();
    let ci: String = read.iter().map(|f| fs::read_to_string(root.join(f)).unwrap_or_default()).collect::<Vec<_>>().join("\n");
    let has_lock = ci.contains("lockcheck::step");
    let has_test = ci.contains("\"test\", \"--workspace\"");
    let said = |b: bool| if b { "present" } else { "missing" };
    let ci = check(
        "release.ci",
        "cargo xtask ci carries the lock check and the workspace tests",
        has_lock && has_test,
        vec![format!("{}: lock check step {}, `test --workspace` step {}", if read.is_empty() { CI_PLAN_FILES.join(", ") } else { read.join(" + ") }, said(has_lock), said(has_test))],
    );

    let (drift, locks) = lock_drift(root);
    fact(facts, "locks", locks.len() as u64);
    fact(facts, "lock_drift", drift.len() as u64);
    let drift = check(
        "release.lock-drift",
        "every path package of every lock is at its manifest's version; no refused crate twice",
        drift.is_empty(),
        listed(format!("{} lock(s) read, {} finding(s)", locks.len(), drift.len()), &drift),
    );

    let heads: Vec<String> = tree
        .docs()
        .flat_map(|(rel, t)| {
            t.lines()
                .enumerate()
                .filter(|(_, l)| l.trim_start().starts_with('#') && l.to_lowercase().contains("release checklist"))
                .map(|(n, _)| format!("{rel}:{}", n + 1))
                .collect::<Vec<_>>()
        })
        .collect();
    let checklist = check(
        "release.checklist",
        "a doc has a release checklist",
        !heads.is_empty(),
        vec![if heads.is_empty() {
            "no \"release checklist\" heading in docs/ or the top-level .md".into()
        } else {
            format!("at {}", heads.join(", "))
        }],
    );

    let outside: Vec<String> = locks.iter().filter(|l| !under(l, "xtask")).cloned().collect();
    let locks = check(
        "release.locks",
        &format!("≤ {MAX_LOCKS} lock files outside xtask/ (root, box side, fast test, build thing, kvm guest)"),
        outside.len() <= MAX_LOCKS,
        listed(format!("{} Cargo.lock file(s) outside xtask/ (bar ≤ {MAX_LOCKS})", outside.len()), &outside),
    );
    vec![ci, drift, checklist, locks]
}

fn area(a: Area, checks: Vec<Check>) -> AreaScore {
    let met = checks.iter().filter(|c| c.pass).count();
    AreaScore { area: a, score: 1 + met.min(4) as u8, checks }
}

/// The entries of `const ALL` inside `mod`: string literals, or identifiers when the
/// list names constants. `None` when the module or the list is not there.
fn const_list(lines: &[String], module: &str, konst: &str) -> Option<Vec<String>> {
    let m = lines.iter().position(|l| l.trim_start().starts_with(module))?;
    let end = item_end(lines, m);
    let k = (m..=end).find(|&i| lines[i].trim_start().starts_with(konst))?;
    let mut body = String::new();
    for l in &lines[k..=end] {
        body.push_str(l);
        body.push('\n');
        if l.contains("];") {
            break;
        }
    }
    let inner = body.split_once("= &[")?.1;
    let inner = inner.split("];").next()?;
    let strings: Vec<String> = inner.split('"').skip(1).step_by(2).map(str::to_string).collect();
    if !strings.is_empty() {
        return Some(strings);
    }
    Some(
        inner
            .split(|c: char| !(c.is_alphanumeric() || c == '_'))
            .filter(|w| !w.is_empty())
            .map(str::to_string)
            .collect(),
    )
}

/// The backticked names in the second column of the guide's "op table" section.
fn guide_op_names(guide: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut inside = false;
    for l in guide.lines() {
        let t = l.trim();
        if t.starts_with('#') {
            inside = t.to_lowercase().contains("op table");
            continue;
        }
        if !inside || !t.starts_with('|') {
            continue;
        }
        let Some(cell) = t.split('|').nth(2) else { continue };
        out.extend(cell.split('`').skip(1).step_by(2).map(|s| s.trim().to_string()).filter(|s| !s.is_empty()));
    }
    out
}

/// Lock drift, statically: every lock of the repo (the lock check's own `locks_of` and
/// `parse_lock`); a path package must be at a version some manifest of that name in
/// the repo or its sibling checkouts carries; a refused crate must not be there twice.
/// The siblings are found by following `path = "…"` out of the repo, transitively.
fn lock_drift(root: &Path) -> (Vec<String>, Vec<String>) {
    let parent = root.parent().unwrap_or(root).to_path_buf();
    let mut versions: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
    let mut scanned: BTreeSet<PathBuf> = BTreeSet::new();
    let mut queue = vec![root.to_path_buf()];
    while let Some(top) = queue.pop() {
        if !top.is_dir() || !scanned.insert(top.clone()) {
            continue;
        }
        let mut files = Vec::new();
        walk(&top, &mut files);
        for c in ["config.toml", "siblings.toml", "estate-siblings.toml"] {
            files.push(top.join(".cargo").join(c));
        }
        for p in files {
            let name = p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
            if !name.ends_with(".toml") {
                continue;
            }
            let Ok(text) = fs::read_to_string(&p) else { continue };
            if name == "Cargo.toml" {
                if let Some((pkg, Some(v))) = manifest_package(&p, &top) {
                    versions.entry(pkg).or_default().insert(v);
                }
            }
            let dir = p.parent().unwrap_or(&top);
            // A cargo config's paths are relative to the directory that holds `.cargo`.
            let base = if dir.file_name().map(|n| n == ".cargo").unwrap_or(false) { dir.parent().unwrap_or(&top) } else { dir };
            for dep in path_deps(&text, base) {
                if dep.starts_with(&top) || !dep.starts_with(&parent) {
                    continue;
                }
                if let Some(Component::Normal(first)) = dep.strip_prefix(&parent).ok().and_then(|r| r.components().next()) {
                    let sib = parent.join(first);
                    if !scanned.contains(&sib) {
                        queue.push(sib);
                    }
                }
            }
        }
    }
    let mut findings = Vec::new();
    let mut locks = Vec::new();
    for lock in lockcheck::locks_of(root) {
        let rel = rel_of(root, &lock);
        locks.push(rel.clone());
        let Ok(text) = fs::read_to_string(&lock) else {
            findings.push(format!("{rel}: unreadable"));
            continue;
        };
        let pkgs = lockcheck::parse_lock(&text);
        for p in pkgs.iter().filter(|p| p.source.is_none()) {
            match versions.get(&p.name) {
                Some(vs) if vs.contains(&p.version) => {}
                Some(vs) => findings.push(format!(
                    "{rel}: {} {}, its manifest says {} (stale)",
                    p.name,
                    p.version,
                    vs.iter().cloned().collect::<Vec<_>>().join(" or ")
                )),
                None => findings.push(format!(
                    "{rel}: {} {} has no manifest here (sibling not checked out?); cannot prove it",
                    p.name, p.version
                )),
            }
        }
        for (name, vs, refused) in lockcheck::twice(&pkgs, lockcheck::DEFAULT_SINGLE) {
            if refused {
                findings.push(format!("{rel}: {name} twice ({})", vs.join(", ")));
            }
        }
    }
    (findings, locks)
}

/// HEAD's commit from `.git`, without running git.
fn head_commit(root: &Path) -> Option<String> {
    let git = root.join(".git");
    let head = fs::read_to_string(git.join("HEAD")).ok()?;
    let head = head.trim();
    let Some(r) = head.strip_prefix("ref: ") else { return Some(head.to_string()) };
    if let Ok(s) = fs::read_to_string(git.join(r)) {
        return Some(s.trim().to_string());
    }
    let packed = fs::read_to_string(git.join("packed-refs")).ok()?;
    packed.lines().find(|l| l.ends_with(&format!(" {r}"))).and_then(|l| l.split_whitespace().next()).map(str::to_string)
}