manifests — design
Class:
design· Function axis:f0the seam ·f1the parsers ·f2the dispatch. Strict: only the shape we run. The system view is indesign.md; the emit counterpart issbom-design.md.
f0 — the seam
manifests (module manifests.rs, feature manifests) is the ingest direction: it reads a project's dependency manifest / lockfile and turns it into the SAME shared component model the crate emits — sbom::SbomComponent ({ ecosystem, name, version, license }). sbom emits that model as CycloneDX; manifests produces it from source. The model is never forked: cargo (Cargo.lock) is the template and every other ecosystem lands its components in that exact struct, so a downstream verdict / SBOM emit never learns which ecosystem it came from.
The single seam is EcosystemParser: ecosystem() -> &'static str, manifests() -> &'static [&'static str] (the basenames it owns), handles(filename) -> bool (default: exact-match the basenames), and parse(filename, content) -> Result<Vec<SbomComponent>, ParseError>. It is object-safe, so all() holds Box<dyn EcosystemParser> and parse_manifest dispatches dynamically. Malformed input never panics — it returns ParseError.
Reuse: manifests.uses = [sbom]. Pure parsing — no warehouse, no network, zero shell: no parser ever runs the target project's pip / npm / go / mvn tooling.
f1 — one parser per ecosystem
Seven ecosystems, one impl each; several own more than one format and disambiguate inside parse by filename:
| ecosystem | struct | manifests | parse route |
| cargo | CargoParser |
Cargo.lock |
TOML [[package]] |
| pip | PipParser |
requirements.txt, poetry.lock, Pipfile.lock |
== pins · TOML · JSON default/develop |
| npm | NpmParser |
package-lock.json, pnpm-lock.yaml |
JSON packages/v1 dependencies · hand-parsed packages: keys |
| maven | MavenParser |
pom.xml |
roxmltree <dependency> → groupId:artifactId |
| go | GoParser |
go.mod, go.sum |
require directives · checksum lines (deduped) |
| nuget | NugetParser |
packages.lock.json, *.csproj |
JSON resolved · roxmltree <PackageReference> |
| gem | GemParser |
Gemfile.lock |
GEM … specs: 4-indent name (version) |
Cargo.lock and poetry.lock share the [[package]] name/version shape, so both fold through one toml_packages helper — the parse, like the model, stays single-sourced. Only resolved versions are emitted: a requirements.txt range, a Maven ${property}, a NuGet central-management reference, or a Gemfile constraint line is skipped (not a pinned version). JSON reuses serde_json, TOML the toml dep, XML roxmltree; text formats (requirements, go, gem, pnpm) are hand-parsed with no dep.
f2 — dispatch + introspection
parse_manifest(filename, content) matches the path's basename against each parser's handles, returning None for an unknown filename and Some(Result<…>) otherwise. Under feature testmatrix it lights one modgunn/manifest-ingest-{ecosystem} functional-status row per parse (GREEN = N components, RED = malformed), so nornir test SEES the seam per ecosystem.
f3 — the driveable surface (modgunn ingest)
The seam is also a CLI verb — cmd_ingest, modgunn ingest --path <manifest-file|dir> [--sbom] [--subject <name>] [--ecosystem <eco>] — so a consumer can drive the ingest without writing Rust. It reads ONE explicit manifest, or walks a directory for every recognised manifest (VCS/build/vendored trees — .git/target/node_modules/vendor/… — skipped, via collect_manifests), folds all components through parse_manifest, then dedups + sorts by (ecosystem, name, version) and prints either the component rows with a per-ecosystem tally (default) or a CycloneDX 1.5 SBOM (--sbom, straight into sbom::to_cyclonedx). --ecosystem narrows to one ecosystem. An explicit file no parser owns is a hard usage error (lists the supported manifests); a malformed manifest is named on stderr and counted, never a panic. Offline, zero-shell, no warehouse — the read-side counterpart to sbom emit. Emits a modgunn/cli-ingest functional-status row under testmatrix; e2e-proven by tests/cli_ingest.rs driving the compiled binary.
Feature gating: manifests = ["scan", "dep:toml", "dep:roxmltree"], OFF by default — the lean scan gate (holger / static-holger) never pulls a TOML/XML parser; nornir's SBOM capture opts in. The ingest verb is gated on manifests with a rebuild-hint stub when absent (mirroring the verify-provenance gate); the binary itself still requires warehouse for its other verbs, so the consumer build is cargo run --features warehouse,manifests -- ingest …. The existing cargo warehouse-read + sbom emit path is untouched — this is purely additive.