autonom — the single coverage subsystem (consolidation design)
Decided 2026-07-11. Answers: where does the coverage machinery live? Today it is smeared across nornir/src/autonom/, nornir/src/warehouse/*, three copy-pasted sentinel-ban blocks in nornir/tests/, and the already-extracted engine in edda/crates/nornir-testmatrix. This doc makes autonom (a new edda crate) THE single home for coverage — both TEST coverage and WIRING coverage — and lays out a GREEN-GATED, staged move-map: nothing here breaks a build; risky moves are recorded as staged steps, not shipped.
Companion docs: nornir/.nornir/{autonom.md, aut5-surface-coverage-gate.md, completeness-gate.md, marker-matrix-plan.md, self-emitters.md} (the nornir-side history) and dwarves/.nornir/ui-embedding-and-standalone.md (§4 below).
0. Three coverages — name them, keep them apart
The constellation has three orthogonal things that all say "coverage". autonom is the umbrella, but they must not be conflated:
| # | Sense | Question it answers | Pure engine (today) |
| A | Test-matrix | per-repo × per-aspect pass/fail (+ metric) | nornir-testmatrix (model.rs, aspect.rs, runner.rs) |
| B | Wiring / surface | is every testable surface reached by a test? Gap = Surface − Covered − Allowlist |
nornir-testmatrix (discover.rs + coverage.rs) |
| C | Source-line | what fraction of source lines did the run execute? (llvm-cov) | nornir-coverage |
autonom owns the B gate end-to-end and is the façade for A and C's gate-facing surface. C (nornir-coverage) stays a standalone leaf — it is a different question (line %, not surface reachability) and already documents that split (nornir-coverage/src/lib.rs:5-9). autonom depends on it for the "coverage %" metric a surface may carry, but does not absorb it.
1. Where it lives TODAY (survey, file:line)
B — wiring/surface gate (the heart)
-
Pure model, already extracted →
edda/crates/nornir-testmatrix:src/discover.rs—Surface,SurfaceNode,SurfaceKind,Mode; the enumerators (viz_tabs,cli_commands,mcp_tools,grpc_handlers,facett_components,unreached_functions,ui_atoms,benchers,covered_*);struct Gap(:631),Gap::is_clean(:645),compute_gap(:671).src/coverage.rs—Verdict(:84),CoverageRow(:120),AllowEntry(:195),Allowlist(:208),seed_allowlist(:237),stale_allowlist_entries(:266),GateReport(:289) +compute/is_green,rows_for*,CoverageSummary,coverage_svg.
-
Iceberg write/read seam (nornir):
nornir/src/warehouse/surface_coverage.rs— re-exports the pure model verbatim (:30-34), addsappend_/query_surface_coverage. -
Gate orchestration / feeder (nornir):
nornir/src/autonom/mod.rs—SurfaceInputs,VIZ_TABS/MCP_TOOLS/CLI_COMMANDS/BENCH_HOT_PATHS,knowledge_to_rows,build_surface_and_covered(the glue),gather*,persist,reseed,gate_coverage_for_repo, and the allowlist file I/O (allowlist_path:647,load_allowlist:652,save_allowlist:665).nornir/src/autonom/megagate.rs— cross-workspaceWorkspaceGate/MegaGateReport.nornir/src/autonom/facett_probe.rs—#[cfg(feature="viz")]Elm-harness drive seam.
-
Release blocker (nornir):
nornir/src/release/gate.rs—coverage_gate(:819),coverage_gate_all_workspaces(:867),coverage_gate_outcome(:909). -
Allowlist file (nornir):
nornir/.nornir/autonom-allow.toml— flat[[allow]]tables of{ key = "kind:id@mode", reason }.
Sentinel-ban — DUPLICATED 3×
Identical const SENTINELS: [&str;4] = ["todo","not wired","not yet","coming soon"] in nornir/tests/viz_surface.rs:60 (+ find_sentinel :64, scan_surface_for_sentinels :86), nornir/tests/viz_functional_matrix.rs:759, and inline in nornir/tests/viz_ops_buttons.rs:168. Single-string runtime guard at nornir/src/viz/coverage_live.rs:595. No shared const — genuine copy-paste.
A — test-matrix; C — source-line (for completeness)
- A:
nornir-testmatrix::model→nornir/src/warehouse/test_results.rs(Iceberg seam, re-export). UI:facett/facett-matrix(already a shared Elm/Facet grid component). - C:
edda/crates/nornir-coverage→nornir/src/warehouse/coverage.rs(Iceberg seam). ⚠️ Name hazard:nornir::warehouse::coverage::CoverageRow(line/region) vsnornir::warehouse::surface_coverage::CoverageRow(verdict) — same identifier, different structs, both undernornir::warehouse::*. See §5.
2. Crate boundary: autonom depends on nornir-testmatrix (does NOT absorb it)
Decision: autonom is a façade that DEPENDS on nornir-testmatrix; it does NOT (now) absorb discover.rs/coverage.rs. Recommended, and here is why.
The pure B-engine (discover.rs + coverage.rs) is already a clean extracted leaf and is imported directly by consumers as nornir_testmatrix::discover::* / nornir_testmatrix::coverage::*: nornir's warehouse re-export, facett-matrix, and leaf repos (znippy, skade, …) that run the matrix. Physically moving those two modules into autonom would either (a) break every one of those import paths, or (b) force nornir-testmatrix to re-export from autonom — which inverts the dep (testmatrix is the low-level engine; autonom is the higher-level gate that already needs testmatrix's Surface), creating a cycle. Both violate the green-gate.
So autonom re-exports the B-model as its public façade (autonom::{…}). The re-export is the COMPLETE gate-facing surface — the fact types (SymbolRow, CallEdge, FacetRow, BencherRow, DiscoveredAtom), every Surface enumerator (viz_tabs, cli_commands, mcp_tools, grpc_handlers, facett_components, ui_atoms, benchers, unreached_functions, the covered_* sets, test_reachable), the difference engine (Surface, Gap, compute_gap, the workspace_* helpers, SERVED_WORKSPACES), and the verdict/allowlist/report model + SVG twin (Allowlist, GateReport, CoverageRow, Verdict, coverage_svg, …). autonom's own feeder/report/bench consume the engine through this façade (crate::…), never nornir_testmatrix::… directly — so any gap in the re-export is a compile error, not a silently-bypassed boundary. A downstream consumer needs only autonom:: for the whole gate.
autonom also owns the pieces that had no shared home:
autonom::allowlist— theautonom-allow.tomlfile model (path/load/save). This was nornir-only (it carries thetomldep); lifting it into autonom lets robotui and dwarves read/write an allowlist with no warehouse and no bespoke toml glue.autonom::sentinel— the one true sentinel-ban scan (SENTINELS,find_sentinel,SentinelHit).- (staged) the generalized feeder (
build_surface_and_coveredand the canonicalVIZ_TABS/MCP_TOOLS/… lists) once its warehouse/knowledge coupling is abstracted behind a trait — see §3.
Long-term option (STAGED, not now): discover.rs + coverage.rs migrate into autonom, and nornir-testmatrix re-exports them for back-compat, so testmatrix becomes "matrix + runner" and autonom becomes "surface + gate". That is the wide, cross-repo move; it ships only behind a version bump of both crates with the re-export shim proven green. Until then autonom = façade + the two owned modules.
depends on depends on
autonom ──────────► nornir-testmatrix (+ later, optionally, nornir-coverage for the % metric)
owns: allowlist file I/O, sentinel-ban, gate façade
re-exports: Surface, Gap, Allowlist, GateReport, CoverageRow, Verdict …
nornir ──────────► autonom (thin consumer: warehouse/knowledge feeder + Iceberg seam)
robotui ──────────► autonom (knows where surfaces/tests are; drives + reports)
dwarves ──────────► autonom (later) (same gate, its own surface)
3. MOVE MAP — each module → new home → who repoints
| Piece (today) | New home | Repoints | Risk / status |
allowlist file I/O — nornir/src/autonom/mod.rs:647-680 |
autonom::allowlist |
nornir mod.rs delegates (keep same fn signatures/re-export) |
LOW — DONE (slice 1+2) |
sentinel-ban SENTINELS+find_sentinel — nornir/tests/viz_surface.rs:60,64 (×3 copies) |
autonom::sentinel |
the 3 test files import autonom::sentinel::* |
LOW logic / STAGED — test files are read by the parallel wiring-audit; do not edit them mid-audit |
Allowlist/AllowEntry/GateReport/Verdict/CoverageRow/compute_gap/Surface* |
stay in nornir-testmatrix; re-exported by autonom |
consumers may switch nornir_testmatrix::… → autonom::… opportunistically |
LOW — DONE (façade) |
build_surface_and_covered + SurfaceInputs + canonical VIZ_TABS/MCP_TOOLS/CLI_COMMANDS/BENCH_HOT_PATHS — nornir/src/autonom/mod.rs |
autonom::feeder (behind a KnowledgeSource trait) |
nornir impls the trait over Iceberg; robotui impls it over its own registry | MED — STAGED: coupled to crate::knowledge/crate::warehouse; needs a trait seam first |
megagate.rs (cross-workspace fold) |
autonom::mega |
nornir keeps served_specs (its roots); autonom owns the fold |
MED — STAGED: depends on the feeder move |
facett_probe.rs (Elm drive) |
stays in nornir (or → robotui) | — | HIGH — STAGED: #[cfg(feature="viz")], pulls 6 facett component crates; belongs with the UI host, not the pure gate |
Iceberg seam warehouse/surface_coverage.rs |
stays in nornir | — | intentional: warehouse coupling is nornir's |
discover.rs + coverage.rs bodily into autonom |
autonom (testmatrix re-exports) | all nornir_testmatrix::{discover,coverage} importers |
HIGH — STAGED: wide cross-repo; needs paired version bump + shim |
nornir-coverage (source-line C) |
stays a standalone leaf; autonom depends on it for the % metric only |
— | keep separate — different question |
4. The test-matrix / coverage UI = a shared facett Elm component
Cross-reference dwarves/.nornir/ui-embedding-and-standalone.md §1 ("no duplicated code, even thin" → every panel is one impl Elm/impl Facet crate, both native hosts embed it, cross-host data rides the thin/fat gRPC split).
The coverage/matrix UI must follow the same rule — one Elm crate embedded by BOTH nornir and dwarves, never reimplemented per host:
-
facett-matrix(EXISTS) — the test-matrix grid (Matrix/RepoRow/Cell+ health score + sparkline +MatrixViewFacet), already adopted from nornir'stest_matrix_gridand built onnornir_testmatrix::{status, summarize_runs, RunSummary, TestResultRow}. This is sense A. Keep it; it is the model. -
facett-coverage(NEW — specify only, do NOT build now) — the wiring-gate pane (sense B): renders aGateReport— theSurface × {covered, allowlisted, missing}breakdown, the burn-down ofautonom-allow.toml, stale-entry callouts, and thecoverage_svgmetric twin. Pure model =autonom::{GateReport, CoverageRow, Verdict, Allowlist, Gap}(already serde). Deps:egui+facett-core+autonom+ a thinnornir-client(gRPC) for cross-host. Lives infacett/.- Thin/fat: fat = in-process
GateReport; thin =nornir-serverstreams the rows over gRPC. dwarves embeds the same crate to show ITS surface's gate, talking todwarves-server(thin) — zero gate logic copied into dwarves. - Test win (per the dwarves doc): the shared component earns ONE
nornir-robotuiscenario + ONE testmatrix row that covers it in BOTH hosts — wire-once, verified-everywhere.
- Thin/fat: fat = in-process
-
WASM is a delivery target for these crates, not the cross-embed mechanism (dwarves doc §2): the same
Elmcrate compiles native egui / wasm / headless-robotui.
facett-coverage is specified, not scheduled here. Building it is a separate slice.
5. Follow-ups / hazards to close (not this slice)
CoverageRowname collision (§1 C): consider renaming the line/region one toLineCoverageRow(innornir-coverage/warehouse::coverage) soCoverageRowmeans only the surface-gate row. Cosmetic but a real footgun. STAGED.- Sentinel-ban rewire: once the parallel wiring-audit releases the three
tests/viz_*.rsfiles, repoint them toautonom::sentinel::{SENTINELS, find_sentinel}and delete the copies.
6. Execution log (this session — GREEN-gated)
- Slice 1 (DONE, green): created
edda/crates/autonom(façade +allowlist+sentinel), added to the edda workspace (members = ["crates/*"]).cargo build -p autonom+ fulleddabuild green; 6 unit tests pass. - Slice 2 (see repo state): repoint nornir's
load_allowlist/save_allowlist/allowlist_pathto delegate toautonom::allowlist(signatures unchanged, so every nornir caller and test is untouched). Kept only ifcargo build(+--features testmatrix) stays green; otherwise reverted and recorded as staged here. - Slice 3 (DONE, green): completed the façade. Extended
autonom's re-export to the FULL gate-facing engine surface (the fact types + every enumerator + reachability +coverage_svg+workspace_*/SERVED_WORKSPACES), and repointed autonom's ownfeeder/report/benchoff directnornir_testmatrix::{discover,coverage}::…imports onto thecrate::…façade — so autonom dogfoods its own re-export and the boundary can't be bypassed.autonom::is now the single import home for the gate; a downstream consumer never needsnornir-testmatrixin its own tree for the WIRING gate. Build green (default +--all-features), 19 autonom unit tests + testmatrix suite pass, clippy clean. - Slice 4 (DONE, green): closed the last façade bypass.
allowlist.rsstill re-exportedAllowEntry/Allowlistfromnornir_testmatrix::coverage::…directly (the one owned module slice 3 missed). Repointed it onto thecrate::…façade so every autonom-owned module (allowlist/feeder/report/bench) now dogfoods the re-export — the only remaining directuse nornir_testmatrix::…in the crate is the façade declaration inlib.rsitself (the single intended boundary point). A gap in the re-export is now a compile error from allowlist.rs too, not a silent bypass. Build green (default +--all-features), 19 autonom unit tests pass. - STAGED (not executed): feeder move (needs
KnowledgeSourcetrait), megagate move,facett_proberelocation, bodilydiscover/coveragemigration, sentinel test-file rewire,CoverageRowrename,facett-coveragebuild.