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 inreasoning.md; the how-to inframework-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 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 is a passthrough to Identity::state_json — the oracle modgunn about --json prints and a robot asserts (wordmark
display_nameboth resolve to Móðguðr). RECONCILED (L5): modgunn no longer twins its ownstruct Identity; the sharedfacett-aboutegui card renders this model.
Version + git-hash (build-version-githash-law). version is modgunn::version_with_hash() = "<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 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 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)builds ajera::container::ContainerSpec— a pinned image (docker.io/library/alpine:3.20.3, never:latest), the stable namemodgunn-demo-feed, an in-container write script, and a read-write bind ofout_dirat/out. It maps onto a draupnir spec that passes draupnir's ownvalidate()(the tested "the wiring is real" gate).tools::run_demo_feed(featureframework-oci) runs it as a tracked job viajera::container::run_container(sink(), &spec, name, "modgunn")— routed THROUGH draupnir's ONE OCI engine. The container writes a demo OSV advisory intoout_dir, whichmodgunn warehouse import --osv <out_dir>then folds andmodgunn warehouse scanVIEWs.tools::sinkopens modgunn'sjera::jobsledger (MODGUNN_JOBSoverride, else<temp>/modgunn_jobs.redb), falling back toJobSink::noopso a demo never breaks.
service — service.rs (feature framework)
service::modgunn_service_spec 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 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, 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 ExecStarts 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.
⚠ 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.
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_jsonmirror. Testmatrix cellmodgunn/about.tools.rs— the menu is the jera demo action;demo_feed_specmaps + validates onto draupnir and the image is pinned (RED-when-broken, verified by drifting to:latest);state_jsonroster. Testmatrix cellmodgunn/tools.service.rs— the system unit renders modgunn's dedicated user + ExecStart (RED-when-broken, verified by blanking the user tonobody);--userscope omitsUser=; a realLifecycleyields the unit name + a closed/open-port TCP health probe. Testmatrix cellmodgunn/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 withUser=modgunn).tests/build_variants.rs—.nornir/build-variants.tomldiscovers +validate_alls (RED-when- broken, every guard seen red — seesuite-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 exactcargo_args, the asymmetry guard rejecting an embedded upstream member,check_bins()/check_bins_declared(cargo_bin_targets())/check_suites()+declares_suites(),suite_cli::resolveselecting--suite ORIGINALwhile refusingNOPE, holger'sMVPand 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 cellmodgunn/build-variants.