src/release/gate.rs
//! Release gates. Each gate returns `Ok(())` on pass, `Err` on fail.
//! Generated binaries propagate the first error and abort the release.
//!
//! # DE-FORKED 2026-07-22 — dwarves owns the portable gates
//!
//! The 2026-07-14 doctrine (*release is a build op*) relocated the PURE gates to
//! [`nornir_release_core::gate`], but the nornir copies were never deleted —
//! a copied-not-moved fork that drifted for a week in both directions. It is
//! resolved the only legal way: the single implementation lives in dwarves (which
//! absorbed nornir's bug-provenance comments and 9 of its regression tests), and
//! nornir **re-exports** it below so every `release::gate::…` call site — the
//! `nornir-server` `Release.GateAll`/`gate_*` RPCs, the MCP `release_gate_*`
//! tools, the CLI — resolves unchanged against ONE implementation.
//!
//! ## Re-exported from dwarves (nornir owns NO copy)
//! `no_path_patches` · `path_dep_audit`/`PathDepFinding` ·
//! `crate_metadata_check`/`CrateMetaCheck` · `links_declarations_scan`/
//! `detect_links_conflicts`/`LinkDecl`/`LinksConflict` · `nexus_floor` ·
//! `no_regression`/`no_regression_against`/`no_regression_windowed`/`MetricStats`
//! + the window/sigma defaults · `integration_roundtrip`(`_via_cargo_test`) ·
//! `git_tag_matches_published`.
//!
//! ## STAYS in nornir (warehouse/domain-coupled — L5a: the warehouse stays here)
//! * [`semantic_link_gate`] — reads the SCIP semantic blast radius computed from
//! the Mimir call-graph; the fact source is nornir's warehouse.
//! * [`core_saturation`]/[`core_saturation_run`]/[`is_latency_microbench`] — reason
//! over `warehouse::iceberg::BenchTelemetryRow`, a nornir warehouse row type, and
//! the microbench exemption list is a fact about nornir's OWN benches.
//! * [`coverage_gate`]/[`coverage_gate_all_workspaces`]/[`coverage_gate_outcome`]/
//! [`gate_all_outcome`] — the `autonom` completeness engine nornir owns.
//! * [`tests_gate`]/[`run_tests_gate`] — verdict over warehouse `TestResultRow`s.
//! * [`render_and_stage_docs`] — drives `crate::docs`, nornir's render engine.
//! * [`selected_gates`]/[`GATE_ORDER`] — resolve nornir's `[gates]` config table.
//!
//! No new crate edge: nornir already path-deps `dwarves` (`features = ["release"]`).
use std::path::Path;
use anyhow::{Context, Result, anyhow};
use crate::bench::{BenchRun, history};
/// The PORTABLE release gates — **implemented once, in dwarves**. These were
/// forked here until 2026-07-22; the fork is deleted and this re-export is the
/// only way nornir reaches them, so the two can never drift again.
pub use nornir_release_core::gate::{
CrateMetaCheck, DEFAULT_REGRESSION_SIGMA, DEFAULT_REGRESSION_WINDOW, LinkDecl, LinksConflict,
MetricStats, PathDepFinding, crate_metadata_check, detect_links_conflicts,
git_tag_matches_published, integration_roundtrip, integration_roundtrip_via_cargo_test,
links_declarations_scan, nexus_floor, no_path_patches, no_regression, no_regression_against,
no_regression_windowed, path_dep_audit,
};
/// Release GATE — the **SCIP semantic LINK check** (EPIC item 7). A version bump
/// that REMOVES a symbol a dependent still calls is a pre-release *link error*: once
/// the crate ships, every such dependent fails to compile/link on that undefined
/// symbol, exactly as a linker fails on an undefined reference. This gate reads the
/// semantic blast radius already computed from the warehouse call-graph
/// ([`crate::release::doctor::run_with_semantic_blast`] →
/// [`nornir_release_core::semantic_blast::SemanticBlast`]) and FAILS the release when any
/// bumped repo carries a link error — the doctor refuses to publish a tree that would
/// not link, just as it refuses a dependency cycle or a fork rider.
///
/// A merely *Changed* symbol (still resolves, may behave differently) is a WARNING,
/// not a failure — it does not block the gate. An empty blast (no dependent calls the
/// changed surface) passes trivially, so a bump with no semantic input never blocks.
pub fn semantic_link_gate(
blast: &std::collections::BTreeMap<String, nornir_release_core::semantic_blast::SemanticBlast>,
) -> Result<()> {
let mut lines: Vec<String> = Vec::new();
let mut total_sites = 0usize;
for sb in blast.values() {
let errs = sb.link_errors();
if errs.is_empty() {
continue;
}
for imp in errs {
total_sites += imp.call_sites.len();
lines.push(format!(
" 🔗 LINK ERROR: {}::{} was removed but is called by {} site(s):",
sb.target_repo,
imp.symbol,
imp.call_sites.len()
));
for site in &imp.call_sites {
lines.push(format!(
" {}::{} — {}:{}",
site.crate_name, site.caller, site.file, site.line
));
}
}
}
if lines.is_empty() {
return Ok(());
}
Err(anyhow!(
"release doctor: pre-release LINK ERROR — {} call site(s) invoke {} removed symbol(s); \
publishing would break the dependents (a linker would fail the same way). \
Restore the symbol(s), deprecate instead of removing, or update the call sites first.\n{}",
total_sites,
lines.iter().filter(|l| l.contains("LINK ERROR")).count(),
lines.join("\n"),
))
}
// `metric_lower_is_better` (the latency-vs-throughput direction classifier) went to
// dwarves with the `no_regression` family it serves — it is crate-private there and
// nornir has no other caller.
/// The CORE-SATURATION LAW as a fraction: a parallel hot path must keep at
/// least this share of the machine's cores busy at peak. The law is "~11/12
/// cores" — 0.9 is the release floor below which under-use is a bug, not
/// "Amdahl-fine". Callers may override via the per-repo `min_core_busy_frac`.
pub const CORE_SATURATION_MIN_BUSY_FRAC: f64 = 0.9;
/// Single-thread bench-variant suffix (holger/ljar `_st`) — exempt from the
/// core-saturation gate: a deliberately single-threaded variant is *supposed*
/// to use one core. Re-exported from [`crate::bench`] so the gate and the
/// mashup docs share ONE notion of "the `_st` suffix marks a single-thread
/// variant of the same system" (the gate exempts it; the mashup keeps it a
/// distinct row).
use crate::bench::is_single_thread;
/// nornir's own **latency / IO microbenches** — deliberately serial arms that are
/// NOT parallel-compute and so, like an `_st` variant, are EXEMPT from the
/// core-saturation LAW (one busy core is correct for them, not an under-use bug):
///
/// * `nornir.dep_graph_build` / `_ext` — one `cargo metadata` spawn per repo
/// (I/O + subprocess bound) plus a warm in-memory closure-query battery. Their
/// stable, authoritative metric is `queries_per_sec` (~600K, run-to-run flat);
/// `build_ms` is host-contention wall-time that swings ~10× and must NOT be
/// read as a compute regression, nor the arm as multi-threaded (the `(mt)`
/// impression the bencher flagged as O1 does not apply).
/// * `nornir.delta_rescan` — a single-file re-parse latency measurement; its
/// headline is `speedup_vs_full`, not a core count.
///
/// The genuinely parallel nornir arms — `knowledge_scan` (all-core `syn` parse),
/// and the warehouse throughput arms — are NOT listed here and stay subject to
/// the LAW. Keeping this list in nornir (not the shared edda `_st` helper) means
/// no cross-repo bump is needed and the exemption is unit-tested here.
pub fn is_latency_microbench(bench: &str) -> bool {
matches!(
bench,
"nornir.dep_graph_build" | "nornir.dep_graph_build_ext" | "nornir.delta_rescan"
)
}
/// **Warehouse-gate: the core-saturation LAW.** Fail the release when a
/// PARALLEL bench under-used the machine's cores — peak occupancy
/// (`proc_cores_max`) below `min_busy_frac × n_cores`. The examples the law
/// names (`lbz2` 8/12, `lgz` 2.7/12) are bugs to fix, never excused as
/// "Amdahl-fine"; this gate makes that enforceable instead of display-only.
///
/// ## Why this reads `proc_cores_max` and not `cores_busy_max`
///
/// Until 2026-08-03 it read `cores_busy_max`, which comes from `/proc/stat` —
/// **the whole host**. Measured on oden (32 cores) that day: the strictly
/// single-threaded arm `gunnar.walk_1m` reported `cores_busy_max` 32/32 and
/// 99.85–99.95% CPU while the benchmark process occupied **0.56 of one core**.
/// The neighbours' load was being read as the bench's own parallelism, so this
/// gate PASSED — on an arm with no parallelism at all, and would have passed
/// forever on any shared box. A guard that cannot report red is worse than no
/// guard, because it is counted as coverage.
///
/// `proc_cores_max` is task-clock ÷ wall from `/proc/self/stat`, which is
/// host-independent. Cross-check from the same measurement:
/// `gunnar.clone_concurrent_64` genuinely occupies 10.99–12.11 of 32 cores
/// (0.37) where the old column claimed 0.94.
///
/// **What the number excludes.** It counts this process's threads plus children
/// **reaped** during the unit. An arm whose work lives in a still-running child
/// (a spawned server left up) under-reports. That shape is a bench bug of its
/// own; it is not silently absorbed here — see `child_cpu_ms`.
///
/// `telemetry` is the `bench_telemetry` rows for the run under test (caller
/// filters to the relevant `run_id`/repo). EXEMPT rows: single-thread variants
/// (`_st` suffix — deliberately serial), nornir's latency microbenches, and
/// degenerate machines (`n_cores <= 1`). Rows with `proc_cores_max == None` are
/// rows written before the measurement existed: they are UNJUDGEABLE, not
/// passing, and are reported as skipped rather than counted as green. A
/// `min_busy_frac` of `0.0` falls back to [`CORE_SATURATION_MIN_BUSY_FRAC`].
pub fn core_saturation(
telemetry: &[crate::warehouse::iceberg::BenchTelemetryRow],
min_busy_frac: f64,
) -> Result<()> {
let frac = if min_busy_frac > 0.0 {
min_busy_frac
} else {
CORE_SATURATION_MIN_BUSY_FRAC
};
let mut violations: Vec<String> = Vec::new();
let mut unmeasured: Vec<&str> = Vec::new();
for t in telemetry {
// Exempt deliberately-serial variants (`_st`), nornir's own latency/IO
// microbenches (dep_graph_build/_ext, delta_rescan — never
// parallel-compute), and degenerate single-core hosts.
if is_single_thread(&t.bench) || is_latency_microbench(&t.bench) || t.n_cores <= 1 {
continue;
}
// NOT `unwrap_or(0.0)`: a pre-2026-08-03 row has no process-scoped
// measurement, and defaulting it to zero would manufacture a RED that
// no consumer could tell from a real under-use.
let Some(peak) = t.proc_cores_max else {
unmeasured.push(&t.bench);
continue;
};
let need = frac * t.n_cores as f64;
if peak < need {
violations.push(format!(
"{} peaked {:.2}/{} cores ({:.0}% < {:.0}% floor)",
t.bench,
peak,
t.n_cores,
peak / t.n_cores as f64 * 100.0,
frac * 100.0,
));
}
}
if !unmeasured.is_empty() {
// Loud, not silent: a skipped row is a row this gate did not check, and
// an operator reading "green" deserves to know how much of the run that
// verdict actually covered.
eprintln!(
"core-saturation: {} row(s) SKIPPED — no process-scoped CPU telemetry \
(written before proc_cores_max existed; re-run the bench to judge them): {}",
unmeasured.len(),
unmeasured.join(", "),
);
}
if violations.is_empty() {
Ok(())
} else {
Err(anyhow!(
"core-saturation gate RED — {} parallel bench(es) under-used the cores \
(the ~11/12 LAW; fix the saturation, don't excuse it as Amdahl-fine): {}",
violations.len(),
violations.join("; "),
))
}
}
/// Run-facing form of [`core_saturation`]: check the LAW against a
/// just-executed [`BenchRun`] IN MEMORY, before it is persisted. Each result's
/// CPU/mem footprint is lifted from its `telem_*` metrics
/// ([`crate::bench::telemetry::from_metrics`]) — the SAME extraction the
/// warehouse's telemetry table uses — so the gate can veto a red run *before*
/// [`crate::warehouse::iceberg::IcebergWarehouse::append_bench_run`] makes it a
/// baseline. Results without a telemetry footprint are skipped.
pub fn core_saturation_run(run: &BenchRun, min_busy_frac: f64) -> Result<()> {
let rows: Vec<crate::warehouse::iceberg::BenchTelemetryRow> = run
.results
.iter()
.filter_map(|r| {
let t = crate::bench::telemetry::from_metrics(&r.metrics)?;
Some(crate::warehouse::iceberg::BenchTelemetryRow {
run_id: String::new(),
repo: String::new(),
bench: r.name.clone(),
n_cores: t.n_cores,
cpu_pct_avg: t.cpu_pct_avg,
cpu_pct_max: t.cpu_pct_max,
cores_busy_avg: t.cores_busy_avg,
cores_busy_max: t.cores_busy_max,
mem_peak_mb: t.mem_peak_mb,
mem_pct_max: t.mem_pct_max,
elapsed_ms: t.elapsed_ms,
proc_cpu_ms: t.proc_cpu_ms,
child_cpu_ms: t.child_cpu_ms,
proc_cores_avg: t.proc_cores_avg,
proc_cores_max: t.proc_cores_max,
})
})
.collect();
core_saturation(&rows, min_busy_frac)
}
// ─── Cargo-pipeline gates (features 1, 4, 5, 8, 16) ─────────────────
/// Gate (autonom n-005): the completeness gate must be GREEN — every discovered
/// surface (viz tabs × {thin,fat}, CLI subcommands, MCP tools, unreached fns)
/// is covered by an inject-assert test or excused by a LIVE allowlist entry, and
/// no allowlist entry is stale. Takes the already-computed [`GateReport`] (the
/// caller has the warehouse + discrete inputs); this is the pure verdict gate.
///
/// HARD zero (not a ratchet): fails if `Gap ≠ ∅` OR any stale allowlist entry.
pub fn coverage_gate(report: &nornir_testmatrix::coverage::GateReport) -> Result<()> {
if report.is_green() {
return Ok(());
}
let mut parts: Vec<String> = Vec::new();
if !report.gap.missing.is_empty() {
let keys: Vec<&str> = report.gap.missing.iter().map(|n| n.id.as_str()).collect();
parts.push(format!(
"{} uncovered+un-allowlisted surface(s): {}",
report.gap.missing.len(),
keys.join(", ")
));
// Bencher-coverage gate ROW (Task #39): call out hot-paths that ship with
// NO bencher as their own line, so the perf-coverage gap reads distinctly
// from a stale UI/CLI surface (same allowlist/burn-down mechanism).
let no_bencher: Vec<&str> = report
.gap
.missing
.iter()
.filter(|n| n.kind == nornir_testmatrix::discover::SurfaceKind::Bencher)
.map(|n| n.id.as_str())
.collect();
if !no_bencher.is_empty() {
parts.push(format!(
"{} hot-path(s) with NO bencher: {}",
no_bencher.len(),
no_bencher.join(", ")
));
}
}
if !report.stale.is_empty() {
let keys: Vec<&str> = report.stale.iter().map(|e| e.key.as_str()).collect();
parts.push(format!(
"{} stale allowlist entr(ies): {}",
report.stale.len(),
keys.join(", ")
));
}
Err(anyhow!(
"autonom completeness gate RED — {}",
parts.join("; ")
))
}
/// Gate (autonom S6): the CROSS-WORKSPACE completeness gate must be GREEN — every
/// served workspace's per-workspace [`coverage_gate`] passes AND (where measured)
/// every RESOLVED metro line / reachable surface is green. Mirrors
/// [`coverage_gate`] but over a [`MegaGateReport`](crate::autonom::megagate::MegaGateReport):
/// it fails on the FIRST non-green workspace, naming `(workspace, key)` so the
/// burn-down points at the exact offending surface in the exact workspace.
///
/// HARD zero per workspace (not a ratchet): an uncovered+un-allowlisted surface,
/// a stale allowlist entry, OR an unmet resolved-metro rollup in ANY workspace
/// reds the whole release. Pure verdict over the already-gathered report (the
/// caller swept the warehouse via `gather_all`); this never reads the warehouse.
pub fn coverage_gate_all_workspaces(
report: &crate::autonom::megagate::MegaGateReport,
) -> Result<()> {
for wg in &report.workspaces {
// The per-workspace coverage gate — reuse the single-workspace verdict so
// the message body (uncovered / stale keys) is identical, just prefixed.
if let Err(e) = coverage_gate(&wg.coverage) {
return Err(anyhow!(
"mega completeness gate RED — workspace `{}`: {}",
wg.workspace,
e
));
}
// The reachable (resolved metro / LAW-9) rollup, when measured: every
// resolved line must be green. Unresolved bin→lib lines are excluded
// upstream (allowlistable until scip is populated — S6 step 4 risk note).
if let Some((green, total)) = wg.reachable {
if green != total {
return Err(anyhow!(
"mega completeness gate RED — workspace `{}`: only {}/{} resolved metro \
lines green (an unlit gating stop on a resolved chain)",
wg.workspace,
green,
total
));
}
}
}
Ok(())
}
/// `release gate coverage <repo>` rendered as the uniform
/// [`crate::cli_outcome::CommandOutcome`] — the CLI's `state_json` for the autonom
/// completeness verdict. Pure SHAPER over the already-computed [`GateReport`] (the
/// caller gathered the surface + covered set via `gate_coverage_for_repo`); it never
/// reads the warehouse.
///
/// GATE ok-semantics: `ok ⟺ the gate PASSED` (`report.is_green()` — `Gap == ∅` and no
/// stale allowlist entry). A FAILING gate is `ok:false` with the uncovered surface +
/// stale entries surfaced in `data` (machine-readable, the actionable burn-down) — that
/// is the CORRECT verdict, not a process error. Both verdicts carry real `data`; only the
/// passing one is `is_sannr` (a red gate is a red surface, by the RAGNARÖK rule). AUT9.
pub fn coverage_gate_outcome(
repo: &str,
report: &nornir_testmatrix::coverage::GateReport,
) -> crate::cli_outcome::CommandOutcome {
use crate::cli_outcome::CommandOutcome;
let cmd = "release gate coverage";
// Empty-is-green guard (bug #14): a report covering ZERO behaviours
// (`total == 0`) has an empty gap and would read green — that is not a pass.
let passed = report.is_green() && report.gap.total > 0;
let missing: Vec<&str> = report.gap.missing.iter().map(|n| n.id.as_str()).collect();
let stale: Vec<&str> = report.stale.iter().map(|e| e.key.as_str()).collect();
// Bencher-coverage gate ROW (Task #39): the hot-paths shipping with NO bencher,
// broken out so the perf-coverage dimension is machine-readable on its own.
let bencher_missing: Vec<&str> = report
.gap
.missing
.iter()
.filter(|n| n.kind == nornir_testmatrix::discover::SurfaceKind::Bencher)
.map(|n| n.id.as_str())
.collect();
let data = serde_json::json!({
"repo": repo,
"gate": "coverage",
"passed": passed,
"covered": report.gap.covered,
"total": report.gap.total,
"missing_count": report.gap.missing.len(),
"stale_count": report.stale.len(),
"missing": missing,
"stale": stale,
"bencher_missing_count": bencher_missing.len(),
"bencher_missing": bencher_missing,
});
let human = format!("release gate coverage `{repo}`: {}", report.summary());
CommandOutcome {
command: cmd.into(),
ok: passed,
data,
human,
}
}
/// `release gate-all <repo>` rendered as the uniform
/// [`crate::cli_outcome::CommandOutcome`] — the CLI's `state_json` for the full
/// per-repo gate sweep. Pure SHAPER over the already-run `passed` / `failed`
/// verdicts (the caller ran every configured gate); it runs no gate itself.
///
/// GATE ok-semantics: `ok ⟺ EVERY enabled gate passed` (`failed` is empty). A run with any
/// failing gate is `ok:false`, with the failed gate names + their error messages in `data`
/// (and `human`) — the correct red verdict, not a process error. The passing verdict (with
/// ≥1 gate run) is `is_sannr`; the failing one is RED. AUT9.
pub fn gate_all_outcome(
repo: &str,
passed: &[String],
failed: &[(String, String)],
) -> crate::cli_outcome::CommandOutcome {
use crate::cli_outcome::CommandOutcome;
use std::fmt::Write;
let cmd = "release gate-all";
let gate_count = passed.len() + failed.len();
// Empty-is-green violates RAGNARÖK (bug #14): a repo with NO configured gates
// (Gates derives Default with every flag false) ran zero gates — that is NOT a
// pass. Require at least one gate to have run AND none failed.
let all_pass = failed.is_empty() && gate_count > 0;
let failed_json: Vec<serde_json::Value> = failed
.iter()
.map(|(n, e)| serde_json::json!({ "name": n, "error": e }))
.collect();
let data = serde_json::json!({
"repo": repo,
"passed": passed,
"failed": failed_json,
"passed_count": passed.len(),
"failed_count": failed.len(),
"gate_count": gate_count,
"all_pass": all_pass,
});
let mut human = format!("=== gate-all: {repo} ===\n");
for n in passed {
let _ = writeln!(human, " ✓ {n}");
}
for (n, e) in failed {
let _ = writeln!(human, " ✗ {n}: {e}");
}
if gate_count == 0 {
let _ = write!(human, "NO gates configured — empty is not green");
} else if all_pass {
let _ = write!(human, "{} gate(s) passed", passed.len());
} else {
let _ = write!(human, "{} gate(s) failed", failed.len());
}
CommandOutcome {
command: cmd.into(),
ok: all_pass,
data,
human,
}
}
/// Release gate (plan #5): the repo's **test matrix** must be green. Takes the
/// repo's `test_results` rows (the SAME rows `nornir test history`/`Ops.
/// RunTestMatrix` produce + persist), rolls them into per-run summaries
/// (newest-first via [`summarize_runs`]), and fails the release if:
///
/// * there is NO recorded run (a release must have exercised its tests), or
/// * the newest run is RED (any `fail`/`stalled` case), or
/// * a REGRESSION vs the last green run: a test that passed in the last green
/// run is failing/stalled in the newest run (caught even if the newest run
/// also added brand-new failures — the regression is named explicitly).
///
/// Pure over already-collected rows so it's unit-testable and identical whether
/// the rows came from an embedded warehouse read or `Ops.RunTestMatrix`. The
/// heavy part (actually RUNNING the matrix) is the caller's job — this is the
/// verdict gate, mirroring `coverage_gate`.
pub fn tests_gate(rows: &[crate::warehouse::test_results::TestResultRow]) -> Result<()> {
use crate::warehouse::test_results::{status, summarize_runs};
let summaries = summarize_runs(rows);
let newest = summaries.first().ok_or_else(|| {
anyhow!("release tests gate RED — no test run recorded; run the matrix first")
})?;
// Names of the newest run's red (failing/stalled) cases, for the message.
let red_cases: Vec<String> = rows
.iter()
.filter(|r| r.run_id == newest.run_id && status::is_red(&r.status))
.map(|r| format!("{}::{}", r.suite, r.test_name))
.collect();
// Regression check: a test green in the last green run, red in the newest.
let regressions: Vec<String> =
if let Some(last_green) = summaries.iter().skip(1).find(|s| s.green()) {
use std::collections::BTreeSet;
let was_green: BTreeSet<(&str, &str)> = rows
.iter()
.filter(|r| r.run_id == last_green.run_id && status::is_green(&r.status))
.map(|r| (r.suite.as_str(), r.test_name.as_str()))
.collect();
rows.iter()
.filter(|r| r.run_id == newest.run_id && status::is_red(&r.status))
.filter(|r| was_green.contains(&(r.suite.as_str(), r.test_name.as_str())))
.map(|r| format!("{}::{}", r.suite, r.test_name))
.collect()
} else {
Vec::new()
};
if newest.green() {
return Ok(());
}
let mut msg = format!(
"release tests gate RED — newest run {} has {} failing/stalled case(s)",
crate::warehouse::test_results::short_run(&newest.run_id),
newest.failed + newest.stalled,
);
if !red_cases.is_empty() {
msg.push_str(&format!(": {}", red_cases.join(", ")));
}
if !regressions.is_empty() {
msg.push_str(&format!(
"; REGRESSION (was green last green run): {}",
regressions.join(", ")
));
}
Err(anyhow!(msg))
}
/// Plan #5 release test-DRIVE: actually RUN the repo's test matrix
/// (`run_full_matrix`) over `repo_root`, fold the fresh rows together with the
/// repo's persisted `history` rows, and apply [`tests_gate`]. The freshly-run
/// rows carry a newer `run_id`/`ts` than any history row (the matrix stamps a
/// new run id + `now()`), so they always lead in the newest-first summary — the
/// gate verdicts the run we just drove, and uses the history to catch a
/// regression vs the last green run.
///
/// HEAVY: this compiles + runs the suite. It is invoked release/on-request only
/// (the `[gates].tests` toggle), never on workspace load — same policy as the
/// rest of the heavy matrix. Returns the freshly-run rows on success so the
/// caller can persist them to the warehouse.
pub fn run_tests_gate(
repo_root: &Path,
history: &[crate::warehouse::test_results::TestResultRow],
aspects: &[crate::test_matrix::Aspect],
) -> Result<Vec<crate::warehouse::test_results::TestResultRow>> {
let fresh = crate::test_matrix::run_full_matrix(repo_root, aspects);
if fresh.is_empty() {
return Err(anyhow!(
"release tests gate RED — the matrix produced no rows for {} (nothing ran)",
repo_root.display()
));
}
// History first, fresh last — summarize_runs sorts newest-first by ts, so
// the just-run rows lead regardless of input order; this is belt-and-braces.
let mut all = history.to_vec();
all.extend(fresh.iter().cloned());
tests_gate(&all)?;
Ok(fresh)
}
/// Outcome of [`render_and_stage_docs`].
#[derive(Debug, Default)]
pub struct RenderStageReport {
/// Repo-relative paths that were (re)rendered AND `git add`-ed (only the ones
/// that actually changed on disk).
pub staged: Vec<std::path::PathBuf>,
/// `true` if the whole-documentation book PDF was (re)built + staged.
pub book_built: bool,
}
/// Release **render-and-stage** (the authoritative docs backstop).
///
/// Unlike the [`docs_fresh`](crate::config::Gates::docs_fresh) gate — which only
/// *checks* for drift and fails the release — this **renders** the managed docs
/// (README/CHANGELOG, with the full SVG/depgraph assets), fills the in-place
/// `.nornir/*.md` markers, builds the book PDF (under `docs-export`), and
/// `git add`s every artifact that changed. Running it *before* the `docs_fresh`
/// check guarantees a release always ships fresh docs and the subsequent check
/// passes (any remaining drift is a real bug, not just staleness).
///
/// Wire it where the other gates run; it is opt-in per repo via the same
/// `[repo.<name>.gates].docs_fresh` flag (rendering is the active half of the
/// freshness contract). The caller supplies a fully-built [`docs::Ctx`] (it owns
/// warehouse access — gates must not open the warehouse themselves).
///
/// Best-effort `git add`: a path that can't be staged (e.g. not a git repo) is
/// reported but does not fail the release — the `docs_fresh` check that follows
/// is the hard assertion.
pub fn render_and_stage_docs(
layout: &crate::docs::RepoLayout,
ctx: &crate::docs::Ctx,
) -> Result<RenderStageReport> {
let mut report = RenderStageReport::default();
let repo_root = layout.repo_root.clone();
// What this repo asks `render` to emit (`.nornir/docs.toml`; default markdown).
let cfg = crate::docs::DocsRenderCfg::load(layout)?;
let mut changed: Vec<std::path::PathBuf> = Vec::new();
if cfg.wants_markdown() {
// Managed docs (README/CHANGELOG) → repo root, with full assets.
for r in crate::docs::render_all(layout, ctx)? {
if r.changed {
changed.push(r.output.clone());
}
}
// In-place marker fills for the non-managed `.nornir/*.md` sources.
for r in crate::docs::render_sources_in_place(layout, ctx)? {
if r.changed {
changed.push(r.path.clone());
}
}
}
// The generated SVG/depgraph assets are tracked; stage the whole assets dir
// so a freshly (re)rendered chart/depgraph rides along with the commit.
let assets = layout.nornir_dir().join("assets");
if assets.exists() {
changed.push(assets);
}
// Whole-documentation book PDF (authoritative, heavy — release-only).
#[cfg(feature = "docs-export")]
if cfg.wants_pdf() {
let format = crate::docs::DocFormat::parse("pdf")?;
let (bytes, _sources) = crate::docs::build_book(&repo_root, ctx, format)?;
let out = layout.export_path("book", format.extension());
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)?;
}
let prev = std::fs::read(&out).ok();
if prev.as_deref() != Some(bytes.as_slice()) {
std::fs::write(&out, &bytes).with_context(|| format!("write {}", out.display()))?;
}
changed.push(out);
report.book_built = true;
}
#[cfg(not(feature = "docs-export"))]
let _ = &repo_root;
// Stage everything that changed (best-effort) — pure-gix `git add` via the
// `nornir-git` leaf (no subprocess). `stage_paths` takes repo-relative paths.
for path in changed {
if !path.exists() {
continue;
}
let rel = path.strip_prefix(&repo_root).unwrap_or(&path).to_path_buf();
let rel_str = rel.to_string_lossy().replace('\\', "/");
let staged = crate::gitops::stage_paths(&repo_root, &[rel_str.as_str()])
.map(|s| !s.is_empty())
.unwrap_or(false);
if staged {
report.staged.push(rel);
}
}
Ok(report)
}
#[cfg(test)]
mod render_stage_tests {
use super::*;
use tempfile::TempDir;
fn git_init(root: &Path) {
let ok = std::process::Command::new("git")
.arg("-C")
.arg(root)
.arg("init")
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(ok, "git init failed (is git installed?)");
}
fn staged_files(root: &Path) -> Vec<String> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["diff", "--cached", "--name-only"])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout)
.lines()
.map(|s| s.to_string())
.collect()
}
/// render_and_stage_docs RENDERS the managed README from its `.nornir/`
/// source, leaves NO drift (the `docs_fresh` check passes after), AND
/// `git add`s the regenerated artifact. Inject a known source body; assert
/// the rendered output carries it + the GENERATED header + is staged.
#[test]
fn renders_stages_and_leaves_no_drift() {
let t = TempDir::new().unwrap();
let root = t.path();
git_init(root);
let layout = crate::docs::RepoLayout::new(root);
std::fs::create_dir_all(layout.nornir_dir()).unwrap();
// Inject a known source with a unique sentinel.
let sentinel = "AUTOTRIGGER-RENDER-SENTINEL-42";
std::fs::write(
layout.source_of("README.md"),
format!("# Title\n\n{sentinel}\n"),
)
.unwrap();
let ctx = crate::docs::Ctx::new(root, root, None)
.with_path_dep_cloner(&crate::security::docs_path_dep_cloner);
// Precondition: the artifact is stale (absent) ⇒ the check would FAIL.
assert!(
crate::docs::render_check_all(&layout, &ctx).is_err(),
"before render, docs_fresh check must fail on the missing artifact"
);
// Render + stage.
let report = render_and_stage_docs(&layout, &ctx).unwrap();
// The README was rendered with the injected sentinel + the header.
let rendered = std::fs::read_to_string(layout.output_of("README.md")).unwrap();
assert!(
rendered.contains(sentinel),
"rendered README must carry the injected body"
);
assert!(
rendered.starts_with(crate::docs::layout::GENERATED_HEADER_PREFIX),
"rendered README must carry the GENERATED header"
);
// It was reported as staged AND is actually in the git index.
assert!(
report.staged.iter().any(|p| p.ends_with("README.md")),
"README.md should be reported staged; got {:?}",
report.staged
);
assert!(
staged_files(root).iter().any(|f| f == "README.md"),
"README.md must be in the git index"
);
// Postcondition: NO drift — the docs_fresh check now passes.
crate::docs::render_check_all(&layout, &ctx)
.expect("after render+stage, docs_fresh check must pass (no drift)");
}
}
#[cfg(test)]
mod cargo_pipeline_tests {
use super::*;
#[test]
fn coverage_gate_passes_green_fails_on_gap_or_stale() {
use nornir_testmatrix::coverage::{AllowEntry, Allowlist, GateReport};
use nornir_testmatrix::discover::{Surface, cli_commands};
use std::collections::BTreeSet;
let mut surface = Surface::new();
surface.extend(cli_commands(["a", "b"])); // 2 NA nodes
let all: BTreeSet<String> = ["cli_command:a@na", "cli_command:b@na"]
.iter()
.map(|s| s.to_string())
.collect();
// All covered + clean allowlist → green → Ok.
let green = GateReport::compute("r", "ws", &surface, &all, &Allowlist::new());
assert!(coverage_gate(&green).is_ok());
// One uncovered, un-allowlisted → gap → Err.
let covered_one: BTreeSet<String> = ["cli_command:a@na".to_string()].into_iter().collect();
let red = GateReport::compute("r", "ws", &surface, &covered_one, &Allowlist::new());
let err = coverage_gate(&red).unwrap_err().to_string();
assert!(err.contains("uncovered"), "{err}");
assert!(err.contains("b"), "names the missing surface: {err}");
// Stale allowlist entry (all covered, but an excuse lingers) → Err.
let stale_al = Allowlist {
entries: vec![AllowEntry {
key: "cli_command:a@na".into(),
reason: "old".into(),
}],
};
let stale = GateReport::compute("r", "ws", &surface, &all, &stale_al);
let err2 = coverage_gate(&stale).unwrap_err().to_string();
assert!(err2.contains("stale"), "{err2}");
}
#[test]
fn coverage_gate_outcome_ok_when_green_red_with_gap_data_when_failing() {
use nornir_testmatrix::coverage::{AllowEntry, Allowlist, GateReport};
use nornir_testmatrix::discover::{Surface, cli_commands};
use std::collections::BTreeSet;
let mut surface = Surface::new();
surface.extend(cli_commands(["a", "b"]));
let all: BTreeSet<String> = ["cli_command:a@na", "cli_command:b@na"]
.iter()
.map(|s| s.to_string())
.collect();
// GREEN gate → ok=true, sannr, real counts in data.
let green = GateReport::compute("r", "ws", &surface, &all, &Allowlist::new());
let og = coverage_gate_outcome("nornir", &green);
assert!(og.ok, "a passing gate is ok");
assert!(og.is_sannr(), "a passing gate carries real verdict data");
assert_eq!(og.command, "release gate coverage");
assert_eq!(og.data["passed"], serde_json::json!(true));
assert_eq!(og.data["missing_count"], serde_json::json!(0));
assert_eq!(og.data["total"], serde_json::json!(2));
// FAILING gate (one uncovered) → ok=false (RED), BUT the missing surface is in data.
let covered_one: BTreeSet<String> = ["cli_command:a@na".to_string()].into_iter().collect();
let red = GateReport::compute("r", "ws", &surface, &covered_one, &Allowlist::new());
let orr = coverage_gate_outcome("nornir", &red);
assert!(
!orr.ok,
"a failing gate is ok:false (the correct red verdict)"
);
assert!(!orr.is_sannr(), "a red gate is a red surface");
assert_eq!(orr.data["passed"], serde_json::json!(false));
assert_eq!(orr.data["missing_count"], serde_json::json!(1));
assert_eq!(orr.data["missing"], serde_json::json!(["b"]));
assert!(orr.human.contains("RED"));
// Stale allowlist entry → also a failing gate, named in data.
let stale_al = Allowlist {
entries: vec![AllowEntry {
key: "cli_command:a@na".into(),
reason: "old".into(),
}],
};
let stale = GateReport::compute("r", "ws", &surface, &all, &stale_al);
let os = coverage_gate_outcome("nornir", &stale);
assert!(!os.ok);
assert_eq!(os.data["stale_count"], serde_json::json!(1));
assert_eq!(os.data["stale"], serde_json::json!(["cli_command:a@na"]));
}
#[test]
fn gate_all_outcome_ok_when_all_pass_red_with_failure_data() {
// All gates passed → ok=true, sannr, the passed names in data.
let passed = vec!["no_path_patches".to_string(), "nexus_floor".to_string()];
let o = gate_all_outcome("nornir", &passed, &[]);
assert!(o.ok, "no failed gates ⇒ ok");
assert!(o.is_sannr(), "a green sweep carries the passed-gate list");
assert_eq!(o.command, "release gate-all");
assert_eq!(o.data["passed_count"], serde_json::json!(2));
assert_eq!(o.data["failed_count"], serde_json::json!(0));
assert_eq!(o.data["all_pass"], serde_json::json!(true));
assert!(o.human.contains("2 gate(s) passed"));
// One gate failed → ok=false (RED), with the failed gate + its error in data.
let failed = vec![("docs_fresh".to_string(), "README drifted".to_string())];
let r = gate_all_outcome("nornir", &["nexus_floor".to_string()], &failed);
assert!(!r.ok, "any failed gate ⇒ ok:false");
assert!(!r.is_sannr(), "a red sweep is a red surface");
assert_eq!(r.data["failed_count"], serde_json::json!(1));
assert_eq!(r.data["failed"][0]["name"], serde_json::json!("docs_fresh"));
assert_eq!(
r.data["failed"][0]["error"],
serde_json::json!("README drifted")
);
assert!(r.human.contains("✗ docs_fresh: README drifted"));
assert!(r.human.contains("1 gate(s) failed"));
// Bug #14: ZERO gates ran (no [gates] in the repo) → NOT green.
let empty = gate_all_outcome("nornir", &[], &[]);
assert!(!empty.ok, "empty gate set must not be green");
assert!(!empty.is_sannr(), "empty is not a true verdict");
assert_eq!(empty.data["gate_count"], serde_json::json!(0));
assert!(empty.human.contains("NO gates configured"));
}
#[test]
fn tests_gate_passes_green_fails_on_red_and_names_regression() {
use crate::warehouse::test_results::{TestResultRow, status};
// No run at all → RED (a release must have exercised its tests).
let err = tests_gate(&[]).unwrap_err().to_string();
assert!(err.contains("no test run"), "{err}");
// An all-green newest run → Ok.
let green = vec![
TestResultRow::unit("g1", "r", "r", "a::ok", status::PASS, 1.0, 100, ""),
TestResultRow::unit("g1", "r", "r", "a::also", status::PASS, 1.0, 100, ""),
];
tests_gate(&green).unwrap();
// Newest run is RED → Err naming the failing case.
let red = vec![
TestResultRow::unit("g1", "r", "r", "a::ok", status::PASS, 1.0, 100, ""),
TestResultRow::unit("r2", "r", "r", "a::ok", status::PASS, 1.0, 200, ""),
TestResultRow::unit("r2", "r", "r", "a::broke", status::FAIL, 1.0, 200, "boom"),
];
let err = tests_gate(&red).unwrap_err().to_string();
assert!(err.contains("RED"), "{err}");
assert!(err.contains("a::broke"), "names the failing case: {err}");
// A REGRESSION: a::ok was green in the last green run (g1), now fails in
// the newest run (r3) → Err must call out the regression explicitly.
let regressed = vec![
TestResultRow::unit("g1", "r", "r", "a::ok", status::PASS, 1.0, 100, ""),
TestResultRow::unit(
"r3",
"r",
"r",
"a::ok",
status::FAIL,
1.0,
300,
"now broken",
),
];
let err = tests_gate(®ressed).unwrap_err().to_string();
assert!(err.contains("REGRESSION"), "flags the regression: {err}");
assert!(err.contains("a::ok"), "names the regressed test: {err}");
// A stalled (hung) newest case is also RED.
let stalled = vec![TestResultRow::unit(
"s1",
"r",
"r",
"a::hang",
status::STALLED,
9e4,
400,
"no output 120s",
)];
assert!(tests_gate(&stalled).is_err(), "a stalled run is red");
}
}
#[cfg(test)]
mod core_saturation_tests {
use super::*;
use crate::bench::{BenchResult, BenchRun};
/// One bench result carrying an arbitrary set of `key=value` f64 metrics.
fn result(name: &str, kv: &[(&str, f64)]) -> BenchResult {
let mut r = BenchResult {
name: name.into(),
metrics: Default::default(),
..Default::default()
};
for (k, v) in kv {
r.metrics
.insert((*k).to_string(), serde_json::Value::from(*v));
}
r
}
fn run_with(results: Vec<BenchResult>) -> BenchRun {
BenchRun {
id: BenchRun::new_id(),
machine_facts: None,
date: "2026-01-01".into(),
timestamp: Some("2026-01-01T00:00:00+00:00".into()),
version: "0.1.0".into(),
machine: "ci".into(),
cores: 1,
results,
tests: Vec::new(),
}
}
/// A telemetry row where the bench's OWN peak occupancy is `proc_max` cores
/// of `n_cores`.
///
/// The host-wide columns are deliberately pinned to **fully saturated**
/// (`cores_busy_max = n_cores`, `cpu_pct_max = 99.9`) — the shape a loaded
/// shared box always produces. Any test that goes green here on a serial arm
/// is reading the host, not the bench. That is not a cosmetic choice: with
/// the old helper (which drove `cores_busy_max` from the same argument) it
/// was impossible to write a row that told the two apart.
fn telem_row(
bench: &str,
n_cores: u32,
proc_max: f64,
) -> crate::warehouse::iceberg::BenchTelemetryRow {
crate::warehouse::iceberg::BenchTelemetryRow {
run_id: "r".into(),
repo: "nornir".into(),
bench: bench.into(),
n_cores,
cpu_pct_avg: 99.9,
cpu_pct_max: 99.9,
cores_busy_avg: n_cores as f64,
cores_busy_max: n_cores,
mem_peak_mb: 0.0,
mem_pct_max: 0.0,
elapsed_ms: 1.0,
proc_cpu_ms: Some(proc_max * 1.0),
child_cpu_ms: Some(0.0),
proc_cores_avg: Some(proc_max),
proc_cores_max: Some(proc_max),
}
}
#[test]
fn core_saturation_law_fails_underused_passes_saturated() {
// 11/12 clears the 0.9 floor; 8/12 (the lbz2 example) is a bug.
assert!(core_saturation(&[telem_row("nornir.lgz", 12, 11.0)], 0.9).is_ok());
let err = core_saturation(&[telem_row("nornir.lbz2", 12, 8.0)], 0.9)
.unwrap_err()
.to_string();
assert!(err.contains("nornir.lbz2"), "names the offender: {err}");
assert!(err.contains("8.00/12"), "reports the ratio: {err}");
// The pathological lgz 2.7/12 case is also RED — and now expressible as
// the fraction it actually is, not rounded to a whole core.
assert!(core_saturation(&[telem_row("nornir.lgz", 12, 2.7)], 0.9).is_err());
}
/// **THE REGRESSION THIS GATE EXISTS TO NOT REPEAT (LAW 2).**
///
/// Every row here has the host pegged: `cores_busy_max == n_cores`,
/// `cpu_pct_max == 99.9`. That is what oden looked like on 2026-08-03 while
/// `gunnar.walk_1m` — strictly single-threaded — occupied 0.56 of one core
/// and this gate said PASS.
///
/// So: hand the gate a fully-saturated HOST and a serial BENCH, and require
/// RED. A gate reading `/proc/stat` cannot fail this test; that is the
/// point of it.
#[test]
fn core_saturation_is_not_fooled_by_a_saturated_host() {
// walk_1m, as measured: 0.56 cores of 32, host reading 32/32 busy.
let row = telem_row("gunnar.walk_1m", 32, 0.56);
assert_eq!(
row.cores_busy_max, 32,
"the host-wide column must be pegged"
);
assert!(
(row.cores_busy_max as f64) >= 0.9 * row.n_cores as f64,
"precondition: the OLD input would have PASSED this row"
);
let err = core_saturation(&[row], 0.9)
.expect_err("a 0.56-core arm on a 32-core box must be RED")
.to_string();
assert!(err.contains("gunnar.walk_1m"), "{err}");
assert!(err.contains("0.56/32"), "reports the real occupancy: {err}");
// clone_concurrent_64, as measured: genuinely 10.99–12.11 of 32 cores
// (saturation 0.37) where the published metric claimed 0.94. Still RED
// at the 0.9 floor — but now for the true reason, at the true number.
let err = core_saturation(&[telem_row("gunnar.clone_concurrent_64", 32, 11.5)], 0.9)
.expect_err("11.5/32 is under the 0.9 floor")
.to_string();
assert!(err.contains("11.50/32"), "{err}");
// And it passes once it really does saturate.
assert!(core_saturation(&[telem_row("gunnar.clone_concurrent_64", 32, 30.0)], 0.9).is_ok());
}
/// History compat: a row written before `proc_cores_max` existed carries no
/// process-scoped measurement. It must be SKIPPED as unjudgeable — neither
/// silently passed as green nor defaulted to `0.0` and reported as a RED
/// that never happened.
#[test]
fn legacy_row_without_proc_telemetry_is_skipped_not_invented() {
let mut legacy = telem_row("nornir.par", 12, 1.0);
legacy.proc_cpu_ms = None;
legacy.child_cpu_ms = None;
legacy.proc_cores_avg = None;
legacy.proc_cores_max = None;
assert!(
core_saturation(&[legacy.clone()], 0.9).is_ok(),
"an unmeasured row must not manufacture a RED"
);
// …but a measured sibling in the same batch is still judged, so the skip
// never swallows a real violation.
let err = core_saturation(&[legacy, telem_row("nornir.par2", 12, 1.0)], 0.9)
.expect_err("the measured row is still gated")
.to_string();
assert!(
err.contains("nornir.par2") && !err.contains("nornir.par "),
"{err}"
);
}
#[test]
fn core_saturation_exempts_single_thread_variants_and_single_core() {
// `_st` variant deliberately uses one core → exempt even at 1/12.
assert!(core_saturation(&[telem_row("ljar.encode_st", 12, 1.0)], 0.9).is_ok());
// The SAME bench WITHOUT the `_st` suffix at 1/12 IS a violation — proving
// it is the shared single-thread suffix (`crate::bench::is_single_thread`,
// the same notion the mashup keeps as a distinct row) that exempts it.
assert!(core_saturation(&[telem_row("ljar.encode", 12, 1.0)], 0.9).is_err());
// Degenerate single-core host → nothing to saturate.
assert!(core_saturation(&[telem_row("ljar.encode", 1, 1.0)], 0.9).is_ok());
// Empty telemetry (nothing measured) → trivially green.
assert!(core_saturation(&[], 0.9).is_ok());
// frac 0.0 falls back to the LAW default (0.9): 8/12 still RED.
assert!(core_saturation(&[telem_row("b", 12, 8.0)], 0.0).is_err());
}
#[test]
fn core_saturation_exempts_nornir_latency_microbenches() {
// dep_graph_build/_ext + delta_rescan are latency/IO microbenches (one
// `cargo metadata` spawn + warm closure queries), NOT parallel-compute —
// so 1/12 cores is correct, not an under-use bug (bencher O1). Exempt.
assert!(core_saturation(&[telem_row("nornir.dep_graph_build", 12, 1.0)], 0.9).is_ok());
assert!(core_saturation(&[telem_row("nornir.dep_graph_build_ext", 12, 1.0)], 0.9).is_ok());
assert!(core_saturation(&[telem_row("nornir.delta_rescan", 12, 1.0)], 0.9).is_ok());
// But the genuinely parallel platform arms stay under the LAW: an all-core
// `syn` scan that only used 1/12 cores is still a real violation.
assert!(core_saturation(&[telem_row("nornir.knowledge_scan", 12, 1.0)], 0.9).is_err());
assert!(is_latency_microbench("nornir.dep_graph_build"));
assert!(!is_latency_microbench("nornir.knowledge_scan"));
}
#[test]
fn core_saturation_run_reads_the_runs_telemetry_footprint() {
use crate::bench::telemetry::{Telemetry, inject_into_metrics};
// Host pegged at fully-busy in every case (see `telem_row`) so this
// exercises the metric-map hop with the SAME trap the warehouse path
// has: only `proc_cores_max` may move the verdict.
let mk = |name: &str, n: u32, proc_cores: f64| {
let mut m = serde_json::Map::new();
let t = Telemetry {
n_cores: n,
cpu_pct_avg: 99.9,
cpu_pct_max: 99.9,
cores_busy_avg: n as f64,
cores_busy_max: n,
mem_peak_mb: 0.0,
mem_pct_max: 0.0,
elapsed_ms: 1.0,
samples: 1,
proc_cpu_ms: Some(proc_cores),
child_cpu_ms: Some(0.0),
proc_cores_avg: Some(proc_cores),
proc_cores_max: Some(proc_cores),
..Default::default()
};
inject_into_metrics(&mut m, &t);
BenchResult {
name: name.into(),
metrics: m,
..Default::default()
}
};
// A saturated run passes; an under-used one trips the in-memory gate —
// even though the host looked 12/12 busy in BOTH.
assert!(core_saturation_run(&run_with(vec![mk("nornir.par", 12, 12.0)]), 0.9).is_ok());
assert!(core_saturation_run(&run_with(vec![mk("nornir.par", 12, 5.0)]), 0.9).is_err());
// A result with NO telemetry footprint is skipped (no false RED).
assert!(
core_saturation_run(&run_with(vec![result("plain", &[("x_mbs", 1.0)])]), 0.9).is_ok()
);
}
}
/// The CROSS-WORKSPACE completeness gate over a constructed [`MegaGateReport`] —
/// Wave-1 covered the single-workspace [`coverage_gate`] but deferred the mega
/// rollup. These build the report directly (the caller normally sweeps the
/// warehouse via `gather_all`; the gate itself is a pure verdict).
#[cfg(test)]
mod megagate_tests {
use super::*;
use crate::autonom::megagate::{MegaGateReport, WorkspaceGate};
use nornir_testmatrix::coverage::{AllowEntry, Allowlist, GateReport};
use nornir_testmatrix::discover::{Surface, cli_commands};
use std::collections::BTreeSet;
/// A per-workspace coverage [`GateReport`] over two CLI-command surfaces, with
/// `cover_all` toggling whether both are covered (green) or one is left as a gap.
fn report(ws: &str, cover_all: bool) -> GateReport {
let mut surface = Surface::new();
surface.extend(cli_commands(["a", "b"]));
let covered: BTreeSet<String> = if cover_all {
["cli_command:a@na", "cli_command:b@na"]
.iter()
.map(|s| s.to_string())
.collect()
} else {
["cli_command:a@na".to_string()].into_iter().collect()
};
GateReport::compute("r", ws, &surface, &covered, &Allowlist::new())
}
/// Every workspace green ⇒ the mega gate passes.
#[test]
fn all_green_workspaces_pass() {
let mega = MegaGateReport {
run_id: "run-1".into(),
workspaces: vec![
WorkspaceGate::from_report("nornir", report("nornir", true)),
WorkspaceGate::from_report("znippy", report("znippy", true)).with_reachable(3, 3),
],
};
assert!(mega.is_green());
assert!(coverage_gate_all_workspaces(&mega).is_ok());
}
/// A gap in ONE workspace reds the whole release, and the error names both the
/// offending workspace and the uncovered surface key.
#[test]
fn one_workspace_with_a_gap_fails_naming_it() {
let mega = MegaGateReport {
run_id: "run-2".into(),
workspaces: vec![
WorkspaceGate::from_report("nornir", report("nornir", true)),
WorkspaceGate::from_report("znippy", report("znippy", false)), // gap on `b`
],
};
assert!(!mega.is_green());
let err = coverage_gate_all_workspaces(&mega).unwrap_err().to_string();
assert!(err.contains("mega completeness gate RED"), "{err}");
assert!(
err.contains("znippy"),
"names the offending workspace: {err}"
);
assert!(err.contains('b'), "names the uncovered surface: {err}");
}
/// A stale allowlist entry (coverage otherwise green) also reds the mega gate.
#[test]
fn stale_allowlist_entry_fails() {
let mut surface = Surface::new();
surface.extend(cli_commands(["a", "b"]));
let all: BTreeSet<String> = ["cli_command:a@na", "cli_command:b@na"]
.iter()
.map(|s| s.to_string())
.collect();
let stale_al = Allowlist {
entries: vec![AllowEntry {
key: "cli_command:a@na".into(),
reason: "old".into(),
}],
};
let coverage = GateReport::compute("r", "nornir", &surface, &all, &stale_al);
let mega = MegaGateReport {
run_id: "run-3".into(),
workspaces: vec![WorkspaceGate::from_report("nornir", coverage)],
};
let err = coverage_gate_all_workspaces(&mega).unwrap_err().to_string();
assert!(err.contains("stale"), "{err}");
assert!(err.contains("nornir"), "{err}");
}
/// Coverage-green but a RESOLVED-metro shortfall (`green != total`) reds the
/// mega gate with the reachable-lines message.
#[test]
fn unmet_reachable_rollup_fails() {
let mega = MegaGateReport {
run_id: "run-4".into(),
workspaces: vec![
WorkspaceGate::from_report("nornir", report("nornir", true)).with_reachable(2, 3),
],
};
assert!(
!mega.is_green(),
"an unlit resolved metro line is not green"
);
let err = coverage_gate_all_workspaces(&mega).unwrap_err().to_string();
assert!(err.contains("2/3 resolved metro"), "{err}");
assert!(err.contains("nornir"), "{err}");
}
}
/// Names of the gates in [`selected_gates`] order — the canonical release suite.
pub const GATE_ORDER: &[&str] = &[
"no_path_patches",
"nexus_floor",
"no_regression",
"integration_roundtrip",
"docs_fresh",
"coverage",
"tests",
];
/// The ONE answer to "which gates does this repo's release suite run?".
///
/// Both release-readiness surfaces walk this list: `Release.GateAll` (the 🚦
/// Release-gates pane) and `Ops.RunRelease` (the 🚀 Release button / the
/// `nornir release gate all` thin path). They used to disagree — `gate_all`
/// honoured `[repo.<name>.gates]` while `run_release` fetched the same config,
/// threw it away with a `let _ = &repo_cfg;`, and ran exactly ONE hardcoded gate
/// (`no_path_patches`). A repo that had explicitly turned `no_path_patches` OFF
/// and turned other gates ON got the one gate it disabled and none of the ones
/// it asked for, then a summary reading "N/N repo(s) ready".
///
/// **The unconfigured floor.** An all-default `[gates]` table (every flag
/// `false`) means "no gates declared", not "this repo is exempt from review". A
/// suite that ran nothing there would report a green release verdict having
/// checked nothing at all. So an empty selection falls back to
/// [`no_path_patches`] — the cheapest gate, correct for every crate, and exactly
/// what `Ops.RunRelease` already ran. A repo that declares ANY gate gets its
/// declaration verbatim, including an explicit `no_path_patches = false`.
pub fn selected_gates(g: &crate::config::Gates) -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
if g.no_path_patches {
out.push("no_path_patches");
}
if g.nexus_floor {
out.push("nexus_floor");
}
if g.no_regression {
out.push("no_regression");
}
if !g.integration_roundtrip.is_empty() {
out.push("integration_roundtrip");
}
if g.docs_fresh {
out.push("docs_fresh");
}
if g.coverage {
out.push("coverage");
}
if g.tests {
out.push("tests");
}
if out.is_empty() {
out.push("no_path_patches");
}
out
}
#[cfg(test)]
mod selected_gates_tests {
use super::selected_gates;
use crate::config::Gates;
/// An undeclared `[gates]` table falls back to the floor gate — never to
/// "nothing ran, therefore ready".
#[test]
fn unconfigured_gates_fall_back_to_the_floor() {
assert_eq!(selected_gates(&Gates::default()), vec!["no_path_patches"]);
}
/// The regression this guards: a repo that turns `no_path_patches` OFF and
/// other gates ON must get ITS gates, not the one it disabled. This is the
/// exact shape of `workspace_holger/release/nornir.toml`'s `[repo.znippy.gates]`.
#[test]
fn an_explicit_off_is_honoured_and_declared_gates_are_selected() {
let g = Gates {
no_path_patches: false,
no_regression: true,
docs_fresh: true,
..Default::default()
};
let sel = selected_gates(&g);
assert!(
!sel.contains(&"no_path_patches"),
"a repo that disabled no_path_patches must not have it run: {sel:?}"
);
assert_eq!(sel, vec!["no_regression", "docs_fresh"]);
}
/// Every gate the config can turn on is reachable, and the order is the
/// declared suite order (cheap/pure gates before the heavy subprocess ones).
#[test]
fn every_gate_is_selectable_and_ordered() {
let g = Gates {
no_path_patches: true,
nexus_floor: true,
no_regression: true,
integration_roundtrip: vec!["roundtrip".into()],
docs_fresh: true,
coverage: true,
tests: true,
..Default::default()
};
assert_eq!(selected_gates(&g), super::GATE_ORDER.to_vec());
}
}
/// WAREHOUSE-BACKED bench-regression gate: seed `bench_runs` in a real temporary
/// [`IcebergWarehouse`], read the history back with `query_bench_runs_async`, and
/// run [`no_regression_against`] over it — closing the "read back from Iceberg"
/// path the doc-comment describes (Wave-1's gate tests were all in-memory slices).
#[cfg(test)]
mod bench_warehouse_tests {
use super::*;
use crate::bench::{BenchResult, BenchRun};
use crate::warehouse::BenchFilter;
use crate::warehouse::iceberg::IcebergWarehouse;
fn run(machine: &str, ts: &str, mbs: f64) -> BenchRun {
let mut r = BenchResult {
name: "codec".into(),
metrics: Default::default(),
..Default::default()
};
r.metrics
.insert("throughput_mbs".into(), serde_json::Value::from(mbs));
BenchRun {
id: BenchRun::new_id(),
machine_facts: None,
date: ts[..10].to_string(),
timestamp: Some(ts.to_string()),
version: "0.1.0".into(),
machine: machine.into(),
cores: 1,
results: vec![r],
tests: Vec::new(),
}
}
/// Persist a jittery baseline for `ci`, read it back, and gate: a within-
/// threshold candidate passes and a beyond-threshold one fails, verdict driven
/// entirely off the warehouse-materialised history.
#[test]
fn gate_over_warehouse_history_passes_and_fails() {
let dir = tempfile::tempdir().unwrap();
let wh = IcebergWarehouse::open(dir.path()).unwrap();
// Baseline: newest same-machine run is 100 MB/s (the pick_baseline anchor).
for (ts, mbs) in [
("2026-02-01T00:00:00+00:00", 90.0),
("2026-02-02T00:00:00+00:00", 100.0),
] {
wh.block_on(wh.append_bench_run_async("znippy", &run("ci", ts, mbs)))
.unwrap();
}
// A different machine's run must NOT pollute the ci baseline.
wh.block_on(wh.append_bench_run_async(
"znippy",
&run("other-box", "2026-02-03T00:00:00+00:00", 999.0),
))
.unwrap();
let history = wh
.block_on(wh.query_bench_runs_async(&BenchFilter::for_repo("znippy")))
.unwrap();
assert!(
history.len() >= 3,
"all persisted runs read back: {}",
history.len()
);
// 5% drop vs the newest ci baseline (100 → 95) within a 10% threshold → ok.
let ok_candidate = run("ci", "2026-03-01T00:00:00+00:00", 95.0);
assert!(no_regression_against(&ok_candidate, &history, 10.0).is_ok());
// 30% drop (100 → 70) beyond the 10% threshold → regression.
let bad_candidate = run("ci", "2026-03-02T00:00:00+00:00", 70.0);
let err = no_regression_against(&bad_candidate, &history, 10.0)
.unwrap_err()
.to_string();
assert!(err.contains("regression"), "got: {err}");
}
/// First run for a machine with no prior warehouse history bootstraps the
/// baseline (nothing to regress against) ⇒ the gate passes.
#[test]
fn no_warehouse_baseline_for_machine_passes() {
let dir = tempfile::tempdir().unwrap();
let wh = IcebergWarehouse::open(dir.path()).unwrap();
wh.block_on(wh.append_bench_run_async(
"znippy",
&run("other-box", "2026-02-01T00:00:00+00:00", 500.0),
))
.unwrap();
let history = wh
.block_on(wh.query_bench_runs_async(&BenchFilter::for_repo("znippy")))
.unwrap();
let candidate = run("fresh-ci", "2026-03-01T00:00:00+00:00", 10.0);
assert!(
no_regression_against(&candidate, &history, 10.0).is_ok(),
"no baseline ⇒ pass"
);
}
}
#[cfg(test)]
mod semantic_link_gate_tests {
use super::*;
use nornir_release_core::semantic_blast::{CallSite, ChangeKind, SemanticBlast, SymbolImpact};
use std::collections::BTreeMap;
fn site(cr: &str) -> CallSite {
CallSite {
crate_name: cr.into(),
caller: format!("{cr}::mod::f"),
callee: "gone".into(),
call_kind: "call".into(),
file: format!("{cr}/src/lib.rs"),
line: 10,
}
}
fn blast(repo: &str, symbol: &str, change: ChangeKind, sites: usize) -> SemanticBlast {
let call_sites: Vec<CallSite> = (0..sites).map(|_| site("korp")).collect();
SemanticBlast {
target_repo: repo.into(),
target_crates: vec![repo.into()],
source: "resolved/scip".into(),
impacts: vec![SymbolImpact {
symbol: symbol.into(),
change,
call_sites,
}],
unused_changes: vec![],
total_call_sites: sites,
affected_crates: vec!["korp".into()],
}
}
/// A REMOVED symbol with a live call site FAILS the gate (link error); the error
/// message names the symbol and the failing call site.
#[test]
fn removed_symbol_with_call_site_fails_like_a_linker() {
let mut map: BTreeMap<String, SemanticBlast> = BTreeMap::new();
map.insert(
"facett".into(),
blast("facett", "render", ChangeKind::Removed, 2),
);
let res = semantic_link_gate(&map);
let failed = res.is_err();
let msg = res.err().map(|e| e.to_string()).unwrap_or_default();
let names_symbol = msg.contains("facett::render") && msg.contains("LINK ERROR");
nornir_testmatrix::functional_status(
"release-gate",
"semantic_link_gate_fails_on_removed",
failed && names_symbol,
&format!("failed={failed} msg_head={:?}", msg.lines().next()),
);
assert!(
failed,
"a removed symbol with a live call site must FAIL the release"
);
assert!(
names_symbol,
"the error names the removed symbol + LINK ERROR: {msg}"
);
}
/// A merely CHANGED symbol (still resolves) is a warning, not a link error → the
/// gate PASSES; and an empty blast passes trivially.
#[test]
fn changed_symbol_and_empty_blast_pass() {
let mut map: BTreeMap<String, SemanticBlast> = BTreeMap::new();
map.insert(
"facett".into(),
blast("facett", "open", ChangeKind::Changed, 3),
);
let changed_passes = semantic_link_gate(&map).is_ok();
let empty_passes = semantic_link_gate(&BTreeMap::new()).is_ok();
nornir_testmatrix::functional_status(
"release-gate",
"semantic_link_gate_passes_on_changed_and_empty",
changed_passes && empty_passes,
&format!("changed_passes={changed_passes} empty_passes={empty_passes}"),
);
assert!(
changed_passes,
"a still-resolving Changed symbol is not a link error"
);
assert!(
empty_passes,
"no semantic input ⇒ nothing to link-check ⇒ pass"
);
}
/// A removed symbol that NO dependent calls (empty call_sites) is not a link error.
#[test]
fn removed_but_uncalled_symbol_passes() {
let mut map: BTreeMap<String, SemanticBlast> = BTreeMap::new();
map.insert(
"facett".into(),
blast("facett", "secret", ChangeKind::Removed, 0),
);
assert!(
semantic_link_gate(&map).is_ok(),
"removing a symbol nobody calls links fine"
);
}
}