nornir architecture wiring — the code-derived map
EPIC ARCH (nordisk plan). The complete, code-derived picture of how every nornir mechanism is wired: ONE core lib with many faces, the sibling crates + ecosystem peers, and — the heart of the ask — which warehouse table each of the 24 viz panes reads/writes and the exact flow path (fat = direct
IcebergWarehouseopen; thin = whichViz.*/Warehouse.*/ … gRPC verb → server → table). Derived by readingsrc/viz/*,src/bin/nornir-server.rs,src/viz/model.rs,proto/nornir.proto, and cross-checked againstwarehouse-schema.md(the gold table↔write/read map).This is the human-authored data model behind EPIC ARCH / AUT7. The long-term plan (ARCH2/ARCH3) is to auto-generate this from
call_edges+ the newfn→tableaccess fact + the UI-Facet ∩ registry()enumeration, rendered skade-style. Until then, this doc is the source of truth and the spec the generator must reproduce.
0. Where this lived before (gap analysis)
What already existed in .nornir/:
- EPIC J flow SVGs (circuit-board house style, all embedded in docs):
release_events_flow.svg(J3),warehouse_layers.svg(J3),deployment_modes.svg+test_matrix_pipeline.svg+viz_thin_rpc.svg(J3b),nordisk-constellation.svg(J5). - Mode docs:
mode-matrix.md,viz-modes.md,design.md. - The warehouse table map:
warehouse-schema.md— every table's write-site and read-site, exhaustively.
The GAP this doc closes: there was no single per-pane → table(+RPC+flow) map for all 24 viz panes, and no one all-component wiring board that draws the constellation, the thin/fat/server flow, and the pane↔table flow together. viz_thin_rpc.svg showed the mechanism (mode resolve → a token-bearing verb → Arrow batch → table) for one generic pane; warehouse-schema.md listed read-sites in prose. Neither answered "open the Test tab — what does it read, by what path?" for every pane at once. That table + the new architecture_wiring.svg are the new artifacts.
No drift was found between the existing SVGs and code; this doc synchronizes text + picture by embedding the relevant existing SVGs alongside the new per-pane board, so every claim here has a matching figure.
1. Component constellation — one core, many faces
nornir is one core library (nornir, src/lib.rs) with several faces, each a thin binary or surface over the same core:
| Face | Crate / bin | Over the core it… | Source |
| CLI | nornir (src/bin/nornir.rs) |
runs every verb in-process, opening the warehouse direct (the fat path) | bin/nornir.rs |
| gRPC server | nornir-server (src/bin/nornir-server.rs) |
the intended single owner of the iceberg/redb lock; mirrors the CLI/MCP API one-for-one over tonic | bin/nornir-server.rs |
| MCP | nornir-mcp (src/bin/nornir-mcp.rs) |
exposes the core as 70 MCP tools (stdio) for LLM agents; writes mcp_requests telemetry |
bin/nornir-mcp.rs |
| viz | nornir viz → execs nornir-ui (src/bin/urdr-threads.rs) |
the egui app; renders 24 panes, fat or thin (see §2) | src/viz/* |
Sibling crates (in this repo's crates/) and ecosystem peers:
| Component | Role | How it connects |
nornir-testmatrix |
the autonom test-matrix engine (Aspect enum, runners, parsers) | feeds the 🧪 Test pane via test_results; the AUTONOM completeness gate |
nornir-robotui |
richer live-drive of the viz (set_field/click_id/inject_event, AccessKit) |
drives the viz headlessly for robot tests (EPIC R) |
skidbladnir (Skíðblaðnir) |
pack/move/unfold/install an air-gapped bundle (EPIC AIRGAP). Extracted out of crates/ into the sibling repo ../Skidbladnir (crate name skidbladnir, path dep in nornir's Cargo.toml — no crates/nornir-airgap any more) |
persists airgap_events to the warehouse; reuses the dep-graph topo sort; nornir's airgap CLI verb + airgap_* MCP tools drive it |
| skade / skade-katalog | the Iceberg catalog + data-plane the warehouse runs on | IcebergWarehouse IS Apache Iceberg over a single-file skade-katalog redb catalog; the warehouse ground rail. Reuses RedbCatalog::commit_table for schema evolution |
znippy (+ znippy-zoomies) |
Arrow-IPC parallel archives + the GATLING no-barrier worker pool | znippy-zoomies is a dep (GATLING + zero-copy law); artifact blobs + live transport are Arrow-IPC; holger serves znippy archives |
| facett | the reusable egui component library the viz renders with | every viz pane uses facett components + the facett theme (vendored as facett_theme/facett_ui pending egui-version align) |
| holger | the artifact/file server — a first-class component, not a dep | nornir monitors holger as a workspace repo (release/dep graph), and holger is nornir's deploy/registry peer: it serves crate/file artifacts (and every ecosystem's native protocol) out of immutable, blake3 content-addressed znippy archives. See §1a |
1a. holger in the picture
Holger (/run/media/rickard/T9/git/holger, codeberg.org/nordisk/holger) is a secure, airgap-first artifact server. Its shape rhymes with nornir's:
- One trait, many backends.
RepositoryBackendTrait(traits/src/lib.rs) is the single seam — every ecosystem (Maven/Python/Rust/npm/NuGet/Go/Gem/Deb/RPM/ Helm/Docker/Conda/Composer + nativeznippy-package) is a plugin boxed asArc<dyn RepositoryBackendTrait>. The engine dispatches through the vtable via an O(1)FastRoutestable and never matches the concrete type. (Compare: nornir's one core, many faces.) - Config is a graph. A RON
holger.ronofExposedEndpoint ← Repository → StorageEndpointnodes, resolved bywire_holger()(server/lib/src/lib.rs). - Dual transport behind one
HolgerObjectfacade:LocalHolger(in-process) vsRemoteHolger(gRPC/tonic) — the same fat/thin split nornir's viz has. - Storage = immutable znippy archives (blake3 content-addressed, one per repo version, random-access
ZnippyReader::extract_file— no full decompress), carriable across the airgap. This is the same znippy nornir depends on.
How they relate: nornir monitors holger as a workspace repo (it appears in nordisk-constellation.svg as a monitored repo) — so holger gets a dep graph, release lineage, knowledge map, test matrix, etc. like every other nordisk repo. And holger is nornir's release target: where nornir release publishes crates / where skidbladnir (Skíðblaðnir, EPIC AIRGAP; sibling repo ../Skidbladnir) ships a bundle's artifacts to be served on the secure side.
The constellation figure (deps + orchestrator role over the monitored repos holger · skade · znippy · knut · facett · korp · njord):
Figure 1 — the constellation (EPIC J5). nornir orchestrates; holger/skade/znippy/facett are peers.
2. Per-UI-pane → warehouse-table flow (the heart)
The viz has 24 tabs (pub const ALL: [Tab; 24] in src/viz/app.rs — the authoritative header order: Nornir, Timeline, DepGraph, CallGraph, Architecture, Metro, Helix, Bloodstream, Funnel, TimeTravel, LiveRun, Release, Knowledge, Warehouse, WarehouseDeck, Mcp, Search, Gates, Bench, Test, Leaderboard, Security, Holger, Manual). The 16 numbered rows below are the original data-bearing panes; rows 17–21 are five chrome/overview panes (Nornir, Architecture, Metro, Holger, Manual). Three newer tabs — Helix, Bloodstream, and WarehouseDeck — are also in Tab::ALL (24 total) but are not yet broken out as rows here. Each pane is built local(...) (fat) or remote(endpoint, token, workspace) (thin) at construction (UrdrThreadsApp::new matching on Source::Local vs Source::Remote). Its load()/refresh()/hydrate() then either opens the warehouse direct or calls a gRPC verb. Every pane exposes what it renders in state_json()[key] (LAW #6).
Authoritative map — derived from each pane's loader in
src/viz/*and the matchingViz.*impl insrc/bin/nornir-server.rs(impl VizSvcTrait for VizSvcat line 2817) +proto/nornir.proto(theVizservice from line 809).
| # | Pane (state_json key) | Warehouse table(s) | Fat path (local query fn) | Thin path (gRPC verb) |
| 1 | 🧵 Timeline (timeline) |
release_lineage + dep_graph_edges |
build_timeline → query_dep_graph_snapshots + release_lineage scan (viz/model.rs) |
Viz.Timeline |
| 2 | 🔗 DepGraph (dep_graph) |
dep_graph_edges |
from the Timeline model's DepGraphSnapshot.edges (depgraph_layout.rs) |
Viz.Timeline (dep snapshots ride the timeline) |
| 3 | 🕸 CallGraph (callgraph) |
symbol_facts + call_edges |
IcebergWarehouse::scan_preview(table, 8000) (callgraph.rs) |
Warehouse.Scan (same two tables) |
| 4 | 🗂 Funnel (funnel) |
funnel_events (+ release_lineage/dep_graph_edges for the auto-planner) |
funnel::Store::open / funnel::history / Store::record (write) (funnel_tab.rs) |
Funnel.Show / Funnel.History / Funnel.SubmitIdea |
| 5 | ⏳ TimeTravel (timetravel) |
release_lineage + dep_graph_edges (via the Timeline model + artifact pins) |
build_timeline (consumes the model; no direct table read in timetravel.rs) |
Viz.Timeline |
| 6 | 📡 LiveRun (live) |
release_events (durable) + /tmp ndjson tail (live) |
query_release_events for the durable hydrate (live.rs) |
Viz.ReleaseEvents (hydrate) + Release.Progress (live stream) |
| 7 | 🚀 Release (release) |
release_events |
query_release_events(&wh, EventSelector::All) (release_tab.rs) |
Viz.ReleaseEvents |
| 8 | 🗺 Knowledge (knowledge) |
symbol_facts + call_edges (server scans the live clones, not the table) |
knowledge::scan_all over each member checkout (knowledge.rs) |
Viz.Knowledge (server runs scan_all over <root>/git/<member>) |
| 9 | 🗄 Warehouse (warehouse) |
ALL tables (generic browser) | IcebergWarehouse::table_names() + scan_preview(table, 500) (warehouse_tab.rs) |
Warehouse.Tables + Warehouse.Scan |
| 10 | 📞 Mcp (mcp) |
mcp_requests |
scan_preview("mcp_requests", 50000) → per-tool aggregate (mcp_tab.rs) |
Warehouse.Scan |
| 11 | 🔍 Search (search) |
tantivy code index + embeddings + symbol_facts (not a single Iceberg scan) |
server-only (no fat path; local shows a hint) | Search.Query (BM25) · Vector.Search (semantic) · Index.Stats · Knowledge.SymbolLookup/Callers/Callees/CallPath |
| 12 | 🚦 Gates (gates) |
none (gate logic + bench_history.jsonl) |
server-only | Release.GateAll + Release.Trace |
| 13 | 📈 Bench (bench) |
bench_telemetry + bench_runs (live panel) ; bench_history.jsonl (history) |
query_bench_telemetry(None) + query_bench_runs (bench_live.rs); history via JSONL |
Viz.BenchTelemetry (live) + Bench.History (history) |
| 14 | 🧪 Test (test) |
test_results |
query_test_results(&wh, TestSelector::All) (test_tab.rs) |
Viz.TestResults |
| 15 | 🏆 Leaderboard (leaderboard) |
agent_model_runs |
query_agent_model_runs(&wh, BakeoffSelector::All) (leaderboard_tab.rs) |
Viz.BakeoffResults |
| 16 | 🛡 Security (security) |
vuln_findings + sbom_components |
crate::security::scan(<root>/<repo>) (live cargo metadata + OSV, warehouse-first cache) (security_tab.rs) |
Mimir.SecurityScan |
| 17 | 🧬 Nornir (nornir) |
registry (redb) + the in-flight jobs tree (no single Iceberg table; an overview/roster pane) | server registry roster + jobs aggregate (nornir_tab.rs / nornir_dashboard.rs) |
Health.Ping (server status) + the per-pane aggregates |
| 18 | 🏛 Architecture (architecture) |
symbol_facts + call_edges + dep_graph_edges (the arch graph) |
crate::arch graph build over the live clones (arch_tab.rs) |
Viz.Architecture |
| 19 | 🚇 Metro (metro) |
test_results (the button→gRPC→warehouse coverage chains) |
metro_feed::build_metro_map over the coverage facts (metro_tab.rs) |
Viz.Coverage |
| 20 | 📦 Holger (holger) |
holger married-registry summary (remote holger warehouse, not a local table) | remote::fetch_holger_registry (holger_tab.rs) |
Viz.HolgerRegistry |
| 21 | 📖 Manual (manual) |
none — packed book-svg pages on disk ($HOME/.nornir/manual) |
local book-svg page loader (manual_tab.rs) |
— (GUI/local only) |
2a. The contract the Viz.* RPCs honor
The five dedicated Viz.* data RPCs are thin mirrors of the fat query fns — same data, JSON-serialized — because the server holds the redb lock so a thin viz can't open the warehouse (src/bin/nornir-server.rs):
Viz.Timeline→build_timeline(&wh, ws, root)→release_lineage+dep_graph_edges.Viz.ReleaseEvents→query_release_events(&wh, EventSelector::All)→release_events.Viz.TestResults→query_test_results(&wh, TestSelector::All)→test_results.Viz.BakeoffResults→query_agent_model_runs(&wh, BakeoffSelector::All)→agent_model_runs.Viz.BenchTelemetry→query_bench_telemetry(None)+query_bench_runs→bench_telemetry+bench_runs.Viz.Knowledge→ server runsknowledge::scan_allover each member clone (the thin client has no checkout).
The write-back RPCs (so a fat CLI run lands in the locked server warehouse) live on the Telemetry service: SubmitReleaseEvents → release_events, SubmitTestResults → test_results, SubmitBakeoff → agent_model_runs, SubmitMcpCalls → mcp_requests. These honor the LAW "persist to warehouse, stream not /tmp".
2b. The pane↔table wiring board (NEW SVG)
Figure 2 — the 23-pane wiring board (this doc's primary new artifact; the SVG still renders the 16 data-bearing panes — regenerate to add the seven overview/chrome panes). Pane → gRPC verb → table → warehouse ground rail. THE pane↔table map.
2c. Known coverage gaps (from the code)
- Search, Gates have no fat path — they are server-only; in local mode the pane shows a hint. (Search needs the warm Tantivy index + embedder the server holds; Gates runs gate logic against
bench_history.jsonl.) - CallGraph, Mcp use the generic
Warehouse.Scanin thin mode (not a dedicatedViz.*verb) — they readsymbol_facts/call_edgesandmcp_requestsby scanning. - DepGraph / TimeTravel have no dedicated DepGraph RPC — the dep snapshots ride inside the
Viz.TimelineJSON response. - The MD-MATRIX test (
tests/viz_md_matrix.rs) tracks the thin-present cells; the dedicatedViz.*verbs (ReleaseEvents/TestResults/BakeoffResults/ BenchTelemetry) closed the earlier Release/Test/Leaderboard/Bench gaps.
3. Thin / fat / server data flow (the mode example)
The pane↔table map above is parameterized by mode. The three modes (mode-matrix.md):
- Fat (embedded): the human's CLI/viz opens the warehouse in-process and owns the redb lock. Every "Fat path" cell in §2 is a direct call.
- Server-monitored: the
nornirserver clones member git URLs and runs the same verbs under its write lock; it is "thin against itself, fed by pull." - Thin client: the viz holds no lock; every "Thin path" cell is a token-bearing gRPC verb to the server, which owns the lock.
Figure 3 — fat / server-monitored / thin (EPIC J3b). The example the ask kept as one section.
The mode-resolution → token-bearing verb → Arrow batch → table mechanism for a single thin pane (the generic shape every "Thin path" row in §2 follows):
Figure 4 — the thin-RPC mechanism (EPIC J3b), one pane generalized.
4. The warehouse layers + the two flagship flows
The ground rail of every row in §2 is the IcebergWarehouse — Apache Iceberg (iceberg-rust) over a single-file skade-katalog redb catalog, append-only, so each append mints a snapshot (free time-travel). Its read-acceleration layers:
Figure 5 — warehouse layers (EPIC J3).
The two write-flows that feed the busiest panes:
Figure 6 — release_events DAG flow → 🚀 Release + 📡 LiveRun (EPIC J3).
Figure 7 — test-matrix pipeline → 🧪 Test (EPIC J3b).
Full per-table schema, write-sites and read-sites: warehouse-schema.md.
5. This is the data model behind EPIC ARCH / AUT7
The §2 table is the human-authored version of the graph EPIC ARCH/AUT7 will auto-generate:
- Nodes: UI-component (pane) · gRPC verb · core fn · warehouse table.
- Edges: pane → verb (from
Facet ∩ registry()+ the tab enum × {thin,fat}), verb → core fn (fromcall_edges), core fn → table (the newfn→tableaccess fact, ARCH2). - Render: the AUT7 owner-fn quotient of
call_edges, drawn skade-style (Typst→SVG, never mermaid), regenerated bynornir docs(ARCH3), surfaced asnornir arch [svg | trace <entrypoint>]+ an MCP tool reusingknowledge_call_path(ARCH5). - Storage: per-
workspace_*/asset (travels with source) and/or a warehousearchitecture_wiringtable historized by git sha (ARCH4).
When the generator lands, regenerating this doc's §2 table + Figure 2 from the warehouse must reproduce exactly what is hand-derived here — this doc is the acceptance fixture.
6. The generator has landed (ARCH1–5 / AUT7) — how to use it
The auto-generator now exists in code (src/arch/mod.rs + src/arch/warehouse.rs), so §5 is no longer forward-looking. The mapping from plan item to code:
| plan item | what | code | proving test |
| ARCH1 | UI-pane ↔ gRPC verb map (Facet impls + *Svc/*SvcTrait handlers → Component / Service.Verb nodes) |
facet_impls_from_symbols, grpc_handlers_from_symbols (arch/mod.rs) |
generate_end_to_end_derives_both_maps, grpc_handlers_derived_from_symbols |
| ARCH2 | coarsened owner-fn quotient + fn→table access fact |
build_graph/classify (arch/mod.rs) + access_scan::scan_source (warehouse/access_scan.rs) |
test_tab_fetch_collapses_to_component_and_table, self_edges_and_dupes_collapse_away, extracts_named_read_and_write_edges |
| ARCH3 | skade circuit-board SVG, never mermaid (direct-emit, no engine, available in every feature set) | ArchGraph::to_svg/render_pcb_svg (arch/mod.rs) |
svg_is_circuit_board_with_expected_labels (asserts no mermaid) |
| ARCH4 | persist to warehouse architecture_wiring, historized by git sha, deduped on (repo,git_sha,graph_sha256) |
arch/warehouse.rs + iceberg_schema::architecture_wiring |
record_then_list_and_load_round_trip, dedup_skips_identical_graph, different_git_sha_makes_new_row |
| ARCH5 / AUT7 | CLI + MCP parity | nornir arch generate|svg|trace|history (bin/nornir.rs run_arch); arch_svg / arch_graph / arch_trace MCP tools (bin/nornir-mcp.rs) |
mcp_every_tool_exercised_and_asserted (coverage gate), trace_from_entrypoint_keeps_only_reachable |
CLI usage (fat, in-process):
nornir arch generate <repo> # scan → coarsen → render → historize (git-sha keyed)
nornir arch svg <repo> [--out f] # print/write the wiring SVG without historizing extra rows
nornir arch trace <repo> <entry> # focused sub-board reachable from an entrypoint (the flow trace)
nornir arch history <repo> # list historized snapshots, newest first
MCP parity (LLM agents, reads the latest historized snapshot from the warehouse): arch_svg{repo} → the board SVG · arch_graph{repo} → the graph JSON · arch_trace{repo,entrypoint} → the reachable sub-board SVG (the visual twin of knowledge_call_path).
Naming note (deviation from the plan prose, by design): the plan sketched the trace surface as nornir flow <entrypoint> + a flow_trace MCP tool and a Typst render under nornir docs. The shipped surface folds the trace into the arch verb (nornir arch trace) and the arch_trace MCP tool — one cohesive arch namespace — and renders a direct-emit skade SVG (no Typst layout engine, so it works in every feature set). The skade-style / no-mermaid / owner-fn-quotient / fn→table / warehouse-historized intent is met verbatim; only the verb/tool names and the (Typst→)SVG mechanism differ.