modgunn's build thing — what the recipe declares, and why
The code is the source (lane AG, 2026-08-30). crates/modgunn-build-thing is modgunn's impl BuildThing; .nornir/build-variants.toml is emitted from it and read back as authority by nothing:
cargo run -p modgunn-build-thing -- emit # regenerate the file
cargo run -p modgunn-build-thing -- toml # the emission, on stdout
cargo run -p modgunn-build-thing -- --list # the roster and its cargo argv
tests/toml_parity.rs fails for any reason the file and the code differ. tests/build_variants.rs — the older, larger guard that asserts every fiction as a positive claim — is untouched and still reads the file, which is now a faithful projection of the code.
What it declares
One atom. ORIGINAL modgunn client fat modgunn — the fat client, the only shape modgunn genuinely has. linkage = fat is verified from cargo and not from the name: warehouse = ["dep:skade", …], so the build compiles the Iceberg engine in and needs no other process running.
The other four shapes of the five-shape matrix are named fictions, each measured on this checkout and each asserted as a positive claim:
modgunn-server— no listener, no serve loop,src/service.rsbehind a feature no row enables, and a declaredExecStartnaming a verbmain.rsdoes not have. Making it real is product work, not a declaration fix;modgunn-cli— impossible by construction: modgunn has no wire, and its one[[bin]]requireswarehouse, the very dependency that makes it fat;modgunn-ui— zero GUI toolkits in the manifest;modgunn-solo— solo is UI plus server, and both halves are missing.
Four fictions named beats four stubs that build and do nothing.
What the emission changed
The six declared-but-never-built features — warehouse-sql, manifests, framework, framework-oci, testmatrix, test-support — were prose in the hand-written file and are [build.allow_unbuilt] data now, one written reason each. That matters because prose is not something a gate can read, and the measurement behind it is not theoretical: compiled one at a time on 2026-08-01, two of the six were actually broken, both with the same error, because warehouse alone already satisfies the bin's required-features while the ⭐ atom always turns scip on as well — so cargo built the bin in a configuration no declared atom ever produces. recipe.rs::the_feature_axis_gate_is_clean runs FeatureAxis::gate() over the declaration, so the next unexcused feature fails there instead of rotting, and a stale excuse fails too.
The embed-allowlist ([build.downstream] modgunn = ["skade"]) is now exercised by a_variant_that_embeds_an_upstream_sibling_is_refused: nornir and holger are UPSTREAM — modgunn exists to break their cycle — so a fat modgunn compiling either in would invert the suite dependency graph.
Appendix — the reasoning that used to sit between the rows
An archive, kept verbatim, in file order. 2026-08-30, lane AG.
Until this day .nornir/build-variants.toml was hand-written, and the reasoning above and between its rows was the most valuable thing in it. The file is emitted now (code → toml, one-directional), and an emission carries data, not argument — a comment typed into it is deleted by the next emit without anybody noticing. So the prose is kept here rather than lost.
New reasoning goes beside the code that acts on it — a doc comment in crates/<repo>-build-thing/src/recipe.rs — or in the section above. Never back into the toml. This appendix is an archive of what was already written, not a place to add to.
after (the file preamble)
modgunn — the suite build matrix, as data (nornir-build-thing / `cargo xtask build`).
modgunn is the SMALLEST verdict core of the nordisk suite: the security +
SCIP world-delta gatekeeper (Gjallarbrú) that holger AND nornir both link to
break their dependency cycle.
═══════════════════════════════════════════════════════════════════════════════
THE MATRIX IS THE LAW — FIVE SHAPES, AND modgunn HAS ONE (2026-08-03)
═══════════════════════════════════════════════════════════════════════════════
`workspace_nordisk/.nornir/build-matrix-2026-08-02.md` settles the shape of every
member's declaration. Five atoms, one suite column:
ORIGINAL <x>-server ⭐ server fat <x>-server
ORIGINAL <x> client fat <x>
ORIGINAL <x>-cli client thin <x>-cli
ORIGINAL <x>-ui client thin <x>-ui
ORIGINAL <x>-solo solo fat <x>-solo
── THE RULE THAT DECIDES WHAT MAY BE IN THIS FILE ────────────────────────────
Rickard, 2026-08-03: *"bara de som är markerade suite=ORIGINAL i matrisen skall
ha built things"* — CARRYING THE SUITE TAG IS WHAT CLAIMS BUILD-THING STATUS.
An atom that claims it and cannot build is not a small documentation defect, it
is the worst kind of declaration: a promise the repo cannot keep. (The pattern
has a name in this constellation — holger's `archive ⭐`, a STARRED DEFAULT that
lives in `SUITES.md` and in no build file, so nothing can build it.)
So this file declares ONE atom, and it is the one modgunn genuinely has:
✔ `modgunn` client · fat · bin `modgunn` — the ⭐ atom, and the whole set.
✗ `modgunn-server` FICTION — no daemon exists. Measured, see below.
✗ `modgunn-cli` FICTION — a THIN client needs a wire, and modgunn has none.
✗ `modgunn-ui` FICTION — zero GUI toolkits in the whole tree.
✗ `modgunn-solo` FICTION by construction — solo = ui + server, and both halves
are missing.
Four fictions named beats four stubs that build and do nothing. Each gap below is
MEASURED on this checkout, not recalled, and each is asserted as a POSITIVE claim
in `tests/build_variants.rs` so it cannot quietly turn back into a phantom the day
somebody copies a sibling repo's file.
── ✗ `modgunn-server` — THERE IS NO DAEMON (three measurements) ──────────────
1. NO LISTENER, NO SERVE LOOP. `grep -rn 'TcpListener|UnixListener|tokio::spawn|
\.serve\(' src/` finds exactly one hit in the whole crate —
`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
slice-pattern dispatch over `std::env::args()`; every verb returns and the
process exits.
2. THE SERVICE MODULE IS NOT IN ANY DECLARED BUILD. `src/service.rs` is
`#[cfg(feature = "framework")]` (src/lib.rs) and NO atom enables `framework`.
So "it wraps as a skidbladnir ServiceSpec" is false for every artifact this
file builds: `modgunn service --unit` on the ⭐ atom prints
"…rebuild with `--features framework`" and exits 2.
3. THE DECLARED ExecStart NAMES A VERB THAT DOES NOT EXIST. Even with the
feature on, `src/service.rs:75` sets `args = ["world-delta", "watch"]` and
`grep -n '"world-delta"' src/main.rs` returns NOTHING. A unit rendered from
that spec would ExecStart into the usage arm and `exit(2)`.
src/service.rs:13-20 says so itself: "it has no long-running `serve` loop
yet … standing up the watcher daemon itself is deferred".
Making `modgunn-server` real is PRODUCT WORK (write the watcher daemon), not a
declaration fix, so it is not declared here.
── ✗ `modgunn-cli` — A THIN modgunn BINARY IS IMPOSSIBLE BY CONSTRUCTION ─────
`<x>-cli` is "talks gRPC to a server, holds no local store". modgunn has NO
wire: `grep -c 'tonic\|prost\|nornir-service' Cargo.toml` = 0. And the only
`[[bin]]` modgunn ships carries `required-features = ["warehouse", "scan"]`
while `warehouse = ["dep:skade", …]` — so the sole executable modgunn can
produce REQUIRES the very dependency that makes it fat. Naming the bin under a
lean feature set does not rescue it, it errors:
error: target `modgunn` in package `modgunn` requires the features: `warehouse`, `scan`
…and dropping the bin gives the silent form instead, which is worse: cargo
SKIPS the target and the "build" succeeds having produced no executable. That
is precisely how the old `modgunn-thin` atom passed for months while building
nothing.
── ✗ `modgunn-ui` — NO UI, MEASURED FROM THE MANIFEST ────────────────────────
Zero egui / eframe / tauri / iced / slint / wasm-bindgen / winit / wgpu in
Cargo.toml. modgunn is a DETECTION AND COLLECTION platform driven by a human or
a script, not a desktop app. Its `nornir-appshell` dep is deliberately the
serde-only About-card MODEL — a shape the shared `facett-about` egui card can
render, not a card of its own. The test reads the manifest as TEXT so a GUI dep
added tomorrow trips it and forces the atom to be declared.
── ✗ `modgunn-solo` — NOTHING TO EMBED, NOTHING TO SHOW ──────────────────────
`solo` is "the UI with its server compiled in". With neither a UI nor a server
there is no window to open and no service to fold into it.
── ⚠ WHAT WAS REMOVED ON 2026-08-03, AND WHAT IT COST ────────────────────────
Rickard: *"ta bort allt inte stämmer med matrisen"*. Three declarations were not
in the matrix and are gone. NO SOURCE CODE WAS DELETED — a Build Thing is a
declared BUILD POINT, and removing one removes a way to ask for an artifact, not
the artifact's source:
• `modgunn-lib` (role `library`, kind `lib`) — `library` is not one of the five
shapes. It rendered `cargo build -p modgunn --lib --features
scan,provenance-verify --no-default-features --profile release`. THE CRATE AND
EVERY CONSUMER EDGE ARE UNTOUCHED: holger `server/lib` takes
`features = ["scan"]`, holger `server/ui` takes `["scan","warehouse"]`, nornir
takes `["warehouse","rustsec","scip"]` — each resolves modgunn's lib as an
ordinary cargo dependency with its OWN feature selection and never went
through this atom. Measured 2026-08-03: the removed atom's closure
(`scan,provenance-verify`) matched NO consumer's, so it was a build point
nobody's build ever took.
• `modgunn-image` (role `client`, kind `container`) — declared a client role
while emitting no `bin`. It delegated to the container descriptor below.
• `[[build.container]] modgunn-world-delta` — the OCI image descriptor
(`debian:bookworm-slim` + the `modgunn` binary at `/usr/local/bin/modgunn`,
tag `modgunn-world-delta:0.0.5`). Fleet-wide grep on 2026-08-03 found NO
consumer outside modgunn's own docs and `workspace_nordisk/.nornir/*.md`: no
CI recipe, no script, no other repo's declaration, and nothing of modgunn is
installed or running on oden. Removing it costs the ability to materialize
that image FROM THE DECLARATION; it costs no running system.
THE CONTAINER-AXIS QUESTION, STATED RATHER THAN ANSWERED: containers are not on
the suite axis in the data model at all — `BuildVariantSet::suites()` and
`declares_suites()` iterate `self.variants` only, so a `[[build.container]]`
CANNOT carry a `suites` tag today. Under Rickard's rule an untagged cell is not
a Build Thing, which is consistent with removing it. Whether modgunn should ship
a deploy image as a first-class Build Thing (which would need edda to put
containers on the suite axis) is HIS call, not this file's, and no edda change
was made to force the answer.
── ⚠ THE ASYMMETRY: modgunn must NOT embed nornir or holger ─────────────────
They DEPEND ON modgunn (modgunn exists to break their cycle), so they are
UPSTREAM of it. A fat modgunn that compiled either IN would invert the suite dep
graph. Only skade (the Iceberg seam modgunn path-deps under `warehouse`) is
downstream. Encoded in [build.downstream]: nornir/holger are absent from
modgunn's allowlist, so a variant that tries `embeds = ["nornir"]` is rejected by
`BuildVariantSet::validate`. No real variant declares `embeds` (kind `exe`
forbids it, and `features = [… "warehouse" …]` already IS the skade embed), so
the rule is exercised in `tests/build_variants.rs` against a synthetic variant —
stated here rather than left to be discovered.
after [build]
── THE DELIVERY (utgåva) ─────────────────────────────────────────────────────
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 that may appear anywhere in this repo. (holger
is the one member that ships a second delivery; that is holger's business and
not a recipe any modgunn tool accepts — `--suite` here has exactly one legal
value and `suite_cli::resolve` fails loudly on any other.)
`check_suites()` is VACUOUS below two suites — with one delivery there is nothing
to be a subset of — so it correctly returns empty here; `declares_suites()` is
what distinguishes "checked and fine" from "nothing to check", and the test
asserts BOTH.
after base_suite = "ORIGINAL"
modgunn's real feature space (Cargo.toml `[features]`): the lean Engine-A
`scan` gate, the heavy Engine-B `scip` world-delta, the `warehouse` Iceberg
seam (over skade), the `rustsec` advisory-db import, plus the manifests /
provenance / framework / testmatrix surfaces. A variant enabling any feature
outside this set is rejected up front.
after ]
⚠ DECLARED-BUT-NEVER-BUILT — RE-MEASURED 2026-08-01 (first recorded 2026-07-22
as prose and left there). `warehouse-sql`, `manifests`, `framework`,
`framework-oci`, `testmatrix` and `test-support` are offered above and enabled
by NO variant below, so no declared build point compiles them. That is the
`required-features` rot pattern: a feature nobody builds is a feature that
breaks silently. Measured one feature at a time with
`cargo check -p modgunn --features <f>` on this checkout:
warehouse-sql ✗ BROKEN → FIXED 2026-08-01
manifests ✓ compiles
framework ✓ compiles
framework-oci ✓ compiles
testmatrix ✓ compiles
test-support ✗ BROKEN → FIXED 2026-08-01
TWO OF SIX WERE ACTUALLY BROKEN — 33%, not a theoretical risk. 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 bigger than those two features. `warehouse-sql ⇒ warehouse`
and `test-support = ["warehouse"]`, and `warehouse` alone already satisfies the
bin's `required-features = ["warehouse", "scan"]` (with `scan` in `default`), so
cargo BUILT the bin — 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.
No atom is invented for them: an atom is a thing somebody SHIPS, and none of
these six is shipped as an artifact of its own. `manifests` and `framework` are
the two carrying real shipped CLI verbs (`modgunn ingest`, `modgunn
tools|demo|service`) and are the first candidates the day modgunn ships a
second delivery. See `.nornir/suite-atoms-design.md`.
after ]
Cargo `[[bin]] required-features` for the `modgunn` binary, mirrored so a bad
feature combo fails fast with a clear message instead of a raw cargo error.
after "modgunn" = ["warehouse", "scan"]
The suite dependency graph as an embed-allowlist: member -> siblings strictly
DOWNSTREAM of it (the only siblings a fat variant may compile IN). modgunn may
embed skade; nornir + holger are deliberately NOT here (they are upstream).
after modgunn = ["skade"]
══ ⭐ THE FAT CLIENT — the one atom modgunn genuinely has ════════════════════
The matrix row this fills: `ORIGINAL modgunn client fat modgunn`.
The one binary modgunn ships: `[[bin]] name = "modgunn"`, `path = "src/main.rs"`,
`required-features = ["warehouse", "scan"]`. A batch CLI that does one job and
exits — see the `modgunn-server` measurements in the header for why this is a
`client` and not a `server`.
`linkage = "fat"` is verified from Cargo, not from the name: `warehouse =
["dep:skade", …]`, so this build genuinely compiles the Iceberg engine in and
needs no other process running — which is exactly what the matrix means by a fat
client ("all functionality compiled in — no gRPC; works with no server running").
It is also the ONLY linkage a modgunn BINARY can have, which is why there is no
`-cli` row.
`no_default_features` + the explicit list is a correction, not a change of
artifact. An older declaration left default features ON, so the build ALSO
carried `provenance-verify` (and its `nornir-build-thing` + `znippy-common/sign`
closure) without saying so — the declared feature set was a strict subset of what
was actually compiled. The list below is exactly `default ∪ declared`, so the
bytes are the same and the declaration is now the whole truth.
after role = "client"
The only modgunn BINARY, and the normal case for the client role.