# verdict (Engine A) — design
> **Class:** `design` · **Function axis:** `f0` the scan fold · `f1` the checks · `f2` the
> policy · `f3` the verdict type. Strict: only the shape we run. The *why* is in
> [`verdict-reasoning.md`](verdict-reasoning.md); the system view in [`design.md`](design.md).

## f0 — the fold
Engine A is the lean, always-on gate (`default = ["scan"]`). It turns an
[`Artifact`](../src/scan.rs) (`scan.rs:58`) into a [`core::Verdict`](../src/core.rs) by folding
three independent checks and reducing their findings to the strictest `Decision`.
[`OsvScanner::scan`](../src/scan.rs) (`impl ScanEngine`) runs, in order:
1. **known-vuln match** — for each advisory `AdvisoryDb::query` returns for the artifact
(`scan.rs:213`), `vuln_finding` produces zero/one `Finding`;
2. **license** — `license_finding` → zero/one `Finding`;
3. **provenance** — `provenance_finding` → zero/one `Finding`;
then [`decide(&findings)`](../src/scan.rs) (`scan.rs:366`) picks the decision. Because
`Decision` derives `Ord` in declaration order `Pass < Warn < Block` (`core.rs:15`), the fold
is a `max()` over `severity → decision` (`decision_for`, `scan.rs:356`) — a clean artifact
is `Pass` (the identity). The result carries its cache key components
(`policy_version`, `advisory_db_rev`) so a consumer invalidates when either moves.
**Reuse:** `verdict.uses = [feeds, provenance, core]`. It matches against `feeds`' range-native
`Advisory`s and folds `provenance`'s findings; it owns no I/O.
## f1 — the three checks
### vuln_finding (`scan.rs`, exploit-intel gating)
One matched `Advisory` → zero/one `Finding`, applying, in order:
- **VEX suppression** — if `policy.vex_not_affected` contains the advisory id, return `None`
(the consumer asserted the vulnerable path is unreachable).
- **KEV escalation** — `adv.kev && policy.kev_blocks` ⇒ `severity.max(Critical)`, tag `[KEV]`.
- **EPSS escalation** — `adv.epss ≥ policy.epss_block_threshold` ⇒ `severity.max(High)`; the
EPSS score is tagged into the summary.
`AdvisoryDb::query` (`scan.rs:213`) filters the db to advisories whose `matches` accepts the
artifact ref — range-native via `feeds`/`versions` (see [`feeds-design.md`](feeds-design.md)).
### license_finding (`scan.rs`)
Against [`LicensePolicy`](../src/scan.rs) (`scan.rs:279`): a denied license → `High`
(`MODGUNN-LICENSE-DENIED`); a license absent from a non-empty allow-list → `Medium`
(`MODGUNN-LICENSE-UNREVIEWED`); no declared license → severity from `on_unknown`
(`MODGUNN-LICENSE-UNKNOWN`); otherwise no finding.
### provenance_finding (`scan.rs`)
A signed artifact yields nothing. An unsigned one maps [`ProvenancePolicy`](../src/scan.rs)
(`scan.rs:307`) `Ignore→Pass` / `Warn→Warn` / `Require→Block` through `severity_for` into a
`MODGUNN-PROVENANCE-MISSING` finding. The `Provenance` value itself is produced by the
[`provenance`](provenance-design.md) function's `evaluate`.
## f2 — the policy
[`Policy`](../src/scan.rs) (`scan.rs:320`) is the whole tunable surface: `version` (a cache-key
component), `license: LicensePolicy`, `provenance: ProvenancePolicy`, `kev_blocks: bool`,
`epss_block_threshold: Option<f64>`, `vex_not_affected: Vec<String>`. `Policy::default`
(`scan.rs:339`) is the shipped baseline. [`LicensePolicy::allowlist`](../src/scan.rs)
(`scan.rs:292`) builds an allow-list policy.
## f3 — the verdict type (`core.rs`)
[`Verdict`](../src/core.rs) (`core.rs:59`) = `{ artifact, decision, findings, policy_version,
advisory_db_rev }`. [`cache_key`](../src/core.rs) (`core.rs:70`) is
`(blob_sha256, policy_version, advisory_db_rev)` — content-addressed so a clean result is
computed once and reused. [`max_severity`](../src/core.rs) is the strongest finding severity.
[`ArtifactRef`](../src/core.rs) carries `(ecosystem, name, version, blob_sha256)`;
[`Finding`](../src/core.rs) is `{ severity, id, summary }`. This is the exact type Engine B
also emits, so consumers read one shape.
**Canonical labels (single source).** [`Decision::as_str`](../src/core.rs) /
`Display` (`core.rs`) is the one home of the `Pass`/`Warn`/`Block` strings, and
[`Severity::as_str`](../src/core.rs) / `Display` the one home of the severity
labels. Consumers — holger's gate & UI, nornir-viz's Security pane — render THESE
rather than hand-rolling a `match` or leaning on `{:?}` Debug output, so the label
set never drifts across the constellation. modgunn's own CLI dogfoods them
(`main.rs` finding/scorecard rows).
## Surface
| entry | signature | file:symbol |
|---|---|---|
| construct | `OsvScanner::new(policy, advisories)` | `scan.rs:394` |
| scan | `ScanEngine::scan(&self, &Artifact) -> Verdict` | `scan.rs:386` |
| fold | `decide(&[Finding]) -> Decision` | `scan.rs:366` |
| cache key | `Verdict::cache_key()` | `core.rs:70` |
Unit tests cover the fold, gating, and cache key (`scan.rs::tests`), plus the
canonical `Decision`/`Severity` label ↔ `Display` agreement (`core.rs::tests`).