specgate/docs v0.20.0

ADR-0015 — The change lifecycle: propose → review → archive#

Status#

Accepted — 2026-08-15

Context#

Until now the tool had no object for "a change in flight". A requirement was either in docs/specs/traceability.md or it was not, and its history lived only in git. Two consequences followed:

  1. There was no reviewable unit of intent. A reviewer opening a PR saw a diff of spec.md, use-cases.md and traceability.md and had to reconstruct what behaviour was being proposed from the wreckage. The review happened over the code, not over the intent.
  2. Brownfield was second class. Every command assumed a project was generated by init or expanded from a pack. Adding one requirement to an existing system meant hand-editing three files in lockstep.

The benchmark against OpenSpec (mejoras/openspec-benchmark-plan.md) made the gap explicit: their entire product is this lifecycle, and it is the reason a reviewer can look at a change and understand it in a minute.

Decision#

Introduce the change as a first-class object: a folder holding everything needed to understand and implement one proposed modification.

text
docs/specs/
├── capabilities/<cap>/spec.md              ← source of truth: how it behaves today
├── changes/<change-id>/
│   ├── proposal.md                         ← why + scope
│   ├── design.md                           ← how (optional, full rigor only)
│   ├── tasks.md                            ← implementation checklist
│   ├── change.yaml                         ← schema, rigor, skip_specs, req_range
│   ├── specs/<cap>/spec.md                 ← DELTA (see ADR-0016)
│   └── features/<module>/<name>.feature    ← proposed Gherkin
└── changes/archive/YYYY-MM-DD-<change-id>/ ← history, full context preserved

Six commands: specgate change new | list | show | status | validate | archive.

Location: under docs/specs/, not a top-level openspec/-style directory. The change lifecycle is part of the specification, and the specification already lives in docs/specs/. A second root would split the spec surface in two and force every existing command to learn about both.

Capabilities are optional and additive. docs/specs/capabilities/ is new; spec.md, use-cases.md and the rest are untouched. A project generated before this ADR has no changes/ directory, and every command above reports "no active changes" rather than failing. That is a tested contract, not a convention.

Rationale#

  • The folder is the reviewable unit. Proposal, design, tasks and deltas in one place means a reviewer opens one directory instead of correlating a scattered diff. It also makes parallel work safe: two changes touching the same capability do not conflict as long as they touch different requirements.
  • Archiving is an engineering act, not a filing act. archive merges the deltas into the capability specs and upserts the traceability matrix and materialises the proposed .feature files. The requirement lands under the existing validate --strict-tdd gate. This is the whole point: where a documentation tool stops at markdown, the archive here arms a gate.
  • Dependencies are enablers, not gates. change status reports which artefact is possible to write next, never which one is mandatory. A change can skip design.md entirely; lite rigor is the default.
  • req_range reserves ids at creation. Two changes worked in parallel would otherwise both mint REQ-014. The reservation scans the matrix, the capability specs, and every other change — including archived ones, because ids must never be recycled.

Alternatives considered#

  1. Keep everything in traceability.md and rely on git history. Rejected — the matrix records state, not intent. "REQ-014 exists and is Implemented" does not tell you why it was proposed, what was ruled out of scope, or what the alternative was. Git history holds the diff but not the argument.
  2. A top-level openspec/-shaped directory. Rejected — see above. Two spec roots is a worse problem than a longer path.
  3. One file per change instead of a folder. Rejected — a single file cannot hold per-capability deltas plus proposed .feature files, and it makes the archive a rename rather than a merge.
  4. Make capabilities mandatory, migrating spec.md into them. Rejected for this phase — it would break every existing project. Capabilities are opt-in; archive creates one on demand the first time a delta targets it.

Consequences#

Positive#

  • Review happens over intent (proposal.md + spec deltas) before it happens over code.
  • Brownfield adoption no longer requires a pack or a generated project: any repo with docs/specs/ can take a change.
  • The archive produces a queue item — specgate plan lists the new REQ immediately — which the harness can pick up without further glue.

Negative / trade-offs#

  • Two places now describe requirements: the capability spec (behaviour) and the traceability matrix (state). The risk of drift is real; doctor gains a drift check in a later phase, and archive is the only writer of both.
  • Deeper paths (docs/specs/changes/<id>/specs/<cap>/spec.md). Accepted as the cost of not splitting the spec root.
  • A change archived in Draft does not immediately fail --strict-tdd; the gate fires when the requirement moves to In Dev. Finishing a specification is not the same as starting to build it, and the status model already encodes that distinction.

Follow-ups#

  • F1B: specops diff --as-change — a pack version bump materialises a reviewable change instead of a file diff (see the benchmark plan §F1B).
  • doctor checks: orphaned deltas, archived changes with unchecked tasks, drift between the matrix and the capability specs.
  • A configurable artefact graph per schema (currently the built-in spec-driven graph is hard-coded in scripts/change/cli.ts).

References#

  • scripts/change/ — common.ts, cli.ts, archive.ts
  • ADR-0016 — the delta spec format
  • ADR-0011 — the --strict-tdd gate this lifecycle arms
  • mejoras/openspec-benchmark-plan.md §F1