Skip to content

SPEC.json Format

One JSON domain envelope per domain, at spec-engine/<KEY>/SPEC.json. Human-readable, machine-validated — every read and write surface (CLI, webapp, MCP) validates it through the same shared schema. Prefer authoring through the CLI (spec domain new, spec req, spec supersede, …), which writes through a single validated seam; hand-edits are legal but validated on the next index.

{
"key": "BILLING",
"owner": "drea",
"updated": "2026-06-02",
"scope": "Charging, renewal, refunds. Does not belong here: tax rules (TAX).",
"requirements": [
{
"id": "BILLING-009",
"status": "active",
"statement": "When a subscription renews, charge the saved payment method at the current plan price.",
"why": "Revenue path. Silent failure loses money with no visible error.",
"issues": [{ "role": "created", "id": "ENG-1432" }],
"relates": ["BILLING-002", "BILLING-007"],
"livesIn": ["@lib-billing/renew.ts"]
}
]
}
FieldDescription
keyThe domain key (UPPERCASE), matching the folder name
ownerOptional owner string (nullable)
updatedLast-touched date; advanced by every authoring command
scopeOptional domain charter — what belongs in this domain and what doesn’t; consulted at authoring time (spec req <domain> prints it)
requirementsThe requirement entries, in order

There is no authored version counter. A requirement domain’s version is derived from its supersede history — version = 1 + the number of superseded entries — so it advances exactly when a requirement is superseded and can never be hand-edited into disagreeing with what happened. The one exception is the reserved TERM domain (the glossary), whose envelope keeps an authored specVersion: spec term revise bumps it in place, and it serves as the pin term-citation drift is measured against.

FieldDescription
idPermanent KEY-NNN id — never reused, never renumbered
statusactive | draft | superseded | retired (lowercase in the file)
statementThe requirement statement — the testable promise
whyRationale: the failure mode if the promise is violated
supersededBySet when superseded: the successor’s id (resolved globally, so a cross-domain spec move works)
supersededAtVersionThe derived domain version at which the entry died — stamped once at supersession
relatesAdvisory links to other requirement ids (rendered by spec relations)
livesInWhere the requirement is enforced@<path> refs. Metadata, not identity
issuesOptional provenance — see below

Glossary TERM entries reuse the same shape plus term (the headword), aliases, cites (pinned term citations), and section (the GLOSSARY.md heading).

  • active — in force
  • draft — not yet approved
  • superseded — replaced by a successor; the old entry stays in the file as history, pointing forward via supersededBy
  • retired — deliberately ended with no successor (requires owner approval)

Optional tracker links, by rolecreated is a different relationship than supersedes-via:

"issues": [
{ "role": "created", "id": "ENG-1432" },
{ "role": "supersedes-via", "id": "ENG-1781" }
]
  • Roles (closed allow-list): created, supersedes-via, amends-via. A token with an unknown role is surfaced as an UNKNOWN_ROLE warning and dropped — never stored silently.
  • Issue IDs are opaque. The engine treats them as strings and never makes a network call. Nothing resolves, routes, stores coverage by, or keys on an issue ID — it is provenance, not identity. One issue fans out into several requirements; one requirement accumulates several issues across its life.
  • Resolution is an adapter concern. The optional tracker adapter turns IDs into titles/status/URLs; the core engine stays fully offline.

spec provenance renders the matrix derived from this field. See Commands.

Amend in place (same ID, no version change) while a requirement has never been true in production. Supersede (new ID; the old entry is retained and points forward) once it has shipped — the domain version derives one step forward. IDs are never reused.