<!-- nornir:class: design -->
# framework — design (the shared suite shell modgunn CONSUMES)
> **Class:** `design` · **Function axis:** `framework` (f: `about` · `tools` · `service`).
> Strict: this states *only the shape we run*. The *why* (and rejected alternatives) live in
> [`reasoning.md`](reasoning.md); the how-to in [`framework-guide.md`](framework-guide.md).

## f0 — one shell, three surfaces, zero twins
Every nordisk suite app (korp · nornir · holger · dwarves · **modgunn**) carries the same
JetBrains-style shell: an **About**, a 🧰 **Tools** menu whose entries start containers / fill
demo data through the job manager **jera**, and a shared **lifecycle**. modgunn is a **CLI**, so
its surfaces are the verbs `modgunn about | tools | demo | service`. The governing rule is
**L5 — CONSUME the shared crate, never twin it**: the demo is a `jera::container::ContainerSpec`
run as a tracked job; the service is a `skidbladnir::service::ServiceSpec` driven through the
shared `Lifecycle`. modgunn writes **no** bespoke docker call, **no** `thread::spawn`, **no**
hand-rolled systemd unit.
**Feature gating (the lean-gate law).** modgunn's lib is linked by holger + static-holger, which
must stay dep-light. So jera + skidbladnir are **optional**, behind `framework`
(`framework = ["dep:jera", "dep:skidbladnir", "dep:anyhow"]`), and the live OCI engine is a
further opt-in `framework-oci = ["framework", "jera/container-oci"]`. `cargo tree
--no-default-features --features scan` pulls **0** jera/skidbladnir/draupnir/bollard. Only the
**About** identity is always compiled (serde_json is a base dep), so even the lean build answers
`modgunn about`.
## about — `about.rs` (always compiled)
[`about::identity`](../src/about.rs) builds the SHARED [`nornir_appshell::Identity`] (an edda
leaf, serde-only): `name` · `display_name` (Móðguðr) · `version` · `subtitle` (the gatekeeper
role) · `tagline` · `repository` · `engines` (both A + B) · `framework` (the shared components
modgunn is built on). [`about::state_json`](../src/about.rs) is a passthrough to
`Identity::state_json` — the oracle `modgunn about --json` prints and a robot asserts (`wordmark`
+ `display_name` both resolve to Móðguðr). **RECONCILED (L5):** modgunn no longer twins its own
`struct Identity`; the shared `facett-about` egui card renders this model.
**Version + git-hash (build-version-githash-law).** `version` is
[`modgunn::version_with_hash()`](../src/lib.rs) = `"<CARGO_PKG_VERSION> (<git-hash>)"` — the SAME
string `modgunn --version` / `-V` prints (handled at the top of `main`, before any command). The
7-char git short-hash is stamped by [`build.rs`](../build.rs) reading the on-disk git plumbing
(pure-FS, zero-shell), so the About card, the `--json` oracle, and `--version` can never disagree
about which build is running. Matches the suite shape (`korp v0.1.0 (7f96892)`).
## tools — `tools.rs` (feature `framework`)
[`tools::entries`](../src/tools.rs) is the 🧰 menu (one source of truth the CLI renders + the
test drives). Its single [`ToolEffect::DemoAdvisoryFeed`] is a *generate → import → scan* chain:
- [`tools::demo_feed_spec(out_dir)`](../src/tools.rs) builds a `jera::container::ContainerSpec`
— a **pinned** image (`docker.io/library/alpine:3.20.3`, never `:latest`), the stable name
`modgunn-demo-feed`, an in-container write script, and a read-write bind of `out_dir` at
`/out`. It maps onto a draupnir spec that passes draupnir's own `validate()` (the tested "the
wiring is real" gate).
- [`tools::run_demo_feed`](../src/tools.rs) (feature `framework-oci`) runs it **as a tracked
job** via `jera::container::run_container(sink(), &spec, name, "modgunn")` — routed THROUGH
draupnir's ONE OCI engine. The container writes a demo OSV advisory into `out_dir`, which
`modgunn warehouse import --osv <out_dir>` then folds and `modgunn warehouse scan` VIEWs.
- [`tools::sink`](../src/tools.rs) opens modgunn's `jera::jobs` ledger (`MODGUNN_JOBS` override,
else `<temp>/modgunn_jobs.redb`), falling back to `JobSink::noop` so a demo never breaks.
## service — `service.rs` (feature `framework`)
[`service::modgunn_service_spec`](../src/service.rs) declares modgunn's world-delta gatekeeper as
a `skidbladnir::service::ServiceSpec`: system scope, `User=modgunn` (its **OWN** dedicated user,
provisioned by `skidbladnir::systemd::ensure_system_user`), `Environment=MODGUNN_WORLDDELTA_ADDR`,
`Restart=on-failure`, a declared `Socket::tcp` + `HealthProbe::Tcp`, `AgentMode::Fat` (the heavy
Engine-B site holds the world-snapshot state). [`modgunn_service_unit`](../src/service.rs) renders
the systemd unit through the ONE shared renderer (`render_systemd_unit`); a real
`skidbladnir::service::Lifecycle` yields the `modgunn.service` unit name and a live TCP health
probe. The world-delta **watcher daemon** (`ExecStart = modgunn world-delta watch`) is the
declared contract; standing it up is deferred.
## build atoms — the ArtifactKind axis (`.nornir/build-variants.toml`)
modgunn declares its buildable outputs once as **data** — [`build-variants.toml`](build-variants.toml),
discovered/parsed/validated through nornir-build-thing's [`BuildVariantSet`] (the surface `cargo
xtask build --suite ORIGINAL` resolves against). The space spans THREE orthogonal axes: the
**ArtifactKind** (`kind`) — *what* is produced; the fat/thin **linkage** (`shape`); and the cargo
`(package × bin × features × profile × target)` point (the atom is host-native, no `--target`).
**ONE atom**, and it is a row of `workspace_nordisk/.nornir/build-matrix-2026-08-02.md`:
| suite | atom | role | kind | linkage | bin | features | siblings |
|---|---|---|---|---|---|---|---|
| `ORIGINAL` | `modgunn` ⭐ | `Client` | `Exe` | `FatService` | `modgunn` | `scan,scip,warehouse,rustsec,provenance-verify` (+ `--no-default-features`) | skade compiled IN via `warehouse = ["dep:skade", …]` |
⚠ **This table used to say `modgunn-fat` / `Service` / `Server`, and `modgunn-thin` / `Service` /
`ThinStandalone` "reaches `skade` (gRPC)". All three of those were false** and were corrected
2026-08-01: modgunn's binary is a batch CLI with no listener, no loop and no `serve` (the service
module is behind a `framework` feature no atom builds, and the unit it renders `ExecStart`s a verb
that does not exist), and modgunn has no `tonic`/`prost`/`nornir-service` anywhere, so there is no
wire for anything to reach over. The measurements are in
[`suite-atoms-design.md`](suite-atoms-design.md).
⚠ **And on 2026-08-03 the table lost two more rows plus a container.** `modgunn-lib`
(`role = library`) and `modgunn-image` (`role = client`, `kind = container`, **no bin**), together
with the `[[build.container]] modgunn-world-delta` descriptor, were REMOVED: *"ta bort allt inte
stämmer med matrisen"*. `library` is not one of the five shapes, and a client atom that emits no
binary builds nothing runnable. Nothing was deleted from `src/` — holger's `server/lib`, holger's
`server/ui` and nornir all consume modgunn's library as an ordinary cargo dependency with their own
feature selection, never through that atom (whose closure `scan,provenance-verify` matched none of
theirs).
`embeds` is empty on the atom: `validate_kind` allows compiled-in siblings only on the
service-shaped kinds, and the fact it carried is already stated twice over — `warehouse` is DEFINED
as `["dep:skade", …]` in Cargo.toml, and `linkage = "fat"` is the self-containment claim itself.
Guardrails validated up front: a variant enabling a feature outside `[build].offers`, or a `modgunn`
bin missing its `[build.required]` (`warehouse` + `scan`), is rejected. The atom carries
`suites = ["ORIGINAL"]` — the tag is what makes it a Build Thing — so `cargo xtask build --suite
ORIGINAL` is the same command here as in korp, dwarves and nornir, and `ORIGINAL` is the only suite
name that exists in this repo.
**THE ASYMMETRY.** modgunn is the cycle-breaker holger + nornir both depend on, so they are
*upstream*: `[build.downstream]` allowlists only `skade`. A fat variant that tried `embeds =
["nornir"]` would invert the suite dep graph and is rejected by `BuildVariantSet::validate` ("may
not embed 'nornir' … DOWNSTREAM"). Deep rationale in the header of
[`build-variants.toml`](build-variants.toml).
## What is CONSUMED vs what is still a twin (L5 ledger)
| surface | shared crate CONSUMED | still copied-not-shared (move-to-edda candidate) |
|---|---|---|
| Tools demo | `jera::container` + `jera::jobs` | the `ToolEffect`/`ToolEntry` **menu model** + the thin `sink()`/`ledger_path()` adapter — korp carries an almost identical `tools.rs` + `jobs.rs`. Target: a `jera::jobs::workspace_sink(workspace)` helper + a generic `ToolMenu` in an edda leaf. |
| Service | `skidbladnir::service::{ServiceSpec,Lifecycle}` + `skidbladnir::systemd` | nothing — fully consumed (mirrors korp's `korp_server_spec`). |
| About | `nornir_appshell::Identity` (the shared model) + the `facett-about` egui card renders it | nothing — RECONCILED (L5): the bespoke `struct Identity` is gone; `about::identity()` builds the shared `nornir_appshell::Identity` so korp / holger / modgunn feed ONE About widget. |
## Tests (L4)
- `about.rs` — identity carries name + version + both engines (RED-when-broken); `state_json`
mirror. Testmatrix cell `modgunn/about`.
- `tools.rs` — the menu is the jera demo action; `demo_feed_spec` maps + validates onto draupnir
**and** the image is pinned (RED-when-broken, verified by drifting to `:latest`); `state_json`
roster. Testmatrix cell `modgunn/tools`.
- `service.rs` — the system unit renders modgunn's **dedicated user** + ExecStart (RED-when-broken,
verified by blanking the user to `nobody`); `--user` scope omits `User=`; a real `Lifecycle`
yields the unit name + a closed/open-port TCP health probe. Testmatrix cell `modgunn/service`.
- `tests/cli_framework.rs` — drives the compiled binary: `about --json` (identity), `tools --json`
(jera roster), `demo advisory-feed` (the jera ContainerSpec plan + recorded pick), `service
--unit` (the skidbladnir-rendered unit with `User=modgunn`).
- `tests/build_variants.rs` — `.nornir/build-variants.toml` discovers + `validate_all`s (RED-when-
broken, every guard seen red — see [`suite-atoms-design.md`](suite-atoms-design.md)): **matrix
conformance** (every declared atom's `(role, linkage, bin)` equals its row of
build-matrix-2026-08-02, and no container is declared at all), the ⭐ atom's exact `cargo_args`,
the asymmetry guard rejecting an embedded upstream member, `check_bins()` /
`check_bins_declared(cargo_bin_targets())` / `check_suites()` + `declares_suites()`,
`suite_cli::resolve` selecting `--suite ORIGINAL` while refusing `NOPE`, holger's `MVP` and every
phantom atom name, and the four MISSING shapes asserted as positive claims (no server role, no
solo role, no thin linkage, no UI dependency, no gRPC dependency). Testmatrix cell
`modgunn/build-variants`.