src/viz/metro_feed.rs
//! 🚇 **Metro feed** — the PURE producer that turns nornir's button → gRPC →
//! warehouse coverage CHAINS into a [`facett_graphview::MetroMap`] the reusable
//! [`facett_graphview::MetroView`] draws (S5b).
//!
//! nornir already computes the chain itself: [`crate::arch::metro::build_metro_map`]
//! walks each [`ButtonRpc`](crate::arch::metro::ButtonRpc) from its UI click-handler
//! (the START) through the syn `call_edges` to the gRPC handler (the END), tagging
//! every fn on the way with [`crate::coverage::Boundary`]. This module is the thin,
//! UI-agnostic ADAPTER from that nornir model onto facett's domain-free transit
//! model — facett never learns what a "warehouse" or a "gRPC handler" is, only how
//! to draw a line of lit/unlit stops.
//!
//! ## How each station kind is derived
//! Per [`build_metro_line`](crate::arch::metro::build_metro_line) line:
//! * **START** = the line's first station (the `ui_handler` fn that owns
//! `.clicked()`) → [`StationKind::Start`].
//! * **chain** = the BFS call-path stations between head and terminus, each
//! classified by its nornir [`Boundary`](crate::coverage::Boundary):
//! `Boundary::Emitter` ⇒ [`StationKind::Emitter`] (a real gating stop), everything
//! else (`Core`/`Ui`) ⇒ [`StationKind::PassThrough`] (ridden through, never gates).
//! * **END / Grpc** = the gRPC handler station (the line's `rpc`, the
//! `grpc_terminus` fn) → [`StationKind::Grpc`].
//! * **TERMINUS** = the warehouse table the chain lands in → [`StationKind::Terminus`].
//! Resolved from the `fn → table` warehouse-access edges where available, else a
//! `rpc`-derived placeholder (see [`terminus_table`]) — honestly labelled.
//!
//! ## The lit overlay
//! A gating station is **lit** iff the latest `surface_coverage` run recorded a
//! `covered` row whose `id`'s last segment matches the station's fn (the same
//! last-segment match [`crate::arch::warehouse`]'s `map_coverage_verdicts` uses for
//! the arch board). No coverage data ⇒ every gating stop is unlit — HONEST: "not
//! yet proven", never a false green.
//!
//! ## Caller-side FORKS off the gRPC station (UI viz vs CLI)
//! The gRPC verb is the server terminus, but it is *called by* multiple CLIENTS.
//! [`caller_arms_for_verb`] scans the `call_edges` for callers of the verb's
//! client-stub method (the snake_case of the RPC's verb, e.g.
//! `Ops.RunRelease` → `run_release`) and classifies each caller's FILE
//! into a [`Caller`]: the viz tab code paths (`src/viz/…`) ⇒ a UI arm, the
//! `nornir`/`nornir-mcp` binaries (`src/bin/…`) ⇒ a CLI arm. Those become the
//! marked, forked [`facett_graphview::MetroBranch`] arms hung off the
//! [`StationKind::Grpc`] station — a UI arm is LIT when the verb is exercised
//! through the UI in the latest coverage run, a CLI arm LIT when exercised
//! through the CLI. The trunk green verdict is unaffected (arms never gate).
//!
//! ## The cross-crate (bin → lib) gap
//! The viz click-handler lives in the `nornir-ui` BINARY and the gRPC handler in
//! the server BINARY; syn `call_edges` are scanned per-crate and DON'T resolve that
//! bin→lib hop, so a line whose nornir `reached == false` has no resolved interior.
//! For those we emit the honest skeleton **Start → (unresolved) → Grpc → Terminus**
//! rather than block on the gap (scip/S6b closes it later).
use facett_graphview::{
MetroBranch as FxBranch, MetroLine as FxLine, MetroMap as FxMap, MetroStation as FxStation,
StationKind,
};
use crate::arch::metro::{MetroLine as NornirLine, build_metro_map as build_nornir_metro_map};
use crate::coverage::Boundary;
use crate::knowledge::symbols::{CallEdgeRow, SymbolRow};
use nornir_testmatrix::coverage::CoverageRow;
/// Last `::`/`.` segment of an identifier, lowercased — the match key shared with
/// the arch board's coverage mapping.
fn last_ident(s: &str) -> String {
s.rsplit(|c| c == ':' || c == '.')
.next()
.unwrap_or(s)
.to_lowercase()
}
/// The set of fn last-segments that were **covered** in the supplied coverage rows
/// (a `covered` verdict). We key on the row `id`'s last segment so the lit signal
/// lines up with the station's fn name regardless of the row's `kind`.
fn lit_idents(coverage: &[CoverageRow]) -> std::collections::HashSet<String> {
coverage
.iter()
.filter(|r| r.verdict == "covered")
.map(|r| last_ident(&r.id))
.collect()
}
/// Map one nornir [`Boundary`] on a non-endpoint chain station to a facett
/// [`StationKind`]: an emitter is a gating [`StationKind::Emitter`]; everything else
/// (core/ui interior) is a non-gating [`StationKind::PassThrough`] ridden through.
fn chain_kind(b: Boundary) -> StationKind {
match b {
Boundary::Emitter => StationKind::Emitter,
_ => StationKind::PassThrough,
}
}
/// A caller CLASS for a gRPC verb — the kind of client that invokes it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Caller {
/// The UI viz tab code paths (`src/viz/…`) — a `StationKind::UiClient` arm.
Ui,
/// The `nornir` / `nornir-mcp` binaries (`src/bin/…`) — a `StationKind::CliClient` arm.
Cli,
}
impl Caller {
fn kind(self) -> StationKind {
match self {
Caller::Ui => StationKind::UiClient,
Caller::Cli => StationKind::CliClient,
}
}
/// The arm's short client label (the binary/surface name).
fn label(self) -> &'static str {
match self {
Caller::Ui => "viz",
Caller::Cli => "cli",
}
}
}
/// Classify a CALLER file into a [`Caller`] class: the viz tab/render code
/// (`src/viz/…`, or a `viz`/`nornir-ui` crate) is the UI client; the
/// `nornir`/`nornir-mcp` binaries (`src/bin/…`) are the CLI client. `None` for a
/// file that is neither (a server-internal or library caller — not a client arm).
fn classify_caller(file: &str) -> Option<Caller> {
let f = file.replace('\\', "/").to_lowercase();
if f.contains("src/viz/") || f.contains("urdr-threads") {
Some(Caller::Ui)
} else if f.contains("src/bin/nornir.rs")
|| f.contains("src/bin/nornir-mcp.rs")
|| f.contains("bin/nornir.rs")
|| f.contains("bin/nornir-mcp.rs")
{
Some(Caller::Cli)
} else {
None
}
}
/// The client-stub METHOD name a caller invokes for a `Service.Verb` RPC: the
/// snake_case of the verb (`Ops.RunRelease` → `run_release`,
/// `Health.Ping` → `ping`). This is the `callee_ident` last-segment a UI/CLI
/// client's call edge carries when it fires the verb through the generated stub.
fn verb_stub_method(rpc: &str) -> String {
let verb = rpc.rsplit('.').next().unwrap_or(rpc);
let mut out = String::with_capacity(verb.len() + 4);
for (i, ch) in verb.chars().enumerate() {
if ch.is_ascii_uppercase() {
if i != 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
/// **THE FORK DERIVATION** — the marked caller arms for a gRPC verb. Scans the
/// call edges for callers of the verb's client-stub method, classifies each
/// caller's file into a [`Caller`] (UI viz / CLI), and emits one
/// [`facett_graphview::MetroBranch`] per distinct client that invokes the verb.
///
/// An arm is **lit** when the verb (or its stub method) is recorded covered AND
/// at least one caller of that class exists — i.e. the verb was exercised through
/// that client in the latest coverage run. With no coverage every arm is unlit
/// (honest: that client path not yet proven). Arms are returned UI-before-CLI for
/// a stable order.
fn caller_arms_for_verb(
rpc: &str,
calls: &[CallEdgeRow],
lit: &std::collections::HashSet<String>,
) -> Vec<FxBranch> {
let stub = verb_stub_method(rpc);
// Which client classes actually call this verb's stub method?
let mut has_ui = false;
let mut has_cli = false;
for e in calls {
if last_ident(&e.callee_ident) != stub {
continue;
}
match classify_caller(&e.file) {
Some(Caller::Ui) => has_ui = true,
Some(Caller::Cli) => has_cli = true,
None => {}
}
}
// The verb is lit (exercised) when the stub method OR the verb's last segment
// appears among covered rows — the per-arm lit signal (same client exercised
// it through the chain). Without coverage, arms are present-but-unlit.
let verb_lit = lit.contains(&stub) || lit.contains(&last_ident(rpc));
let mut arms = Vec::new();
if has_ui {
arms.push(FxBranch::new(
format!("caller::ui::{rpc}"),
Caller::Ui.label(),
Caller::Ui.kind(),
verb_lit,
));
}
if has_cli {
arms.push(FxBranch::new(
format!("caller::cli::{rpc}"),
Caller::Cli.label(),
Caller::Cli.kind(),
verb_lit,
));
}
arms
}
/// The warehouse TERMINUS table for a line. Prefers a real `fn → table`
/// warehouse-access edge for the gRPC handler fn (the most precise), else falls back
/// to a `Service.Verb`-derived placeholder so the line always has a terminus — the
/// placeholder is honestly distinguishable (id prefixed `table?::`).
///
/// `access` is the `(handler_fn, table)` edge list (empty when unresolved).
/// Returns `(station_id, table_label, resolved)`.
fn terminus_table(
handler_seg: Option<&str>,
rpc: &str,
access: &[(String, String)],
) -> (String, String, bool) {
if let Some(seg) = handler_seg {
let want = last_ident(seg);
if let Some((_, table)) = access.iter().find(|(fnseg, _)| last_ident(fnseg) == want) {
return (format!("warehouse::{table}"), table.clone(), true);
}
}
// Placeholder: derive a readable table name from the RPC's service noun.
let svc = rpc.split('.').next().unwrap_or(rpc).to_lowercase();
let placeholder = format!("{svc}_results");
(format!("table?::{placeholder}"), placeholder, false)
}
/// Build one facett [`FxLine`] from a resolved nornir [`NornirLine`].
///
/// Station order: Start(button) → interior chain (Emitter/PassThrough) → Grpc(rpc)
/// → Terminus(table). When the nornir line did NOT reach its gRPC handler
/// (`reached == false` — the cross-crate bin→lib gap), the interior is unresolved,
/// so we emit the honest skeleton Start → (unresolved) → Grpc → Terminus.
fn line_to_fx(
nl: &NornirLine,
lit: &std::collections::HashSet<String>,
access: &[(String, String)],
calls: &[CallEdgeRow],
) -> FxLine {
let is_lit = |seg: &str| lit.contains(&last_ident(seg));
let mut stations: Vec<FxStation> = Vec::new();
// ── START: the ui_handler (first station on the nornir line). ────────────
let head = nl.stations.first();
let head_name = head
.map(|s| s.fn_name.as_str())
.unwrap_or(nl.ui_handler.as_str());
let head_id = head
.map(|s| s.symbol.clone())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| head_name.to_string());
stations.push(FxStation::new(
head_id,
head_name,
StationKind::Start,
is_lit(head_name),
));
let terminus_seg = nl.grpc_terminus.clone();
if nl.reached {
// The interior chain: everything between head and the grpc terminus. The
// LAST nornir station IS the grpc handler — rendered as the Grpc stop below,
// so skip it here.
let interior_end = nl.stations.len().saturating_sub(1);
for st in nl.stations.iter().take(interior_end).skip(1) {
let id = if st.symbol.is_empty() {
st.fn_name.clone()
} else {
st.symbol.clone()
};
let kind = chain_kind(st.boundary);
stations.push(FxStation::new(id, &st.fn_name, kind, is_lit(&st.fn_name)));
}
} else {
// UNRESOLVED interior (bin→lib gap): a single honest gap tick, no fabricated
// stations. The id carries the marker so a reader sees a skeleton, not green.
stations.push(FxStation::new(
format!("unresolved::{}", nl.button_id),
"(unresolved)",
StationKind::PassThrough,
true,
));
}
// ── END / Grpc: the gRPC handler implementing the RPC, FORKED back to its
// caller clients (UI viz / CLI) as marked arms. ──────────────────────────
let grpc_id = format!("grpc::{}", nl.rpc);
let grpc_lit = terminus_seg.as_deref().map(is_lit).unwrap_or(false)
|| is_lit(nl.rpc.rsplit('.').next().unwrap_or(&nl.rpc));
let arms = caller_arms_for_verb(&nl.rpc, calls, lit);
stations.push(FxStation::with_branches(
grpc_id,
nl.rpc.clone(),
StationKind::Grpc,
grpc_lit,
arms,
));
// ── TERMINUS: the warehouse table. ───────────────────────────────────────
let (term_id, term_label, term_resolved) =
terminus_table(terminus_seg.as_deref(), &nl.rpc, access);
// The terminus lights when its table appears among covered rows, OR (when the
// chain reached + grpc is lit) follows the grpc verdict so a fully-covered chain
// can go green. An unresolved/unreached line keeps it tied to grpc only.
let term_lit = if term_resolved {
is_lit(&term_label) || grpc_lit
} else {
grpc_lit
};
stations.push(FxStation::new(
term_id,
term_label,
StationKind::Terminus,
term_lit,
));
FxLine::new(
format!("{}→{}", nl.button_id, nl.rpc),
format!("{} ({})", nl.button_id, nl.tab),
stations,
)
}
/// **THE PRODUCER** — build the facett [`FxMap`] from nornir's scanned facts +
/// coverage. Pure + unit-testable: real symbols/calls/coverage in, a real
/// [`facett_graphview::MetroMap`] out (one line per registered button).
///
/// * `symbols` / `calls` — the warehouse `symbol_facts` / `call_edges` for the
/// workspace (a [`crate::knowledge::query::KnowledgeView`]'s `symbols`/`calls`).
/// * `coverage` — the latest `surface_coverage` run's rows (the lit overlay). Empty
/// ⇒ every gating stop unlit (honest: not-yet-proven).
/// * `access` — `(handler_fn, table)` warehouse-access edges for terminus
/// resolution; empty ⇒ placeholder terminus tables.
pub fn build_metro_map(
symbols: &[SymbolRow],
calls: &[CallEdgeRow],
coverage: &[CoverageRow],
access: &[(String, String)],
) -> FxMap {
let lit = lit_idents(coverage);
let nornir_lines = build_nornir_metro_map(symbols, calls);
let lines = nornir_lines
.iter()
.map(|nl| line_to_fx(nl, &lit, access, calls))
.collect();
let map = FxMap::new(lines);
// EMIT-DOCTRINE (§selftest): the metro FEED is the pure producer behind the 🚇
// pane — ok ⇔ it built at least one button→…→warehouse line from the workspace
// facts; zero lines means no registered chains resolved (RED). The rollup
// reports how many resolved lines are fully green (every gating stop lit).
#[cfg(feature = "testmatrix")]
{
let (green, total) = metro_coverage(&map);
crate::selftest::emit(
"viz/metro (feed)",
"metro_map_built",
!map.lines.is_empty(),
&format!(
"{} line(s), {green}/{total} resolved-green",
map.lines.len()
),
);
}
map
}
/// The per-workspace facts the metro feed consumes — the warehouse
/// `symbol_facts` / `call_edges`, the latest `surface_coverage` lit overlay, and
/// the `(handler_fn, table)` warehouse-access edges (all for ONE workspace).
#[derive(Debug, Clone, Default)]
pub struct MetroFacts {
pub symbols: Vec<SymbolRow>,
pub calls: Vec<CallEdgeRow>,
pub coverage: Vec<CoverageRow>,
pub access: Vec<(String, String)>,
}
/// **CROSS-WORKSPACE producer (S6 step 4)** — build one [`FxMap`] per workspace
/// from its [`MetroFacts`], preserving the input order. The mega-gate folds each
/// map's [`metro_coverage`] rollup into its per-workspace verdict. Pure: real
/// per-workspace facts in, a `(workspace, MetroMap)` list out.
pub fn build_metro_maps_all(per_ws: &[(String, MetroFacts)]) -> Vec<(String, FxMap)> {
per_ws
.iter()
.map(|(ws, f)| {
(
ws.clone(),
build_metro_map(&f.symbols, &f.calls, &f.coverage, &f.access),
)
})
.collect()
}
/// **THE METRO ROLLUP (S6 step 4)** — `(green_lines, total_lines)` over the
/// **RESOLVED** lines of a [`FxMap`]. A line is GREEN iff every gating stop is lit
/// ([`facett_graphview::MetroLine::is_green`]).
///
/// UNRESOLVED bin→lib lines (the skeleton `Start → (unresolved) → Grpc →
/// Terminus`, carrying a station whose id is `unresolved::…`) are **EXCLUDED** from
/// both counts: until `scip_occurrences` is populated (S6b) their interior can't
/// resolve, so they can never go green — counting them would make the mega-gate
/// permanently RED. They are ALLOWLISTABLE, not hard-RED (the plan's metro risk).
/// So `green == total` ⟺ every line we CAN verify is green.
pub fn metro_coverage(map: &FxMap) -> (usize, usize) {
let mut green = 0usize;
let mut total = 0usize;
for line in &map.lines {
let unresolved = line
.stations
.iter()
.any(|s| s.id.starts_with("unresolved::"));
if unresolved {
continue;
}
total += 1;
if line.is_green() {
green += 1;
}
}
(green, total)
}
#[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) -> CallEdgeRow {
CallEdgeRow {
crate_name: "nornir".into(),
caller_path: caller.into(),
callee_ident: callee.into(),
call_kind: "call".into(),
file: "src/x.rs".into(),
line: 1,
}
}
fn cov(id: &str, verdict: &str) -> CoverageRow {
CoverageRow {
run_id: "r".into(),
workspace: "ws".into(),
surface_key: format!("function:{id}@fat"),
kind: "function".into(),
id: id.into(),
mode: "fat".into(),
verdict: verdict.into(),
reason: String::new(),
ts_micros: 1,
utfallsrum_covered: 0.0,
}
}
/// A resolved `run_release_gate → Ops.RunRelease` slice (draw_release_op → an
/// emitter → a core fn → the server handler). Mirrors arch::metro's fixture.
fn fixture() -> (Vec<SymbolRow>, Vec<CallEdgeRow>) {
let symbols = vec![
sym(
"nornir::viz::release_tab::ReleaseTabState",
"draw_release_op",
"fn",
"src/viz/release_tab.rs",
207,
),
sym(
"nornir::viz::trace",
"emit_in",
"fn",
"src/viz/trace.rs",
90,
),
sym(
"nornir::viz::remote",
"run_release_rpc",
"fn",
"src/viz/remote.rs",
300,
),
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",
),
call("nornir::viz::trace::emit_in", "run_release_rpc"),
call(
"nornir::viz::remote::run_release_rpc",
"OpsSvc::run_release",
),
];
(symbols, calls)
}
#[test]
fn map_has_a_line_per_button() {
let (symbols, calls) = fixture();
let map = build_metro_map(&symbols, &calls, &[], &[]);
assert_eq!(map.lines.len(), crate::arch::metro::button_registry().len());
}
/// Inject-assert keystone: a resolved chain (button → emitter → core → grpc),
/// coverage marking the emitter + handler + table lit, a resolved terminus
/// table → a fully-green line with Start + Emitter(lit) + PassThrough + Grpc +
/// Terminus, in the right station kinds.
#[test]
fn resolved_line_lights_up_to_green() {
let (symbols, calls) = fixture();
let coverage = vec![
cov("draw_release_op", "covered"), // the START button handler ran
cov("nornir::viz::trace::emit_in", "covered"),
cov("run_release", "covered"),
cov("test_matrix_runs", "covered"),
];
let access = vec![(
"OpsSvc::run_release".to_string(),
"test_matrix_runs".to_string(),
)];
let map = build_metro_map(&symbols, &calls, &coverage, &access);
let line = map
.lines
.iter()
.find(|l| l.id == "run_release_gate→Ops.RunRelease")
.expect("the run_release_gate line");
let kinds: Vec<StationKind> = line.stations.iter().map(|s| s.kind).collect();
assert_eq!(
kinds.first(),
Some(&StationKind::Start),
"head is the button"
);
assert_eq!(
kinds.last(),
Some(&StationKind::Terminus),
"tail is the warehouse table"
);
assert!(kinds.contains(&StationKind::Grpc), "the gRPC handler stop");
assert!(
kinds.contains(&StationKind::Emitter),
"an emitter station on the chain"
);
assert!(
kinds.contains(&StationKind::PassThrough),
"a ridden-through core fn"
);
let emitter = line
.stations
.iter()
.find(|s| s.kind == StationKind::Emitter)
.unwrap();
assert_eq!(emitter.label, "emit_in");
assert!(emitter.lit, "the covered emitter is lit");
let term = line.stations.last().unwrap();
assert_eq!(
term.label, "test_matrix_runs",
"terminus resolved from the access edge"
);
assert!(
line.is_green(),
"fully-covered resolved chain is green: {line:?}"
);
}
/// SENSITIVITY: with the emitter NOT covered, the line is RED (the emitter stop
/// is the unlit reason). Real coverage gap → real red verdict.
#[test]
fn unlit_emitter_makes_line_red() {
let (symbols, calls) = fixture();
let coverage = vec![
cov("run_release", "covered"),
cov("test_matrix_runs", "covered"),
];
let access = vec![(
"OpsSvc::run_release".to_string(),
"test_matrix_runs".to_string(),
)];
let map = build_metro_map(&symbols, &calls, &coverage, &access);
let line = map
.lines
.iter()
.find(|l| l.id == "run_release_gate→Ops.RunRelease")
.unwrap();
assert!(!line.is_green(), "an unlit emitter keeps the line red");
let unlit_labels: Vec<&str> = line.unlit().iter().map(|s| s.label.as_str()).collect();
assert!(
unlit_labels.contains(&"emit_in"),
"emit_in is the unlit gating stop: {unlit_labels:?}"
);
}
/// The cross-crate (bin→lib) gap: a button whose chain DOESN'T resolve to its
/// gRPC handler (no call edges) yields the honest Start → (unresolved) → Grpc →
/// Terminus skeleton, NOT a fake interior.
#[test]
fn unresolved_chain_emits_skeleton() {
let (symbols, _) = fixture();
let map = build_metro_map(&symbols, &[], &[], &[]); // no call edges → unreached
let line = map
.lines
.iter()
.find(|l| l.id == "run_release_gate→Ops.RunRelease")
.unwrap();
let kinds: Vec<StationKind> = line.stations.iter().map(|s| s.kind).collect();
assert_eq!(kinds.first(), Some(&StationKind::Start));
assert_eq!(kinds.last(), Some(&StationKind::Terminus));
assert!(kinds.contains(&StationKind::Grpc));
assert!(
line.stations
.iter()
.any(|s| s.id.starts_with("unresolved::")),
"an honest unresolved marker, not a fabricated interior"
);
assert!(!line.is_green(), "an unproven skeleton is not green");
}
// ── caller-side FORKS (UI viz vs CLI) ────────────────────────────────────
/// A call edge with an explicit CALLER FILE (so the fork can classify it).
fn call_in(caller: &str, callee: &str, file: &str) -> CallEdgeRow {
CallEdgeRow {
crate_name: "nornir".into(),
caller_path: caller.into(),
callee_ident: callee.into(),
call_kind: "call".into(),
file: file.into(),
line: 1,
}
}
#[test]
fn verb_stub_method_snake_cases_the_verb() {
assert_eq!(verb_stub_method("Ops.RunRelease"), "run_release");
assert_eq!(verb_stub_method("Health.Ping"), "ping");
assert_eq!(
verb_stub_method("Workspaces.PopulateStatus"),
"populate_status"
);
}
#[test]
fn classify_caller_splits_ui_from_cli() {
assert_eq!(classify_caller("src/viz/remote.rs"), Some(Caller::Ui));
assert_eq!(classify_caller("src/bin/nornir.rs"), Some(Caller::Cli));
assert_eq!(classify_caller("src/bin/nornir-mcp.rs"), Some(Caller::Cli));
assert_eq!(
classify_caller("src/bin/nornir-server.rs"),
None,
"the server is not a client"
);
assert_eq!(classify_caller("src/lib.rs"), None);
}
/// THE fork keystone: a verb called by BOTH the UI (src/viz) and the CLI
/// (src/bin/nornir.rs) produces TWO marked arms off the gRPC station; with the
/// verb covered they light. Asserted on the built map's station branches.
#[test]
fn grpc_verb_forks_to_ui_and_cli_arms() {
let (symbols, mut calls) = fixture();
// Both a UI caller and a CLI caller invoke the verb's stub method.
calls.push(call_in(
"nornir::viz::remote::run_release",
"run_release",
"src/viz/remote.rs",
));
calls.push(call_in(
"nornir::run_release_remote",
"run_release",
"src/bin/nornir.rs",
));
let coverage = vec![cov("run_release", "covered")];
let map = build_metro_map(&symbols, &calls, &coverage, &[]);
let line = map
.lines
.iter()
.find(|l| l.id == "run_release_gate→Ops.RunRelease")
.unwrap();
let grpc = line
.stations
.iter()
.find(|s| s.kind == StationKind::Grpc)
.expect("a grpc stop");
assert_eq!(
grpc.branches.len(),
2,
"called by BOTH ui+cli → two arms: {:?}",
grpc.branches
);
let kinds: Vec<StationKind> = grpc.branches.iter().map(|b| b.kind).collect();
assert!(kinds.contains(&StationKind::UiClient), "a UI arm");
assert!(kinds.contains(&StationKind::CliClient), "a CLI arm");
// Markers + lit (the verb was covered).
let ui = grpc
.branches
.iter()
.find(|b| b.kind == StationKind::UiClient)
.unwrap();
let cli = grpc
.branches
.iter()
.find(|b| b.kind == StationKind::CliClient)
.unwrap();
assert_eq!(ui.marker(), Some("UI"));
assert_eq!(cli.marker(), Some("CLI"));
assert!(ui.lit && cli.lit, "covered verb lights both client arms");
// The fork doesn't change the trunk — head/terminus unchanged.
assert_eq!(line.stations.first().unwrap().kind, StationKind::Start);
assert_eq!(line.stations.last().unwrap().kind, StationKind::Terminus);
}
/// A verb called by ONLY the CLI renders a single CLI arm (no UI arm).
#[test]
fn grpc_verb_with_only_cli_caller_renders_one_arm() {
let (symbols, mut calls) = fixture();
calls.push(call_in(
"nornir::run_release_remote",
"run_release",
"src/bin/nornir.rs",
));
let map = build_metro_map(&symbols, &calls, &[], &[]);
let line = map
.lines
.iter()
.find(|l| l.id == "run_release_gate→Ops.RunRelease")
.unwrap();
let grpc = line
.stations
.iter()
.find(|s| s.kind == StationKind::Grpc)
.unwrap();
assert_eq!(grpc.branches.len(), 1, "only the CLI calls it → one arm");
assert_eq!(grpc.branches[0].kind, StationKind::CliClient);
assert_eq!(grpc.branches[0].marker(), Some("CLI"));
assert!(
!grpc.branches[0].lit,
"no coverage → the CLI arm is present-but-unlit"
);
}
// ── the metro ROLLUP (S6 step 4) ─────────────────────────────────────────
/// A hand-built [`FxStation`] of a given kind + lit state (so the rollup test
/// controls the line shape exactly, independent of chain resolution).
fn st(id: &str, kind: StationKind, lit: bool) -> FxStation {
FxStation::new(id, id, kind, lit)
}
/// [`metro_coverage`] counts only RESOLVED lines: a resolved fully-lit line is
/// green, a resolved line with an unlit gating stop is red-but-counted, and an
/// UNRESOLVED skeleton line (an `unresolved::` station) is EXCLUDED from both
/// counts (allowlistable, not hard-red) — so `(green, total) == (1, 2)`.
#[test]
fn metro_coverage_counts_resolved_lines_excludes_unresolved() {
let resolved_green = FxLine::new(
"g",
"g",
vec![
st("start::g", StationKind::Start, true),
st("emit::g", StationKind::Emitter, true),
st("grpc::G", StationKind::Grpc, true),
st("warehouse::t", StationKind::Terminus, true),
],
);
let resolved_red = FxLine::new(
"r",
"r",
vec![
st("start::r", StationKind::Start, true),
st("emit::r", StationKind::Emitter, false), // unlit gating stop → red
st("grpc::R", StationKind::Grpc, true),
st("warehouse::u", StationKind::Terminus, true),
],
);
let unresolved = FxLine::new(
"u",
"u",
vec![
st("start::u", StationKind::Start, true),
st("unresolved::btn", StationKind::PassThrough, true),
st("grpc::U", StationKind::Grpc, false),
st("table?::z", StationKind::Terminus, false),
],
);
let map = FxMap::new(vec![resolved_green, unresolved, resolved_red]);
let (green, total) = metro_coverage(&map);
assert_eq!(total, 2, "only the two RESOLVED lines are counted");
assert_eq!(green, 1, "one resolved line is fully lit");
// An empty map is vacuously (0, 0) — green==total holds (nothing to gate).
assert_eq!(metro_coverage(&FxMap::new(vec![])), (0, 0));
}
/// `build_metro_maps_all` builds one map per workspace, preserving order, and
/// its rollup matches `build_metro_map` + `metro_coverage` per workspace.
#[test]
fn build_metro_maps_all_rolls_up_per_workspace() {
let (symbols, calls) = fixture();
// ws "a": the resolved run_release_gate chain is fully covered → green.
let cov_a = vec![
cov("draw_release_op", "covered"),
cov("nornir::viz::trace::emit_in", "covered"),
cov("run_release", "covered"),
cov("test_matrix_runs", "covered"),
];
let access = vec![(
"OpsSvc::run_release".to_string(),
"test_matrix_runs".to_string(),
)];
let per_ws = vec![
(
"a".to_string(),
MetroFacts {
symbols: symbols.clone(),
calls: calls.clone(),
coverage: cov_a,
access: access.clone(),
},
),
// ws "b": NO coverage → the same resolved chain is red.
(
"b".to_string(),
MetroFacts {
symbols,
calls,
coverage: vec![],
access,
},
),
];
let maps = build_metro_maps_all(&per_ws);
assert_eq!(
maps.iter().map(|(w, _)| w.as_str()).collect::<Vec<_>>(),
vec!["a", "b"]
);
// ws "a": the run_release_gate resolved line is green (others are
// unresolved skeletons, excluded) → at least one resolved-green line.
let (ga, ta) = metro_coverage(&maps[0].1);
assert!(
ta >= 1 && ga == ta,
"ws a: every resolved line green ({ga}/{ta})"
);
// ws "b": the same resolved line is now RED (no coverage) → green < total.
let (gb, tb) = metro_coverage(&maps[1].1);
assert_eq!(tb, ta, "same resolved-line count in both workspaces");
assert!(
gb < tb,
"ws b: the uncovered resolved line is red ({gb}/{tb})"
);
}
/// No client caller of the verb's stub ⇒ no fork arms (an honest empty branch
/// list, not a fabricated arm).
#[test]
fn grpc_verb_with_no_client_caller_has_no_arms() {
let (symbols, calls) = fixture(); // fixture calls don't hit the stub method directly
let map = build_metro_map(&symbols, &calls, &[], &[]);
let line = map
.lines
.iter()
.find(|l| l.id == "run_release_gate→Ops.RunRelease")
.unwrap();
let grpc = line
.stations
.iter()
.find(|s| s.kind == StationKind::Grpc)
.unwrap();
assert!(
grpc.branches.is_empty(),
"no client caller → no arms: {:?}",
grpc.branches
);
}
}