# verdict (Engine A) — reasoning
> **Class:** `reasoning`. Why Engine A is shaped this way, and the forks not taken. The
> descriptive shape is in [`verdict-design.md`](verdict-design.md), which mentions none of this.
## Why a `max()`-fold over an ordered `Decision`
Both `Decision` (`Pass < Warn < Block`) and `Severity` (`Info…Critical`) derive `Ord` in
declaration order so folding many findings into one verdict is a single `max()` with `Pass` as
the identity for a clean artifact. The alternative — a hand-written precedence table or an
"if any Block then Block, else if any Warn…" ladder — is what `Ord` already expresses for
free, and it would drift the moment a severity is added. Encoding precedence in the enum order
makes "strictest wins" a type-level fact, and it is why Engine B can reuse the exact same fold.
## Why exploit-intel gating lives in `vuln_finding`, not in the feed
KEV/EPSS/VEX change a finding's *severity and presence*, not the underlying match. Applying
them at scan time (against the live `Policy`) — rather than baking an escalated severity into
the stored `Advisory` — means the same imported advisory generation can be scored under
different policies (one consumer blocks on KEV, another only warns) without re-importing.
`Advisory.kev`/`epss` are raw facts from the feed; the *decision* those facts drive is policy,
so it belongs at the gate.
## Why VEX suppression is a policy list, not a deletion
A VEX `not_affected` assertion ("this CVE's vulnerable path is unreachable here") is a
*consumer* claim about *this* artifact, not a fact about the advisory. Dropping the advisory
from the db would hide it from every other artifact; keeping it and suppressing per-scan via
`policy.vex_not_affected` (mirrored by the warehouse-scan `--vex` flag) preserves the advisory
for everyone else and keeps the suppression auditable. Engine B's reachability analysis
(A8) is the *automated* version of the same idea — see [`worlddelta-reasoning.md`](worlddelta-reasoning.md).
## Why the verdict is content-addressed by `(blob_sha256, policy_version, advisory_db_rev)`
A verdict is expensive relative to a cache hit, and it is a pure function of exactly those
three inputs: the bytes scanned, the policy applied, and the advisory generation matched
against. Keying the cache on all three means a consumer (holger) invalidates precisely when
one moves — a new policy version, a new imported `db_rev`, or a different artifact — and never
serves a stale verdict. `policy_version` is a plain string the operator bumps; `advisory_db_rev`
is the import generation the warehouse already tracks.
## Rejected: expand advisory ranges into exact versions at import
The first `cve_advisories` schema stored exact `affected_versions` and matched by equality, so
`Advisory::matches` was set-membership. Importing a range-only OSV/NVD entry then meant
expanding it against the full published-version universe per package. Rejected because it
explodes row counts, is lossy for versions published after import, and needs an offline
registry catalog we do not have. The scanner now matches range-native via
`versions::affected_by` at scan time; the exact-list path survives only as a fallback for
tiny/explicit advisories. Full argument + migration: [`cve-warehouse-ingest.md`](cve-warehouse-ingest.md) §0.
## Why Engine A carries no I/O
`OsvScanner::scan` takes an `AdvisoryDb` and an `Artifact` and returns a `Verdict` — no file,
no network, no warehouse. That is what lets holger's gate and static-holger link it with zero
heavy dependencies (the whole point of the leaf; see [`reasoning.md`](reasoning.md)). Getting
advisories *in* is the `feeds`/`warehouse` functions' job, kept on the other side of a feature
wall.