Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/modgunn

.nornir/manifests-design.md

manifests — design

Class: design · Function axis: f0 the seam · f1 the parsers · f2 the dispatch. Strict: only the shape we run. The system view is in design.md; the emit counterpart is sbom-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.