use std::collections::BTreeSet;
use std::path::Path;
use anyhow::{Context, Result};
use crate::knowledge::query::{load_latest, KnowledgeView};
use crate::knowledge::symbols::{CallEdgeRow, SymbolRow as KSymbolRow};
use crate::warehouse::iceberg::IcebergWarehouse;
use crate::warehouse::surface_coverage::{
append_surface_coverage, latest_surface_coverage, query_surface_coverage, rows_for,
seed_allowlist, Allowlist, CoverageRow, CoverageSelector, CoverageSummary, GateReport,
};
use nornir_testmatrix::discover::{
cli_commands, facett_components, mcp_tools, unreached_functions, viz_tabs, CallEdge, FacetRow,
Surface, SymbolRow,
};
#[derive(Debug, Clone, Default)]
pub struct SurfaceInputs {
pub viz_tabs: Vec<String>,
pub cli_commands: Vec<String>,
pub mcp_tools: Vec<String>,
pub facett: Vec<FacetRow>,
}
pub const VIZ_TABS: &[&str] = &[
"Timeline",
"DepGraph",
"CallGraph",
"Funnel",
"TimeTravel",
"LiveRun",
"Release",
"Knowledge",
"Warehouse",
"Mcp",
"Search",
"Gates",
"Bench",
"Test",
"Leaderboard",
"Security",
];
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",
];
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",
];
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(),
facett: Vec::new(),
}
}
pub struct GateState {
pub surface: Surface,
pub covered: BTreeSet<String>,
pub allowlist: Allowlist,
pub report: GateReport,
}
impl GateState {
pub fn summary(&self) -> String {
self.report.summary()
}
}
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)
}
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_")
}
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(facett_components(&inputs.facett))
.extend(unreached_functions(symbols, edges));
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 {
Function => false,
VizTab | CliCommand | McpTool | FacettComponent => {
reachable_idents.contains(&node.id.to_lowercase())
}
};
if is_covered {
covered.insert(node.key_str());
}
}
(surface, covered)
}
pub fn gather(
wh: &IcebergWarehouse,
workspace: &str,
repo: &str,
inputs: &SurfaceInputs,
allowlist: Allowlist,
run_id: &str,
) -> Result<GateState> {
let view = load_latest(wh, repo)
.with_context(|| format!("load knowledge map for repo `{repo}`"))?;
let (symbols, edges) = knowledge_to_rows(&view);
let (surface, covered) = build_surface_and_covered(inputs, &symbols, &edges);
let report = GateReport::compute(run_id, workspace, &surface, &covered, &allowlist);
Ok(GateState { surface, covered, allowlist, report })
}
pub fn persist(wh: &IcebergWarehouse, state: &GateState, run_id: &str, workspace: &str) -> Result<()> {
let ts = chrono::Utc::now().timestamp_micros();
let rows = rows_for(
run_id,
workspace,
&state.surface,
&state.covered,
&state.allowlist,
ts,
);
wh.block_on(append_surface_coverage(wh, &rows))
}
pub fn reseed(state: &GateState) -> Allowlist {
seed_allowlist(&state.surface, &state.covered, &state.allowlist)
}
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))
}
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())))
}
pub fn allowlist_path(repo_root: &Path) -> std::path::PathBuf {
repo_root.join(".nornir").join("autonom-allow.toml")
}
pub fn load_allowlist(path: &Path) -> Result<Allowlist> {
if !path.exists() {
return Ok(Allowlist::new());
}
let text = std::fs::read_to_string(path)
.with_context(|| format!("read allowlist {}", path.display()))?;
let al: Allowlist =
toml::from_str(&text).with_context(|| format!("parse allowlist {}", path.display()))?;
Ok(al)
}
pub fn save_allowlist(path: &Path, allowlist: &Allowlist) -> Result<()> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)
.with_context(|| format!("create dir {}", dir.display()))?;
}
let header = "# autonom-allow.toml — the completeness-gate allowlist.\n\
# SEEDED by `nornir test coverage --seed-allowlist` with every currently-\n\
# uncovered surface node. BURN IT DOWN: delete an entry once a real\n\
# inject-assert test covers that surface. A STALE entry (now-covered or\n\
# surface-gone) FAILS the gate — the excuse must not outlive its surface.\n\n";
let body = toml::to_string_pretty(allowlist)
.with_context(|| format!("serialize allowlist {}", path.display()))?;
std::fs::write(path, format!("{header}{body}"))
.with_context(|| format!("write allowlist {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use nornir_testmatrix::discover::SurfaceKind;
fn sym(name: &str, module: &str, is_test_mod: bool) -> SymbolRow {
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 { symbols: vec![krow], calls: vec![] };
knowledge_to_rows(&view).0.pop().unwrap()
}
#[test]
fn knowledge_mapping_detects_test_roots_and_idents() {
let view = KnowledgeView {
symbols: 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,
},
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,
},
],
calls: 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() {
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);
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");
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");
}
#[test]
fn discrete_surface_covered_when_handler_reachable_else_seed_to_green() {
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![],
};
let (surface, covered) = build_surface_and_covered(&inputs, &symbols, &edges);
assert!(covered.contains("viz_tab:Test@fat"));
assert!(covered.contains("viz_tab:Test@thin"));
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");
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");
assert!(!seeded.entries.iter().any(|e| e.key == "viz_tab:Test@fat"));
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"));
}
#[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: "TODO(autonom): wire thin RPC test".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");
let empty = load_allowlist(&dir.path().join("nope.toml")).unwrap();
assert!(empty.entries.is_empty());
}
}