🚇 Metro map — warehouse-backed read (thin mode) + the populated-state EMITTER
Design + guide for the fix that makes the 🏛 Architecture Metro map show the real button → gRPC call PATH in thin / remote mode, and emit its populated-or-empty state as DATA so it is verifiable WITHOUT a human looking at the GUI. (Authored 2026-06-22, branch agent/nornir-metro-emitter.)
The bug
src/viz/arch_tab.rs built the metro lines ONLY from a local checkout scan (ensure_symbols → crate::knowledge::scan_all(repo_root, repo)). In thin / remote mode repo_root is empty, so the scan produced no symbols / calls and every line degraded to a click-handler-head stub ("no local checkout — call edges weren't scanned"). That silent degrade is a BUG, not a fallback: the warehouse already holds call_edges + symbol_facts for the workspace (written by append_symbol_scan during republish), so thin mode should READ them, not give up.
The fix — read the warehouse over Viz.Architecture
The metro fuel now rides in the existing Viz.Architecture RPC payload.
Server (src/bin/nornir-server.rs, architecture handler)
After merging the board, the handler reads each member's latest knowledge snapshot via nornir::knowledge::query::load_latest(&wh, member) (the SAME read the knowledge MCP tools use) and merges every member's symbols + calls into the JSON payload:
{ "graph": …, "members_with_data": …, "errors": …, "coverage": …,
"symbols": [SymbolRow…], "calls": [CallEdgeRow…] }
Payload (ArchPayload)
Gained symbols: Vec<SymbolRow> + calls: Vec<CallEdgeRow> (both #[serde(default)] for back-compat). SymbolRow / CallEdgeRow gained Deserialize so the thin client can decode them.
Client (arch_tab.rs)
ensure_loaded's remote branch seedsself.symbols/self.callsfrom the payload (only when the server actually supplied facts), then dropsmetro_linesso they rebuild from the warehouse facts.ensure_symbolstriggersensure_loadedin remote mode (the Metro lens doesn't otherwise load the board), so opening Metro fetches the warehouse fuel.- The "no local checkout → stubs" degrade is GONE in thin mode. The only legitimate thin empty-state is an unpopulated warehouse (zero
call_edges), which the pane now reports with a POPULATE-DIAGNOSTIC ("run a populate / deep scan so the server persists call_edges"). - Fat mode is unchanged: it still scans the local checkout.
fuel_source (warehouse thin / checkout fat) is on the metro state slice so a drive can prove thin mode is genuinely warehouse-backed.
The EMITTER — verify-without-looking (RAGNARÖK)
A metro draw is a pixel / render stage whose correctness can't be read from a return value (EMIT-DOCTRINE), so emit_metro_status() implants a functional_status row gated behind #[cfg(feature = "testmatrix")], plus an architecture.metro.status trace. It is called from ensure_metro (build time).
Aggregates (MetroAggregates, also folded into state_json["architecture"]["metro"]):
| field | meaning |
metro_edges |
call-path rail segments drawn, summed over lines (0 ⇒ blank) |
metro_nodes |
total stations (functions) drawn |
metro_emitter_nodes |
stations MARKED emitters (emit*/trace/functional_status sites) |
metro_stub_lines |
lines degraded to a head-only stub whose handler IS resolvable |
reached_lines |
lines that reached their gRPC terminus |
metro_stub_lines deliberately counts ONLY lines whose grpc_terminus resolved (the path COULD exist) yet didn't reach. A button whose handler symbol is simply absent from the current fuel set isn't a stub — it has no resolvable target. This makes metro_stub_lines == 0 the exact populated-state invariant: every line that could reach did.
Emit verdict: ok ⇔ metro_edges > 0 && metro_emitter_nodes > 0 && metro_stub_lines == 0.
The RAGNARÖK tests (tests/viz_arch_metro.rs)
metro_thin_warehouse_backed_reaches_grpc_and_emits_populated— thin pane fed a populatedArchPayload(warehouse facts),repo_rootempty: assertsfuel_source == warehouse,metro_edges > 0,metro_stub_lines == 0,metro_emitter_nodes > 0, and the selected line reachesrun_test_matrix.metro_thin_empty_warehouse_is_red_not_silently_green— symbols but NOcall_edges: asserts the populated verdict is FALSE (RED),metro_edges == 0,metro_emitter_nodes == 0, a resolvable line degraded to a stub. The sensitivity / inverse proof.metro_thin_reads_real_warehouse_call_edges— seeds a realIcebergWarehousewithappend_symbol_scan, reads it back viaquery::load_latest(the server's read path), feeds THAT into the thin metro, asserts it populates. Proves the server READ PATH yields real fuel, not just that client seeding works.
Emitter-node marking — wired vs partial
Emitter nodes ARE marked: each station classified Boundary::Emitter by the shared coverage::classify is counted into metro_emitter_nodes and rendered as the "emitter" interchange (the ⟲ tag + point-colour station). Marking is therefore fully wired for any line that REACHES its gRPC terminus (the BFS path is walked and each station classified). A line that does NOT reach (an unresolved button or an unpopulated warehouse) has only its head station, so it contributes zero emitter nodes — correct by construction (there are no intermediate stations to mark). So: emitter marking is fully wired on reached lines; on unreached lines there is nothing to mark, which the emitter reports as metro_emitter_nodes contribution 0.
Live deploy note
The live server runs the INSTALLED binary; these changes need a human reinstall to go live. The tests run against a TEST warehouse (temp IcebergWarehouse), never the live server.