Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/modgunn

.nornir/suite-atoms-design.md

suite atoms — design

Class: design · Function axis: f0 the four-atom model in modgunn · f1 the three gaps, measured · f2 the role = "server" correction · f3 the extra KINDS kept · f4 the ONE build verb · f5 the guards, seen RED · f6 the required-features rot, measured · f7 the matrix is the law: three declarations removed (2026-08-03). Strict: only the shape we run. Authority: workspace_nordisk/.nornir/SUITE-MODEL.md. The declaration itself is build-variants.toml; the guard is tests/build_variants.rs.

Everything dated in this document was measured on 2026-08-01, on modgunn a3f10a5 (branch feat/four-atoms, worktree /home/rickard/git/modgunn-wt-atoms), against edda nornir-build-thing 0.1.4 at bdd2f78.

⚠ SUPERSEDED IN PART — 2026-08-03

Two things changed after this document was written, and both are settled by workspace_nordisk/.nornir/build-matrix-2026-08-02.md plus Rickard's rule of 2026-08-03.

1. The model is FIVE shapes, not four. The matrix adds the bare <x> as the FAT client — all functionality compiled in, no gRPC, works with no server running — and moves <x>-cli to bin <x>-cli. modgunn's one atom is that fat client, so the atom itself did not change; the row it fills did.

2. modgunn-lib, modgunn-image and the modgunn-world-delta container are GONE. Rickard: "bara de som är markerade suite=ORIGINAL i matrisen skall ha built things" and "ta bort allt inte stämmer med matrisen". Carrying the suites tag is what CLAIMS Build-Thing status, and the matrix has no library row and no container row. Section f7 at the end of this document records what was removed, what it cost and what still depends on the crate.

Everything else below — the measurements, the corrections, the RED proofs — stands as the 2026-08-01 record and is left unedited on purpose.

f0 — the four-atom model, and what modgunn actually has

The constellation aims each member repo at FOUR atoms off ONE delivery (⚠ five as of the 2026-08-02 matrix — see the banner above):

atom role linkage bin
<x>-server ⭐ server fat <x>-server
<x>-cli client thin <x>
<x>-ui client thin <x>-ui
<x>-solo solo fat <x>-solo

solo is UI + server embedded in ONE binary: start it and it puts the UI up with the server running inside itself, nothing else to install. It is its own ROLE and not a flavour of the other two, because the two mis-filings are symmetric and both silent — filed under client it sits in a menu next to a thin UI that is useless without a daemon, and filed under server a deploy step picks it up and tries to run a GUI under a system user with no display.

modgunn fills exactly one of the four. Here is the whole space as landed:

atom role kind linkage bin verdict
modgunn ⭐ client exe fat modgunn the one binary. Was modgunn-fat, role CORRECTED from server
modgunn-lib library lib not-applicable — was modgunn-thin, renamed for honesty. ⚠ REMOVED 2026-08-03 — f7
modgunn-image client container fat — the OCI deploy image. ⚠ REMOVED 2026-08-03 — f7
<x>-server — — — — GAP — modgunn has no daemon (f1)
<x>-ui — — — — GAP — modgunn has no UI at all (f1)
<x>-solo — — — — GAP — no UI and no server ⇒ nothing to embed (f1)

1 of 4 = 25%, and the honest number matters more than the shape of the table. The four atoms are a TARGET, not a quota; declaring an atom that points at a binary which does not exist is the exact rot this pass exists to end. A named gap beats a phantom atom.

f1 — the three gaps, measured

No server

Three independent measurements, each sufficient on its own:

  1. No listener, no loop, no serve. grep -rn 'TcpListener|UnixListener|tokio::spawn|\.serve\(' src/ over the whole crate returns exactly ONE hit: src/service.rs:171, inside #[cfg(test)] mod tests, where a test binds a throwaway port to prove skidbladnir's health probe can see an OPEN socket. No shipping code path in modgunn ever listens on anything. src/main.rs is a hand-rolled slice-pattern dispatch over std::env::args() (src/main.rs:84-127) and every arm returns.

  2. The service module is not in the shipped artifact. src/service.rs is #[cfg(feature = "framework")] (src/lib.rs:56-57), and the modgunn atom does not enable framework. Run the binary this declaration actually builds:

$ ./modgunn service --unit
the Tools menu / demo / service lifecycle need the shared framework crates (jera + skidbladnir):
rebuild with `--features framework` (add `framework-oci` to run the demo container live).
`modgunn about` works without it.
$ echo $?
2

So the tempting defence — "kind = "service" is fine, the artifact really is wrapped as a skidbladnir ServiceSpec by modgunn service --unit" — is false for the artifact this declaration builds. And even with --features framework on, cmd_service (src/main.rs:264-285) only RENDERS a unit file or dumps JSON. It starts nothing.

  1. The declared ExecStart names a verb that does not exist. modgunn_service_spec sets spec.args = ["world-delta", "watch"] (src/service.rs:75). There is no ["world-delta", ..] arm anywhere in main.rs's dispatch — grep -n '"world-delta"' src/main.rs returns nothing. On the built binary:
$ ./modgunn world-delta watch
usage:
  modgunn about [--json]
  …
$ echo $?
2

A systemd unit rendered from that spec would ExecStart a process that prints usage and exits non-zero. src/service.rs:13-20 says as much itself: "it has no long-running serve loop yet … standing up the watcher daemon itself is deferred".

No UI

Zero egui / eframe / wasm-bindgen / tauri / iced / slint anywhere in modgunn's Cargo.toml. The one thing that looks like a UI dep is not one: nornir-appshell (Cargo.toml:105-113) is the constellation-wide About-card MODEL, deliberately serde-only, so that the shared facett-about egui card elsewhere can render modgunn's identity without modgunn linking egui. An About-card model is not a card.

No solo

solo means the UI with the server running inside it. With neither half there is nothing to embed and nothing to put on screen. This gap is not a decision that could have gone the other way; it is entailed by the other two.

The gaps are asserted, not just written down. tests/build_variants.rs:: the_four_missing_atoms_are_declared_missing (renamed from …three… in f7, when the matrix's fifth shape made <x>-cli a fourth named gap) asserts default_for_role(Server) == None, that no variant carries Role::Server or Role::Solo, that no variant is ThinStandalone, and that no UI crate and no gRPC crate appears as a dependency line in Cargo.toml. A gap that nothing checks turns back into a phantom the moment somebody copies a sibling repo's declaration — and every one of those assertions was seen RED (f5, f7).

f2 — the role = "server" correction, and why kind moved too

modgunn-fat declared role = "server", kind = "service", embeds = ["skade"]. The role is disproved three times over by f1. The kind needed to move with it, and that was the one real decision in this pass, because edda's validate_kind forbids embeds on ArtifactKind::Exe — only the service-shaped kinds (Service, Container) may carry compiled-in siblings — and modgunn genuinely does compile skade in.

The two live options were:

BuildVariant::cargo_args() is byte-identical for Exe and Service — only Lib adds an argument (--lib). The correction changed the declaration and not one byte of the build.

One consequence is stated rather than left to be discovered: [build.downstream] is now exercised only by tests/build_variants.rs::embedding_an_upstream_member_is_rejected, which builds a synthetic embeds = ["nornir"] variant, because no real atom declares embeds any more. The asymmetry guard is still live and still red-when-broken; it is simply no longer reachable from the real declaration.

The rename modgunn-thin → modgunn-lib

"thin" is a LINKAGE word, and this variant's linkage is not-applicable. The name asserted a position on an axis the artifact declares it is not on. The kind was already lib; the name now agrees with it. (For the older, larger defect — when modgunn-thin was a service whose rendered argv built literally nothing because the bin's required-features were unmet — see the header of build-variants.toml.)

f3 — what was kept that is outside the four, and why

Both extras are legitimate artifact KINDS, not extra roles:

Its role was corrected too, server → client, for the same reason as the ⭐ atom's and one level up: the image's entrypoint is /usr/local/bin/modgunn and its cmd is ["warehouse", "stats"]. That is a batch job in a container that prints stats and exits. A container is not a daemon just because it is a container.

f4 — the ONE build verb, and what it replaced

modgunn had no xtask crate, no CI config and no Makefile before this pass. Its build recipes lived only in .nornir/*.md prose — and prose that names a cargo invocation IS a second declaration, which is why three of those files had drifted from the TOML.

Added:

Why --manifest-path and not -p xtask. modgunn is a single-crate repo whose root Cargo.toml has a [package] and no [workspace]. Adding members = [".", "xtask"] would make the root a real workspace, and a workspace unifies features across members — the shipped crate's resolution would then depend on what a build tool pulls in. modgunn's whole point is a LEAN closure (holger and static-holger must not acquire an arrow/iceberg tree through it), so xtask/Cargo.toml carries its own empty [workspace] table and is self-rooting, the same shape as korp's crates/* leaves. Verified after the change:

$ cargo metadata --no-deps --format-version 1 | jq -r '.workspace_members[]'
path+file:///home/rickard/git/modgunn-wt-atoms#modgunn@0.0.5

One member, unchanged. xtask is invisible to modgunn's resolution.

$ cargo xtask build --suite ORIGINAL --list
delivery 'ORIGINAL' — 3 atom(s):
  modgunn                  client    exe      modgunn
  modgunn-lib              library   lib      <default bin>
  modgunn-image            client    container <default bin>

Purged / rerouted (every caller of the old recipes, all inside this repo):

file was now
.nornir/framework-guide.md:87-88,101-108 a nornir build --variant modgunn-{fat,thin,image} block; modgunn-thin described as a service/thin that "reaches the warehouse over gRPC"; the fat argv missing provenance-verify and --no-default-features the atom table + cargo xtask build --suite ORIGINAL [--atom …], with the real argv
.nornir/framework-design.md:74-92,125 the same Service/ThinStandalone/gRPC table; the test description naming the old variants the corrected table, an explicit ⚠ note that the old one was false, and the new guard list
.nornir/design.md:188-194 "Three variants — modgunn-fat (Engine A+B service…), modgunn-thin (lean scan-only service, reaches the warehouse over gRPC)…" the three atoms as landed + a pointer to the gaps
Cargo.toml:216 "through nornir build's BuildVariantSet" "through cargo xtask build's"

No Rust anywhere hardcodes modgunn's variant names outside tests/build_variants.rs — confirmed by grepping the repo for modgunn-fat / modgunn-thin / modgunn-image / modgunn-world-delta across every extension. There was no second build path in shipping Rust to delete, because there was no build tooling at all. The feature-axis teaching recipes in guide.md / for-idiots.md / README-full.md (cargo build --features scip, etc.) were left alone deliberately: they teach the raw cargo feature axis, they name no variant, and framework-guide.md already distinguishes the two.

The bin-collision defect: modgunn was clean, and that is not the same as guarded

edda's cross-repo sweep found three siblings emitting one file from several atoms — korp (3 variants → bin korp), nornir (3 → nornir), dwarves (2 → dwarves) — so the last build silently overwrites the others and nothing on disk says which one you have. modgunn appears in none of them, and set.check_bins() is empty here.

But modgunn's emptiness is STRUCTURAL, not earned: exactly one atom declares a bin at all, so there is nothing that could collide, and an assertion that can only pass is not a guard. The test therefore also drives check_bins() against a deliberately-colliding twin declaration and asserts it REPORTS (f5, case 1).

f5 — every guard seen RED

Each guard's INPUT was flipped, the test re-run, and the real failure text captured. All six were restored and the suite is green (7 passed; 0 failed).

1. check_bins() — a second atom on bin modgunn:

["delivery 'ORIGINAL': 2 variants emit the SAME artifact — bin 'modgunn' (package modgunn,
profile release, target <host>) — so the last build silently OVERWRITES the others and nothing on
disk says which one you have: modgunn, modgunn-lean. Give each its own `bin` name off the same
source (holger: holger-mikro / holger-full), or fold them into one variant."]

2. check_bins_declared() — the declared bin renamed to a target that does not exist:

["variant 'modgunn' declares bin 'modgunn-server', which no `[[bin]]` target exists for —
`cargo build --bin modgunn-server` cannot succeed. Declared targets: modgunn"]

The oracle is READ out of the manifests by BuildVariantSet::cargo_bin_targets(repo_root), never hardcoded — a hardcoded twin is exactly the drift this guard removes (korp renamed korp → korp-ui in Cargo.toml and left three variants, and its own test's asserted argv string, pointing at a target that no longer existed). The test additionally asserts that an EMPTY oracle is reported rather than passing vacuously, and that the walk really found modgunn.

3. declares_suites() — every suites = ["ORIGINAL"] tag removed:

check_suites() is empty EITHER because the tags are fine OR because there are none — this is what
tells the two apart

This is the point of asserting both: check_suites() is VACUOUS below two deliveries (with one delivery there is nothing to be a subset of) and correctly returns empty either way.

4. suite_cli::resolve — the delivery renamed to MAIN:

ORIGINAL resolves: --suite ORIGINAL: no such delivery. Declared: MAIN

and in the other direction, --suite NOPE against the real declaration fails with --suite NOPE: no such delivery. Declared: ORIGINAL. A selector that shrugs at a name it does not recognise is the silent-green shape.

5. The server GAP — role = "server" re-declared on the ⭐ atom:

assertion `left == right` failed: modgunn declares a SERVER atom, but it has no daemon: no listener
outside #[cfg(test)], the service module is behind a feature no atom builds, and the unit it
renders ExecStarts a verb that does not exist
  left: Some(BuildVariant { name: "modgunn", … role: Some(Server), … })
 right: None

6. The UI GAP — egui = "0.29" added to [dependencies]:

modgunn declared a UI dependency ('egui') — the `<x>-ui` gap is no longer a gap, so declare the
atom instead of leaving this test lying: egui = "0.29"

The atoms BUILD, by name, and land on disk

Not --all-targets (a required-features mismatch makes cargo skip a target SILENTLY — exactly how holger's gui rotted), and not an exit code. The argv came from set.get("modgunn").command_line(), materialized by the verb:

$ cargo xtask build --suite ORIGINAL
── ORIGINAL · modgunn · cargo build -p modgunn --bin modgunn --features scan,scip,warehouse,rustsec,provenance-verify --no-default-features --profile release
    Finished `release` profile [optimized] target(s) in 1m 10s
── ORIGINAL · modgunn-lib · cargo build -p modgunn --lib --features scan,provenance-verify --no-default-features --profile release
    Finished `release` profile [optimized] target(s) in 28.69s
not cargo builds, skipped (use the image/vm/iso builders): modgunn-image (container)
built: modgunn, modgunn-lib
atom artifact on disk bytes
modgunn /home/rickard/scratch/cargo/modgunn-atoms/release/modgunn 39 475 336
modgunn-lib /home/rickard/scratch/cargo/modgunn-atoms/release/libmodgunn.rlib 6 119 858
$ file modgunn
modgunn: ELF 64-bit LSB pie executable, x86-64 … BuildID[sha1]=aa5a3a91…
$ ./modgunn --version
modgunn v0.0.5 (a3f10a5)

One unrelated RED found on the way, and fixed

Running the suite under the features the CLI tests need (cargo test --features test-support,scip,manifests,framework,rustsec) turned up a test that was red before this pass touched anything:

tests/cli_framework.rs:37: assertion `left == right` failed
  left: String("0.0.5 (a3f10a5)")
 right: "0.0.5"

about::identity() builds its version from version_with_hash() (src/about.rs:50) per the build-version-githash-law, and the in-crate unit test at src/about.rs:84 already asserts it with starts_with + contains. The CLI test had drifted to a bare == against CARGO_PKG_VERSION, so it was red on every checkout with git metadata. Corrected to the same shape as the unit test — prefix plus hash, which is the actual claim. Full suite after: 388 passed, 0 failed.

f6 — the required-features rot, measured (and 2 of 6 were BROKEN)

modgunn offers eleven features and its atoms build five of them. The other six — warehouse-sql, manifests, framework, framework-oci, testmatrix, test-support — are enabled by NO declared build point. That had been recorded as prose since 2026-07-22 and never measured. It was measured today, one feature at a time, with cargo check -p modgunn --features <f>:

feature before after
warehouse-sql ✗ BROKEN ✓
manifests ✓ ✓
framework ✓ ✓
framework-oci ✓ ✓
testmatrix ✓ ✓
test-support ✗ BROKEN ✓

Two of six — 33% — did not compile. Both failed identically:

src/main.rs:822:38: error[E0599]: no method named `reachable_vuln_findings` found for struct
`Warehouse` in the current scope
error: could not compile `modgunn` (bin "modgunn") due to 1 previous error

The root cause is larger than those two features. warehouse-sql ⇒ warehouse and test-support = ["warehouse"], and warehouse alone already satisfies the bin's required-features = ["warehouse", "scan"] (scan rides in default), so cargo BUILT the binary — in a configuration no declared atom ever produces, because the ⭐ atom always turns scip on as well. Warehouse::reachable_vuln_findings is #[cfg(feature = "scip")] (src/warehouse.rs:1635) but cmd_reachable_vuln was not, so warehouse && !scip did not compile. The plainest demonstration is not a never-built feature at all:

$ cargo check -p modgunn --features warehouse     # the recipe Cargo.toml:238 and guide.md:18 document
error[E0599]: no method named `reachable_vuln_findings` found for struct `Warehouse`

Fixed by gating cmd_reachable_vuln on scip and adding the #[cfg(not(feature = "scip"))] fallback that mirrors cmd_import_deps / cmd_ingest — the verb now says what it needs instead of breaking the build. All six re-measured green afterwards.

No atom was invented for any of the six. An atom is a thing somebody SHIPS, and none of these is shipped as an artifact of its own; declaring one to make the table look busier would be the same phantom this pass exists to remove. manifests and framework are the two that carry real shipped CLI verbs (modgunn ingest; modgunn tools|demo|service) and are the first candidates the day modgunn ships a second delivery. The measured status now lives in build-variants.toml with today's date, not as undated prose.

That the ⭐ atom's feature set is the ONLY combination anyone ever builds is itself the finding: a feature nobody builds is a feature that breaks silently, and here it took a four-atom audit rather than a user to notice.

The suite tag

Every atom carries suites = ["ORIGINAL"] and [build] carries base_suite = "ORIGINAL". modgunn ships ONE delivery, so the tag looks redundant. It is not: it makes ONE command shape work in every member repo —

$ cargo xtask build --suite ORIGINAL      # korp · dwarves · nornir · modgunn

ORIGINAL is the ONLY suite name in this repo, and suite_cli::resolve fails loudly on any other (the guard drives it with holger's MVP as well as with a nonsense name, so "some other member's delivery" is not a special case that quietly works here). Without the tag, the one member that ships two deliveries would need a --suite the others reject, and every tool, doc and CI recipe would carry two shapes forever. With it, adding a second delivery to modgunn later is a DECLARATION change and nothing else.

What could NOT be landed

f7 — the matrix is the law: three declarations removed (2026-08-03)

Measured on this checkout, branch feat/matrix-original-atoms off modgunn b501766, against edda nornir-build-thing 0.1.4.

Rickard, 2026-08-03: "bara de som är markerade suite=ORIGINAL i matrisen skall ha built things" and "ta bort allt inte stämmer med matrisen". Carrying the suites tag is what claims Build-Thing status, so an atom that carries it and cannot build is not a documentation nit — it is the archive ⭐ defect (holger's starred default that lives in SUITES.md and in no build file, so nothing can build it). The matrix has five rows, none of them library and none of them a container, so three declarations went.

What was removed, what it built, and what it cost

removed what it rendered who used it what stops working
modgunn-lib (role = library, kind = lib) cargo build -p modgunn --lib --features scan,provenance-verify --no-default-features --profile release nobody — see below cargo xtask build --suite ORIGINAL --atom modgunn-lib now errors no such atom
modgunn-image (role = client, kind = container, no bin) nothing runnable of its own; delegated to the descriptor below nobody the image can no longer be asked for by atom name
[[build.container]] modgunn-world-delta the OCI descriptor: debian:bookworm-slim + /usr/local/bin/modgunn, cmd = ["warehouse","stats"], tag modgunn-world-delta:0.0.5 nobody the image can no longer be materialized FROM THE DECLARATION

Nothing was deleted from src/. A Build Thing is a declared BUILD POINT; removing one removes a way to ASK for an artifact, not the artifact's source. [[bin]] modgunn, the lib target, every module and every feature are untouched.

modgunn-lib in particular — the library is not going anywhere. Fleet grep, 2026-08-03:

holger/server/lib/Cargo.toml:93   modgunn = { …, default-features = false, features = ["scan"] }
holger/server/ui/Cargo.toml:99    modgunn = { …, default-features = false, features = ["scan", "warehouse"] }
nornir/Cargo.toml:673             modgunn = { path = "../modgunn", features = ["warehouse", "rustsec", "scip"] }
modgunn/bench/Cargo.toml:24       modgunn = { path = "..", default-features = false, features = ["scan"] }

Every one of those is an ordinary cargo dependency edge that resolves modgunn's lib with its OWN feature selection. None of them matches the removed atom's closure (scan,provenance-verify), so modgunn-lib was a build point no consumer's build ever took. Removing the atom changes no consumer's bytes.

The container, measured before removal: a fleet-wide grep for modgunn-image / modgunn-world-delta found hits ONLY in modgunn's own .nornir/*.md + tests/ and in three workspace_nordisk/.nornir/*.md inventory tables. No CI recipe, no script, no other repo's declaration. And nothing of modgunn is installed or running on oden (which modgunn, /usr/local/bin, systemctl system + user, ps — all empty), so no running system depended on it.

The container-axis question — STATED, NOT ANSWERED

[[build.container]] cannot carry a suites tag today: BuildVariantSet::suites() and declares_suites() (edda/crates/nornir-build-thing/src/variant.rs:1346,1352) iterate self.variants only, so containers are not on the suite axis in the data model at all. Under the rule "the tag is what makes it a Build Thing", an untagged container is a cell outside the set — which is consistent with removing it, and is why no edda change was made to force the other answer. Whether modgunn should ship a deploy image as a first-class Build Thing is Rickard's call; it would require edda to put containers on the suite axis first.

f7's guards, every one seen RED

tests/build_variants.rs gained every_declared_atom_is_a_matrix_row, which compares the declaration against the matrix's five (atom, role, linkage, bin) rows — written out as the matrix's own names, so the guard checks a declaration against the matrix and not against itself. Seven RED cases, all run on this checkout, all restored green afterwards (8 passed; 0 failed):

  1. the two removed atoms re-added → left: ["modgunn", "modgunn-lib"] right: ["modgunn"], plus atom 'modgunn-lib' (role Some(Library), kind Lib) is not a row of the build matrix and the same for modgunn-image from the in-test synthetic declaration.
  2. the container descriptor re-added alone → containers are not on the suite axis; declared: ["modgunn-world-delta"].
  3. the [[bin]] renamed out from under the declaration (modgunn → modgunn-renamed) → the manifest walk found: ["modgunn-renamed", "xtask"]. The oracle is WALKED from the manifests by BuildVariantSet::cargo_bin_targets, never written down here.
  4. the suites tag removed → check_suites() is empty EITHER because the tags are fine OR because there are none — this is what tells the two apart.
  5. a UI dependency added (egui = { version = "0.29", optional = true }) → modgunn declared a UI dependency ('egui') — the modgunn-ui gap is no longer a gap, so declare the atom instead of leaving this test lying.
  6. a gRPC dependency added (tonic = "0.12") → modgunn declared a gRPC dependency ('tonic') — a THIN client is now possible, so declare modgunn-cli.
  7. the manifest oracle emptied (Cargo.toml reduced to two lines) → the manifest scan found no known dependency line — the oracle is empty: 2 lines. Case 7 is the guard on the guard: without it, cases 5 and 6 would pass vacuously the day the manifest read silently returned nothing.

The four shapes that remain FICTION, and why none was stubbed

modgunn-server, modgunn-cli, modgunn-ui, modgunn-solo. Re-measured 2026-08-03, not recalled: zero tonic/prost/nornir-service in the manifest; zero egui/eframe/tauri/iced/slint/wasm/winit/ wgpu; the only TcpListener in the crate is inside #[cfg(test)]; grep -c '"world-delta"' src/main.rs = 0 against a ServiceSpec whose ExecStart args are exactly that. Making any of them real is PRODUCT work — a daemon, a wire, a window — not a declaration pass, and a tagged stub would be the very defect this section exists to remove.