src/autonom/mod.rs
//! # autonom — the FEEDERS for the completeness gate (AUT2 / n-005)
//!
//! The pure enumerators + the gap/verdict model live in
//! [`nornir_testmatrix::discover`] + [`nornir_testmatrix::coverage`]. This module
//! is nornir's **iceberg-backed feeder layer**: it gathers the REAL inputs —
//! `symbol_facts` / `call_edges` from the warehouse, the viz tab enum, the clap
//! subcommands, the MCP `tools/list`, the facett-style registry — turns them
//! into the pure enumerators' row shapes, and assembles a [`Surface`] +
//! `covered` set so [`GateReport`] can be computed and persisted.
//!
//! ```text
//! gather_inputs (iceberg + clap + tab enum + tools/list)
//! → discover::{viz_tabs, cli_commands, mcp_tools, facett_components, unreached_functions}
//! → Surface + covered set (test-reachable closure over call_edges)
//! → GateReport::compute(Surface, covered, allowlist) (the verdict)
//! → rows_for(...) → surface_coverage iceberg table (persisted)
//! ```
//!
//! The allowlist (`autonom-allow.toml`, checked in) is SEEDED (`--seed-allowlist`)
//! with every currently-uncovered surface so the gate is GREEN now and burns
//! down by deletion. The gate FAILS if any surface is uncovered + un-allowlisted
//! OR an allowlist entry is stale (HARD zero, not a ratchet).
pub mod megagate;
pub mod ui_values;
use std::collections::BTreeSet;
use std::path::Path;
use anyhow::Result;
use crate::knowledge::query::KnowledgeView;
use crate::knowledge::symbols::{CallEdgeRow, SymbolRow as KSymbolRow};
use crate::warehouse::iceberg::IcebergWarehouse;
use std::collections::BTreeMap;
use crate::warehouse::surface_coverage::{
Allowlist, CoverageRow, CoverageSelector, CoverageSummary, GateReport, Verdict,
append_surface_coverage, latest_surface_coverage,
query_surface_coverage, rows_for_with_utfallsrum, seed_allowlist,
};
use nornir_testmatrix::discover::{
BencherRow, CallEdge, DiscoveredAtom, FacetRow, Surface, SymbolRow, benchers, cli_commands,
covered_atoms, covered_benchers, covered_facetts, facett_components, grpc_handlers, mcp_tools,
ui_atoms, unreached_functions, viz_tabs,
};
use nornir_testmatrix::utfallsrum::UtfallsrumSummary;
/// **The Elm-harness DRIVE seam** (`viz`-only): construct + drive each embedded
/// facett Elm component headless and produce driven [`FacetRow`]s. Lives in the
/// binary crate (behind `viz`, with the facett deps) so `nornir-testmatrix` stays
/// facett-free. See [`facett_probe::probe_facett_components`].
#[cfg(feature = "viz")]
pub mod facett_probe;
/// The driven facett [`FacetRow`]s for nornir's embedded components — the REAL
/// headless-harness drive when `viz` is on, an empty vec (skip) otherwise. This is
/// what fills the formerly-empty `facett` slot in [`nornir_surface_inputs`].
fn embedded_facett_rows() -> Vec<FacetRow> {
#[cfg(feature = "viz")]
{
facett_probe::probe_facett_components()
}
#[cfg(not(feature = "viz"))]
{
Vec::new()
}
}
/// The discrete-surface inputs the binary feeds in (it does the clap / tab-enum
/// / tools-list / registry introspection — the lib stays binary-free). Functions
/// are NOT in here: they're read from the warehouse knowledge map.
#[derive(Debug, Clone, Default)]
pub struct SurfaceInputs {
/// Viz tab names (the `Tab` enum's debug names) — each yields a fat + thin node.
pub viz_tabs: Vec<String>,
/// CLI subcommand names (clap `Command::get_subcommands`).
pub cli_commands: Vec<String>,
/// MCP tool names (the registered `tools/list`).
pub mcp_tools: Vec<String>,
/// facett-style components (registry ∩ impl Facet). For nornir the viz tabs
/// ARE the components; a leaf repo plugs its own. Often empty for nornir
/// (the viz_tabs axis already covers the renderable surface).
pub facett: Vec<FacetRow>,
/// gRPC handler labels (`Service.verb`), derived from the knowledge map via
/// [`crate::arch::grpc_handlers_from_symbols`] — each becomes a `Thin`-mode
/// [`SurfaceKind::Grpc`](nornir_testmatrix::discover::SurfaceKind::Grpc) node so
/// every RPC backend is a must-be-covered surface (not an invisible handler).
pub grpc: Vec<String>,
/// UI atoms discovered by the live atom-walk (`nornir_robotui::all_atoms`),
/// one [`DiscoveredAtom`] per `(tab, label)`. The binary/test feeds these from
/// the running viz; each becomes a `ui_atom` surface node (covered ⟺ its tab
/// ran AND its state was clean). Empty for a gate run without a live walk.
pub atoms: Vec<DiscoveredAtom>,
/// Hot-paths that should carry a bencher (Task #39), one [`BencherRow`] per
/// canonical hot-path (see [`BENCH_HOT_PATHS`]). `has_bencher` is filled from
/// the `inventory` seam ([`nornir_bench::api::registered_bench_ids`]) — a
/// hot-path with no registered bencher becomes an uncovered
/// [`SurfaceKind::Bencher`](nornir_testmatrix::discover::SurfaceKind::Bencher)
/// surface node, so a module that should be benched but isn't outs itself in
/// the completeness gate (and burns down via the checked-in allowlist).
pub benchers: Vec<BencherRow>,
}
/// nornir's canonical facett palette names (`facett_core::theme::Theme::names()`).
///
/// Lives HERE, not next to the themes, for the same reason [`VIZ_TABS`] does: the
/// palette type comes from `facett-core`, which depends on `egui` unconditionally,
/// so a headless build (`--no-default-features --features "server mcp"`) cannot
/// reach `Theme::names()` just to name-check a string. `nornir-mcp` used to carry
/// its own literal copy for exactly that reason — a defensible twin with an
/// indefensible ending, since nothing compared the two. The
/// `viz_palettes_const_matches_theme_names` test in the viz crate (which already
/// has egui) is the gate that was missing.
pub const VIZ_PALETTES: &[&str] = &[
"default",
"sci-fi",
"nordic-aurora",
"cyberpunk-neon",
"amber-crt",
"deep-space",
"hugin-noir",
];
/// nornir's canonical viz tab names (the `viz::app::Tab` enum debug names).
/// The viz crate is feature-gated, so the gate references this stable list; the
/// `tabs_match_enum` test in the viz crate keeps it honest against `Tab::ALL`.
pub const VIZ_TABS: &[&str] = &[
"Nornir",
"Timeline",
"DepGraph",
"CallGraph",
"Architecture",
"Metro",
"Helix",
"Bloodstream",
"Funnel",
"TimeTravel",
"LiveRun",
"Release",
"Knowledge",
"Warehouse",
"WarehouseDeck",
"Mcp",
"Search",
"Gates",
"Chat",
"Security",
"Dwarves",
"Holger",
"Manual",
"Claude",
"Models",
// 🎛 Settings — the preferences pane and the ONE home of the viz's look & feel
// (the app-wide facett palette picker, moved off the root top bar; matches
// dwarves-demo's 🎛 `settings` tab).
"Settings",
];
/// nornir's canonical MCP tool names (the registered `tools/list`). The MCP
/// matrix harness (`tests/mcp_matrix`) enforces tools/list completeness against
/// the LIVE server, so this list is the gate's join key, kept in step there.
pub const MCP_TOOLS: &[&str] = &[
"affected_by_change",
"bench_history",
"build_order",
"callees_of",
"callers_of",
"changed_since_last_release",
"crate_published",
"defined_in",
"dep_graph_svg",
"dep_path",
"dependents_of",
"deps_of",
"docs_book",
"docs_check",
"docs_export",
"docs_history",
"docs_init",
"docs_render",
"dwarf_call_path",
"dwarf_callees",
"dwarf_callers",
"dwarf_defined_in",
"dwarf_symbol_lookup",
"external_dep_users",
"funnel_add_node",
"funnel_create_plan",
"funnel_link",
"funnel_next",
"funnel_show",
"funnel_status",
"funnel_submit_idea",
"guard_apply",
"guard_status",
"guard_verify",
"index_status",
"knowledge_call_path",
"knowledge_callees",
"knowledge_callers",
"knowledge_defined_in",
"knowledge_symbol_lookup",
"path_between",
"regression_trace",
"release_gate_all",
"release_gate_coverage",
"release_gate_docs_fresh",
"release_gate_nexus_floor",
"release_gate_no_regression",
"release_gate_path_patches",
"repo_overview",
"repos_list",
"search",
"symbol_lookup",
"sync_now",
"test_coverage",
"vector_search",
"viz.click",
"viz.state",
"workspace_register",
"workspace_use",
"workspaces_list",
];
/// nornir's canonical CLI subcommand names (clap kebab-case of the `Cmd` enum).
/// The `nornir` binary OVERRIDES this with live `Cli::command()` introspection
/// (the true anti-drift source); this const is the fallback for callers without
/// the clap tree (e.g. the MCP server), and is asserted against the binary by a
/// functest.
pub const CLI_COMMANDS: &[&str] = &[
"guard",
"bench",
"release",
"test",
"docs",
"introspect",
"warehouse",
"index",
"robot",
"vector",
"knowledge",
"map",
"funnel",
"repos",
"root",
"serve",
"viz",
"install",
"key",
"workspace",
"security",
"mimir",
"diagram",
"bakeoff",
];
/// nornir's canonical **bencher hot-paths** (Task #39) — the perf-critical
/// module/crate keys that SHOULD carry a bencher, in the `<repo>.<scenario>`
/// namespace the bencher `id()`s use. A hot-path is *covered* iff a registered
/// bencher's id equals it or is prefixed by `"<key>_"` / `"<key>."` (so
/// `nornir.dep_graph` is satisfied by `nornir.dep_graph_build`). Every entry that
/// no linked bencher targets becomes an uncovered `Bencher` surface node — a
/// VISIBLE gap that burns down via the checked-in allowlist, never a silent pass.
///
/// Kept deliberately honest + small: these are nornir's real hot paths, not every
/// module. `dep_graph` already has a bencher (in `examples/nornir-bench.rs`); the
/// rest are the known-missing benchers this gate now surfaces.
pub const BENCH_HOT_PATHS: &[&str] = &[
"nornir.dep_graph", // dep-graph build (benched in examples/nornir-bench.rs)
"nornir.vector_search", // embedding vector search
"nornir.warehouse_query", // iceberg warehouse read path
];
/// The bencher hot-path rows for the given canonical hot-paths, with `has_bencher`
/// resolved against `registered` (the `inventory`-swept bencher ids from
/// [`nornir_bench::api::registered_bench_ids`]). A hot-path `key` is covered by any
/// registered id that equals `key` or starts with `"<key>_"` / `"<key>."` — the
/// `<repo>.<scenario>` naming convention — and the matching ids ride along as
/// `bench_ids`. This is the pure mapping from the discovery seam to the fed rows.
pub fn bench_rows(hot_paths: &[&str], registered: &[String]) -> Vec<BencherRow> {
hot_paths
.iter()
.map(|key| {
let matches: Vec<String> = registered
.iter()
.filter(|id| {
id.as_str() == *key
|| id.starts_with(&format!("{key}_"))
|| id.starts_with(&format!("{key}."))
})
.cloned()
.collect();
BencherRow {
module: (*key).to_string(),
has_bencher: !matches.is_empty(),
bench_ids: matches,
}
})
.collect()
}
/// The bencher hot-path rows for nornir's canonical [`BENCH_HOT_PATHS`], with
/// coverage resolved against the LIVE `inventory` registry (the discovery seam).
/// Benchers registered only in `examples/nornir-bench.rs` are not linked into the
/// library/server, so from a gate run they read as not-yet-benched — a visible,
/// allowlist-able gap (never hidden).
fn nornir_bench_rows() -> Vec<BencherRow> {
let registered = crate::bench::api::registered_bench_ids();
bench_rows(BENCH_HOT_PATHS, ®istered)
}
/// nornir's discrete-surface inputs from the canonical lists. The binary may
/// override `cli_commands` with live clap introspection (the lib stays
/// binary-free); an EMPTY `cli_commands` falls back to [`CLI_COMMANDS`]. The tab
/// + tool lists are the canonical nornir surface.
pub fn nornir_surface_inputs(cli_commands: Vec<String>) -> SurfaceInputs {
let cli_commands = if cli_commands.is_empty() {
CLI_COMMANDS.iter().map(|s| s.to_string()).collect()
} else {
cli_commands
};
SurfaceInputs {
viz_tabs: VIZ_TABS.iter().map(|s| s.to_string()).collect(),
cli_commands,
mcp_tools: MCP_TOOLS.iter().map(|s| s.to_string()).collect(),
// The six embedded facett Elm components, DRIVEN headless through the
// `facett_core::harness` (behind `viz`); each becomes a surface node whose
// coverage is the REAL drive verdict, not a handler-name match. Empty (skip)
// in a non-`viz` build — the classic missing-tool skip.
facett: embedded_facett_rows(),
// gRPC handlers are repo-specific (read from the knowledge map) — filled
// by `enumerate_surface`/`gather` from `grpc_labels_from_view`, not here.
grpc: Vec::new(),
// UI atoms come from a LIVE atom-walk (the viz crate / a test), not the
// static lists — a gate run without a walk has none.
atoms: Vec::new(),
// Bencher hot-paths: the canonical BENCH_HOT_PATHS, each with its coverage
// resolved against the LIVE inventory registry (the discovery seam).
benchers: nornir_bench_rows(),
}
}
/// The gathered, fully-differenced gate state for one workspace.
pub struct GateState {
pub surface: Surface,
pub covered: BTreeSet<String>,
pub allowlist: Allowlist,
pub report: GateReport,
/// Per-surface **outcome-space coverage** (`surface_key → utfallsrum
/// summary`) measured this run. Empty when no outcome-space rows were
/// recorded (the threshold-off / legacy path). Drives the utfallsrum gate:
/// a surface only counts as covered when it ran end-to-end AND its
/// function(s) swept ≥`utfallsrum_threshold` outcome classes.
pub utfallsrum: BTreeMap<String, UtfallsrumSummary>,
/// The minimum outcome classes a surface must sweep to count as covered
/// ([`DEFAULT_UTFALLSRUM_THRESHOLD`]); `0` disables the rule (legacy: ran ⟺
/// covered).
pub utfallsrum_threshold: usize,
}
impl GateState {
pub fn summary(&self) -> String {
self.report.summary()
}
}
// ─── building the surface ────────────────────────────────────────────────
/// Map the warehouse knowledge view into the pure discover row shapes.
///
/// Reachability over the persisted `call_edges` is at **identifier
/// granularity** (the syn scan records callees as the last path segment, e.g.
/// `Arc::new` → `new`), so both the caller path and the symbol fqn are reduced
/// to their last segment for matching. A function is a **test root** iff it
/// lives in a test module (`module_path` contains `tests`) or its name is a
/// conventional test name (`test_*` / `it_*` / `prop_*`). This is intentionally
/// approximate — the pure reachability is exhaustively unit-tested; here we feed
/// it the best available facts.
pub fn knowledge_to_rows(view: &KnowledgeView) -> (Vec<SymbolRow>, Vec<CallEdge>) {
let symbols: Vec<SymbolRow> = view
.symbols
.iter()
.filter(|s| s.item_kind == "fn")
.map(|s| {
let fqn = last_seg(&s.item_name).to_string();
SymbolRow {
fqn,
is_test: is_test_symbol(s),
label: Some(format!("{}::{}", s.module_path, s.item_name)),
}
})
.collect();
let edges: Vec<CallEdge> = view
.calls
.iter()
.map(|c: &CallEdgeRow| CallEdge {
caller: last_seg(&c.caller_path).to_string(),
callee: last_seg(&c.callee_ident).to_string(),
})
.collect();
(symbols, edges)
}
fn last_seg(s: &str) -> &str {
s.rsplit("::").next().unwrap_or(s)
}
/// A symbol is a test root iff it sits in a test module or has a test-y name.
fn is_test_symbol(s: &KSymbolRow) -> bool {
let m = s.module_path.to_lowercase();
let n = s.item_name.as_str();
m.split("::").any(|seg| seg == "tests" || seg == "test")
|| n.starts_with("test_")
|| n.starts_with("it_")
|| n.starts_with("prop_")
}
/// Assemble the full [`Surface`] from the discrete inputs + the warehouse
/// knowledge map, and compute the `covered` set.
///
/// COVERED:
/// - **functions**: a fn is covered iff it's in the test-reachable closure over
/// `call_edges` (so the surface gap is exactly `unreached_functions`).
/// - **viz tabs / CLI / MCP / facett**: covered iff a test-reachable function's
/// identifier matches a wiring marker for that surface — pragmatically, we
/// treat these discrete surfaces as covered when a same-named handler fn is
/// test-reachable, else they fall to the allowlist (seeded). This keeps the
/// gate honest (a never-tested tab outs itself) while letting the allowlist
/// burn down as real interaction tests land.
pub fn build_surface_and_covered(
inputs: &SurfaceInputs,
symbols: &[SymbolRow],
edges: &[CallEdge],
) -> (Surface, BTreeSet<String>) {
let reachable = nornir_testmatrix::discover::test_reachable(symbols, edges);
let mut surface = Surface::new();
surface
.extend(viz_tabs(inputs.viz_tabs.iter().cloned()))
.extend(cli_commands(inputs.cli_commands.iter().cloned()))
.extend(mcp_tools(inputs.mcp_tools.iter().cloned()))
.extend(grpc_handlers(inputs.grpc.iter().cloned()))
.extend(facett_components(&inputs.facett))
.extend(ui_atoms(&inputs.atoms))
.extend(benchers(&inputs.benchers))
.extend(unreached_functions(symbols, edges));
// Covered set:
// - every function NOT in `unreached_functions` is covered → its key.
// - a discrete surface (tab/cli/mcp/facett) is covered iff a test-reachable
// fn's identifier matches its id (case-insensitive last-segment match).
let reachable_idents: BTreeSet<String> = reachable.iter().map(|s| s.to_lowercase()).collect();
let mut covered: BTreeSet<String> = BTreeSet::new();
for node in &surface.nodes {
use nornir_testmatrix::discover::SurfaceKind::*;
let is_covered = match node.kind {
// A Function node only EXISTS when unreached → never covered here.
Function => false,
// gRPC: covered iff its VERB (the handler fn name, the `Service.verb`
// last segment) is test-reachable — the RPC handler has a test.
Grpc => {
let verb = node
.id
.rsplit('.')
.next()
.unwrap_or(&node.id)
.to_lowercase();
reachable_idents.contains(&verb)
}
// Discrete surfaces: covered iff a same-named test-reachable handler.
VizTab | CliCommand | McpTool => reachable_idents.contains(&node.id.to_lowercase()),
// facett components have their OWN verdict — the REAL headless-harness
// drive (`covered_facetts` over the fed `FacetRow::driven`), added below,
// NOT the handler-name rule. A never-driven component stays uncovered.
FacettComponent => false,
// UI atoms have their OWN verdict (the live atom-walk's ran-and-clean
// rule), added below via `covered_atoms` — not the handler-name rule.
UiAtom => false,
// Benchers have their OWN verdict — the `inventory`-seam discovery
// (`covered_benchers` over the fed `BencherRow::has_bencher`), added
// below, NOT the handler-name rule. A hot-path with no registered
// bencher stays uncovered.
Bencher => false,
};
if is_covered {
covered.insert(node.key_str());
}
}
// UI atoms: covered ⟺ the atom\'s tab RAN and its state was CLEAN (no error
// atom). This is the live atom-walk\'s verdict, computed by the pure
// `covered_atoms` over the fed `DiscoveredAtom`s — so a `ui_atom` surface node
// the gate counts is only green when the viz actually rendered it error-free.
covered.extend(covered_atoms(&inputs.atoms));
// facett components: covered ⟺ the component was actually DRIVEN clean through
// the `facett_core::harness` (its `driven` flag), NOT a static handler-name
// match. This REPLACES the name-match verdict for `FacettComponent` nodes — the
// peer of the atom-walk's ran-and-clean rule above.
covered.extend(covered_facetts(&inputs.facett));
// Benchers: covered ⟺ a registered bencher (found via the `inventory` seam)
// targets the hot-path (its `has_bencher` flag), computed by the pure
// `covered_benchers` over the fed `BencherRow`s. A hot-path with no bencher
// stays an uncovered `Bencher` surface node — the "no bencher" gap Task #39
// makes enforceable, burned down via the checked-in allowlist.
covered.extend(covered_benchers(&inputs.benchers));
// Functions: the gap is exactly `unreached_functions`, so covered fns are the
// reachable ones — but they aren't surface nodes (only unreached fns are), so
// they don't need a covered key. The `total` is over surface nodes only.
(surface, covered)
}
/// Enumerate the FULL nornir surface in one call (plan node n-000's unified
/// entry point). Assembles the discrete-surface [`SurfaceInputs`] from the
/// canonical [`VIZ_TABS`] / [`MCP_TOOLS`] / [`CLI_COMMANDS`] lists (via
/// [`nornir_surface_inputs`]), maps the warehouse `view` into discover rows
/// (via [`knowledge_to_rows`]), and runs [`build_surface_and_covered`] —
/// returning the discovered [`Surface`]. The caller no longer hand-assembles
/// `SurfaceInputs`.
///
/// `cli_commands` lets the binary inject live clap introspection; pass an EMPTY
/// vec to fall back to the canonical [`CLI_COMMANDS`]. `view` is the warehouse
/// knowledge map — function nodes are read from it (the lib stays binary-free,
/// so the warehouse read itself stays at the caller, e.g. [`gather`]'s
/// `load_latest`). For just the surface without coverage, this is the one call.
pub fn enumerate_surface(view: &KnowledgeView, cli_commands: Vec<String>) -> Surface {
let mut inputs = nornir_surface_inputs(cli_commands);
inputs.grpc = grpc_labels_from_view(view);
let (symbols, edges) = knowledge_to_rows(view);
let (surface, _covered) = build_surface_and_covered(&inputs, &symbols, &edges);
surface
}
/// Like [`enumerate_surface`] but ALSO folds in the live atom-walk's discovered
/// [`DiscoveredAtom`]s, so the returned [`Surface`] carries one `ui_atom` node per
/// rendered atom alongside the tabs/CLI/MCP/gRPC/function surface. This is the
/// entry the S3 atom-coverage path uses: the binary/test runs the walk, hands the
/// atoms here, and the completeness gate then counts atoms as surface nodes.
pub fn enumerate_surface_with_atoms(
view: &KnowledgeView,
cli_commands: Vec<String>,
atoms: Vec<DiscoveredAtom>,
) -> Surface {
let mut inputs = nornir_surface_inputs(cli_commands);
inputs.grpc = grpc_labels_from_view(view);
inputs.atoms = atoms;
let (symbols, edges) = knowledge_to_rows(view);
let (surface, _covered) = build_surface_and_covered(&inputs, &symbols, &edges);
surface
}
/// **S3 atom-coverage PRODUCER** — turn a batch of live atom-walk results into
/// `surface_coverage` [`CoverageRow`]s the gate reads, WITHOUT needing the
/// warehouse knowledge map. One row per discovered atom: `kind = "ui_atom"`,
/// `id = "<tab>/<label>"`, `surface_key = "ui_atom:<tab>/<label>@na"` (the
/// [`SurfaceNode::key_str`](nornir_testmatrix::discover::SurfaceNode) convention),
/// `verdict = covered` iff the atom\'s tab ran AND its state was clean (no error
/// atom), else `missing`. Append these via
/// [`append_surface_coverage`](crate::warehouse::surface_coverage::append_surface_coverage)
/// and `autonom`/the gate counts them like any other surface node (same table,
/// same `CoverageSummary::from_rows` roll-up).
///
/// Deduped by `(tab, label)` so a label that maps to several galley nodes is one
/// row. The role is recorded in the row\'s `reason` (`"role:Button"`) since a
/// `ui_atom` carries no allowlist reason — keeping the warehouse row self-describing.
pub fn atom_coverage_rows(
run_id: &str,
workspace: &str,
atoms: &[DiscoveredAtom],
ts_micros: i64,
) -> Vec<CoverageRow> {
let mut surface = Surface::new();
surface.extend(ui_atoms(atoms));
let covered = covered_atoms(atoms);
// Map role onto each node so we can stamp it into the row reason.
let role_for = |id: &str| -> String {
atoms
.iter()
.find(|a| format!("{}/{}", a.tab, a.label) == id)
.map(|a| {
if a.role.is_empty() {
String::new()
} else {
format!("role:{}", a.role)
}
})
.unwrap_or_default()
};
let mut rows: Vec<CoverageRow> = surface
.nodes
.iter()
.map(|node| {
let key = node.key_str();
let (verdict, reason) = if covered.contains(&key) {
(Verdict::Covered, role_for(&node.id))
} else {
(Verdict::Missing, role_for(&node.id))
};
CoverageRow::from_node(run_id, workspace, node, verdict, &reason, ts_micros)
})
.collect();
rows.sort_by(|a, b| a.surface_key.cmp(&b.surface_key));
rows
}
/// gRPC handler labels (`Service.verb`) discovered in the knowledge map — the
/// values of [`crate::arch::grpc_handlers_from_symbols`], sorted + deduped. This
/// is autonom FINDING the already-marked gRPC handlers (a tonic handler impl IS
/// the mark); it never assigns the mark.
fn grpc_labels_from_view(view: &KnowledgeView) -> Vec<String> {
let mut labels: Vec<String> = crate::arch::grpc_handlers_from_symbols(&view.symbols)
.into_values()
.collect();
labels.sort();
labels.dedup();
labels
}
// ─── the gate (gather → compute → persist) ─────────────────────────────────
/// Gather the gate state for `workspace` against the checked-in `allowlist`,
/// from a door that reaches ONLY the warehouse. The surface is the code
/// room's, so this fails CLOSED (`no_room`); the doors that hold a room call
/// [`gather_in`].
pub fn gather(
wh: &IcebergWarehouse,
workspace: &str,
repo: &str,
inputs: &SurfaceInputs,
allowlist: Allowlist,
run_id: &str,
) -> Result<GateState> {
gather_in(&crate::code_room::Reach { room: None, warehouse: Some(wh) }, workspace, repo, inputs, allowlist, run_id)
}
/// [`gather`] over the code room the door reaches.
pub fn gather_in(
reach: &crate::code_room::Reach<'_>,
workspace: &str,
repo: &str,
inputs: &SurfaceInputs,
allowlist: Allowlist,
run_id: &str,
) -> Result<GateState> {
gather_with_utfallsrum_in(reach, workspace, repo, inputs, allowlist, run_id, BTreeMap::new(), 0)
}
/// Like [`gather`] but applies the **utfallsrum threshold** — see
/// [`gather_with_utfallsrum_in`]. A door with only the warehouse: fails closed.
#[allow(clippy::too_many_arguments)]
pub fn gather_with_utfallsrum(
wh: &IcebergWarehouse,
workspace: &str,
repo: &str,
inputs: &SurfaceInputs,
allowlist: Allowlist,
run_id: &str,
utfallsrum: BTreeMap<String, UtfallsrumSummary>,
utfallsrum_threshold: usize,
) -> Result<GateState> {
gather_with_utfallsrum_in(
&crate::code_room::Reach { room: None, warehouse: Some(wh) },
workspace,
repo,
inputs,
allowlist,
run_id,
utfallsrum,
utfallsrum_threshold,
)
}
/// The gate over the code room. The fn surface is the repo's NAMEABLE API
/// (effective visibility `direct` / `reexported`, from the rustc driver); the
/// roots are its `#[test]` fns; a fn is covered iff a test reaches it over
/// CALLS / TESTS — walked by the kept [`build_surface_and_covered`] at the
/// name granularity its discrete surfaces (tabs / CLI / MCP / gRPC handlers)
/// match on. gRPC handlers come from the repo's impl rows as before.
///
/// **Utfallsrum threshold**: a surface that ran end-to-end is only counted as
/// covered when its function(s) swept at least `utfallsrum_threshold` outcome
/// classes (`utfallsrum` maps `surface_key → the measured outcome-space
/// summary`). `0` is the legacy behaviour (ran ⟺ covered).
///
/// **Fails CLOSED** whenever the room cannot give the surface — the repo is
/// not consumed, the door has no room, the log carries no visibility: a
/// completeness gate over a surface it cannot read would report it
/// covered-by-absence. The error names the room's state.
#[allow(clippy::too_many_arguments)]
pub fn gather_with_utfallsrum_in(
reach: &crate::code_room::Reach<'_>,
workspace: &str,
repo: &str,
inputs: &SurfaceInputs,
allowlist: Allowlist,
run_id: &str,
utfallsrum: BTreeMap<String, UtfallsrumSummary>,
utfallsrum_threshold: usize,
) -> Result<GateState> {
use crate::code_room::reader;
let refuse = |what: &str, r: crate::code_room::Refusal| {
anyhow::anyhow!(
"autonom completeness gate (run {run_id}, workspace `{workspace}`, repo `{repo}`): the code room cannot give {what} — {r} [{}]. \
Refusing (fail closed) rather than gating over a surface it cannot enumerate.",
r.state
)
};
let api = reader::api_surface(reach, repo).map_err(|r| refuse("the public API surface", r))?;
let view = reader::knowledge_view(reach, repo).map_err(|r| refuse("the repo's gRPC handlers", r))?;
// Enrich the discrete inputs with the gRPC handlers FOUND in this repo, so
// every RPC is a counted surface (autonom finds the marks).
let mut inputs = inputs.clone();
inputs.grpc = grpc_labels_from_view(&view);
let symbols: Vec<SymbolRow> = api.fns.iter().map(|f| SymbolRow { fqn: f.name.clone(), is_test: f.is_test, label: Some(f.path.clone()) }).collect();
let edges: Vec<CallEdge> = api.calls.iter().map(|(caller, callee)| CallEdge { caller: caller.clone(), callee: callee.clone() }).collect();
let (surface, ran) = build_surface_and_covered(&inputs, &symbols, &edges);
// The covered set requires BOTH ran AND the utfallsrum threshold (≥K classes
// swept). With threshold 0 / empty map this is exactly `ran` (legacy).
let covered = nornir_testmatrix::coverage::covered_with_utfallsrum(&ran, &utfallsrum, utfallsrum_threshold);
let report = GateReport::compute(run_id, workspace, &surface, &covered, &allowlist);
Ok(GateState { surface, covered, allowlist, report, utfallsrum, utfallsrum_threshold })
}
/// Persist a gate run's full per-node coverage rows to `surface_coverage`.
pub fn persist(
wh: &IcebergWarehouse,
state: &GateState,
run_id: &str,
workspace: &str,
) -> Result<()> {
let ts = chrono::Utc::now().timestamp_micros();
let rows = rows_for_with_utfallsrum(
run_id,
workspace,
&state.surface,
&state.covered,
&state.allowlist,
&state.utfallsrum,
ts,
);
wh.block_on(append_surface_coverage(wh, &rows))
}
/// Re-seed the allowlist with every currently-uncovered surface node (the
/// `--seed-allowlist` mode). Preserves existing reasons. Returns the new
/// allowlist for the caller to serialize to `autonom-allow.toml`.
pub fn reseed(state: &GateState) -> Allowlist {
seed_allowlist(&state.surface, &state.covered, &state.allowlist)
}
/// Read the latest persisted gate verdict for `workspace` and roll it into a
/// [`CoverageSummary`] — what the viz Test pane + `test_coverage` tool show.
pub fn read_latest_summary(wh: &IcebergWarehouse, workspace: &str) -> Result<CoverageSummary> {
let rows = wh.block_on(latest_surface_coverage(wh, workspace))?;
Ok(CoverageSummary::from_rows(&rows))
}
/// Read all persisted rows for a run (for the CLI `--json` dump).
pub fn read_run(wh: &IcebergWarehouse, run_id: &str) -> Result<Vec<CoverageRow>> {
wh.block_on(query_surface_coverage(
wh,
&CoverageSelector::Run(run_id.to_string()),
))
}
/// AUT5 release-blocking gate helper: gather the surface vs covered verdict for
/// `repo` (canonical nornir surface inputs + the repo's checked-in allowlist),
/// PERSIST the run to `surface_coverage`, and return the [`GateState`]. The
/// release pipeline (`release gate-all` / the server `gate_all` RPC) calls this
/// and then applies [`crate::release::gate::coverage_gate`] to the returned
/// `report` — RED iff `Surface − Covered ≠ ∅` (any uncovered+un-allowlisted
/// surface) OR a stale allowlist entry. One call so the CLI, the server RPC, and
/// the `release_gate_coverage` MCP tool share identical behaviour (CLI parity).
///
/// `cli_commands` lets the binary inject live clap introspection; pass EMPTY to
/// fall back to the canonical [`CLI_COMMANDS`]. `run_id` stamps the persisted run.
pub fn gate_coverage_for_repo(
wh: &IcebergWarehouse,
workspace: &str,
repo: &str,
repo_root: &Path,
cli_commands: Vec<String>,
run_id: &str,
) -> Result<GateState> {
gate_coverage_for_repo_in(&crate::code_room::Reach { room: None, warehouse: Some(wh) }, workspace, repo, repo_root, cli_commands, run_id)
}
/// [`gate_coverage_for_repo`] over the code room the door reaches (the
/// verdict is persisted to the warehouse the same door reaches).
pub fn gate_coverage_for_repo_in(
reach: &crate::code_room::Reach<'_>,
workspace: &str,
repo: &str,
repo_root: &Path,
cli_commands: Vec<String>,
run_id: &str,
) -> Result<GateState> {
let inputs = nornir_surface_inputs(cli_commands);
let allowlist = load_allowlist(&allowlist_path(repo_root))?;
let state = gather_in(reach, workspace, repo, &inputs, allowlist, run_id)?;
let wh = reach.warehouse.ok_or_else(|| anyhow::anyhow!("autonom completeness gate: this door reaches no warehouse to persist the verdict to"))?;
persist(wh, &state, run_id, workspace)?;
Ok(state)
}
// ─── allowlist file I/O ─────────────────────────────────────────────────────
//
// The file model now lives in the shared `autonom` crate (edda) so robotui and
// dwarves can read/write an allowlist without nornir. These are thin re-exports
// that keep every existing `crate::autonom::{allowlist_path, load_allowlist,
// save_allowlist}` call site working verbatim.
pub use edda_autonom::allowlist::{allowlist_path, load_allowlist, save_allowlist};
#[cfg(test)]
mod tests {
use super::*;
use nornir_testmatrix::discover::SurfaceKind;
fn sym(name: &str, module: &str, is_test_mod: bool) -> SymbolRow {
// Build via the knowledge mapping so is_test detection is exercised.
let krow = KSymbolRow {
crate_name: "nornir".into(),
module_path: if is_test_mod {
format!("{module}::tests")
} else {
module.into()
},
item_kind: "fn".into(),
item_name: name.into(),
visibility: "pub".into(),
file: "src/x.rs".into(),
line: 1,
doc_lines: 0,
signature: None,
};
let view = KnowledgeView::new(vec![krow], vec![]);
knowledge_to_rows(&view).0.pop().unwrap()
}
#[test]
fn knowledge_mapping_detects_test_roots_and_idents() {
let view = KnowledgeView::new(
vec![
KSymbolRow {
crate_name: "nornir".into(),
module_path: "nornir::foo::tests".into(),
item_kind: "fn".into(),
item_name: "test_it".into(),
visibility: "".into(),
file: "f.rs".into(),
line: 1,
doc_lines: 0,
signature: None,
},
KSymbolRow {
crate_name: "nornir".into(),
module_path: "nornir::foo".into(),
item_kind: "fn".into(),
item_name: "render".into(),
visibility: "pub".into(),
file: "f.rs".into(),
line: 2,
doc_lines: 0,
signature: None,
},
// a non-fn item is dropped.
KSymbolRow {
crate_name: "nornir".into(),
module_path: "nornir::foo".into(),
item_kind: "struct".into(),
item_name: "Thing".into(),
visibility: "pub".into(),
file: "f.rs".into(),
line: 3,
doc_lines: 0,
signature: None,
},
],
vec![CallEdgeRow {
crate_name: "nornir".into(),
caller_path: "nornir::foo::tests::test_it".into(),
callee_ident: "render".into(),
call_kind: "call".into(),
file: "f.rs".into(),
line: 1,
}],
);
let (symbols, edges) = knowledge_to_rows(&view);
assert_eq!(symbols.len(), 2, "only fn items, struct dropped");
let test_it = symbols.iter().find(|s| s.fqn == "test_it").unwrap();
assert!(test_it.is_test, "test_ name in tests module is a root");
let render = symbols.iter().find(|s| s.fqn == "render").unwrap();
assert!(!render.is_test);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].caller, "test_it");
assert_eq!(edges[0].callee, "render", "callee reduced to last segment");
}
#[test]
fn unreached_fn_is_a_gap_reached_fn_is_not() {
// test_it → render (reached) ; orphan (unreached).
let symbols = vec![
sym("test_it", "nornir::foo", true),
sym("render", "nornir::foo", false),
sym("orphan", "nornir::foo", false),
];
let edges = vec![CallEdge {
caller: "test_it".into(),
callee: "render".into(),
}];
let inputs = SurfaceInputs::default();
let (surface, covered) = build_surface_and_covered(&inputs, &symbols, &edges);
// Only the orphan fn is a surface node (unreached); render is reached.
let fn_nodes: Vec<_> = surface
.nodes
.iter()
.filter(|n| n.kind == SurfaceKind::Function)
.collect();
assert_eq!(fn_nodes.len(), 1, "only the unreached fn is surface");
assert_eq!(fn_nodes[0].id, "orphan");
// The orphan is NOT in covered → it's the gap.
assert!(!covered.contains(&fn_nodes[0].key_str()));
let report = GateReport::compute("r", "ws", &surface, &covered, &Allowlist::new());
assert!(!report.is_green(), "an unreached fn makes the gate RED");
assert_eq!(report.gap.missing.len(), 1);
assert_eq!(report.gap.missing[0].id, "orphan");
}
/// Task #39: `bench_rows` maps the canonical hot-paths against the LIVE
/// inventory registry — a hot-path with a matching registered bencher id
/// (equal, or `<key>_`/`<key>.` prefixed) is covered; one with none is not.
#[test]
fn bench_rows_resolve_coverage_from_registered_ids() {
let registered = vec![
"nornir.dep_graph_build".to_string(), // covers "nornir.dep_graph"
"nornir.dep_graph_build_ext".to_string(), // also covers it
];
let rows = bench_rows(BENCH_HOT_PATHS, ®istered);
assert_eq!(rows.len(), BENCH_HOT_PATHS.len());
let dg = rows
.iter()
.find(|r| r.module == "nornir.dep_graph")
.unwrap();
assert!(
dg.has_bencher,
"dep_graph_build satisfies the dep_graph hot-path"
);
assert_eq!(dg.bench_ids.len(), 2, "both dep_graph benchers matched");
let vs = rows
.iter()
.find(|r| r.module == "nornir.vector_search")
.unwrap();
assert!(
!vs.has_bencher,
"no registered bencher targets vector_search"
);
}
/// Task #39: a hot-path lacking a bencher makes the bencher-coverage gate row
/// RED (an uncovered `Bencher` surface node); allowlisting it flips green —
/// the SAME honest burn-down mechanism the other surfaces use.
#[test]
fn missing_bencher_reds_gate_allowlist_excuses_it() {
let inputs = SurfaceInputs {
benchers: vec![
BencherRow {
module: "nornir.dep_graph".into(),
has_bencher: true,
bench_ids: vec!["nornir.dep_graph_build".into()],
},
BencherRow {
module: "nornir.vector_search".into(),
has_bencher: false,
bench_ids: vec![],
},
],
..Default::default()
};
let (surface, covered) = build_surface_and_covered(&inputs, &[], &[]);
// Two Bencher surface nodes; the benched one is covered, the other is not.
assert_eq!(surface.count_kind(SurfaceKind::Bencher), 2);
assert!(covered.contains("bencher:nornir.dep_graph@na"));
assert!(!covered.contains("bencher:nornir.vector_search@na"));
// No allowlist → the un-benched hot-path is the lone gap → gate RED.
let report = GateReport::compute("r", "ws", &surface, &covered, &Allowlist::new());
assert!(
!report.is_green(),
"a hot-path with no bencher reds the gate"
);
assert_eq!(report.gap.missing.len(), 1);
assert_eq!(
report.gap.missing[0].key_str(),
"bencher:nornir.vector_search@na"
);
assert_eq!(report.gap.missing[0].kind, SurfaceKind::Bencher);
// Allowlist the known-missing hot-path → GREEN (visible burn-down excuse).
let allow = Allowlist {
entries: vec![crate::warehouse::surface_coverage::AllowEntry {
key: "bencher:nornir.vector_search@na".into(),
reason: "TODO(#39): wire a bencher for the vector-search hot-path".into(),
}],
};
let report2 = GateReport::compute("r", "ws", &surface, &covered, &allow);
assert!(
report2.is_green(),
"an allowlisted hot-path passes the gate row"
);
assert_eq!(report2.gap.allowlisted.len(), 1);
}
/// S3: the atom-walk's discovered atoms become `ui_atom` surface nodes that
/// the completeness gate COUNTS, and the producer's verdict (covered <=> a
/// clean tab, missing <=> a dirty tab) round-trips into the rows. Proves
/// `build_surface_and_covered` + `atom_coverage_rows` agree.
#[test]
fn ui_atoms_become_counted_surface_nodes_with_clean_dirty_verdicts() {
let atoms = vec![
DiscoveredAtom {
tab: "test".into(),
label: "Run full matrix".into(),
role: "Button".into(),
ran: true,
clean: true, // clean tab -> covered
},
DiscoveredAtom {
tab: "nornir".into(),
label: "is not served".into(),
role: "Label".into(),
ran: true,
clean: false, // dirty tab -> missing
},
];
let inputs = SurfaceInputs {
viz_tabs: vec![],
cli_commands: vec![],
mcp_tools: vec![],
facett: vec![],
grpc: vec![],
atoms: atoms.clone(),
benchers: vec![],
};
let (surface, covered) = build_surface_and_covered(&inputs, &[], &[]);
// Two ui_atom nodes are in the discovered surface (the gate counts them).
let ui_nodes: Vec<_> = surface
.nodes
.iter()
.filter(|n| n.kind == SurfaceKind::UiAtom)
.collect();
assert_eq!(ui_nodes.len(), 2, "both atoms are counted surface nodes");
// The clean-tab atom is covered; the dirty-tab atom is NOT.
assert!(
covered.contains("ui_atom:test/Run full matrix@na"),
"clean atom covered"
);
assert!(
!covered.contains("ui_atom:nornir/is not served@na"),
"dirty (error) tab atom is not covered",
);
// The producer agrees: a covered + a missing row, with the role in reason.
let rows = atom_coverage_rows("r-atom", "nornir", &atoms, 7);
assert_eq!(rows.len(), 2);
let run = rows
.iter()
.find(|r| r.id == "test/Run full matrix")
.unwrap();
assert_eq!(run.verdict(), Verdict::Covered);
assert_eq!(run.reason, "role:Button", "role recorded in the row reason");
let dirty = rows
.iter()
.find(|r| r.id == "nornir/is not served")
.unwrap();
assert_eq!(dirty.verdict(), Verdict::Missing);
}
#[test]
fn discrete_surface_covered_when_handler_reachable_else_seed_to_green() {
// A test reaches `test_tab` (the Test viz tab handler) but nothing reaches
// the `bench_tab` handler. The Test tab is covered; Bench is a gap until
// seeded.
let symbols = vec![
sym("test_root", "nornir::viz", true),
sym("Test", "nornir::viz", false),
];
let edges = vec![CallEdge {
caller: "test_root".into(),
callee: "Test".into(),
}];
let inputs = SurfaceInputs {
viz_tabs: vec!["Test".into(), "Bench".into()],
cli_commands: vec!["coverage".into()],
mcp_tools: vec!["test_coverage".into()],
facett: vec![],
grpc: vec![],
atoms: vec![],
benchers: vec![],
};
let (surface, covered) = build_surface_and_covered(&inputs, &symbols, &edges);
// Test@fat + Test@thin are covered (handler `Test` is reachable).
assert!(covered.contains("viz_tab:Test@fat"));
assert!(covered.contains("viz_tab:Test@thin"));
// Bench is NOT covered (no reachable handler).
assert!(!covered.contains("viz_tab:Bench@fat"));
let report = GateReport::compute("r", "ws", &surface, &covered, &Allowlist::new());
assert!(
!report.is_green(),
"Bench/cli/mcp are uncovered → RED before seeding"
);
// SEED the allowlist → every uncovered surface excused → GREEN now.
let seeded = seed_allowlist(&surface, &covered, &Allowlist::new());
let report2 = GateReport::compute("r", "ws", &surface, &covered, &seeded);
assert!(
report2.is_green(),
"seeded allowlist makes the gate green now"
);
// The covered Test tab is NOT seeded (no excuse needed).
assert!(!seeded.entries.iter().any(|e| e.key == "viz_tab:Test@fat"));
// Bench/cli/mcp ARE seeded.
assert!(seeded.entries.iter().any(|e| e.key == "viz_tab:Bench@fat"));
assert!(
seeded
.entries
.iter()
.any(|e| e.key == "cli_command:coverage@na")
);
assert!(
seeded
.entries
.iter()
.any(|e| e.key == "mcp_tool:test_coverage@na")
);
}
/// A door that reaches no code room FAILS CLOSED: the surface is the
/// room's, and an unreadable surface must never read as "everything covered".
#[test]
fn the_gate_fails_closed_when_the_door_has_no_code_room() {
let dir = tempfile::tempdir().unwrap();
let wh = IcebergWarehouse::open(dir.path()).unwrap();
let err = gather(&wh, "ws", "myrepo", &SurfaceInputs::default(), Allowlist::new(), "run-1")
.err()
.expect("the gate must refuse, not pass");
let msg = format!("{err:#}");
assert!(msg.contains("[no_room]") && msg.contains("fail closed") && msg.contains("myrepo"), "{msg}");
}
/// The gate over a consumed repo: the surface is its nameable API, the
/// roots its tests. `checkout` is public and no test reaches it; `charge`
/// is `pub` in a private module (not API) and never counted.
#[cfg(feature = "code-room")]
#[test]
fn the_gate_reads_the_public_api_and_the_tests_from_the_code_room() {
use crate::code_room::{Reach, reader::fixture};
let reach = Reach::room_only(&fixture::shop().handle);
let state = gather_in(&reach, "ws", "shop", &SurfaceInputs::default(), Allowlist::new(), "run-1").unwrap();
let fns: Vec<(&str, &str)> = state.surface.nodes.iter().filter(|n| n.kind == SurfaceKind::Function).map(|n| (n.id.as_str(), n.label.as_str())).collect();
assert_eq!(fns, vec![("checkout", "shop::checkout")], "add / total / cart::open are public and test-reached; charge / audit are not API");
assert!(state.covered.is_empty());
assert!(crate::release::gate::coverage_gate(&state.report).is_err(), "an uncovered public fn is RED: {}", state.summary());
// allow-listed with a reason, the same surface is green
let mut allow = Allowlist::new();
allow.entries.push(nornir_testmatrix::coverage::AllowEntry {
key: state.surface.nodes[0].key_str(),
reason: "entry point, exercised by the appliance test".into(),
});
let state = gather_in(&reach, "ws", "shop", &SurfaceInputs::default(), allow, "run-2").unwrap();
assert!(crate::release::gate::coverage_gate(&state.report).is_ok(), "{}", state.summary());
// a repo nothing was consumed of, and a workspace nothing was consumed into: closed, by state
for (reach, repo) in [(reach, "nobody"), (Reach::room_only(&fixture::unconsumed().handle), "shop")] {
let err = gather_in(&reach, "ws", repo, &SurfaceInputs::default(), Allowlist::new(), "run-3").err().expect("must refuse");
let msg = format!("{err:#}");
assert!(msg.contains("[not_consumed]") && msg.contains("fail closed"), "{msg}");
}
}
#[test]
fn grpc_handler_is_a_thin_surface_covered_when_verb_reachable() {
use nornir_testmatrix::discover::{Mode, SurfaceKind};
// A test reaches `submit` (the Bench.Submit handler verb); nothing reaches
// `architecture`. So Bench.Submit is covered, Viz.Architecture is a gap —
// an RPC with no test now OUTS itself as a red surface.
let symbols = vec![
sym("test_root", "nornir::server", true),
sym("submit", "nornir::server", false),
];
let edges = vec![CallEdge {
caller: "test_root".into(),
callee: "submit".into(),
}];
let inputs = SurfaceInputs {
viz_tabs: vec![],
cli_commands: vec![],
mcp_tools: vec![],
facett: vec![],
grpc: vec!["Bench.Submit".into(), "Viz.Architecture".into()],
atoms: vec![],
benchers: vec![],
};
let (surface, covered) = build_surface_and_covered(&inputs, &symbols, &edges);
// Both handlers are Thin-mode gRPC surface nodes (the UI/gRPC layer mark).
let grpc: Vec<_> = surface
.nodes
.iter()
.filter(|n| n.kind == SurfaceKind::Grpc)
.collect();
assert_eq!(grpc.len(), 2);
assert!(grpc.iter().all(|n| n.mode == Mode::Thin));
// Covered iff the verb (handler fn) is test-reachable.
assert!(
covered.contains("grpc:Bench.Submit@thin"),
"verb `submit` is reached"
);
assert!(
!covered.contains("grpc:Viz.Architecture@thin"),
"verb `architecture` is not"
);
}
#[test]
fn enumerate_surface_yields_canonical_nornir_surface_in_one_call() {
// Feed a real warehouse-shaped knowledge view (one test root reaching the
// `Funnel` viz handler, plus an unreached `orphan` fn) and assert the
// single-call entry point returns the FULL canonical surface.
let view = KnowledgeView::new(
vec![
KSymbolRow {
crate_name: "nornir".into(),
module_path: "nornir::viz::tests".into(),
item_kind: "fn".into(),
item_name: "test_funnel".into(),
visibility: "".into(),
file: "f.rs".into(),
line: 1,
doc_lines: 0,
signature: None,
},
KSymbolRow {
crate_name: "nornir".into(),
module_path: "nornir::viz".into(),
item_kind: "fn".into(),
item_name: "Funnel".into(),
visibility: "pub".into(),
file: "f.rs".into(),
line: 2,
doc_lines: 0,
signature: None,
},
KSymbolRow {
crate_name: "nornir".into(),
module_path: "nornir::misc".into(),
item_kind: "fn".into(),
item_name: "orphan".into(),
visibility: "pub".into(),
file: "f.rs".into(),
line: 3,
doc_lines: 0,
signature: None,
},
],
vec![CallEdgeRow {
crate_name: "nornir".into(),
caller_path: "nornir::viz::tests::test_funnel".into(),
callee_ident: "Funnel".into(),
call_kind: "call".into(),
file: "f.rs".into(),
line: 1,
}],
);
// ONE call — no hand-assembled SurfaceInputs; empty cli → canonical fallback.
let surface = enumerate_surface(&view, Vec::new());
// The canonical viz tabs are all present (assert known names explicitly).
let viz_ids: BTreeSet<&str> = surface
.nodes
.iter()
.filter(|n| n.kind == SurfaceKind::VizTab)
.map(|n| n.id.as_str())
.collect();
assert!(viz_ids.contains("Funnel"), "Funnel tab must be enumerated");
assert!(
viz_ids.contains("Warehouse"),
"Warehouse tab must be enumerated"
);
// Every canonical tab made it into the surface.
for tab in VIZ_TABS {
assert!(
viz_ids.contains(tab),
"canonical viz tab `{tab}` missing from surface"
);
}
// MCP tools: count > 0 and equal to the canonical list size.
let mcp_count = surface
.nodes
.iter()
.filter(|n| n.kind == SurfaceKind::McpTool)
.count();
assert!(mcp_count > 0, "MCP tools must be enumerated");
assert_eq!(
mcp_count,
MCP_TOOLS.len(),
"all canonical MCP tools enumerated"
);
assert!(
surface
.nodes
.iter()
.any(|n| n.kind == SurfaceKind::McpTool && n.id == "funnel_show"),
"a known MCP tool (funnel_show) must be present",
);
// CLI commands: non-empty, fell back to the canonical list.
let cli_count = surface
.nodes
.iter()
.filter(|n| n.kind == SurfaceKind::CliCommand)
.count();
assert!(cli_count > 0, "CLI commands must be enumerated");
assert_eq!(cli_count, CLI_COMMANDS.len(), "canonical CLI fallback used");
// The unreached `orphan` fn surfaced as a Function node; the reached
// `Funnel` handler did NOT (only gaps are function nodes).
let fn_ids: BTreeSet<&str> = surface
.nodes
.iter()
.filter(|n| n.kind == SurfaceKind::Function)
.map(|n| n.id.as_str())
.collect();
assert!(fn_ids.contains("orphan"), "unreached fn must surface");
assert!(
!fn_ids.contains("Funnel"),
"test-reached handler is not a gap"
);
}
#[test]
fn allowlist_toml_round_trips_through_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".nornir").join("autonom-allow.toml");
let al = Allowlist {
entries: vec![super::super::warehouse::surface_coverage::AllowEntry {
key: "viz_tab:Bench@thin".into(),
reason: "Bench thin variant renders client-side only — no RPC surface to assert"
.into(),
}],
};
save_allowlist(&path, &al).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
assert!(
text.contains("BURN IT DOWN"),
"header carries the burn-down rule"
);
assert!(text.contains("viz_tab:Bench@thin"));
let back = load_allowlist(&path).unwrap();
assert_eq!(back, al, "allowlist round-trips through TOML");
// A missing file loads as empty (not an error).
let empty = load_allowlist(&dir.path().join("nope.toml")).unwrap();
assert!(empty.entries.is_empty());
}
}