Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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
        );
    }
}