use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceKind {
FacettComponent,
VizTab,
CliCommand,
McpTool,
Grpc,
Function,
}
impl SurfaceKind {
pub fn label(self) -> &'static str {
match self {
SurfaceKind::FacettComponent => "facett_component",
SurfaceKind::VizTab => "viz_tab",
SurfaceKind::CliCommand => "cli_command",
SurfaceKind::McpTool => "mcp_tool",
SurfaceKind::Grpc => "grpc",
SurfaceKind::Function => "function",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
Fat,
Thin,
#[serde(rename = "na")]
NA,
}
impl Mode {
pub fn label(self) -> &'static str {
match self {
Mode::Fat => "fat",
Mode::Thin => "thin",
Mode::NA => "na",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SurfaceNode {
pub kind: SurfaceKind,
pub id: String,
pub mode: Mode,
pub label: String,
pub caps: BTreeSet<String>,
}
impl SurfaceNode {
pub fn key(&self) -> (SurfaceKind, &str, Mode) {
(self.kind, self.id.as_str(), self.mode)
}
pub fn key_str(&self) -> String {
format!("{}:{}@{}", self.kind.label(), self.id, self.mode.label())
}
fn new(kind: SurfaceKind, id: impl Into<String>, mode: Mode) -> Self {
let id = id.into();
Self { kind, label: id.clone(), id, mode, caps: BTreeSet::new() }
}
fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
fn with_caps<I, S>(mut self, caps: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.caps = caps.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FacetRow {
pub component: String,
#[serde(default)]
pub in_registry: bool,
#[serde(default)]
pub has_local_ctor: bool,
#[serde(default)]
pub has_remote_ctor: bool,
#[serde(default)]
pub caps: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolRow {
pub fqn: String,
#[serde(default)]
pub is_test: bool,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallEdge {
pub caller: String,
pub callee: String,
}
pub fn facett_components(rows: &[FacetRow]) -> Vec<SurfaceNode> {
let mut out = Vec::new();
for r in rows.iter().filter(|r| r.in_registry) {
let mk = |mode: Mode| {
SurfaceNode::new(SurfaceKind::FacettComponent, &r.component, mode)
.with_caps(r.caps.iter().cloned())
};
match (r.has_local_ctor, r.has_remote_ctor) {
(false, false) => out.push(mk(Mode::NA)),
(l, t) => {
if l {
out.push(mk(Mode::Fat));
}
if t {
out.push(mk(Mode::Thin));
}
}
}
}
out
}
pub fn viz_tabs<I, S>(tabs: I) -> Vec<SurfaceNode>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut out = Vec::new();
for tab in tabs {
let tab = tab.into();
for mode in [Mode::Fat, Mode::Thin] {
out.push(SurfaceNode::new(SurfaceKind::VizTab, &tab, mode));
}
}
out
}
pub fn cli_commands<I, S>(subcommands: I) -> Vec<SurfaceNode>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
subcommands
.into_iter()
.map(|c| SurfaceNode::new(SurfaceKind::CliCommand, c, Mode::NA))
.collect()
}
pub fn mcp_tools<I, S>(tool_names: I) -> Vec<SurfaceNode>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
tool_names
.into_iter()
.map(|t| SurfaceNode::new(SurfaceKind::McpTool, t, Mode::NA))
.collect()
}
pub fn grpc_handlers<I, S>(labels: I) -> Vec<SurfaceNode>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
labels
.into_iter()
.map(|l| SurfaceNode::new(SurfaceKind::Grpc, l, Mode::Thin))
.collect()
}
pub fn unreached_functions(symbols: &[SymbolRow], edges: &[CallEdge]) -> Vec<SurfaceNode> {
let reachable = test_reachable(symbols, edges);
symbols
.iter()
.filter(|s| !s.is_test && !reachable.contains(s.fqn.as_str()))
.map(|s| {
let label = s.label.clone().unwrap_or_else(|| s.fqn.clone());
SurfaceNode::new(SurfaceKind::Function, &s.fqn, Mode::NA).with_label(label)
})
.collect()
}
pub fn test_reachable<'a>(symbols: &'a [SymbolRow], edges: &'a [CallEdge]) -> BTreeSet<&'a str> {
use std::collections::BTreeMap;
let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for e in edges {
adj.entry(e.caller.as_str()).or_default().push(e.callee.as_str());
}
let mut reached: BTreeSet<&str> = BTreeSet::new();
let mut stack: Vec<&str> = symbols
.iter()
.filter(|s| s.is_test)
.map(|s| s.fqn.as_str())
.collect();
while let Some(n) = stack.pop() {
if !reached.insert(n) {
continue; }
if let Some(callees) = adj.get(n) {
for &c in callees {
if !reached.contains(c) {
stack.push(c);
}
}
}
}
reached
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Surface {
pub nodes: Vec<SurfaceNode>,
}
impl Surface {
pub fn new() -> Self {
Self::default()
}
pub fn extend(&mut self, nodes: impl IntoIterator<Item = SurfaceNode>) -> &mut Self {
self.nodes.extend(nodes);
self
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn count_kind(&self, kind: SurfaceKind) -> usize {
self.nodes.iter().filter(|n| n.kind == kind).count()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Gap {
pub missing: Vec<SurfaceNode>,
pub allowlisted: Vec<SurfaceNode>,
pub covered: usize,
pub total: usize,
}
impl Gap {
pub fn is_clean(&self) -> bool {
self.missing.is_empty()
}
pub fn summary(&self) -> String {
format!(
"{}/{} surface nodes covered · {} allowlisted · {} MISSING — {}",
self.covered,
self.total,
self.allowlisted.len(),
self.missing.len(),
if self.is_clean() { "GREEN" } else { "RED (gap not empty)" },
)
}
}
pub fn compute_gap(
surface: &Surface,
covered: &BTreeSet<String>,
allowlist: &BTreeSet<String>,
) -> Gap {
let mut missing = Vec::new();
let mut allowlisted = Vec::new();
let mut covered_count = 0usize;
for node in &surface.nodes {
let key = node.key_str();
if covered.contains(&key) {
covered_count += 1;
} else if allowlist.contains(&key) {
allowlisted.push(node.clone());
} else {
missing.push(node.clone());
}
}
missing.sort_by_key(|n| n.key_str());
allowlisted.sort_by_key(|n| n.key_str());
Gap {
covered: covered_count,
total: surface.nodes.len(),
missing,
allowlisted,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn caps_of(n: &SurfaceNode) -> Vec<&str> {
n.caps.iter().map(String::as_str).collect()
}
#[test]
fn facett_intersects_registry_and_splits_thin_fat() {
let rows = vec![
FacetRow {
component: "WarehouseView".into(),
in_registry: true,
has_local_ctor: true,
has_remote_ctor: true,
caps: vec!["reads_warehouse".into(), "interactive".into()],
},
FacetRow {
component: "AboutPanel".into(),
in_registry: true,
has_local_ctor: false,
has_remote_ctor: false,
caps: vec![],
},
FacetRow {
component: "ScratchView".into(),
in_registry: false,
has_local_ctor: true,
has_remote_ctor: true,
caps: vec![],
},
];
let nodes = facett_components(&rows);
assert_eq!(nodes.len(), 3, "registry intersection + thin/fat split");
let wh: Vec<_> = nodes.iter().filter(|n| n.id == "WarehouseView").collect();
assert_eq!(wh.len(), 2, "data component yields a fat AND a thin node");
let modes: BTreeSet<_> = wh.iter().map(|n| n.mode).collect();
assert!(modes.contains(&Mode::Fat) && modes.contains(&Mode::Thin));
assert_eq!(caps_of(wh[0]), vec!["interactive", "reads_warehouse"]);
assert!(
nodes.iter().any(|n| n.id == "AboutPanel" && n.mode == Mode::NA),
"stateless registered component is a single NA node"
);
assert!(
!nodes.iter().any(|n| n.id == "ScratchView"),
"a Facet impl absent from registry() is NOT a surface"
);
}
#[test]
fn viz_tabs_yield_thin_and_fat_each() {
let nodes = viz_tabs(["Search", "Test", "Bench"]);
assert_eq!(nodes.len(), 6, "3 tabs × 2 modes");
let test_thin = nodes
.iter()
.find(|n| n.id == "Test" && n.mode == Mode::Thin)
.expect("Test tab has a thin node — the bug class autonom kills");
assert_eq!(test_thin.kind, SurfaceKind::VizTab);
assert_eq!(test_thin.key_str(), "viz_tab:Test@thin");
}
#[test]
fn cli_and_mcp_model_provided_lists() {
let cli = cli_commands(["test", "bench", "viz"]);
assert_eq!(cli.len(), 3);
assert!(cli.iter().all(|n| n.kind == SurfaceKind::CliCommand && n.mode == Mode::NA));
let mcp = mcp_tools(["search", "build_order", "viz_state"]);
assert_eq!(mcp.len(), 3);
assert_eq!(mcp[1].key_str(), "mcp_tool:build_order@na");
}
#[test]
fn grpc_handlers_make_thin_nodes() {
let g = grpc_handlers(["Viz.Architecture", "Bench.Submit"]);
assert_eq!(g.len(), 2);
assert!(g.iter().all(|n| n.kind == SurfaceKind::Grpc && n.mode == Mode::Thin));
assert_eq!(SurfaceKind::Grpc.label(), "grpc");
assert_eq!(g[0].key_str(), "grpc:Viz.Architecture@thin");
}
#[test]
fn unreached_fn_shows_in_gap_covered_one_does_not() {
let symbols = vec![
SymbolRow { fqn: "test_a".into(), is_test: true, label: None },
SymbolRow { fqn: "helper_covered".into(), is_test: false, label: None },
SymbolRow { fqn: "deep_covered".into(), is_test: false, label: None },
SymbolRow { fqn: "orphan_fn".into(), is_test: false, label: Some("orphan".into()) },
];
let edges = vec![
CallEdge { caller: "test_a".into(), callee: "helper_covered".into() },
CallEdge { caller: "helper_covered".into(), callee: "deep_covered".into() },
];
let reach = test_reachable(&symbols, &edges);
assert!(reach.contains("helper_covered") && reach.contains("deep_covered"));
assert!(!reach.contains("orphan_fn"));
let unreached = unreached_functions(&symbols, &edges);
assert_eq!(unreached.len(), 1, "only the orphan is unreached");
assert_eq!(unreached[0].id, "orphan_fn");
assert_eq!(unreached[0].label, "orphan", "label carried from symbol row");
assert!(!unreached.iter().any(|n| n.id == "deep_covered"));
assert!(!unreached.iter().any(|n| n.id == "test_a"));
}
#[test]
fn reachability_handles_cycles() {
let symbols = vec![
SymbolRow { fqn: "t".into(), is_test: true, label: None },
SymbolRow { fqn: "a".into(), is_test: false, label: None },
SymbolRow { fqn: "b".into(), is_test: false, label: None },
SymbolRow { fqn: "c".into(), is_test: false, label: None },
];
let edges = vec![
CallEdge { caller: "t".into(), callee: "a".into() },
CallEdge { caller: "a".into(), callee: "b".into() },
CallEdge { caller: "b".into(), callee: "a".into() }, CallEdge { caller: "c".into(), callee: "c".into() }, ];
let unreached = unreached_functions(&symbols, &edges);
let ids: BTreeSet<_> = unreached.iter().map(|n| n.id.clone()).collect();
assert_eq!(ids, BTreeSet::from(["c".to_string()]), "cycle doesn't hang; c is the only gap");
}
#[test]
fn compute_gap_subtracts_covered_and_allowlist() {
let mut surface = Surface::new();
surface
.extend(viz_tabs(["Search", "Test"])) .extend(mcp_tools(["search"])) .extend(cli_commands(["doctor"]));
assert_eq!(surface.len(), 6);
assert_eq!(surface.count_kind(SurfaceKind::VizTab), 4);
let covered: BTreeSet<String> = [
"viz_tab:Search@fat",
"viz_tab:Search@thin",
"viz_tab:Test@fat",
"mcp_tool:search@na",
]
.iter()
.map(|s| s.to_string())
.collect();
let allowlist: BTreeSet<String> = ["cli_command:doctor@na".to_string()].into_iter().collect();
let gap = compute_gap(&surface, &covered, &allowlist);
assert_eq!(gap.covered, 4);
assert_eq!(gap.total, 6);
assert_eq!(gap.allowlisted.len(), 1);
assert_eq!(gap.allowlisted[0].id, "doctor");
assert_eq!(gap.missing.len(), 1, "exactly one uncovered, un-allowlisted node");
assert_eq!(gap.missing[0].key_str(), "viz_tab:Test@thin");
assert!(!gap.is_clean(), "a missing surface makes the gate RED");
assert!(gap.summary().contains("RED"));
}
#[test]
fn empty_gap_is_green() {
let mut surface = Surface::new();
surface.extend(mcp_tools(["a", "b"]));
let covered: BTreeSet<String> =
["mcp_tool:a@na", "mcp_tool:b@na"].iter().map(|s| s.to_string()).collect();
let gap = compute_gap(&surface, &covered, &BTreeSet::new());
assert!(gap.is_clean(), "every surface covered → Gap == ∅ → GREEN");
assert_eq!(gap.covered, 2);
assert_eq!(gap.missing.len(), 0);
assert!(gap.summary().contains("GREEN"));
}
#[test]
fn end_to_end_full_surface_round_trips_through_serde() {
let facets = vec![FacetRow {
component: "GraphView".into(),
in_registry: true,
has_local_ctor: true,
has_remote_ctor: true,
caps: vec!["reads_warehouse".into()],
}];
let symbols = vec![
SymbolRow { fqn: "t".into(), is_test: true, label: None },
SymbolRow { fqn: "wired".into(), is_test: false, label: None },
SymbolRow { fqn: "dead".into(), is_test: false, label: None },
];
let edges = vec![CallEdge { caller: "t".into(), callee: "wired".into() }];
let mut surface = Surface::new();
surface
.extend(facett_components(&facets))
.extend(viz_tabs(["Graph"]))
.extend(cli_commands(["graph"]))
.extend(mcp_tools(["dep_graph_mermaid"]))
.extend(unreached_functions(&symbols, &edges));
assert_eq!(surface.len(), 7);
assert!(surface
.nodes
.iter()
.any(|n| n.kind == SurfaceKind::Function && n.id == "dead"));
let covered = BTreeSet::new();
let gap = compute_gap(&surface, &covered, &BTreeSet::new());
assert_eq!(gap.missing.len(), 7, "nothing covered → all 7 are gaps");
let json = serde_json::to_string(&gap).unwrap();
let back: Gap = serde_json::from_str(&json).unwrap();
assert_eq!(back, gap, "Gap round-trips through serde (the warehouse row)");
assert!(back
.missing
.iter()
.any(|n| n.kind == SurfaceKind::Function && n.id == "dead"));
}
}