# Móðguðr — design (f0, the whole)
> **Class:** `design` · **Function axis:** `f0` (the system) then one `##` per function —
> `verdict` · `feeds` · `behavior` · `warehouse` · `worlddelta` · `provenance` · `sbom` · `manifests` · `rollup`.
> Strict: this file states *only the shape we run*. Every *why*, every rejected alternative,
> lives in [`reasoning.md`](reasoning.md); each function has its own deep `-design.md`.

## f0 — the system
Móðguðr is a **leaf Rust crate** that hands any artifact a security **verdict** and, on the
heavy plane, diffs the *behaviour* of a whole dependency world between two snapshots. One
type unifies both halves: every analysis emits a [`core::Finding`](../src/core.rs) folded
into a [`core::Verdict`](../src/core.rs) — `Pass < Warn < Block` (`core.rs:15`), content-keyed
by `(blob_sha256, policy_version, advisory_db_rev)` (`Verdict::cache_key`, `core.rs:70`).
The crate is organised as nine functions wired through that shared verdict shape:
| function | module(s) | role | feature |
|---|---|---|---|
| **verdict** (Engine A) | `scan.rs`, `core.rs` | artifact CVE/license/provenance → `Verdict` | `scan` (default) |
| **feeds** | `osv.rs`, `nvd.rs`, `feeds.rs`, `vex.rs`, `versions.rs` | offline advisory feeds → `Advisory` (range-native) | `scan` |
| **behavior** | `behavior.rs` | npm `package.json` / PyPI `setup.py` / `pyproject.toml` / `setup.cfg` (both section-aware) install-code → A23 feature vector → `Finding` | `scan` |
| **warehouse** | `warehouse.rs` | skade/Iceberg seam, CDC, fleet scan | `warehouse` |
| **worlddelta** (Engine B) | `scip.rs` | `WorldSnapshot` diff → `BehaviorDelta` + tripwires | `scip` |
| **provenance** | `provenance.rs` | attestation / SLSA / Scorecard checks | `scan` |
| **sbom** | `sbom.rs` | CycloneDX 1.5 emit (multi-ecosystem purls) | `scan` |
| **manifests** | `manifests.rs` | manifest/lockfile ingest → `SbomComponent` (7 ecosystems) | `manifests` |
| **rollup** | `rollup.rs` | pure aggregate/fleet folds over findings | always |
| **framework** | `about.rs`, `tools.rs`, `service.rs` | the shared suite shell: About identity · 🧰 Tools menu (jera demo) · service lifecycle (skidbladnir) | `about` always; `tools`/`service` need `framework` |
**The reuse graph (`uses`).** `verdict.uses = [feeds, provenance]` — the scanner matches
against `feeds`' `Advisory`s and folds `provenance`'s findings. `warehouse.uses = [feeds,
worlddelta]` — it round-trips `Advisory`s and captures `WorldSnapshot`s. `worlddelta.uses =
[core]` only. `rollup.uses = [warehouse]` (maps its `FleetFinding`s in) but degrades to pure
input. `feeds`, `behavior`, `provenance`, `sbom`, `manifests` are leaf-of-leaf: pure parse/emit,
no warehouse, no network (`manifests.uses = [sbom]` — it produces the SAME `SbomComponent` `sbom`
emits; `behavior.uses = [core]` — it reuses `core::shannon_entropy`, the SAME obfuscation metric
the SCIP A17 tripwire keys on, no twin). Every function speaks `core::{Finding, Verdict, Severity,
Decision}` and nothing speaks nornir.
**Two engines, one shape (the chosen split).** Engine A (`verdict`) is the lean, always-on
gate — holger's promotion path and static-holger quarantine link it with `default = ["scan"]`
and pull no arrow/iceberg tree. Engine B (`worlddelta`) is the heavy world-delta site
(128 GB–1 TB RAM), compiled only under `--features scip`. Both fold into the *same*
`core::Verdict`, so a consumer reads one type regardless of which engine produced it —
[`scip::verdict_from`](../src/scip.rs) builds a `Verdict` from a `BehaviorDelta` exactly as
[`scan::OsvScanner::scan`](../src/scan.rs) builds one from an `Artifact`.
**Leaf by construction.** `holger → modgunn` and `nornir → modgunn`; `modgunn → skade` (the
warehouse feature) and never `modgunn → nornir`. That one forbidden edge is the cycle the
crate exists to break; the *why* is in [`reasoning.md`](reasoning.md).
The per-function `##` sections below are the map; the territory is each function's own
`{fn}-design.md`.
## verdict (Engine A) — `scan.rs`
[`OsvScanner::scan`](../src/scan.rs) folds three checks into one `Verdict`: known-vuln match
(`AdvisoryDb::query`, `scan.rs:213`), `license_finding`, and `provenance_finding`, reduced by
[`decide`](../src/scan.rs) (`scan.rs:366`, strictest `Decision` wins). Exploit-intel gating
lives in `vuln_finding`: KEV → escalate to `Critical`, EPSS ≥ `epss_block_threshold` → `High`,
VEX `not_affected` → suppress. Policy is [`Policy`](../src/scan.rs) (`scan.rs:320`).
Deep: [`verdict-design.md`](verdict-design.md).
## feeds — `osv.rs` · `nvd.rs` · `feeds.rs` · `vex.rs` · `versions.rs`
Offline feed parsers produce [`scan::Advisory`](../src/scan.rs) carrying **ranges**, not
pre-expanded versions: `osv::parse_doc`, `nvd::parse_nvd`/`enrich_with_nvd`,
`feeds::{parse_epss_csv, parse_kev_json, parse_malicious, enrich}`, `vex::collect_suppressions`.
[`scan::merge_by_alias`](../src/scan.rs) (`scan.rs:224`) dedups CVE↔GHSA↔RUSTSEC into one row;
match is [`versions::affected_by`](../src/versions.rs) (`versions.rs:55`) at scan time.
Deep: [`feeds-design.md`](feeds-design.md).
## behavior — `behavior.rs`
The A23 malicious-package **feature extractor** — scores a package with *no CVE yet*. **Four
ecosystem front-ends, one folder:** [`behavior::parse_npm_package`](../src/behavior.rs) reads an
npm `package.json`'s auto-run scripts (`preinstall`/`install`/`postinstall`/`prepare`),
[`behavior::parse_pypi_setup`](../src/behavior.rs) reads a PyPI `setup.py` (the whole file runs at
sdist install; the install-hook signal is a `cmdclass` override),
[`behavior::parse_pyproject_toml`](../src/behavior.rs) reads a PEP 621/517 `pyproject.toml`
**section-aware** — folding only the build/install-active sections (`[build-system]` / a PEP 517
in-tree `backend-path` / a `cmdclass` table / a `[tool.*.build.hooks…]` build-hook) and skipping the
declarative `[project]` deps & URLs so an honest `urllib3` dep / `https://` homepage does not
false-positive; a tiny line-scanner with **no `toml` dep**, so it stays in the lean `scan` gate — and
[`behavior::parse_setup_cfg`](../src/behavior.rs) reads a setuptools `setup.cfg` **section-aware INI**
(no `ini` dep, same gate) — folding only the `[options]` `cmdclass` / `dependency_links` keys and a
build-active `[options.entry_points]` group, skipping `[metadata]` (incl. `url`) and the declarative
`install_requires` / `extras_require` / … keys. All
four funnel through the SAME pure `extract_from_install_text` and land in the SAME
[`PackageBehavior`](../src/behavior.rs), no per-ecosystem twin (L5). Features: install-hook set,
`network_in_install` (dropper stage-2 fetch), `url_count`, `max_token_entropy` (reusing
[`core::shannon_entropy`](../src/core.rs) — the SAME metric the SCIP A17 tripwire keys on), and the
sensitive-API set (npm + Python primitives). [`PackageBehavior::findings`](../src/behavior.rs) ranks
the vector into `core::Finding`s (network-in-install → High, sensitive-API/obfuscation → Medium,
install-hook → Low). Reads text only, no code executed. Driveable from the CLI — `modgunn behavior
--path <package.json|setup.py|pyproject.toml|setup.cfg> [--ecosystem npm|pypi|pyproject|setupcfg] [--subject <name>]
[--fail-on-finding] [--warehouse <root>]` folds one package, prints the vector
+ findings, and (with `--fail-on-finding`) exits 3 so a promotion gate can block a suspicious
package. With `--warehouse` it PERSISTS the vector + derived verdict into the `behavior_features`
table (durable + queryable), read back via `warehouse behavior-log`. Deep: `feeds-design.md` f4.
## warehouse — `warehouse.rs`
[`Warehouse`](../src/warehouse.rs) opens the shared skade catalog (`open` / `open_read_only`),
`ensure_modgunn_tables`, and OWNs `cve_advisories` / `world_snapshots` / `dep_edges` /
`scip_symbols` / `behavior_deltas` / `tripwire_events` / `scorecard_signals` / `behavior_features`
(the A23 behaviour-verdict ledger — `append_package_behavior` / `load_package_behaviors`, a
field-identical round-trip) while READing nornir's `sbom_components`. CDC is `commit_seq` + `table_delta` + `scan_sbom_delta` over
skade's `read_delta`; `scan_fleet` joins the whole SBOM × the latest advisories.
Deep: [`warehouse-design.md`](warehouse-design.md).
## worlddelta (Engine B) — `scip.rs`
A [`WorldSnapshot`](../src/scip.rs) (`scip.rs:295`) holds symbols, call edges, dep nodes/edges
and vuln sinks. [`diff`](../src/scip.rs) runs the [`default_tripwires`](../src/scip.rs) battery
— `CallGraphDeltaTripwire`, `CapabilityDriftTripwire`, `SinkNewlyReachableTripwire`,
`FingerprintDriftTripwire`, `ObfuscationTripwire`, `MaintainerAnomalyTripwire`,
`SbomDriftTripwire`, plus the rule-configured `PolicyTripwire` / `ThresholdTripwire` at the heavy
site — into a [`BehaviorDelta`](../src/scip.rs). Standing analyses:
`reachability_gated_vulns`, `capability_surface`, `blast_radius` (PageRank), and the full
25-of-25 catalog (honeytoken, world time-travel diff, …). Deep:
[`worlddelta-design.md`](worlddelta-design.md), catalog: [`engine-b-analyses.md`](engine-b-analyses.md).
## provenance — `provenance.rs`
[`ProvenanceRequirement::evaluate`](../src/provenance.rs) (`provenance.rs:60`) checks an
[`Attestation`](../src/provenance.rs) against min-SLSA / trusted-signer / source-repo,
returning a `scan::Provenance` + findings; [`attestation_delta`](../src/provenance.rs) reports
signer/source change; [`ScorecardSignals::findings`](../src/provenance.rs) turns OSSF Scorecard
signals into findings. Deep: [`provenance-design.md`](provenance-design.md).
## sbom — `sbom.rs`
[`to_cyclonedx`](../src/sbom.rs) (`sbom.rs:47`) serializes a [`SbomInput`](../src/sbom.rs)
(components + vulns) into a CycloneDX 1.5 document; [`purl`](../src/sbom.rs) builds the
multi-ecosystem package URL. The emit direction, complementing the warehouse SBOM read.
Deep: [`sbom-design.md`](sbom-design.md).
## manifests — `manifests.rs`
The **ingest** direction, complementing `sbom`'s emit: [`parse_manifest`](../src/manifests.rs)
dispatches a manifest/lockfile by basename to one [`EcosystemParser`](../src/manifests.rs) per
ecosystem (cargo/pip/npm/maven/go/nuget/gem) and produces the SAME
[`sbom::SbomComponent`](../src/sbom.rs) model — one seam, one parser each, model never forked
(`Cargo.lock` is the template). Pure-Rust, **zero-shell** (`toml`/`roxmltree`/`serde_json`/
hand-parse; never runs `pip`/`npm`/…). Feature `manifests`, off by default so the lean gate
pulls no TOML/XML parser; the existing cargo warehouse-read + `sbom` emit path is untouched.
Deep: [`manifests-design.md`](manifests-design.md), [`manifests-guide.md`](manifests-guide.md).
## rollup — `rollup.rs`
Pure higher-level folds for altitude: [`SeverityHistogram::of`](../src/rollup.rs),
[`repo_health`](../src/rollup.rs), [`fleet_rollup`](../src/rollup.rs), and
[`rollup_fleet_findings`](../src/rollup.rs) (`rollup.rs:191`) which maps `warehouse::FleetFinding`s
in. No warehouse/network of its own. Deep: [`rollup-design.md`](rollup-design.md).
## framework — `about.rs` · `tools.rs` · `service.rs`
modgunn's slice of the **constellation-wide COMMON FRAMEWORK** — the JetBrains-style shared
shell every suite app (korp / nornir / holger / dwarves / modgunn) carries: an **About**, a
🧰 **Tools** menu of demo actions, and a shared **lifecycle**. modgunn is a CLI, so these are
CLI surfaces (`modgunn about | tools | demo | service`). The rule is **CONSUME, never twin**
(L5): the demo runs a **jera** `ContainerSpec`, the service is a **skidbladnir** `ServiceSpec`.
- **About** — [`about::identity`](../src/about.rs) / [`about::state_json`](../src/about.rs):
builds the SHARED [`nornir_appshell::Identity`] (name · Móðguðr · version · role/`subtitle` ·
both engines · the framework it is built on). **Always compiled** (serde-only), so the lean
`scan` build — and holger / static-holger, which link modgunn's lib — answer `modgunn about`
with no heavy dep. *(RECONCILED L5: the bespoke `struct Identity` is gone — the model lives
ONCE in the `nornir-appshell` edda leaf, and the shared **`facett-about`** egui card renders
exactly this shape, so korp / holger / modgunn feed ONE About widget.)*
- **Tools** (feature `framework`) — [`tools::entries`](../src/tools.rs) is the 🧰 menu; its one
entry `DemoAdvisoryFeed` is a **jera-backed** *generate → import → scan* action:
[`tools::demo_feed_spec`](../src/tools.rs) builds a `jera::container::ContainerSpec` (pinned
`alpine`, a `/out` bind) that [`tools::run_demo_feed`](../src/tools.rs) (feature
`framework-oci`) runs **as a tracked job** via `jera::container::run_container` — routed
THROUGH draupnir's ONE OCI engine, **never** a bespoke docker call or `thread::spawn`. The
container writes a demo OSV doc that `warehouse import --osv` then folds.
- **Service** (feature `framework`) — [`service::modgunn_service_spec`](../src/service.rs)
declares modgunn's world-delta gatekeeper as a `skidbladnir::service::ServiceSpec` driven
through the shared `Lifecycle` (the ONE systemd renderer + a **dedicated `modgunn` system
user** via `skidbladnir::systemd::ensure_system_user`, L5). `modgunn service --unit` renders
the unit; the watcher daemon itself (`ExecStart = modgunn world-delta watch`) is deferred.
- **Build atoms** — [`.nornir/build-variants.toml`](build-variants.toml) declares modgunn's
outputs as data (nornir-build-thing / `cargo xtask build --suite ORIGINAL`). The constellation's
build matrix (`workspace_nordisk/.nornir/build-matrix-2026-08-02.md`) gives every member the SAME
five shapes — `<x>-server` ⭐ · `<x>` · `<x>-cli` · `<x>-ui` · `<x>-solo` — and **modgunn declares
exactly ONE of them**: `modgunn` (client · **fat** · bin `modgunn`), the Engine A+B CLI with skade
compiled in via `warehouse`. The other four are FICTION for this member and are declared GAPS with
the measurements behind them: no daemon, no gRPC wire, no UI, and therefore no solo. The suite
**asymmetry** is encoded too: modgunn may embed only downstream `skade`, never the upstream nornir
/ holger it exists to decouple. On 2026-08-03 the three off-matrix declarations (`modgunn-lib`,
`modgunn-image` and the `modgunn-world-delta` container) were REMOVED — declarations only; the
crate, its `[[bin]]` and every consumer edge are untouched. See
[`suite-atoms-design.md`](suite-atoms-design.md).
**The reuse graph.** `framework.uses = [jera, skidbladnir]` (both optional, off the lean gate).
`about.uses = []` (pure data). Deep: [`framework-design.md`](framework-design.md) · guide:
[`framework-guide.md`](framework-guide.md).

## Which recipe reader this repo uses, and why (lane AJ, 2026-08-30)
**Reader:** `nornir-build-thing` **0.1.11** in `[dependencies]` (registry floor) and **0.1.12**
in `xtask`; both resolve to edda's tree locally through `[patch.crates-io]`.
**Why:** modgunn is BOTH — in-constellation locally and a PUBLISHED transitive of holger,
nornir, znippy and knut. Its floor is therefore the newest version that exists on
**crates.io**, because those four resolve modgunn from the registry where the
`[patch]` does not reach; a `0.1.12` floor there would fail every one of them. Its
old floating `"0.1"` is what let znippy sit on a registry 0.1.3. modgunn's recipe
is schema 1.
The rule itself — `[nornir-build-thing] schema = N`, a refusal that names the
version to upgrade to, and a loader that refuses to DROP a key it cannot read —
lives once, in **edda `.nornir/build-design.md`, "THE COMPATIBILITY RULE"**. The
cross-version guard that would have caught the 2026-08-30 skew is
`edda/nornir-build-thing-skew`.