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)
}