src/autonom/megagate.rs
//! # megagate — the CROSS-WORKSPACE completeness gate (S6, step 2)
//!
//! The single-workspace gate ([`super::gather`] → [`GateReport`] →
//! [`crate::release::gate::coverage_gate`]) verdicts ONE workspace's surface. The
//! real release ships EVERY served workspace, so the mega-gate sweeps the whole
//! [`SERVED_WORKSPACES`](nornir_testmatrix::discover::SERVED_WORKSPACES) dimension:
//! it gathers each workspace's [`GateState`] (reusing the per-workspace path
//! verbatim — same surface, same allowlist, same stale detection), composes them
//! into a [`MegaGateReport`], and — where available — folds in the cross-workspace
//! metro rollup (S6 step 4) and the LAW-9 reachability rollup.
//!
//! ```text
//! gather_all(wh, served, run_id)
//! for each WorkspaceSpec: gate_coverage_for_repo(...) → GateReport (persisted)
//! → WorkspaceGate { coverage, [utfallsrum], [reachable] }
//! → MegaGateReport
//! → coverage_gate_all_workspaces(&report) (the RED/GREEN verdict)
//! ```
//!
//! NO new warehouse table: every per-workspace run persists to the SHARED
//! `surface_coverage` table (the per-workspace [`super::persist`]), so the
//! cross-workspace history is just `CoverageSelector::All`/`Workspace` over the one
//! table. The mega-gate's verdict is the conjunction of the per-workspace verdicts:
//! the FIRST workspace whose coverage gap is non-empty (or whose RESOLVED metro
//! lines aren't all green) reds the whole release, naming `(workspace, key)`.
use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::warehouse::iceberg::IcebergWarehouse;
use crate::warehouse::surface_coverage::GateReport;
use nornir_testmatrix::discover::SERVED_WORKSPACES;
use super::gate_coverage_for_repo;
/// The gate inputs for ONE served workspace: which workspace, which primary repo's
/// knowledge map to read, the repo root that owns the checked-in
/// `autonom-allow.toml`, and (optionally) the live clap subcommands the binary
/// injects (empty ⇒ the canonical [`super::CLI_COMMANDS`] fallback).
#[derive(Debug, Clone)]
pub struct WorkspaceSpec {
/// The served workspace name (the `surface_coverage` `workspace` column).
pub workspace: String,
/// The primary repo whose latest knowledge scan drives the surface (usually
/// the workspace name).
pub repo: String,
/// The repo root that owns `.nornir/autonom-allow.toml`.
pub repo_root: PathBuf,
/// Live clap subcommand names (empty ⇒ canonical [`super::CLI_COMMANDS`]).
pub cli_commands: Vec<String>,
}
impl WorkspaceSpec {
/// A spec for `workspace`, reading the same-named repo's map under `repo_root`.
pub fn new(workspace: impl Into<String>, repo_root: impl Into<PathBuf>) -> Self {
let workspace = workspace.into();
Self {
repo: workspace.clone(),
workspace,
repo_root: repo_root.into(),
cli_commands: Vec::new(),
}
}
}
/// The canonical served-workspace specs against `roots_dir` — one
/// [`WorkspaceSpec`] per [`SERVED_WORKSPACES`] entry, each rooted at
/// `roots_dir/<workspace>` (the sibling-repo layout the constellation uses). The
/// binary overrides this with the LIVE served set from `workspaces_list`; this is
/// the canonical fallback so the dimension is discoverable from the lib.
pub fn served_specs(roots_dir: &Path) -> Vec<WorkspaceSpec> {
SERVED_WORKSPACES
.iter()
.map(|w| WorkspaceSpec::new(*w, roots_dir.join(w)))
.collect()
}
/// One served workspace's full cross-gate verdict: its coverage [`GateReport`]
/// plus the OPTIONAL outcome-space + reachability rollups (folded in by the
/// `viz`/metro-aware caller — [`gather_all`] populates only `coverage`, keeping
/// `autonom` free of the `viz` feature).
#[derive(Debug, Clone, Default)]
pub struct WorkspaceGate {
/// The served workspace name.
pub workspace: String,
/// The per-workspace completeness gate (surface − covered − allowlist + stale).
pub coverage: GateReport,
/// Outcome-space rollup `(surfaces meeting the threshold, surfaces with an
/// outcome space)`, when measured. `None` when no utfallsrum was recorded.
pub utfallsrum: Option<(usize, usize)>,
/// Reachability rollup `(green, total)` over the RESOLVED metro lines (S6 step
/// 4) and/or the LAW-9 walk. `None` when neither was computed. Unresolved
/// bin→lib metro lines are EXCLUDED upstream (allowlistable, not hard-red).
pub reachable: Option<(usize, usize)>,
/// Bencher-coverage rollup `(benched-or-excused, total hot-paths)` (Task #39):
/// how many `bencher` surface nodes are covered-or-allowlisted vs the total
/// discovered. `None` when the workspace declared no bencher hot-paths. GREEN
/// iff `met == total`; a hot-path shipping with no bencher (and no allowlist
/// excuse) drops `met` below `total`.
pub bencher: Option<(usize, usize)>,
}
impl WorkspaceGate {
/// Wrap a per-workspace [`GateReport`] as a coverage-only [`WorkspaceGate`].
pub fn from_report(workspace: impl Into<String>, coverage: GateReport) -> Self {
Self {
workspace: workspace.into(),
coverage,
utfallsrum: None,
reachable: None,
bencher: None,
}
}
/// Fold in the bencher-coverage rollup `(met, total)` over the workspace's
/// `bencher` surface nodes (met = covered-or-allowlisted, total = discovered).
pub fn with_bencher(mut self, met: usize, total: usize) -> Self {
self.bencher = Some((met, total));
self
}
/// Compute the bencher rollup `(met, total)` from a gate `state`: total =
/// `bencher` surface nodes, met = those NOT in the gap's `missing` list
/// (covered or allowlisted). `None` when the workspace has no bencher hot-paths.
pub fn bencher_rollup(state: &super::GateState) -> Option<(usize, usize)> {
use nornir_testmatrix::discover::SurfaceKind::Bencher;
let total = state
.surface
.nodes
.iter()
.filter(|n| n.kind == Bencher)
.count();
if total == 0 {
return None;
}
let missing = state
.report
.gap
.missing
.iter()
.filter(|n| n.kind == Bencher)
.count();
Some((total - missing, total))
}
/// Fold in the outcome-space rollup `(met, total)`.
pub fn with_utfallsrum(mut self, met: usize, total: usize) -> Self {
self.utfallsrum = Some((met, total));
self
}
/// Fold in the reachability rollup `(green, total)` over the resolved metro
/// lines / LAW-9 walk.
pub fn with_reachable(mut self, green: usize, total: usize) -> Self {
self.reachable = Some((green, total));
self
}
/// GREEN ⟺ the coverage gate is green AND (when present) every RESOLVED metro
/// line / reachable surface is green. The utfallsrum rollup is advisory here
/// (the threshold already reflected in the coverage `covered` set).
pub fn is_green(&self) -> bool {
self.coverage.is_green() && self.reachable.map(|(g, t)| g == t).unwrap_or(true)
}
/// The first failing surface KEY in this workspace (an uncovered/un-allowlisted
/// node, a stale allowlist entry, or the reachable shortfall) — for naming the
/// offender in the mega verdict. `None` when [`is_green`](Self::is_green).
pub fn first_failure(&self) -> Option<String> {
if let Some(node) = self.coverage.gap.missing.first() {
return Some(format!("uncovered surface `{}`", node.key_str()));
}
if let Some(stale) = self.coverage.stale.first() {
return Some(format!("stale allowlist entry `{}`", stale.key));
}
if let Some((g, t)) = self.reachable {
if g != t {
return Some(format!("{}/{} resolved metro lines green", g, t));
}
}
None
}
}
/// The cross-workspace completeness verdict — one [`WorkspaceGate`] per served
/// workspace, in sweep order. The release-blocking gate
/// ([`crate::release::gate::coverage_gate_all_workspaces`]) fails on the FIRST
/// non-green workspace, naming `(workspace, key)`.
#[derive(Debug, Clone, Default)]
pub struct MegaGateReport {
/// The run id that stamped every per-workspace persisted gate run.
pub run_id: String,
/// Per-workspace verdicts, in `served` order.
pub workspaces: Vec<WorkspaceGate>,
}
impl MegaGateReport {
/// GREEN ⟺ every workspace is green.
pub fn is_green(&self) -> bool {
self.workspaces.iter().all(|w| w.is_green())
}
/// The first failing `(workspace, key)`, or `None` when fully green.
pub fn first_failure(&self) -> Option<(String, String)> {
self.workspaces
.iter()
.find_map(|w| w.first_failure().map(|k| (w.workspace.clone(), k)))
}
/// A one-line human summary for the CLI / viz.
pub fn summary(&self) -> String {
let green = self.workspaces.iter().filter(|w| w.is_green()).count();
format!(
"{}/{} workspaces green — {}",
green,
self.workspaces.len(),
if self.is_green() {
"GREEN"
} else {
"RED (mega gate)"
},
)
}
}
/// **THE CROSS-WORKSPACE GATHER** — sweep every served workspace's completeness
/// gate, persisting each to the SHARED `surface_coverage` table, and compose them
/// into a [`MegaGateReport`].
///
/// For each [`WorkspaceSpec`] this runs the SAME [`gate_coverage_for_repo`] path
/// the single-workspace gate uses (load the repo's knowledge map + checked-in
/// allowlist, difference the surface, persist the per-node rows), so the
/// per-workspace verdict — including stale-allowlist detection — is identical. The
/// metro / LAW-9 rollups are NOT computed here (they need the `viz` feature); the
/// caller folds them in via [`WorkspaceGate::with_reachable`].
pub fn gather_all(
wh: &IcebergWarehouse,
served: &[WorkspaceSpec],
run_id: &str,
) -> Result<MegaGateReport> {
let mut workspaces = Vec::with_capacity(served.len());
for spec in served {
let state = gate_coverage_for_repo(
wh,
&spec.workspace,
&spec.repo,
&spec.repo_root,
spec.cli_commands.clone(),
run_id,
)?;
let bencher = WorkspaceGate::bencher_rollup(&state);
let mut wg = WorkspaceGate::from_report(&spec.workspace, state.report);
if let Some((met, total)) = bencher {
wg = wg.with_bencher(met, total);
}
workspaces.push(wg);
}
Ok(MegaGateReport {
run_id: run_id.to_string(),
workspaces,
})
}
#[cfg(test)]
mod tests {
use super::*;
use nornir_testmatrix::coverage::{AllowEntry, GateReport};
use nornir_testmatrix::discover::{Surface, SurfaceNode, cli_commands};
use std::collections::BTreeSet;
fn surface_ab() -> Surface {
let mut s = Surface::new();
s.extend(cli_commands(["a", "b"]));
s
}
fn green_report(ws: &str) -> GateReport {
let surface = surface_ab();
let covered: BTreeSet<String> = surface.nodes.iter().map(SurfaceNode::key_str).collect();
GateReport::compute("r", ws, &surface, &covered, &Default::default())
}
fn red_report(ws: &str) -> GateReport {
let surface = surface_ab();
let covered: BTreeSet<String> = ["cli_command:a@na".to_string()].into_iter().collect();
GateReport::compute("r", ws, &surface, &covered, &Default::default())
}
#[test]
fn mega_is_green_iff_every_workspace_green() {
let report = MegaGateReport {
run_id: "r".into(),
workspaces: vec![
WorkspaceGate::from_report("alpha", green_report("alpha")),
WorkspaceGate::from_report("beta", green_report("beta")),
],
};
assert!(
report.is_green(),
"all-green sweep is green: {}",
report.summary()
);
assert!(report.first_failure().is_none());
}
#[test]
fn mega_names_first_failing_workspace_and_key() {
let report = MegaGateReport {
run_id: "r".into(),
workspaces: vec![
WorkspaceGate::from_report("alpha", green_report("alpha")),
WorkspaceGate::from_report("beta", red_report("beta")), // b uncovered
],
};
assert!(!report.is_green());
let (ws, key) = report.first_failure().expect("a failure");
assert_eq!(ws, "beta", "names the offending workspace");
assert!(
key.contains("cli_command:b@na"),
"names the uncovered surface: {key}"
);
}
#[test]
fn reachable_shortfall_reds_a_coverage_green_workspace() {
// Coverage is green but a resolved metro line is unlit → the workspace reds.
let wg = WorkspaceGate::from_report("alpha", green_report("alpha")).with_reachable(1, 2);
assert!(
!wg.is_green(),
"an unmet reachable rollup reds the workspace"
);
assert_eq!(
wg.first_failure().as_deref(),
Some("1/2 resolved metro lines green")
);
// All resolved lines green → the workspace stays green.
let wg2 = WorkspaceGate::from_report("alpha", green_report("alpha")).with_reachable(2, 2);
assert!(wg2.is_green());
}
#[test]
fn stale_allowlist_entry_is_named_as_the_failure() {
let surface = surface_ab();
let covered: BTreeSet<String> = surface.nodes.iter().map(SurfaceNode::key_str).collect();
let allow = nornir_testmatrix::coverage::Allowlist {
entries: vec![AllowEntry {
key: "cli_command:a@na".into(),
reason: "old".into(),
}],
};
let report = GateReport::compute("r", "alpha", &surface, &covered, &allow);
let wg = WorkspaceGate::from_report("alpha", report);
assert!(!wg.is_green(), "a stale allowlist entry reds the workspace");
assert!(
wg.first_failure()
.unwrap()
.contains("stale allowlist entry `cli_command:a@na`"),
"names the stale entry: {:?}",
wg.first_failure(),
);
}
#[test]
fn served_specs_cover_the_canonical_dimension() {
let specs = served_specs(Path::new("/tmp/roots"));
assert_eq!(specs.len(), SERVED_WORKSPACES.len());
let nornir = specs.iter().find(|s| s.workspace == "nornir").unwrap();
assert_eq!(nornir.repo, "nornir");
assert_eq!(nornir.repo_root, PathBuf::from("/tmp/roots/nornir"));
}
}