src/arch/metro.rs
//! 🚇 **Metro map** — the button → gRPC CALL PATH, drawn as a subway line.
//!
//! Where [`super`] coarsens the WHOLE fn graph into a component/gRPC/core/table
//! board, THIS module answers the single most operationally-useful question a
//! user has in front of the running viz: *"I am about to click this button —
//! what fires, and where does the request go?"*
//!
//! For one UI button (`run_full_matrix → Ops.RunTestMatrix`) it computes the
//! path from the button's egui **click-handler** fn (the fn that contains the
//! `.clicked()` branch) down through the syn `call_edges` to the **gRPC handler**
//! fn that implements the button's RPC, and labels every function on the way as a
//! STATION. Boundary functions (the ui start, the grpc terminus, every emitter
//! along the line) are classified with the SAME [`crate::coverage::classify`] the
//! coverage feature uses — so the metro line's interchange stations are exactly
//! the layer boundaries the test matrix must reach.
//!
//! PURE: std + serde + the `knowledge`/`coverage` facts only, no egui. The viz
//! `arch_tab` lays this out octolinearly; the headless matrix asserts it as DATA.
//!
//! ## Why a registry, not a scrape
//! The viz already emits each tab's buttons as `{id, rpc}` literals in its
//! `state_json` (`Test`: `run_full_matrix → Ops.RunTestMatrix`; `Bench`:
//! `run_bencher → Ops.RunBench`; …). Those literals are scattered across the tab
//! files. [`button_registry`] is the single CANONICAL, enumerable copy of that
//! mapping — plus the one fact the JSON literal can't carry: `ui_handler`, the
//! Rust fn that owns the `.clicked()` branch (the head of the metro line). The
//! `viz_button_registry_matches_tab_json` test pins the two in sync so the
//! registry can never silently drift from what the tabs actually paint.
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, VecDeque};
use crate::coverage::{Boundary, classify};
use crate::knowledge::symbols::{CallEdgeRow, SymbolRow};
use super::grpc_handlers_from_symbols;
/// One UI button → its RPC + the fn that owns the click. The canonical, code-
/// enumerable twin of the `{id, rpc}` literals the tab `state_json`s emit.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ButtonRpc {
/// Owning viz tab (the `state_json` section key: `test`/`bench`/…).
pub tab: &'static str,
/// Stable button id (matches the tab's `ops.buttons[].id`).
pub id: &'static str,
/// The `Service.Verb` RPC the button fires (matches `ops.buttons[].rpc`).
pub rpc: &'static str,
/// The Rust fn that contains the button's `.clicked()` branch — the head
/// station of the metro line (the code that fires on click).
pub ui_handler: &'static str,
}
/// The canonical button → RPC → click-handler registry. The single source of
/// truth the 🚇 metro map enumerates from. Kept in lock-step with the tab
/// `state_json` `ops.buttons` literals by `viz_button_registry_matches_tab_json`.
pub fn button_registry() -> Vec<ButtonRpc> {
vec![
// The 🧪 Test + 📈 Bench per-pane RUN buttons moved to the dwarves forge UI
// (separation of concerns). nornir keeps the Ops.RunTestMatrix / Ops.RunBench
// server RPCs (data production) but no longer draws their buttons, so they are
// not metro stations here.
ButtonRpc {
tab: "release",
id: "run_release_gate",
rpc: "Ops.RunRelease",
ui_handler: "draw_release_op",
},
ButtonRpc {
tab: "nornir",
id: "server_status",
rpc: "Health.Ping",
ui_handler: "draw",
},
ButtonRpc {
tab: "nornir",
id: "add_workspace",
rpc: "Workspaces.Register",
ui_handler: "draw",
},
ButtonRpc {
tab: "nornir",
id: "kill_workspace",
rpc: "Workspaces.Remove",
ui_handler: "draw",
},
ButtonRpc {
tab: "nornir",
id: "populate",
rpc: "Workspaces.Fetch",
ui_handler: "draw",
},
ButtonRpc {
tab: "nornir",
id: "refresh",
rpc: "Workspaces.Fetch",
ui_handler: "draw",
},
ButtonRpc {
tab: "nornir",
id: "populate_status",
rpc: "Viz.CloneEvents",
ui_handler: "draw_populate_status",
},
ButtonRpc {
tab: "nornir",
id: "workspaces_roster",
rpc: "Workspaces.PopulateStatus",
ui_handler: "draw_roster",
},
]
}
/// Look up a single button by its id (first match — `run_full_matrix`/`run_repo`
/// share `draw_run_ops`, but the id is unique).
pub fn button_by_id(id: &str) -> Option<ButtonRpc> {
button_registry().into_iter().find(|b| b.id == id)
}
/// One STATION on a metro line — a function the request passes through.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetroStation {
/// The fn identifier (last `::` segment — the call graph's node granularity).
pub fn_name: String,
/// The fully-qualified-ish symbol the station resolved to (when a defining
/// symbol exists), for click-to-code + disambiguation. Empty for an
/// unresolved leaf identifier.
pub symbol: String,
/// Defining file (repo-relative), empty when unresolved.
pub file: String,
/// 1-based defining line, 0 when unresolved.
pub line: u32,
/// The layer boundary this station sits on (ui / grpc / emitter / core),
/// classified by [`crate::coverage::classify`] (REUSED).
pub boundary: Boundary,
}
impl MetroStation {
/// Is this an interchange (boundary) station — a labelled stop the metro map
/// draws prominently (ui head, grpc terminus, emitter stop)?
pub fn is_interchange(&self) -> bool {
self.boundary.is_boundary()
}
}
/// One METRO LINE — a button's full click → gRPC call path, with every fn on it
/// as a station and the boundaries flagged.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetroLine {
/// The button id this line belongs to.
pub button_id: String,
/// The owning tab.
pub tab: String,
/// The `Service.Verb` RPC the line terminates at.
pub rpc: String,
/// The UI click-handler fn (the head of the line) as declared in the registry.
pub ui_handler: String,
/// The ordered stations from the click-handler to the gRPC handler.
pub stations: Vec<MetroStation>,
/// The gRPC terminus station's fn name (the handler that implements `rpc`),
/// `None` when the handler symbol couldn't be located.
pub grpc_terminus: Option<String>,
/// `true` when the line actually REACHES the gRPC handler (the path is real,
/// not a stub). A line that doesn't reach its terminus is a FAIL the matrix
/// must catch.
pub reached: bool,
/// How many of the stations are EMITTERS (state_json / emit* / trace) — the
/// readable-data stops the line passes through.
pub emitter_stations: usize,
}
impl MetroLine {
/// The boundary tally on this line: (ui, grpc, emitter) interchange counts.
pub fn boundary_tally(&self) -> (usize, usize, usize) {
let (mut u, mut g, mut e) = (0, 0, 0);
for s in &self.stations {
match s.boundary {
Boundary::Ui => u += 1,
Boundary::Grpc => g += 1,
Boundary::Emitter => e += 1,
Boundary::Core => {}
}
}
(u, g, e)
}
}
fn last_seg(s: &str) -> &str {
s.rsplit("::").next().unwrap_or(s)
}
/// Resolve the gRPC handler fn's fully-qualified path for an `Service.Verb` RPC,
/// by REVERSING [`grpc_handlers_from_symbols`] (the same handler map the arch
/// board uses): the symbol whose mapped label == `rpc`. Returns the fq path so we
/// can classify + locate it; `None` when no handler symbol matches.
pub fn grpc_handler_fq_for_rpc(symbols: &[SymbolRow], rpc: &str) -> Option<String> {
grpc_handlers_from_symbols(symbols)
.into_iter()
.find(|(_, label)| label == rpc)
.map(|(fq, _)| fq)
}
/// Build the metro line for one button, from the scanned symbols + call edges.
///
/// 1. The HEAD station is `button.ui_handler` (the fn owning the `.clicked()`).
/// 2. The TERMINUS is the gRPC handler fn implementing `button.rpc` (resolved by
/// reversing the handler map). When the handler symbol is found we BFS the
/// call edges from the head to the handler's last segment; otherwise we fall
/// back to the head's reachable frontier so the line is never blank.
/// 3. Every fn on the path becomes a [`MetroStation`], classified with
/// [`crate::coverage::classify`]; emitters are tallied.
pub fn build_metro_line(
symbols: &[SymbolRow],
calls: &[CallEdgeRow],
button: &ButtonRpc,
) -> MetroLine {
let handler_fq = grpc_handler_fq_for_rpc(symbols, button.rpc);
let terminus_seg = handler_fq.as_deref().map(|fq| last_seg(fq).to_string());
// BFS over call edges at identifier granularity (caller last-seg → callee
// last-seg), the same model as `KnowledgeView::call_path`, from the head
// click-handler to the gRPC terminus segment.
let path_segs: Vec<String> = match &terminus_seg {
Some(term) => bfs_path(calls, button.ui_handler, term)
.unwrap_or_else(|| vec![button.ui_handler.to_string()]),
None => vec![button.ui_handler.to_string()],
};
// Did we actually reach the terminus? (the last station is the handler seg).
let reached = match &terminus_seg {
Some(term) => path_segs.last().map(|s| s == term).unwrap_or(false),
None => false,
};
// Resolve + classify each station.
let mut stations = Vec::with_capacity(path_segs.len());
for (i, seg) in path_segs.iter().enumerate() {
// For the terminus we already know the fq handler path; prefer it so the
// station is classified as gRPC (the handler fq is in the handler map).
let is_terminus = reached && i + 1 == path_segs.len();
let (symbol, file, line) = if is_terminus {
if let Some(fq) = &handler_fq {
// Locate the EXACT handler fq — there can be several fns sharing
// the verb's last segment (e.g. the viz CLIENT `remote::run_test_
// matrix` vs the server `OpsSvc::run_test_matrix`); by-name would
// pick the wrong one. Match the full module_path::name.
let loc = locate_symbol_exact(symbols, fq);
(fq.clone(), loc.0, loc.1)
} else {
let loc = locate_symbol(symbols, seg);
(seg.clone(), loc.0, loc.1)
}
} else {
// Prefer a defining symbol; fall back to the bare identifier.
match locate_symbol_fq(symbols, seg) {
Some((fq, file, line)) => (fq, file, line),
None => (seg.clone(), String::new(), 0),
}
};
// Boundary: the TERMINUS is an AUTHORITATIVE gRPC handler (it's in the
// handler map), so tag it Grpc directly — `coverage::classify`'s verb list
// is intentionally a small allowlist and doesn't enumerate every RPC verb
// (`run_release`/`fetch`/`register`/…), so relying on it would mislabel a
// real handler as core. Every NON-terminus station is classified by the
// shared `coverage::classify` (REUSED) on its fq name + file.
let boundary = if is_terminus && handler_fq.is_some() {
Boundary::Grpc
} else {
let class_name = if symbol.is_empty() {
seg.clone()
} else {
symbol.clone()
};
classify(&class_name, &file)
};
stations.push(MetroStation {
fn_name: seg.clone(),
symbol,
file,
line,
boundary,
});
}
let emitter_stations = stations
.iter()
.filter(|s| s.boundary == Boundary::Emitter)
.count();
MetroLine {
button_id: button.id.to_string(),
tab: button.tab.to_string(),
rpc: button.rpc.to_string(),
ui_handler: button.ui_handler.to_string(),
grpc_terminus: terminus_seg,
reached,
emitter_stations,
stations,
}
}
/// Build the whole metro map: one line per button in the registry.
pub fn build_metro_map(symbols: &[SymbolRow], calls: &[CallEdgeRow]) -> Vec<MetroLine> {
button_registry()
.iter()
.map(|b| build_metro_line(symbols, calls, b))
.collect()
}
/// BFS the shortest caller→callee chain at identifier granularity from `from` to
/// `to`, inclusive. Mirrors [`crate::knowledge::query::KnowledgeView::call_path`]
/// but lives here so the metro core is self-contained + unit-testable in one place.
fn bfs_path(calls: &[CallEdgeRow], from: &str, to: &str) -> Option<Vec<String>> {
use std::collections::BTreeMap;
let from = last_seg(from).to_string();
let to = last_seg(to).to_string();
let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
let mut nodes: BTreeSet<&str> = BTreeSet::new();
for e in calls {
let f = last_seg(&e.caller_path);
let t = last_seg(&e.callee_ident);
adj.entry(f).or_default().push(t);
nodes.insert(f);
nodes.insert(t);
}
if from == to {
return nodes.contains(from.as_str()).then(|| vec![from]);
}
if !nodes.contains(from.as_str()) {
return None;
}
let mut parent: BTreeMap<String, String> = BTreeMap::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut q: VecDeque<String> = VecDeque::new();
seen.insert(from.clone());
q.push_back(from.clone());
while let Some(cur) = q.pop_front() {
let Some(outs) = adj.get(cur.as_str()) else {
continue;
};
for &c in outs {
if !seen.insert(c.to_string()) {
continue;
}
parent.insert(c.to_string(), cur.clone());
if c == to {
let mut path = vec![to.clone()];
let mut node = to.clone();
while let Some(p) = parent.get(&node) {
path.push(p.clone());
node = p.clone();
}
path.reverse();
return Some(path);
}
q.push_back(c.to_string());
}
}
None
}
/// `(file, line)` of the best symbol whose last segment == `seg`, else `("", 0)`.
fn locate_symbol(symbols: &[SymbolRow], seg: &str) -> (String, u32) {
locate_symbol_fq(symbols, seg)
.map(|(_, f, l)| (f, l))
.unwrap_or_default()
}
/// `(file, line)` of the symbol whose `module_path::item_name` == `fq` exactly —
/// the unambiguous locator for a known fully-qualified path (the gRPC handler),
/// so a verb shared by several fns resolves to the RIGHT one. `("", 0)` if absent.
fn locate_symbol_exact(symbols: &[SymbolRow], fq: &str) -> (String, u32) {
for s in symbols {
let cand = if s.module_path.is_empty() {
s.item_name.clone()
} else {
format!("{}::{}", s.module_path, s.item_name)
};
if cand == fq {
return (s.file.clone(), s.line);
}
}
(String::new(), 0)
}
/// `(fq, file, line)` of the best fn symbol whose last segment == `seg`.
/// Prefers a `fn`-kind symbol; ties break toward the shorter file then lower line.
fn locate_symbol_fq(symbols: &[SymbolRow], seg: &str) -> Option<(String, String, u32)> {
let mut best: Option<&SymbolRow> = None;
for s in symbols {
if s.item_name != seg {
continue;
}
let better = match best {
None => true,
Some(b) => {
// prefer fn, then shorter file, then lower line.
let (sf, bf) = (s.item_kind == "fn", b.item_kind == "fn");
match (sf, bf) {
(true, false) => true,
(false, true) => false,
_ => (s.file.len(), &s.file, s.line) < (b.file.len(), &b.file, b.line),
}
}
};
if better {
best = Some(s);
}
}
best.map(|s| {
let fq = if s.module_path.is_empty() {
s.item_name.clone()
} else {
format!("{}::{}", s.module_path, s.item_name)
};
(fq, s.file.clone(), s.line)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn sym(module: &str, name: &str, kind: &str, file: &str, line: u32) -> SymbolRow {
SymbolRow {
crate_name: "nornir".into(),
module_path: module.into(),
item_kind: kind.into(),
item_name: name.into(),
visibility: "pub".into(),
file: file.into(),
line,
doc_lines: 0,
signature: None,
}
}
fn call(caller: &str, callee: &str, file: &str, line: u32) -> CallEdgeRow {
CallEdgeRow {
crate_name: "nornir".into(),
caller_path: caller.into(),
callee_ident: callee.into(),
call_kind: "call".into(),
file: file.into(),
line,
}
}
/// A realistic `Release → Ops.RunRelease` slice: the `draw_release_op` UI
/// handler fires the RPC client, which reaches the server's
/// `OpsSvc::run_release` gRPC handler, passing through an emitter on the way.
/// (The test/bench run buttons moved to the dwarves forge UI, so the surviving
/// worked example is the Release gate op.)
fn fixture() -> (Vec<SymbolRow>, Vec<CallEdgeRow>) {
let symbols = vec![
// UI handler (head) — under src/viz, a draw_* fn → ui boundary.
sym(
"nornir::viz::release_tab::ReleaseTabState",
"draw_release_op",
"fn",
"src/viz/release_tab.rs",
207,
),
// an emitter stop along the line.
sym(
"nornir::viz::trace",
"emit_in",
"fn",
"src/viz/trace.rs",
90,
),
// a plain core fn.
sym(
"nornir::viz::remote",
"run_release_rpc",
"fn",
"src/viz/remote.rs",
300,
),
// the gRPC handler trait impl marker + the handler fn (server file).
sym(
"nornir_server",
"impl OpsSvcTrait for OpsSvc",
"impl",
"src/bin/nornir-server.rs",
3243,
),
sym(
"nornir_server::OpsSvc",
"run_release",
"fn",
"src/bin/nornir-server.rs",
3244,
),
];
let calls = vec![
call(
"nornir::viz::release_tab::ReleaseTabState::draw_release_op",
"emit_in",
"src/viz/release_tab.rs",
210,
),
call(
"nornir::viz::trace::emit_in",
"run_release_rpc",
"src/viz/trace.rs",
92,
),
call(
"nornir::viz::remote::run_release_rpc",
"OpsSvc::run_release",
"src/viz/remote.rs",
305,
),
];
(symbols, calls)
}
#[test]
fn registry_covers_the_known_buttons() {
let reg = button_registry();
let find = |id: &str| reg.iter().find(|b| b.id == id).cloned();
// The test/bench run buttons moved to dwarves; the release gate op survives.
assert!(
find("run_full_matrix").is_none(),
"the test-matrix button moved to dwarves"
);
assert!(
find("run_bencher").is_none(),
"the bench button moved to dwarves"
);
assert_eq!(find("run_release_gate").unwrap().rpc, "Ops.RunRelease");
assert_eq!(
find("run_release_gate").unwrap().ui_handler,
"draw_release_op"
);
assert_eq!(find("server_status").unwrap().rpc, "Health.Ping");
}
#[test]
fn grpc_handler_fq_reverses_the_handler_map() {
let (symbols, _) = fixture();
let fq = grpc_handler_fq_for_rpc(&symbols, "Ops.RunRelease").expect("handler found");
assert_eq!(fq, "nornir_server::OpsSvc::run_release");
assert!(grpc_handler_fq_for_rpc(&symbols, "Ops.Nonexistent").is_none());
}
/// THE keystone: a real button → real gRPC path with a real emitter station,
/// reaching the terminus. (the test the live drive mirrors.)
#[test]
fn metro_line_reaches_grpc_with_emitter_station() {
let (symbols, calls) = fixture();
let btn = button_by_id("run_release_gate").unwrap();
let line = build_metro_line(&symbols, &calls, &btn);
// path: draw_release_op → emit_in → run_release_rpc → run_release
let names: Vec<&str> = line.stations.iter().map(|s| s.fn_name.as_str()).collect();
assert_eq!(
names,
vec![
"draw_release_op",
"emit_in",
"run_release_rpc",
"run_release"
]
);
// It reaches the gRPC terminus.
assert!(line.reached, "line reaches the gRPC handler");
assert_eq!(line.grpc_terminus.as_deref(), Some("run_release"));
assert_eq!(line.rpc, "Ops.RunRelease");
// The head is a UI boundary, the terminus is a gRPC boundary, and there
// is >=1 emitter interchange on the line.
assert_eq!(
line.stations.first().unwrap().boundary,
Boundary::Ui,
"head is ui"
);
assert_eq!(
line.stations.last().unwrap().boundary,
Boundary::Grpc,
"terminus is grpc"
);
assert!(
line.emitter_stations >= 1,
"an emitter station on the line: {line:?}"
);
let (u, g, e) = line.boundary_tally();
assert!(
u >= 1 && g >= 1 && e >= 1,
"ui/grpc/emitter interchanges: {u}/{g}/{e}"
);
// The head + terminus carry real file:line (click-to-code targets).
assert_eq!(
line.stations.first().unwrap().file,
"src/viz/release_tab.rs"
);
assert_eq!(line.stations.first().unwrap().line, 207);
assert_eq!(
line.stations.last().unwrap().file,
"src/bin/nornir-server.rs"
);
assert_eq!(line.stations.last().unwrap().line, 3244);
}
/// SENSITIVITY: with no call edges the line is a STUB (head only, doesn't
/// reach) — exactly the blank/broken state the matrix must FAIL on.
#[test]
fn empty_calls_yield_unreached_stub() {
let (symbols, _) = fixture();
let btn = button_by_id("run_release_gate").unwrap();
let line = build_metro_line(&symbols, &[], &btn);
assert!(!line.reached, "no call edges → cannot reach gRPC");
assert_eq!(line.stations.len(), 1, "stub is the head only");
assert_eq!(line.emitter_stations, 0);
}
#[test]
fn whole_map_has_a_line_per_button() {
let (symbols, calls) = fixture();
let map = build_metro_map(&symbols, &calls);
assert_eq!(map.len(), button_registry().len());
// The run_release_gate line is the one that reaches (the fixture wires it).
let rfm = map
.iter()
.find(|l| l.button_id == "run_release_gate")
.unwrap();
assert!(rfm.reached);
}
}