# Móðguðr — reasoning (f0, the whole)
> **Class:** `reasoning`. The *why*, the trade-offs, and **every fork not taken**. The
> descriptive shape we run is in [`design.md`](design.md), which by rule mentions none of
> this. If you want "why two engines?", "why a leaf?", "why not depend on nornir?" — it is here.
> The deep analysis catalogs and the ingest roadmap live in
> [`modgunn-master-plan.md`](modgunn-master-plan.md), [`engine-b-analyses.md`](engine-b-analyses.md)
> and [`cve-warehouse-ingest.md`](cve-warehouse-ingest.md) (also `reasoning`-class).

## Why this overall shape
The goal was to give **holger** a lean promotion gate and **nornir** a rich security plane
from **one** body of security logic, without the two coupling to each other and without a
dependency cycle. Every structural choice below serves that goal: one crate, one verdict
type, two feature-gated engines, one leaf edge to skade.
## Why a leaf crate (and why `modgunn → nornir` is forbidden)
nornir owns the viz + orchestration; holger owns promotion. Both need verdicts. If the
security domain lived in nornir, holger would depend on nornir (heavy) to get a gate; if it
lived in holger, nornir would depend on holger. Extracting it into a **leaf** — depended on
by both, depending on neither — is the only shape with no cycle. `modgunn → skade` is
allowed because skade is itself a leaf (embedded Iceberg); `modgunn → nornir` is the exact
edge that would recreate the cycle, so it is banned. modgunn reaches the shared warehouse
**through skade's tables**, opening the same physical `catalog.redb` in the `nornir`
namespace rather than calling nornir's API.
## Why two engines split by a feature (not one, not two crates)
- **Rejected: one always-on engine.** The world-delta engine pulls SCIP closures, graph
algorithms and (eventually) embeddings — 128 GB–1 TB RAM. holger's gate and static-holger
must stay pure-leaf with no arrow/iceberg/redb tree. A single engine forces that weight on
every consumer. So Engine B is `--features scip`, off by default; Engine A is `default =
["scan"]`.
- **Rejected: two separate crates.** Splitting `modgunn-gate` and `modgunn-delta` would
duplicate the shared `core::Verdict`/`Finding` vocabulary and the `Advisory` types across a
crate boundary, and force consumers to track two versions. One crate with feature walls
keeps the shared shape in one place while still letting a lean consumer compile out the
heavy half. The same reasoning gates `warehouse`, `warehouse-sql`, `rustsec` and
`testmatrix` behind features — each pulls a dependency (skade/DataFusion/toml/nornir-testmatrix)
the lean gate must not carry.
- **Why one verdict type for both.** `core::Verdict` is emitted by Engine A
(`OsvScanner::scan`) and Engine B (`scip::verdict_from`) alike, so holger and nornir read
**one** shape no matter which engine ran. The field's competitors need 5+ tools stitched
together for this surface; a single verdict A→B is the point.
## Why store advisory ranges and match at scan time (not expand at import)
Early `cve_advisories` stored `affected_versions` as **exact** versions and matched by
equality. Importing a range-only OSV/NVD entry then required expanding the range against the
*full published version universe per package* — which explodes row counts, is lossy (a
version published after import silently reads "not affected"), and couples the advisory feed
to a registry-version catalog you do not have offline. So we evolved to **range-native**
matching: persist the OSV range events (`introduced`/`fixed`/`last_affected`) and evaluate
`versions::affected_by(eco, ranges, version)` against the artifact's *actual* version at scan
time. Expansion became lazy or unnecessary. Full rationale + the schema-v2 migration:
[`cve-warehouse-ingest.md`](cve-warehouse-ingest.md) §0.
## Why offline / airgap by construction
The government posture ("know exactly what happens to the smallest bit") rules out live feed
access on the secure side. OSV-Scanner and friends need the network. modgunn's advisory DB is
**warehouse-resident**: the internet side fetches + seals a `.znippy`, it is carried across
the gap, and the secure side runs `modgunn warehouse import` — `db_rev` is the import
generation *and* a component of the verdict cache key, so a scan is reproducible against a
pinned corpus.
## Why the split of domain vs build-orchestration (the nornir cutover)
Only the **security domain** moved into modgunn (`scan`, advisory feeds, `to_cyclonedx`,
verdict). **Build-orchestration stayed in nornir** — resolving a repo's dep closure (cargo
metadata, sibling-repo cloning, `sbom_components` persistence) is nornir's build concern, not
a security primitive. nornir computes `Component`s and hands them to modgunn; modgunn returns
verdicts. Extracting the verdict also deleted nornir's old all-High severity mapper: real
severity/EPSS/KEV now drives the decision, and matching happens once, in modgunn. The
extraction plan and consumer touch-points: [`modgunn-master-plan.md`](modgunn-master-plan.md) §6.
## Where the deep "why" lives
- The **25-analysis Engine B catalog** (which analyses, the math, which scale each needs) —
[`engine-b-analyses.md`](engine-b-analyses.md) and [`modgunn-master-plan.md`](modgunn-master-plan.md) §3.
- The **world survey** (prior art, modgunn's edge vs Trivy/Snyk/GUAC/Socket, the adopt-list) —
master-plan §7.
- The two former "open decisions" are now both resolved in code: the tripwire
rule DSL shipped as `PolicyRule`/`PolicyTripwire` (`src/scip.rs:1858`, tested
at `src/scip.rs:2796`), and single-writer coordination shipped as
`Warehouse::open_read_only` (`src/warehouse.rs:182`, tested at
`src/warehouse.rs:1681`). Detail: master-plan §8.