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

nordisk/nornir

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