<!-- nornir:class: design -->
# 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
> `IcebergWarehouse` open; thin = which `Viz.*` / `Warehouse.*` / … gRPC verb →
> server → table). Derived by reading `src/viz/*`, `src/bin/nornir-server.rs`,
> `src/viz/model.rs`, `proto/nornir.proto`, and cross-checked against
> [`warehouse-schema.md`](warehouse-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 new `fn→table` access 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`](mode-matrix.md), [`viz-modes.md`](viz-modes.md),
[`design.md`](design.md).
- **The warehouse table map:** [`warehouse-schema.md`](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`](#3-the-wiring-board-new-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 + native `znippy-package`) is a plugin boxed as
`Arc<dyn RepositoryBackendTrait>`. The engine dispatches through the vtable via
an O(1) `FastRoutes` table and never matches the concrete type. (Compare:
nornir's one core, many faces.)
- **Config is a graph.** A RON `holger.ron` of `ExposedEndpoint ← Repository →
StorageEndpoint` nodes, resolved by `wire_holger()` (`server/lib/src/lib.rs`).
- **Dual transport** behind one `HolgerObject` facade: `LocalHolger` (in-process)
vs `RemoteHolger` (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
> matching `Viz.*` impl in `src/bin/nornir-server.rs` (`impl VizSvcTrait for
> VizSvc` at line 2817) + `proto/nornir.proto` (the `Viz` service 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 runs `knowledge::scan_all` over 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.Scan`** in thin mode (not a
dedicated `Viz.*` verb) — they read `symbol_facts`/`call_edges` and
`mcp_requests` by scanning.
- **DepGraph / TimeTravel** have **no dedicated DepGraph RPC** — the dep
snapshots ride inside the `Viz.Timeline` JSON response.
- The MD-MATRIX test (`tests/viz_md_matrix.rs`) tracks the thin-present cells;
the dedicated `Viz.*` 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`](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 `nornir` server 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`](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 (from `call_edges`), core fn → table (the **new `fn→table`
access fact**, ARCH2).
- **Render:** the AUT7 owner-fn quotient of `call_edges`, drawn skade-style
(Typst→SVG, **never mermaid**), regenerated by `nornir docs` (ARCH3), surfaced
as `nornir arch [svg | trace <entrypoint>]` + an MCP tool reusing
`knowledge_call_path` (ARCH5).
- **Storage:** per-`workspace_*/` asset (travels with source) and/or a warehouse
`architecture_wiring` table 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.