crates/nornir-release-core/src/gate.rs
//! # `gate` — the release QUALITY GATES, hosted (fat/compiled-in) in dwarves
//!
//! Relocated from `nornir/src/release/gate.rs` (DOCTRINE 2026-07-14 — *release is a
//! build op*): the forge OWNS the release gate core instead of reaching it over gRPC.
//! Only the gates that are PURE (filesystem/manifest/process) or backed by an EXISTING
//! edda leaf move here; the warehouse-/nornir-domain gates STAY in nornir (see below).
//!
//! Each gate returns `Ok(())` on pass, `Err` on fail. The generated release binaries
//! propagate the first error and abort the release.
//!
//! ## Hosted here (relocated)
//! * [`no_path_patches`] — no LOCAL/forked `[patch.crates-io]` (pure manifest scan).
//! * [`path_dep_audit`] / [`gate_path_dep_versions`] — every published `path =` dep
//! carries a `version =` (pure `toml`).
//! * [`crate_metadata_check`] / [`gate_crate_metadata`] — readme/license/repository/
//! description present on every publishable crate (pure `toml`).
//! * [`links_declarations_scan`] + [`detect_links_conflicts`] / [`gate_links_conflicts`]
//! — duplicate `links =` keys in the resolved tree (`cargo_metadata`).
//! * [`gate_edition`] — Rust-2024 / cargo-standards, wired to the relocated
//! [`crate::edition`] gate.
//! * [`gate_guard_intact`] — the tamper-evident guard manifest, via the shared
//! [`nornir_guard`] leaf.
//! * [`unavailable_requirement_scan`] — ADVISORY: requirements pinning a concrete version
//! the registry does not have (never published, or yanked). Not a blocker; its real use
//! is naming everyone who still asks for a version right after it is yanked.
//! * [`registry_reach_scan`] / [`gate_registry_reach`] — no workspace member may reach a
//! SIBLING MEMBER by registry version instead of by `path` (the edge that links one
//! crate twice at two versions and silently splits its types).
//! * [`gate_source_drift`] / [`gate_source_drift_with`] / [`gate_source_drift_verdicts`]
//! — the SAME-VERSION drift gate: a publishable crate may not carry EDITED source
//! under a version already live on crates.io (one version number, two artifacts).
//! Wraps the [`crate::source_drift`] detector, which existed but had never been wired
//! to a blocker — the gap that let `nornir-build-thing@0.1.3` drift to 3.4× its
//! published size unnoticed.
//! * [`integration_roundtrip`] / [`integration_roundtrip_via_cargo_test`] — the
//! `cargo test --test roundtrip_*` driver (pure process/cargo).
//! * [`nexus_floor`] — holger ops/sec ≥ nexus ops/sec (pure, over `nornir_bench::BenchRun`).
//! * [`no_regression`] / [`no_regression_against`] / [`no_regression_windowed`] — the
//! no-regression gate family (pure, over `nornir_bench::BenchRun` slices).
//! * [`git_tag_matches_published`] — the post-publish "shipped but forgot to tag" check
//! (pure, over the shared `nornir_git` leaf). Absorbed 2026-07-22.
//! * [`gate`] — the name→gate dispatcher for the repo-root gates; it returns a clear
//! [`GateDispatch::HandledInNornir`] for the deferred gates and
//! [`GateDispatch::NeedsInput`] for the gates that need bench/roundtrip inputs.
//!
//! ## DE-FORK 2026-07-22 — this is now the ONLY copy
//! The 2026-07-14 relocation COPIED these gates here but never deleted nornir's, so the
//! two drifted for a week: nornir kept the richer bug-provenance comments (bug #11
//! dev-deps, issue #13 static rows, the facett 26 → 95 dot-dir sweep) while dwarves
//! gained the real functional advances ([`PathDepFinding::consumer`], `license-file` in
//! [`crate_metadata_check`], the `gate_*` wrappers + [`GateDispatch`]). BOTH sides are
//! now merged HERE, and `nornir/src/release/gate.rs` re-exports this module instead of
//! carrying a second implementation (move=delete).
//!
//! ## Deferred (STAY in nornir — warehouse/domain-coupled, per L5a)
//! * `docs_fresh` / `render_and_stage_docs` — call `crate::docs::*` (the render engine
//! nornir OWNS).
//! * `coverage` / `coverage_gate` / `coverage_gate_all_workspaces` (megagate) — call
//! `crate::autonom` (the completeness/coverage engine nornir OWNS).
//! * `core_saturation` — reasons over the warehouse `BenchTelemetryRow`, which is NOT a
//! leaf type and is consumed across nornir's viz/warehouse/server; moving it would
//! require editing `nornir/src`. Left in nornir (its only consumer).
//!
//! ## Import relocation deltas vs nornir's `gate.rs`
//! `crate::bench::{history, BenchRun}` → [`nornir_bench`]; `crate::release::cargo` →
//! [`crate::cargo`]; `crate::release::edition` → [`crate::edition`]; `crate::guard` →
//! [`nornir_guard`].
use std::path::Path;
use anyhow::{anyhow, Context, Result};
use nornir_bench::{history, BenchRun};
// ─── path_patches — no LOCAL/forked `[patch.crates-io]` ─────────────────────
/// Gate: no LOCAL/forked `[patch.crates-io]` entries in the repo's `Cargo.toml`. A
/// `path =` or `git =` patch source is stripped by cargo on publish, so the published
/// crate would resolve the stock registry version — incompatible with whatever the fork
/// changed. A `version =` patch (a registry pin) IS publishable and is NOT flagged.
///
/// Flags any non-comment patch entry whose source is `path=`/`git=`, across the bare
/// `[patch.crates-io]`, quoted `[patch."crates-io"]`, and sub-table
/// `[patch.crates-io.<dep>]` forms. Textual scan (keeps the offending line number).
pub fn no_path_patches(repo_root: &Path) -> Result<()> {
let cargo = repo_root.join("Cargo.toml");
let text = std::fs::read_to_string(&cargo)
.with_context(|| format!("read {}", cargo.display()))?;
let mut in_patch = false;
for (i, line) in text.lines().enumerate() {
let l = line.trim();
if l.starts_with('#') {
continue;
}
if l.starts_with('[') {
in_patch = l.starts_with("[patch.crates-io")
|| l.starts_with("[patch.\"crates-io\"");
continue;
}
if in_patch && mentions_local_patch_source(l) {
return Err(anyhow!(
"[patch.crates-io] local/forked patch (path=/git=) at {}:{} — \
stripped on publish; strip before release",
cargo.display(),
i + 1
));
}
}
Ok(())
}
/// True when a `[patch.crates-io]` entry line declares a `path =` or `git =` source (a
/// local/forked patch), as opposed to a registry `version =` pin. The key must be a
/// standalone token immediately followed by `=`, so a crate named `pathfinder` or a
/// value like `"0.1.0-git"` does not false-match. Comment lines are the caller's
/// responsibility to skip.
fn mentions_local_patch_source(l: &str) -> bool {
for key in ["path", "git"] {
let mut search_from = 0;
while let Some(rel) = l[search_from..].find(key) {
let pos = search_from + rel;
let before_is_ident = l[..pos]
.chars()
.next_back()
.map(|c| c.is_alphanumeric() || c == '_' || c == '-')
.unwrap_or(false);
let after_eq = l[pos + key.len()..].trim_start().starts_with('=');
if !before_is_ident && after_eq {
return true;
}
search_from = pos + key.len();
}
}
false
}
// ─── path_dep_versions — every published path dep carries a version= ────────
/// One audit finding from [`path_dep_audit`].
#[derive(Debug, Clone)]
pub struct PathDepFinding {
pub manifest: std::path::PathBuf,
/// The CONSUMING crate — the `[package].name` of the manifest declaring this path dep
/// (so the gate message can name WHICH crate carries the offending dep).
pub consumer: String,
pub dep_name: String,
pub dep_path: String,
pub has_version: bool,
pub version_req: Option<String>,
}
impl PathDepFinding {
pub fn ok(&self) -> bool {
self.has_version
}
}
/// Gate: every `path =` dep in every published `Cargo.toml` in the workspace must also
/// carry a `version =` field. Otherwise `cargo publish` rejects the manifest because the
/// patched-out path-dep loses its version constraint in the uploaded `.crate`.
///
/// cargo strips `dev-dependencies` from the published `.crate`, so a version-less path
/// dev-dep is LEGAL and must not be flagged — only `dependencies` and
/// `build-dependencies` are audited. A `publish = false` member is skipped entirely.
pub fn path_dep_audit(repo_root: &Path) -> Result<Vec<PathDepFinding>> {
let mut findings = Vec::new();
const AUDITED: [&str; 2] = ["dependencies", "build-dependencies"];
for manifest in walk_cargo_tomls(repo_root)? {
let text = std::fs::read_to_string(&manifest)
.with_context(|| format!("read {}", manifest.display()))?;
let doc: toml::Value = toml::from_str(&text)
.with_context(|| format!("parse {}", manifest.display()))?;
if is_publish_false(&doc) {
continue;
}
// The consuming crate's own name (fallback to the manifest's parent dir), so a
// finding can name WHICH crate declares the version-less path dep.
let consumer = doc
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
manifest
.parent()
.and_then(|d| d.file_name())
.and_then(|n| n.to_str())
.unwrap_or("?")
.to_string()
});
for section in AUDITED {
collect_path_deps(&doc, &consumer, section, &manifest, &mut findings);
}
if let Some(targets) = doc.get("target").and_then(|t| t.as_table()) {
for (_cfg, t) in targets {
for section in AUDITED {
collect_path_deps(t, &consumer, section, &manifest, &mut findings);
}
}
}
}
Ok(findings)
}
/// [`path_dep_audit`] rolled up to a pass/fail verdict: any version-less published path
/// dep reds the gate.
pub fn gate_path_dep_versions(repo_root: &Path) -> Result<()> {
let findings = path_dep_audit(repo_root)?;
let bad: Vec<&PathDepFinding> = findings.iter().filter(|f| !f.ok()).collect();
if !bad.is_empty() {
// Surface the specific offenders (consumer crate + dep name + dep path) so the
// operator can fix them directly, instead of a bare count that names nobody, e.g.
// "knut-popviz: path-dep `znippy-zoomies` (../../../znippy-zoomies) missing version=".
let detail = bad
.iter()
.map(|f| {
format!(
"{}: path-dep `{}` ({}) missing version=",
f.consumer, f.dep_name, f.dep_path
)
})
.collect::<Vec<_>>()
.join("; ");
return Err(anyhow!("{} path-dep(s) missing version= — {detail}", bad.len()));
}
Ok(())
}
/// True when a manifest's `[package].publish` is `false` or an empty array (both mean
/// "never published") — such a member is excluded from publish-time gates.
fn is_publish_false(doc: &toml::Value) -> bool {
match doc.get("package").and_then(|p| p.get("publish")) {
Some(toml::Value::Boolean(false)) => true,
Some(toml::Value::Array(a)) => a.is_empty(),
_ => false,
}
}
fn collect_path_deps(
parent: &toml::Value,
consumer: &str,
section: &str,
manifest: &Path,
out: &mut Vec<PathDepFinding>,
) {
let Some(deps) = parent.get(section).and_then(|d| d.as_table()) else {
return;
};
for (name, v) in deps {
let Some(t) = v.as_table() else { continue };
let path = t.get("path").and_then(|p| p.as_str());
let Some(p) = path else { continue };
let version = t.get("version").and_then(|v| v.as_str()).map(|s| s.to_string());
out.push(PathDepFinding {
manifest: manifest.to_path_buf(),
consumer: consumer.to_string(),
dep_name: name.clone(),
dep_path: p.to_string(),
has_version: version.is_some(),
version_req: version,
});
}
}
/// **Every `Cargo.toml` under `root`** — re-exported from the leaf that owns it.
///
/// This was thirty duplicated lines of `read_dir` (LAW 5). It now lives in
/// `nornir-feature-graph`, which needs the identical walk to derive
/// `feature_dep_edges` and is a true leaf, so the heavy end of the dependency
/// (gix, ureq, tar, cargo_metadata) does not have to be dragged along to get a
/// directory listing. One writer, not two copies a guard has to watch agree.
fn walk_cargo_tomls(root: &Path) -> Result<Vec<std::path::PathBuf>> {
nornir_feature_graph::walk_cargo_tomls(root).map_err(|e| anyhow::anyhow!("{e}"))
}
// ─── crate_metadata — readme/license/repository/description present ─────────
/// One metadata-check row from [`crate_metadata_check`].
#[derive(Debug, Clone)]
pub struct CrateMetaCheck {
pub manifest: std::path::PathBuf,
pub crate_name: String,
pub version: String,
pub has_readme: bool,
pub has_license: bool,
pub license_expr: Option<String>,
/// A `license-file = "…"` entry — crates.io accepts EITHER `license` or `license-file`.
pub has_license_file: bool,
pub has_repository: bool,
pub repository_url: Option<String>,
pub has_description: bool,
pub description_len: Option<usize>,
}
impl CrateMetaCheck {
/// crates.io / `cargo publish` REQUIRES a `description` and a license (either a
/// `license` SPDX expression or a `license-file`). A publishable crate missing either
/// is a HARD blocker — it cannot reach crates.io.
pub fn publish_ready(&self) -> bool {
self.has_description && (self.has_license || self.has_license_file)
}
/// nordisk-RECOMMENDED (but NOT crates.io-required) metadata: `readme` and
/// `repository`. Missing ones are advisory, never a publish blocker.
pub fn recommended_complete(&self) -> bool {
self.has_readme && self.has_repository
}
/// Every field present — publish-required AND nordisk-recommended.
pub fn ok(&self) -> bool {
self.publish_ready() && self.recommended_complete()
}
}
/// Gather each publishable crate's `[package]` metadata presence — `description`,
/// `license`/`license-file`, `readme`, `repository`. [`gate_crate_metadata`] decides which
/// missing fields are HARD (publish-required) vs ADVISORY (recommended).
/// Resolves `field.workspace = true` inherits against the root `[workspace.package]`.
/// Skips workspace virtual roots (no `[package]`) and any manifest with `publish = false`.
pub fn crate_metadata_check(repo_root: &Path) -> Result<Vec<CrateMetaCheck>> {
let manifests = walk_cargo_tomls(repo_root)?;
// First pass: capture the workspace root's `[workspace.package]` fields, since
// members commonly inherit metadata via `license.workspace = true` etc.
let mut ws: std::collections::BTreeMap<String, String> = Default::default();
for manifest in &manifests {
let Ok(text) = std::fs::read_to_string(manifest) else { continue };
let Ok(doc) = toml::from_str::<toml::Value>(&text) else { continue };
if let Some(wp) = doc
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.as_table())
{
for k in ["version", "license", "license-file", "repository", "description", "readme"] {
if let Some(v) = wp.get(k).and_then(|v| v.as_str()) {
ws.entry(k.to_string()).or_insert_with(|| v.to_string());
}
if k == "readme" {
if let Some(true) = wp.get(k).and_then(|v| v.as_bool()) {
ws.entry(k.to_string()).or_insert_with(|| "README.md".to_string());
}
}
}
}
}
let resolve = |field: Option<&toml::Value>, key: &str| -> Option<String> {
match field {
Some(toml::Value::String(s)) if !s.is_empty() => Some(s.clone()),
Some(toml::Value::Boolean(true)) if key == "readme" => Some("README.md".to_string()),
Some(toml::Value::Table(t)) if t.get("workspace").and_then(|w| w.as_bool()) == Some(true) => {
ws.get(key).cloned()
}
_ => None,
}
};
let mut out = Vec::new();
for manifest in &manifests {
let text = std::fs::read_to_string(manifest)
.with_context(|| format!("read {}", manifest.display()))?;
let doc: toml::Value = toml::from_str(&text)
.with_context(|| format!("parse {}", manifest.display()))?;
let Some(pkg) = doc.get("package").and_then(|p| p.as_table()) else { continue };
if pkg.get("publish").and_then(|p| p.as_bool()) == Some(false) {
continue;
}
let crate_name = pkg.get("name").and_then(|n| n.as_str()).unwrap_or("?").to_string();
let version = resolve(pkg.get("version"), "version").unwrap_or_else(|| "0.0.0".to_string());
let license_expr = resolve(pkg.get("license"), "license");
let license_file = resolve(pkg.get("license-file"), "license-file");
let repo_url = resolve(pkg.get("repository"), "repository");
let desc = resolve(pkg.get("description"), "description");
let readme_ok = resolve(pkg.get("readme"), "readme").is_some();
out.push(CrateMetaCheck {
manifest: manifest.clone(),
crate_name,
version,
has_readme: readme_ok,
has_license: license_expr.as_ref().map(|s| !s.is_empty()).unwrap_or(false),
license_expr,
has_license_file: license_file.as_ref().map(|s| !s.is_empty()).unwrap_or(false),
has_repository: repo_url.as_ref().map(|s| !s.is_empty()).unwrap_or(false),
repository_url: repo_url,
has_description: desc.as_ref().map(|s| !s.is_empty()).unwrap_or(false),
description_len: desc.as_ref().map(|s| s.len()),
});
}
Ok(out)
}
/// [`crate_metadata_check`] rolled up to a pass/fail verdict, split by what crates.io
/// ACTUALLY requires:
///
/// * HARD — a publishable crate missing a crates.io publish-REQUIRED field (`description`,
/// and a license via `license` or `license-file`) reds the gate. These genuinely block
/// `cargo publish`.
/// * ADVISORY — a crate that IS publish-ready but is missing a nordisk-RECOMMENDED field
/// (`readme`, `repository`) is reported as a warning note; the gate STILL PASSES, since
/// crates.io does not require these.
pub fn gate_crate_metadata(repo_root: &Path) -> Result<()> {
let checks = crate_metadata_check(repo_root)?;
// HARD: crates.io publish requirements (description + license/license-file).
let missing_required: Vec<&CrateMetaCheck> =
checks.iter().filter(|c| !c.publish_ready()).collect();
if !missing_required.is_empty() {
let detail = missing_required
.iter()
.map(|c| {
let mut fields = Vec::new();
if !c.has_description {
fields.push("description");
}
if !c.has_license && !c.has_license_file {
fields.push("license/license-file");
}
format!("{} (missing {})", c.crate_name, fields.join(", "))
})
.collect::<Vec<_>>()
.join("; ");
return Err(anyhow!(
"{} crate(s) MISSING publish-required metadata (description + license/license-file): {detail}",
missing_required.len(),
));
}
// ADVISORY: nordisk-recommended metadata (readme, repository) — a NOTE, never a block.
let missing_recommended: Vec<&CrateMetaCheck> = checks
.iter()
.filter(|c| !c.recommended_complete())
.collect();
if !missing_recommended.is_empty() {
let detail = missing_recommended
.iter()
.map(|c| {
let mut fields = Vec::new();
if !c.has_readme {
fields.push("readme");
}
if !c.has_repository {
fields.push("repository");
}
format!("{} (missing {})", c.crate_name, fields.join(", "))
})
.collect::<Vec<_>>()
.join("; ");
eprintln!(
"⚠ advisory: {} crate(s) missing recommended metadata (readme/repository) — \
NOT a publish blocker: {detail}",
missing_recommended.len(),
);
}
Ok(())
}
// ─── links_conflicts — duplicate `links =` keys in the resolved tree ────────
/// One link-declaration record from [`links_declarations_scan`].
#[derive(Debug, Clone)]
pub struct LinkDecl {
pub crate_name: String,
pub version: String,
pub links_value: String,
pub manifest: std::path::PathBuf,
}
/// One conflict surfaced by [`detect_links_conflicts`].
#[derive(Debug, Clone)]
pub struct LinksConflict {
pub links_value: String,
pub crates: Vec<(String, String)>, // (crate_name, version)
}
/// Collect every `links =` declaration in the resolved dep tree via `cargo metadata`.
/// Multiple crates with the same `links` key cause cryptic linker failures at build
/// time — detect them up front.
pub fn links_declarations_scan(repo_root: &Path) -> Result<Vec<LinkDecl>> {
let meta = cargo_metadata::MetadataCommand::new()
.current_dir(repo_root)
.exec()
.with_context(|| format!("cargo metadata in {}", repo_root.display()))?;
let mut out = Vec::new();
for pkg in &meta.packages {
if let Some(links) = &pkg.links {
out.push(LinkDecl {
crate_name: pkg.name.to_string(),
version: pkg.version.to_string(),
links_value: links.clone(),
manifest: pkg.manifest_path.clone().into_std_path_buf(),
});
}
}
Ok(out)
}
/// Bucket the `links =` declarations by value and surface any value claimed by more than
/// one distinct crate.
pub fn detect_links_conflicts(decls: &[LinkDecl]) -> Vec<LinksConflict> {
use std::collections::BTreeMap;
let mut buckets: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
for d in decls {
buckets
.entry(d.links_value.clone())
.or_default()
.push((d.crate_name.clone(), d.version.clone()));
}
buckets
.into_iter()
.filter(|(_, c)| c.iter().map(|(n, _)| n).collect::<std::collections::HashSet<_>>().len() > 1)
.map(|(links_value, crates)| LinksConflict { links_value, crates })
.collect()
}
/// [`links_declarations_scan`] + [`detect_links_conflicts`] rolled up to a pass/fail
/// verdict: any duplicate `links=` key reds the gate.
pub fn gate_links_conflicts(repo_root: &Path) -> Result<()> {
let decls = links_declarations_scan(repo_root)?;
let conflicts = detect_links_conflicts(&decls);
if !conflicts.is_empty() {
return Err(anyhow!("{} links= conflict(s) detected", conflicts.len()));
}
Ok(())
}
// ─── edition — Rust-2024 / cargo-standards (wired to crate::edition) ───────
/// Gate: the repo must pass the relocated Rust-2024 / cargo-standards
/// [`crate::edition`] check — clean static findings AND no `rust-2024-compatibility`
/// lints. HEAVY (the dynamic pass runs `cargo check`); release/on-request only.
pub fn gate_edition(repo_root: &Path) -> Result<()> {
let report = crate::edition::run_gate(repo_root)?;
if report.is_clean() {
return Ok(());
}
Err(anyhow!(
"edition gate RED — {} static finding(s), {} rust-2024-compatibility lint(s)",
report.static_findings.len(),
report.lints.len(),
))
}
// ─── git_tag_matches_published — the "shipped but forgot to tag" gate ──────
/// Gate (feature 16): the `v<version>` git tag for the most-recently published crate
/// version must exist locally and point at HEAD. Run AFTER a successful `cargo publish`
/// to catch the "shipped to crates.io but forgot to tag" failure mode.
///
/// Pure-Rust via the shared [`nornir_git`] leaf (gix); annotated tags are peeled to
/// their target commit before the HEAD comparison.
///
/// Relocated from `nornir/src/release/gate.rs` (2026-07-22 de-fork): it is a
/// POST-PUBLISH check on a publish dwarves performs, so it belongs with the publisher.
/// It had no nornir caller — the only consumer is the `git_tag_matches_published`
/// column of the release-report schema ([`crate::schema`]), which dwarves owns too.
///
/// **The tag is `<crate>-v<version>`**, the one dwarves `crate-release` creates per
/// published crate (dwarves `agent/crate-tags-only` f79c342, owner ruling 2026-09-22:
/// never a bare `v<version>`). This gate used to look for the bare `v<version>`, so
/// once the publisher stopped minting it, every correctly tagged release would have
/// read `false`. A bare tag no longer satisfies it either: on a many-crate repo a bare
/// `v0.1.17` says nothing about WHICH crate shipped.
pub fn git_tag_matches_published(repo_root: &Path, krate: &str, version: &str) -> Result<bool> {
nornir_git::gitio::tag_points_at_head(repo_root, &release_tag_name(krate, version))
}
/// The release tag name for `krate` at `version`: `<crate>-v<version>`. One writer,
/// so this gate and anything else that names a release tag cannot drift apart.
pub fn release_tag_name(krate: &str, version: &str) -> String {
format!("{krate}-v{version}")
}
// ─── guard_intact — the tamper-evident guard manifest (nornir_guard leaf) ───
/// Gate: every `[guard].forbidden` path must match the recorded tamper-evident sha256
/// manifest. Reads the recorded manifest and verifies the on-disk state against it via
/// the shared [`nornir_guard`] leaf; any drift reds the release.
pub fn gate_guard_intact(workspace_root: &Path) -> Result<()> {
let recorded = nornir_guard::read_manifest(workspace_root)?;
nornir_guard::intact(workspace_root, &recorded)
}
// ─── duplicate_sources — the SAME crate linked from BOTH a path and the registry ──
/// A crate that the resolved graph contains TWICE: once from local source (a workspace
/// member or a `path` dep) and once from the registry. Its types are two different types.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DuplicateSource {
/// The crate name present under both origins.
pub krate: String,
/// Versions resolved from LOCAL source (workspace member / path dep).
pub local: Vec<String>,
/// Versions resolved from a REGISTRY.
pub registry: Vec<String>,
}
impl DuplicateSource {
/// True when the two copies carry the SAME version number — the most confusing
/// shape, since every diagnostic will print one indistinguishable version.
pub fn same_version(&self) -> bool {
self.local.iter().any(|v| self.registry.contains(v))
}
}
/// Find crates the LOCKFILE resolves from both local source and a registry.
///
/// This is the DIRECT detector for the duplicate-linkage bug, and it is deliberately not
/// a manifest scan. [`registry_reach_scan`] catches the manifest-level *cause* it can see
/// — one workspace member naming a sibling by version — but that definition is narrower
/// than the bug in two ways it cannot fix:
///
/// * CROSS-REPO. `knut-pipelines` reaching `facett-pipeline` by registry version while
/// facett's own workspace uses the path copy is the identical fault, but the two
/// crates live in different repos, so no workspace relationship exists to inspect.
/// * TRANSITIVE. The duplicate is often pulled in by a dep-of-a-dep, so no manifest in
/// the workspace names it at all.
///
/// Reading the resolved graph finds every instance of both, because it looks at the
/// OUTCOME (two copies linked) rather than at one syntactic cause. Offline and cheap: a
/// `Cargo.lock` parse, no network and no `cargo` subprocess.
///
/// A lockfile entry with no `source` key is local (workspace member or `path` dep); a
/// `source = "registry+…"` entry came from a registry. Anything else (`git+…`) is left
/// alone — a git pin is an explicit, deliberate choice, not an accident.
pub fn duplicate_crate_sources(lock_path: &Path) -> Result<Vec<DuplicateSource>> {
let text = std::fs::read_to_string(lock_path)
.with_context(|| format!("read {}", lock_path.display()))?;
let doc: toml::Value =
toml::from_str(&text).with_context(|| format!("parse {}", lock_path.display()))?;
let Some(pkgs) = doc.get("package").and_then(|p| p.as_array()) else {
return Ok(Vec::new());
};
let mut by_name: std::collections::BTreeMap<String, (Vec<String>, Vec<String>)> =
Default::default();
for p in pkgs {
let (Some(name), Some(ver)) = (
p.get("name").and_then(|v| v.as_str()),
p.get("version").and_then(|v| v.as_str()),
) else {
continue;
};
let entry = by_name.entry(name.to_string()).or_default();
match p.get("source").and_then(|v| v.as_str()) {
None => entry.0.push(ver.to_string()),
Some(s) if s.starts_with("registry+") => entry.1.push(ver.to_string()),
Some(_) => {}
}
}
Ok(by_name
.into_iter()
.filter(|(_, (l, r))| !l.is_empty() && !r.is_empty())
.map(|(krate, (local, registry))| DuplicateSource { krate, local, registry })
.collect())
}
/// Gate: the workspace's resolved graph must not link any crate from BOTH local source
/// and a registry.
///
/// When it does, that crate is compiled twice and its types are two distinct types. The
/// build succeeds, the tests pass, and it fails only where a value crosses between the
/// halves — as `expected PipelineModel, found facett_pipeline::PipelineModel`, a message
/// that points at everything except dependency resolution.
///
/// Returns `Ok(())` when the repo has no `Cargo.lock` (nothing resolved yet ⇒ nothing to
/// judge), never a false block.
pub fn gate_duplicate_crate_sources(ws_root: &Path) -> Result<()> {
let lock = ws_root.join("Cargo.lock");
if !lock.is_file() {
return Ok(());
}
let dupes = duplicate_crate_sources(&lock)?;
if dupes.is_empty() {
return Ok(());
}
let detail = dupes
.iter()
.map(|d| {
format!(
"{} (local {} | registry {}){}",
d.krate,
d.local.join(","),
d.registry.join(","),
if d.same_version() { " SAME VERSION" } else { "" }
)
})
.collect::<Vec<_>>()
.join("; ");
Err(anyhow!(
"duplicate-source gate RED — {} crate(s) are linked from BOTH local source and the \
registry, so each is compiled twice and its types are two different types. Give the \
registry edge a `path`, or bridge it with `[patch.crates-io]`: {detail}",
dupes.len(),
))
}
// ─── unavailable_req — a requirement naming a version the registry doesn't have ──
/// A dependency requirement that pins a CONCRETE version which is not available on the
/// registry — either it never existed, or it has been yanked.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnavailableRequirement {
/// The crate declaring the requirement.
pub from: String,
/// The dependency being required.
pub dep: String,
/// The concrete `x.y.z` the requirement names.
pub req: String,
/// The declaring manifest.
pub manifest: std::path::PathBuf,
}
/// Extract the concrete `x.y.z` a requirement pins, if it pins one at all.
///
/// `"0.1.2"` / `"^0.1.2"` / `"=0.1.2"` name a specific version worth checking; `"0.1"`
/// and `"*"` name a RANGE and are deliberately ignored — a range is a statement about
/// what the crate can accept, not a claim that some particular version exists.
fn pinned_concrete_version(req: &str) -> Option<String> {
let t = req.trim().trim_start_matches(['^', '=']).trim();
let parts: Vec<&str> = t.split('.').collect();
if parts.len() == 3 && parts.iter().all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit())) {
Some(t.to_string())
} else {
None
}
}
/// Scan a workspace for requirements naming a version the registry does not have.
///
/// ADVISORY by design — it returns findings rather than a `Result<()>`, and no dispatcher
/// entry makes it a blocker. A requirement like `nornir-build-thing = "0.1.2"` against a
/// registry that never had a 0.1.2 is *satisfiable* (the caret floats up to 0.1.3), so it
/// breaks nothing today; it is a false belief written into a manifest, and false beliefs
/// are worth reporting but not worth failing a release over.
///
/// Its sharper use is AFTER A YANK. Yanking blocks only new resolution, so a consumer
/// still pinning the yanked version keeps building from its lockfile and looks fine right
/// up until someone regenerates it. This scan is what turns "we yanked it" into "and here
/// is everyone who still asks for it".
///
/// Inconclusive probes (`has_version` ⇒ `None`, i.e. registry unreachable) are DROPPED,
/// never reported — same correctness rule as the drift gate: report on proof, never on
/// absence of evidence.
pub fn unavailable_requirement_scan(
backend: &dyn crate::registry::RegistryBackend,
ws_root: &Path,
) -> Result<Vec<UnavailableRequirement>> {
let meta = cargo_metadata::MetadataCommand::new()
.current_dir(ws_root)
.no_deps()
.exec()
.with_context(|| format!("cargo metadata in {}", ws_root.display()))?;
let member_ids: std::collections::BTreeSet<_> = meta.workspace_members.iter().collect();
let local: std::collections::BTreeSet<String> = meta
.packages
.iter()
.filter(|p| member_ids.contains(&p.id))
.map(|p| p.name.to_string())
.collect();
let mut out = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for pkg in meta.packages.iter().filter(|p| member_ids.contains(&p.id)) {
for dep in &pkg.dependencies {
// Skip IN-WORKSPACE siblings only. Their requirement is allowed to name a
// version the registry lacks — that is exactly what a prepared-but-unpublished
// bump looks like, and it is CORRECT.
//
// A CROSS-REPO path dep is deliberately NOT skipped, even though the path makes
// it build locally. Cargo strips that path on publish, so the version string is
// what a published build actually resolves — and a wrong one there is a claim
// nobody ever checks. `nornir` carried `nornir-build-thing = { version = "0.1.2",
// path = … }` for months against a registry that never had a 0.1.2; the caret
// floated up to 0.1.3, so nothing ever complained.
if local.contains(dep.name.as_str()) {
continue;
}
let Some(v) = pinned_concrete_version(&dep.req.to_string()) else { continue };
if !seen.insert((pkg.name.to_string(), dep.name.to_string(), v.clone())) {
continue;
}
if backend.has_version(dep.name.as_str(), &v) == Some(false) {
out.push(UnavailableRequirement {
from: pkg.name.to_string(),
dep: dep.name.to_string(),
req: v,
manifest: pkg.manifest_path.clone().into_std_path_buf(),
});
}
}
}
out.sort_by(|a, b| (&a.from, &a.dep).cmp(&(&b.from, &b.dep)));
Ok(out)
}
// ─── registry_reach — an in-workspace crate depended on by REGISTRY VERSION ──
/// A dependency edge that reaches a crate SITTING IN THE SAME WORKSPACE through the
/// registry instead of through a `path`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegistryReach {
/// The member declaring the dependency.
pub from: String,
/// The in-workspace crate being reached by version instead of by path.
pub to: String,
/// The version requirement it resolves from the registry (`"0.1"`, `"0.1.3"`, …).
pub req: String,
/// The declaring member's manifest.
pub manifest: std::path::PathBuf,
}
/// Scan a workspace for dependency edges that reach a SIBLING MEMBER by registry
/// version rather than by `path`.
///
/// Driven by `cargo metadata`, deliberately: cargo has already resolved
/// `dep.workspace = true` inheritance against the root `[workspace.dependencies]`, so an
/// inherited dep whose ROOT entry carries a path reads as a path dep here — exactly as
/// it behaves in a real build. A hand-rolled TOML scan cannot see that and reports every
/// inheriting crate in the constellation as a false positive (68 of them across garmr and
/// heimdal alone, all of them correct code). A gate that cries wolf 68 times is not a gate.
pub fn registry_reach_scan(ws_root: &Path) -> Result<Vec<RegistryReach>> {
// `--no-deps`: this gate reasons about DECLARED edges, not the resolved graph, so it
// needs no lockfile, no network, and cannot be broken by an unresolvable requirement
// — including the very "version that was never published" case it is meant to survive.
let meta = cargo_metadata::MetadataCommand::new()
.current_dir(ws_root)
.no_deps()
.exec()
.with_context(|| format!("cargo metadata in {}", ws_root.display()))?;
let member_ids: std::collections::BTreeSet<_> = meta.workspace_members.iter().collect();
let members: std::collections::BTreeSet<String> = meta
.packages
.iter()
.filter(|p| member_ids.contains(&p.id))
.map(|p| p.name.to_string())
.collect();
let mut out = Vec::new();
for pkg in meta.packages.iter().filter(|p| member_ids.contains(&p.id)) {
for dep in &pkg.dependencies {
// Only sibling members, never self, and only edges cargo would resolve from
// a registry (`path` unset ⇒ the registry copy wins over the local source).
if dep.name == pkg.name.as_str() || !members.contains(dep.name.as_str()) {
continue;
}
if dep.path.is_some() {
continue;
}
out.push(RegistryReach {
from: pkg.name.to_string(),
to: dep.name.to_string(),
req: dep.req.to_string(),
manifest: pkg.manifest_path.clone().into_std_path_buf(),
});
}
}
out.sort_by(|a, b| (&a.from, &a.to).cmp(&(&b.from, &b.to)));
out.dedup();
Ok(out)
}
/// Gate: no workspace member may depend on a SIBLING MEMBER by registry version only.
///
/// This is a nastier failure than stale content, and it is why it earns its own gate.
/// When a member is reached by version instead of by path, cargo resolves the REGISTRY
/// copy — so the same crate is linked TWICE into one build, at two different versions,
/// and its types are two DIFFERENT types. It compiles. It links. `cargo test` is green.
/// It then fails wherever the two halves meet, in a way nothing attributes to dependency
/// resolution.
///
/// `nornir-config` did exactly this to `nornir-build-thing`: registry 0.1.3 there, local
/// 0.1.4 everywhere else, two incompatible `BuildVariantSet` types in one workspace, and
/// the registry half silently dropped the `[[iso]]` / `[[installer]]` sections of every
/// config it parsed.
///
/// The fix is always the same and always cheap — add `path = "../<crate>"` beside the
/// version. Keeping the version is correct and required: it is what a published build
/// resolves once cargo strips the path.
///
/// # SCOPE — read this before treating a green as broad
///
/// This gate sees exactly one shape of the bug: a workspace member naming a SIBLING
/// MEMBER of the same workspace by version. It is structurally blind to two others:
///
/// * CROSS-REPO — `knut-pipelines` reaching `facett-pipeline` by registry version while
/// facett's workspace uses the path copy is the identical fault, but the crates live
/// in different repos so there is no workspace relationship to inspect.
/// * TRANSITIVE — when a dep-of-a-dep drags in the registry copy, no manifest in the
/// workspace names it at all.
///
/// [`gate_duplicate_crate_sources`] covers both, because it reads the RESOLVED graph and
/// judges the outcome (two copies linked) instead of one syntactic cause. Run that one
/// for coverage; this one for a precise, directly-actionable manifest fix. A green here
/// means "no member names a sibling by version", NOT "no crate is linked twice".
pub fn gate_registry_reach(ws_root: &Path) -> Result<()> {
let reaches = registry_reach_scan(ws_root)?;
if reaches.is_empty() {
return Ok(());
}
let detail = reaches
.iter()
.map(|r| format!("{} -> {} = \"{}\"", r.from, r.to, r.req))
.collect::<Vec<_>>()
.join("; ");
Err(anyhow!(
"registry-reach gate RED — {} in-workspace dep(s) resolve a SIBLING MEMBER from the registry \
instead of by path, so that crate links twice at two versions and its types stop being the \
same type. Add `path = \"../<crate>\"` beside the version: {detail}",
reaches.len(),
))
}
// ─── source_drift — the SAME-VERSION "two artifacts, one identity" gate ─────
/// One crate whose LOCAL source drifted from the copy already PUBLISHED at the SAME
/// version — i.e. two different artifacts wearing one `<crate>@<version>` identity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DriftFinding {
/// The crate name.
pub krate: String,
/// The version that is BOTH published and local — the contested identity.
pub version: String,
/// Crate-root-relative paths that differ (added / removed / changed), capped by
/// [`crate::source_drift::summarize_differing`]'s presentation limit.
pub differing: Vec<String>,
}
/// PURE decision core: fold per-crate drift verdicts into the crates that must block a
/// publish. Only [`DriftVerdict::Drifted`] — a positive, content-proven mismatch —
/// becomes a finding; [`DriftVerdict::Unchanged`] and [`DriftVerdict::Unknown`]
/// (unpublished, offline, extraction failure) never do. That asymmetry is the gate's
/// correctness rule: we block on proof, never on absence of evidence.
///
/// Split out from the IO so the gate's judgement is testable with zero network.
pub fn drift_findings(
verdicts: &[(String, String, crate::source_drift::DriftVerdict)],
) -> Vec<DriftFinding> {
verdicts
.iter()
.filter_map(|(krate, version, verdict)| match verdict {
crate::source_drift::DriftVerdict::Drifted { differing } => Some(DriftFinding {
krate: krate.clone(),
version: version.clone(),
differing: differing.clone(),
}),
_ => None,
})
.collect()
}
/// PURE roll-up: [`drift_findings`] turned into the gate's pass/fail `Result`, with a
/// message that names every contested `<crate>@<version>` and the bump it demands.
pub fn gate_source_drift_verdicts(
verdicts: &[(String, String, crate::source_drift::DriftVerdict)],
) -> Result<()> {
let findings = drift_findings(verdicts);
if findings.is_empty() {
return Ok(());
}
let detail = findings
.iter()
.map(|f| {
format!(
"{}@{} ({})",
f.krate,
f.version,
crate::source_drift::summarize_differing(&f.differing)
)
})
.collect::<Vec<_>>()
.join("; ");
Err(anyhow!(
"source-drift gate RED — {} crate(s) differ from the copy already published at the SAME version, \
so one version number names two different artifacts. BUMP the version (do NOT re-publish over it): {detail}",
findings.len(),
))
}
/// Gate over an INJECTED published-source oracle — the seam that makes the whole gate
/// runnable offline in tests. Enumerates the repo's publishable crates (the same walk
/// [`crate_metadata_check`] uses), assesses each against the oracle, and rolls the
/// verdicts up through [`gate_source_drift_verdicts`].
pub fn gate_source_drift_with(
oracle: &dyn crate::source_drift::PublishedSourceOracle,
repo_root: &Path,
) -> Result<()> {
let checks = crate_metadata_check(repo_root)?;
let mut verdicts = Vec::with_capacity(checks.len());
for c in &checks {
let Some(dir) = c.manifest.parent() else { continue };
let verdict = crate::source_drift::assess(oracle, dir, &c.crate_name, &c.version);
verdicts.push((c.crate_name.clone(), c.version.clone(), verdict));
}
gate_source_drift_verdicts(&verdicts)
}
/// Gate: no publishable crate in `repo_root` may have EDITED source while keeping a
/// version number that is already live on crates.io.
///
/// This is the guard for the failure mode that `cargo publish` itself cannot catch and
/// that no changelog will tell you about: the registry is immutable per
/// `<crate>@<version>`, so once `foo@0.1.3` is published, every later edit to `foo`'s
/// tree that does NOT bump the version creates a second artifact wearing the first
/// one's name. Anyone who resolves `0.1.3` gets code that does not match the source
/// tree, and — because the drift is usually ADDITIVE — it compiles, so nothing is red
/// anywhere. It simply behaves like older software. `nornir-build-thing@0.1.3` reached
/// 3.4× its published source size (4 946 → 17 052 lines, 15 new modules) before anyone
/// noticed, and the registry copy silently dropped the `[[iso]]` / `[[installer]]`
/// sections of every `build-variants.toml` it parsed.
///
/// HEAVY (one CDN fetch per already-published crate) — release/on-request only, exactly
/// like [`gate_edition`]. Degrades to PASS on any inconclusive crate: a first publish,
/// an offline runner, or a tarball it cannot extract is a NOTE, never a false block.
pub fn gate_source_drift(repo_root: &Path) -> Result<()> {
gate_source_drift_with(&crate::source_drift::CratesIoSources, repo_root)
}
// ─── roundtrip — the `cargo test --test roundtrip_*` driver ─────────────────
/// Gate: integration round-trip. Caller supplies a closure that performs `agent push →
/// server store → agent pull` for one artifact kind; this gate runs them in order.
pub fn integration_roundtrip<F>(kinds: &[&str], mut run_one: F) -> Result<()>
where
F: FnMut(&str) -> Result<()>,
{
for k in kinds {
run_one(k).with_context(|| format!("roundtrip failed for {k}"))?;
}
Ok(())
}
/// Roundtrip driver: invoke the gate by shelling out to
/// `cargo test --test roundtrip_<kind> --release` per kind. Consumer repos implement the
/// actual roundtrip logic as Rust `#[test]` functions under `tests/roundtrip_<kind>.rs`.
pub fn integration_roundtrip_via_cargo_test(repo_root: &Path, kinds: &[&str]) -> Result<()> {
integration_roundtrip(kinds, |k| {
let test_name = format!("roundtrip_{k}");
let status = std::process::Command::new(crate::cargo::cargo_bin())
.args(["test", "--test", &test_name, "--release"])
.current_dir(repo_root)
.status()
.with_context(|| format!("spawn cargo test --test {test_name}"))?;
if !status.success() {
return Err(anyhow!("cargo test --test {test_name} exited {status}"));
}
Ok(())
})
}
// ─── nexus_floor — holger ops/sec ≥ nexus ops/sec ───────────────────────────
/// Gate: holger ops/sec must be ≥ nexus ops/sec for every result. Looks for
/// `holger_ops_sec` and `nexus_ops_sec` in each result's metrics map. Results lacking
/// both keys are skipped.
pub fn nexus_floor(run: &BenchRun) -> Result<()> {
for r in &run.results {
let h = r.metrics.get("holger_ops_sec").and_then(|v| v.as_f64());
let n = r.metrics.get("nexus_ops_sec").and_then(|v| v.as_f64());
if let (Some(h), Some(n)) = (h, n) {
if h < n {
return Err(anyhow!("nexus floor: {} holger={:.0} < nexus={:.0}", r.name, h, n));
}
}
}
Ok(())
}
// ─── no_regression — the no-regression gate family ──────────────────────────
/// Gate: no result drops more than `max_drop_pct` versus the last same-machine entry in
/// the JSONL history at `history_path`.
pub fn no_regression(run: &BenchRun, history_path: &Path, max_drop_pct: f64) -> Result<()> {
let history = history::read_all(history_path)?;
no_regression_against(run, &history, max_drop_pct)
}
/// Core of [`no_regression`] operating on an in-memory history slice (e.g. read back from
/// the Iceberg warehouse) instead of a JSONL file. The baseline is the **newest** prior
/// run for the same `machine`, chosen deterministically by `timestamp`. No prior run for
/// the machine ⇒ `Ok` (first run bootstraps the baseline). Static-capability rows are
/// skipped (machine facts, not measurements). Direction-aware: latency-shaped keys
/// (`*_ms`/`*_latency`/…) trip on a RISE, throughput keys on a DROP.
pub fn no_regression_against(run: &BenchRun, history: &[BenchRun], max_drop_pct: f64) -> Result<()> {
let same: Vec<&BenchRun> = history.iter().filter(|h| h.machine == run.machine).collect();
let Some(last) = pick_baseline(&same) else {
return Ok(());
};
for r in &run.results {
// Static-capability rows (issue #13) are machine facts, not timed
// measurements — a change in `n_cores`/SIMD flags is not a perf regression,
// so the no-regression gate skips them.
if r.is_static() {
continue;
}
let Some(prev) = last.find(&r.name) else { continue };
for (key, new_val) in &r.metrics {
let Some(new_f) = new_val.as_f64() else { continue };
let Some(prev_f) = prev.metrics.get(key).and_then(|v| v.as_f64()) else {
continue;
};
if prev_f <= 0.0 {
continue;
}
if metric_lower_is_better(key) {
let rise_pct = (new_f - prev_f) / prev_f * 100.0;
if rise_pct > max_drop_pct {
return Err(anyhow!(
"regression: {} {} rose {:.1}% ({:.2} → {:.2}) [lower-is-better]",
r.name, key, rise_pct, prev_f, new_f
));
}
} else {
let drop_pct = (prev_f - new_f) / prev_f * 100.0;
if drop_pct > max_drop_pct {
return Err(anyhow!(
"regression: {} {} dropped {:.1}% ({:.2} → {:.2})",
r.name, key, drop_pct, prev_f, new_f
));
}
}
}
}
Ok(())
}
/// Whether a bench metric is **latency-shaped** (lower = better) rather than the default
/// throughput-shaped (higher = better) direction the regression gate assumes. Recognised
/// by key convention: duration-unit suffixes, an explicit `latency` token, and
/// tail-latency percentiles. Throughput keys (`*_mbs`, `*_ops_sec`, …) do NOT match —
/// `ops_sec` ends in `_sec`, not `_secs`, so it stays higher-is-better.
pub(crate) fn metric_lower_is_better(key: &str) -> bool {
let k = key.to_ascii_lowercase();
const LOWER_SUFFIXES: &[&str] = &[
"_ms", "_us", "_ns", "_micros", "_nanos", "_millis", "_secs", "_latency", "_lat",
"_p50", "_p90", "_p95", "_p99", "_p999",
];
LOWER_SUFFIXES.iter().any(|s| k.ends_with(s)) || k.contains("latency")
}
/// Pick the newest baseline run deterministically: by `timestamp` when all candidates
/// carry one (lexicographic RFC3339 is chronological), else last-in-slice.
fn pick_baseline<'a>(runs: &[&'a BenchRun]) -> Option<&'a BenchRun> {
if runs.is_empty() {
return None;
}
if runs.iter().all(|r| r.timestamp.is_some()) {
runs.iter().copied().max_by(|a, b| a.timestamp.cmp(&b.timestamp))
} else {
runs.last().copied()
}
}
/// Default size of the baseline sample window for [`no_regression_windowed`].
pub const DEFAULT_REGRESSION_WINDOW: usize = 10;
/// Default significance band for [`no_regression_windowed`], in sample standard
/// deviations.
pub const DEFAULT_REGRESSION_SIGMA: f64 = 3.0;
/// Summary statistics of one metric's baseline sample window.
#[derive(Debug, Clone, Copy)]
pub struct MetricStats {
pub n: usize,
pub mean: f64,
/// **Sample** standard deviation (Bessel-corrected, `n-1`); `0.0` when `n < 2`.
pub stddev: f64,
}
impl MetricStats {
pub fn from_samples(samples: &[f64]) -> Self {
let n = samples.len();
if n == 0 {
return MetricStats { n: 0, mean: 0.0, stddev: 0.0 };
}
let mean = samples.iter().sum::<f64>() / n as f64;
let stddev = if n < 2 {
0.0
} else {
let var = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
var.sqrt()
};
MetricStats { n, mean, stddev }
}
/// How many sample-σ a `candidate` sits **below** the mean (higher = better).
pub fn drop_sigma(&self, candidate: f64) -> f64 {
if self.stddev <= 0.0 {
return 0.0;
}
(self.mean - candidate) / self.stddev
}
}
/// The last `window` same-machine prior runs, newest-first. `window == 0` keeps them all.
fn baseline_window<'a>(machine: &str, history: &'a [BenchRun], window: usize) -> Vec<&'a BenchRun> {
let mut same: Vec<&BenchRun> = history.iter().filter(|h| h.machine == machine).collect();
if same.iter().all(|r| r.timestamp.is_some()) {
same.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
} else {
same.reverse();
}
if window > 0 && same.len() > window {
same.truncate(window);
}
same
}
/// Distribution-aware, **noise-robust** successor to [`no_regression_against`]. Builds
/// each metric's baseline *distribution* from the last `window` same-machine runs and
/// fails a metric only when the candidate is **both** meaningfully lower
/// (`drop% > max_drop_pct` vs the window mean) AND outside the noise band (more than
/// `sigma` sample stddevs below the mean). Degenerate windows (`< 2` samples / `σ == 0`)
/// fall back to the magnitude check alone. No prior run ⇒ `Ok`.
pub fn no_regression_windowed(
run: &BenchRun,
history: &[BenchRun],
max_drop_pct: f64,
window: usize,
sigma: f64,
) -> Result<()> {
let baseline = baseline_window(&run.machine, history, window);
if baseline.is_empty() {
return Ok(());
}
for r in &run.results {
// Static-capability rows (issue #13) are machine facts, not measured
// throughput — never gate a release on them.
if r.is_static() {
continue;
}
for (key, new_val) in &r.metrics {
let Some(new_f) = new_val.as_f64() else { continue };
let samples: Vec<f64> = baseline
.iter()
.filter_map(|h| h.find(&r.name))
.filter_map(|res| res.metrics.get(key).and_then(|v| v.as_f64()))
.filter(|f| *f > 0.0)
.collect();
if samples.is_empty() {
continue;
}
let stats = MetricStats::from_samples(&samples);
if stats.mean <= 0.0 {
continue;
}
let drop_pct = (stats.mean - new_f) / stats.mean * 100.0;
if drop_pct <= max_drop_pct {
continue;
}
let z = stats.drop_sigma(new_f);
if stats.stddev > 0.0 && z < sigma {
continue;
}
let band = if stats.stddev > 0.0 {
format!("{z:.1}σ over {} run(s)", stats.n)
} else {
format!("no noise estimate, {} run(s)", stats.n)
};
return Err(anyhow!(
"regression: {} {} dropped {:.1}% (μ {:.2} → {:.2}; {})",
r.name, key, drop_pct, stats.mean, new_f, band,
));
}
}
Ok(())
}
// ─── the unified name→gate dispatcher ───────────────────────────────────────
/// Verdict of [`gate`]: what happened when a gate NAME was dispatched over a repo root.
#[derive(Debug)]
pub enum GateDispatch {
/// The gate ran HERE in dwarves; carries its pass/fail `Result`.
Ran(Result<()>),
/// The gate is OWNED by nornir (docs-render / autonom) and is not hosted in the
/// forge — reach it over gRPC. Carries the reason.
HandledInNornir(&'static str),
/// A valid gate name, but it needs extra input the repo-root-only dispatcher can't
/// supply (a `BenchRun`/history slice, or the roundtrip kinds). Carries the
/// call-the-typed-fn hint.
NeedsInput(&'static str),
/// Unknown gate name.
Unknown,
}
/// Dispatch a gate by NAME over a repo/workspace `root`. Handles every relocated gate
/// that needs only the root (path_patches, path_dep_versions, crate_metadata,
/// links_conflicts, edition, guard_intact); returns [`GateDispatch::NeedsInput`] for the
/// bench/roundtrip gates (call their typed fns), and [`GateDispatch::HandledInNornir`]
/// for the deferred `docs_fresh`/`coverage` gates.
pub fn gate(name: &str, root: &Path) -> GateDispatch {
match name {
"path_patches" | "no_path_patches" => GateDispatch::Ran(no_path_patches(root)),
"path_dep_versions" => GateDispatch::Ran(gate_path_dep_versions(root)),
"crate_metadata" => GateDispatch::Ran(gate_crate_metadata(root)),
"links_conflicts" => GateDispatch::Ran(gate_links_conflicts(root)),
"edition" | "edition_2024" | "rust_2024" => GateDispatch::Ran(gate_edition(root)),
"guard_intact" => GateDispatch::Ran(gate_guard_intact(root)),
"source_drift" | "same_version_drift" => GateDispatch::Ran(gate_source_drift(root)),
"registry_reach" | "workspace_member_paths" => GateDispatch::Ran(gate_registry_reach(root)),
"duplicate_sources" | "duplicate_crate_sources" => {
GateDispatch::Ran(gate_duplicate_crate_sources(root))
}
"roundtrip" | "integration_roundtrip" => GateDispatch::NeedsInput(
"roundtrip needs the artifact kinds — call integration_roundtrip_via_cargo_test(root, &kinds)",
),
"nexus_floor" => GateDispatch::NeedsInput(
"nexus_floor needs a BenchRun — call nexus_floor(&run)",
),
"no_regression" => GateDispatch::NeedsInput(
"no_regression needs a BenchRun + history — call no_regression_against(&run, &history, max_drop_pct)",
),
"docs_fresh" => GateDispatch::HandledInNornir(
"docs_fresh renders managed docs via crate::docs — nornir OWNS docs-render",
),
"coverage" | "coverage_all" | "megagate" => GateDispatch::HandledInNornir(
"coverage/megagate runs the autonom completeness engine — nornir OWNS autonom",
),
_ => GateDispatch::Unknown,
}
}
#[cfg(test)]
mod tests {
use super::*;
use nornir_bench::{BenchResult, BenchRun};
fn write_manifest(body: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), body).unwrap();
dir
}
fn run_with(results: Vec<BenchResult>) -> BenchRun {
BenchRun {
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(),
id: Default::default(),
machine_facts: None,
}
}
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
}
/// RED-WHEN-BROKEN: a `[patch.crates-io]` local/git fork trips the gate, a clean /
/// version-pinned manifest passes. If `no_path_patches` stopped flagging forks the
/// `tripped` half goes false and this fails.
#[test]
fn no_path_patches_flags_forks_passes_clean() {
let clean = write_manifest("[package]\nname=\"x\"\nversion=\"0.1.0\"\n");
let znippy = write_manifest("[patch.crates-io]\nznippy = { path = \"../znippy\" }\n");
let iceberg = write_manifest("[patch.crates-io]\niceberg = { git = \"https://x/iceberg\" }\n");
let pinned = write_manifest("[patch.crates-io]\nfoo = \"1.2\"\nbar = { version = \"1.2\" }\n");
let subtable = write_manifest("[patch.crates-io.skade]\npath = \"../skade\"\n");
let clean_ok = no_path_patches(clean.path()).is_ok();
let pinned_ok = no_path_patches(pinned.path()).is_ok();
let tripped = no_path_patches(znippy.path()).is_err()
&& no_path_patches(iceberg.path()).is_err()
&& no_path_patches(subtable.path()).is_err();
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"no_path_patches_flags_forks",
clean_ok && pinned_ok && tripped,
&format!("clean_ok={clean_ok} pinned_ok={pinned_ok} tripped={tripped}"),
);
assert!(clean_ok, "a clean manifest passes");
assert!(pinned_ok, "registry version pins are publishable");
assert!(tripped, "path=/git= forks (bare/quoted/sub-table) must all trip");
}
/// RED-WHEN-BROKEN: only the crates.io publish-REQUIRED fields (`description` +
/// `license`/`license-file`) HARD-red `gate_crate_metadata`. A crate that IS
/// publish-ready but is missing only the nordisk-RECOMMENDED `readme`/`repository`
/// PASSES (advisory), and a `license-file` satisfies the license requirement. If
/// someone re-promotes readme/repository to a hard blocker, `advisory_passes` flips
/// false and this fails.
#[test]
fn crate_metadata_gate_reds_on_missing_fields() {
// HARD: no `description` ⇒ not publishable ⇒ RED (has license, still reds).
let missing_desc = write_manifest(
"[package]\nname=\"y\"\nversion=\"0.1.0\"\nedition=\"2021\"\nlicense=\"MIT\"\n",
);
// HARD: no license at all ⇒ RED.
let missing_license = write_manifest(
"[package]\nname=\"y2\"\nversion=\"0.1.0\"\nedition=\"2021\"\ndescription=\"y2\"\n",
);
// ADVISORY: description + license present, but NO readme/repository ⇒ PASSES.
let advisory_only = write_manifest(
"[package]\nname=\"a\"\nversion=\"0.1.0\"\nedition=\"2021\"\n\
license=\"MIT\"\ndescription=\"an a crate\"\n",
);
// ADVISORY: `license-file` (not `license`) still satisfies the license requirement.
let license_file_only = write_manifest(
"[package]\nname=\"lf\"\nversion=\"0.1.0\"\nedition=\"2021\"\n\
license-file=\"LICENSE\"\ndescription=\"an lf crate\"\n",
);
// Everything present ⇒ PASSES.
let full = write_manifest(
"[package]\nname=\"z\"\nversion=\"0.1.0\"\nedition=\"2021\"\n\
license=\"MIT\"\nrepository=\"https://x/z\"\ndescription=\"a z crate\"\nreadme=\"README.md\"\n",
);
let reds_desc = gate_crate_metadata(missing_desc.path()).is_err();
let reds_license = gate_crate_metadata(missing_license.path()).is_err();
let advisory_passes = gate_crate_metadata(advisory_only.path()).is_ok();
let license_file_passes = gate_crate_metadata(license_file_only.path()).is_ok();
let full_passes = gate_crate_metadata(full.path()).is_ok();
let pass = reds_desc
&& reds_license
&& advisory_passes
&& license_file_passes
&& full_passes;
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"crate_metadata_gate_reds_on_missing",
pass,
&format!(
"reds_desc={reds_desc} reds_license={reds_license} \
advisory_passes={advisory_passes} license_file_passes={license_file_passes} \
full_passes={full_passes}"
),
);
assert!(reds_desc, "missing description (publish-required) must red the gate");
assert!(reds_license, "missing license (publish-required) must red the gate");
assert!(
advisory_passes,
"description+license present but no readme/repository ⇒ advisory note, gate PASSES",
);
assert!(license_file_passes, "license-file satisfies the license requirement");
assert!(full_passes, "all fields present ⇒ gate passes");
}
/// `path_dep_audit` flags a version-less path dep, ignores version-less dev-deps, and
/// `gate_path_dep_versions` reds accordingly.
#[test]
fn path_dep_versions_gate() {
let dir = write_manifest(
"[package]\nname=\"x\"\nversion=\"0.1.0\"\nedition=\"2021\"\n\n\
[dependencies]\nsibling-a = { path = \"../a\" }\nsibling-b = { path = \"../b\", version = \"0.2\" }\n\n\
[dev-dependencies]\nharness = { path = \"../harness\" }\n",
);
let findings = path_dep_audit(dir.path()).unwrap();
// dev-dep ignored → only sibling-a + sibling-b audited.
assert_eq!(findings.len(), 2, "{findings:?}");
assert!(gate_path_dep_versions(dir.path()).is_err(), "sibling-a lacks version=");
let ok_dir = write_manifest(
"[package]\nname=\"x\"\nversion=\"0.1.0\"\nedition=\"2021\"\n\n\
[dependencies]\nsibling-b = { path = \"../b\", version = \"0.2\" }\n",
);
assert!(gate_path_dep_versions(ok_dir.path()).is_ok());
}
/// RED-WHEN-BROKEN: the path-dep gate message must NAME the specific offender —
/// consuming crate + dep name + dep path — not just a bare count, so the operator
/// knows WHICH crate/dep to fix (the dogfood defect where "N path-dep(s) missing
/// version=" named nobody). If the message ever collapses back to a count, this fails.
#[test]
fn path_dep_gate_message_names_the_offender() {
let dir = write_manifest(
"[package]\nname=\"knut-popviz\"\nversion=\"0.1.0\"\nedition=\"2021\"\n\n\
[dependencies]\nznippy-zoomies = { path = \"../../../znippy-zoomies\" }\n",
);
let err = gate_path_dep_versions(dir.path()).unwrap_err().to_string();
let names_consumer = err.contains("knut-popviz");
let names_dep = err.contains("znippy-zoomies");
let names_path = err.contains("../../../znippy-zoomies");
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"path_dep_gate_names_offender",
names_consumer && names_dep && names_path,
&err,
);
assert!(names_consumer, "message must name the consuming crate: {err}");
assert!(names_dep, "message must name the offending dep: {err}");
assert!(names_path, "message must name the dep path: {err}");
}
/// `detect_links_conflicts` (pure over synthetic decls — no cargo metadata) reds on a
/// duplicated `links=` key and passes when every key is unique.
#[test]
fn links_conflicts_detection() {
let decl = |cr: &str, links: &str| LinkDecl {
crate_name: cr.into(),
version: "0.1.0".into(),
links_value: links.into(),
manifest: std::path::PathBuf::from("Cargo.toml"),
};
let clean = vec![decl("a", "z"), decl("b", "lz4")];
assert!(detect_links_conflicts(&clean).is_empty());
let clash = vec![decl("a", "z"), decl("b", "z")];
let conflicts = detect_links_conflicts(&clash);
assert_eq!(conflicts.len(), 1, "{conflicts:?}");
assert_eq!(conflicts[0].links_value, "z");
}
/// RED-WHEN-BROKEN: a bench slice where holger < nexus reds `nexus_floor`; holger ≥
/// nexus (and results missing either key) pass.
#[test]
fn nexus_floor_reds_when_holger_below_nexus() {
let below = run_with(vec![result("decode", &[("holger_ops_sec", 900.0), ("nexus_ops_sec", 1000.0)])]);
let above = run_with(vec![result("decode", &[("holger_ops_sec", 5000.0), ("nexus_ops_sec", 1000.0)])]);
let skip = run_with(vec![result("compress", &[("compress_mbs", 800.0)])]);
let reds = nexus_floor(&below).is_err();
let passes = nexus_floor(&above).is_ok();
let skipped = nexus_floor(&skip).is_ok();
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"nexus_floor_reds_below_floor",
reds && passes && skipped,
&format!("reds={reds} passes={passes} skipped={skipped}"),
);
assert!(reds, "holger 900 < nexus 1000 breaches the floor");
assert!(passes, "holger ≥ nexus holds");
assert!(skipped, "a run lacking both keys is skipped, not failed");
}
/// `no_regression_against`: a beyond-threshold drop is rejected, a within-threshold
/// drop passes, and a first-run (no same-machine baseline) bootstraps green.
#[test]
fn no_regression_point_gate() {
let mk = |machine: &str, ts: &str, mbs: f64| {
let mut r = run_with(vec![result("codec", &[("throughput_mbs", mbs)])]);
r.machine = machine.into();
r.timestamp = Some(ts.into());
r
};
let history = vec![mk("ci", "2026-01-01T00:00:00+00:00", 100.0)];
assert!(no_regression_against(&mk("ci", "2026-01-02T00:00:00+00:00", 95.0), &history, 10.0).is_ok());
assert!(no_regression_against(&mk("ci", "2026-01-02T00:00:00+00:00", 80.0), &history, 10.0).is_err());
// First run for a fresh machine — nothing to regress against.
assert!(no_regression_against(&mk("fresh", "2026-01-02T00:00:00+00:00", 1.0), &history, 10.0).is_ok());
}
/// The windowed gate: a 15% dip inside a jittery baseline's noise band does NOT flip,
/// while the same dip against a tight baseline (many σ out) IS flagged.
#[test]
fn no_regression_windowed_is_noise_aware() {
let mk = |day: u32, mbs: f64| {
let mut r = run_with(vec![result("codec", &[("throughput_mbs", mbs)])]);
r.timestamp = Some(format!("2026-02-{day:02}T00:00:00+00:00"));
r
};
let candidate = {
let mut c = run_with(vec![result("codec", &[("throughput_mbs", 85.0)])]);
c.timestamp = Some("2026-03-01T00:00:00+00:00".into());
c
};
let jittery = vec![mk(1, 100.0), mk(2, 90.0), mk(3, 110.0), mk(4, 95.0), mk(5, 105.0)];
let tight = vec![mk(1, 100.0), mk(2, 101.0), mk(3, 99.0), mk(4, 100.0), mk(5, 100.0)];
// Legacy point gate rejects it; the noise-aware gate does not (within 3σ).
assert!(no_regression_against(&candidate, &jittery, 10.0).is_err());
assert!(no_regression_windowed(&candidate, &jittery, 10.0, DEFAULT_REGRESSION_WINDOW, DEFAULT_REGRESSION_SIGMA).is_ok());
assert!(no_regression_windowed(&candidate, &tight, 10.0, DEFAULT_REGRESSION_WINDOW, DEFAULT_REGRESSION_SIGMA).is_err());
}
/// `metric_lower_is_better` classifies latency-shaped keys, leaving throughput keys
/// (including `ops_sec`, which ends `_sec` not `_secs`) higher-is-better.
#[test]
fn metric_direction_classifier() {
assert!(metric_lower_is_better("decode_latency_ms"));
assert!(metric_lower_is_better("p99_ms"));
assert!(metric_lower_is_better("wall_secs"));
assert!(!metric_lower_is_better("throughput_mbs"));
assert!(!metric_lower_is_better("nexus_ops_sec"));
}
/// `integration_roundtrip`: every kind runs in order; the first failure aborts and is
/// contextualised with the failing kind.
#[test]
fn roundtrip_runs_in_order_and_aborts_on_failure() {
use std::cell::RefCell;
let seen = RefCell::new(Vec::new());
let res = integration_roundtrip(&["blob", "symbol", "edge"], |k| {
seen.borrow_mut().push(k.to_string());
if k == "symbol" {
anyhow::bail!("store rejected the artifact")
}
Ok(())
});
let chain = format!("{:#}", res.unwrap_err());
assert!(chain.contains("roundtrip failed for symbol"), "{chain}");
assert_eq!(seen.into_inner(), vec!["blob", "symbol"], "edge not attempted after failure");
}
/// The dispatcher: relocated repo-root gates RUN, deferred gates report
/// HandledInNornir, bench/roundtrip gates report NeedsInput, and an unknown name is
/// Unknown.
#[test]
fn dispatcher_routes_relocated_deferred_and_bench_gates() {
let clean = write_manifest("[package]\nname=\"x\"\nversion=\"0.1.0\"\n");
assert!(matches!(gate("path_patches", clean.path()), GateDispatch::Ran(Ok(()))));
let forked = write_manifest("[patch.crates-io]\nz = { path = \"../z\" }\n");
assert!(matches!(gate("path_patches", forked.path()), GateDispatch::Ran(Err(_))));
assert!(matches!(gate("docs_fresh", clean.path()), GateDispatch::HandledInNornir(_)));
assert!(matches!(gate("coverage", clean.path()), GateDispatch::HandledInNornir(_)));
assert!(matches!(gate("nexus_floor", clean.path()), GateDispatch::NeedsInput(_)));
assert!(matches!(gate("roundtrip", clean.path()), GateDispatch::NeedsInput(_)));
assert!(matches!(gate("does_not_exist", clean.path()), GateDispatch::Unknown));
}
// ─── ABSORBED from nornir's gate.rs (2026-07-22 de-fork) ────────────────
// These guard behaviours the dwarves copy implemented but never TESTED. They
// came over with the de-fork so deleting nornir's twin loses no coverage.
fn ts_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 {
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(),
id: Default::default(),
machine_facts: None,
}
}
/// The baseline is the NEWEST run by timestamp, not the last slice element.
/// History is deliberately out of order: against the newest (100) the candidate
/// 90 is a 10% drop == the threshold → ok; against the wrongly-picked
/// last-in-slice 1000 it would be a 91% drop and error. Passing proves the pick.
#[test]
fn baseline_is_newest_by_timestamp_not_slice_order() {
let candidate = ts_run("ci", "2026-03-01T00:00:00+00:00", 90.0);
let history = vec![
ts_run("ci", "2026-02-01T00:00:00+00:00", 100.0),
ts_run("ci", "2026-01-01T00:00:00+00:00", 1000.0),
];
assert!(no_regression_against(&candidate, &history, 10.0).is_ok());
}
/// … and the mirror: a real 50% drop vs the newest baseline is REJECTED even
/// though an older, slower run sits later in the slice.
#[test]
fn baseline_picks_newest_even_when_older_run_is_last_in_slice() {
let candidate = ts_run("ci", "2026-03-01T00:00:00+00:00", 50.0);
let history = vec![
ts_run("ci", "2026-02-01T00:00:00+00:00", 100.0),
ts_run("ci", "2026-01-01T00:00:00+00:00", 40.0),
];
assert!(no_regression_against(&candidate, &history, 10.0).is_err());
}
/// First-ever run for a machine: nothing to regress against, never a false red.
#[test]
fn no_baseline_for_machine_passes() {
let candidate = ts_run("ci", "2026-01-02T00:00:00+00:00", 100.0);
let history = vec![ts_run("other-box", "2026-01-01T00:00:00+00:00", 999.0)];
assert!(no_regression_against(&candidate, &history, 10.0).is_ok());
}
/// RED-WHEN-BROKEN (issue #13): a static-capabilities row carries MACHINE FACTS,
/// not timed measurements — a 32→8 core move must not read as a 75% regression,
/// in EITHER gate variant. The control proves the same drop on a MEASURED row
/// still trips, so this is a skip, not a disabled gate.
#[test]
fn static_capability_rows_are_never_a_regression() {
let mut baseline = run_with(vec![BenchResult::static_capabilities(
"ci.capabilities",
Default::default(),
)]);
baseline.results[0].metrics.insert("n_cores".into(), serde_json::json!(32.0));
baseline.timestamp = Some("2026-01-01T00:00:00+00:00".into());
let mut now = run_with(vec![BenchResult::static_capabilities(
"ci.capabilities",
Default::default(),
)]);
now.results[0].metrics.insert("n_cores".into(), serde_json::json!(8.0));
now.timestamp = Some("2026-02-01T00:00:00+00:00".into());
assert!(no_regression_against(&now, std::slice::from_ref(&baseline), 5.0).is_ok());
assert!(no_regression_windowed(&now, std::slice::from_ref(&baseline), 5.0, 10, 3.0).is_ok());
let meas_base = run_with(vec![result("decode", &[("x_mbs", 100.0)])]);
let meas_now = run_with(vec![result("decode", &[("x_mbs", 25.0)])]);
assert!(no_regression_against(&meas_now, std::slice::from_ref(&meas_base), 5.0).is_err());
}
/// A latency-shaped metric is lower-is-better: a DROP is an improvement, only a
/// RISE past the band reds — and the message names the direction. The throughput
/// control proves the branch is direction-aware, not symmetric.
#[test]
fn lower_is_better_metric_trips_only_on_a_rise() {
let base = run_with(vec![result("decode", &[("decode_ms", 10.0)])]);
let faster = run_with(vec![result("decode", &[("decode_ms", 5.0)])]);
assert!(
no_regression_against(&faster, std::slice::from_ref(&base), 5.0).is_ok(),
"a latency DROP is an improvement, never a regression"
);
let slower = run_with(vec![result("decode", &[("decode_ms", 15.0)])]);
let err = no_regression_against(&slower, std::slice::from_ref(&base), 5.0)
.unwrap_err()
.to_string();
assert!(err.contains("rose"), "message names the rise: {err}");
assert!(err.contains("lower-is-better"), "message tags direction: {err}");
let tput_base = run_with(vec![result("decode", &[("x_mbs", 100.0)])]);
let tput_up = run_with(vec![result("decode", &[("x_mbs", 150.0)])]);
assert!(
no_regression_against(&tput_up, std::slice::from_ref(&tput_base), 5.0).is_ok(),
"a throughput RISE is an improvement"
);
}
/// `nexus_floor` edge cases: equality passes (>= floor), ONE bad result reds the
/// WHOLE run, and a result missing either key is skipped rather than assumed bad.
#[test]
fn nexus_floor_equality_partial_and_missing_keys() {
let equal = run_with(vec![result("a", &[("holger_ops_sec", 100.0), ("nexus_ops_sec", 100.0)])]);
assert!(nexus_floor(&equal).is_ok(), "holger == nexus clears the floor");
let one_bad = run_with(vec![
result("good", &[("holger_ops_sec", 200.0), ("nexus_ops_sec", 100.0)]),
result("bad", &[("holger_ops_sec", 50.0), ("nexus_ops_sec", 100.0)]),
]);
let err = nexus_floor(&one_bad).unwrap_err().to_string();
assert!(err.contains("bad"), "the offending result is named: {err}");
let missing = run_with(vec![
result("only_holger", &[("holger_ops_sec", 1.0)]),
result("only_nexus", &[("nexus_ops_sec", 999.0)]),
result("neither", &[("x", 1.0)]),
]);
assert!(nexus_floor(&missing).is_ok(), "rows lacking a key are skipped, not failed");
}
/// An empty kind list passes vacuously — an unconfigured roundtrip gate must not
/// invent a failure.
#[test]
fn roundtrip_empty_kind_list_passes_vacuously() {
let mut called = false;
let r = integration_roundtrip(&[], |_k| {
called = true;
anyhow::bail!("must not run")
});
assert!(r.is_ok());
assert!(!called, "no kinds ⇒ the closure is never invoked");
}
/// `path_dep_audit` skips what cargo itself strips: version-less path DEV-deps are
/// legal (cargo drops `dev-dependencies` from the published `.crate`), and a
/// `publish = false` member is never uploaded at all. Flagging either is a false red.
#[test]
fn path_dep_audit_ignores_dev_deps_and_unpublished_members() {
let dev_only = write_manifest(
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n\
[dev-dependencies]\nhelper = { path = \"../helper\" }\n",
);
assert!(
path_dep_audit(dev_only.path()).unwrap().iter().all(|f| f.ok()),
"a version-less path DEV-dep is legal"
);
let unpublished = write_manifest(
"[package]\nname=\"x\"\nversion=\"0.1.0\"\npublish=false\n\
[dependencies]\nhelper = { path = \"../helper\" }\n",
);
assert!(
path_dep_audit(unpublished.path()).unwrap().is_empty(),
"a publish=false member is never uploaded, so its path deps cannot break a publish"
);
}
/// `crate_metadata_check` resolves `field.workspace = true` inherits against the root
/// `[workspace.package]`. Without this every unified-versioning workspace (facett,
/// edda, …) reds falsely on license/repository/description.
#[test]
fn crate_metadata_check_resolves_workspace_inheritance() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers=[\"m\"]\n\
[workspace.package]\nversion=\"1.2.3\"\nlicense=\"MIT\"\n\
repository=\"https://example/x\"\ndescription=\"d\"\nreadme=\"README.md\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("m")).unwrap();
std::fs::write(
dir.path().join("m/Cargo.toml"),
"[package]\nname=\"m\"\nversion.workspace=true\nlicense.workspace=true\n\
repository.workspace=true\ndescription.workspace=true\nreadme.workspace=true\n",
)
.unwrap();
let checks = crate_metadata_check(dir.path()).unwrap();
let m = checks.iter().find(|c| c.crate_name == "m").expect("member is checked");
assert_eq!(m.version, "1.2.3", "inherited version resolves");
assert!(m.has_license && m.has_repository && m.has_description && m.has_readme,
"workspace-inherited metadata must not read as missing: {m:?}");
}
// ─── source_drift gate ──────────────────────────────────────────────────
use crate::source_drift::{DriftVerdict, PublishedSourceOracle};
use std::collections::BTreeMap;
fn verdicts(
v: Vec<(&str, &str, DriftVerdict)>,
) -> Vec<(String, String, DriftVerdict)> {
v.into_iter().map(|(k, ver, d)| (k.to_string(), ver.to_string(), d)).collect()
}
/// RED-WHEN-BROKEN (pure core). A `Drifted` verdict MUST red the gate and the message
/// MUST name the contested `<crate>@<version>`; `Unchanged` and `Unknown` must both
/// PASS. If the gate ever started blocking on `Unknown` it would false-block every
/// first publish and every offline runner; if it stopped blocking on `Drifted` the
/// guard would be decorative — this test fails in both directions.
#[test]
fn source_drift_gate_reds_only_on_proven_same_version_drift() {
let drifted = verdicts(vec![(
"nornir-build-thing",
"0.1.3",
DriftVerdict::Drifted { differing: vec!["src/variant.rs".into(), "src/lib.rs".into()] },
)]);
let unchanged = verdicts(vec![("clean", "1.0.0", DriftVerdict::Unchanged)]);
let unknown = verdicts(vec![(
"never-published",
"0.1.0",
DriftVerdict::Unknown("404 not found".into()),
)]);
let err = gate_source_drift_verdicts(&drifted).unwrap_err().to_string();
let blocks_drift = err.contains("nornir-build-thing@0.1.3") && err.contains("src/variant.rs");
let passes_unchanged = gate_source_drift_verdicts(&unchanged).is_ok();
let passes_unknown = gate_source_drift_verdicts(&unknown).is_ok();
// One drifted crate inside a mostly-clean set must still red the whole gate.
let mixed = verdicts(vec![
("clean", "1.0.0", DriftVerdict::Unchanged),
("bad", "2.0.0", DriftVerdict::Drifted { differing: vec!["src/a.rs".into()] }),
("new", "0.1.0", DriftVerdict::Unknown("not published".into())),
]);
let mixed_err = gate_source_drift_verdicts(&mixed).unwrap_err().to_string();
let finds_needle = mixed_err.contains("bad@2.0.0") && !mixed_err.contains("clean@");
assert_eq!(drift_findings(&mixed).len(), 1, "only the proven-drifted crate is a finding");
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"source_drift_reds_only_on_proven_drift",
blocks_drift && passes_unchanged && passes_unknown && finds_needle,
"Drifted blocks; Unchanged/Unknown pass; message names only the drifted crate",
);
assert!(blocks_drift, "a proven same-version edit must RED the gate: {err}");
assert!(passes_unchanged, "byte-identical source must pass");
assert!(passes_unknown, "an inconclusive verdict must NEVER false-block");
assert!(finds_needle, "the message must name the drifted crate only: {mixed_err}");
}
/// A [`PublishedSourceOracle`] stand-in that serves a fixed map (or a failure), so the
/// full gate — manifest walk → `cargo package --list` → tarball diff → verdict — runs
/// with zero network.
struct StubOracle(Result<BTreeMap<String, Vec<u8>>, String>);
impl PublishedSourceOracle for StubOracle {
fn published_sources(&self, _k: &str, _v: &str) -> Result<BTreeMap<String, Vec<u8>>, String> {
self.0.clone()
}
}
/// RED-WHEN-BROKEN (end-to-end). Builds a REAL publishable crate on disk, takes its
/// genuine shipped-source map as the "published" copy, and drives the whole
/// `gate_source_drift_with` path three ways: identical → PASS, one edited byte in a
/// shipped file → RED, oracle failure → PASS. This is the test that would have caught
/// `nornir-build-thing@0.1.3`: it exercises the real diff, not a hand-fed verdict.
#[test]
fn source_drift_gate_catches_a_real_edited_file_at_the_same_version() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname=\"driftee\"\nversion=\"0.1.3\"\nedition=\"2021\"\n\
description=\"d\"\nlicense=\"MIT\"\nrepository=\"https://example/x\"\n",
)
.unwrap();
std::fs::write(root.join("src/lib.rs"), "pub fn shipped() -> u8 { 1 }\n").unwrap();
// The honest "published" map: exactly what this crate ships right now.
let published = crate::source_drift::local_shipped_sources(root)
.expect("cargo package --list must work on a standalone crate");
let unchanged_ok = gate_source_drift_with(&StubOracle(Ok(published.clone())), root).is_ok();
// Now EDIT a shipped file while leaving the version at 0.1.3 — the exact trap.
std::fs::write(root.join("src/lib.rs"), "pub fn shipped() -> u8 { 2 }\npub fn added() {}\n")
.unwrap();
let drift_res = gate_source_drift_with(&StubOracle(Ok(published)), root);
let err = drift_res.as_ref().err().map(|e| e.to_string()).unwrap_or_default();
let caught = drift_res.is_err() && err.contains("driftee@0.1.3") && err.contains("src/lib.rs");
// An oracle that cannot answer (unpublished / offline) must never false-block,
// even though the tree is now genuinely drifted.
let unknown_ok =
gate_source_drift_with(&StubOracle(Err("404".into())), root).is_ok();
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"source_drift_catches_real_edited_file",
unchanged_ok && caught && unknown_ok,
"end-to-end over a real crate: identical passes, edited-at-same-version reds, oracle failure passes",
);
assert!(unchanged_ok, "an unedited tree must pass against its own shipped map");
assert!(caught, "an edited file at an already-published version must RED: {err}");
assert!(unknown_ok, "an unreachable registry must degrade to a note, not a block");
}
/// The gate must be reachable by NAME through the dispatcher — an unwired gate is no
/// gate. Guards against the `gate.rs`-has-it / dispatcher-forgot-it split that left
/// `source_drift` dead for its whole existence.
#[test]
fn source_drift_is_wired_into_the_dispatcher() {
let wired = matches!(gate("source_drift", Path::new("/nonexistent")), GateDispatch::Ran(_))
&& matches!(
gate("same_version_drift", Path::new("/nonexistent")),
GateDispatch::Ran(_)
)
&& matches!(gate("registry_reach", Path::new("/nonexistent")), GateDispatch::Ran(_))
&& matches!(
gate("workspace_member_paths", Path::new("/nonexistent")),
GateDispatch::Ran(_)
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"source_drift_dispatcher_wired",
wired,
"`source_drift` / `same_version_drift` / `registry_reach` resolve to real gates, not Unknown",
);
assert!(wired, "`source_drift` must dispatch to a real gate, not Unknown");
}
// ─── registry_reach gate ────────────────────────────────────────────────
/// Build a two-member workspace where `app` depends on sibling `core` via `dep_line`.
fn reach_ws(dep_line: &str, root_extra: &str) -> tempfile::TempDir {
let d = tempfile::tempdir().unwrap();
std::fs::write(
d.path().join("Cargo.toml"),
format!("[workspace]\nresolver=\"2\"\nmembers=[\"app\",\"core\"]\n{root_extra}"),
)
.unwrap();
for (m, deps) in [("app", dep_line), ("core", "")] {
std::fs::create_dir_all(d.path().join(m).join("src")).unwrap();
std::fs::write(
d.path().join(m).join("Cargo.toml"),
format!(
"[package]\nname=\"{m}\"\nversion=\"0.1.0\"\nedition=\"2021\"\n\
description=\"d\"\nlicense=\"MIT\"\n\n[dependencies]\n{deps}\n"
),
)
.unwrap();
std::fs::write(d.path().join(m).join("src/lib.rs"), "").unwrap();
}
d
}
/// RED-WHEN-BROKEN. Reproduces the EXACT `nornir-config` -> `nornir-build-thing` state
/// that was just fixed: a member reaching a sibling by registry version only. The gate
/// must RED and name the edge; adding the `path` (version kept — a published build
/// still needs it) must turn it GREEN.
#[test]
fn registry_reach_gate_reds_on_the_nornir_config_shape() {
let bad = reach_ws("core = { version = \"0.1\" }", "");
let res = gate_registry_reach(bad.path());
let err = res.as_ref().err().map(|e| e.to_string()).unwrap_or_default();
let caught = res.is_err() && err.contains("app -> core");
// The fix that was applied to nornir-config: path beside the version.
let good = reach_ws("core = { version = \"0.1\", path = \"../core\" }", "");
let fixed = gate_registry_reach(good.path()).is_ok();
// A bare `path` with no version is not a registry reach either (a DIFFERENT gate,
// `gate_path_dep_versions`, owns the missing-version complaint).
let pathonly = reach_ws("core = { path = \"../core\" }", "");
let pathonly_ok = gate_registry_reach(pathonly.path()).is_ok();
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"registry_reach_reds_on_sibling_by_version",
caught && fixed && pathonly_ok,
"version-only sibling dep reds; version+path and path-only pass",
);
assert!(caught, "a sibling reached by registry version must RED: {err}");
assert!(fixed, "adding the path must clear the gate");
assert!(pathonly_ok, "a plain path dep is not a registry reach");
}
/// A registry stand-in: `avail` lists the versions it will admit to having; anything
/// in `unreachable` probes as inconclusive (`None`).
struct StubBackend {
avail: Vec<(&'static str, &'static str)>,
unreachable: Vec<&'static str>,
}
impl crate::registry::RegistryBackend for StubBackend {
fn published_max_version(&self, _k: &str) -> Option<String> {
None
}
fn has_version(&self, k: &str, v: &str) -> Option<bool> {
if self.unreachable.contains(&k) {
return None;
}
Some(self.avail.contains(&(k, v)))
}
}
/// `pinned_concrete_version` must recognise a CONCRETE pin and ignore a RANGE. This is
/// the whole precision of the advisory: `"0.1"` is a statement about what the crate
/// accepts, not a claim that a version exists, and flagging it would be noise.
#[test]
fn concrete_pin_detection_ignores_ranges() {
assert_eq!(pinned_concrete_version("0.1.2").as_deref(), Some("0.1.2"));
assert_eq!(pinned_concrete_version("^0.1.2").as_deref(), Some("0.1.2"));
assert_eq!(pinned_concrete_version("=0.1.2").as_deref(), Some("0.1.2"));
for range in ["0.1", "*", "1", ">=0.1, <0.3", "0.1.x"] {
assert!(pinned_concrete_version(range).is_none(), "{range} is a range, not a pin");
}
}
/// RED-WHEN-BROKEN. The `nornir` 0.1.2 shape: a manifest pinning a version the registry
/// never had. Must be reported when PROVEN absent, and must stay silent for a version
/// that exists, for a range, and for an unreachable registry.
#[test]
fn unavailable_requirement_scan_reports_only_proven_absent_pins() {
let ws = reach_ws(
"ghost = { version = \"0.1.2\" }\nreal = { version = \"0.1.3\" }\n\
loose = { version = \"0.1\" }\noffline = { version = \"9.9.9\" }",
"",
);
let backend = StubBackend {
avail: vec![("real", "0.1.3"), ("loose", "0.1.9")],
unreachable: vec!["offline"],
};
let found = unavailable_requirement_scan(&backend, ws.path()).unwrap();
let names: Vec<&str> = found.iter().map(|f| f.dep.as_str()).collect();
let ok = names == vec!["ghost"];
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"unavailable_requirement_reports_proven_absent_only",
ok,
"absent pin reported; live pin, range pin and unreachable registry all silent",
);
assert!(ok, "expected only the absent pin, got {names:?}");
assert_eq!(found[0].from, "app");
assert_eq!(found[0].req, "0.1.2");
}
/// A sibling reached by PATH must never be reported, even though its bumped version is
/// not on the registry yet. That is precisely what a prepared-but-unpublished release
/// looks like — flagging it would make the advisory fire on every correct bump, which
/// would train everyone to ignore it.
#[test]
fn unavailable_requirement_scan_ignores_path_siblings() {
let ws = reach_ws("core = { version = \"9.9.9\", path = \"../core\" }", "");
let backend = StubBackend { avail: vec![], unreachable: vec![] };
let found = unavailable_requirement_scan(&backend, ws.path()).unwrap();
assert!(found.is_empty(), "a path sibling is served from the tree, not the registry: {found:?}");
}
fn write_lock(body: &str) -> tempfile::TempDir {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("Cargo.lock"), body).unwrap();
d
}
/// RED-WHEN-BROKEN. The duplicate-linkage bug in its real lockfile shape — the same
/// crate resolved once from local source and once from a registry. Reproduces the
/// facett/knut case (`facett-pipeline` 0.1.13 local AND 0.1.13 registry) that
/// `registry_reach` is structurally blind to, since those crates are in different
/// repos with no workspace relationship.
#[test]
fn duplicate_sources_gate_catches_the_cross_repo_shape() {
let dup = write_lock(
"[[package]]\nname = \"facett-pipeline\"\nversion = \"0.1.13\"\n\n\
[[package]]\nname = \"facett-pipeline\"\nversion = \"0.1.13\"\n\
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n\n\
[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\n\
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
);
let res = gate_duplicate_crate_sources(dup.path());
let err = res.as_ref().err().map(|e| e.to_string()).unwrap_or_default();
let caught = res.is_err()
&& err.contains("facett-pipeline")
&& err.contains("SAME VERSION")
&& !err.contains("serde");
// A purely-registry graph and a purely-local graph are both fine.
let clean = write_lock(
"[[package]]\nname = \"a\"\nversion = \"1.0.0\"\n\
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n\n\
[[package]]\nname = \"b\"\nversion = \"0.1.0\"\n",
);
let clean_ok = gate_duplicate_crate_sources(clean.path()).is_ok();
// A git pin beside a local copy is a DELIBERATE choice, not an accident.
let gitpin = write_lock(
"[[package]]\nname = \"c\"\nversion = \"0.1.0\"\n\n\
[[package]]\nname = \"c\"\nversion = \"0.1.0\"\nsource = \"git+https://x/c\"\n",
);
let git_ok = gate_duplicate_crate_sources(gitpin.path()).is_ok();
// No lockfile at all ⇒ nothing resolved ⇒ nothing to judge.
let bare = tempfile::tempdir().unwrap();
let bare_ok = gate_duplicate_crate_sources(bare.path()).is_ok();
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"duplicate_sources_catches_local_plus_registry",
caught && clean_ok && git_ok && bare_ok,
"local+registry duplicate reds (incl. cross-repo); clean, git-pinned and lockless pass",
);
assert!(caught, "a local+registry duplicate must RED and be named: {err}");
assert!(clean_ok, "a graph with no duplicate must pass");
assert!(git_ok, "a git pin is deliberate, not the accident this gate hunts");
assert!(bare_ok, "no lockfile must never be a block");
}
/// The two gates are COMPLEMENTARY, and this pins the difference so nobody reads one
/// green as the other's. The cross-repo duplicate is invisible to `registry_reach`
/// (no workspace relationship exists) and visible to `duplicate_crate_sources`.
#[test]
fn registry_reach_is_narrower_than_duplicate_sources_by_construction() {
let d = write_lock(
"[[package]]\nname = \"x\"\nversion = \"1.0.0\"\n\n\
[[package]]\nname = \"x\"\nversion = \"1.0.0\"\n\
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
);
// No workspace, no members — `registry_reach` has nothing to inspect here at all,
// while the resolved-graph gate still sees the duplicate.
assert!(
gate_duplicate_crate_sources(d.path()).is_err(),
"the resolved-graph gate sees a duplicate with no workspace context"
);
let dupes = duplicate_crate_sources(&d.path().join("Cargo.lock")).unwrap();
assert_eq!(dupes.len(), 1);
assert!(dupes[0].same_version(), "same-version duplicate is flagged as such");
}
/// RED-WHEN-BROKEN, the FALSE-POSITIVE half — the half that decides whether this gate
/// is usable at all. `dep.workspace = true` inherits from the root
/// `[workspace.dependencies]`; when that root entry carries a path, the edge IS a path
/// dep and must NOT be flagged. A naive TOML scan misses this and reports 68 correct
/// crates across garmr and heimdal as violations. If this test ever goes red the gate
/// has started crying wolf and would be turned off — which is how guards die.
#[test]
fn registry_reach_gate_does_not_flag_workspace_inherited_path_deps() {
let inherited = reach_ws(
"core.workspace = true",
"\n[workspace.dependencies]\ncore = { path = \"core\" }\n",
);
let quiet = gate_registry_reach(inherited.path()).is_ok();
// …but inheritance of a REGISTRY entry is still a real reach and must be caught,
// otherwise the exemption above becomes a hole big enough to hide the bug in.
let inherited_registry = reach_ws(
"core.workspace = true",
"\n[workspace.dependencies]\ncore = { version = \"0.1\" }\n",
);
let still_caught = gate_registry_reach(inherited_registry.path()).is_err();
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"release-gate",
"registry_reach_respects_workspace_inheritance",
quiet && still_caught,
"inherited PATH deps pass; inherited REGISTRY deps still red",
);
assert!(quiet, "an inherited path dep is a path dep — flagging it is a false positive");
assert!(still_caught, "inheritance must not become a way to hide a registry reach");
}
/// **The published-tag gate looks for `<crate>-v<version>`, and a bare
/// `v<version>` does not satisfy it.** RED on the old gate, which named no crate
/// and read `v<version>`: here the bare `v0.1.17` at HEAD would have passed it and
/// the crate-named tag would have been invisible to it.
#[test]
fn git_tag_matches_published_wants_the_crate_named_tag_not_the_bare_one() {
let td = tempfile::tempdir().unwrap();
let root = td.path();
nornir_git::gitio::init(root).unwrap();
std::fs::write(root.join("data.txt"), "v1\n").unwrap();
let head = nornir_git::gitio::commit_all(root, "release").unwrap();
// Only the bare form at HEAD: the gate says NO.
nornir_git::gitio::tag_lightweight(root, "v0.1.17", &head).unwrap();
assert!(!git_tag_matches_published(root, "korp-installer", "0.1.17").unwrap(), "a bare v<version> is not the release tag");
// The crate-named tag at HEAD: yes, for that crate and only that crate.
nornir_git::gitio::tag_lightweight(root, &release_tag_name("korp-installer", "0.1.17"), &head).unwrap();
assert!(git_tag_matches_published(root, "korp-installer", "0.1.17").unwrap());
assert!(!git_tag_matches_published(root, "nornir-tpm", "0.1.17").unwrap(), "another crate's release is not this one");
assert_eq!(release_tag_name("korp-installer", "0.1.17"), "korp-installer-v0.1.17");
}
}