# feeds — reasoning
> **Class:** `reasoning`. Why the ingest layer is shaped this way, and the forks not taken.
> The descriptive shape is in [`feeds-design.md`](feeds-design.md). The full ingest roadmap
> (phases P0–P6, schema v2) is [`cve-warehouse-ingest.md`](cve-warehouse-ingest.md).
## Why OSV is the lingua franca (and NVD is only enrichment)
OSV.dev's per-ecosystem exports already federate GHSA, PyPA/PYSEC, RUSTSEC, Go vulndb, npm,
Maven, NuGet, Hex, Pub *and* several distro feeds, all in one range-native JSON shape. Leading
with OSV means one parser (`osv.rs`) covers nearly every ecosystem; native adapters are only
written where OSV.dev's coverage is thin. NVD is treated as **enrichment, not authority**
because its CPE↔package mapping is lossy and product-centric — it backfills CVSS vectors, CWE
ids and severity onto advisories OSV already matched, and only emits standalone advisories
(under a separate `ecosystem = "cpe"` namespace, opt-in) for CVEs OSV misses entirely, so a
lossy CPE guess can never false-match a real OSV package.
## Why store ranges, not exact versions (the P0 decision)
This is the single highest-leverage choice in modgunn and it is argued in full in
[`cve-warehouse-ingest.md`](cve-warehouse-ingest.md) §0 and [`verdict-reasoning.md`](verdict-reasoning.md).
In short: expand-at-import explodes rows, is lossy for later-published versions, and needs an
offline registry catalog we lack. Persisting OSV range events and evaluating `affected_by` at
scan time against the artifact's actual version is lossless and lazy. The exact `affected_versions`
column stays only as an optional cache for tiny/explicit advisories.
## Why dedup by alias union (not by primary id)
The same vulnerability appears as a CVE in NVD, a GHSA in GitHub's DB, and a RUSTSEC id in
the Rust advisory DB — three rows for one bug. Keying dedup on the *primary* id would keep all
three; keying on the **alias set** (`merge_by_alias`) collapses them into one row with the
union of ranges, the max severity, the best CVSS, and the OR of the KEV flag. That is why a
GHSA-primary advisory still gets EPSS/KEV enrichment from its CVE alias, and why the fleet
scan reports one finding per real bug instead of three.
## Why the RUSTSEC range inversion is oracle-tested
RUSTSEC `.md` advisories express the *safe* set (`patched` / `unaffected`), the inverse of
OSV's *affected* ranges. `rustsec_affected_ranges` inverts `!patched && !unaffected` into
affected `RangeEvent`s. Because that inversion is subtle (multi-branch cases like aws-lc), it
is checked for equivalence against cargo-audit's own predicate expressed with the real
`semver::VersionReq` — as a **dev-dependency only**, so the lean build stays semver-free while
the test gets the true rule. The pure inversion is ungated; only the toml front-matter parse
needs the `rustsec` feature, keeping toml out of holger's gate.
## Why distro comparators are per-ecosystem, not one grammar
deb `1:2.3-4`, rpm `1.2-3.el9` and apk `1.2.3_alpha-r1` each have their own ordering rules
(epoch/revision, `~`/`^` weighting, suffix ranks) that semver cannot express. A single "loose"
comparator would mis-order them and silently mis-match advisories. So `versions.rs` carries
`deb_cmp`/`rpm_cmp`/`apk_cmp` and `normalize_ecosystem` routes OSV's distro labels to the
right one — which is what lets OSV.dev's distro exports ingest correctly with **no** native
adapter.
## Why parse failures are named and counted, not swallowed
A silently dropped advisory file is silently reduced CVE coverage — the worst failure mode for
a security tool. `cmd_import` names each unparseable file on stderr, counts it (`n_fail`), and
surfaces the tally in the import summary, so a truncated feed is visible rather than a quiet
gap in the verdict.