--- name: openspec-traceability description: Assigns and maintains stable requirement/scenario IDs that OpenSpec has no native concept of, then generates the TRACEABILITY.md matrix and the machine-readable EVIDENCE.json log. The one skill in this capability permitted to write inside openspec/ — and only ever to add an inert HTML-comment ID line, never to touch requirement text. allowed-tools: Read, Edit, Write, Bash, Grep, Glob license: MIT metadata: author: openspec-gsd-integration version: "0.1.0" --- ## Purpose Everything else in this capability keys off stable IDs, and OpenSpec identifies requirements only by heading text (verified: `openspec show`/`validate --json` expose `requirementCount` and `requirements[]`, but no persistent identifier). This skill supplies that missing layer additively, then uses it to join five otherwise-disconnected artifact families — OpenSpec specs, GSD plans, git diffs, tests, evidence — into one matrix. ## Invocation conditions - Automatically as part of `openspec-audit` in `standard` mode and above. - Automatically at `discuss:post` time, indirectly: the `discuss-post` fragment refuses to invent IDs itself and flags `ids_pending: true` in `OPENSPEC-LINK.md`; this skill is what clears that flag. - Directly, `/openspec-traceability`, to refresh the matrix after execution without a full audit. ## Required inputs - A change-id, or "all active changes" (default). - `.planning/openspec/id-registry.json` if it exists (created on first run). ## Files read - `openspec/changes//specs/**/spec.md`, `openspec/specs/**/spec.md` - `openspec/changes//tasks.md` (OpenSpec task numbering — `1.1`, `2.3`, …) - `.planning/phases/-/-OPENSPEC-LINK.md`, `--PLAN.md`, `--SUMMARY.md` - `-OPENSPEC-VERIFICATION.md` (per-requirement statuses — this skill never re-derives them) - Git log/diff for the phase's commits ## Files this skill may write - `openspec/**/spec.md` — **ID comment lines only.** This is the single sanctioned write into OpenSpec content in this whole capability. It inserts `` on the line after a heading and changes nothing else — no requirement text, no RFC-2119 keyword, no scenario body, no heading wording. - `.planning/openspec/id-registry.json` - `.planning/openspec/TRACEABILITY.md` - `.planning/openspec/EVIDENCE.json` ## Algorithm **Phase A — ID assignment (idempotent).** 1. Parse every `### Requirement: ` and `#### Scenario: ` heading in scope. 2. Compute `key = sha256(normalized_heading_text)[:12]`, where normalization lowercases, collapses whitespace, and strips trailing punctuation. 3. Look `key` up in `id-registry.json`: - **Hit** → reuse the recorded ID. Never renumber, even if the heading was reworded (the registry additionally stores `last_seen_heading` so a reword is visible without being destructive). - **Miss, but the heading already carries an `` comment** → adopt `X` and back-fill the registry. This is how a hand-written or previously-injected ID survives a lost registry file. - **Miss entirely** → mint the next ID in that domain's sequence: `REQ--` for requirements, `SCN--` for scenarios, where `` is the uppercased spec directory name (`specs/health-check/spec.md` → `HEALTH`). Counters are per-domain and monotonic — a deleted requirement's number is retired, never recycled. 4. Write missing comment lines into the spec files. A heading that already carries the correct comment is left byte-identical (this is what makes a second run a genuine no-op). 5. Any registry entry whose `key` no longer appears in any spec is marked `"status": "REMOVED"` with the commit SHA where it vanished — recorded, never silently dropped. **Consumer note (measured, not assumed).** The injected comment does not break parsing or validation, but `openspec show --json` *does* include it in the extracted `requirement.text` and scenario `rawText`. Any step that reads those fields — including this skill's own Phase B and `openspec-verify`'s Level 2 — must strip it first: ``` /\s*/g ``` Verified on OpenSpec 1.6.0. IDs survive `openspec archive`'s merge into `openspec/specs/`, which is what keeps traceability resolvable after a change is closed. **Phase B — Matrix join.** For each requirement ID, resolve the row left to right: | Column | Resolution | Automatic? | |---|---|---| | Requirement | Phase A registry | ✅ | | Scenario | OpenSpec heading nesting under that requirement | ✅ | | OpenSpec Task | `tasks.md` checklist item citing the requirement (`(REQ-INTG-01)` convention in the section heading) | ✅ | | GSD Task | `PLAN.md` task frontmatter `requirements:` list | ✅ | | Code | files in the git diff of the commits that plan produced | ✅ | | Test | test file cited by `SUMMARY.md` for that task | ✅ | | Evidence | `EVIDENCE.json` entry keyed by requirement+scenario | ✅ | | Status | copied verbatim from `OPENSPEC-VERIFICATION.md` | ⚠️ — the *status* is automatic here, but the judgment that produced it (does this test really exercise the GIVEN/WHEN/THEN?) was `openspec-verify`'s, and is not re-litigated | A column that cannot be resolved renders as `—`, never as a plausible guess. An `—` in Test or Evidence for a `MUST` requirement is exactly the signal the demo's acceptance criterion (zero `NOT_TESTED` rows) checks for. **Phase C — Emit.** Write `TRACEABILITY.md` (human) and `EVIDENCE.json` (machine, schema at `docs/EVIDENCE-SCHEMA.json`). ## Outputs - `.planning/openspec/TRACEABILITY.md` — the matrix, one row per requirement×scenario, plus a legend distinguishing automatically-derived from judgment-derived cells. - `.planning/openspec/EVIDENCE.json` — append-only log; entries are content-addressed by `requirement_id + scenario_id + commit_sha`, so re-running adds nothing when nothing changed. - `.planning/openspec/id-registry.json` — the ID ledger. ## Failure states - Two different headings normalize to the same `key` (genuine duplicate requirement names in one domain) → stop and report; do not assign both the same ID and do not silently disambiguate. This is a real spec problem for `/opsx:update` to fix. - A spec file is not writable (read-only checkout, submodule) → emit the matrix with `ids_pending: true` and skip Phase A rather than failing the whole run. ## Guardrails - Writes into `openspec/` are restricted to inserting/leaving `` lines. Any other diff to a spec file is a bug in this skill, and the run should abort rather than produce it. - Never renumbers an existing ID. Never recycles a retired number. - Never fabricates a matrix cell — unresolved is `—`. - Never writes a `Status` column value of its own invention; status is `openspec-verify`'s output, copied. ## Idempotency Running twice against unchanged specs and unchanged code produces a byte-identical `id-registry.json`, byte-identical spec files, and a `TRACEABILITY.md` differing only in its generated-at timestamp. This is the property task 8's acceptance criterion checks by diffing the registry across two consecutive runs. ## Example ``` $ /openspec-traceability add-health-check-endpoint [openspec-traceability] domain HEALTH · 2 requirements, 3 scenarios REQ-HEALTH-001 (reused from registry) REQ-HEALTH-002 (reused from registry) SCN-HEALTH-003 (new — minted, comment injected into specs/health-check/spec.md:41) registry: 3 active, 0 removed → .planning/openspec/TRACEABILITY.md (5 rows, 0 unresolved cells) → .planning/openspec/EVIDENCE.json (+1 entry) ```