| name | soup-inventory |
| description | Maintain a SOUP (Software Of Unknown Provenance) register for every third-party software item the project depends on. Discovers candidates from lockfiles (package-lock.json, go.sum, Cargo.lock, requirements.txt, pyproject.toml, pom.xml, packages.lock.json), enriches with human-supplied risk class and verification refs, validates against a license allow-list and the requirements catalogue, and emits a per-supplier report. Outputs docs/.index/soup.yaml plus docs/.index/soup.md. Subcommands: discover | enrich | validate | list | report. Bidirectional linking with traceability via the soup_ids[] field on requirement rows. Opt-in: no-op when no lockfile is detected and docs/.index/soup.yaml is absent. Atomic writes (*.tmp + rename); idempotent (records sorted by id). Implements IEC 62304 sections 5.3.3 (SOUP requirements) and 8.1.1 (configuration items). |
| argument-hint | <discover|enrich|validate|list|report> [--ci] [--verbose] |
| user-invocable | true |
| disable-model-invocation | true |
| allowed-tools | ["Bash","Read","Write","Glob","Grep"] |
| loop_safe | true |
| max_iterations | 1 |
| halt_conditions | [{"type":"success","expr":"subcommand completed successfully and soup.yaml is well-formed (atomic rename succeeded)"},{"type":"success","expr":"no-op exit when no lockfile is detected and docs/.index/soup.yaml is absent"},{"type":"failure","expr":"validate --ci reports a missing record, a forbidden license, an unset risk_class, or a dangling verification reference"},{"type":"failure","expr":"lockfile/soup-file parse error or write step errors out"}] |
| on_halt | Print per-record findings table (or no-op message) and exit non-zero on failure, zero on success or no-op |
| tiers | {"light":{"ref_docs":[],"deep_checks":false},"standard":{"ref_docs":["schema","sources"],"deep_checks":true},"deep":{"ref_docs":["schema","sources","license"],"deep_checks":true}} |
| default_tier | standard |
| iso_class | A |
| applies_at_or_above | A |
soup-inventory Skill
Maintain the SOUP (Software Of Unknown Provenance) register for projects that have adopted
the regulated-industry track. The skill is the third-party-software counterpart to
risk-control (which owns the hazard records) and traceability (which owns the
requirements-to-evidence matrix): it produces and maintains the per-project SOUP file that
the traceability matrix references via a new soup_ids[] field on each requirement row, and
that the evidence-pack skill collects under the soup_register kind (see
global/skills/_internal/evidence-pack/reference/source-mapping.md).
The skill does not invent regulatory content; it provides a structured, validatable home
for the records that IEC 62304 sections 5.3.3 (specify functional and performance
requirements of SOUP items) and 8.1.1 (identify configuration items) already require.
Usage
/soup-inventory discover # Scan lockfiles; produce candidate records (status: needs_review)
/soup-inventory enrich <id> # Open editor for human-supplied fields (purpose, risk_class, verification_refs)
/soup-inventory validate # Schema + cross-reference + license check (warnings only)
/soup-inventory validate --ci # Same; exit non-zero on any finding
/soup-inventory list # Print a table of all records and their status
/soup-inventory list --verbose # Same with control_measures and verification expanded
/soup-inventory report # Emit docs/.index/soup.md grouped by supplier
Exactly one subcommand is required as the first positional argument. The enrich subcommand
takes a record id (SOUP-NNN) as the second positional argument; the others take none.
Arguments
| Position / Flag | Behavior |
|---|
<subcommand> (positional, required) | One of discover, enrich, validate, list, report. Any other value exits non-zero with a usage hint. |
<id> (positional, required for enrich) | Record identifier matching ^SOUP-[0-9]{3}$ (see reference/soup-record-schema.md). enrich refuses an id absent from the register. |
--ci | Applies to validate. Exit non-zero on any finding. Intended for the GitHub Actions validate job. |
--verbose | Applies to list. Expand dependent_requirements and verification_refs columns. Otherwise prints only the headline status per record. |
--ci is ignored (with a warning) on discover, enrich, list, and report.
Inputs
| Path | Required | Purpose |
|---|
Project lockfile (any of: package-lock.json, go.sum, Cargo.lock, requirements.txt, pyproject.toml, pom.xml, packages.lock.json) | Yes (gate) for discover; optional otherwise | Source of candidate SOUP entries. The skill is a no-op when no lockfile is present and docs/.index/soup.yaml does not yet exist. |
docs/.index/soup.yaml | Optional on discover (created or merged); required on enrich/validate/list/report | The SOUP register the skill maintains. See reference/soup-record-schema.md for the exact YAML shape. |
soup-license-policy.yaml (repo root) | Optional | Per-project override of the default license allow-list and forbid-list. Defaults documented in reference/license-policy.md. |
docs/.index/manifest.yaml | Optional | Used by validate to confirm every requirement listed under dependent_requirements[] resolves to a real document. Skipped (with a warning) when absent. |
compliance/iec-62304.md | Optional | Source of truth for clause IDs referenced in verification_refs[]. When present, validate confirms every cited clause exists. Skipped (with one info line) when absent. |
The skill never reads source code, never runs builds, and never calls out to external systems.
Outputs
| Path | Format | Audience |
|---|
docs/.index/soup.yaml | YAML | Single normalized SOUP register. Consumed by traceability (via soup_ids[]) and evidence-pack (via the soup_register kind). Schema: reference/soup-record-schema.md. |
docs/.index/soup.md | Markdown | Human-readable per-supplier report produced by report. Suitable for inclusion in audit-time exports. |
Both files are written atomically: the skill writes to a sibling *.tmp file and renames on
success. A failed run never produces a half-written artifact. Records are sorted by id on
every write to guarantee byte-identical output for unchanged input (idempotency contract,
matches traceability, evidence-pack, and risk-control).
Opt-in Gate
The skill is purely additive and must not break repos that have not adopted the regulated track.
if ! detect_lockfile && [[ ! -f docs/.index/soup.yaml ]]; then
echo "soup-inventory: no lockfile detected and docs/.index/soup.yaml absent -- skill is a no-op for this repo"
exit 0
fi
detect_lockfile returns true when any of the lockfiles enumerated in
reference/discovery-sources.md is present at the repo root or under a documented
sub-directory. The check runs before any other phase. The exit code is 0 so CI invocations
on non-regulated repos do not fail. To opt in, a consumer project either checks a lockfile
into the repo (the common case) or hand-creates an empty docs/.index/soup.yaml (rare).
Instructions
Phase 0: Validate Environment
- Run the opt-in gate above; on no-op, print the message and exit 0.
- Validate the subcommand is one of
discover, enrich, validate, list, report.
Otherwise exit non-zero with a usage hint.
- For
enrich, validate the second positional argument matches ^SOUP-[0-9]{3}$. Exit
non-zero on malformed ids.
- Detect optional inputs:
soup-license-policy.yaml at repo root, docs/.index/manifest.yaml,
compliance/iec-62304.md. Record their presence; later phases gate on them.
- If
docs/.index/soup.yaml exists, parse it and verify _meta.schema major matches the
schema this skill writes (current major: 1). On a major mismatch, exit non-zero with the
mismatch detail. Minor mismatches are tolerated; the writer upgrades on next save.
Phase 1: Dispatch by Subcommand
Each subcommand runs an independent code path. The phases below describe the per-subcommand
behavior. All paths share the Phase 0 gate and the Phase 4 atomic-write step.
Subcommand: discover
Reference: reference/discovery-sources.md defines the per-language lockfile parsers and the
auto-populated fields each one yields.
- For each lockfile present, invoke the parser documented in
discovery-sources.md and
produce a candidate record per dependency. Auto-populated fields: name, version,
supplier, source_url, and license_spdx where the lockfile carries it.
- Mint a record id of the form
SOUP-NNN where NNN is the next free three-digit slot
(zero-padded, sorted ascending) not already in the existing register. Reuse the existing
id when a candidate's (name, version) pair already appears in the register.
- Set
status: needs_review on every newly created record. Existing records are left
untouched (the skill never overwrites enrichment data on re-discovery).
- Set
risk_class: unset (sentinel) and purpose: "" on new records. The operator fills
these in via enrich.
- Merge the new and existing record lists; proceed to Phase 4 to write the updated register.
Subcommand: enrich
- Refuse if
<id> is absent from docs/.index/soup.yaml.
- Load the existing record into memory.
- Prompt the operator for each enrichment field listed in
reference/soup-record-schema.md
"Enrichment Fields" section, prefilling current values:
purpose (one-line description of why this SOUP is used)
risk_class (one of A, B, C; matches IEC 62304 software item classification)
dependent_requirements[] (list of SRS-{CAT}-{NNN} ids that depend on this SOUP)
dependent_si_ids[] (list of SI-{CODE} design ids that allocate this SOUP)
verification_refs[] (list of clause ids, requirement ids, or evidence file paths)
- On successful submission, set
status: ready. Auto-populated fields (name, version,
supplier, source_url, license_spdx) are not editable through enrich; they are
refreshed only by discover.
- Replace the in-memory record; proceed to Phase 4.
Subcommand: validate
Reference: reference/license-policy.md defines the default allow-list and the override
mechanism. reference/soup-record-schema.md "Validation Rules" defines the full finding set.
- Load every record from
docs/.index/soup.yaml.
- Cross-check the lockfile entry list against the register: every lockfile entry must have a
matching SOUP record (finding
lockfile_entry_without_record). Records present in the
register but absent from the lockfile are warned only (the dependency may have been
removed; the operator decides whether to retire the record).
- For each record in the register, run the validation checks documented in
reference/soup-record-schema.md, which cover at minimum:
risk_class is one of A, B, C (finding risk_class_unset when value is unset).
license_spdx is in the active allow-list and not in the forbid-list (findings
license_outside_allow_list and license_in_forbid_list).
- Every
dependent_requirements[] entry resolves to a SRS-* id present in
manifest.yaml (when manifest is available; finding dangling_requirement_ref).
- Every
verification_refs[] entry resolves to one of: a clause id present in
compliance/iec-62304.md (when present), a SRS-* id present in manifest.yaml, or
a repo-relative file path that exists on disk (finding dangling_verification_ref).
status is one of needs_review or ready.
- Print the findings table to stdout. Format documented in
reference/soup-record-schema.md "Validation Output" section.
- In
--ci mode, exit non-zero on any finding. Default mode emits warnings and exits 0.
validate does not write to disk; skip Phase 4.
Subcommand: list
- Load every record from
docs/.index/soup.yaml.
- Print a Markdown table sorted by
id. Default columns: id, name, version,
license_spdx, risk_class, status. With --verbose, additionally expand
dependent_requirements and verification_refs as nested rows under each record.
list does not write to disk; skip Phase 4.
Subcommand: report
- Load every record from
docs/.index/soup.yaml.
- Group records by
supplier. Within each group, sort by id.
- Render
docs/.index/soup.md.tmp with one section per supplier. Each section contains a
table of records and a list of dependent requirements / design items, suitable for
inclusion in audit-time exports.
- Atomically rename
docs/.index/soup.md.tmp to docs/.index/soup.md. On any error,
delete the *.tmp file and exit non-zero.
report does not modify soup.yaml; skip Phase 4 for the YAML file.
Phase 4: Atomic Write
Skip when subcommand is validate, list, or report (the latter writes its own atomic
file under report's Phase 1 step 4), or when no record changed.
- Sort the in-memory record list by
id (stable, ASCII order). Recompute _meta.generated
timestamp.
- Render the YAML using the canonical layout in
reference/soup-record-schema.md. Empty
list fields are emitted as [], never as ~ or as an omitted key, so consumers never
need to distinguish "absent" from "empty".
- Write to
docs/.index/soup.yaml.tmp.
- On success, rename to
docs/.index/soup.yaml. On any error, delete the *.tmp file and
exit non-zero.
Phase 5: Report
Emit a one-line summary on stdout per subcommand:
| Subcommand | Summary line |
|---|
discover | soup-inventory: discovered <N> candidates from <M> lockfile(s); records=<R> (new=<X>, existing=<E>) |
enrich | `soup-inventory: enriched (risk_class=<A |
validate | soup-inventory: validated <N> records; findings=<F> (errors=<E>, warnings=<W>) |
list | soup-inventory: listed <N> records |
report | soup-inventory: wrote docs/.index/soup.md with <S> supplier sections covering <N> records |
Append the per-finding or per-record table required by the subcommand to stdout above the
summary.
Bidirectional SOUP-Requirement Linking
Each SOUP record carries dependent_requirements[] and dependent_si_ids[] lists naming
the SRS and SI ids that depend on this third-party component. The link is one-directional in
the SOUP file (SOUP -> requirements), but the next traceability skill run picks it up in
the other direction without any new wiring: the matrix row schema reserves soup_ids[] on
every requirement row (see traceability/reference/matrix-schema.md), and the matrix's
Phase 1 step would collect SOUP ids from docs/.index/soup.yaml when it is present. When
the SOUP register is the source of truth, the matrix's SOUP-side data source is this skill
rather than ad-hoc registries.
The integration contract is: soup-inventory owns the SOUP records; traceability reads
them and stitches them into the matrix. No bidirectional write coordination is required
because the SOUP register is the single source of truth for third-party-component metadata.
Output
The skill produces only docs/.index/soup.yaml (and, for report, docs/.index/soup.md)
plus the on-stdout summary. No other files in the consumer project are modified -- in
particular, the skill never edits manifest.yaml, bundles.yaml, graph.yaml,
router.yaml, traceability.yaml, or risk-file.yaml. Those remain doc-index's,
traceability's, and risk-control's responsibilities.
Error Handling
| Condition | Action |
|---|
No lockfile detected and docs/.index/soup.yaml absent | No-op exit 0 (opt-in gate, see Phase 0). |
| Subcommand argument missing or invalid | Exit non-zero with usage hint. |
<id> malformed for enrich | Exit non-zero with the expected format from the schema. |
enrich invoked with an absent id | Exit non-zero with the suggestion to run discover first. |
_meta.schema major mismatch on existing SOUP file | Exit non-zero with the detected vs. expected schema versions. |
Lockfile parse error during discover | Exit non-zero with the lockfile path and the parse error; do not partially update the register. |
soup-license-policy.yaml malformed | Exit non-zero with the YAML parse error and the offending line. |
Validation finding in --ci mode | Print the finding table, exit non-zero. (Per IEC 62304 8.1.1, an unidentified configuration item must not pass review; CI mirrors that policy.) |
| Validation finding in default mode | Print the finding table, exit 0. |
| Write step fails (permission, disk) | Delete the *.tmp file, report the path, exit non-zero. |
compliance/iec-62304.md referenced but absent | Skip clause validation; print one informational line; do not fail. |
Policies
Side Effects and Loop-Safety
This skill is loop_safe: true. The validate, list, and report subcommands are pure
reads of the YAML register (with report writing only the regenerated Markdown view, which
the idempotency contract makes safe). The discover subcommand is also idempotent: re-running
on an unchanged lockfile produces a byte-identical register because (a) ids are stable for
existing (name, version) pairs and (b) records are sorted by id on every write. The
enrich subcommand is gated by operator input, so wrapping in /loop would no-op after the
first prompt -- safe but pointless.
Command-Specific Rules
| Item | Rule |
|---|
| Inputs | Lockfiles enumerated in reference/discovery-sources.md, docs/.index/{manifest,soup}.yaml, optional soup-license-policy.yaml, optional compliance/iec-62304.md. The skill never crawls source-code directories. |
| Outputs | Only docs/.index/soup.yaml and docs/.index/soup.md. No other file is touched. |
| Opt-in | No lockfile and no existing register is a no-op exit 0, never a failure. |
| Atomicity | Writes go to *.tmp first, then rename on success. Failure deletes the temp file. |
| Idempotency | Same input always produces byte-identical output. Re-running discover on an unchanged lockfile is a no-op; re-running report produces a byte-identical Markdown file. |
| Bidirectional linking | One-directional in the SOUP file (SOUP -> requirements). traceability reads it and produces the inverse view in the matrix. |
| External submission | Never. Submission to eQMS / regulator portals is out of scope. |
| CVE / vulnerability scanning | Out of scope. The register tracks provenance, license, and verification, not vulnerabilities. A separate skill (future) handles the CVE channel. |
Validation Message Style
When validate --ci reports a finding, the message must include the relevant IEC 62304
clause id (e.g. IEC-62304-5.3.3 for a missing SOUP requirement, IEC-62304-8.1.1 for a
missing configuration item) so an auditor reading the CI log can pivot directly to the
standard. The clause-to-finding mapping is documented in
reference/soup-record-schema.md "Validation Rules" -- the skill body must use that table
verbatim rather than inventing new mappings.
How Other Components Use the SOUP Register
| Consumer | Use |
|---|
traceability skill (P0-1) | Reads soup.yaml to populate soup_ids[] on requirement matrix rows. Bidirectional linking comes for free. |
evidence-pack skill (P1-1) | Mirrors soup.yaml (and the per-supplier soup.md report when present) under the soup_register kind in the per-release evidence pack. See global/skills/_internal/evidence-pack/reference/source-mapping.md (kind: soup_register) and manifest-schema.md (allowed kinds, schema 1.1.0). |
risk-control skill (P1-2) | Cross-references SOUP records when a hazard's failure mode involves third-party software; the link is recorded as a verification_refs[] entry pointing to the relevant H-NN id. |
traceability-guard PreToolUse hook (P0-2) | Could be extended to detect when a PR adds or bumps a lockfile entry without updating the SOUP register. |
| External auditor | Reads soup.yaml and soup.md directly from the repo at any tagged release; consumes them alongside the matrix as the operational SOUP-management record. |
References
- SOUP record schema:
reference/soup-record-schema.md
- Per-language discovery sources:
reference/discovery-sources.md
- Default license policy and per-project override format:
reference/license-policy.md
- IEC 62304 clause source:
compliance/iec-62304.md (project root, when present)
- Sibling matrix consumer:
global/skills/_internal/traceability/SKILL.md
- Sibling release-time consumer:
global/skills/_internal/evidence-pack/SKILL.md
- Sibling risk-side consumer:
global/skills/_internal/risk-control/SKILL.md
- Parent epic:
kcenon/claude-config#588
- Originating issue:
kcenon/claude-config#601