spec-driven-docs 0.6.5

Spec-driven documentation: current specs, immutable decision records, and executable gates kept coherent for people and coding agents.
Documentation
# 09 — Spec to Code

A spec can exist before the code it binds. This chapter owns the seam between the two: how a requirement written first becomes work, how the work declares what it changed, and how coverage is derived rather than stored. It states the contract any planning tool can satisfy. It names none.

## A failing verification is an unimplemented rule

Every requirement carries a `Verify:` command that exits non-zero on violation. Before the behavior exists, the command fails. That failure is not a defect in the spec. It is the definition of "not yet built."

- An author MAY write a requirement whose verification command does not yet pass.
- A unit of work enacting a requirement MUST leave its verification command passing.

This is what makes the spec a legitimate greenfield artifact. The requirement states the agreement, the failing command states the distance, and the work closes it. Writing the check before the behavior is the same discipline as writing the failing test first, applied to documentation.

## Requirement state is derived, never stored

Run the verification commands of a domain and the failures are its unimplemented requirements. That is the whole status system.

- A specification MUST NOT carry a status marker on a requirement.

A stored status (`status: implemented`, a checkbox, a phase column) is a second copy of a fact the command already decides. The copy drifts the first time behavior changes without the marker. Derived state cannot disagree with the code, because it is recomputed from the code on every ask.

## Precedence is phase-dependent

[00 Model](./00-model.md) owns precedence and states both directions. The marker that selects the direction lives here: a unit of work is in flight for a rule while an open entry document cites that rule's ID. While it is, the spec states the agreement and divergent code is the defect. When no work cites the rule, the code is the observed truth and a divergent spec is the defect.

## The entry document enacts rules by ID

[05 Agent Context](./05-agent-context.md) gives each unit of work one entry document that names its sources by path. When the work changes agreed behavior, path-level naming is not enough: the entry document also names the rules, so enactment is greppable.

- An entry document that changes agreed behavior MUST cite the affected rule IDs.
- An entry document citing a spec change MUST type it as `ADDED`, `MODIFIED`, or `REMOVED`.

The three types are the three operations of [07 Lifecycle](./07-lifecycle.md), stated from the work's side. One clause per affected rule, on the line that names the owning spec:

```markdown
- `_docs/specs/SPEC-auth.md` — ADDED `auth:token-expiry-is-bounded`
- `_docs/specs/SPEC-auth.md` — MODIFIED `auth:refresh-requires-reauth`
```

The clause grammar is fixed so a command can check the shape: the type in capitals, then the rule ID in inline code, matching `[a-z0-9-]+:[a-z0-9-]+`. A typed clause whose ID token is malformed is a gate failure. Whether a story that changed a spec declared the clause at all is a review question, because no command can see the omission.

## A comment cites the rule, never the record

Code is the last word on behavior, so a comment restating behavior is a second copy of it. What a comment holds is the rule this code exists to satisfy, and an invariant no spec of this project owns.

- A comment citing an agreement MUST cite it by rule ID.
- A comment MUST NOT name a decision record.

The clause grammar is the entry document's, extended to code: the type in capitals, then the rule ID. `SATISFIES` marks the code that implements a rule, and `VERIFIES` marks the test that proves it.

```python
# SATISFIES retry-artifacts:cleanup-follows-upload
cleanup_after_upload()
```

Naming a record instead breaks the walk in both directions. A record is frozen, can be superseded, and holds the argument rather than the obligation. As a result, a reader who follows it arrives at what was decided once rather than at what binds now. The rule ID resolves to the binding sentence, and resolves under a grep.

An invariant imposed by another system carries no rule ID, because no spec here owns it. It stays in the comment, stated so a reader can falsify it.

```python
# The vendor returns 200 with an empty body on a replayed idempotency key.
if not response.body:
    return cached
```

Everything else is a deletion or a rename. A comment restating the next line goes, and a comment compensating for a vague name becomes the name.

## A suppression names its case and its exit

The invariant a comment can hold has a second form: a live defect in a system this project does not own, worked around here. It carries no rule ID, because nothing about it was agreed. What makes it honest is the case it names and the condition that ends it.

- A suppression over a defect this project does not own MUST name its case id at the suppression.
- A suppression over a defect this project does not own MUST carry the condition under which it is removed.
- A suppression that masks no external defect MUST state its reason at the suppression and MUST NOT name a case.

The rule reaches every tool, not only the test runner. A formatter range, a linter disable comment, and a dependency pinned back one version are the same act with the same failure mode: the hazard is that a suppression with no exit becomes permanent by default.

```python
@pytest.mark.xfail(reason="KI-upstream-500-on-replayed-webhook", strict=True)
def test_webhook_replay_is_idempotent():
    ...
```

Prefer the strict form. A non-strict expected failure keeps passing after the upstream fix lands. As a result, the suppression outlives the bug it was written for and nobody learns the case can close. A strict one turns the suite red the moment the fix arrives, which is the signal that closes it.

Some suppressions have no exit. A lint disabled over a construct this project chose and keeps masks nothing external. No record can carry a condition anyone meets. Writing one anyway produces the unremovable mask the case rule exists to prevent. That suppression states its reason instead. The two forms are exclusive: a suppression names a case or states a permanent reason, never both.

The reason goes where the tool that honors the suppression already reads one. Most modern linters define that position, and the idiom differs per tool.

```text
zizmor    # zizmor: ignore[dangerous-triggers] <reason>
ESLint    // eslint-disable-next-line no-eval -- <reason>
Rust      #[expect(dead_code, reason = "<reason>")]
pytest    @pytest.mark.skip(reason="<reason>")
unittest  @unittest.skip("<reason>")
```

The reason belongs to the suppression that carries it, and reaches no further than the construct it sits in. A second attribute on the line states its own reason, never its neighbour's.

Where the tool defines no reason position, as `noqa` and `shellcheck disable=` do not, the reason carries the `sdd: permanent <reason>` marker, on the suppression line or in the comment above it. Writing the reason in the tool's own idiom keeps a generated file another project owns readable by both: it satisfies this rule without one byte of this convention inside it.

Only the position the tool defines counts. A comment that merely sits near a suppression states no reason, because accepting nearby prose would let an unrelated sentence close the rule.

A test that must not hide the bug at all keeps failing, with the case id in a comment beside it. The case id is the record's filename, so it resolves the same way a rule ID does. The reason string needs no restated summary, because the record it names holds the symptom, the workaround, and the retire condition. The case, its states, and its retirement belong to [07 Lifecycle](./07-lifecycle.md).

## Coverage is a grep

The rule ID is one string in four record sets: the spec defines it, a decision record argues for it, an entry document enacts it, and a comment marks the code that satisfies it. Traceability is therefore derived on demand, in both directions, from the records that already exist.

```bash
rg -o '^### `([a-z0-9-]+:[a-z0-9-]+)`' -r '$1' _docs/specs | sort -u > /tmp/agreed
zone=${SDD_PLAN_ZONE:-$(sdd status --json | jq -r '.plan_zone.path // empty')}
[ -d "$zone" ] || { echo 'FAIL no plan zone resolved'; exit 1; }
rg -oe '(ADDED|MODIFIED|REMOVED) `[a-z0-9-]+:[a-z0-9-]+`' -r '$0' "$zone" \
  | rg -o '[a-z0-9-]+:[a-z0-9-]+' | sort -u > /tmp/enacted
comm -23 /tmp/agreed /tmp/enacted
```

The third command prints the agreed rules that no typed clause cites: the spec-first backlog, computed from two record sets and stored in neither. The same shape run against code prints the opposite defect.

```bash
rg -o '(SATISFIES|VERIFIES) ([a-z0-9-]+:[a-z0-9-]+)' -r '$2' --glob '!_docs/**' \
  | sort -u > /tmp/cited
comm -13 /tmp/agreed /tmp/cited
```

What that prints is a rule ID cited in code that no spec defines: a fabricated citation, and the check that makes citing worth anything.

- A project MUST NOT maintain a stored coverage artifact.

A traceability matrix, a rules-to-stories index, or a backlog file restates what the greps derive. Each is the filesystem-index shape [00 Model](./00-model.md) forbids: a copy kept because the records exist, drifting on the next change to either side.

## What the planning tool owes

This framework does not name a planning tool. Any tool serves whose work record is readable by the greps in this chapter and satisfies the contract the rules above already state: one entry document per unit of work, sources named by path, and spec changes cited by typed rule ID. The inverse dependency is also bounded: the specs never name the tool, so replacing it edits the plan zone and nothing under `specs/` or `decisions/`.

The zone's path is a declared value too. The project declares it once and the instance records it, `SDD_PLAN_ZONE` overrides that record, and no spec, chapter, or gate carries the path itself. A tool whose records live outside the checkout is served the same way as one whose records sit beside the specs.

## Unenforced

Two rules in this chapter no command can decide: that a unit of work which changed a spec declared the typed clause at all, and that the cited type matches the diff. A gate checks every declared clause and cannot see an omitted or mistyped one. The reviewer compares the spec diff against the entry document.

A third condition is unenforced by the project's own choice rather than by kind. Only a plan zone the project declared tracked is gated. A project that keeps its entry documents untracked, or reaches them through `SDD_PLAN_ZONE`, has a zone no clone carries. The gate declines it, and a reviewer holds the clause shape instead. [08 Gates](./08-gates.md) carries all three in the unenforced list.

## Sources

- GitHub Spec Kit, on tests written first and confirmed to fail before implementation: <https://github.com/github/spec-kit/blob/main/spec-driven.md>
- OpenSpec, for the `ADDED` / `MODIFIED` / `REMOVED` delta typing: <https://github.com/Fission-AI/OpenSpec/blob/main/docs/concepts.md>
- AWS Kiro, on tasks tracing to requirement identifiers: <https://kiro.dev/docs/specs/>
- StrictDoc, for the relation marker in a source comment and its implementation and verification roles: <https://strictdoc.readthedocs.io/en/stable/stable/docs/strictdoc_01_user_guide.html>
- OpenFastTrace, for coverage tags written as source comments against specification item ids: <https://github.com/itsallcode/openfasttrace>
- DO-178C, on every source code element tracing back to a requirement: <https://www.parasoft.com/learning-center/do-178c/requirements-traceability/>