Gunnar

mikronova/modgunn

.nornir/build-thing-design.md

# 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.rs` behind a
  feature no row enables, and a declared `ExecStart` naming a verb `main.rs` does
  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]]` requires `warehouse`, 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)`

```text
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]`

```text
── 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"`

```text
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 `]`

```text
⚠ 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 `]`

```text
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"]`

```text
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"]`

```text
══ ⭐ 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"`

```text
The only modgunn BINARY, and the normal case for the client role.
```