Engine A / Engine B — the reasoning (why two, and how they relate)
Rationale + the A-vs-B comparison. The chosen design (the trait + both engines behind it) lives in the code (
src/engine.rs) and is summarised in.nornir/README-full.md; the head-to-head numbers are in.nornir/engine-benchmarks.md. This file is the why.
Two different "Engine A / Engine B" axes — don't conflate them
modgunn already uses "Engine A / Engine B" at the subsystem level:
- Engine A =
scan— the artifact security verdict (CVE / license / provenance →core::Verdict). Lean leaf. - Engine B =
scip— the whole-world behaviour-delta (heavy, 128 GB–1 TB RAM).
That split is about what is computed and stays as-is. This work adds a second, finer A/B axis, entirely inside Engine-A-the-scan: two interchangeable implementations of the same verdict contract that differ only in how they match advisories. Both names are legitimate; this doc + the code are careful to say "matching strategy A/B" so the two axes never blur.
The seam we split
The verdict pipeline is: find matching advisories → fold into a Verdict. The fold (Policy::assemble: vuln + license + provenance → decision table) is fixed and shared. The matching — "which advisories affect this artifact?" — is the hot path at whole-OSS-world scale (components × advisories), and it is the only thing that varies between the engines. So the trait engine::AdvisoryEngine abstracts exactly that one method:
fn matching(&self, artifact: &ArtifactRef) -> Vec<&Advisory>;
Everything else (verdict, fleet) is a provided method on top of it, so both engines are guaranteed to share verdict semantics — they cannot drift.
Engine A — linear scan (LinearEngine)
The strategy modgunn shipped: AdvisoryDb::query walks every advisory for every artifact, keeping those whose Advisory::matches returns true.
- Pro: zero build cost, minimal memory, trivially correct, cache-friendly for a small db; nothing to invalidate when the db moves.
- Con: fleet cost is
Θ(artifacts × advisories)— quadratic-ish as both the SBOM and the advisory corpus grow. At a real CVE-warehouse db (10⁵–10⁶ advisories) it collapses.
Engine B — inverted index (IndexedEngine)
The alternative: one up-front pass buckets advisory indices by (ecosystem, package) (lower-cased). A query grabs at most one bucket and version-tests only those advisories.
- Pro: fleet cost is
Θ(artifacts × hits_per_package)after anΘ(advisories)build — the win grows with db size (measured 195× at 25 k advisories × 1 M artifacts, 12 cores; see benchmarks). - Con: the index costs memory (
Θ(advisories)inusizehandles) and a build step; for a tiny db or a one-shot single-artifact scan that overhead isn't repaid.
Why they are provably equal (not "usually")
Advisory::matches returns false unless advisory.ecosystem == artifact.ecosystem && advisory.package == artifact.name. So an advisory can only match an artifact in its own (ecosystem, package) bucket — bucketing on that key is lossless. B tests exactly the advisories A's package guard would have admitted, in the same original order (bucket indices are pushed ascending), so the matched set and its order are identical → byte-identical Verdicts. The key is lower-cased on both sides so the bucket lookup can never miss a case-variant match that A's own compare would make; matches still applies its own rule, so B never adds a match A wouldn't. This is pinned by unit tests (src/engine.rs), a 20 000-artifact randomized integration test (tests/engine_ab_equivalence.rs), and the benchmark's own assert_eq! on total findings.
Where they legitimately differ
Only in cost (build time, memory, throughput) — never in results. That is the whole point of the split and exactly what the benchmark measures.
Selection (the runtime swap)
- Library:
engine::build(EngineKind, &AdvisoryDb) -> Box<dyn AdvisoryEngine>. - CLI:
modgunn warehouse scan --engine a|b(aliaseslinear/indexed). - Config/env:
MODGUNN_ENGINE=a|bis the fallback when no flag is given.
Default is A (EngineKind::default()), so every existing caller keeps modgunn's shipped linear behaviour until it opts into B — a pure, back-compatible addition.
Recommendation
Default A for the lean per-artifact gate (holger's promotion path: one artifact, tiny db — the index would never be repaid). Switch to B for any fleet / warehouse scan over the real advisory corpus (warehouse scan --engine b), where the index is built once and amortised over the whole SBOM. The warehouse-sql pushdown remains a third, orthogonal strategy (narrow candidates in the warehouse before Rust ever runs) for the S3/S4 scale where even loading the db into RAM stops being affordable.