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
1---2name: soup-inventory3description: 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).4---56# soup-inventory Skill78Maintain the SOUP (Software Of Unknown Provenance) register for projects that have adopted9the regulated-industry track. The skill is the third-party-software counterpart to10`risk-control` (which owns the hazard records) and `traceability` (which owns the11requirements-to-evidence matrix): it produces and maintains the per-project SOUP file that12the traceability matrix references via a new `soup_ids[]` field on each requirement row, and13that the `evidence-pack` skill collects under the `soup_register` kind (see14`global/skills/_internal/evidence-pack/reference/source-mapping.md`).1516The skill does not invent regulatory content; it provides a structured, validatable home17for the records that IEC 62304 sections 5.3.3 (specify functional and performance18requirements of SOUP items) and 8.1.1 (identify configuration items) already require.1920## Usage2122```23/soup-inventory discover # Scan lockfiles; produce candidate records (status: needs_review)24/soup-inventory enrich <id> # Open editor for human-supplied fields (purpose, risk_class, verification_refs)25/soup-inventory validate # Schema + cross-reference + license check (warnings only)26/soup-inventory validate --ci # Same; exit non-zero on any finding27/soup-inventory list # Print a table of all records and their status28/soup-inventory list --verbose # Same with control_measures and verification expanded29/soup-inventory report # Emit docs/.index/soup.md grouped by supplier30```3132Exactly one subcommand is required as the first positional argument. The `enrich` subcommand33takes a record id (`SOUP-NNN`) as the second positional argument; the others take none.3435## Arguments3637| Position / Flag | Behavior |38|-----------------|----------|39| `<subcommand>` (positional, required) | One of `discover`, `enrich`, `validate`, `list`, `report`. Any other value exits non-zero with a usage hint. |40| `<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. |41| `--ci` | Applies to `validate`. Exit non-zero on any finding. Intended for the GitHub Actions validate job. |42| `--verbose` | Applies to `list`. Expand `dependent_requirements` and `verification_refs` columns. Otherwise prints only the headline status per record. |4344`--ci` is ignored (with a warning) on `discover`, `enrich`, `list`, and `report`.4546## Inputs4748| Path | Required | Purpose |49|------|----------|---------|50| 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. |51| `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. |52| `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`. |53| `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. |54| `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. |5556The skill never reads source code, never runs builds, and never calls out to external systems.5758## Outputs5960| Path | Format | Audience |61|------|--------|----------|62| `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`. |63| `docs/.index/soup.md` | Markdown | Human-readable per-supplier report produced by `report`. Suitable for inclusion in audit-time exports. |6465Both files are written atomically: the skill writes to a sibling `*.tmp` file and renames on66success. A failed run never produces a half-written artifact. Records are sorted by `id` on67every write to guarantee byte-identical output for unchanged input (idempotency contract,68matches `traceability`, `evidence-pack`, and `risk-control`).6970## Opt-in Gate7172The skill is purely additive and must not break repos that have not adopted the regulated track.7374```bash75if ! detect_lockfile && [[ ! -f docs/.index/soup.yaml ]]; then76 echo "soup-inventory: no lockfile detected and docs/.index/soup.yaml absent -- skill is a no-op for this repo"77 exit 078fi79```8081`detect_lockfile` returns true when any of the lockfiles enumerated in82`reference/discovery-sources.md` is present at the repo root or under a documented83sub-directory. The check runs before any other phase. The exit code is `0` so CI invocations84on non-regulated repos do not fail. To opt in, a consumer project either checks a lockfile85into the repo (the common case) or hand-creates an empty `docs/.index/soup.yaml` (rare).8687## Instructions8889### Phase 0: Validate Environment90911. Run the opt-in gate above; on no-op, print the message and exit 0.922. Validate the subcommand is one of `discover`, `enrich`, `validate`, `list`, `report`.93 Otherwise exit non-zero with a usage hint.943. For `enrich`, validate the second positional argument matches `^SOUP-[0-9]{3}$`. Exit95 non-zero on malformed ids.964. Detect optional inputs: `soup-license-policy.yaml` at repo root, `docs/.index/manifest.yaml`,97 `compliance/iec-62304.md`. Record their presence; later phases gate on them.985. If `docs/.index/soup.yaml` exists, parse it and verify `_meta.schema` major matches the99 schema this skill writes (current major: `1`). On a major mismatch, exit non-zero with the100 mismatch detail. Minor mismatches are tolerated; the writer upgrades on next save.101102### Phase 1: Dispatch by Subcommand103104Each subcommand runs an independent code path. The phases below describe the per-subcommand105behavior. All paths share the Phase 0 gate and the Phase 4 atomic-write step.106107#### Subcommand: discover108109Reference: `reference/discovery-sources.md` defines the per-language lockfile parsers and the110auto-populated fields each one yields.1111121. For each lockfile present, invoke the parser documented in `discovery-sources.md` and113 produce a candidate record per dependency. Auto-populated fields: `name`, `version`,114 `supplier`, `source_url`, and `license_spdx` where the lockfile carries it.1152. Mint a record id of the form `SOUP-NNN` where `NNN` is the next free three-digit slot116 (zero-padded, sorted ascending) not already in the existing register. Reuse the existing117 id when a candidate's `(name, version)` pair already appears in the register.1183. Set `status: needs_review` on every newly created record. Existing records are left119 untouched (the skill never overwrites enrichment data on re-discovery).1204. Set `risk_class: unset` (sentinel) and `purpose: ""` on new records. The operator fills121 these in via `enrich`.1225. Merge the new and existing record lists; proceed to Phase 4 to write the updated register.123124#### Subcommand: enrich1251261. Refuse if `<id>` is absent from `docs/.index/soup.yaml`.1272. Load the existing record into memory.1283. Prompt the operator for each enrichment field listed in `reference/soup-record-schema.md`129 "Enrichment Fields" section, prefilling current values:130 - `purpose` (one-line description of why this SOUP is used)131 - `risk_class` (one of `A`, `B`, `C`; matches IEC 62304 software item classification)132 - `dependent_requirements[]` (list of `SRS-{CAT}-{NNN}` ids that depend on this SOUP)133 - `dependent_si_ids[]` (list of `SI-{CODE}` design ids that allocate this SOUP)134 - `verification_refs[]` (list of clause ids, requirement ids, or evidence file paths)1354. On successful submission, set `status: ready`. Auto-populated fields (`name`, `version`,136 `supplier`, `source_url`, `license_spdx`) are not editable through `enrich`; they are137 refreshed only by `discover`.1385. Replace the in-memory record; proceed to Phase 4.139140#### Subcommand: validate141142Reference: `reference/license-policy.md` defines the default allow-list and the override143mechanism. `reference/soup-record-schema.md` "Validation Rules" defines the full finding set.1441451. Load every record from `docs/.index/soup.yaml`.1462. Cross-check the lockfile entry list against the register: every lockfile entry must have a147 matching SOUP record (finding `lockfile_entry_without_record`). Records present in the148 register but absent from the lockfile are warned only (the dependency may have been149 removed; the operator decides whether to retire the record).1503. For each record in the register, run the validation checks documented in151 `reference/soup-record-schema.md`, which cover at minimum:152 - `risk_class` is one of `A`, `B`, `C` (finding `risk_class_unset` when value is `unset`).153 - `license_spdx` is in the active allow-list and not in the forbid-list (findings154 `license_outside_allow_list` and `license_in_forbid_list`).155 - Every `dependent_requirements[]` entry resolves to a `SRS-*` id present in156 `manifest.yaml` (when manifest is available; finding `dangling_requirement_ref`).157 - Every `verification_refs[]` entry resolves to one of: a clause id present in158 `compliance/iec-62304.md` (when present), a `SRS-*` id present in `manifest.yaml`, or159 a repo-relative file path that exists on disk (finding `dangling_verification_ref`).160 - `status` is one of `needs_review` or `ready`.1614. Print the findings table to stdout. Format documented in162 `reference/soup-record-schema.md` "Validation Output" section.1635. In `--ci` mode, exit non-zero on any finding. Default mode emits warnings and exits 0.1646. `validate` does not write to disk; skip Phase 4.165166#### Subcommand: list1671681. Load every record from `docs/.index/soup.yaml`.1692. Print a Markdown table sorted by `id`. Default columns: `id`, `name`, `version`,170 `license_spdx`, `risk_class`, `status`. With `--verbose`, additionally expand171 `dependent_requirements` and `verification_refs` as nested rows under each record.1723. `list` does not write to disk; skip Phase 4.173174#### Subcommand: report1751761. Load every record from `docs/.index/soup.yaml`.1772. Group records by `supplier`. Within each group, sort by `id`.1783. Render `docs/.index/soup.md.tmp` with one section per supplier. Each section contains a179 table of records and a list of dependent requirements / design items, suitable for180 inclusion in audit-time exports.1814. Atomically rename `docs/.index/soup.md.tmp` to `docs/.index/soup.md`. On any error,182 delete the `*.tmp` file and exit non-zero.1835. `report` does not modify `soup.yaml`; skip Phase 4 for the YAML file.184185### Phase 4: Atomic Write186187Skip when subcommand is `validate`, `list`, or `report` (the latter writes its own atomic188file under `report`'s Phase 1 step 4), or when no record changed.1891901. Sort the in-memory record list by `id` (stable, ASCII order). Recompute `_meta.generated`191 timestamp.1922. Render the YAML using the canonical layout in `reference/soup-record-schema.md`. Empty193 list fields are emitted as `[]`, never as `~` or as an omitted key, so consumers never194 need to distinguish "absent" from "empty".1953. Write to `docs/.index/soup.yaml.tmp`.1964. On success, rename to `docs/.index/soup.yaml`. On any error, delete the `*.tmp` file and197 exit non-zero.198199### Phase 5: Report200201Emit a one-line summary on stdout per subcommand:202203| Subcommand | Summary line |204|------------|--------------|205| `discover` | `soup-inventory: discovered <N> candidates from <M> lockfile(s); records=<R> (new=<X>, existing=<E>)` |206| `enrich` | `soup-inventory: enriched <id> (risk_class=<A|B|C>); records=<N>` |207| `validate` | `soup-inventory: validated <N> records; findings=<F> (errors=<E>, warnings=<W>)` |208| `list` | `soup-inventory: listed <N> records` |209| `report` | `soup-inventory: wrote docs/.index/soup.md with <S> supplier sections covering <N> records` |210211Append the per-finding or per-record table required by the subcommand to stdout above the212summary.213214## Bidirectional SOUP-Requirement Linking215216Each SOUP record carries `dependent_requirements[]` and `dependent_si_ids[]` lists naming217the SRS and SI ids that depend on this third-party component. The link is one-directional in218the SOUP file (SOUP -> requirements), but the next `traceability` skill run picks it up in219the other direction without any new wiring: the matrix row schema reserves `soup_ids[]` on220every requirement row (see `traceability/reference/matrix-schema.md`), and the matrix's221Phase 1 step would collect SOUP ids from `docs/.index/soup.yaml` when it is present. When222the SOUP register is the source of truth, the matrix's SOUP-side data source is this skill223rather than ad-hoc registries.224225The integration contract is: `soup-inventory` owns the SOUP records; `traceability` reads226them and stitches them into the matrix. No bidirectional write coordination is required227because the SOUP register is the single source of truth for third-party-component metadata.228229## Output230231The skill produces only `docs/.index/soup.yaml` (and, for `report`, `docs/.index/soup.md`)232plus the on-stdout summary. No other files in the consumer project are modified -- in233particular, the skill never edits `manifest.yaml`, `bundles.yaml`, `graph.yaml`,234`router.yaml`, `traceability.yaml`, or `risk-file.yaml`. Those remain `doc-index`'s,235`traceability`'s, and `risk-control`'s responsibilities.236237## Error Handling238239| Condition | Action |240|-----------|--------|241| No lockfile detected and `docs/.index/soup.yaml` absent | No-op exit 0 (opt-in gate, see Phase 0). |242| Subcommand argument missing or invalid | Exit non-zero with usage hint. |243| `<id>` malformed for `enrich` | Exit non-zero with the expected format from the schema. |244| `enrich` invoked with an absent id | Exit non-zero with the suggestion to run `discover` first. |245| `_meta.schema` major mismatch on existing SOUP file | Exit non-zero with the detected vs. expected schema versions. |246| Lockfile parse error during `discover` | Exit non-zero with the lockfile path and the parse error; do not partially update the register. |247| `soup-license-policy.yaml` malformed | Exit non-zero with the YAML parse error and the offending line. |248| 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.) |249| Validation finding in default mode | Print the finding table, exit 0. |250| Write step fails (permission, disk) | Delete the `*.tmp` file, report the path, exit non-zero. |251| `compliance/iec-62304.md` referenced but absent | Skip clause validation; print one informational line; do not fail. |252253## Policies254255### Side Effects and Loop-Safety256257This skill is `loop_safe: true`. The `validate`, `list`, and `report` subcommands are pure258reads of the YAML register (with `report` writing only the regenerated Markdown view, which259the idempotency contract makes safe). The `discover` subcommand is also idempotent: re-running260on an unchanged lockfile produces a byte-identical register because (a) ids are stable for261existing `(name, version)` pairs and (b) records are sorted by id on every write. The262`enrich` subcommand is gated by operator input, so wrapping in `/loop` would no-op after the263first prompt -- safe but pointless.264265### Command-Specific Rules266267| Item | Rule |268|------|------|269| 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. |270| Outputs | Only `docs/.index/soup.yaml` and `docs/.index/soup.md`. No other file is touched. |271| Opt-in | No lockfile and no existing register is a no-op exit 0, never a failure. |272| Atomicity | Writes go to `*.tmp` first, then rename on success. Failure deletes the temp file. |273| 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. |274| Bidirectional linking | One-directional in the SOUP file (SOUP -> requirements). `traceability` reads it and produces the inverse view in the matrix. |275| External submission | Never. Submission to eQMS / regulator portals is out of scope. |276| CVE / vulnerability scanning | Out of scope. The register tracks provenance, license, and verification, not vulnerabilities. A separate skill (future) handles the CVE channel. |277278### Validation Message Style279280When `validate --ci` reports a finding, the message must include the relevant IEC 62304281clause id (e.g. `IEC-62304-5.3.3` for a missing SOUP requirement, `IEC-62304-8.1.1` for a282missing configuration item) so an auditor reading the CI log can pivot directly to the283standard. The clause-to-finding mapping is documented in284`reference/soup-record-schema.md` "Validation Rules" -- the skill body must use that table285verbatim rather than inventing new mappings.286287## How Other Components Use the SOUP Register288289| Consumer | Use |290|----------|-----|291| `traceability` skill (P0-1) | Reads `soup.yaml` to populate `soup_ids[]` on requirement matrix rows. Bidirectional linking comes for free. |292| `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`). |293| `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. |294| `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. |295| 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. |296297## References298299- SOUP record schema: `reference/soup-record-schema.md`300- Per-language discovery sources: `reference/discovery-sources.md`301- Default license policy and per-project override format: `reference/license-policy.md`302- IEC 62304 clause source: `compliance/iec-62304.md` (project root, when present)303- Sibling matrix consumer: `global/skills/_internal/traceability/SKILL.md`304- Sibling release-time consumer: `global/skills/_internal/evidence-pack/SKILL.md`305- Sibling risk-side consumer: `global/skills/_internal/risk-control/SKILL.md`306- Parent epic: `kcenon/claude-config#588`307- Originating issue: `kcenon/claude-config#601`