Gunnar

nordisk/nornir

.nornir/architecture-wiring.md

<!-- 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):

![nordisk constellation: nornir's crate deps (znippy-zoomies, nornir-testmatrix, skade-katalog + skade) and its orchestrator role — workspace descriptor + registry to git clone/fetch to republish hook to per-workspace warehouse to viz/MCP, over the monitored repos holger, skade, znippy, knut, facett, korp, njord.](assets/nordisk-constellation.svg)

*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)

![nornir architecture wiring: 24 viz panes on the left, each routed to the gRPC verb it calls in thin mode (Viz.Timeline, Viz.ReleaseEvents, Warehouse.Scan, Funnel.Show, Release.GateAll, Mimir.SecurityScan, …) in the middle, and the warehouse table(s) it ultimately reads/writes on the right (release_lineage+dep_graph_edges, release_events, test_results, agent_model_runs, bench_telemetry+bench_runs, symbol_facts+call_edges, funnel_events, mcp_requests, vuln_findings+sbom_components), all grounding on the IcebergWarehouse rail — Apache Iceberg over a skade-katalog redb catalog. Cyan traces are the fat direct-open path; dashed amber are the thin gRPC path.](assets/architecture_wiring.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.

![three deployment modes: fat reads a local checkout and runs every verb in-process under the embedded redb lock; server-monitored clones member git URLs and the poll loop runs the same compute under the server's write lock; thin pushes the client's working tree to the server which computes on it — all three drive identical verbs into one per-workspace warehouse.](assets/deployment_modes.svg)

*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):

![viz thin-RPC mechanism: mode resolution picks remote, a tab issues a token-bearing gRPC Viz verb, the server scans its locked warehouse and returns an Arrow/JSON batch from the backing table, which the pane renders.](assets/viz_thin_rpc.svg)

*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:

![warehouse layers: the IcebergWarehouse over a skade-katalog redb catalog, with metadata cache, handle cache and a snapshot-id index accelerating reads, falling through to redb as the ACID source of truth.](assets/warehouse_layers.svg)

*Figure 5 — warehouse layers (EPIC J3).*

The two write-flows that feed the busiest panes:

![release_events flow: the release pipeline (server + fat CLI) emits one row per component×op×phase boundary through the ReleaseEventEmitter into the release_events table, which the Release tab and LiveRun durable-hydrate read back.](assets/release_events_flow.svg)

*Figure 6 — `release_events` DAG flow → 🚀 Release + 📡 LiveRun (EPIC J3).*

![test-matrix pipeline: nornir test run drives detect_runner, the parser turns libtest/nextest output into rows, append_test_results writes the test_results table, and the Test pane reads it back into the per-repo×per-aspect matrix.](assets/test_matrix_pipeline.svg)

*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.