@spec-engine/spec-engine 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/AGENTS.md +678 -0
  3. package/dist/agent/mcp.json +11 -0
  4. package/dist/agent/pre-commit +3 -0
  5. package/dist/agent/skills/req-author/SKILL.md +102 -0
  6. package/dist/docs/agent-reference/index.html +1280 -33
  7. package/dist/docs/architecture/index.html +6 -6
  8. package/dist/docs/checks/index.html +19 -266
  9. package/dist/docs/commands/index.html +32 -164
  10. package/dist/docs/format/index.html +2 -9
  11. package/dist/docs/getting-started/index.html +3 -3
  12. package/dist/docs/guides/platform-setup/index.html +2 -2
  13. package/dist/docs/pagefind/fragment/en_10e3da6.pf_fragment +0 -0
  14. package/dist/docs/pagefind/fragment/en_37cae5e.pf_fragment +0 -0
  15. package/dist/docs/pagefind/fragment/en_7fc2fcd.pf_fragment +0 -0
  16. package/dist/docs/pagefind/fragment/en_96d3bc1.pf_fragment +0 -0
  17. package/dist/docs/pagefind/fragment/en_a26aa07.pf_fragment +0 -0
  18. package/dist/docs/pagefind/fragment/en_b7e68ef.pf_fragment +0 -0
  19. package/dist/docs/pagefind/fragment/en_d8a5bcd.pf_fragment +0 -0
  20. package/dist/docs/pagefind/fragment/en_de67937.pf_fragment +0 -0
  21. package/dist/docs/pagefind/fragment/en_fc3c55b.pf_fragment +0 -0
  22. package/dist/docs/pagefind/index/en_1664b45.pf_index +0 -0
  23. package/dist/docs/pagefind/index/en_a8b8bb6.pf_index +0 -0
  24. package/dist/docs/pagefind/pagefind-entry.json +1 -1
  25. package/dist/docs/pagefind/pagefind.en_bc3d4a7766.pf_meta +0 -0
  26. package/dist/docs/platform-mapping/index.html +2 -1
  27. package/dist/docs/tags/index.html +9 -92
  28. package/dist/impl.js +2011 -703
  29. package/package.json +1 -1
  30. package/dist/docs/pagefind/fragment/en_1f98c2d.pf_fragment +0 -0
  31. package/dist/docs/pagefind/fragment/en_24f76b6.pf_fragment +0 -0
  32. package/dist/docs/pagefind/fragment/en_4bd9de6.pf_fragment +0 -0
  33. package/dist/docs/pagefind/fragment/en_9cc1878.pf_fragment +0 -0
  34. package/dist/docs/pagefind/fragment/en_9cfc306.pf_fragment +0 -0
  35. package/dist/docs/pagefind/fragment/en_b2867b9.pf_fragment +0 -0
  36. package/dist/docs/pagefind/fragment/en_b81dc52.pf_fragment +0 -0
  37. package/dist/docs/pagefind/fragment/en_c3abf3d.pf_fragment +0 -0
  38. package/dist/docs/pagefind/fragment/en_c3e92c6.pf_fragment +0 -0
  39. package/dist/docs/pagefind/index/en_6e50ed4.pf_index +0 -0
  40. package/dist/docs/pagefind/index/en_afc98e5.pf_index +0 -0
  41. package/dist/docs/pagefind/pagefind.en_d0b5c328bc.pf_meta +0 -0
@@ -0,0 +1,678 @@
1
+ The reference for a coding agent working in a repository that uses Spec Engine. It has three parts: the model and the working loop, the standard every requirement is written against, and the exact contract of every `spec` command.
2
+
3
+ ## What Spec Engine is
4
+
5
+ Spec Engine helps developers, product teams, and agents write better specs and keep them current, so the software changes accurately.
6
+
7
+ It works with four things:
8
+
9
+ | Noun | What it is | Where it lives | Authored or derived |
10
+ | --- | --- | --- | --- |
11
+ | Domain | A named subject area of requirements with an UPPERCASE key (`BILLING`), an owner, and a charter that says what belongs in it | `spec-engine/<KEY>/SPEC.json`, one file per domain | Authored |
12
+ | Requirement | One durable rule of the product's behavior, addressed by a permanent id `KEY-NNN` (`BILLING-009`), with a statement, a why, and a status | An entry in its domain's `SPEC.json` | Authored |
13
+ | Tag | A comment `@spec KEY-NNN` that binds a line of code or a test to a requirement | Source and test files, in comments | Authored in code |
14
+ | Index | A SQLite database of every domain, requirement, and tag, which the read and check commands query | `.spec-engine/index.sqlite` at the platform root | Derived |
15
+
16
+ - The `SPEC.json` files are the only authored truth about what the product promises. Write them through the `spec` commands. An id is never reused or renumbered: a changed promise is superseded by a new id, and an ended one is deprecated.
17
+ - Tags in code are the only truth about where a requirement is implemented and where it is verified. A tag in implementation code implements; a tag in a test file verifies. The kind comes from the file path, never from the tag text.
18
+ - The index is derived and disposable. Every command rebuilds it from the `SPEC.json` files and the tags, and deleting `.spec-engine/` is always safe.
19
+ - Requirements are not tickets. One ticket usually becomes several requirements. A ticket id is provenance only: it goes in `--issue`, never in a requirement id or a tag.
20
+
21
+ ## The loop
22
+
23
+ ```shell
24
+ spec query "renewal charge" . --json # 1. route: requirements on the topic
25
+ spec resolve src/billing/renew.ts . --json # or the ones tagged in a file
26
+ # 2. implement the change
27
+ # 3. tag the function that produces the behavior and the test that proves it
28
+ spec check . --ci --json # 4. exit 1: fix before the PR
29
+ spec guard . --json # 5. exit 1: the change loses a requirement
30
+ ```
31
+
32
+ When `spec guard` blocks, relay its `🛑 spec-guard:` block verbatim to the user. Then either `spec supersede` the requirement with a successor or get the user's explicit decision to `spec deprecate` it. Never delete a requirement to make the gate pass.
33
+
34
+ ## What to tag
35
+
36
+ ```ts
37
+ export function renew() { /* … */ } // @spec BILLING-009
38
+ it("charges current price", () => {}) // @spec BILLING-009 unit
39
+ ```
40
+
41
+ - A tag binds the one function that produces the behavior and the one test that proves it.
42
+ - Helpers, glue code, and configuration carry no tag.
43
+ - If you cannot name the behavior a line produces, do not tag it.
44
+ - A test tag may end with a level: `unit`, `integration`, or `e2e`.
45
+
46
+ ## In CI
47
+
48
+ ```shell
49
+ spec check . --ci --base "$(git merge-base HEAD origin/main)"
50
+ ```
51
+
52
+ Without `--base`, `check` diffs against `HEAD`, which in a CI checkout already holds the change, so the removal and loss checks find nothing to report.
53
+
54
+ ## Requirement authoring standard
55
+
56
+ The rules every new requirement is written against. The statement shapes and the statement rules are also checked at write time and by `spec check`.
57
+
58
+ 0. **A requirement is one rule of the product's behavior, stated from the user's point of view.** The statement rules below are the mechanical half of this rule: `spec req`, `amend`, `supersede`, `move`, and `accept` refuse a statement that breaks one, `spec check` reports an Active statement that breaks one as `STATEMENT_RULES`, and `spec lint` lists every finding. The judgment half stays with the author.
59
+ 1. **One behavior per requirement.** A ticket usually splits into several requirements. Never mint one requirement per ticket.
60
+ 2. **Write the statement in a fixed shape.** Pick the sentence shape that matches the situation. The word "shall" separates who acts from the observable result:
61
+
62
+ | Situation | Shape | Example |
63
+ | --- | --- | --- |
64
+ | Always true | The `<system>` shall `<result>` | The spec CLI shall write JSON output to stdout only. |
65
+ | Triggered by an event | When `<trigger>`, the `<system>` shall `<result>` | When an unknown requirement id is passed, spec resolve shall print `[]` and exit 0. |
66
+ | Only in a certain state | While `<state>`, the `<system>` shall `<result>` | While the index file is missing, read commands shall rebuild it before answering. |
67
+ | Handling something going wrong | If `<failure>`, then the `<system>` shall `<result>` | If the results file cannot be parsed, then spec check shall report the file and exit 2. |
68
+ | Only when a feature is present | Where `<feature>`, the `<system>` shall `<result>` | Where a member config lists ignore directories, the scanner shall skip them. |
69
+
70
+ A statement that fits no row has not yet decided what triggers the behavior or what observably happens.
71
+
72
+ A statement may end with a trailing `so <observable consequence>` clause ("…shall report NO_SPEC_CONFIG at warning severity, so a lone warning never fails the gate") when the consequence is itself observable. The reason the behavior exists belongs in the why, never in the statement.
73
+
74
+ One promise per statement. A result that chains two behaviors with `and`, or that sets two severities, is two requirements: a single test must be able to prove the whole statement. Prefer `When`, `If … then`, and the bare `shall` form. Use `While` and `Where` only when a state or a feature is what gates the behavior.
75
+ 3. **It must make sense on its own.** A reader who sees only this one statement, in search results or a report, understands it. A statement that says "the normalization" or "that prefix" without naming the subject fails.
76
+ 4. **Describe what it does, not how.** Say what a user or calling agent observes. If renaming an internal function would make the statement wrong, it is written at the wrong level.
77
+ 5. **No expiration dates.** No "currently", no "the new behavior", no references to things that were removed.
78
+ 6. **The why states what breaks.** Answer "what goes wrong if this stops being true". Never restate the requirement in other words.
79
+ 7. **Three link types, three jobs.** Tests prove it. Code implements it. Tickets are only history: never an id, never proof.
80
+ 8. **File it where the promise lives, and search first.** Pick the domain whose charter covers the behavior, and run `spec query` before minting. If something close exists, amend it instead.
81
+
82
+ ### Requirement id format
83
+
84
+ - Ids are `KEY-NNN`: the domain key, a hyphen, and a three-digit zero-padded number (`BILLING-010`).
85
+ - Ids are permanent. A requirement is superseded or deprecated, never renumbered and never deleted.
86
+
87
+ ### The one-promise lint
88
+
89
+ A domain envelope may declare `grammar: "ears"` with `grammarSeverity: "warning" | "error"` (default warning). Set both with `spec domain grammar <KEY>`, never by hand. In such a domain a statement must fit one of the five shapes, and a statement that fits a shape still fails when:
90
+
91
+ | Rule | Fails | Passes |
92
+ | --- | --- | --- |
93
+ | Semicolon clause | `; ` anywhere after the first `shall` | `When a; b holds, X shall …` |
94
+ | Second subject | `, and <subject> shall` after the first `shall` | `X shall A, and shall B` |
95
+ | Rationale | `because`, `so that`, `in order to` anywhere | a trailing `, so <observable consequence>` |
96
+
97
+ - `spec check` reports a nonconforming Active or Draft statement as `STATEMENT_GRAMMAR` at the declared severity, one row per statement, the detail naming the first failed shape or rule.
98
+ - `spec req`, `amend`, `supersede`, and `move` judge new text at write time: they warn and write under warning, and refuse (exit 2) under error.
99
+ - The lint is a pattern, not a reading: a statement that passes can still chain two behaviors, and the reviewer still owns that call.
100
+ - The TERM domain is exempt.
101
+
102
+ ### The statement rules
103
+
104
+ Every domain is judged by these rules, whatever its grammar. `spec req`, `amend`, `supersede`, `move` (with new text), and `accept` refuse a statement that breaks a rule graded `error` (the default): exit 2, one `<rule>: <detail> — <reason>` line per finding, rule 0 last. A rule graded `warning` writes the statement and prints the same finding lines on stderr. There is no override flag; the `statementRules` block of `check.json` grades a rule, tunes it, or turns it off. `spec check` reports an Active statement that breaks one as `STATEMENT_RULES`, one row per rule per statement. The TERM domain is never judged.
105
+
106
+ | Rule | Reports | Printed reason |
107
+ | --- | --- | --- |
108
+ | `one-sentence` | more than one sentence, no closing period, or a line break | a statement is one testable sentence |
109
+ | `length` | over `maxLength` characters (280 by default) | a sentence a reader holds at once |
110
+ | `no-file-path` | a repository path or file name | which file holds what is layout, not behaviour |
111
+ | `no-test-narration` | how a test is written | how a test is written is not what the product does |
112
+ | `no-toolchain-layout` | CI or tooling layout | how CI and tooling are arranged is not a product behaviour |
113
+ | `no-history` | how the requirement changed | a statement says what holds now, not how it got there |
114
+ | `no-jargon` | a word from the jargon list | a reader without the codebase cannot parse project jargon |
115
+ | `no-tool-statement` | which tool is in use | naming the tool in use is a decision record, not a requirement |
116
+
117
+ ## Authoring requirements (brief → mint)
118
+
119
+ The procedure for turning a brief or ticket into requirements. Each draft is written to the requirement authoring standard above.
120
+
121
+ 1. **Split the brief.** One requirement per behavior, never one per ticket.
122
+ 2. **Place each.** `spec domain list --json` prints every charter. File the requirement where the promise lives.
123
+ 3. **Dedup.** `spec query "<phrase>" . --json` before minting. Overlap means `spec amend` or relate the existing requirement.
124
+ 4. **Draft to the standard.** Every draft quotes the phrase of the brief it derives from.
125
+ 5. **Mint.** `spec req <domain> --text "<statement>" --why "<what breaks>" [--issue <ticket>]`.
126
+ 6. **Provenance, not identity.** The ticket goes in `--issue`, never in an id or a tag.
127
+ 7. **Verify.** `spec index . && spec check . --ci` (exit 0) and `spec guard .` before the PR.
128
+
129
+ ## Command reference
130
+
131
+ ### Invocation
132
+
133
+ ```shell
134
+ spec <command> [...] # after: bun add -g @spec-engine/spec-engine
135
+ bunx @spec-engine/spec-engine <command> [...] # without a global install (requires Bun)
136
+ ```
137
+
138
+ - Almost every command takes an optional trailing `platformDir` positional (default: cwd). The platform dir is the directory that contains `spec-engine/`.
139
+ - Every command is non-interactive when stdin is not a TTY.
140
+ - `--json` goes to **stdout only**. Guidance, warnings, and diagnostics chrome go to **stderr**. Parse stdout, surface stderr.
141
+
142
+ ### Platforms, members, and pins
143
+
144
+ - A platform is the directory holding `spec-engine/`. When declared, its platform file `platform-map.json` is `{ name, members, ignore }`.
145
+ - A member is a repository named in that file, carrying a `{ platform, member }` marker. A monorepo's coverage columns are its workspace packages, named by path (`packages/engine`). A lone repository with no platform file is one column or its packages. A monorepo's files outside every package are one more column, named by the member or, for a lone monorepo, the directory.
146
+ - `spec-engine.member.json` adds one fact: the member's pin, `spec-engine@N`. `spec init <name>` declares and pins in one step.
147
+ - A requirement domain's version is derived from its supersede history. The platform version is the max domain version. Drift compares a member's pin against each referenced requirement's `changed_at_version`. A stray `spec-engine.platform.json` is ignored with a warning.
148
+ - A member's `dependsOn` (the platform's own package names in its manifest) orders `spec propagation`: providers before consumers.
149
+
150
+ ### Tags
151
+
152
+ - The tag is `@spec KEY-NNN`, optionally followed by a level token `unit` | `integration` | `e2e` on a test tag. Never write "implements" or "verifies" in the tag.
153
+ - The scanner reads `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.sh`, `.lua`, `.luau`, `.go`, `.rs`, `.tf`, `.toml`, `.sql`, `.css`, `.yml`, `.yaml`, and `.astro` files, any file named `Dockerfile` or `Dockerfile.<suffix>`, and `.md` documents, whose tags document.
154
+ - The tag regex ignores the comment leader, so `--`, `//`, `#`, `<!-- -->`, and `/* … */` all work.
155
+ - Dot-prefixed directories are skipped except `.github/`. `fixtures/` directories are ignored everywhere.
156
+ - A tag in implementation code implements; a tag in a test path verifies. A test path contains `.test.`, `.spec.`, `__tests__/`, `/tests/`, `/e2e/`, or `.e2e.`, or ends in `_test.go`, `_test.rs`, `_spec.lua`, or `_test.lua`, or lives under `spec/` (busted).
157
+ - Rust tests inline under `#[cfg(test)]` sit in the implementation file and count as implements; put a verifying tag in `tests/` or a `*_test.rs` file.
158
+
159
+ ### Exit code contract
160
+
161
+ | Code | Meaning |
162
+ | --- | --- |
163
+ | 0 | Success. Empty results are success (`[]` on stdout, guidance on stderr). |
164
+ | 1 | Data-level failure: `check` found an error-severity diagnostic; `guard` found a loss; `gate` failed; `index` crashed mid-build. |
165
+ | 2 | Usage/environment error: bad args, not a Spec Engine platform (no `spec-engine/`), path-containment violation, invalid `--limit`/`--port`, FTS5 syntax error. |
166
+
167
+ Branch on exit codes, not on output text.
168
+
169
+ ### Commands
170
+
171
+ | Command | Synopsis | `--json` | Exits |
172
+ | --- | --- | --- | --- |
173
+ | `spec index [platformDir]` | Build or refresh the derived index | yes (IndexResult) | 0 / 1 / 2 |
174
+ | `spec check [platformDir] [--ci]` | Integrity + coverage + drift diagnostics | yes (array) | 0 / 1 / 2 |
175
+ | `spec guard [platformDir] [--against <ref>]` | Block a change about to lose a requirement, its last implementation, or its last test | yes (array) | 0 / 1 / 2 |
176
+ | `spec map [platformDir]` | Requirement × repo coverage matrix | yes (array) | 0 / 2 |
177
+ | `spec get <KEY-NNN> [platformDir]` | One requirement's full record and its tag sites; unknown id is `[]` | yes (object, or `[]`) | 0 / 2 |
178
+ | `spec list [platformDir] [--domain KEY] [--status S]` | Every requirement record in (key, seq) order, history included | yes (array) | 0 / 2 |
179
+ | `spec query <text> [platformDir]` | Full-text retrieval over requirements | yes (array) | 0 / 2 |
180
+ | `spec relations [platformDir]` | Mermaid graph of `relates` links | yes (array) | 0 / 2 |
181
+ | `spec provenance [issueId] [platformDir]` | Per-requirement provenance matrix | yes (array) | 0 / 2 |
182
+ | `spec resolve <files…> [platformDir]` | Requirements tagged in the given files | yes (array) | 0 / 2 |
183
+ | `spec propagation <KEY-NNN> [platformDir]` | Per-repo migrated/drifted state for a superseded requirement | yes (array) | 0 / 2 |
184
+ | `spec gate <repo> <KEY-NNN> [platformDir]` | Approval gate: pass iff Active and the pin covers it | yes (object) | 0 / 1 / 2 |
185
+ | `spec init [repo]` | Declare a member through platform-map and write its `spec-engine.member.json` pin | yes (object) | 0 / 2 |
186
+ | `spec domain new <KEY>` / `spec domain list` / `spec domain grammar <KEY>` | Scaffold / list `spec-engine/<KEY>/SPEC.json` / set its statement grammar and severity | `list`: yes (array); `grammar`: yes (object) | 0 / 2 |
187
+ | `spec migrate [platformDir]` | Strip the retired `livesIn` and `verifiedIn` fields from every `SPEC.json` (idempotent) | yes (object) | 0 / 2 |
188
+ | `spec req <domain-prefix> [platformDir]` | Piped: next unused id. `--text`: author non-interactively | yes (object) | 0 / 2 |
189
+ | `spec term …` | Author, list, revise, and confirm glossary terms | yes | 0 / 2 |
190
+ | `spec glossary [platformDir]` | Generate `GLOSSARY.md` from the TERM store; `--migrate`, `--check` | yes (object) | 0 / 1 / 2 |
191
+ | `spec deprecate <KEY-NNN> --reason "…" [platformDir]` | End an Active/Draft requirement with a recorded reason | yes (object) | 0 / 2 |
192
+ | `spec supersede <KEY-NNN> [platformDir]` | Flip to superseded, mint the successor, retag every site to it | yes (object) | 0 / 2 |
193
+ | `spec move <KEY-NNN> <NEW-DOMAIN> [platformDir]` | Cross-domain supersede | yes (object) | 0 / 2 |
194
+ | `spec amend <KEY-NNN> [platformDir]` | Revise an unshipped entry in place | yes (object) | 0 / 2 |
195
+ | `spec accept <KEY-NNN> [platformDir]` | Promote a Draft requirement to Active | yes (object) | 0 / 2 |
196
+ | `spec lint [KEY \| KEY-NNN] [platformDir] [--all]` | Every statement-rule finding for the platform, a domain, or a requirement | yes (array) | 0 / 1 / 2 |
197
+ | `spec serve [platformDir] [--port N] [--host IP]` | Local webapp + `/api/*` over the index | n/a (HTTP) | 0 / 1 / 2 |
198
+ | `spec docs [--port N]` | Serve the bundled docs site offline | n/a (HTTP) | 0 / 1 / 2 |
199
+ | `spec mcp [platformDir]` | MCP server over stdio | n/a (JSON-RPC) | 0 / 2 |
200
+
201
+ Common flags on the index-touching commands (`index`, `check`, `map`, `get`, `list`, `query`, `relations`, `resolve`, `propagation`, `provenance`, `gate`, `serve`):
202
+
203
+ | Flag | Effect |
204
+ | --- | --- |
205
+ | `--out <path>` | Index DB path override. Must resolve inside `platformDir` (exit 2 otherwise). |
206
+ | `--no-prompt` | Suppress the member-onboarding prompt. |
207
+ | `--json` | Machine output (all except `serve`). |
208
+ | `--fresh` | Read commands only: delete the DB plus WAL/SHM and reindex before answering. |
209
+
210
+ `init`, `domain list`, `req` register `--json` only. `domain new` registers none.
211
+
212
+ **Index freshness.** Read commands (`map`, `get`, `list`, `query`, `relations`, `resolve`, `propagation`, `provenance`) build the index when it is missing and otherwise trust it. A warm index older than a changed `SPEC.json` produces a stderr warning. Tag-only code edits are not detected. `gate` always rebuilds cold, and so does `check --ci`. After editing specs or tags, pass `--fresh`.
213
+
214
+ ### spec index
215
+
216
+ Build or refresh the derived index.
217
+
218
+ `--json`: `{ build_id, repos, domains, requirements, tags, diagnostics }`.
219
+
220
+ Exit: 0 built / 1 an invalid SPEC.json refused the build, or the build crashed / 2 not a platform or bad args.
221
+
222
+ - `build_id` is deterministic for identical inputs (cold-rebuild equivalence).
223
+ - A `SPEC.json` that is not JSON or fails the shared schema refuses the whole build, and the index on disk is left as it was. Every file is parsed first, so one run reports every invalid file. Under `--json` stdout holds only the `INVALID_DOMAIN_FILE` rows in the `spec check --json` row shape; text mode prints only those rows on stderr. Every other index-building command prints only the same rows and exits 1 (`check`, `gate`, `guard`, `serve`, and the read commands); a lifecycle command that reindexes for its retag (`supersede`, `amend`, `deprecate`, `move`, `term confirm`) exits 2 after its write.
224
+ - Also writes `.spec-engine/doctor.md`, a list of doc lines that mention a requirement id without an explicit `<!-- @spec ID -->` binding.
225
+
226
+ ### spec check
227
+
228
+ Integrity, coverage, drift, and governance diagnostics.
229
+
230
+ | Flag | Effect |
231
+ | --- | --- |
232
+ | `--ci` | Delete the DB first. Correctness never trusts a warm index. |
233
+ | `--results <junit.xml>` | The trusted-red gate: a verifying tag counts only when its test passed. Path resolves inside `platformDir`. |
234
+ | `--base <ref>` | Diff the requirement set against a git ref (deletion and status-flip governance). |
235
+ | `--approved-by <list>` | Approver set for the status-flip check. |
236
+ | `--require-owner-approval` | Escalate `UNAPPROVED_STATUS_FLIP` from warning to error. |
237
+ | `--unsourced-change` | Also warn on superseded requirements with no `supersedes-via` issue. |
238
+
239
+ `--json` rows: `{ code, severity, repo, source_file, line, req_id, detail }`, nullable except `code`, `severity`, `detail`, sorted deterministically.
240
+
241
+ Exit: 0 clean or warnings only / 1 any error-severity row / 2 usage.
242
+
243
+ Codes, in `DiagnosticCode` order:
244
+
245
+ | Code | Severity | Condition |
246
+ | --- | --- | --- |
247
+ | `DUP_ID` | error | The same requirement id appears twice |
248
+ | `BROKEN_SUPERSEDE` | error | A `supersededBy` names a missing id, or a `supersedes` names a missing id or the entry itself |
249
+ | `BAD_STATUS` | error | An entry status outside `active` / `draft` / `superseded` / `deprecated` |
250
+ | `DANGLING_TAG` | error | A tag references no requirement |
251
+ | `SUPERSEDED_REFERENCED` | error | Code still tags a superseded requirement (retag to the successor) |
252
+ | `DEPRECATED_REFERENCED` | error | Code still tags a deprecated requirement |
253
+ | `STATEMENT_GRAMMAR` | per domain, default warning | A statement fails its domain's declared grammar |
254
+ | `STATEMENT_RULES` | error; per rule from `statementRules.severity` | An Active statement breaks a statement rule: one row per rule per statement, the detail `<rule>: <detail> — <reason>` |
255
+ | `DRAFT_REFERENCED` | error on a verifying tag, warning on an implementing or documenting tag | A tag references a Draft requirement |
256
+ | `GLOSSARY_DRIFT` | warning | The committed glossary differs from the one generated from the term store |
257
+ | `ORPHAN_REQ` | error | An Active requirement has no implementing tag |
258
+ | `UNVERIFIED_REQ` | warning | An Active requirement is implemented but has no verifying tag |
259
+ | `DRIFT` | error | A member is pinned behind the version at which a requirement it references changed |
260
+ | `NO_SPEC_CONFIG` | warning | A declared member is present on disk with no `spec-engine.member.json` |
261
+ | `BROKEN_RELATES` | warning | A `relates` entry names a nonexistent requirement |
262
+ | `RELATES_SUPERSEDED` | warning | A `relates` entry names a superseded requirement |
263
+ | `CYCLIC_SUPERSEDE` | error | A circular supersession chain |
264
+ | `SELF_RELATES` | warning | A `relates` entry names its own requirement (dropped, but surfaced) |
265
+ | `UNKNOWN_ROLE` | warning | An `issues` entry uses a role outside `created` / `supersedes-via` / `amends-via` (surfaced and dropped, never stored) |
266
+ | `UNSOURCED_CHANGE` | warning, only with `--unsourced-change` | A superseded requirement carries no `supersedes-via` issue |
267
+ | `INVALID_DOMAIN_FILE` | error | A `SPEC.json` is not JSON or fails the shared schema; one row per failing field |
268
+ | `MIGRATION_PENDING` | warning | A `SPEC.json` still carries the retired `livesIn` or `verifiedIn` fields; run `spec migrate` |
269
+ | `UNPROVEN_REQ` | error, only with `--results` | An Active requirement has verifying tags but no passing correlated test |
270
+ | `PROOFS_UNCONFIRMED` | warning, only without `--results` | The proof gate fell back to presence-only mode |
271
+ | `REQUIREMENT_REMOVED` | error | An Active requirement present at the base ref is gone with no same-change supersession |
272
+ | `IMPL_LOST` | error, when a base ref resolves | The change removes the last implementing tag of a surviving Active requirement |
273
+ | `VERIFY_LOST` | warning, when a base ref resolves | The change removes the last verifying tag of a surviving Active requirement |
274
+ | `SPEC_FILE_DELETED` | error, when a base ref resolves | A `SPEC.json` present at the base ref is gone |
275
+ | `UNAPPROVED_STATUS_FLIP` | warning; error with `--require-owner-approval` | A flip to superseded or deprecated whose CODEOWNERS domain owner is absent from `--approved-by` |
276
+ | `PARTIAL_PROPAGATION` | error, needs `--base` and `--results` | A changed requirement where some bound tests pass and some fail |
277
+ | `MALFORMED_FILE` | platform-map's severity: error, or warning for a manifest | A platform file, a package or workspace manifest, or platform-map's per-user file failed to parse or validate |
278
+ | `MEMBER_MISSING` | warning | A declared member is not on this machine; it is not scanned |
279
+ | `MARKER_MISSING` | warning | A declared member has no marker |
280
+ | `MARKER_MISMATCH` | error | A member's marker names a different platform |
281
+ | `UNLISTED_REPO` | warning (platform-map info, promoted) | A repository in the platform folder is not a member; its tags are not scanned |
282
+ | `PLATFORM_NOT_LOCATED` | warning | A marker names a platform this machine cannot find |
283
+ | `UNDECLARED_PLATFORM` | warning (platform-map info, promoted) | A folder of repositories with no platform file; nothing in it is a member |
284
+ | `SCAN_TRUNCATED` | warning | A workspace walk hit its depth or entry cap |
285
+ | `UNDEFINED_TERM` | error | A `cites` entry resolves to no glossary term |
286
+ | `ORPHAN_TERM` | warning | An Active term that nothing cites |
287
+ | `TERM_DRIFT` | warning | A citation's pin is behind the term's current version |
288
+ | `SUPERSEDED_TERM_REFERENCED` | error | A citation names a superseded term |
289
+
290
+ The four TERM codes are the last four rows. Only `error` rows drive exit 1.
291
+
292
+ The eight codes before them are platform-map's, surfaced with the platform-map code: every platform-map error and warning keeps its severity; `UNLISTED_REPO` and `UNDECLARED_PLATFORM` are promoted from info to warning because they mean a repository's tags are not scanned; `UNMATCHED_PATTERN` and `AMBIGUOUS_ECOSYSTEM` are not surfaced. `repo` is the diagnostic's subject when it names a member; `source_file` is the subject. `NO_SPEC_CONFIG` stays Spec Engine's own code: a declared member present on disk with no `spec-engine.member.json`.
293
+
294
+ - Deletion detection runs by default: with no `--base`, the working tree is diffed against `HEAD` whenever git resolves. An Active requirement absent from the tree without a same-change supersession is `REQUIREMENT_REMOVED`. History (superseded and deprecated entries) may never be deleted. Outside git the check stays quiet.
295
+ - Without `--results`, `check` emits `PROOFS_UNCONFIRMED` and skips proof enforcement. The results file is never hashed into `build_id`. Generate it with the test runner's JUnit reporter, for example `bun test --reporter=junit --reporter-outfile=.spec-engine/results.xml`.
296
+ - Term citations: `UNDEFINED_TERM`, `SUPERSEDED_TERM_REFERENCED`, and `TERM_DRIFT` clear with `spec term confirm`.
297
+ - Losses: whenever a base ref resolves (`--base`, else `HEAD`), `check` reports the `IMPL_LOST`, `VERIFY_LOST`, and `SPEC_FILE_DELETED` rows `spec guard` would report against that ref. Guard's `REQUIREMENT_REMOVED` rows are dropped; the removal check already reports that id.
298
+ - Under `--ci` with no `--base`, a working tree whose tracked files equal `HEAD` makes every loss check pass vacuously, so `check` warns on stderr with the line to run instead: `spec check . --ci --base "$(git merge-base HEAD origin/<default>)"`. The exit code and stdout are unchanged.
299
+ - Coverage counts only Active requirements: `ORPHAN_REQ` and `UNVERIFIED_REQ` fire for Active entries only.
300
+
301
+ #### check.json
302
+
303
+ `spec-engine/check.json` regrades diagnostics. Absent means no rules.
304
+
305
+ ```json
306
+ { "severity": [
307
+ { "code": "UNVERIFIED_REQ", "severity": "error" },
308
+ { "code": "UNVERIFIED_REQ", "severity": "warning", "match": ["LEGACY", "BILLING-004"] }
309
+ ] }
310
+ ```
311
+
312
+ - `code` is any diagnostic code; `severity` is `error`, `warning`, or `off`.
313
+ - `match` lists domain keys and requirement ids. A scoped rule applies only to rows whose `req_id` is listed or carries a listed key.
314
+ - The most specific rule wins: an id match, then a domain match, then an unscoped rule. Among equals, the later rule wins.
315
+ - `off` drops the row from every output.
316
+ - Grading runs inside the operation, so the CLI, `spec_check`, and every other surface agree, and exit 1 follows the graded severities.
317
+ - A file that fails the schema (unknown code or key, a bad severity, a malformed match entry) exits 2 with `spec-engine/check.json: <path>: <reason>`.
318
+
319
+ `statementRules` configures the statement rules on every surface: the write-time refusal, `STATEMENT_RULES`, and `spec lint`.
320
+
321
+ ```json
322
+ { "severity": [],
323
+ "statementRules": {
324
+ "maxLength": 280,
325
+ "jargon": ["seam", "rung"],
326
+ "exemptNames": ["SPEC.json", "spec-engine/"],
327
+ "severity": { "no-jargon": "off", "length": "error" }
328
+ } }
329
+ ```
330
+
331
+ - `maxLength` defaults to 280. `jargon` replaces the default list (`seam`, `seams`, `rung`, `rungs`, `residue`, `matrix entry`, `stage runner`, `the stage`).
332
+ - `exemptNames` are product names `no-file-path` never reports. A name ending in `/` exempts every path under it.
333
+ - `severity` maps a rule to `error`, `warning`, or `off`. A rule with no entry is `error`. `error` refuses the write and grades the `STATEMENT_RULES` row error; `warning` writes, prints the finding on stderr, and grades the row warning; `off` turns the rule off everywhere. The `severity` array regrades by code after it.
334
+ - The rules: `one-sentence`, `length`, `no-file-path`, `no-test-narration`, `no-toolchain-layout`, `no-history`, `no-jargon`, `no-tool-statement`. Unknown keys exit 2.
335
+
336
+ ### spec guard
337
+
338
+ The pre-commit loss gate. Diffs the requirement derivation at `--against <ref>` (default `HEAD`) against the working tree, scoped to the files in `git diff`. Rebuilds the worktree index cold every run.
339
+
340
+ `--json` rows: `{ kind, req_id, file, line, detail }`, sorted by `(req_id, kind, file, line)`. `kind` is one of `REQUIREMENT_REMOVED`, `IMPL_LOST`, `VERIFY_LOST`, `SPEC_FILE_DELETED` (`req_id` null).
341
+
342
+ Exit: 0 clean / 1 any loss / 2 usage.
343
+
344
+ - A loss is suppressed only by ending the requirement in the same change: supersede it (either direction) or `spec deprecate` it. There is no override comment.
345
+ - A non-git tree, a repo with no `HEAD`, or an unknown ref prints `NOT_A_GIT_REPO` to stderr and exits 0.
346
+ - A ref that begins with `-` or holds a character outside `A-Z a-z 0-9 . _ / -` is refused before git runs: `guard` treats it as an unknown ref, and `check --base` exits 2.
347
+ - Text mode prints a `🛑 spec-guard:` block per requirement, written in the second person. Relay it verbatim to the user before proceeding.
348
+
349
+ ### spec map
350
+
351
+ `--json` rows keyed `(req_id, repo)`: `{ req_id, domain_key, req_status, req_spec_version, req_changed_at_version, repo, repo_pin, implemented: 0|1, verified: 0|1, test_levels }`. Text mode renders `src`, `test`, `src+test`, `—`.
352
+
353
+ Exit: 0 / 2.
354
+
355
+ ### spec get
356
+
357
+ One requirement's full record from the derived index, with the code sites its tags name. Tags are the only record of where a requirement is implemented and verified.
358
+
359
+ `--json`: the `Requirement` row `{ id, key, seq, status, superseded_by, text, why, source_file, line, spec_version, changed_at_version, superseded_at_version }` plus `implemented_in: [{ repo, file, line, kind }]` (implementing and documenting tags) and `verified_in: [{ repo, file, line, level }]` (verifying tags), each sorted by (repo, file, line). The same object is served by `GET /api/requirements/:id` and `spec_get`, and shown on the webapp `/requirements/:id` page. Text mode is a one-row table (`ID STATUS KEY SEQ CHANGED_AT TEXT`) followed by an `implemented in:` and a `verified in:` list of `file:line`.
360
+
361
+ Exit: 0 / 2. An unknown id prints `[]` on stdout, guidance on stderr, and exits 0. A malformed id exits 2.
362
+
363
+ ### spec list
364
+
365
+ Every requirement record in (key, seq) order. Unlike `query`, superseded and deprecated rows are included and nothing is ranked.
366
+
367
+ | Flag | Effect |
368
+ | --- | --- |
369
+ | `--domain KEY` | Only that domain (case-insensitive key). |
370
+ | `--status S` | Only that status: `active`, `draft`, `superseded`, `deprecated`. Any other word exits 2. |
371
+
372
+ `--json`: an array of `Requirement` rows, the same rows `GET /api/requirements` serves. Text mode is the `ID STATUS KEY SEQ CHANGED_AT TEXT` table.
373
+
374
+ Exit: 0 / 2. An empty result is `[]` under `--json`, guidance on stderr in text mode, exit 0.
375
+
376
+ ### spec query
377
+
378
+ FTS5 `MATCH` syntax. `--limit N` (default 10, max 1000).
379
+
380
+ Exit: 0 / 2 (bare `AND` or `OR` is an FTS5 grammar error).
381
+
382
+ - Wrap multi-word phrases in double quotes inside the shell-quoted arg.
383
+ - Results rank ascending (best first). Superseded requirements are excluded.
384
+ - Glossary term definitions ride the same index. A matching term appears in a separate Terms group in text mode, discriminated by `key` under `--json`.
385
+
386
+ ### spec relations
387
+
388
+ Text mode: mermaid `graph LR`, one node per requirement in a `relates` field, one undirected deduped edge per pair. `--json` rows: `{ from_id, to_id, source_file, line }`, sorted by (from, to).
389
+
390
+ Exit: 0 / 2. Empty graph is `[]` and exit 0.
391
+
392
+ ### spec provenance
393
+
394
+ Per requirement: the creating issue, the revising issues (`supersedes-via`, `amends-via`), the backing tests, and the `source_file:line` pointer.
395
+
396
+ | Flag | Effect |
397
+ | --- | --- |
398
+ | `[issueId]` positional | Display-only reverse filter. Never a join key. |
399
+ | `--resolve-issues` | Overlay tracker title/status/URL. Needs `SPEC_TRACKER_TOKEN`; degrades to bare ids without it. |
400
+
401
+ `--json` rows: `{ req_id, role, issue_id, source_file, line, req_status, implemented, verified, test_levels }`, sorted on the full composite key.
402
+
403
+ Exit: 0 / 2. Empty matrix is `[]` and exit 0.
404
+
405
+ ### spec resolve
406
+
407
+ Accepts multiple positionals, comma-split inside one positional, and absolute paths under the platform dir.
408
+
409
+ | Flag | Effect |
410
+ | --- | --- |
411
+ | `--req KEY-NNN` | Reverse query: every tag site for that requirement. Takes no file positionals. |
412
+
413
+ `--json` rows (`--req`): `{ req_id, repo, file, line, kind, level }` sorted by (repo, file, line). `kind` is `implements`, `verifies`, or `documents`.
414
+
415
+ Exit: 0 / 2. Empty result is `[]` and exit 0. Unknown id under `--req` is `[]`.
416
+
417
+ Path rules. `tags.file` is platform-relative: `<member>/<path>` for a file inside a declared member or a workspace package, and the bare `<path>` for a file a lone repository owns itself (its directory-named or `root` column). The key never carries the checkout's folder name.
418
+
419
+ - Multi-repo platform: pass `<repo>/<path>`, e.g. `spec resolve api/src/renew.ts .`
420
+ - Single repo: pass the path from the platform root, e.g. `spec resolve src/orders.ts .` or `spec resolve .github/workflows/ci.yml .`
421
+ - Absolute paths are normalized against `platformDir`. A path outside it exits 2.
422
+
423
+ ### spec propagation
424
+
425
+ One row per member repo: `{ repo, state, via_req_id, drifted }`. `state` is one of `MIGRATED_VERIFIED`, `MIGRATED_UNVERIFIED`, `ON_PREDECESSOR`, `ON_OTHER_DOMAIN_REQ`, `NO_DOMAIN_REFERENCE`.
426
+
427
+ Rows are in dependency order: a member comes after every member it depends on, read from platform-map's `dependsOn` (the platform's own package names in a member's manifest), and members at the same depth come by name. The same order is served by `/api/propagation/:id`, `spec_propagation`, and the webapp's `/propagation/:id` page. A platform with no in-platform dependencies lists members by name.
428
+
429
+ Exit: 0 / 2.
430
+
431
+ ### spec gate
432
+
433
+ `--json`: `{ pass, reason, repo, req_id, detail, status, changed_at_version, pinned_spec_version }`. `reason` is one of `PASS`, `NOT_FOUND`, `DRAFT`, `SUPERSEDED`, `DEPRECATED`, `VERSION_PIN`, decided in that order. Pin equality passes.
434
+
435
+ Exit: 0 pass / 1 fail / 2 unknown repo name or bad args.
436
+
437
+ ### spec init
438
+
439
+ Declares a member through platform-map and writes `spec-engine.member.json`.
440
+
441
+ | Flag | Effect |
442
+ | --- | --- |
443
+ | `--specs spec-engine@N` | Override the pin. |
444
+ | `--force` | Rewrite an existing config, preserving `ignore`. Any other key refuses, as does a malformed `ignore`. |
445
+ | `--platform <dir>` | The platform root, for a declared member checked out outside the platform folder. Its location is recorded in platform-map's per-user file first. |
446
+
447
+ `--json`: `{ action: "wrote", path, pin, source, declared, linked, agent_surface }`, `{ action: "already-configured", path, pin, extra_fields, declared, linked, agent_surface }`, or, on a platform directory, `{ action: "surface-only", agent_surface }`. `declared` lists the platform-map files written (the platform file, the marker), empty when the member was already declared; `linked` is the per-user file written under `--platform`, else null; `agent_surface` lists the agent-surface files written or changed, `[]` on a re-run.
448
+
449
+ Exit: 0 wrote or already configured / 2. Branch on `action`, not the exit code.
450
+
451
+ - Where the target sits decides what is written:
452
+ - a repository directly under a platform folder (a directory holding `spec-engine/`) that the platform file does not list is declared: a `platform-map.json` platform file entry at the root (the file is created when absent) and a `{ platform, member }` marker in the repository, then the pin. platform-map declares only a directory with a `.git` entry or a `package.json`; a plain folder is refused with exit 2;
453
+ - a declared member only gets the pin (a missing marker is written back);
454
+ - a workspace package of a monorepo gets a nested pin and no declaration; a repository inside a lone monorepo that is not a workspace package is refused;
455
+ - a directory under no platform gets the `spec-engine@1` fallback pin;
456
+ - a platform directory (holding `spec-engine/`) gets the agent surface and no pin.
457
+ - Every action then installs the agent surface into the repository (the member; the monorepo root for a workspace package), bundled in the npm package under `dist/agent/`. Hints for anything left alone go to stderr.
458
+ - `AGENTS.md`: the agent reference between a `spec-engine:agent-reference:begin` and a `spec-engine:agent-reference:end` HTML comment marker, appended or created, replaced in place when the markers exist; text outside them is never touched. A `CLAUDE.md` that does not mention `AGENTS.md` gets a hint only.
459
+ - `.claude/skills/req-author/`: written when absent; a differing copy is replaced only under `--force`.
460
+ - `.mcp.json`: a `spec` server entry (`spec mcp <platform>`) merged in; other keys and an existing `spec` entry are kept.
461
+ - `pre-commit`: `spec guard <platform>`, written to `.githooks/` or else `.git/hooks/` when no hook exists there; an existing hook gets a hint.
462
+ - With no platform located, the MCP entry and the hook are skipped.
463
+ - The platform root is platform-map's `locate()`: the nearest `platform-map.json` or `.git` boundary, a marker resolved through the parent directory or the per-user file; the parent directory when it holds `spec-engine/`. A `.git` boundary between the target and an outer `spec-engine/` hides that platform.
464
+ - Default pin: `--specs`, else the derived platform version, else `spec-engine@1` with a printed note.
465
+ - A resolved path with a `spec-engine` segment is refused, except on the engine's own checkout (root package named `@spec-engine/spec-engine`), where only the path below the platform root is judged.
466
+ - `ignore: ["dir", …]` excludes repo-relative directory prefixes from that repo's scans: `vendor` hides `vendor/x.ts` but not `src/vendor/x.ts`. A monorepo root column excludes its workspace package paths the same way. A monorepo's columns are its workspace manifest's packages.
467
+
468
+ ### spec domain
469
+
470
+ `spec domain new <KEY>` scaffolds `spec-engine/<KEY>/SPEC.json`. `spec domain list` prints one key per line; `--json` prints a sorted array of `{ key, scope }` (scope is the charter sentence or null), read from the filesystem.
471
+
472
+ `spec domain grammar <KEY> [platformDir] [--grammar ears|freeform] [--severity warning|error]` sets the envelope's `grammar` and/or `grammarSeverity` through the validating write path; every other field is written back as read. `--json` prints `{ key, grammar, grammar_severity, file }` (an unset field is null). An unknown domain, neither flag, or a value outside the enums exits 2. CLI only: no MCP tool or HTTP route.
473
+
474
+ ```shell
475
+ spec domain grammar CHCK --severity error
476
+ ```
477
+
478
+ Exit: 0 / 2.
479
+
480
+ ### spec migrate
481
+
482
+ Rewrites every `SPEC.json` that still carries the retired `livesIn` or `verifiedIn` fields through the write seam, which drops them. A file without them is not touched, so a second run writes nothing. Until a file is migrated, the fields are read and ignored, and the next write of a file drops them.
483
+
484
+ `--json`: `{ files }`, the platform-relative paths rewritten.
485
+
486
+ Exit: 0 / 2.
487
+
488
+ ### spec req
489
+
490
+ | Mode | Behavior |
491
+ | --- | --- |
492
+ | stdin not a TTY, no `--text` | Print the bare next unused id (e.g. `BILLING-010`), exit 0. No prompts, no writes. |
493
+ | `--json`, no `--text` | Print `{ domain, next_id }`. Same zero-write contract, even on a TTY. |
494
+ | `--text "<statement>"` | Append an Active entry with zero prompts. `--why`, `--relates`, `--issue <ticket>` fill the other fields and error without `--text`. `--relates` repeats and comma-separates. |
495
+ | `--draft` | Author the entry as Draft (with `--text`, or on a TTY). Off a TTY without `--text` it exits 2. |
496
+ | TTY, no flags | Interactive authoring. |
497
+
498
+ `--json` with `--text`: `{ id, file }`, plus `clauses: { pattern, condition, system, response }` when the statement parses as EARS.
499
+
500
+ Exit: 0 / 2 (ambiguous prefix, unknown domain, grammar refusal, statement-rule refusal).
501
+
502
+ - The domain prefix is case-insensitive: `bil` resolves to `BILLING`.
503
+ - The resolved domain's charter is echoed to stderr on every authoring path.
504
+ - Unresolvable `@<path>` refs warn on stderr and never block.
505
+
506
+ ### spec term
507
+
508
+ A term is a requirement row in the reserved `TERM` domain: definition in `statement`, headword in `term`, synonyms in `aliases`.
509
+
510
+ | Form | Effect |
511
+ | --- | --- |
512
+ | `spec term "<name>" --def "<definition>" [--aliases a,b] [--section s]` | Append a `TERM-NNN`. `--json`: `{ id, file }`. |
513
+ | `spec term` with no `--def` | Pure id query, mirrors `spec req`: bare next `TERM` id, or `{ domain, next_id }`. |
514
+ | `spec term list` | `id name status` per line, or `[{ id, term, status }]`. Reads the filesystem. |
515
+ | `spec term revise <TERM-NNN> --def "…"` | Rewrite the definition in place and bump the TERM envelope's `specVersion`. `--no-bump` opts out. `--json`: `{ id, file, spec_version }`. |
516
+ | `spec term confirm <KEY-NNN> <TERM-NNN>` | Re-pin a citation to the term's current version (clears `TERM_DRIFT`); re-point to the successor when the term is superseded (clears `SUPERSEDED_TERM_REFERENCED`). `--json`: `{ req_id, term_id, pinned, file }`. |
517
+
518
+ `spec supersede TERM-NNN` and `spec amend TERM-NNN --term/--aliases` work on term ids as on requirements.
519
+
520
+ Exit: 0 / 2.
521
+
522
+ ### spec glossary
523
+
524
+ | Form | Effect |
525
+ | --- | --- |
526
+ | `spec glossary` | Overwrite `GLOSSARY.md` from the TERM store. Byte-stable: fixed header, terms in id order, `## {section}` once per change, one `- **{term}** — {statement}` bullet each. |
527
+ | `--migrate` | One-time parse of an existing `GLOSSARY.md` into `TERM-001..N`. Skips when the TERM domain already has entries. |
528
+ | `--check` | Regenerate into a buffer and diff against the committed file. Exit 1 on drift. |
529
+
530
+ `--json`: `{ generated }`, `{ migrated }`, or `{ ok }`.
531
+
532
+ Exit: 0 / 1 (`--check` drift) / 2.
533
+
534
+ ### spec deprecate
535
+
536
+ Marks an Active/Draft requirement `deprecated`, records `--reason` on the entry, and removes every `@spec` tag of the id from the code. A line that holds only the tag (and its comment leader) is deleted; on a line with other text, only the tag and its level token go. Text mode prints the removed sites.
537
+
538
+ `--json`: `{ id, file, reason, removed: [{ req_id, repo, file, line, kind, level }] }`, the sites as they stood before the removal.
539
+
540
+ Tag sites come from the freshly built index, so `fixtures/` and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
541
+
542
+ Exit: 0 / 2.
543
+
544
+ ### spec supersede
545
+
546
+ Requires an Active target. Flips it to `superseded by NEW`, mints the successor in the domain file that holds the target, reindexes fresh, and rewrites every `@spec OLD` tag in the code to `@spec NEW`, keeping the level token and the rest of the line. Text mode prints the rewritten sites. Tag sites come from the freshly built index, so `fixtures/` and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
547
+
548
+ The successor copies the predecessor's `why`, `relates`, `cites`, `aliases`, `term`, and `section` unless a flag overrides the field. Its `issues` hold only the `created` row from `--issue`.
549
+
550
+ | Flag | Effect |
551
+ | --- | --- |
552
+ | `--text` | The successor statement. Required when not a TTY. |
553
+ | `--why`, `--relates` | Successor fields. Default to copies of the predecessor's. `--relates` repeats or comma-separates and replaces the whole list; an empty value clears it. |
554
+ | `--issue <ticket>` | Recorded as `supersedes-via` on the predecessor and `created` on the successor. |
555
+ | `--no-bump` | TERM domain only: keep the authored `specVersion`. No-op elsewhere. |
556
+
557
+ `--json`: `{ old_id, new_id, file, spec_version, retagged: [{ req_id, repo, file, line, kind, level }] }`, the rewritten sites (`req_id` is the old id). On a requirement domain `spec_version` is the derived domain version after the supersession, and the same number is stamped as the predecessor's `supersededAtVersion`.
558
+
559
+ Exit: 0 / 2. All guards run before the first byte is written.
560
+
561
+ ### spec move
562
+
563
+ Cross-domain supersede. Mints the successor as the next id in `<NEW-DOMAIN>` (which must exist), copies `statement`, `why`, `relates`, `cites`, `aliases`, `term`, and `section` (`--text` and `--why` override), flips the source to superseded with a cross-domain `supersededBy`, and rewrites every tag of the source to the successor as `spec supersede` does. The successor carries no `issues`.
564
+
565
+ `--json`: `{ old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged: [...] }`.
566
+
567
+ Exit: 0 / 2 (non-Active source, or the target is the source's own domain).
568
+
569
+ - Both envelopes validate before either is written.
570
+ - The source version advances; the target version is unchanged.
571
+
572
+ ### spec amend
573
+
574
+ Revise an Active or Draft entry in place. Same id, no version change.
575
+
576
+ | Flag | Effect |
577
+ | --- | --- |
578
+ | `--text`, `--why`, `--relates` | Field to change. At least one flag is required. Untouched fields stay byte-identical. `--relates` repeats or comma-separates and replaces the whole list (the `--aliases` idiom); an empty value clears it. |
579
+ | `--issue <ticket>` | Append `amends-via` provenance. |
580
+ | `--term`, `--aliases` | TERM ids only. |
581
+
582
+ `--json`: `{ id, file, fields_changed }`.
583
+
584
+ A released requirement is an Active entry present in its domain file at the newest `v*` tag merged into HEAD. Its promise does not change in place: a `--text`, `--why`, `--term`, or `--aliases` amend refuses with exit 2, naming the tag. Two paths stay open: `spec supersede` changes the promise, and an amend of only `--relates` or `--issue` writes in place. With no `v*` tag merged into HEAD, or outside git, every entry is unreleased. A Draft entry always amends.
585
+
586
+ Exit: 0 / 2 (superseded or deprecated target; a released entry's promise fields).
587
+
588
+ ### spec accept
589
+
590
+ Promotes a Draft requirement to Active. Every other field stays byte-identical and the domain version does not move. Reword a draft with `spec amend` first; end one with `spec deprecate`.
591
+
592
+ `--json`: `{ id, file }`.
593
+
594
+ Exit: 0 / 2 (the target is not Draft: nothing is written).
595
+
596
+ - A Draft never raises `ORPHAN_REQ` or `UNVERIFIED_REQ` and is not counted in the coverage report. A code tag on one is `DRAFT_REFERENCED` (warning).
597
+
598
+ ### spec lint
599
+
600
+ Every statement-rule finding. Reads `SPEC.json` directly, not the index.
601
+
602
+ | Form | Scope |
603
+ | --- | --- |
604
+ | `spec lint [platformDir]` | the platform |
605
+ | `spec lint <KEY> [platformDir]` | one domain |
606
+ | `spec lint <KEY-NNN> [platformDir]` | one requirement |
607
+ | `--all` | Draft requirements as well as Active ones |
608
+
609
+ Text mode prints `<id> <rule>: <detail> — <reason>` per finding on stdout, then rule 0 on stderr. `--json` prints a sorted array of `{ req_id, rule, detail, reason, source_file, line }`. The TERM domain is never judged. A lone positional that holds `spec-engine/` is the platform dir. CLI only: no MCP tool or HTTP route.
610
+
611
+ Exit: 0 clean / 1 any finding / 2 usage, unknown domain or id, or a bad `check.json`.
612
+
613
+ ### spec serve
614
+
615
+ Binds one address: `127.0.0.1`, or `--host <ip>` (a literal loopback, RFC 1918, or `100.64/10` address only; anything else exits 2). `--port 0` (default) picks a free port. `--probe` boots on loopback, fetches `/`, and exits 0 or 1. The startup line, `/api/platform` (`name`), and every page's title and sidebar name the platform (platform file `name`, else the directory name), never a path; `spec mcp` logs the same name on stderr.
616
+
617
+ A non-loopback `--host` is LAN mode: stdout prints `http://<ip>:<port>/?token=…`; every request needs that token (the query parameter is exchanged for an `HttpOnly; SameSite=Strict` cookie, then a 302 drops it), else 401. A `Host` other than loopback or the bound address is 403. Read-only unless `--allow-writes`: non-GET requests, `/provenance`, and `/api/provenance?resolve=1` answer 403. `--token` / `SPEC_SERVE_TOKEN` pin the token (≥ 16 of `A-Z a-z 0-9 _ -`); otherwise it is 256 random bits.
618
+
619
+ When the reindex after a write refuses on an invalid `SPEC.json` elsewhere on the platform, the route answers 422 `{ error: "invalid_domain_file", diagnostics: [...] }`, the rows in the `check --json` shape (the CLI exits 2 with the same rows).
620
+
621
+ Always-on routes: `/`, `/requirements[/:id]`, `/propagation/:id`, `/api/coverage`, `/api/report`, `/api/repos`, `/api/platform`, `/api/requirements[/:id]`, `/api/propagation/:id`, `/api/resolve?files=…`.
622
+
623
+ Flag-gated, 404 until enabled. `SPEC_FLAGS` is comma-separated; unknown keys are ignored.
624
+
625
+ | `SPEC_FLAGS` key | Routes |
626
+ | --- | --- |
627
+ | `query` | `/query`, `/api/query?q=&limit=` |
628
+ | `relations` | `/relations`, `/api/relations[?format=mermaid]` |
629
+ | `provenance` | `/provenance`, `/api/provenance[?resolve=1]`, `/api/provenance/by-issue?issue=` |
630
+ | `editor` | `/editor`, `POST` and `PUT /api/requirements`, `POST /api/requirements/:id/supersede`, `POST /api/requirements/:id/move`, `POST /api/requirements/:id/deprecate`, `POST /api/requirements/:id/accept` |
631
+ | `glossary`, `logs` | Placeholder pages |
632
+
633
+ Exit: 0 / 1 / 2.
634
+
635
+ ### spec docs
636
+
637
+ Serves the prebuilt docs site on `127.0.0.1`, fully offline.
638
+
639
+ - Takes no platform dir. Registers none of `--json`, `--out`, `--no-prompt`.
640
+ - The npm artifact ships the site at `dist/docs/`. A source checkout serves `packages/site/dist`; run `bun run build:site` first, else exit 2.
641
+ - `--probe` mirrors `serve --probe`.
642
+
643
+ Exit: 0 / 1 / 2.
644
+
645
+ ### spec mcp
646
+
647
+ MCP server over stdio. Register it as `{ "command": "spec", "args": ["mcp", "<platformDir>"] }`.
648
+
649
+ | Tool | Returns |
650
+ | --- | --- |
651
+ | `spec_query` | FTS hits, the `spec query --json` shape |
652
+ | `spec_resolve` | files → requirements |
653
+ | `spec_req_tags` | requirement → tag sites |
654
+ | `spec_coverage_report` | per-domain rollup `{ domain, active, implemented, verified, orphans, unverified }` |
655
+ | `spec_check` | diagnostic rows |
656
+ | `spec_propagation` | per-repo migration state |
657
+ | `spec_next_id` | `{ domain, next_id }`, nothing written |
658
+ | `spec_get` | one `Requirement` row with `implemented_in` and `verified_in`, or `[]` for an unknown id; the `spec get --json` shape |
659
+ | `spec_list` | `Requirement` rows in (key, seq) order, `domain` and `status` filters; the `spec list --json` shape |
660
+ | `spec_supersede` | `{ old_id, new_id, file, spec_version, retagged }`, the `spec supersede --json` shape; optional `why`, `relates`, `issue` arguments |
661
+ | `spec_deprecate` | `{ id, file, reason, removed }`, the `spec deprecate --json` shape |
662
+ | `spec_accept` | `{ id, file }`, the `spec accept --json` shape |
663
+ | `spec_req` | `{ id, file, clauses? }`, the `spec req --text --json` shape; `domain` (key or prefix) and `text` required, optional `why`, `relates`, `issue`, `draft` |
664
+ | `spec_amend` | `{ id, file, fields_changed }`, the `spec amend --json` shape; `id` required, optional `text`, `why`, `relates`, `issue` |
665
+ | `spec_move` | `{ old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged }`, the `spec move --json` shape; `id` and `newDomain` required, optional `text`, `why` |
666
+
667
+ Every tool call reindexes fresh. stdout is the protocol channel; chrome goes to stderr. One prompt, `author_requirements` (`brief` required, `domain` optional), returns a static authoring playbook with the brief and the domain's charter substituted. The engine runs no model.
668
+
669
+ Exit: 0 / 2.
670
+
671
+ ### Gotchas
672
+
673
+ - **`fixtures/` is globally ignored by the tag scanner.** Tags under any `fixtures/` folder are test data, not coverage claims.
674
+ - **The index owns nothing.** Never edit `.spec-engine/index.sqlite`. Edit the `SPEC.json` source and re-run `spec index`.
675
+ - **`storage_unavailable` / `SQLITE_IOERR*` means your environment.** The index runs in WAL mode and needs real file locks. A sandboxed process gets `SQLITE_IOERR_VNODE`; re-run unsandboxed or grant write and lock access to `<platformDir>/.spec-engine/`. `SQLITE_BUSY` means another `spec` process holds the index.
676
+ - **Empty is not an error.** Read commands exit 0 with `[]` and guidance on stderr.
677
+ - **Determinism is contractual.** JSON outputs are sorted and byte-stable.
678
+ - A directory passed as the last positional is `platformDir` only if it contains `spec-engine/`. Otherwise it is treated as a file, with a warning.