# traceable-reqs specification

This is the source of truth for the CLI behavior. The core behavior is the smallest useful implementation; optional capabilities are defined here without prescribing implementation order.

---

## Core contract

The tool reads a required `traceable-reqs.toml` manifest, scans configured files for requirement tags, reports whether every declared requirement has evidence for its required stages, and exposes the same trace facts through pyramid-friendly human and agent output plus structured JSON.

An implementation of the core behavior must:

1. Read `traceable-reqs.toml` from the current working directory.
2. Treat the current working directory as the repository root.
3. Treat manifest `[[requirements]].id` values as the authoritative set of declared requirement IDs.
4. Scan Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake (`.cmake`, `CMakeLists.txt`), YAML (`.yml`, `.yaml`), shell (`.sh`, `.bash`), PowerShell (`.ps1`, `.psm1`, `.psd1`), and TLA+ (`.tla`) files under configured roots, enumerated per [Scan enumeration](#scan-enumeration).
5. Parse tags of the form `[stage->REQ-ID]` with no internal spaces.
6. Recognize `doc`, `impl`, `unit`, and `int` as built-in stage keywords, plus custom stages declared in `[stages.<name>]`.
7. Accept requirement IDs matching `REQ-[A-Z0-9]+(-[A-Z0-9]+)*`.
8. Accept multiple tags on one scanned file line and record each valid tag independently.
9. Read manifest-declared custom stages from `[stages.<name>]` tables.
10. Apply tag parsing, completeness, finding, and JSON rules to declared custom stages in the same way as built-in stages.
11. Read manifest-declared `[[signoffs]]` entries as external URL evidence.
12. Evaluate completeness against the stages required by the manifest policy for each requirement.
13. Report findings with stable finding codes.
14. Provide structured JSON output for requirements, stage status, and findings.
15. Present human, agent, and machine output in pyramid form: summary first, requirement-level status next, detail available for drill-down.

The core mental model is: the manifest says what must exist; evidence proves where it is covered.

---

## Manifest

The manifest is required and authoritative. Tags never create requirement IDs.

Minimum shape:

```toml
[scan]
roots = ["src", "docs", "tests"]

[policy]
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-LOGIN-001"
title = "User can sign in"
type = "FR"
required_stages = ["doc", "impl", "unit", "int"]

[[requirements]]
id = "REQ-LOGOUT-001"
title = "User can end session"
```

### Manifest rules

- The tool reads `traceable-reqs.toml` from the current working directory.
- The current working directory is the repository root for all manifest-relative paths and repository-relative output paths.
- If `traceable-reqs.toml` is missing from the current working directory, emit `manifest_error`.
- A missing or unparsable manifest is a `manifest_error`.
- Invalid manifest configuration is a `manifest_error`, including duplicate requirement IDs, invalid required stages, and scan roots that are absolute, unreadable, nonexistent, or escape the current working directory.
- `[[requirements]].id` is required.
- `[[requirements]].id` values must be unique. Duplicate IDs are a `manifest_error`.
- `[[requirements]].title` is recommended for human output.
- `[[requirements]].type` is optional. If present, it classifies the requirement for filtering and grouping.
- If a `[types]` table is declared, every `[[requirements]].type` value must match one declared key.
- Type names must match `[A-Z][A-Z0-9_-]*`. Invalid or undeclared type values are a `manifest_error`.
- `[policy].required_stages` is optional. If present, it defines the default required stages for requirements that do not set their own `required_stages`.
- `[[requirements]].required_stages` is optional. If present, it overrides `[policy].required_stages` for that requirement.
- If neither `[policy].required_stages` nor `[[requirements]].required_stages` is present, the default required stages are `doc`, `impl`, and `unit`.
- Every required stage must be one of `doc`, `impl`, `unit`, or `int`, or a custom stage declared in `[stages.<name>]`.
- Unknown fields are ignored by the core behavior.
- `scan.roots` is optional; if omitted, the implementation must behave as if `roots = ["."]` were configured.
- Scan roots are interpreted relative to the current working directory and must not escape it after normalization.
- `scan.git` is optional and selects the enumeration strategy; see [Scan enumeration](#scan-enumeration).
- `[stages.<name>]` declares custom stages alongside the built-ins, and `[[signoffs]]` contributes external evidence. Both are required core capabilities defined under [Core extensibility](#core-extensibility).

Fields such as `priority`, custom include/exclude globs, and manifest format `version` are optional capabilities.

### Scan enumeration

`scan.roots` is an allowlist of directories. On its own it cannot express "everything except the generated trees": including `.` to pick up repository-root files (`CMakeLists.txt`, `README.md`) also drags in `build/`, `.venv/`, and every other ignored tree, while omitting `.` silently drops the root-level evidence and turns correctly tagged requirements into `missing_stage`. Enumeration therefore starts from what the version control system already knows is source.

- If the repository root is inside a git work tree, the candidate file set is the union of git's tracked files and its untracked-but-not-ignored files — the same set `git status` would show. Files ignored by `.gitignore`, `.git/info/exclude`, or the global exclude file are never scanned.
- Otherwise, the candidate file set is a recursive filesystem walk of each configured root.
- Either way, a candidate is scanned only if it lies under one of the configured `scan.roots`, its extension is recognized, and it is not excluded. Roots narrow the candidate set; they never widen it.
- Overlapping or nested roots (`roots = ["src", "."]`) scan each file exactly once. A file is not double-counted because two roots contain it.
- Symbolic links are not followed in either strategy. Git submodule directories are not descended into; a submodule's own manifest traces it.
- A candidate that git lists but that is absent from the working tree (staged deletion) is skipped, not a `scan_error`.

`scan.git` selects the strategy explicitly:

| Value | Meaning |
|-------|---------|
| `"auto"` | Default. Use git enumeration inside a git work tree, filesystem walk otherwise. |
| `"off"` | Always use the filesystem walk, even inside a git work tree. |

Any other value is a `manifest_error`. `"auto"` falls back to the filesystem walk whenever git is unavailable or the directory is not a work tree; the fallback is not an error.

```toml
[scan]
roots = ["."]
git = "auto"                 # the default — may be omitted
exclude = ["third_party/*"]  # for vendored trees that are tracked by design
```

Scan everything version control calls source; name what you exclude on top of that.

---

## Tags

Tags live in source comments or Markdown lines.

Canonical form:

```text
[impl->REQ-LOGIN-001]
```

Rules:

- `stage` must be one of `doc`, `impl`, `unit`, or `int`, or a name declared in the manifest's `[stages.<name>]` table.
- `REQ-ID` must match `REQ-[A-Z0-9]+(-[A-Z0-9]+)*`.
- The tag must not contain internal spaces.
- A scanned file line may contain multiple tags. Each valid tag is recorded independently.
- Duplicate identical tags on the same scanned file line are allowed and count as one evidence item.
- A tag for an ID not listed in the manifest is an `undeclared_id`.
- A tag-shaped token is bracketed text on a scanned line whose text before `->`, after trimming outer whitespace, has stage-name shape `[a-z][a-z0-9_-]*`, such as `[left->right]`.
- A tag-shaped token that does not exactly match `[stage->REQ-ID]` is a `parse_error`.

Line scanning rules:

- Each physical file line is one evidence location.
- Rust, C, C++, JavaScript, and TypeScript line comments and block comments are scanned by physical line. A line is in block-comment context if it lies between an open `/*` and its matching `*/`, inclusive of both.
- Python `#` comments and triple-quoted documentation strings (`"""..."""` and `'''...'''`) are scanned by physical line.
- Shell and PowerShell `#` line comments are scanned by physical line (text from the first `#` to end of line). PowerShell `<# ... #>` block comments are not scanned — place tags on `#` lines.
- TLA+ `\*` line comments are scanned by physical line (text from the first `\*` to end of line). TLA+ `(* ... *)` block comments are not scanned — place tags on `\*` lines.
- Markdown files are scanned one physical line at a time, including Markdown text and HTML comments.
- Tag-shaped text in non-comment source code is ignored for Rust, C, C++, JavaScript, TypeScript, and Python.
- Markdown diagram labels or prose such as `["rgb24 -> yuv420p"]` are not tag-shaped unless the bracketed text begins with a stage-name-shaped token before `->`.

Examples:

```rust
// [impl->REQ-LOGIN-001]
pub fn authenticate(email: &str, password: &str) -> Result<Session, AuthError> { ... }
```

```rust
// [impl->REQ-LOGIN-001] [impl->REQ-AUDIT-001]
pub fn authenticate_with_audit(email: &str, password: &str) -> Result<Session, AuthError> { ... }
```

```python
# [unit->REQ-LOGIN-001]
def test_rejects_empty_password(): ...
```

```markdown
<!-- [doc->REQ-LOGIN-001] -->
# Login
```

```cpp
// [int->REQ-LOGIN-001]
TEST_F(LoginE2E, FullFlow) { ... }
```

---

## Trace completeness

A declared requirement is complete when at least one piece of valid evidence covers each stage required for that requirement. Evidence may come from a scanned tag or from a manifest-declared `[[signoffs]]` entry pointing at an external URL.

| Stage | Meaning |
|-------|---------|
| `doc` | Requirement is documented in prose. |
| `impl` | Requirement has production implementation evidence. |
| `unit` | Requirement has unit test evidence. |
| `int` | Requirement has integration or end-to-end test evidence. |

The four stages above are the built-in vocabulary; manifests may extend that vocabulary with declared custom stages. Completeness policy is manifest-driven. A requirement can require only `doc`, or it can require all four built-in stages plus a custom one, depending on its `required_stages` value or the manifest-level default.

Duplicate coverage is allowed. The same stage for the same requirement may appear in multiple files or signoffs; one valid evidence item is enough to satisfy the stage.

---

## Tag placement enforcement

Completeness asks whether a requirement has evidence *somewhere*. Placement asks the second question: whether each tag is attached to the evidence it claims. The two are independent — a tag that has drifted off its test still completes the stage, so a checker that only counts tags reports green while the trace decays.

`check` enforces placement by default.

### The rule

A tag is **attached** to the item it sits inside, or to the item that starts directly below it across only blank, comment, attribute, and decorator lines. Outside any enclosing item, anything else between a tag and an item — a statement, a declaration, an import — is what the tag attaches to instead. This is the same attach-above convention the `[stage->REQ-ID]` examples above use; enforcement makes it checkable rather than a matter of discipline.

**Inside an enclosing item, the enclosing item is a fallback anchor.** A bare value declaration directly below the tag never steals the anchor from a non-declaration item the tag is already inside: a tag written inside a test body directly above a local `const` still attaches to the test. The consequence is deliberate — tags written inside evidence bodies stay green — and it has a cost stated under [What is not judged](#what-is-not-judged): the same interposed-declaration drift that is caught at top level is not caught when it happens inside a containing item, because the container absorbs it.

The attached item must be able to carry the tag's stage:

| Stage | Attaches to |
|-------|-------------|
| `doc` | Any item, any documentation context (a file's leading comment header, a Markdown document). |
| `impl` | Any declaration or statement — function, type, module, value declaration, command. |
| `unit`, `int` | An item with a body: a function, type, module, block, or statement. A bare value declaration is not test evidence. |
| declared custom stages | As `doc`: the spec assigns their evidence no shape, so their project decides. |

A tag in a file's leading comment header — before the first line of code — is a **module banner**. It is documentation context, so `doc` and custom stages accept it; `impl`, `unit`, and `int` reject it, because a banner claims nothing in particular. Projects that tag whole modules deliberately can accept banners with `[placement].module_banner = "accept"`.

Markdown has no items. A `doc` or custom-stage tag in Markdown is placed when the file carries documentation content; a code-stage tag there is misplaced, since prose cannot be implementation or test evidence.

A violation emits `misplaced_tag`, one finding per tag, carrying `requirementId`, `stage`, `path`, `line`, `criterion = "tag-placement"`, and a message naming the reason and the item the tag landed on. Severity is the requirement's priority, so `--fail-on` applies as it does to other findings. Like `undeclared_id`, placement findings are file hygiene: `--group` and `--ids` do not hide them.

### What is not judged

- **Languages without a grammar.** Placement needs a parse. TLA+, files mapped through `[scan.extensions]`, and any language the implementation does not parse are skipped: no finding, the same silence they get for `enclosingSymbol`.
- **Parses that recovered from errors.** A recovered parse drops constructs the grammar could not follow, and a dropped construct is indistinguishable from a tag attached to nothing. Such a file is skipped rather than judged. So that per-file skipping cannot make an unenforced repository read as a clean one, `check`'s human output notes how many tag-carrying files were not placement-judged; the JSON schema is unchanged.
- **Drift inside a containing item.** The enclosing-item fallback means a tag inside a `mod tests` block (or any container) whose nearest below-item is an interposed declaration still anchors to the container and passes — the exact drift shape that is a finding at top level. A tag displaced *within* the container it names is indistinguishable from a tag deliberately placed on the container, so the rule does not guess. The displacement is caught where a tag has no enclosing item to fall back on, which is where the convention puts evidence tags in the first place.
- **Whether the code is really the evidence.** That `[impl->REQ-LOGIN-001]` sits on a function is structural and checkable; that the function implements login is not. That remains the agent-evaluated [Tag placement review](#tag-placement-review).
- **Which functions are tests.** `unit` and `int` require an item with a body, not a proven test. Test-ness is a per-framework judgement — `#[test]`, `def test_*`, `TEST_F`, `test('...')` — and demanding it would reject correct tags in every framework the tool has not been taught.

### Configuration

```toml
[placement]
enforce = "on"            # "on" (default) | "off"
module_banner = "reject"   # "reject" (default) | "accept"
```

Omitting the table enforces the rule with banners rejected. `enforce = "off"` restores coverage-only checking for a project that is not ready to fix its tags. Any other value for either key is a `manifest_error`.

---

## Core extensibility

Extensibility is part of the core contract. The built-in vocabulary remains
small, while the manifest can declare the lifecycle stages and external
evidence that a real project needs.

### Custom stages

Projects can extend the built-in `doc`, `impl`, `unit`, `int` vocabulary by declaring named stages in the manifest. The most common motivation is a stage that does not fit any of the built-ins — for example QC signoff or security review.

```toml
[stages.qc]
description = "QC engineer signoff"
```

Rules:

- `[stages.<name>]` is a TOML table keyed by stage name. The name must match `[a-z][a-z0-9_-]*` and must not collide with a built-in.
- `description` is an optional human-readable string.
- A declared custom stage is valid wherever a built-in stage is valid: in `[policy].required_stages`, in `[[requirements]].required_stages`, in scanned `[stage->REQ-ID]` tags, and in `[[signoffs]].stage`.
- An undeclared custom stage in any of those positions is a `manifest_error` in the manifest or a `parse_error` in a tag.
- Stage findings (`missing_stage`) and JSON output (`requirements[].stages`) extend uniformly to custom stages. The four built-in stage keys remain present in JSON regardless of whether they are required.

### External signoff evidence

Some stages are not best satisfied by a tag in source — for example, a QC signoff that lives in a GitHub issue comment. The manifest may declare external evidence directly:

```toml
[[signoffs]]
requirement = "REQ-LOGIN-001"
stage = "qc"
url = "https://github.com/org/repo/issues/42#issuecomment-123"
```

Rules:

- `requirement` must reference a declared `[[requirements]].id`. An unknown id is a `manifest_error`.
- `stage` must be a built-in stage or a declared custom stage. An unknown stage is a `manifest_error`.
- `url` must be a non-empty string. The implementation does not fetch or validate the URL — the signer and date live at the linked resource, so the trace tool only records the pointer.
- A signoff contributes one piece of external evidence to the named (`requirement`, `stage`) pair. Completeness rules treat it identically to a scanned tag.
- Tag evidence and signoff evidence for the same (`requirement`, `stage`) pair coexist; neither replaces the other.
- External evidence appears in JSON as `{ "url": "..." }` inside `requirements[].stages[stage].evidence`.
- Human trace output renders the signoff URL as evidence detail.

Date and signer are intentionally not captured in the manifest because the signoff URL is the source of truth for both; storing them in the manifest would invite drift.

---

## Commands

Core commands:

```text
traceable-reqs list
traceable-reqs list --missing impl
traceable-reqs list --type FR
traceable-reqs trace REQ-LOGIN-001
traceable-reqs check
traceable-reqs check --type FR
```

Every command accepts `--json` to emit the structured document defined in [Structured output](#structured-output) instead of human-readable text.

Command behavior:

- `list` shows all declared requirements and their stage status.
- `list --missing <stage>` shows requirements missing the requested stage. The flag may be repeated and uses OR semantics.
- `list --type <TYPE>` shows requirements with the requested type. The flag may be repeated and uses OR semantics.
- `trace <REQ-ID>` shows one requirement end to end.
- `check` exits non-zero if any finding exists.
- `check --type <TYPE>` scopes requirement completeness findings and exit status to the requested type. Operational findings such as parse and scan errors still surface.

Tags whose IDs are not declared in the manifest surface as `undeclared_id` findings and appear in the findings section of any command that emits findings.

Command results go to stdout, including findings returned by successful query commands. Diagnostics and operational failures go to stderr.
Operational failures should use the stable finding code in their diagnostic when one applies, such as `manifest_error` or `scan_error`.

Exit behavior:

- `check` exits non-zero if any finding exists.
- `list` and `trace` exit zero when they successfully produce trace facts, even if those facts include findings.
- Every command exits non-zero for operational failures such as invalid arguments, manifest errors, or unreadable scanned files.
- `trace <REQ-ID>` exits non-zero when the requested ID is not declared in the manifest.
- Exact numeric exit codes are future work; 0 means success and non-zero means failure.

---

## Pyramid reporting

Output must be pyramid-friendly: the same trace facts are available at multiple levels of detail so humans, CI, and coding agents can start broad and drill down only where needed.

For agents, pyramid reporting is part of the contract. It lets an agent inspect many requirements at low cost, choose the IDs or findings that need attention, and request deeper evidence only for those cases.

Required levels:

1. **Summary:** counts and overall status, such as number of requirements, complete requirements, incomplete requirements, and findings.
2. **Requirement status:** one row or object per requirement with required stages and stage completion for `doc`, `impl`, `unit`, and `int`.
3. **Findings:** stable finding codes with enough context to identify the affected requirement, stage, path, and line when available.
4. **Evidence detail:** for drill-down commands or nested JSON detail, the files and lines that satisfy each stage when available.

Human output should default to compact summaries for broad commands such as `list` and use `trace <REQ-ID>` for drill-down. JSON output should preserve the same pyramid shape with top-level summary, requirement-level status, findings, and nested evidence detail so agents can consume the data incrementally.

---

## Structured output

Machine-readable output lets CI, agents, and downstream tools consume pyramid reporting data without scraping human text.

The `--json` flag on any command emits one JSON document with:

- top-level `schemaVersion`
- summary counts
- declared requirements
- required stages and stage status for each requirement
- findings using the stable finding codes below
- evidence detail

Contractual shape:

```json
{
  "schemaVersion": 1,
  "summary": {
    "requirementCount": 2,
    "completeCount": 1,
    "incompleteCount": 1,
    "findingCount": 1
  },
  "requirements": [
    {
      "id": "REQ-LOGIN-001",
      "title": "User can sign in",
      "type": "FR",
      "requiredStages": ["doc", "impl", "unit", "int"],
      "stages": {
        "doc": { "complete": true, "evidence": [{ "path": "docs/login.md", "line": 1 }] },
        "impl": { "complete": true, "evidence": [{ "path": "src/auth.rs", "line": 12 }] },
        "unit": { "complete": true, "evidence": [{ "path": "tests/auth_test.py", "line": 4 }] },
        "int": { "complete": true, "evidence": [{ "path": "tests/login_e2e.cpp", "line": 8 }] }
      }
    },
    {
      "id": "REQ-LOGOUT-001",
      "title": "User can end session",
      "requiredStages": ["doc", "impl", "unit"],
      "stages": {
        "doc": { "complete": true, "evidence": [{ "path": "docs/logout.md", "line": 1 }] },
        "impl": { "complete": false, "evidence": [] },
        "unit": { "complete": true, "evidence": [{ "path": "tests/auth_test.py", "line": 18 }] },
        "int": { "complete": false, "evidence": [] }
      }
    }
  ],
  "findings": [
    {
      "code": "missing_stage",
      "requirementId": "REQ-LOGOUT-001",
      "stage": "impl",
      "message": "Requirement is missing implementation evidence"
    }
  ]
}
```

JSON rules:

- `schemaVersion`, `summary`, `requirements`, and `findings` are required top-level fields.
- `requirements[].stages` always contains `doc`, `impl`, `unit`, and `int`. When the manifest declares custom stages, those keys appear too.
- `requirements[].type` is present when the manifest requirement declares a type.
- `summary.countsByType` may appear when at least one requirement declares a type.
- `requirements[].requiredStages` contains the stages that determine completeness for that requirement.
- Each stage object contains `complete` and `evidence`.
- `complete` is `true` for non-required stages only when evidence exists; missing non-required stages do not emit `missing_stage`.
- Evidence is a discriminated union by key presence:
  - **File evidence** has `path` (repository-relative, `/` separators) and `line` (1-based).
  - **External evidence** has `url` (a non-empty string from a manifest `[[signoffs]]` entry).
  Consumers should dispatch on which key is present; the two shapes do not co-occur on the same evidence object.
- Findings always include `code` and `message`. They include `path`, `line`, `requirementId`, and `stage` when applicable.

---

## Findings

Findings use stable `code` values so humans, CI, and agents do not scrape prose.

| Code | Meaning |
|------|---------|
| `missing_stage` | A declared requirement is missing a stage required by its manifest policy. |
| `undeclared_id` | A scanned tag references an ID outside the manifest-declared set. |
| `parse_error` | A tag-shaped token cannot be accepted. |
| `misplaced_tag` | A tag is not attached to an item that can carry its stage. See [Tag placement enforcement](#tag-placement-enforcement). |
| `manifest_error` | The manifest is missing, cannot be parsed, lacks required fields, or contains invalid configuration. |
| `scan_error` | A scanned file cannot be read. |

Findings should include enough context for humans to fix the issue: file path, line number when available, requirement ID when applicable, code, and a short message.

---

## Acceptance examples

1. **Complete trace:** The manifest lists `REQ-LOGIN-001` with required stages `doc`, `impl`, `unit`, and `int`; scanned files contain `[doc->REQ-LOGIN-001]`, `[impl->REQ-LOGIN-001]`, `[unit->REQ-LOGIN-001]`, and `[int->REQ-LOGIN-001]`. The requirement is complete.
2. **Missing implementation:** The manifest lists `REQ-LOGOUT-001` with required stage `impl`; scanned files contain `doc`, `unit`, and `int` tags but no `impl` tag. Emit `missing_stage` for `impl`.
3. **Typo:** The manifest lists `REQ-LOGIN-001`; scanned code contains `[impl->REQ-LOGNI-001]`. Emit `undeclared_id` for `REQ-LOGNI-001`.
4. **Unknown stage:** Scanned code contains `[test->REQ-LOGIN-001]`. Emit `parse_error`.
5. **Multiple tags:** Scanned code contains `[impl->REQ-LOGIN-001] [impl->REQ-AUDIT-001]` on one scanned file line. Record both tags independently.
6. **Missing manifest:** `traceable-reqs.toml` is absent. Emit `manifest_error`; `check` exits non-zero.
7. **Custom stage tag:** The manifest declares `[stages.qc]` and lists `REQ-LOGIN-001` with required stages including `qc`; scanned code contains `[qc->REQ-LOGIN-001]` in a comment. The `qc` stage is complete from that tag. A bare `[qc->REQ-LOGIN-001]` tag without a declared `qc` stage is a `parse_error`.
8. **Signoff URL evidence:** The manifest declares `[stages.qc]` and a `[[signoffs]]` row with `requirement = "REQ-LOGIN-001"`, `stage = "qc"`, `url = "https://github.com/org/repo/issues/42#issuecomment-123"`. No `qc` tag is found in source. The `qc` stage is still complete because the signoff contributes external evidence; JSON output contains `{ "url": "https://..." }` in the `qc` evidence array.
9. **Priority-scoped check:** The manifest lists `REQ-NICE-001` with `priority = "could"` and a missing required stage. Default `check` (or `check --fail-on must`) prints the `missing_stage` finding with `severity: "could"` but exits zero. `check --fail-on could` exits non-zero on the same input.
10. **Misplaced tag:** A file contains `[unit->REQ-AUDIT-001]`, then `RETRIES = 2`, then the test that covers the requirement. The tag attaches to the constant, which cannot be test evidence. Emit `misplaced_tag` at the tag's line with `stage = "unit"` and `criterion = "tag-placement"`; `check` exits non-zero. The `unit` stage still reports complete — coverage and placement are independent, which is why the finding is the only signal.

---

## Optional capabilities

These capabilities are part of the broader product definition, but the spec does not prescribe when or in what order an implementor must add them. Custom stages and external signoff evidence are not optional; they are part of [Core extensibility](#core-extensibility).

### Richer manifest

Additional manifest fields can make scanning and policy more precise.

- `scan.include` and `scan.exclude` globs.
- Stricter validation for known manifest fields.
- Top-level manifest format `version`.

#### `scan.exclude` (defined)

`scan.exclude` is an optional list of glob patterns. A file whose repository-relative path (with `/` separators) matches any pattern is skipped entirely during scanning — it contributes no evidence and no `parse_error`. Patterns support `*`, `**`, `?`, and `[...]` character classes; matching is not anchored to directory boundaries (`docs/plans/*` also matches nested files). An absolute pattern or a pattern that fails to parse as a glob is a `manifest_error`. Exclusion is applied after root expansion and wins over roots. Rationale: planning and process documents often contain tag-shaped illustrative text that is not evidence.

A directory whose repository-relative path matches an exclude pattern is not descended into during the filesystem walk. Nothing inside it is read, so an excluded tree contributes no `scan_error` either — matching the guarantee for excluded files, and keeping the walk off vendored trees rather than merely discarding their contents.

```toml
[scan]
roots = ["src", "docs"]
exclude = ["docs/plans/*", "docs/process/*"]
```

Together with [Scan enumeration](#scan-enumeration), `exclude` covers the case git cannot: trees that are vendored but tracked on purpose, such as `third_party/`.

`scan.include` remains undefined.

#### `scan.extensions` (defined)

`[scan.extensions]` is an optional TOML table mapping extra file extensions onto a named comment-style family. It lets projects trace requirement-bearing artifacts written in DSLs the scanner does not know natively (Graphviz `.dot` workflows, Kotlin sources, GDScript, and so on) without per-language upstream changes. Files whose extension is not built in and not mapped remain skipped.

```toml
[scan]
roots = ["src", "workflows"]

[scan.extensions]
".dot" = "c_like"
".kt"  = "c_like"
".gd"  = "hash"
```

Family names select one of the existing comment-region scanners:

| Family | Scanned regions | Built-in analogue |
|--------|-----------------|-------------------|
| `c_like` | `//` line comments and `/* ... */` block comments, by physical line | Rust, C/C++, JS/TS |
| `hash` | text from the first `#` to end of line | CMake, YAML, shell, PowerShell |
| `python` | `#` comments plus triple-quoted documentation strings | Python |
| `markdown` | every physical line | Markdown |
| `tla` | text from the first `\*` to end of line | TLA+ |

Rules:

- Keys must start with `.` and name a single extension (`.kt`, not `kt` or `.tar.gz`). Matching is ASCII-case-insensitive.
- The family value must be one of the names above. An unknown family is a `manifest_error` whose message lists the valid families.
- A key that remaps a built-in extension is a `manifest_error` — built-in scanning cannot be overridden.
- Mapped files behave exactly like the family's built-in languages: tags count only inside the family's comment regions, and tag-shaped junk in those regions is a `parse_error`.

Each `[[requirements]]` entry may carry an optional `type` field for project-level classification, such as `FR`, `NFR`, `CON`, `SAFETY`, or `SECURITY`.

```toml
[types]
FR = { description = "Functional requirement" }
NFR = { description = "Non-functional requirement" }

[[requirements]]
id = "REQ-LOGIN-001"
title = "User can sign in"
type = "FR"
```

Rules:

- `type` is optional and omitted from JSON for untyped requirements.
- Type names must match `[A-Z][A-Z0-9_-]*`.
- If `[types]` is absent, any valid type name is accepted.
- If `[types]` is present, every `[[requirements]].type` value must be one of the declared keys. Unknown type values are a `manifest_error`.
- `list --type <TYPE>` and `check --type <TYPE>` filter requirements by type. Repeating the flag uses OR semantics.
- Human `list` output groups typed requirements by type. JSON requirements include `"type": "<TYPE>"`, and summaries may include `countsByType`.

### Requirement groups

`check` and `list` normally evaluate every declared requirement. Named groups let a caller scope them to a subset — a CI job gating one milestone, or a consumer checking only the requirements bound to a phase — without parsing full JSON output client-side.

Manifest:

```toml
[[groups]]
name = "phase-1"
requirements = ["REQ-PKG-01", "REQ-PKG-02", "REQ-BOOT-01"]

[[groups]]
name = "milestone-v2"
requirements = ["REQ-PKG-01", "REQ-LOGIN-001"]
```

Rules:

- `name` must match `[a-z][a-z0-9_-]*` and be unique across the manifest. Violations are a `manifest_error`.
- `requirements` lists declared `[[requirements]].id` values. A reference to an undeclared id is a `manifest_error`.
- The same requirement may appear in multiple groups. An empty `requirements = []` is valid (vacuously satisfied).

CLI, accepted on `check` and `list` (`trace <REQ-ID>` is already single-ID scoped):

```text
traceable-reqs check --group phase-1
traceable-reqs check --group phase-1 --group milestone-v2     # union
traceable-reqs check --ids REQ-PKG-01,REQ-BOOT-01
traceable-reqs check --group phase-1 --ids REQ-EXTRA-01       # union
traceable-reqs list  --group phase-1 --missing impl
```

Semantics:

- `--group` references a manifest group by exact name. `--ids` takes comma-separated declared requirement ids. Both are repeatable; the effective scope is the union of all groups and ids given.
- An unknown group name or undeclared id is an operational error (non-zero exit with a usage message on stderr), not a `manifest_error`.
- When a filter is active, `missing_stage` findings are emitted only for requirements in the scope, and `check`'s exit code considers only those. Tag-level findings (`undeclared_id`, `parse_error`, `scan_error`) are file/manifest hygiene, independent of scope, and remain visible.
- An empty effective scope exits zero with zero `missing_stage` findings.

JSON (additive within `schemaVersion = 1`): when a filter is active, `requirements[]` contains only the scoped requirements, `summary` counts reflect the scoped set, and a top-level `filter` block echoes the resolved scope:

```json
"filter": {
  "groups": ["phase-1"],
  "ids": ["REQ-EXTRA-01"],
  "effectiveIds": ["REQ-BOOT-01", "REQ-EXTRA-01", "REQ-PKG-01", "REQ-PKG-02"]
}
```

`effectiveIds` is the resolved union, sorted and deduplicated, so consumers can confirm what was scoped without re-resolving groups themselves.

Out of scope: wildcard or prefix matching on group names, and any discovery of group membership from external planners — membership is declared in the manifest.

### Priority gates (MoSCoW)

Each `[[requirements]]` entry may carry an optional `priority` field with the value `must`, `should`, or `could`. When omitted, the requirement defaults to `must`, which preserves the original "every finding blocks `check`" behaviour.

```toml
[[requirements]]
id = "REQ-X-001"
title = "Nice-to-have export format"
priority = "could"
```

Semantics:

- Each finding carries a `severity` field equal to the priority of the requirement it concerns. `missing_stage` and `undeclared_id` for declared requirements inherit the requirement's priority. `parse_error`, `scan_error`, and `manifest_error` are always `must` because they signal the tool cannot do its job.
- `check --fail-on <LEVEL>` selects the lowest severity that flips the exit code. Default is `must`. Findings below the threshold still appear in the output but do not change `check`'s exit code.
  - `--fail-on must` (default): only `must` findings fail; `should`/`could` are warnings.
  - `--fail-on should`: `must` and `should` findings fail.
  - `--fail-on could`: any finding fails (the strictest mode; matches the pre-priority behaviour for manifests with mixed priorities).
- Invalid `--fail-on` values are an operational error (`check` exits non-zero with a usage message).
- The `severity` field is added to JSON `findings[]` objects; this is additive within `schemaVersion = 1`.

### Requirement quality lint

Implementations may provide a `lint` command that evaluates each requirement's `title` against a quality rubric. A subset of rubric items may be evaluated deterministically by the implementation and emitted as findings with `code = "requirement_quality"`. The remainder may be deferred to a calling agent by emitting a fixed-shape markdown prompt on the same output stream.

- `lint` is **separate from `check`** — `check` remains deterministic and offline, while `lint` may emit instructions intended for an agent caller.
- The `requirement_quality` finding code carries `requirementId`, `criterion`, `message`, and an optional `suggestedRevision`. The `criterion` field identifies which rubric item flagged the title.
- `lint` exits non-zero when at least one deterministic finding exists. Agent-evaluated rubric items return concerns out of band and do not affect the tool's exit code.
- The set of deterministic rules and the agent rubric is implementation-defined; what is stable is the finding code, the field shape, and the separation from `check`.

### Scoping `lint` and `review` to a commit range

Both `lint` and `review` accept an optional `--changed-since <REF>` flag that scopes their work to changes since the given git ref. This is intended for PR-time use: rather than re-evaluate every requirement and tag on every commit, only the items affected by the diff are sent through.

- `lint --changed-since <REF>`: filters the manifest to requirements whose `id` was added or whose `title` changed between `<REF>` and the working tree. Both the deterministic findings and the agent prompt are restricted to that set.
- `review --changed-since <REF>`: filters file evidence to `(path, line)` pairs that appear in the post-image of the diff against `<REF>`. External (signoff URL) evidence is excluded as before.
- The implementation shells out to `git show` and `git diff --unified=0`. If the working directory is not a git repository or `<REF>` cannot be resolved, the command exits non-zero with a `git_diff` error.
- An empty scope (no requirements changed; no source lines touched) is not an error — the command exits zero with empty findings and a prompt that explicitly enumerates zero work items.

### Tag placement review

The `review` command offers an agent-evaluated review of where tags are placed in source. The deterministic CLI cannot tell whether `[impl->REQ-LOGIN-001]` actually sits on code that implements login — an agent reading the surrounding source can. The structural half of the question — whether the tag is attached to an item that can carry its stage at all — is deterministic and belongs to `check`; see [Tag placement enforcement](#tag-placement-enforcement). This section is about the semantic half that survives it.

`review` mirrors `lint`'s shape: it emits a fixed-shape markdown prompt for a calling agent to evaluate, listing one row per file-evidence tag the scanner found. The prompt deliberately does NOT bundle source snippets — the agent uses its own file-reading tools to fetch context. This keeps prompt size bounded by the number of tags rather than by repository size.

- The deterministic side of `review` is empty today; the entire output is the agent prompt plus zero deterministic findings. Future versions may add deterministic checks.
- Findings emitted by the agent reuse the `requirement_quality` finding code with `criterion = "tag-placement"`. They include `requirementId`, `stage`, `path`, `line`, `message`, and an optional `suggestedAction` of `remove`, `move`, `fix-stage`, `rewrite-code`, or `null`.
- External (signoff URL) evidence is not enumerated for review — there is no source location to inspect. Verifying the URL is a separate concern out of scope for this command.
- `review` exits zero when no deterministic findings exist; agent-evaluated concerns are returned out of band like `lint`.

### Change compilation

A draft `change compile` command — compiling declared requirement IDs into
a deterministic Change Contract for consumption by coding agents — is
specified separately in [SPEC-CHANGE.md](./SPEC-CHANGE.md), with its own
golden fixture at `example/expected-change-compile.json`. It reuses this
spec's scanner, manifest rules, and finding codes unchanged and introduces
no new finding codes. It is not part of core; the kernel contract in this
document is unaffected by it.

### Stability promises

Compatibility promises become relevant once external tools depend on the CLI or export formats.

- Additive JSON fields are allowed within a schema version.
- Removing fields, renaming fields, or changing field meaning requires a `schemaVersion` bump.
- Finding `code` values should not be renamed without a compatibility note.
- Stable CLI commands and flag semantics.
- Stable manifest format.
- Stable tag grammar for the canonical form.
- Stable JSON export schema.
- Stable finding taxonomy.
- Clear migration notes for future breaking changes.
