# traceable-reqs change specification

Status: **draft**. This document specifies the first command of the `change`
subsystem, `change compile`. It is a separate specification so the core
kernel — [SPEC.md](./SPEC.md), its finding codes, and the
`example/expected.json` fixture — remains untouched and independently
verifiable.

If this document and SPEC.md disagree about kernel behavior (tag grammar,
finding codes, manifest rules, scanning), SPEC.md wins.

---

## Purpose

`change compile` turns a set of declared requirement IDs into a **Change
Contract**: a single JSON document containing everything the trace graph
knows that is relevant to changing those requirements — current evidence,
co-located requirements that the change could disturb, the proof
obligations the change must leave satisfied, and the minimal set of files
an implementing agent needs to read.

The intended consumer is a coding agent (or the human directing one).
Instead of exploring the repository, the agent starts from the contract.

Natural-language task resolution ("allow capture nodes to reconnect" →
`REQ-CAPTURE-004`) is **explicitly out of scope** for this tool. Mapping
prose to requirement IDs requires judgment; it belongs to a calling agent
or an interactive picker built on `list --json`. This command takes
requirement IDs only.

---

## The determinism contract

Everything in this document — and every future command added to the
`change` subsystem — is bound by the following rules. They are the
admission test for what may live in this tool at all.

1. **Pure function.** Output is a function of exactly three inputs: the
   manifest, the scanned file tree, and the command-line arguments.
   Nothing else may influence the output.
2. **Forbidden inputs.** No network access, no clocks or timestamps, no
   randomness, no environment variables, no locale sensitivity, no file
   modification times, no absolute paths, and no model or agent calls.
   A machine with no network connection must produce the same document
   as one with.
3. **Fully specified ordering.** Every array in the output document has
   a defined sort order (see [JSON rules](#json-rules)). Two runs of the
   same build on identical inputs must produce byte-identical output.
   Two independent conforming implementations must produce documents
   that are byte-identical after normalizing `message` text, which
   remains illustrative as in the kernel.
4. **Judgment lives outside.** Anything requiring interpretation —
   resolving prose to requirement IDs, ranking relevance, judging
   whether behavior was preserved, generating mutations — is not core
   and must not be added to this subsystem. The kernel's `lint` and
   `review` commands are the precedent: deterministic skeleton in the
   tool, judgment deferred to a calling agent.
5. **Inference is labeled, structurally.** The contract document
   reserves an `annotations` array for agent layers to append findings
   of their own. Core always emits it empty. Every annotation an agent
   appends must carry `"basis": "inference"`. Core-emitted facts never
   carry a `basis` field — absence of the field means the entry is a
   graph fact. A consumer can therefore distinguish proven from judged
   without trusting the producer's prose.
6. **Deterministic failure.** Error behavior is specified like success
   behavior: the same bad input produces the same diagnostic and exit
   status every time.
7. **The manifest is the boundary.** If a desired contract section
   cannot be derived from the manifest plus scanned tags by a rule
   written in this document, the fix is to extend the manifest (a
   declared, diffable input) — never to consult an outside source.

---

## Command

```text
traceable-reqs change compile <REQ-ID> [<REQ-ID>...]
traceable-reqs change compile <REQ-ID> --json
```

- Takes one or more requirement IDs. Every ID must be declared in the
  manifest; an undeclared ID is an operational error and the command
  exits non-zero without emitting a contract (same rule as
  `trace <REQ-ID>`).
- Duplicate IDs on the command line are deduplicated.
- `--json` emits the Change Contract document defined below. Human
  output presents the same pyramid: summary first, then per-target
  status, obligations, related requirements, context files, findings.
- The scan is the same scan `check` performs: same roots, same tag
  grammar, same comment rules. This command adds **no** scanning
  behavior of its own.

Exit behavior:

- Exits zero when a contract is produced, even if obligations are
  unsatisfied or findings exist — `change compile` is a query, not a
  gate. `check` remains the gate.
- Exits non-zero for operational failures: unknown target ID,
  `manifest_error`, unreadable scanned files surfacing as `scan_error`.

---

## Derivation rules

Each contract section is computed by a fixed rule over the manifest and
the scanned tags. Definitions:

- A **target** is a requirement named on the command line.
- **File evidence of a requirement** is every `(path, line, stage)`
  triple the scanner recorded for that requirement's tags. External
  signoff evidence contributes `(url, stage)` pairs.

Sections:

1. **`targets`** — one object per target, identical in shape to
   `requirements[]` entries in the kernel's `check --json` output
   (id, title, optional type/priority, `requiredStages`, full `stages`
   map with evidence).
2. **`obligations`** — one entry per (target, required stage) pair.
   An obligation is `satisfied` when that stage is complete under the
   kernel's completeness rules, and carries the stage's current
   evidence. The contract's meaning: after the change, every listed
   obligation must hold again — satisfied ones must be preserved or
   re-evidenced, unsatisfied ones are the change's job to close if the
   change claims that requirement.
3. **`related`** — every non-target declared requirement that has file
   evidence in at least one file where a target also has file evidence,
   with the sorted list of shared files. This is the deterministic core
   of impact: these are the requirements a careless edit to the context
   files is most likely to disturb. No call-graph or semantic analysis
   is implied — co-location in a file is the rule, exactly.
4. **`contextFiles`** — every file containing file evidence of at least
   one target. Each entry lists **all** declared-requirement tags in
   that file (targets and non-targets alike), so the implementing agent
   sees every obligation co-located with the code it is about to edit.
   External signoff URLs are not context files; they appear only as
   evidence inside `targets` and `obligations`.
5. **`findings`** — two categories, nothing else:
   - Scan-health findings from the whole scan: `parse_error`,
     `undeclared_id`, `scan_error`. These appear even when they touch
     files unrelated to the targets, because they mean the evidence
     graph the contract was compiled from may be incomplete — a
     contract must not present itself as reliable when the scan was
     not.
   - `missing_stage` findings **for targets only**. A non-target
     requirement's missing stage is `check`'s business, not this
     contract's.
6. **`annotations`** — always `[]` from core. Reserved for agent
   layers per determinism rule 5.

---

## The Change Contract document

```json
{
  "schemaVersion": 1,
  "kind": "changeContract",
  "summary": {
    "targetCount": 1,
    "relatedCount": 2,
    "obligationCount": 5,
    "unsatisfiedObligationCount": 0,
    "contextFileCount": 4,
    "findingCount": 2
  },
  "targets": [
    {
      "id": "REQ-LOGIN-001",
      "title": "User can sign in",
      "requiredStages": ["doc", "impl", "unit", "int", "qc"],
      "stages": {
        "doc": { "complete": true, "evidence": [{ "path": "docs/login.md", "line": 1 }] },
        "impl": { "complete": true, "evidence": [{ "path": "src/auth.rs", "line": 4 }] },
        "unit": { "complete": true, "evidence": [{ "path": "tests/auth_test.py", "line": 1 }] },
        "int": { "complete": true, "evidence": [{ "path": "tests/login_e2e.cpp", "line": 2 }] },
        "qc": { "complete": true, "evidence": [{ "url": "https://github.com/example/repo/issues/42#issuecomment-1234567890" }] }
      }
    }
  ],
  "obligations": [
    { "requirementId": "REQ-LOGIN-001", "stage": "doc", "satisfied": true,
      "evidence": [{ "path": "docs/login.md", "line": 1 }] },
    { "requirementId": "REQ-LOGIN-001", "stage": "impl", "satisfied": true,
      "evidence": [{ "path": "src/auth.rs", "line": 4 }] },
    { "requirementId": "REQ-LOGIN-001", "stage": "unit", "satisfied": true,
      "evidence": [{ "path": "tests/auth_test.py", "line": 1 }] },
    { "requirementId": "REQ-LOGIN-001", "stage": "int", "satisfied": true,
      "evidence": [{ "path": "tests/login_e2e.cpp", "line": 2 }] },
    { "requirementId": "REQ-LOGIN-001", "stage": "qc", "satisfied": true,
      "evidence": [{ "url": "https://github.com/example/repo/issues/42#issuecomment-1234567890" }] }
  ],
  "related": [
    { "id": "REQ-AUDIT-001", "title": "Authentication actions are logged",
      "sharedFiles": ["src/auth.rs", "tests/auth_test.py"] },
    { "id": "REQ-LOGOUT-001", "title": "User can end session",
      "sharedFiles": ["tests/auth_test.py"] }
  ],
  "contextFiles": [
    { "path": "docs/login.md", "tags": [
      { "requirementId": "REQ-LOGIN-001", "stage": "doc", "line": 1 } ] },
    { "path": "src/auth.rs", "tags": [
      { "requirementId": "REQ-AUDIT-001", "stage": "impl", "line": 4 },
      { "requirementId": "REQ-LOGIN-001", "stage": "impl", "line": 4 } ] },
    { "path": "tests/auth_test.py", "tags": [
      { "requirementId": "REQ-LOGIN-001", "stage": "unit", "line": 1 },
      { "requirementId": "REQ-LOGOUT-001", "stage": "unit", "line": 6 },
      { "requirementId": "REQ-AUDIT-001", "stage": "unit", "line": 11 } ] },
    { "path": "tests/login_e2e.cpp", "tags": [
      { "requirementId": "REQ-LOGIN-001", "stage": "int", "line": 2 } ] }
  ],
  "annotations": [],
  "findings": [
    { "code": "parse_error", "severity": "must",
      "path": "src/audit.cpp", "line": 3,
      "message": "Tag-shaped token does not match [stage->REQ-ID]" },
    { "code": "undeclared_id", "severity": "must",
      "requirementId": "REQ-LOGNI-001", "path": "src/typos.py", "line": 3,
      "message": "Tag references an ID not declared in the manifest" }
  ]
}
```

---

## JSON rules

- `schemaVersion`, `kind`, `summary`, `targets`, `obligations`,
  `related`, `contextFiles`, `annotations`, and `findings` are required
  top-level fields. `kind` is always `"changeContract"` and lets
  consumers distinguish this document from kernel output.
- Evidence objects use the kernel's discriminated union: file evidence
  has `path` + `line`, external evidence has `url`. The shapes do not
  co-occur.
- **Stage order** is: `doc`, `impl`, `unit`, `int`, then declared custom
  stages in lexicographic order. This order is used wherever stages are
  sorted below.
- Sort orders (binding, per determinism rule 3):
  - `targets` by `id`.
  - `obligations` by (`requirementId`, stage order).
  - Within an obligation, file evidence sorted by (`path`, `line`)
    before external evidence sorted by `url`.
  - `related` by `id`; each `sharedFiles` lexicographic.
  - `contextFiles` by `path`; each `tags` array by (`line`,
    `requirementId`, stage order).
  - `findings` by (`code`, `path`, `line`, `requirementId`, `stage`),
    absent fields sorting before present ones.
- Unlike the kernel fixture, array ordering here is **not**
  implementor-defined: the orders above are part of the contract, so
  conforming implementations can be diffed byte-for-byte (after
  normalizing `message` text).
- Kernel finding codes are reused unchanged. This document introduces
  **no new finding codes**. Implementations supporting the kernel's
  priority capability include its additive `severity` field on findings;
  the bundled golden fixture includes it, so implementations without that
  capability differ from the fixture by that field's absence only.
- Additive fields are allowed within a `schemaVersion`; removals,
  renames, or meaning changes require a bump — same stability promise
  as the kernel.
- Annotations appended by agent layers must be objects carrying at
  least `"basis": "inference"` and a `message`. Their further shape is
  reserved for the (future) verification specification; core's only
  obligations are to emit `[]` and never to emit `basis` itself.

---

## Optional capability: the syntactic tier

Implementations may enrich the contract's `contextFiles` section with
parser-derived symbol information. This capability is deterministic
under the contract's rules: it is a pure function of the scanned file
contents and a pinned grammar (grammar versions are fixed by the
implementation's dependency lock; a grammar upgrade that changes output
is a behavior change and requires a fixture update).

Supported languages in this revision, with extension lists mirroring
the kernel scanner's: **Rust** (`.rs`), **Python** (`.py`), **C/C++**
(`.c`, `.cc`, `.cpp`, `.cxx`, `.c++`, `.h`, `.hh`, `.hpp`, `.hxx`,
`.h++`), **JavaScript** (`.js`, `.jsx`, `.mjs`, `.cjs`),
**TypeScript** (`.ts`, `.mts`, `.cts`, and `.tsx` via the TSX
grammar), **shell** (`.sh`, `.bash` via the Bash grammar),
**PowerShell** (`.ps1`, `.psm1`, `.psd1`), **YAML** (`.yml`,
`.yaml`), and **CMake** (`.cmake`, plus the `CMakeLists.txt` filename
special case). Files in other languages, unreadable files, and files
the parser cannot process are left exactly as the core specifies —
degradation is silent and downward, never mislabeled.

Symbols recognized:

- Rust: functions (`fn`) and `impl` blocks. A function inside an
  `impl T` block is qualified `T::name`; the block itself is the
  symbol `impl T`.
- Python: function and class definitions, including decorated forms
  (a decorated definition's span includes its decorators). A function
  inside a class is qualified `Class.name`; nested definitions join
  with `.`.
- C/C++: function definitions (the name resolved through pointer,
  reference, and function declarator wrappers; a template's span
  includes its `template<>` header), plus class, struct, and namespace
  definitions with bodies — forward declarations are not symbols.
  Nesting qualifies with `::` (`audit::log_it`, `C::m`).
- JavaScript/TypeScript: function, generator, class, method, and
  interface declarations, plus `const`/`let`/`var` declarators whose
  value is an arrow function or function expression (the declaration
  keyword line is part of the span). Methods qualify as
  `Class.method`; decorators are trivia for attach-above.
- Shell (Bash grammar): function definitions.
- PowerShell: `function` statements, classes, and class methods
  (`K.M`).
- YAML: every block-mapping key is a symbol, with nesting forming
  dotted paths (`jobs.build`) — what tags in workflow-style YAML most
  usefully anchor to.
- CMake: `function()` and `macro()` definitions, named by the opening
  command's first argument.

**The enclosing-symbol rule.** For a tag on line *L*:

1. The **containing** candidate is the innermost symbol whose span
   contains *L* (the one with the greatest start line).
2. The **attach-above** candidate is the symbol with the smallest
   start line strictly greater than *L*, provided every line strictly
   between *L* and that start is blank or trivia (a comment,
   attribute, or decorator line). This mirrors the "on or directly
   above" tagging convention.
3. The attach-above candidate wins when it is nested inside the
   containing candidate (its start line is within the containing
   span) — a tag directly above a method belongs to the method, not
   the surrounding `impl` or class. Otherwise the containing candidate
   wins. With neither, the tag has no enclosing symbol.

Output additions (all additive within `schemaVersion` 1):

- `contextFiles[].resolution` — the string `"syntactic"`, present
  exactly when the file was parsed by this tier. Consistent with the
  reserved resolution vocabulary below: entries without a `resolution`
  field are tag-derived facts.
- `contextFiles[].tags[].enclosingSymbol` — present when the rule
  above yields a symbol. A parsed file (`resolution` present) whose
  tag has **no** `enclosingSymbol` is a *stranded-tag candidate*: the
  tag is attached to nothing — a deterministic signal worth surfacing
  to `review`.

The bundled golden fixture includes this capability's output
(`src/auth.rs`, `tests/auth_test.py`, and `tests/login_e2e.cpp` are
annotated; the Markdown file is not). Implementations without the
capability differ from the fixture by these fields' absence only.

---

## Acceptance examples

1. **Complete target:** `change compile REQ-LOGIN-001` against the
   bundled fixture emits exactly
   [`example/expected-change-compile.json`](./example/expected-change-compile.json)
   under the comparison rules above. All five obligations are
   satisfied; `REQ-AUDIT-001` and `REQ-LOGOUT-001` appear as related
   via shared evidence files; the fixture's `parse_error` and
   `undeclared_id` appear as scan-health findings; `REQ-LOGOUT-001`'s
   `missing_stage` does **not** appear (not a target).
2. **Incomplete target:** `change compile REQ-LOGOUT-001` against the
   fixture yields an `impl` obligation with `"satisfied": false` and
   empty evidence, plus the corresponding `missing_stage` finding for
   `REQ-LOGOUT-001`. The command still exits zero.
3. **Unknown target:** `change compile REQ-NOPE-001` exits non-zero
   with a diagnostic on stderr and emits no contract document.
4. **Multiple targets:** `change compile REQ-LOGIN-001 REQ-LOGOUT-001`
   produces obligations for both, `related` containing only
   `REQ-AUDIT-001` (the remaining non-target), and context files
   covering both targets' evidence.

---

## Out of scope, and where it goes instead

How a calling agent or workflow engine is intended to drive this command
end to end — including every non-deterministic step and its interface
back into the tool — is described non-normatively in
[WORKFLOW.md](./WORKFLOW.md).

These are the boundaries that keep the subsystem inside the determinism
contract. Each excluded capability has a designated home; none of them
may migrate into this tool.

- **Natural-language task resolution** — calling agent, on top of
  `list --json` and this command.
- **Relevance ranking or context trimming** — calling agent. The
  contract is minimal by construction (rule-derived), not by judgment.
- **Semantic change verification** ("was REQ-97's behavior preserved?")
  — a future specification. Its deterministic half (diff → touched
  lines → enclosing tags → undeclared requirement impact) may join this
  subsystem because it passes the determinism contract; its judgment
  half enters only as `annotations` with `basis: "inference"`.
- **Requirement mutation testing** — an orchestration workflow that
  *calls* this tool. The deterministic harness (apply a structural
  manifest mutation, re-run `check`, require the findings to change) is
  a candidate for this subsystem later; mutation *generation* and
  test-discrimination judgment are not.
- **Symbol- or call-graph-level impact** — requires an index the tool
  does not have. If added, it must be specified with the same rigor
  (pinned parser behavior, fixture-tested) before any contract field
  depends on it; `contextFiles[].tags` is deliberately shaped so an
  `enclosingSymbol` field would be additive. The reserved shapes for
  this are sketched below.

---

## Reserved: the semantic index capability

**Not yet normative.** This section reserves manifest shapes, output
labels, and provenance rules for a future optional capability, so that
implementations and callers do not invent conflicting conventions in
the meantime. The derivation rules (how index entries map to contract
edges) will be specified — with a fixture — before any implementation
ships. Nothing here changes the behavior of `change compile` as
specified above.

The capability admits compiler-grade cross-reference data without
violating the determinism contract, by rule 7: the tool never *builds*
an index (that requires a toolchain and a build environment — caller
territory), it *consumes* one declared in the manifest as an explicit,
diffable input, exactly as `[[signoffs]]` admits external evidence.

Reserved manifest shape:

```toml
[index]
scip = ".trace/index.scip"   # repo-relative; produced by the caller
```

Reserved rules:

- The path is repository-relative and must not escape the manifest
  directory after normalization (same rule as scan roots). A declared
  but unreadable index file is a `manifest_error`.
- Output enriched from the index is a pure function of (manifest, tree,
  args, index file). The index's own reproducibility is the caller's
  concern; the tool's determinism claim extends only over its declared
  inputs.
- **Resolution labels.** Contract entries derived from anything other
  than scanned tags carry a `resolution` field: `"syntactic"` (parser-
  derived names/symbols, no cross-file resolution) or `"semantic"`
  (index-derived resolved references). Entries with no `resolution`
  field are tag-derived graph facts — the same absence-means-fact rule
  as `basis`.
- **Provenance.** A contract enriched from an index includes an `index`
  object recording at minimum the generating tool name, tool version,
  and a content hash of the index file. Consumers use it to judge
  staleness against the tree; the tool does not.
- **Degradation.** No declared index means no `semantic` entries and no
  `index` object — the contract is exactly the one specified above.
  Implementations must not substitute lower-rung data under a
  higher-rung label.

The operational picture — who builds the index, when, and how the
rungs relate — is described in WORKFLOW.md ("The resolution ladder").
