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 indesign.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 inmodgunn-master-plan.md,engine-b-analyses.mdandcve-warehouse-ingest.md(alsoreasoning-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 isdefault = ["scan"]. - Rejected: two separate crates. Splitting
modgunn-gateandmodgunn-deltawould duplicate the sharedcore::Verdict/Findingvocabulary and theAdvisorytypes 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 gateswarehouse,warehouse-sql,rustsecandtestmatrixbehind features — each pulls a dependency (skade/DataFusion/toml/nornir-testmatrix) the lean gate must not carry. - Why one verdict type for both.
core::Verdictis 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 §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 Components 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 §6.
Where the deep "why" lives
- The 25-analysis Engine B catalog (which analyses, the math, which scale each needs) —
engine-b-analyses.mdandmodgunn-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 atsrc/scip.rs:2796), and single-writer coordination shipped asWarehouse::open_read_only(src/warehouse.rs:182, tested atsrc/warehouse.rs:1681). Detail: master-plan §8.