feeds — reasoning
Class:
reasoning. Why the ingest layer is shaped this way, and the forks not taken. The descriptive shape is infeeds-design.md. The full ingest roadmap (phases P0–P6, schema v2) iscve-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 §0 and 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 RangeEvents. 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.