Markers → Matrix → Architecture-viz — design, data-paths, and the build plan
The authoritative design for nornir's marker / test-matrix / Architecture-tab wiring. Decided with Rickard, 2026-06-16. This doc is also the instructions a programmer reads to know how to mark a function (§4). Companions: autonom.md, self-emitters.md, architecture-wiring.md, viz-architecture-tab.md, mode-matrix.md.
1. The goal (the keystone)
The 🏛 Architecture tab walks every marker (UI-component / gRPC / core-fn / table) and cross-checks it against the test matrix (surface_coverage): green = a test exists for it, red = none. Markers → matrix → visible coverage.
2. Two orthogonal axes (never conflate)
- fat ↔ thin — where the data comes from: embedded warehouse (fat) vs gRPC to
nornir-server(thin). Selected by the--serverflag (present → thin, absent → fat). No environment variables. - UI ↔ gRPC — a function's layer (
arch::NodeKind: Component / Grpc / CoreFn / Table). The coder marks it in source; autonom only FINDS it (by reading the warehouse). autonom never marks.
2b. The ATOM axis — the live AccessKit/kittest walk (L0) — added 2026-06-22
The markers above are the declared truth (which UI surface ↔ which gRPC). They do NOT, on their own, prove every rendered atom ran and is error-free — and they are per-button / per-tab, so sub-panels (e.g. the 🧬 tab's CloneEvents "populate status" + Jobs panels) are in no enumeration and slip through (the `` is not served leaks). The fix is a fourth layer that VERIFIES the markers against the real UI, down to the atoms — it does NOT replace them:
L0 egui_kittest + AccessKit — walk the REAL rendered widget tree (every button,
label, error-text = an AccessKit node); the only
layer that sees sub-panel atoms. EGUI-NATIVE.
L1 markers / metro — atom ↔ gRPC wiring (button_registry → 🚇). DECLARED.
L2 Facet state_json — the data a surface renders. (the atom-walk also
covers hand-rolled egui, which has no Facet.)
L3 autonom surface_coverage — the GATE: green ⟺ every atom covered.
The atom-walk lives in nornir (the dogfood); the framework nornir-testmatrix owns only the generic concept — a surface enumerator emitting surface_coverage rows at atom granularity with a verifier verdict (UI-agnostic):
- render each
tab × {empty-registry, populated}through akittest::Harness; - walk the AccessKit tree and assert: (a) NO error atom (
is not served/Failed to load/unavailable) in the empty state; (b) the expected populated atoms present (LAW 2 RAGNARÖK) when populated; - emit one
surface_coveragerow per atom (workspace × tab × atom × state).
Integration / what CHANGES (nothing removed):
button_registry()becomes auto-derived from the walk (kills the hand-list + its drift); the metro reads the same atoms.- autonom reads atom rows exactly like marker rows; the gate fails on any uncovered atom or empty-state error atom.
- bespoke per-surface tests (e.g.
viz_empty_registry_onboarding's hand-list of 9 tabs) fold into the one atom-walk (all tabs × all atoms × states).
The AccessKit label is the UNIVERSAL MARKER — one kit, two oracles (Rickard 2026-06-22). The reusable driver is nornir-robotui (the egui_kittest robot — crates/nornir-robotui, addresses widgets by WidgetRef::label/id). Every widget — a nornir-viz atom OR a facett component's control — carries an AccessKit label, so the marker primitive is the same for both; only the oracle differs:
- nornir hand-rolled egui → oracle = the AccessKit-tree scan (atom presence / no-error; coarse data, so read the rendered tree).
- facett components (the multidimensional / sample-space values) → oracle = the Facet
state_json:nornir-robotuiDRIVES the input sample-space (set slider v1…vN, click, type), andstate_jsonrecords the multidimensional outputs (LAW 3 utfallsrum). NOT the pixel tree. So the atom-walk and facett's sample-space sweep are the same driver, different oracle — both addressed by AccessKit-label markers, both emittingsurface_coveragerows. No separate marker scheme for facett.
THIRD oracle — wgpu/WGSL renders (map3d, graph3d) are NOT egui (Rickard 2026-06-22). A GPU render is a single OPAQUE AccessKit node — kit sees *that the 3D view is present
-
ran*, but cannot see INSIDE it (no atoms, just a texture). So its correctness oracle is neither the a11y tree nor pixels-alone, but the RENDER oracle:
-
the component emits its geometry as readable data (
state_json: faces / ink /clipped_escapes/ camera pose), AND -
renders to an offscreen PNG read back + asserted (non-blank, 0 px outside the widget rect), swept across the camera sample-space (360° × zoom × elev — one pose proves nothing in 3D). Both halves are needed: geometry alone missed the OSM-3D zero-area-sliver smear that pixels showed; pixels alone miss what the GPU culled. The AccessKit label still marks the 3D view (present/reachable/ran); correctness is the render oracle. Already built:
facett-map3d/tests/partstep_sweep.rs(MAP-TEST-SWEEP) is this oracle —scissor.clipped_escapes == 0over 7200 poses + the worst-pose PNG-blob. It slots intosurface_coveragevia its Facetstate_json+ PNG-blob, same kit/marker.
The metro LINE = the chain, start→terminus (Rickard 2026-06-22)
A metro line is one button-to-warehouse chain; the atoms/emitters are its stations:
START stations (emitters) END terminus
[UI button atom] → f1 ▪ f2 ▪ f3 ▪ … (call_edges; → [gRPC RPC] → server → warehouse
.clicked(), each functional_status emit handler table (thin: LAW 4/8)
AccessKit marker) that fired = a LIT station)
- start = the button atom (its
.clicked()handler) — the AccessKit marker the atom-walk clicks; - stations = the emitters on the call-chain — each fired emit = a lit station;
- end = the gRPC handler; in thin mode the line continues server → warehouse table (the real terminus — LAW 4 end-to-end-through-server, LAW 8 warehouse-is-home). A line is green only when every station lit when its start atom was clicked. So the metro is the chain-coverage made visual; the atom-walk lights the start, the emitters prove the middle, autonom colours the line.
Emitters are the oracle of LAST RESORT, testmatrix-flag-gated (Rickard 2026-06-22). An emitter is a side-channel return value, compiled ONLY under the testmatrix feature (zero production cost). EMIT-DOCTRINE: emit a functional_status only when the normal return value can't carry what you must assert (a render that returns (), a deep stage like clipped_escapes nothing surfaces). So the three oracles minimize stations: where the return value, the Facet state_json, or the AccessKit tree already exposes the fact, read that — no emitter. A station (emitter) exists exactly where all three are blind. Station rule: return → state_json → a11y-tree → (only if blind) emit, gated.
Oracle taxonomy (one kit + one marker primitive, three oracles)
| component kind | driver | oracle (correctness) |
| hand-rolled egui | nornir-robotui | AccessKit-tree scan (atom presence / no-error) |
| facett data component | nornir-robotui | Facet state_json (multidimensional / sample-space) |
| wgpu/WGSL (map3d, graph3d) | nornir-robotui | RENDER: geometry state_json + offscreen PNG, swept over the pose sample-space |
2c. ATOM-LAYER BUILD PLAN (checklist — 2026-06-22; polish design/guide/idiot-guide AFTER it works)
-
0.
nornir-robotuiis the shared kit — it BUILDS (the "can't find crate" was a feature-resolution hiccup, not a break);RobotSessionwraps a kittestHarness. It addresses widgetsby_label; the atom-walk needs the tree-SCAN below. kit API found:kittest::By::new().label_contains(s)/.predicate(|n:&AccessKitNode| …)Queryable::query_all(by)over the live tree (Harness::root()/node()); node text via.label()/.value(). So the "walk" = aquery_allfilter, not a manual recursion. Add arobotui::error_atoms(&harness) -> Vec<String>helper (query_all of the error-marker predicate) — the reusable scan.
-
1. Atom-walk #1 (the proof) — DONE 2026-06-22:
robotui::error_atoms()+tests/viz_atom_walk.rs(empty server + remote viz + kittest walk, RED→GREEN proven); supersedesviz_empty_registry_onboarding. ORIGINAL: —nornir-testmatrix(generic) gets thesurface_coverageatom-row + verifier-verdict concept; nornir gets the egui driver: pertab × {empty-registry, populated}→ kittest render →error_atoms()query → assert (a) NO error atom (is not served/Failed to load/unavailable) in empty, (b) populated atoms present. KEY constraint: the CloneEvents/Jobs errors are REMOTE-mode (gRPC) — the walk must drive the viz in--servermode against a spawned EMPTY server to reproduce them (local mode never calls gRPC, so a local walk would pass vacuously). So atom-walk #1 = aspawn_server(empty)+ remote-Sourceviz- kittest harness +
error_atoms(). It SUPERSEDES thestate_json-basedviz_empty_registry_onboarding(which is blind to the rendered sub-panel atoms).
- kittest harness +
-
2. Auto-derive
button_registryfrom the walk (kill the hand-list); metro + autonom read the same atoms; emit atom-granularitysurface_coveragerows. -
3. Roll the three oracles — data components →
state_jsonsample-space; wgpu (map3d/graph3d) → render oracle (geometry + offscreen PNG over pose sweep, reusepartstep_sweep). Emitters only where all oracles are blind (gated). -
4. Fix the two live errors — DONE 2026-06-22: empty-workspace guard in
nornir_tab.rsdraw_populate_status+draw_jobs(CloneEvents/Jobs skip the RPC + show "no workspace selected" whenactive_ws.is_empty()); the atom-walk's RED proved them, GREEN proves the fix. ORIGINAL: (CloneEvents/Jobs empty-workspace guard) — the atom-walk RED proves them, then green proves the fix. -
5. DOCS (LATER, when working) — full design + guide + idiot-guide across
.nornir/(this file + autonom + robotui + test-matrix), then sync workspace_nordisk.
3. The FLOW
mark in source → knowledge scan (syn) → warehouse (symbol_facts/call_edges)
→ arch::classify → architecture_wiring table
→ autonom reads the warehouse → test matrix (surface_coverage)
→ 🏛 Architecture tab visualizes (green=tested, red=untested)
4. How a programmer marks a function (READ THIS BEFORE MARKING)
A function's layer is derived, not hand-annotated — you make it discoverable and arch::classify does the rest:
- UI-component —
impl Facet for T(a viz pane/tab) → classifiedComponent. - gRPC — a tonic service RPC handler (the
async fnin the#[tonic::async_trait]service impl insrc/bin/nornir-server.rs) → classifiedGrpc. - core-fn / table — plain fns and warehouse
query_*/append_*accessors →CoreFn/Table(viaaccess_scanfn→tablefacts). Mark = be discoverable in one of those shapes; the knowledge scan persists it,classifytags it, autonom finds it. No attribute soup, no doc list.
5. Data-paths: leaf repo → warehouse (the emitter, both modes)
The emitter crate nornir-testmatrix is itself a fat/thin client — small, dev-dependency only (test-only, never shipped), so it may pull what it needs.
FAT (nornir test, no --server):
leaf #[test] → functional_status() → nornir-testmatrix[warehouse feat] → skade → local warehouse
→ nornir test / 🏛 Architecture tab read back
THIN (nornir test --server <url>):
leaf #[test] → functional_status() → nornir-testmatrix (thin client) → Submit RPC (gRPC)
→ nornir-server → skade → warehouse → matrix / 🏛 Architecture tab read back
- One writer either way = skade. skade (2M lookups/s, 40M rows/s) makes serial ingest free — the single-writer lock is a non-issue. No JSON anywhere (it was only ever a lock-free drop box; skade removes the reason).
- Why skade, not nornir: routing through nornir is a dependency cycle (nornir → nornir-testmatrix → nornir). skade is published, lean, below the leaves → no cycle.
warehousefeature is OFF by default (published crate stays pure-serde); ON pulls skade for the direct write.- Thin wrinkle (pick one): extract the Submit proto+client into a tiny
nornir-protocrate both depend on, OR give testmatrix its own slim Submit client. Fat (skade-direct) has no wrinkle.
6. fat == thin parity (AUT4)
Same seeded warehouse → identical state_json for fat and thin, asserted as a gate. Law: thin and fat commands are identical in ~99% of cases; any divergence is rare, justified, and explicitly marked GAP(fat) / GAP(thin) in mode-matrix.md. The audit decides the direction per verb — make both shine, don't pick a winner. (Known seed leads: index upload & mimir changed are thin-only; fat workspace populate may be a docs gap, not a code bug — verify first; workspace_nordisk is missing its nornir-workspace.fat.toml.)
7. skade-style flow SVGs (docs — a FEW, not many)
Document §3 + §5 as regenerated skade circuit-board SVGs (arch::to_svg), embedded in nornir docs — NOT mermaid, NOT a hand list of markers.
8. Build order (main is king — NO BRANCHES LEFT)
- Commit this design (anchor).
- Emitter: optional
warehousefeature → skade-direct (fat); thin via Submit RPC; kill JSON. - nornir self-adoption (the uncommitted
selftest.rs+self_emitters_*redone on this design). - autonom: enumerate the warehouse-marked UI/gRPC fns; add
SurfaceKind::Grpc+ alayerfield on rows. - 🏛 Architecture tab ↔
surface_coveragecross-check (green/red per marker). - fat↔thin parity gate (AUT4) + the divergence audit + add
nornir-workspace.fat.toml. - Consolidate:
main← the 0.4.42 line + this work; cherry-pick the ~3–4 genuinely-new orphan commits; delete every branch (merged / superseded / spike) + clean worktrees; preservenornir-orch's uncommitted edits onto a salvage branch first. (Blind-merging the stale branches would REGRESS — they carry 0.4.36-era airgap, a Graphviz revert of NUKE-MERMAID, and the old JSON-sink emitter.) - Docs: mode-matrix truth + GAP markers; the FLOW; §4 marking instructions; the skade-style SVGs.
- Regenerate
docs/book.{md,pdf}at the consolidated version. - Push
main.