@spec-engine/spec-engine 0.1.1 → 0.3.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 (38) hide show
  1. package/dist/docs/404.html +4 -4
  2. package/dist/docs/_astro/{Code.DfIJP77p.css → Code.CAimF0uL.css} +1 -1
  3. package/dist/docs/_astro/{common.xlUlWolS.css → common.DslAiFsT.css} +1 -1
  4. package/dist/docs/agent-reference/index.html +8 -8
  5. package/dist/docs/architecture/index.html +8 -8
  6. package/dist/docs/checks/index.html +10 -10
  7. package/dist/docs/commands/index.html +14 -13
  8. package/dist/docs/format/index.html +10 -10
  9. package/dist/docs/getting-started/index.html +8 -8
  10. package/dist/docs/guides/adoption/index.html +9 -9
  11. package/dist/docs/guides/platform-setup/index.html +8 -8
  12. package/dist/docs/guides/single-repo/index.html +8 -8
  13. package/dist/docs/index.html +1 -1
  14. package/dist/docs/overview/index.html +28 -43
  15. package/dist/docs/pagefind/fragment/en_10bec64.pf_fragment +0 -0
  16. package/dist/docs/pagefind/fragment/en_435dca1.pf_fragment +0 -0
  17. package/dist/docs/pagefind/fragment/en_4bdd17f.pf_fragment +0 -0
  18. package/dist/docs/pagefind/fragment/en_6c144c1.pf_fragment +0 -0
  19. package/dist/docs/pagefind/fragment/en_8d65739.pf_fragment +0 -0
  20. package/dist/docs/pagefind/fragment/en_93176f1.pf_fragment +0 -0
  21. package/dist/docs/pagefind/fragment/en_adb4528.pf_fragment +0 -0
  22. package/dist/docs/pagefind/index/en_90d86c1.pf_index +0 -0
  23. package/dist/docs/pagefind/pagefind-entry.json +1 -1
  24. package/dist/docs/pagefind/pagefind.en_4cf42761c7.pf_meta +0 -0
  25. package/dist/docs/pagefind/wasm.en.pagefind +0 -0
  26. package/dist/docs/pagefind/wasm.unknown.pagefind +0 -0
  27. package/dist/docs/tags/index.html +8 -8
  28. package/dist/impl.js +1355 -653
  29. package/package.json +5 -1
  30. package/dist/docs/pagefind/fragment/en_591940a.pf_fragment +0 -0
  31. package/dist/docs/pagefind/fragment/en_5f79492.pf_fragment +0 -0
  32. package/dist/docs/pagefind/fragment/en_629887a.pf_fragment +0 -0
  33. package/dist/docs/pagefind/fragment/en_6339f38.pf_fragment +0 -0
  34. package/dist/docs/pagefind/fragment/en_9f5f7d8.pf_fragment +0 -0
  35. package/dist/docs/pagefind/fragment/en_a4de4f3.pf_fragment +0 -0
  36. package/dist/docs/pagefind/fragment/en_ae342c4.pf_fragment +0 -0
  37. package/dist/docs/pagefind/index/en_c844839.pf_index +0 -0
  38. package/dist/docs/pagefind/pagefind.en_f3a4ba28d6.pf_meta +0 -0
package/dist/impl.js CHANGED
@@ -35,6 +35,10 @@ var init_diagnostics = __esm(() => {
35
35
  BAD_STATUS: "BAD_STATUS",
36
36
  DANGLING_TAG: "DANGLING_TAG",
37
37
  SUPERSEDED_REFERENCED: "SUPERSEDED_REFERENCED",
38
+ DEPRECATED_REFERENCED: "DEPRECATED_REFERENCED",
39
+ STATEMENT_GRAMMAR: "STATEMENT_GRAMMAR",
40
+ DRAFT_REFERENCED: "DRAFT_REFERENCED",
41
+ GLOSSARY_DRIFT: "GLOSSARY_DRIFT",
38
42
  ORPHAN_REQ: "ORPHAN_REQ",
39
43
  UNVERIFIED_REQ: "UNVERIFIED_REQ",
40
44
  DRIFT: "DRIFT",
@@ -103,6 +107,8 @@ function orderDomain(d) {
103
107
  specVersion: d.specVersion,
104
108
  updated: d.updated,
105
109
  scope: d.scope ?? null,
110
+ grammar: d.grammar,
111
+ grammarSeverity: d.grammarSeverity,
106
112
  requirements: d.requirements.map((r) => ({
107
113
  id: r.id,
108
114
  status: r.status,
@@ -115,6 +121,7 @@ function orderDomain(d) {
115
121
  issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
116
122
  changedAtVersion: r.changedAtVersion,
117
123
  supersededAtVersion: r.supersededAtVersion,
124
+ deprecatedReason: r.deprecatedReason,
118
125
  term: r.term,
119
126
  aliases: r.aliases,
120
127
  cites: r.cites.map((c) => ({ term: c.term, pinned: c.pinned })),
@@ -157,6 +164,7 @@ var init_domain = __esm(() => {
157
164
  issues: z2.array(SpecIssueSchema).optional().default([]),
158
165
  changedAtVersion: z2.number().int().optional(),
159
166
  supersededAtVersion: z2.number().int().optional(),
167
+ deprecatedReason: z2.string().optional(),
160
168
  term: z2.string().optional(),
161
169
  aliases: z2.array(z2.string()).optional().default([]),
162
170
  cites: z2.array(SpecCiteSchema).optional().default([]),
@@ -168,9 +176,109 @@ var init_domain = __esm(() => {
168
176
  specVersion: z2.number().int().positive().optional(),
169
177
  updated: z2.string(),
170
178
  scope: z2.string().nullable().optional(),
179
+ grammar: z2.enum(["ears", "freeform"]).optional(),
180
+ grammarSeverity: z2.enum(["warning", "error"]).optional(),
171
181
  requirements: z2.array(SpecRequirementSchema)
172
182
  }).strict();
173
183
  });
184
+
185
+ // ../shared/src/ears.ts
186
+ function reject(problem) {
187
+ return { ok: false, problem, expected: EARS_EXPECTED };
188
+ }
189
+ function parseEarsStatement(raw) {
190
+ const statement = raw.trim();
191
+ if (statement === "")
192
+ return reject("the statement is empty");
193
+ if (!/\bshall\b/.test(statement)) {
194
+ return reject('missing the word "shall" \u2014 it separates who acts from the observable result (e.g. "spec check shall exit 1")');
195
+ }
196
+ for (const { keyword, pattern, re } of CONDITIONAL) {
197
+ if (!statement.startsWith(`${keyword} `))
198
+ continue;
199
+ const m2 = statement.match(re);
200
+ if (m2) {
201
+ const [, condition, system, response] = m2;
202
+ return {
203
+ ok: true,
204
+ clauses: {
205
+ pattern,
206
+ condition: condition.trim(),
207
+ system: system.trim().replace(/^the\s+/i, ""),
208
+ response: response.trim()
209
+ }
210
+ };
211
+ }
212
+ if (keyword === "If" && !/,\s*then\s/.test(statement)) {
213
+ return reject('starts with "If" but has no ", then" \u2014 the failure clause never hands over to the system');
214
+ }
215
+ return reject(`starts with "${keyword}" but the condition clause never ends \u2014 add a comma before naming the system that acts`);
216
+ }
217
+ const m = statement.match(ALWAYS_RE);
218
+ if (m) {
219
+ const [, system, response] = m;
220
+ if (/,/.test(system)) {
221
+ return reject('the clause before "shall" contains a comma but starts with no condition keyword (When/While/If/Where) \u2014 either drop the comma or lead with the condition');
222
+ }
223
+ return {
224
+ ok: true,
225
+ clauses: {
226
+ pattern: "always",
227
+ condition: null,
228
+ system: system.trim().replace(/^the\s+/i, ""),
229
+ response: response.trim()
230
+ }
231
+ };
232
+ }
233
+ return reject('nothing follows "shall" \u2014 the observable result is missing');
234
+ }
235
+ var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE;
236
+ var init_ears = __esm(() => {
237
+ EARS_EXPECTED = [
238
+ 'When <trigger>, the <system> shall <result> \u2014 e.g. "When an unknown id is passed, spec resolve shall print [] and exit 0."',
239
+ "While <state>, the <system> shall <result>",
240
+ "If <failure>, then the <system> shall <result>",
241
+ "Where <feature>, the <system> shall <result>",
242
+ "The <system> shall <result>"
243
+ ].join(`
244
+ `);
245
+ CONDITIONAL = [
246
+ { keyword: "When", pattern: "when", re: /^When\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s },
247
+ { keyword: "While", pattern: "while", re: /^While\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s },
248
+ { keyword: "If", pattern: "if", re: /^If\s+(.+?),\s*then\s+(.+?)\s+shall\s+(.+)$/s },
249
+ { keyword: "Where", pattern: "where", re: /^Where\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s }
250
+ ];
251
+ ALWAYS_RE = /^(.+?)\s+shall\s+(.+)$/s;
252
+ });
253
+
254
+ // ../shared/src/flags.ts
255
+ function parseFlags(value) {
256
+ const enabled = new Set;
257
+ for (const rawKey of (value ?? "").split(",")) {
258
+ const key = rawKey.trim().toLowerCase();
259
+ if (FEATURE_KEYS.includes(key))
260
+ enabled.add(key);
261
+ }
262
+ return enabled;
263
+ }
264
+ function featureEnabled(key) {
265
+ return parseFlags(process.env.SPEC_FLAGS).has(key);
266
+ }
267
+ function featureDisabledMessage(key) {
268
+ return `${FEATURES[key].label} is not enabled (coming soon). To try it locally, run with SPEC_FLAGS=${key}.`;
269
+ }
270
+ var FEATURES, FEATURE_KEYS;
271
+ var init_flags = __esm(() => {
272
+ FEATURES = {
273
+ glossary: { label: "Glossary" },
274
+ relations: { label: "Relations" },
275
+ provenance: { label: "Provenance" },
276
+ query: { label: "Query" },
277
+ editor: { label: "Editor" },
278
+ logs: { label: "Logs" }
279
+ };
280
+ FEATURE_KEYS = Object.keys(FEATURES);
281
+ });
174
282
  // ../shared/src/net.ts
175
283
  function isLoopbackHostname(hostname) {
176
284
  return LOOPBACK_HOSTNAMES.has(hostname);
@@ -340,7 +448,7 @@ CREATE INDEX IF NOT EXISTS idx_term_citations_term ON term_citations(term_id);
340
448
  -- Invariant #4). source_file + line locate the authored **Issues:** field line.
341
449
  -- Exactly ONE index, on req_id (SC3) \u2014 never on issue_id, which would read as
342
450
  -- treating the opaque payload as a lookup key (PROV-02, grep-fenced in Plan 04).
343
- -- @spec PROV-001
451
+ -- @spec PROV-004
344
452
  CREATE TABLE IF NOT EXISTS provenance (
345
453
  id INTEGER PRIMARY KEY AUTOINCREMENT,
346
454
  req_id TEXT NOT NULL,
@@ -398,6 +506,7 @@ LEFT JOIN tags t
398
506
  -- cell. The Q4 ORPHAN_REQ / Q5 UNVERIFIED_REQ queries (sqlite.ts) carry the
399
507
  -- SAME literal exclusion so a term never fires a coverage ERROR. The match is
400
508
  -- the exact literal 'TERM' \u2014 never a real domain key.
509
+ -- @spec SCHM-017
401
510
  WHERE r.key != 'TERM'
402
511
  GROUP BY r.id, repos.name;
403
512
 
@@ -424,7 +533,7 @@ WHERE r.changed_at_version > repos.pinned_spec_version;
424
533
  -- the Wave-D diagnostics. term.key='TERM' keeps the VIEW scoped to the reserved
425
534
  -- glossary domain. Present-and-empty until Wave C populates term_citations; the
426
535
  -- diagnostic (TERM_DRIFT) reads it in Wave E.
427
- -- @spec CHCK-005
536
+ -- @spec CHCK-018
428
537
  CREATE VIEW IF NOT EXISTS term_drift AS
429
538
  SELECT
430
539
  tc.req_id AS req_id,
@@ -510,7 +619,7 @@ var init_schema = __esm(() => {
510
619
  // ../shared/src/storage.ts
511
620
  var REQUIREMENT_STATUSES;
512
621
  var init_storage = __esm(() => {
513
- REQUIREMENT_STATUSES = ["Active", "Superseded", "Draft", "Retired"];
622
+ REQUIREMENT_STATUSES = ["Active", "Superseded", "Draft", "Deprecated"];
514
623
  });
515
624
 
516
625
  // ../shared/src/index.ts
@@ -518,6 +627,8 @@ var init_src = __esm(() => {
518
627
  init_config();
519
628
  init_diagnostics();
520
629
  init_domain();
630
+ init_ears();
631
+ init_flags();
521
632
  init_net();
522
633
  init_propagation();
523
634
  init_schema();
@@ -550,6 +661,23 @@ var init_constants = __esm(() => {
550
661
  OUT_HELP = `DB path override (default: <platformDir>/${INDEX_DIR}/${INDEX_DB_FILENAME})`;
551
662
  });
552
663
 
664
+ // src/parser/requirementLine.ts
665
+ function findRequirementIdLine(rawLines, id, from = 0) {
666
+ const needle = `"id": "${id}"`;
667
+ const exact = `${SEAM_INDENT}${needle}`;
668
+ const start = from > 0 ? from : 0;
669
+ let loose = -1;
670
+ for (let i = start;i < rawLines.length; i++) {
671
+ const line = rawLines[i];
672
+ if (line === exact || line === `${exact},`)
673
+ return i;
674
+ if (loose < 0 && line.includes(needle))
675
+ loose = i;
676
+ }
677
+ return loose;
678
+ }
679
+ var SEAM_INDENT = " ";
680
+
553
681
  // src/parser/domainJson.ts
554
682
  function parseDomainJsonFile(opts) {
555
683
  let parsed;
@@ -587,6 +715,7 @@ function parseDomainJson(opts) {
587
715
  const updated = data.updated ?? null;
588
716
  const rawLines = text.split(`
589
717
  `);
718
+ let lineCursor = 0;
590
719
  const working = [];
591
720
  const relatesAcc = {
592
721
  relations: [],
@@ -607,7 +736,10 @@ function parseDomainJson(opts) {
607
736
  };
608
737
  for (let idx = 0;idx < data.requirements.length; idx++) {
609
738
  const r = data.requirements[idx];
610
- const line = deriveRequirementLine(rawLines, r.id, idx);
739
+ const found = findRequirementIdLine(rawLines, r.id, lineCursor);
740
+ const line = found >= 0 ? found + 1 : idx + 1;
741
+ if (found >= 0)
742
+ lineCursor = found + 1;
611
743
  working.push(mapRequirementRow(r, key, sourceFile, spec_version, line));
612
744
  flattenRelates(r, sourceFile, line, relatesAcc);
613
745
  flattenIssues(r, sourceFile, line, issuesAcc);
@@ -631,11 +763,6 @@ function parseDomainJson(opts) {
631
763
  term_citations: termAcc.citations
632
764
  };
633
765
  }
634
- function deriveRequirementLine(rawLines, id, idx) {
635
- const needle = `"id": "${id}"`;
636
- const found = rawLines.findIndex((l) => l.includes(needle));
637
- return found >= 0 ? found + 1 : idx + 1;
638
- }
639
766
  function mapRequirementRow(r, key, sourceFile, spec_version, line) {
640
767
  const id = r.id;
641
768
  const seq = Number(id.split("-")[1]);
@@ -772,7 +899,8 @@ var init_domainJson = __esm(() => {
772
899
  active: "Active",
773
900
  draft: "Draft",
774
901
  superseded: "Superseded",
775
- retired: "Retired"
902
+ deprecated: "Deprecated",
903
+ retired: "Deprecated"
776
904
  });
777
905
  VALID_ROLES = new Set(["created", "supersedes-via", "amends-via"]);
778
906
  });
@@ -1251,10 +1379,11 @@ class SqliteStorage {
1251
1379
  return row ?? null;
1252
1380
  }
1253
1381
  listDomains() {
1254
- return [];
1382
+ return this.#db.query(LIST_DOMAINS_SQL).all();
1255
1383
  }
1256
- getDomain(_key) {
1257
- return null;
1384
+ getDomain(key) {
1385
+ const row = this.#db.query(GET_DOMAIN_SQL).get({ key });
1386
+ return row ?? null;
1258
1387
  }
1259
1388
  listRequirements(opts) {
1260
1389
  const key = opts?.key;
@@ -1292,6 +1421,8 @@ class SqliteStorage {
1292
1421
  for (const sql of [
1293
1422
  Q1_DANGLING_TAG_SQL,
1294
1423
  Q2_SUPERSEDED_REFERENCED_SQL,
1424
+ Q2B_DEPRECATED_REFERENCED_SQL,
1425
+ Q2D_DRAFT_REFERENCED_SQL,
1295
1426
  Q3_DRIFT_SQL,
1296
1427
  Q4_ORPHAN_REQ_SQL,
1297
1428
  Q5_UNVERIFIED_REQ_SQL,
@@ -1651,6 +1782,34 @@ FROM tags t
1651
1782
  JOIN requirements r ON r.id = t.req_id
1652
1783
  WHERE r.status = 'Superseded'
1653
1784
  ORDER BY t.repo, t.file, t.line
1785
+ `, Q2B_DEPRECATED_REFERENCED_SQL = `
1786
+ SELECT
1787
+ 'DEPRECATED_REFERENCED' AS code,
1788
+ t.repo AS repo,
1789
+ t.file AS source_file,
1790
+ t.line AS line,
1791
+ t.req_id AS req_id,
1792
+ ('Tag references deprecated requirement ' || t.req_id ||
1793
+ ' \u2014 it is end-of-life with no successor') AS detail,
1794
+ 'error' AS severity
1795
+ FROM tags t
1796
+ JOIN requirements r ON r.id = t.req_id
1797
+ WHERE r.status = 'Deprecated'
1798
+ ORDER BY t.repo, t.file, t.line
1799
+ `, Q2D_DRAFT_REFERENCED_SQL = `
1800
+ SELECT
1801
+ 'DRAFT_REFERENCED' AS code,
1802
+ t.repo AS repo,
1803
+ t.file AS source_file,
1804
+ t.line AS line,
1805
+ t.req_id AS req_id,
1806
+ ('Tag references Draft requirement ' || t.req_id ||
1807
+ ' \u2014 code is bound to a promise nobody approved yet') AS detail,
1808
+ 'warning' AS severity
1809
+ FROM tags t
1810
+ JOIN requirements r ON r.id = t.req_id
1811
+ WHERE r.status = 'Draft'
1812
+ ORDER BY t.repo, t.file, t.line
1654
1813
  `, Q3_DRIFT_SQL = `
1655
1814
  SELECT
1656
1815
  'DRIFT' AS code,
@@ -1680,6 +1839,7 @@ WHERE r.status = 'Active'
1680
1839
  -- code-coverage obligation \u2014 exclude the reserved TERM domain so a migrated
1681
1840
  -- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
1682
1841
  -- mirrors the coverage VIEW exclusion (WHERE r.key != 'TERM') in schema.ts.
1842
+ -- @spec SCHM-017
1683
1843
  AND r.key != 'TERM'
1684
1844
  AND NOT EXISTS (
1685
1845
  SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
@@ -1853,7 +2013,7 @@ SELECT
1853
2013
  FROM repos
1854
2014
  WHERE repos.name != 'spec-engine'
1855
2015
  ORDER BY repos.name
1856
- `, FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status != 'Superseded' ORDER BY rank ASC LIMIT $limit`, RESOLVE_BY_FILES_SQL_PREFIX = "SELECT DISTINCT r.id, r.key, r.seq, r.status, r.superseded_by, r.text, r.why, r.source_file, r.line, r.spec_version, r.changed_at_version, r.superseded_at_version FROM tags t JOIN requirements r ON r.id = t.req_id WHERE t.file IN (", RESOLVE_BY_FILES_SQL_SUFFIX = ") ORDER BY r.key, r.seq", LIST_REPOS_SQL = "SELECT name, path, pinned_spec_version FROM repos ORDER BY name", GET_REPO_SQL = "SELECT name, path, pinned_spec_version FROM repos WHERE name = $name", LIST_TAGS_SQL = `SELECT id, req_id, repo, file, line, kind, level FROM tags
2016
+ `, FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status NOT IN ('Superseded', 'Deprecated') ORDER BY rank ASC LIMIT $limit`, RESOLVE_BY_FILES_SQL_PREFIX = "SELECT DISTINCT r.id, r.key, r.seq, r.status, r.superseded_by, r.text, r.why, r.source_file, r.line, r.spec_version, r.changed_at_version, r.superseded_at_version FROM tags t JOIN requirements r ON r.id = t.req_id WHERE t.file IN (", RESOLVE_BY_FILES_SQL_SUFFIX = ") ORDER BY r.key, r.seq", LIST_REPOS_SQL = "SELECT name, path, pinned_spec_version FROM repos ORDER BY name", GET_REPO_SQL = "SELECT name, path, pinned_spec_version FROM repos WHERE name = $name", LIST_DOMAINS_SQL = "SELECT key, owner, schema, spec_version, source_repo FROM domains ORDER BY key", GET_DOMAIN_SQL = "SELECT key, owner, schema, spec_version, source_repo FROM domains WHERE key = $key", LIST_TAGS_SQL = `SELECT id, req_id, repo, file, line, kind, level FROM tags
1857
2017
  WHERE ($repo IS NULL OR repo = $repo)
1858
2018
  AND ($req_id IS NULL OR req_id = $req_id)
1859
2019
  AND ($file IS NULL OR file = $file)
@@ -2659,8 +2819,8 @@ var init_errors = __esm(() => {
2659
2819
  });
2660
2820
 
2661
2821
  // src/commands/_shared.ts
2662
- import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
2663
- import { dirname as dirname5 } from "path";
2822
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync4 } from "fs";
2823
+ import { dirname as dirname5, join as join7 } from "path";
2664
2824
  function assertContainedPath(resolved, platformDir, subject) {
2665
2825
  if (!isContainedPath(resolved, platformDir)) {
2666
2826
  console.error(`${subject} path must be inside platformDir (resolved to ${resolved})`);
@@ -2692,6 +2852,23 @@ async function reindexAndListTags(platformDir, reqId) {
2692
2852
  storage2.close();
2693
2853
  }
2694
2854
  }
2855
+ function warnIfIndexStale(platformDir, dbPath) {
2856
+ try {
2857
+ const dbMtime = statSync4(dbPath).mtimeMs;
2858
+ const specsDir = join7(platformDir, "spec-engine");
2859
+ for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
2860
+ if (!entry.isDirectory())
2861
+ continue;
2862
+ const specPath = join7(specsDir, entry.name, "SPEC.json");
2863
+ if (!existsSync4(specPath))
2864
+ continue;
2865
+ if (statSync4(specPath).mtimeMs > dbMtime) {
2866
+ console.error(`spec: warning \u2014 spec-engine/${entry.name}/SPEC.json changed after the index was built; results may be stale. Pass --fresh to rebuild.`);
2867
+ return;
2868
+ }
2869
+ }
2870
+ } catch {}
2871
+ }
2695
2872
  async function withReadStorage(opts, fn) {
2696
2873
  const { platformDir, dbPath, fresh } = opts;
2697
2874
  try {
@@ -2704,6 +2881,8 @@ async function withReadStorage(opts, fn) {
2704
2881
  try {
2705
2882
  if (needsIndex || storage2.listRepos().length === 0) {
2706
2883
  await runIndex({ platformDir, storage: storage2 });
2884
+ } else {
2885
+ warnIfIndexStale(platformDir, dbPath);
2707
2886
  }
2708
2887
  await fn(storage2);
2709
2888
  } finally {
@@ -2944,6 +3123,173 @@ function matchesSegment(pat, seg) {
2944
3123
  return p === pat.length;
2945
3124
  }
2946
3125
 
3126
+ // src/authoring/domains.ts
3127
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
3128
+ import { join as join8 } from "path";
3129
+ function normalizeDomainKey(raw) {
3130
+ return raw.toUpperCase().replace(/\s+/g, "");
3131
+ }
3132
+ function listDomainKeys(platformDir) {
3133
+ const specsDir = join8(platformDir, "spec-engine");
3134
+ return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(join8(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
3135
+ }
3136
+ async function domainScope(platformDir, key) {
3137
+ const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
3138
+ if (!existsSync5(jsonPath))
3139
+ return null;
3140
+ try {
3141
+ const env = JSON.parse(await Bun.file(jsonPath).text());
3142
+ if (typeof env.scope !== "string" || env.scope.trim() === "")
3143
+ return null;
3144
+ return env.scope;
3145
+ } catch (e) {
3146
+ if (e instanceof SyntaxError)
3147
+ return null;
3148
+ console.error(`spec: could not read scope for ${key}: ${e.message}`);
3149
+ return null;
3150
+ }
3151
+ }
3152
+ async function domainsWithScope(platformDir) {
3153
+ const keys = listDomainKeys(platformDir);
3154
+ return Promise.all(keys.map(async (key) => ({ key, scope: await domainScope(platformDir, key) })));
3155
+ }
3156
+ function scaffoldDomainObject(key, today) {
3157
+ return {
3158
+ key,
3159
+ owner: null,
3160
+ ...key === "TERM" ? { specVersion: 1 } : {},
3161
+ updated: today,
3162
+ requirements: []
3163
+ };
3164
+ }
3165
+ function maxSeqOf(domain2) {
3166
+ const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
3167
+ return reqs.reduce((m, r) => {
3168
+ const id = typeof r?.id === "string" ? r.id : "";
3169
+ const seq = Number(id.split("-")[1]);
3170
+ return Number.isFinite(seq) ? Math.max(m, seq) : m;
3171
+ }, 0);
3172
+ }
3173
+ async function headMaxSeq(platformDir, key) {
3174
+ try {
3175
+ const proc = Bun.spawn(["git", "-C", join8(platformDir, "spec-engine", key), "show", "HEAD:./SPEC.json"], { stdout: "pipe", stderr: "ignore" });
3176
+ const text = await new Response(proc.stdout).text();
3177
+ if (await proc.exited !== 0)
3178
+ return 0;
3179
+ return maxSeqOf(JSON.parse(text));
3180
+ } catch {
3181
+ return 0;
3182
+ }
3183
+ }
3184
+ async function nextRequirementId(platformDir, key) {
3185
+ const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
3186
+ if (!existsSync5(jsonPath))
3187
+ return `${key}-001`;
3188
+ const domain2 = JSON.parse(await Bun.file(jsonPath).text());
3189
+ const maxSeq = Math.max(maxSeqOf(domain2), await headMaxSeq(platformDir, key));
3190
+ const next = String(maxSeq + 1).padStart(3, "0");
3191
+ return `${key}-${next}`;
3192
+ }
3193
+ var init_domains = __esm(() => {
3194
+ init_src();
3195
+ });
3196
+
3197
+ // src/authoring/filerefs.ts
3198
+ import { existsSync as existsSync6 } from "fs";
3199
+ import { resolve as resolve7 } from "path";
3200
+ function extractRefsFromText(s) {
3201
+ const out = [];
3202
+ for (const m of s.matchAll(FILE_REF_RE)) {
3203
+ const candidate = m[1].replace(/\.+$/, "");
3204
+ if (candidate.includes("/"))
3205
+ out.push(candidate);
3206
+ }
3207
+ return out;
3208
+ }
3209
+ function resolveFileRef(platformDir, ref) {
3210
+ const root = resolve7(platformDir);
3211
+ const abs = resolve7(root, ref);
3212
+ if (!(abs === root || abs.startsWith(`${root}/`)))
3213
+ return false;
3214
+ return existsSync6(abs);
3215
+ }
3216
+ var FILE_REF_RE;
3217
+ var init_filerefs = __esm(() => {
3218
+ FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
3219
+ });
3220
+
3221
+ // src/check/filerefs.ts
3222
+ import { join as join9 } from "path";
3223
+ async function readDomain(platformDir, key) {
3224
+ try {
3225
+ const raw = await Bun.file(join9(platformDir, "spec-engine", key, "SPEC.json")).text();
3226
+ const doc = JSON.parse(raw);
3227
+ return {
3228
+ raw,
3229
+ requirements: Array.isArray(doc.requirements) ? doc.requirements : []
3230
+ };
3231
+ } catch {
3232
+ return null;
3233
+ }
3234
+ }
3235
+ function rowsForRequirement(platformDir, key, reqId, livesIn, line) {
3236
+ if (!Array.isArray(livesIn))
3237
+ return [];
3238
+ const rows = [];
3239
+ for (const entry of livesIn) {
3240
+ if (typeof entry !== "string")
3241
+ continue;
3242
+ const ref = entry.startsWith("@") ? entry.slice(1) : entry;
3243
+ if (ref.length === 0 || resolveFileRef(platformDir, ref))
3244
+ continue;
3245
+ rows.push({
3246
+ code: DiagnosticCode.BROKEN_FILE_REF,
3247
+ severity: "error",
3248
+ repo: null,
3249
+ source_file: `spec-engine/${key}/SPEC.json`,
3250
+ line,
3251
+ req_id: reqId,
3252
+ detail: `livesIn entry ${entry} does not resolve to a file under the platform root`
3253
+ });
3254
+ }
3255
+ return rows;
3256
+ }
3257
+ function rowsForDomain(platformDir, key, domain2) {
3258
+ const rows = [];
3259
+ const rawLines = domain2.raw.split(`
3260
+ `);
3261
+ let lineCursor = 0;
3262
+ for (const req of domain2.requirements) {
3263
+ if (typeof req?.id !== "string")
3264
+ continue;
3265
+ const found = findRequirementIdLine(rawLines, req.id, lineCursor);
3266
+ if (found >= 0)
3267
+ lineCursor = found + 1;
3268
+ const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
3269
+ if (!CHECKED_STATUSES.has(status))
3270
+ continue;
3271
+ rows.push(...rowsForRequirement(platformDir, key, req.id, req.livesIn, found >= 0 ? found + 1 : 0));
3272
+ }
3273
+ return rows;
3274
+ }
3275
+ async function brokenFileRefDiagnostics(platformDir) {
3276
+ const rows = [];
3277
+ for (const key of listDomainKeys(platformDir)) {
3278
+ const domain2 = await readDomain(platformDir, key);
3279
+ if (domain2 !== null)
3280
+ rows.push(...rowsForDomain(platformDir, key, domain2));
3281
+ }
3282
+ rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? "") || a.detail.localeCompare(b.detail));
3283
+ return rows;
3284
+ }
3285
+ var CHECKED_STATUSES;
3286
+ var init_filerefs2 = __esm(() => {
3287
+ init_src();
3288
+ init_domains();
3289
+ init_filerefs();
3290
+ CHECKED_STATUSES = new Set(["active", "draft"]);
3291
+ });
3292
+
2947
3293
  // src/check/format.ts
2948
3294
  function compareRepoNullsLast(a, b) {
2949
3295
  if (a === b)
@@ -2987,6 +3333,74 @@ function renderDiagnostics(rows, mode) {
2987
3333
  `);
2988
3334
  }
2989
3335
 
3336
+ // src/check/grammar.ts
3337
+ import { join as join10 } from "path";
3338
+ async function readDeclaredDomain(platformDir, key) {
3339
+ try {
3340
+ const raw = await Bun.file(join10(platformDir, "spec-engine", key, "SPEC.json")).text();
3341
+ const doc = JSON.parse(raw);
3342
+ if (doc.grammar !== "ears")
3343
+ return null;
3344
+ return {
3345
+ raw,
3346
+ severity: doc.grammarSeverity === "error" ? "error" : "warning",
3347
+ requirements: Array.isArray(doc.requirements) ? doc.requirements : []
3348
+ };
3349
+ } catch {
3350
+ return null;
3351
+ }
3352
+ }
3353
+ function rowsForDomain2(key, domain2) {
3354
+ const rows = [];
3355
+ const rawLines = domain2.raw.split(`
3356
+ `);
3357
+ let lineCursor = 0;
3358
+ for (const req of domain2.requirements) {
3359
+ if (typeof req?.id !== "string")
3360
+ continue;
3361
+ const found = findRequirementIdLine(rawLines, req.id, lineCursor);
3362
+ if (found >= 0)
3363
+ lineCursor = found + 1;
3364
+ if (typeof req.statement !== "string")
3365
+ continue;
3366
+ const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
3367
+ if (!CHECKED_STATUSES2.has(status))
3368
+ continue;
3369
+ const result = parseEarsStatement(req.statement);
3370
+ if (result.ok)
3371
+ continue;
3372
+ rows.push({
3373
+ code: DiagnosticCode.STATEMENT_GRAMMAR,
3374
+ severity: domain2.severity,
3375
+ repo: null,
3376
+ source_file: `spec-engine/${key}/SPEC.json`,
3377
+ line: found >= 0 ? found + 1 : 0,
3378
+ req_id: req.id,
3379
+ detail: `${result.problem}. Expected one of:
3380
+ ${result.expected}`
3381
+ });
3382
+ }
3383
+ return rows;
3384
+ }
3385
+ async function statementGrammarDiagnostics(platformDir) {
3386
+ const rows = [];
3387
+ for (const key of listDomainKeys(platformDir)) {
3388
+ if (key === "TERM")
3389
+ continue;
3390
+ const domain2 = await readDeclaredDomain(platformDir, key);
3391
+ if (domain2 !== null)
3392
+ rows.push(...rowsForDomain2(key, domain2));
3393
+ }
3394
+ rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
3395
+ return rows;
3396
+ }
3397
+ var CHECKED_STATUSES2;
3398
+ var init_grammar = __esm(() => {
3399
+ init_src();
3400
+ init_domains();
3401
+ CHECKED_STATUSES2 = new Set(["active", "draft"]);
3402
+ });
3403
+
2990
3404
  // src/check/proven.ts
2991
3405
  function normalizeToSegments(path) {
2992
3406
  const out = [];
@@ -3154,7 +3568,8 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
3154
3568
  for (const b of baseReqs) {
3155
3569
  if (changeIds.has(b.id))
3156
3570
  continue;
3157
- const exempt = b.supersededBy != null && changeIds.has(b.supersededBy) || changeReqs.some((y) => y.supersedes === b.id);
3571
+ const activeAtBase = (b.status ?? "").toLowerCase() === "active";
3572
+ const exempt = activeAtBase && (b.supersededBy != null && changeIds.has(b.supersededBy) || changeReqs.some((y) => y.supersedes === b.id));
3158
3573
  if (exempt)
3159
3574
  continue;
3160
3575
  out.push({
@@ -3163,7 +3578,7 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
3163
3578
  line: 0,
3164
3579
  repo: null,
3165
3580
  req_id: b.id,
3166
- detail: `${b.id} was present in the base ref but is absent from the change with no approved supersession`,
3581
+ detail: activeAtBase ? `${b.id} was present in the base ref but is absent from the change with no approved supersession` : `${b.id} (${b.status}) was present in the base ref but is absent from the change \u2014 a requirement is never deleted, and its history is the record`,
3167
3582
  severity: "error"
3168
3583
  });
3169
3584
  }
@@ -3193,11 +3608,11 @@ function normalizeHandle(h) {
3193
3608
  return (h.startsWith("@") ? h.slice(1) : h).toLowerCase();
3194
3609
  }
3195
3610
  function isNewTerminalFlip(base, change) {
3196
- if (change !== "superseded" && change !== "retired")
3611
+ if (!TERMINAL_STATUSES.has(change))
3197
3612
  return false;
3198
3613
  if (base === undefined)
3199
3614
  return false;
3200
- if (base === "superseded" || base === "retired")
3615
+ if (TERMINAL_STATUSES.has(base))
3201
3616
  return false;
3202
3617
  return base !== change;
3203
3618
  }
@@ -3226,8 +3641,76 @@ function unapprovedStatusFlip(baseReqs, changeReqs, codeowners, approvedBy, relP
3226
3641
  }
3227
3642
  return out;
3228
3643
  }
3644
+ var TERMINAL_STATUSES;
3229
3645
  var init_statusflip = __esm(() => {
3230
3646
  init_src();
3647
+ TERMINAL_STATUSES = new Set(["superseded", "deprecated", "retired"]);
3648
+ });
3649
+
3650
+ // src/check/supersedes.ts
3651
+ import { join as join11 } from "path";
3652
+ async function entriesForDomain(platformDir, key) {
3653
+ let raw;
3654
+ let reqs;
3655
+ try {
3656
+ raw = await Bun.file(join11(platformDir, "spec-engine", key, "SPEC.json")).text();
3657
+ const doc = JSON.parse(raw);
3658
+ reqs = Array.isArray(doc.requirements) ? doc.requirements : [];
3659
+ } catch {
3660
+ return [];
3661
+ }
3662
+ const out = [];
3663
+ const rawLines = raw.split(`
3664
+ `);
3665
+ let lineCursor = 0;
3666
+ for (const r of reqs) {
3667
+ if (typeof r?.id !== "string")
3668
+ continue;
3669
+ const found = findRequirementIdLine(rawLines, r.id, lineCursor);
3670
+ if (found >= 0)
3671
+ lineCursor = found + 1;
3672
+ out.push({
3673
+ id: r.id,
3674
+ supersedes: typeof r.supersedes === "string" ? r.supersedes : null,
3675
+ line: found >= 0 ? found + 1 : 0,
3676
+ sourceFile: `spec-engine/${key}/SPEC.json`
3677
+ });
3678
+ }
3679
+ return out;
3680
+ }
3681
+ async function collectEntries(platformDir) {
3682
+ const out = [];
3683
+ for (const key of listDomainKeys(platformDir)) {
3684
+ out.push(...await entriesForDomain(platformDir, key));
3685
+ }
3686
+ return out;
3687
+ }
3688
+ async function supersedesPointerDiagnostics(platformDir) {
3689
+ const entries = await collectEntries(platformDir);
3690
+ const ids = new Set(entries.map((e) => e.id));
3691
+ const rows = [];
3692
+ for (const e of entries) {
3693
+ if (e.supersedes === null)
3694
+ continue;
3695
+ const detail = e.supersedes === e.id ? `${e.id} declares supersedes on itself` : ids.has(e.supersedes) ? null : `${e.id} supersedes ${e.supersedes} which does not exist`;
3696
+ if (detail === null)
3697
+ continue;
3698
+ rows.push({
3699
+ code: DiagnosticCode.BROKEN_SUPERSEDE,
3700
+ severity: "error",
3701
+ repo: null,
3702
+ source_file: e.sourceFile,
3703
+ line: e.line,
3704
+ req_id: e.id,
3705
+ detail
3706
+ });
3707
+ }
3708
+ rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
3709
+ return rows;
3710
+ }
3711
+ var init_supersedes = __esm(() => {
3712
+ init_src();
3713
+ init_domains();
3231
3714
  });
3232
3715
 
3233
3716
  // src/check/unsourced.ts
@@ -3521,24 +4004,255 @@ var init_junit = __esm(() => {
3521
4004
  ]);
3522
4005
  });
3523
4006
 
3524
- // src/commands/check.ts
3525
- var exports_check = {};
3526
- __export(exports_check, {
3527
- checkCommand: () => checkCommand
4007
+ // src/authoring/edit.ts
4008
+ function localToday() {
4009
+ const d = new Date;
4010
+ return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
4011
+ }
4012
+
4013
+ // src/commands/glossary.ts
4014
+ var exports_glossary = {};
4015
+ __export(exports_glossary, {
4016
+ parseGlossary: () => parseGlossary,
4017
+ glossaryDriftDiagnostic: () => glossaryDriftDiagnostic,
4018
+ glossaryCommand: () => glossaryCommand,
4019
+ generateGlossary: () => generateGlossary
3528
4020
  });
3529
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync } from "fs";
3530
- import { dirname as dirname7, join as join7, resolve as resolve7 } from "path";
4021
+ import { existsSync as existsSync7, readFileSync } from "fs";
4022
+ import { join as join12, resolve as resolve8 } from "path";
3531
4023
  import { defineCommand as defineCommand3 } from "citty";
3532
- function readCodeownersText(platformDir) {
3533
- const path = join7(platformDir, ".github", "CODEOWNERS");
3534
- if (!existsSync5(path))
3535
- return null;
3536
- return readFileSync(path, "utf-8");
3537
- }
3538
- function domainReqsFromText(text, platformRel) {
3539
- let parsed;
3540
- try {
3541
- parsed = JSON.parse(text);
4024
+ function parseGlossary(md) {
4025
+ const terms = [];
4026
+ let section = null;
4027
+ let buf = null;
4028
+ const flush = () => {
4029
+ if (buf === null)
4030
+ return;
4031
+ const m = buf.match(/^\*\*(.+?)\*\*/);
4032
+ if (m) {
4033
+ const remainder = buf.slice(m[0].length);
4034
+ const statement = remainder.replace(/^ \u2014 /, "").trim();
4035
+ terms.push({ term: m[1], statement, section });
4036
+ }
4037
+ buf = null;
4038
+ };
4039
+ for (const line of md.split(`
4040
+ `)) {
4041
+ if (line.startsWith("## ")) {
4042
+ flush();
4043
+ section = line.slice(3).trim();
4044
+ } else if (line.startsWith("- ")) {
4045
+ flush();
4046
+ buf = line.slice(2);
4047
+ } else if (buf !== null) {
4048
+ if (line.trim() === "")
4049
+ flush();
4050
+ else
4051
+ buf += ` ${line.trim()}`;
4052
+ }
4053
+ }
4054
+ flush();
4055
+ return terms;
4056
+ }
4057
+ function generateGlossary(terms) {
4058
+ const parts = ["# Glossary", "", GLOSSARY_INTRO];
4059
+ let section;
4060
+ for (const t of terms) {
4061
+ if (t.section !== section) {
4062
+ section = t.section;
4063
+ if (section !== null && section !== undefined) {
4064
+ parts.push("", `## ${section}`, "");
4065
+ }
4066
+ }
4067
+ parts.push(`- **${t.term}** \u2014 ${t.statement}`);
4068
+ }
4069
+ return `${parts.join(`
4070
+ `)}
4071
+ `;
4072
+ }
4073
+ function resolvePlatform(platformDirRaw) {
4074
+ const platformDir = resolve8(platformDirRaw ?? process.cwd());
4075
+ try {
4076
+ assertSpecPlatform(platformDir);
4077
+ } catch (e) {
4078
+ handleNotAPlatform(e);
4079
+ }
4080
+ return platformDir;
4081
+ }
4082
+ function readStoreTerms(platformDir) {
4083
+ const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
4084
+ if (!existsSync7(specPath))
4085
+ return [];
4086
+ const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
4087
+ const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
4088
+ return reqs.filter((r) => String(r.status).toLowerCase() === "active").sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0).map((r) => ({
4089
+ term: String(r.term ?? ""),
4090
+ statement: String(r.statement ?? ""),
4091
+ section: r.section ?? null
4092
+ }));
4093
+ }
4094
+ function glossaryPath(platformDir) {
4095
+ return join12(platformDir, "GLOSSARY.md");
4096
+ }
4097
+ async function migrateGlossary(platformDir, json) {
4098
+ const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
4099
+ const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
4100
+ if (!existsSync7(specPath)) {
4101
+ console.error(`spec glossary: no TERM domain (expected ${relFile} under ${platformDir})`);
4102
+ process.exit(EXIT.USAGE);
4103
+ }
4104
+ const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
4105
+ const existing = Array.isArray(domain2.requirements) ? domain2.requirements : [];
4106
+ if (existing.length > 0) {
4107
+ console.error(`spec glossary: TERM domain already holds ${existing.length} entries \u2014 migration skipped`);
4108
+ if (json)
4109
+ console.log(JSON.stringify({ migrated: 0, skipped: true }));
4110
+ else
4111
+ console.log("migration skipped (TERM domain not empty)");
4112
+ return;
4113
+ }
4114
+ const gPath = glossaryPath(platformDir);
4115
+ if (!existsSync7(gPath)) {
4116
+ console.error(`spec glossary: no GLOSSARY.md to migrate (expected ${gPath})`);
4117
+ process.exit(EXIT.USAGE);
4118
+ }
4119
+ const parsed = parseGlossary(readFileSync(gPath, "utf8"));
4120
+ domain2.requirements = parsed.map((t, i) => ({
4121
+ id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
4122
+ status: "active",
4123
+ statement: t.statement,
4124
+ term: t.term,
4125
+ why: null,
4126
+ supersedes: null,
4127
+ supersededBy: null,
4128
+ relates: [],
4129
+ livesIn: [],
4130
+ issues: [],
4131
+ aliases: [],
4132
+ cites: [],
4133
+ changedAtVersion: 1,
4134
+ section: t.section
4135
+ }));
4136
+ domain2.updated = localToday();
4137
+ const res = await validateAndWrite(specPath, domain2, relFile);
4138
+ if (!res.ok) {
4139
+ for (const diag of res.diagnostics)
4140
+ console.error(`spec glossary: ${diag.detail}`);
4141
+ process.exit(EXIT.USAGE);
4142
+ }
4143
+ const count = parsed.length;
4144
+ if (json)
4145
+ console.log(JSON.stringify({ migrated: count, file: relFile }));
4146
+ else
4147
+ console.log(`migrated ${count} terms into ${relFile}`);
4148
+ }
4149
+ async function writeGlossary(platformDir, json) {
4150
+ const terms = readStoreTerms(platformDir);
4151
+ await Bun.write(glossaryPath(platformDir), generateGlossary(terms));
4152
+ if (json)
4153
+ console.log(JSON.stringify({ generated: terms.length, file: "GLOSSARY.md" }));
4154
+ else
4155
+ console.log(`generated GLOSSARY.md from ${terms.length} terms`);
4156
+ }
4157
+ function glossaryDriftDiagnostic(platformDir) {
4158
+ const terms = readStoreTerms(platformDir);
4159
+ if (terms.length === 0)
4160
+ return null;
4161
+ const gPath = glossaryPath(platformDir);
4162
+ if (!existsSync7(gPath))
4163
+ return null;
4164
+ const generated = generateGlossary(terms);
4165
+ const committed = readFileSync(gPath, "utf8");
4166
+ if (generated === committed)
4167
+ return null;
4168
+ return {
4169
+ code: DiagnosticCode.GLOSSARY_DRIFT,
4170
+ severity: "warning",
4171
+ repo: null,
4172
+ source_file: "GLOSSARY.md",
4173
+ line: 0,
4174
+ req_id: null,
4175
+ detail: "GLOSSARY.md does not match the TERM store \u2014 run `spec glossary .` to regenerate the human view"
4176
+ };
4177
+ }
4178
+ function checkGlossary(platformDir, json) {
4179
+ const generated = generateGlossary(readStoreTerms(platformDir));
4180
+ const gPath = glossaryPath(platformDir);
4181
+ const committed = existsSync7(gPath) ? readFileSync(gPath, "utf8") : "";
4182
+ if (generated === committed) {
4183
+ if (json)
4184
+ console.log(JSON.stringify({ ok: true }));
4185
+ else
4186
+ console.error("glossary round-trip: OK (committed GLOSSARY.md == generated)");
4187
+ return;
4188
+ }
4189
+ console.error("glossary drift: committed GLOSSARY.md != generated from the TERM store");
4190
+ console.error("run `spec glossary .` to regenerate the human view from the store");
4191
+ if (json)
4192
+ console.log(JSON.stringify({ ok: false }));
4193
+ process.exit(EXIT.FAILURE);
4194
+ }
4195
+ var TERM_KEY = "TERM", GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
4196
+ docs need one of these ideas, use the term below \u2014 not a synonym. Terms are ordered
4197
+ by the data model, outside-in.`, glossaryCommand;
4198
+ var init_glossary = __esm(() => {
4199
+ init_src();
4200
+ init_constants();
4201
+ init_discover();
4202
+ init__shared();
4203
+ glossaryCommand = defineCommand3({
4204
+ meta: {
4205
+ name: "glossary",
4206
+ description: "Generate GLOSSARY.md from the TERM store (byte-stable); --migrate parses GLOSSARY.md into TERM-001..N; --check fails on drift (committed != generated)"
4207
+ },
4208
+ args: {
4209
+ platformDir: {
4210
+ type: "positional",
4211
+ required: false,
4212
+ description: "Platform directory containing spec-engine/ (default: cwd)"
4213
+ },
4214
+ migrate: {
4215
+ type: "boolean",
4216
+ description: "One-time: parse the committed GLOSSARY.md into TERM-001..N (idempotent-skip)"
4217
+ },
4218
+ check: {
4219
+ type: "boolean",
4220
+ description: "Fail (exit 1) if the committed GLOSSARY.md differs from the generated output"
4221
+ },
4222
+ json: { type: "boolean", description: "Emit a JSON result instead of the text summary" }
4223
+ },
4224
+ async run({ args }) {
4225
+ const platformDir = resolvePlatform(args.platformDir);
4226
+ const json = Boolean(args.json);
4227
+ if (args.migrate) {
4228
+ await migrateGlossary(platformDir, json);
4229
+ } else if (args.check) {
4230
+ checkGlossary(platformDir, json);
4231
+ } else {
4232
+ await writeGlossary(platformDir, json);
4233
+ }
4234
+ }
4235
+ });
4236
+ });
4237
+
4238
+ // src/commands/check.ts
4239
+ var exports_check = {};
4240
+ __export(exports_check, {
4241
+ checkCommand: () => checkCommand
4242
+ });
4243
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
4244
+ import { dirname as dirname7, join as join13, resolve as resolve9 } from "path";
4245
+ import { defineCommand as defineCommand4 } from "citty";
4246
+ function readCodeownersText(platformDir) {
4247
+ const path = join13(platformDir, ".github", "CODEOWNERS");
4248
+ if (!existsSync8(path))
4249
+ return null;
4250
+ return readFileSync2(path, "utf-8");
4251
+ }
4252
+ function domainReqsFromText(text, platformRel) {
4253
+ let parsed;
4254
+ try {
4255
+ parsed = JSON.parse(text);
3542
4256
  } catch (err) {
3543
4257
  const msg = err instanceof Error ? err.message : String(err);
3544
4258
  return {
@@ -3570,7 +4284,7 @@ async function ingestResults(storage2, platformDir, resultsArg, jsonMode, diagno
3570
4284
  }
3571
4285
  return { resultsParsed: undefined, verifyingTags: undefined };
3572
4286
  }
3573
- const resultsPath = resolve7(platformDir, resultsArg);
4287
+ const resultsPath = resolve9(platformDir, resultsArg);
3574
4288
  if (!isContainedPath(resultsPath, platformDir)) {
3575
4289
  console.error(`spec check: --results path must be inside platformDir (resolved to ${resultsPath})`);
3576
4290
  try {
@@ -3590,7 +4304,7 @@ async function collectChangeReqs(canonicalDir) {
3590
4304
  const relPathById = new Map;
3591
4305
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
3592
4306
  const platformRel = `spec-engine/${rel}`;
3593
- const text = await Bun.file(join7(canonicalDir, rel)).text();
4307
+ const text = await Bun.file(join13(canonicalDir, rel)).text();
3594
4308
  const parsed = domainReqsFromText(text, platformRel);
3595
4309
  for (const r of parsed.reqs) {
3596
4310
  reqs.push(r);
@@ -3630,7 +4344,7 @@ async function runGovernanceGate(storage2, platformDir, args, diagnostics2, resu
3630
4344
  } catch {}
3631
4345
  process.exit(EXIT.USAGE);
3632
4346
  }
3633
- const canonicalDir = join7(platformDir, "spec-engine");
4347
+ const canonicalDir = join13(platformDir, "spec-engine");
3634
4348
  const change = await collectChangeReqs(canonicalDir);
3635
4349
  const base = collectBaseReqs(platformDir, ref, diagnostics2);
3636
4350
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
@@ -3656,10 +4370,13 @@ function exitAfterCrash(e, storage2) {
3656
4370
  var checkCommand;
3657
4371
  var init_check = __esm(() => {
3658
4372
  init_src();
4373
+ init_filerefs2();
4374
+ init_grammar();
3659
4375
  init_propagation_teeth();
3660
4376
  init_proven();
3661
4377
  init_removed();
3662
4378
  init_statusflip();
4379
+ init_supersedes();
3663
4380
  init_unsourced();
3664
4381
  init_constants();
3665
4382
  init_discover();
@@ -3669,7 +4386,8 @@ var init_check = __esm(() => {
3669
4386
  init_fs();
3670
4387
  init_sqlite();
3671
4388
  init__shared();
3672
- checkCommand = defineCommand3({
4389
+ init_glossary();
4390
+ checkCommand = defineCommand4({
3673
4391
  meta: {
3674
4392
  name: "check",
3675
4393
  description: "Cross-repo integrity check. --ci rebuilds the index cold (rm db+wal+shm). Exits 1 on any error-severity diagnostic."
@@ -3718,7 +4436,7 @@ var init_check = __esm(() => {
3718
4436
  }
3719
4437
  },
3720
4438
  async run({ args }) {
3721
- const platformDir = resolve7(args.platformDir ?? process.cwd());
4439
+ const platformDir = resolve9(args.platformDir ?? process.cwd());
3722
4440
  const outArg = args.out;
3723
4441
  const dbPath = resolveDbPath(platformDir, outArg);
3724
4442
  if (outArg)
@@ -3747,12 +4465,24 @@ var init_check = __esm(() => {
3747
4465
  const provenance = storage2.listProvenance();
3748
4466
  diagnostics2.push(...unsourcedChanges(superseded, provenance));
3749
4467
  }
4468
+ diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
4469
+ diagnostics2.push(...await brokenFileRefDiagnostics(platformDir));
4470
+ diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
4471
+ const glossaryDrift = glossaryDriftDiagnostic(platformDir);
4472
+ if (glossaryDrift !== null)
4473
+ diagnostics2.push(glossaryDrift);
3750
4474
  const results = await ingestResults(storage2, platformDir, args.results, !!args.json, diagnostics2);
3751
4475
  await runGovernanceGate(storage2, platformDir, {
3752
4476
  base: args.base,
3753
4477
  approvedBy: args.approvedBy,
3754
4478
  requireOwnerApproval: args.requireOwnerApproval
3755
4479
  }, diagnostics2, results);
4480
+ if (!args.base && gitRefResolves(platformDir, "HEAD")) {
4481
+ const change = await collectChangeReqs(join13(platformDir, "spec-engine"));
4482
+ const baseParseSink = [];
4483
+ const base = collectBaseReqs(platformDir, "HEAD", baseParseSink);
4484
+ diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
4485
+ }
3756
4486
  const output = renderDiagnostics(diagnostics2, args.json ? "json" : "text");
3757
4487
  console.log(output);
3758
4488
  if (!args.json) {
@@ -3768,63 +4498,6 @@ var init_check = __esm(() => {
3768
4498
  });
3769
4499
  });
3770
4500
 
3771
- // src/authoring/domains.ts
3772
- import { existsSync as existsSync6, readdirSync } from "fs";
3773
- import { join as join8 } from "path";
3774
- function normalizeDomainKey(raw) {
3775
- return raw.toUpperCase().replace(/\s+/g, "");
3776
- }
3777
- function listDomainKeys(platformDir) {
3778
- const specsDir = join8(platformDir, "spec-engine");
3779
- return readdirSync(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync6(join8(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
3780
- }
3781
- async function domainScope(platformDir, key) {
3782
- const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
3783
- if (!existsSync6(jsonPath))
3784
- return null;
3785
- try {
3786
- const env = JSON.parse(await Bun.file(jsonPath).text());
3787
- if (typeof env.scope !== "string" || env.scope.trim() === "")
3788
- return null;
3789
- return env.scope;
3790
- } catch (e) {
3791
- if (e instanceof SyntaxError)
3792
- return null;
3793
- console.error(`spec: could not read scope for ${key}: ${e.message}`);
3794
- return null;
3795
- }
3796
- }
3797
- async function domainsWithScope(platformDir) {
3798
- const keys = listDomainKeys(platformDir);
3799
- return Promise.all(keys.map(async (key) => ({ key, scope: await domainScope(platformDir, key) })));
3800
- }
3801
- function scaffoldDomainObject(key, today) {
3802
- return {
3803
- key,
3804
- owner: null,
3805
- ...key === "TERM" ? { specVersion: 1 } : {},
3806
- updated: today,
3807
- requirements: []
3808
- };
3809
- }
3810
- async function nextRequirementId(platformDir, key) {
3811
- const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
3812
- if (!existsSync6(jsonPath))
3813
- return `${key}-001`;
3814
- const domain2 = JSON.parse(await Bun.file(jsonPath).text());
3815
- const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
3816
- const maxSeq = reqs.reduce((m, r) => {
3817
- const id = typeof r?.id === "string" ? r.id : "";
3818
- const seq = Number(id.split("-")[1]);
3819
- return Number.isFinite(seq) ? Math.max(m, seq) : m;
3820
- }, 0);
3821
- const next = String(maxSeq + 1).padStart(3, "0");
3822
- return `${key}-${next}`;
3823
- }
3824
- var init_domains = __esm(() => {
3825
- init_src();
3826
- });
3827
-
3828
4501
  // src/commands/domain.ts
3829
4502
  var exports_domain = {};
3830
4503
  __export(exports_domain, {
@@ -3832,9 +4505,9 @@ __export(exports_domain, {
3832
4505
  domainListCommand: () => domainListCommand,
3833
4506
  domainCommand: () => domainCommand
3834
4507
  });
3835
- import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
3836
- import { dirname as dirname8, join as join9, resolve as resolve8 } from "path";
3837
- import { defineCommand as defineCommand4 } from "citty";
4508
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
4509
+ import { dirname as dirname8, join as join14, resolve as resolve10 } from "path";
4510
+ import { defineCommand as defineCommand5 } from "citty";
3838
4511
  var domainNewCommand, domainListCommand, domainCommand;
3839
4512
  var init_domain2 = __esm(() => {
3840
4513
  init_src();
@@ -3842,7 +4515,7 @@ var init_domain2 = __esm(() => {
3842
4515
  init_constants();
3843
4516
  init_discover();
3844
4517
  init__shared();
3845
- domainNewCommand = defineCommand4({
4518
+ domainNewCommand = defineCommand5({
3846
4519
  meta: {
3847
4520
  name: "new",
3848
4521
  description: "Scaffold a fresh spec-engine/<KEY>/SPEC.json (input normalized)"
@@ -3861,7 +4534,7 @@ var init_domain2 = __esm(() => {
3861
4534
  },
3862
4535
  async run({ args }) {
3863
4536
  const raw = args.name;
3864
- const platformDir = resolve8(args.platformDir ?? process.cwd());
4537
+ const platformDir = resolve10(args.platformDir ?? process.cwd());
3865
4538
  const key = normalizeDomainKey(raw);
3866
4539
  if (key !== raw) {
3867
4540
  console.log(`spec domain: normalized "${raw}" \u2192 ${key}`);
@@ -3871,21 +4544,21 @@ var init_domain2 = __esm(() => {
3871
4544
  process.exit(EXIT.USAGE);
3872
4545
  return;
3873
4546
  }
3874
- const dest = join9(platformDir, "spec-engine", key, "SPEC.json");
3875
- const resolvedDest = resolve8(dest);
3876
- const resolvedRoot = resolve8(platformDir);
4547
+ const dest = join14(platformDir, "spec-engine", key, "SPEC.json");
4548
+ const resolvedDest = resolve10(dest);
4549
+ const resolvedRoot = resolve10(platformDir);
3877
4550
  if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
3878
4551
  console.error(`spec domain new: refusing to write outside platformDir (${resolvedDest})`);
3879
4552
  process.exit(EXIT.USAGE);
3880
4553
  return;
3881
4554
  }
3882
- if (existsSync7(dest)) {
4555
+ if (existsSync9(dest)) {
3883
4556
  console.error(`refusing to overwrite ${dest}`);
3884
4557
  process.exit(EXIT.USAGE);
3885
4558
  return;
3886
4559
  }
3887
- const mdSibling = join9(dirname8(dest), "SPEC.md");
3888
- if (existsSync7(mdSibling)) {
4560
+ const mdSibling = join14(dirname8(dest), "SPEC.md");
4561
+ if (existsSync9(mdSibling)) {
3889
4562
  console.error(`refusing to create ${dest}: domain ${key} already exists as spec-engine/${key}/SPEC.md ` + "\u2014 migrate the SPEC.md first (a fresh empty SPEC.json would shadow it on the next index)");
3890
4563
  process.exit(EXIT.USAGE);
3891
4564
  return;
@@ -3905,7 +4578,7 @@ var init_domain2 = __esm(() => {
3905
4578
  console.log(`created ${relFile}`);
3906
4579
  }
3907
4580
  });
3908
- domainListCommand = defineCommand4({
4581
+ domainListCommand = defineCommand5({
3909
4582
  meta: {
3910
4583
  name: "list",
3911
4584
  description: "List domain keys (sorted, read from the filesystem)"
@@ -3922,7 +4595,7 @@ var init_domain2 = __esm(() => {
3922
4595
  }
3923
4596
  },
3924
4597
  async run({ args }) {
3925
- const platformDir = resolve8(args.platformDir ?? process.cwd());
4598
+ const platformDir = resolve10(args.platformDir ?? process.cwd());
3926
4599
  try {
3927
4600
  assertSpecPlatform(platformDir);
3928
4601
  } catch (e) {
@@ -3937,7 +4610,7 @@ var init_domain2 = __esm(() => {
3937
4610
  }
3938
4611
  }
3939
4612
  });
3940
- domainCommand = defineCommand4({
4613
+ domainCommand = defineCommand5({
3941
4614
  meta: {
3942
4615
  name: "domain",
3943
4616
  description: "Manage spec domains (spec-engine/<KEY>/)"
@@ -4016,15 +4689,15 @@ var exports_map = {};
4016
4689
  __export(exports_map, {
4017
4690
  mapCommand: () => mapCommand
4018
4691
  });
4019
- import { resolve as resolve9 } from "path";
4020
- import { defineCommand as defineCommand5 } from "citty";
4692
+ import { resolve as resolve11 } from "path";
4693
+ import { defineCommand as defineCommand6 } from "citty";
4021
4694
  var mapCommand;
4022
4695
  var init_map = __esm(() => {
4023
4696
  init_constants();
4024
4697
  init_discover();
4025
4698
  init_prompt();
4026
4699
  init__shared();
4027
- mapCommand = defineCommand5({
4700
+ mapCommand = defineCommand6({
4028
4701
  meta: {
4029
4702
  name: "map",
4030
4703
  description: "Render the cross-repo coverage matrix from the coverage VIEW. --json emits a deterministically sorted JSON array."
@@ -4053,7 +4726,7 @@ var init_map = __esm(() => {
4053
4726
  }
4054
4727
  },
4055
4728
  async run({ args }) {
4056
- const platformDir = resolve9(args.platformDir ?? process.cwd());
4729
+ const platformDir = resolve11(args.platformDir ?? process.cwd());
4057
4730
  const outArg = args.out;
4058
4731
  const dbPath = resolveDbPath(platformDir, outArg);
4059
4732
  if (outArg)
@@ -4103,15 +4776,15 @@ var exports_propagation = {};
4103
4776
  __export(exports_propagation, {
4104
4777
  propagationCommand: () => propagationCommand
4105
4778
  });
4106
- import { resolve as resolve10 } from "path";
4107
- import { defineCommand as defineCommand6 } from "citty";
4779
+ import { resolve as resolve12 } from "path";
4780
+ import { defineCommand as defineCommand7 } from "citty";
4108
4781
  var propagationCommand;
4109
4782
  var init_propagation2 = __esm(() => {
4110
4783
  init_constants();
4111
4784
  init_discover();
4112
4785
  init_prompt();
4113
4786
  init__shared();
4114
- propagationCommand = defineCommand6({
4787
+ propagationCommand = defineCommand7({
4115
4788
  meta: {
4116
4789
  name: "propagation",
4117
4790
  description: "Classify each member repo's relationship to a target requirement (PROP-01). States: MIGRATED_VERIFIED, MIGRATED_UNVERIFIED, ON_PREDECESSOR, ON_OTHER_DOMAIN_REQ, NO_DOMAIN_REFERENCE."
@@ -4151,7 +4824,7 @@ var init_propagation2 = __esm(() => {
4151
4824
  process.exit(EXIT.USAGE);
4152
4825
  return;
4153
4826
  }
4154
- const platformDir = resolve10(args.platformDir ?? process.cwd());
4827
+ const platformDir = resolve12(args.platformDir ?? process.cwd());
4155
4828
  const outArg = args.out;
4156
4829
  const dbPath = resolveDbPath(platformDir, outArg);
4157
4830
  if (outArg)
@@ -4225,8 +4898,8 @@ var exports_query = {};
4225
4898
  __export(exports_query, {
4226
4899
  queryCommand: () => queryCommand
4227
4900
  });
4228
- import { resolve as resolve11 } from "path";
4229
- import { defineCommand as defineCommand7 } from "citty";
4901
+ import { resolve as resolve13 } from "path";
4902
+ import { defineCommand as defineCommand8 } from "citty";
4230
4903
  var queryCommand;
4231
4904
  var init_query = __esm(() => {
4232
4905
  init_src();
@@ -4234,7 +4907,7 @@ var init_query = __esm(() => {
4234
4907
  init_discover();
4235
4908
  init_prompt();
4236
4909
  init__shared();
4237
- queryCommand = defineCommand7({
4910
+ queryCommand = defineCommand8({
4238
4911
  meta: {
4239
4912
  name: "query",
4240
4913
  description: "FTS5 retrieval over requirement text + why. Default LIMIT 10. Query syntax follows SQLite FTS5 MATCH (wrap phrases in double quotes; literal AND/OR/NOT are operators)."
@@ -4291,7 +4964,7 @@ var init_query = __esm(() => {
4291
4964
  process.exit(EXIT.USAGE);
4292
4965
  return;
4293
4966
  }
4294
- const platformDir = resolve11(args.platformDir ?? process.cwd());
4967
+ const platformDir = resolve13(args.platformDir ?? process.cwd());
4295
4968
  const outArg = args.out;
4296
4969
  const dbPath = resolveDbPath(platformDir, outArg);
4297
4970
  if (outArg)
@@ -4380,8 +5053,8 @@ var exports_relations = {};
4380
5053
  __export(exports_relations, {
4381
5054
  relationsCommand: () => relationsCommand
4382
5055
  });
4383
- import { resolve as resolve12 } from "path";
4384
- import { defineCommand as defineCommand8 } from "citty";
5056
+ import { resolve as resolve14 } from "path";
5057
+ import { defineCommand as defineCommand9 } from "citty";
4385
5058
  function formatNoRelations(platformDir) {
4386
5059
  return [
4387
5060
  `No Relates links indexed under ${platformDir}.`,
@@ -4394,7 +5067,7 @@ var init_relations = __esm(() => {
4394
5067
  init_constants();
4395
5068
  init_prompt();
4396
5069
  init__shared();
4397
- relationsCommand = defineCommand8({
5070
+ relationsCommand = defineCommand9({
4398
5071
  meta: {
4399
5072
  name: "relations",
4400
5073
  description: "Render the Relates links between requirements as a mermaid entity diagram. --json emits the sorted relation rows."
@@ -4423,7 +5096,7 @@ var init_relations = __esm(() => {
4423
5096
  }
4424
5097
  },
4425
5098
  async run({ args }) {
4426
- const platformDir = resolve12(args.platformDir ?? process.cwd());
5099
+ const platformDir = resolve14(args.platformDir ?? process.cwd());
4427
5100
  const outArg = args.out;
4428
5101
  const dbPath = resolveDbPath(platformDir, outArg);
4429
5102
  if (outArg)
@@ -4510,7 +5183,8 @@ function renderProvenance(rows, mode) {
4510
5183
  function decorateRow(row, resolved) {
4511
5184
  const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
4512
5185
  if (resolved?.ok && resolved.title && resolved.url) {
4513
- return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}`;
5186
+ const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
5187
+ return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
4514
5188
  }
4515
5189
  return `${prefix} (${TOKEN_HINT})`;
4516
5190
  }
@@ -4519,7 +5193,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
4519
5193
  if (mode === "json") {
4520
5194
  return JSON.stringify(sorted.map((r) => {
4521
5195
  const hit = resolved.get(r.issue_id);
4522
- const resolvedField = hit?.ok === true ? { title: hit.title ?? "", status: hit.status ?? "", url: hit.url ?? "" } : null;
5196
+ const resolvedField = hit?.ok === true ? {
5197
+ title: hit.title ?? "",
5198
+ status: hit.status ?? "",
5199
+ url: hit.url ?? "",
5200
+ stale: hit.stale === true
5201
+ } : null;
4523
5202
  return { ...r, resolved: resolvedField };
4524
5203
  }));
4525
5204
  }
@@ -4553,7 +5232,7 @@ var init_adapter = () => {};
4553
5232
 
4554
5233
  // ../tracker/src/cache.ts
4555
5234
  import { mkdirSync as mkdirSync6 } from "fs";
4556
- import { dirname as dirname9, join as join10 } from "path";
5235
+ import { dirname as dirname9, join as join15 } from "path";
4557
5236
  function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
4558
5237
  const t = Date.parse(entry.fetched_at);
4559
5238
  if (Number.isNaN(t))
@@ -4567,7 +5246,7 @@ function isValidEntry(e) {
4567
5246
  return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
4568
5247
  }
4569
5248
  function sidecarPath(platformDir) {
4570
- return join10(platformDir, ".spec-engine", SIDECAR_FILE);
5249
+ return join15(platformDir, ".spec-engine", SIDECAR_FILE);
4571
5250
  }
4572
5251
  async function readCache(platformDir) {
4573
5252
  try {
@@ -4735,7 +5414,13 @@ async function resolveAndCache(rows, platformDir, adapter = makeLinearAdapter(),
4735
5414
  const out = new Map;
4736
5415
  for (const id of uniqueIds) {
4737
5416
  const entry = next[id];
4738
- out.set(id, entry ? { ok: true, title: entry.title, status: entry.status, url: entry.url } : { ok: false });
5417
+ out.set(id, entry ? {
5418
+ ok: true,
5419
+ title: entry.title,
5420
+ status: entry.status,
5421
+ url: entry.url,
5422
+ stale: !isFresh(entry, now)
5423
+ } : { ok: false });
4739
5424
  }
4740
5425
  return out;
4741
5426
  }
@@ -4748,9 +5433,9 @@ var exports_provenance = {};
4748
5433
  __export(exports_provenance, {
4749
5434
  provenanceCommand: () => provenanceCommand
4750
5435
  });
4751
- import { statSync as statSync4 } from "fs";
4752
- import { resolve as resolve13 } from "path";
4753
- import { defineCommand as defineCommand9 } from "citty";
5436
+ import { statSync as statSync5 } from "fs";
5437
+ import { resolve as resolve15 } from "path";
5438
+ import { defineCommand as defineCommand10 } from "citty";
4754
5439
  function formatNoProvenance(platformDir) {
4755
5440
  return [
4756
5441
  `No provenance links indexed under ${platformDir}.`,
@@ -4767,7 +5452,7 @@ var init_provenance = __esm(() => {
4767
5452
  init_prompt();
4768
5453
  init_resolve();
4769
5454
  init__shared();
4770
- provenanceCommand = defineCommand9({
5455
+ provenanceCommand = defineCommand10({
4771
5456
  meta: {
4772
5457
  name: "provenance",
4773
5458
  description: "Render the per-requirement provenance matrix (creating/revising issues + backing tests + git pointer). --json emits the deterministically-sorted matrix rows."
@@ -4810,7 +5495,7 @@ var init_provenance = __esm(() => {
4810
5495
  if (issueId && platformArg === undefined) {
4811
5496
  let isDir = false;
4812
5497
  try {
4813
- isDir = statSync4(resolve13(issueId)).isDirectory();
5498
+ isDir = statSync5(resolve15(issueId)).isDirectory();
4814
5499
  } catch {
4815
5500
  isDir = false;
4816
5501
  }
@@ -4819,7 +5504,7 @@ var init_provenance = __esm(() => {
4819
5504
  issueId = "";
4820
5505
  }
4821
5506
  }
4822
- const platformDir = resolve13(platformArg ?? process.cwd());
5507
+ const platformDir = resolve15(platformArg ?? process.cwd());
4823
5508
  const outArg = args.out;
4824
5509
  const dbPath = resolveDbPath(platformDir, outArg);
4825
5510
  if (outArg)
@@ -4844,34 +5529,42 @@ var init_provenance = __esm(() => {
4844
5529
  });
4845
5530
  });
4846
5531
 
4847
- // src/authoring/edit.ts
4848
- function localToday() {
4849
- const d = new Date;
4850
- return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
4851
- }
4852
-
4853
- // src/authoring/filerefs.ts
4854
- import { existsSync as existsSync8 } from "fs";
4855
- import { resolve as resolve14 } from "path";
4856
- function extractRefsFromText(s) {
4857
- const out = [];
4858
- for (const m of s.matchAll(FILE_REF_RE)) {
4859
- const candidate = m[1].replace(/\.+$/, "");
4860
- if (candidate.includes("/"))
4861
- out.push(candidate);
5532
+ // src/authoring/grammar.ts
5533
+ import { join as join16 } from "path";
5534
+ async function grammarConfig(platformDir, key) {
5535
+ try {
5536
+ const doc = JSON.parse(await Bun.file(join16(platformDir, "spec-engine", key, "SPEC.json")).text());
5537
+ return {
5538
+ grammar: doc.grammar === "ears" ? "ears" : "freeform",
5539
+ severity: doc.grammarSeverity === "error" ? "error" : "warning"
5540
+ };
5541
+ } catch {
5542
+ return { grammar: "freeform", severity: "warning" };
4862
5543
  }
4863
- return out;
4864
5544
  }
4865
- function resolveFileRef(platformDir, ref) {
4866
- const root = resolve14(platformDir);
4867
- const abs = resolve14(root, ref);
4868
- if (!(abs === root || abs.startsWith(`${root}/`)))
4869
- return false;
4870
- return existsSync8(abs);
5545
+ async function enforceStatementGrammar(platformDir, key, statement, cmdName) {
5546
+ if (key === "TERM")
5547
+ return;
5548
+ const cfg = await grammarConfig(platformDir, key);
5549
+ if (cfg.grammar !== "ears")
5550
+ return;
5551
+ const result = parseEarsStatement(statement);
5552
+ if (result.ok)
5553
+ return;
5554
+ const message = `${cmdName}: the statement does not match the domain's EARS shape \u2014 ${result.problem}.
5555
+ ` + `Expected one of:
5556
+ ${result.expected}`;
5557
+ if (cfg.severity === "error") {
5558
+ console.error(`${message}
5559
+ (${key} sets grammarSeverity: "error", so the write is refused)`);
5560
+ process.exit(EXIT.USAGE);
5561
+ }
5562
+ console.error(`${message}
5563
+ (warning \u2014 written anyway; ${key} sets grammar: "ears")`);
4871
5564
  }
4872
- var FILE_REF_RE;
4873
- var init_filerefs = __esm(() => {
4874
- FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
5565
+ var init_grammar2 = __esm(() => {
5566
+ init_src();
5567
+ init_constants();
4875
5568
  });
4876
5569
 
4877
5570
  // src/commands/req.ts
@@ -4882,10 +5575,10 @@ __export(exports_req, {
4882
5575
  askLine: () => askLine,
4883
5576
  appendEntry: () => appendEntry
4884
5577
  });
4885
- import { existsSync as existsSync9 } from "fs";
4886
- import { join as join11, resolve as resolve15 } from "path";
5578
+ import { existsSync as existsSync10 } from "fs";
5579
+ import { join as join17, resolve as resolve16 } from "path";
4887
5580
  import { createInterface as createInterface2 } from "readline";
4888
- import { defineCommand as defineCommand10 } from "citty";
5581
+ import { defineCommand as defineCommand11 } from "citty";
4889
5582
  function resolveDomainKey(input, platformDir) {
4890
5583
  const prefix = normalizeDomainKey(input);
4891
5584
  const keys = listDomainKeys(platformDir);
@@ -4912,17 +5605,17 @@ async function authorFromFieldFlags(platformDir, key, nextId, textFlag, opts) {
4912
5605
  }
4913
5606
  await printResolvedCharter(platformDir, key);
4914
5607
  const why = (opts.why ?? "").trim();
4915
- const binds = (opts.binds ?? "").trim();
4916
5608
  const lives = (opts.lives ?? "").trim();
4917
- warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
5609
+ warnUnresolvableRefs(platformDir, [requirement, why, lives]);
4918
5610
  const relFile = await appendEntry(platformDir, key, nextId, {
4919
5611
  requirement,
4920
5612
  why,
4921
- binds,
4922
- lives
5613
+ lives,
5614
+ issue: (opts.issue ?? "").trim() || undefined
4923
5615
  });
4924
5616
  if (opts.json) {
4925
- console.log(JSON.stringify({ id: nextId, file: relFile }));
5617
+ const parsed = parseEarsStatement(requirement);
5618
+ console.log(JSON.stringify(parsed.ok ? { id: nextId, file: relFile, clauses: parsed.clauses } : { id: nextId, file: relFile }));
4926
5619
  } else {
4927
5620
  console.log(`appended ${nextId} to ${relFile}`);
4928
5621
  }
@@ -4946,8 +5639,9 @@ function warnUnresolvableRefs(platformDir, fieldValues) {
4946
5639
  }
4947
5640
  async function appendEntry(platformDir, key, id, fields) {
4948
5641
  const relFile = `spec-engine/${key}/SPEC.json`;
4949
- const specPath = join11(platformDir, "spec-engine", key, "SPEC.json");
4950
- const domain2 = existsSync9(specPath) ? JSON.parse(await Bun.file(specPath).text()) : null;
5642
+ const specPath = join17(platformDir, "spec-engine", key, "SPEC.json");
5643
+ await enforceStatementGrammar(platformDir, key, fields.requirement, "spec req");
5644
+ const domain2 = existsSync10(specPath) ? JSON.parse(await Bun.file(specPath).text()) : null;
4951
5645
  if (domain2 === null) {
4952
5646
  console.error(`spec req: no domain ${key} (expected ${relFile} under ${platformDir})`);
4953
5647
  process.exit(EXIT.USAGE);
@@ -4962,7 +5656,7 @@ async function appendEntry(platformDir, key, id, fields) {
4962
5656
  supersededBy: null,
4963
5657
  relates: [],
4964
5658
  livesIn: fields.lives ? [fields.lives] : [],
4965
- issues: []
5659
+ issues: fields.issue ? [{ role: "created", id: fields.issue }] : []
4966
5660
  });
4967
5661
  domain2.requirements = requirements;
4968
5662
  domain2.updated = localToday();
@@ -4988,10 +5682,11 @@ var init_req = __esm(() => {
4988
5682
  init_src();
4989
5683
  init_domains();
4990
5684
  init_filerefs();
5685
+ init_grammar2();
4991
5686
  init_constants();
4992
5687
  init_discover();
4993
5688
  init__shared();
4994
- reqCommand = defineCommand10({
5689
+ reqCommand = defineCommand11({
4995
5690
  meta: {
4996
5691
  name: "req",
4997
5692
  description: "Author a new requirement interactively (TTY) or print the next unused id (non-TTY)"
@@ -5019,18 +5714,18 @@ var init_req = __esm(() => {
5019
5714
  type: "string",
5020
5715
  description: "Why it matters field (only with --text)"
5021
5716
  },
5022
- binds: {
5023
- type: "string",
5024
- description: "Binds field (only with --text)"
5025
- },
5026
5717
  lives: {
5027
5718
  type: "string",
5028
5719
  description: "Lives in field (only with --text)"
5720
+ },
5721
+ issue: {
5722
+ type: "string",
5723
+ description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
5029
5724
  }
5030
5725
  },
5031
5726
  async run({ args }) {
5032
5727
  const input = args.domainPrefix;
5033
- const platformDir = resolve15(args.platformDir ?? process.cwd());
5728
+ const platformDir = resolve16(args.platformDir ?? process.cwd());
5034
5729
  try {
5035
5730
  assertSpecPlatform(platformDir);
5036
5731
  } catch (e) {
@@ -5039,17 +5734,17 @@ var init_req = __esm(() => {
5039
5734
  const key = resolveDomainKey(input, platformDir);
5040
5735
  const nextId = await nextRequirementId(platformDir, key);
5041
5736
  const textFlag = args.text;
5042
- const hasFieldFlags = typeof args.why === "string" || typeof args.binds === "string" || typeof args.lives === "string";
5737
+ const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.issue === "string";
5043
5738
  if (textFlag === undefined && hasFieldFlags) {
5044
- console.error("spec req: --why/--binds/--lives require --text (the Requirement field is mandatory)");
5739
+ console.error("spec req: --why/--lives/--issue require --text (the Requirement field is mandatory)");
5045
5740
  process.exit(EXIT.USAGE);
5046
5741
  return;
5047
5742
  }
5048
5743
  if (textFlag !== undefined) {
5049
5744
  await authorFromFieldFlags(platformDir, key, nextId, textFlag, {
5050
5745
  why: args.why,
5051
- binds: args.binds,
5052
5746
  lives: args.lives,
5747
+ issue: args.issue,
5053
5748
  json: Boolean(args.json)
5054
5749
  });
5055
5750
  return;
@@ -5071,13 +5766,11 @@ var init_req = __esm(() => {
5071
5766
  return;
5072
5767
  }
5073
5768
  const why = (await askLine("Why it matters: ")).trim();
5074
- const binds = (await askLine("Binds: ")).trim();
5075
5769
  const lives = (await askLine("Lives in: ")).trim();
5076
- warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
5770
+ warnUnresolvableRefs(platformDir, [requirement, why, lives]);
5077
5771
  const relFile = await appendEntry(platformDir, key, nextId, {
5078
5772
  requirement,
5079
5773
  why,
5080
- binds,
5081
5774
  lives
5082
5775
  });
5083
5776
  console.log(`appended ${nextId} to ${relFile}`);
@@ -5086,7 +5779,7 @@ var init_req = __esm(() => {
5086
5779
  });
5087
5780
 
5088
5781
  // src/parser/grammar.ts
5089
- var init_grammar = __esm(() => {
5782
+ var init_grammar3 = __esm(() => {
5090
5783
  init_src();
5091
5784
  });
5092
5785
 
@@ -5098,16 +5791,16 @@ __export(exports_term, {
5098
5791
  termConfirmCommand: () => termConfirmCommand,
5099
5792
  termCommand: () => termCommand
5100
5793
  });
5101
- import { existsSync as existsSync10 } from "fs";
5102
- import { join as join12, resolve as resolve16 } from "path";
5103
- import { defineCommand as defineCommand11 } from "citty";
5794
+ import { existsSync as existsSync11 } from "fs";
5795
+ import { join as join18, resolve as resolve17 } from "path";
5796
+ import { defineCommand as defineCommand12 } from "citty";
5104
5797
  function parseAliases(raw) {
5105
5798
  if (typeof raw !== "string" || raw.trim() === "")
5106
5799
  return [];
5107
5800
  return raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
5108
5801
  }
5109
- function resolvePlatform(platformDirRaw) {
5110
- const platformDir = resolve16(platformDirRaw ?? process.cwd());
5802
+ function resolvePlatform2(platformDirRaw) {
5803
+ const platformDir = resolve17(platformDirRaw ?? process.cwd());
5111
5804
  try {
5112
5805
  assertSpecPlatform(platformDir);
5113
5806
  } catch (e) {
@@ -5116,16 +5809,16 @@ function resolvePlatform(platformDirRaw) {
5116
5809
  return platformDir;
5117
5810
  }
5118
5811
  async function readTermEnvelope(platformDir, cmd) {
5119
- const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
5120
- if (!existsSync10(specPath)) {
5812
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5813
+ if (!existsSync11(specPath)) {
5121
5814
  console.error(`spec ${cmd}: no TERM domain (expected spec-engine/TERM/SPEC.json under ${platformDir})`);
5122
5815
  process.exit(EXIT.USAGE);
5123
5816
  }
5124
5817
  return JSON.parse(await Bun.file(specPath).text());
5125
5818
  }
5126
5819
  async function appendTermEntry(platformDir, id, fields) {
5127
- const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
5128
- const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
5820
+ const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
5821
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5129
5822
  const domain2 = await readTermEnvelope(platformDir, "term");
5130
5823
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5131
5824
  const entry = {
@@ -5158,11 +5851,11 @@ async function appendTermEntry(platformDir, id, fields) {
5158
5851
  return relFile;
5159
5852
  }
5160
5853
  async function authorOrQuery(opts) {
5161
- const platformDir = resolvePlatform(opts.platformDirRaw);
5162
- const nextId = await nextRequirementId(platformDir, TERM_KEY);
5854
+ const platformDir = resolvePlatform2(opts.platformDirRaw);
5855
+ const nextId = await nextRequirementId(platformDir, TERM_KEY2);
5163
5856
  if (opts.def === undefined) {
5164
5857
  if (opts.json) {
5165
- console.log(JSON.stringify({ domain: TERM_KEY, next_id: nextId }));
5858
+ console.log(JSON.stringify({ domain: TERM_KEY2, next_id: nextId }));
5166
5859
  } else {
5167
5860
  console.log(nextId);
5168
5861
  }
@@ -5188,9 +5881,9 @@ async function authorOrQuery(opts) {
5188
5881
  }
5189
5882
  }
5190
5883
  async function listTerms(platformDirRaw, json) {
5191
- const platformDir = resolvePlatform(platformDirRaw);
5192
- const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
5193
- const domain2 = existsSync10(specPath) ? JSON.parse(await Bun.file(specPath).text()) : { requirements: [] };
5884
+ const platformDir = resolvePlatform2(platformDirRaw);
5885
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5886
+ const domain2 = existsSync11(specPath) ? JSON.parse(await Bun.file(specPath).text()) : { requirements: [] };
5194
5887
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5195
5888
  const rows = requirements.map((r) => ({
5196
5889
  id: typeof r.id === "string" ? r.id : "",
@@ -5210,18 +5903,18 @@ async function resolveReviseTarget(id, platformDirRaw, defRaw) {
5210
5903
  console.error(`spec term revise: id must be a requirement id (TERM-NNN); got ${id}`);
5211
5904
  process.exit(EXIT.USAGE);
5212
5905
  }
5213
- if (id.slice(0, id.indexOf("-")) !== TERM_KEY) {
5906
+ if (id.slice(0, id.indexOf("-")) !== TERM_KEY2) {
5214
5907
  console.error(`spec term revise: ${id} is not a TERM id \u2014 revise operates on the TERM store`);
5215
5908
  process.exit(EXIT.USAGE);
5216
5909
  }
5217
- const platformDir = resolvePlatform(platformDirRaw);
5910
+ const platformDir = resolvePlatform2(platformDirRaw);
5218
5911
  const def = (defRaw ?? "").trim();
5219
5912
  if (def === "") {
5220
5913
  console.error("spec term revise: --def <definition> is required (a non-empty revision)");
5221
5914
  process.exit(EXIT.USAGE);
5222
5915
  }
5223
- const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
5224
- const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
5916
+ const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
5917
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5225
5918
  const domain2 = await readTermEnvelope(platformDir, "term revise");
5226
5919
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5227
5920
  const req = requirements.find((r) => r?.id === id);
@@ -5279,7 +5972,7 @@ async function resolveConfirmTarget(platformDir, termId) {
5279
5972
  const termReqs = Array.isArray(termDomain.requirements) ? termDomain.requirements : [];
5280
5973
  const term = termReqs.find((r) => r?.id === termId);
5281
5974
  if (term === undefined) {
5282
- console.error(`spec term confirm: no term ${termId} in spec-engine/${TERM_KEY}/SPEC.json`);
5975
+ console.error(`spec term confirm: no term ${termId} in spec-engine/${TERM_KEY2}/SPEC.json`);
5283
5976
  process.exit(EXIT.USAGE);
5284
5977
  }
5285
5978
  const targetPin = typeof termDomain.specVersion === "number" ? termDomain.specVersion : 1;
@@ -5297,8 +5990,8 @@ async function resolveConfirmTarget(platformDir, termId) {
5297
5990
  async function locateCitation(platformDir, reqId, termId) {
5298
5991
  const citKey = reqId.slice(0, reqId.indexOf("-"));
5299
5992
  const relFile = `spec-engine/${citKey}/SPEC.json`;
5300
- const specPath = join12(platformDir, "spec-engine", citKey, "SPEC.json");
5301
- if (!existsSync10(specPath)) {
5993
+ const specPath = join18(platformDir, "spec-engine", citKey, "SPEC.json");
5994
+ if (!existsSync11(specPath)) {
5302
5995
  console.error(`spec term confirm: no domain ${citKey} (expected ${relFile} under ${platformDir})`);
5303
5996
  process.exit(EXIT.USAGE);
5304
5997
  }
@@ -5309,6 +6002,12 @@ async function locateCitation(platformDir, reqId, termId) {
5309
6002
  console.error(`spec term confirm: no entry ${reqId} in ${relFile}`);
5310
6003
  process.exit(EXIT.USAGE);
5311
6004
  }
6005
+ const rawStatus = typeof citReq.status === "string" ? citReq.status : "";
6006
+ const statusLc = rawStatus.toLowerCase();
6007
+ if (statusLc !== "active" && statusLc !== "draft") {
6008
+ console.error(`spec term confirm: ${reqId} is ${rawStatus} \u2014 only Active/Draft entries re-pin a citation (a superseded entry is history)`);
6009
+ process.exit(EXIT.USAGE);
6010
+ }
5312
6011
  const cites = Array.isArray(citReq.cites) ? citReq.cites : [];
5313
6012
  const cite = cites.find((c) => c?.term === termId);
5314
6013
  if (cite === undefined) {
@@ -5323,11 +6022,11 @@ async function confirmCitation(opts) {
5323
6022
  console.error(`spec term confirm: req id must be a requirement id (KEY-NNN); got ${reqId}`);
5324
6023
  process.exit(EXIT.USAGE);
5325
6024
  }
5326
- if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !== TERM_KEY) {
6025
+ if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !== TERM_KEY2) {
5327
6026
  console.error(`spec term confirm: ${termId} is not a TERM id \u2014 confirm re-pins a TERM citation`);
5328
6027
  process.exit(EXIT.USAGE);
5329
6028
  }
5330
- const platformDir = resolvePlatform(opts.platformDirRaw);
6029
+ const platformDir = resolvePlatform2(opts.platformDirRaw);
5331
6030
  const { targetId, targetPin } = await resolveConfirmTarget(platformDir, termId);
5332
6031
  const { citDomain, specPath, relFile, cite } = await locateCitation(platformDir, reqId, termId);
5333
6032
  cite.term = targetId;
@@ -5347,18 +6046,18 @@ async function confirmCitation(opts) {
5347
6046
  console.log(`confirmed ${reqId} cites ${targetId} @${targetPin} in ${relFile}`);
5348
6047
  }
5349
6048
  }
5350
- var TERM_KEY = "TERM", termListCommand, termReviseCommand, termConfirmCommand, termCommand;
6049
+ var TERM_KEY2 = "TERM", termListCommand, termReviseCommand, termConfirmCommand, termCommand;
5351
6050
  var init_term = __esm(() => {
5352
6051
  init_src();
5353
6052
  init_domains();
5354
6053
  init_constants();
5355
6054
  init_discover();
5356
6055
  init_pipeline();
5357
- init_grammar();
6056
+ init_grammar3();
5358
6057
  init_sqlite();
5359
6058
  init__shared();
5360
6059
  init_req();
5361
- termListCommand = defineCommand11({
6060
+ termListCommand = defineCommand12({
5362
6061
  meta: {
5363
6062
  name: "list",
5364
6063
  description: "List the glossary TERM entries (id, name, status), sorted by id"
@@ -5378,7 +6077,7 @@ var init_term = __esm(() => {
5378
6077
  await listTerms(args.platformDir, Boolean(args.json));
5379
6078
  }
5380
6079
  });
5381
- termReviseCommand = defineCommand11({
6080
+ termReviseCommand = defineCommand12({
5382
6081
  meta: {
5383
6082
  name: "revise",
5384
6083
  description: "Revise a TERM's definition IN PLACE (same id) and BUMP the envelope specVersion \u2014 the drift signal (A2). Requirements have no such op."
@@ -5412,7 +6111,7 @@ var init_term = __esm(() => {
5412
6111
  });
5413
6112
  }
5414
6113
  });
5415
- termConfirmCommand = defineCommand11({
6114
+ termConfirmCommand = defineCommand12({
5416
6115
  meta: {
5417
6116
  name: "confirm",
5418
6117
  description: "Re-pin a requirement's TERM citation to the term's current version (clears TERM_DRIFT); re-points to the successor when the term is superseded (clears SUPERSEDED_TERM_REFERENCED)."
@@ -5447,7 +6146,7 @@ var init_term = __esm(() => {
5447
6146
  });
5448
6147
  }
5449
6148
  });
5450
- termCommand = defineCommand11({
6149
+ termCommand = defineCommand12({
5451
6150
  meta: {
5452
6151
  name: "term",
5453
6152
  description: "Author a glossary TERM (spec term <name> --def <definition>), list terms (spec term list), or revise a definition in place (spec term revise TERM-NNN)"
@@ -5466,261 +6165,58 @@ var init_term = __esm(() => {
5466
6165
  extra: {
5467
6166
  type: "positional",
5468
6167
  required: false,
5469
- description: "After `revise <TERM-NNN>`: the platform directory (default: cwd). After `confirm <KEY-NNN>`: the TERM-NNN id."
5470
- },
5471
- extra2: {
5472
- type: "positional",
5473
- required: false,
5474
- description: "After `confirm <KEY-NNN> <TERM-NNN>`: the platform directory (default: cwd)"
5475
- },
5476
- def: { type: "string", description: "The term's definition (stored in the statement field)" },
5477
- text: { type: "string", description: "Alias for --def (the definition)" },
5478
- aliases: { type: "string", description: "Comma-separated synonyms \u2192 aliases[]" },
5479
- section: { type: "string", description: "GLOSSARY.md layout bucket (Wave F)" },
5480
- noBump: { type: "boolean", description: "revise: do not bump the envelope specVersion" },
5481
- json: {
5482
- type: "boolean",
5483
- description: "Author: { id, file } (or { domain, next_id } as an id query). list: the entries array. revise: { id, file, spec_version }."
5484
- }
5485
- },
5486
- async run({ args }) {
5487
- const verb = args.name;
5488
- const def = args.def ?? args.text;
5489
- const json = Boolean(args.json);
5490
- if (verb === "list") {
5491
- await listTerms(args.platformDir, json);
5492
- return;
5493
- }
5494
- if (verb === "revise") {
5495
- await reviseTerm({
5496
- id: args.platformDir ?? "",
5497
- platformDirRaw: args.extra,
5498
- def,
5499
- noBump: Boolean(args.noBump),
5500
- json
5501
- });
5502
- return;
5503
- }
5504
- if (verb === "confirm") {
5505
- await confirmCitation({
5506
- reqId: args.platformDir ?? "",
5507
- termId: args.extra ?? "",
5508
- platformDirRaw: args.extra2,
5509
- json
5510
- });
5511
- return;
5512
- }
5513
- await authorOrQuery({
5514
- name: verb,
5515
- platformDirRaw: args.platformDir,
5516
- def,
5517
- aliasesRaw: args.aliases,
5518
- section: args.section,
5519
- json
5520
- });
5521
- }
5522
- });
5523
- });
5524
-
5525
- // src/commands/glossary.ts
5526
- var exports_glossary = {};
5527
- __export(exports_glossary, {
5528
- parseGlossary: () => parseGlossary,
5529
- glossaryCommand: () => glossaryCommand,
5530
- generateGlossary: () => generateGlossary
5531
- });
5532
- import { existsSync as existsSync11, readFileSync as readFileSync2 } from "fs";
5533
- import { join as join13, resolve as resolve17 } from "path";
5534
- import { defineCommand as defineCommand12 } from "citty";
5535
- function parseGlossary(md) {
5536
- const terms = [];
5537
- let section = null;
5538
- let buf = null;
5539
- const flush = () => {
5540
- if (buf === null)
5541
- return;
5542
- const m = buf.match(/^\*\*(.+?)\*\*/);
5543
- if (m) {
5544
- const remainder = buf.slice(m[0].length);
5545
- const statement = remainder.replace(/^ \u2014 /, "").trim();
5546
- terms.push({ term: m[1], statement, section });
5547
- }
5548
- buf = null;
5549
- };
5550
- for (const line of md.split(`
5551
- `)) {
5552
- if (line.startsWith("## ")) {
5553
- flush();
5554
- section = line.slice(3).trim();
5555
- } else if (line.startsWith("- ")) {
5556
- flush();
5557
- buf = line.slice(2);
5558
- } else if (buf !== null) {
5559
- if (line.trim() === "")
5560
- flush();
5561
- else
5562
- buf += ` ${line.trim()}`;
5563
- }
5564
- }
5565
- flush();
5566
- return terms;
5567
- }
5568
- function generateGlossary(terms) {
5569
- const parts = ["# Glossary", "", GLOSSARY_INTRO];
5570
- let section;
5571
- for (const t of terms) {
5572
- if (t.section !== section) {
5573
- section = t.section;
5574
- if (section !== null && section !== undefined) {
5575
- parts.push("", `## ${section}`, "");
5576
- }
5577
- }
5578
- parts.push(`- **${t.term}** \u2014 ${t.statement}`);
5579
- }
5580
- return `${parts.join(`
5581
- `)}
5582
- `;
5583
- }
5584
- function resolvePlatform2(platformDirRaw) {
5585
- const platformDir = resolve17(platformDirRaw ?? process.cwd());
5586
- try {
5587
- assertSpecPlatform(platformDir);
5588
- } catch (e) {
5589
- handleNotAPlatform(e);
5590
- }
5591
- return platformDir;
5592
- }
5593
- function readStoreTerms(platformDir) {
5594
- const specPath = join13(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5595
- if (!existsSync11(specPath))
5596
- return [];
5597
- const domain2 = JSON.parse(readFileSync2(specPath, "utf8"));
5598
- const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5599
- return reqs.filter((r) => String(r.status).toLowerCase() === "active").sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0).map((r) => ({
5600
- term: String(r.term ?? ""),
5601
- statement: String(r.statement ?? ""),
5602
- section: r.section ?? null
5603
- }));
5604
- }
5605
- function glossaryPath(platformDir) {
5606
- return join13(platformDir, "GLOSSARY.md");
5607
- }
5608
- async function migrateGlossary(platformDir, json) {
5609
- const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
5610
- const specPath = join13(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5611
- if (!existsSync11(specPath)) {
5612
- console.error(`spec glossary: no TERM domain (expected ${relFile} under ${platformDir})`);
5613
- process.exit(EXIT.USAGE);
5614
- }
5615
- const domain2 = JSON.parse(readFileSync2(specPath, "utf8"));
5616
- const existing = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5617
- if (existing.length > 0) {
5618
- console.error(`spec glossary: TERM domain already holds ${existing.length} entries \u2014 migration skipped`);
5619
- if (json)
5620
- console.log(JSON.stringify({ migrated: 0, skipped: true }));
5621
- else
5622
- console.log("migration skipped (TERM domain not empty)");
5623
- return;
5624
- }
5625
- const gPath = glossaryPath(platformDir);
5626
- if (!existsSync11(gPath)) {
5627
- console.error(`spec glossary: no GLOSSARY.md to migrate (expected ${gPath})`);
5628
- process.exit(EXIT.USAGE);
5629
- }
5630
- const parsed = parseGlossary(readFileSync2(gPath, "utf8"));
5631
- domain2.requirements = parsed.map((t, i) => ({
5632
- id: `${TERM_KEY2}-${String(i + 1).padStart(3, "0")}`,
5633
- status: "active",
5634
- statement: t.statement,
5635
- term: t.term,
5636
- why: null,
5637
- supersedes: null,
5638
- supersededBy: null,
5639
- relates: [],
5640
- livesIn: [],
5641
- issues: [],
5642
- aliases: [],
5643
- cites: [],
5644
- changedAtVersion: 1,
5645
- section: t.section
5646
- }));
5647
- domain2.updated = localToday();
5648
- const res = await validateAndWrite(specPath, domain2, relFile);
5649
- if (!res.ok) {
5650
- for (const diag of res.diagnostics)
5651
- console.error(`spec glossary: ${diag.detail}`);
5652
- process.exit(EXIT.USAGE);
5653
- }
5654
- const count = parsed.length;
5655
- if (json)
5656
- console.log(JSON.stringify({ migrated: count, file: relFile }));
5657
- else
5658
- console.log(`migrated ${count} terms into ${relFile}`);
5659
- }
5660
- async function writeGlossary(platformDir, json) {
5661
- const terms = readStoreTerms(platformDir);
5662
- await Bun.write(glossaryPath(platformDir), generateGlossary(terms));
5663
- if (json)
5664
- console.log(JSON.stringify({ generated: terms.length, file: "GLOSSARY.md" }));
5665
- else
5666
- console.log(`generated GLOSSARY.md from ${terms.length} terms`);
5667
- }
5668
- function checkGlossary(platformDir, json) {
5669
- const generated = generateGlossary(readStoreTerms(platformDir));
5670
- const gPath = glossaryPath(platformDir);
5671
- const committed = existsSync11(gPath) ? readFileSync2(gPath, "utf8") : "";
5672
- if (generated === committed) {
5673
- if (json)
5674
- console.log(JSON.stringify({ ok: true }));
5675
- else
5676
- console.error("glossary round-trip: OK (committed GLOSSARY.md == generated)");
5677
- return;
5678
- }
5679
- console.error("glossary drift: committed GLOSSARY.md != generated from the TERM store");
5680
- console.error("run `spec glossary .` to regenerate the human view from the store");
5681
- if (json)
5682
- console.log(JSON.stringify({ ok: false }));
5683
- process.exit(EXIT.FAILURE);
5684
- }
5685
- var TERM_KEY2 = "TERM", GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
5686
- docs need one of these ideas, use the term below \u2014 not a synonym. Terms are ordered
5687
- by the data model, outside-in.`, glossaryCommand;
5688
- var init_glossary = __esm(() => {
5689
- init_src();
5690
- init_constants();
5691
- init_discover();
5692
- init__shared();
5693
- glossaryCommand = defineCommand12({
5694
- meta: {
5695
- name: "glossary",
5696
- description: "Generate GLOSSARY.md from the TERM store (byte-stable); --migrate parses GLOSSARY.md into TERM-001..N; --check fails on drift (committed != generated)"
5697
- },
5698
- args: {
5699
- platformDir: {
5700
- type: "positional",
5701
- required: false,
5702
- description: "Platform directory containing spec-engine/ (default: cwd)"
6168
+ description: "After `revise <TERM-NNN>`: the platform directory (default: cwd). After `confirm <KEY-NNN>`: the TERM-NNN id."
5703
6169
  },
5704
- migrate: {
5705
- type: "boolean",
5706
- description: "One-time: parse the committed GLOSSARY.md into TERM-001..N (idempotent-skip)"
6170
+ extra2: {
6171
+ type: "positional",
6172
+ required: false,
6173
+ description: "After `confirm <KEY-NNN> <TERM-NNN>`: the platform directory (default: cwd)"
5707
6174
  },
5708
- check: {
6175
+ def: { type: "string", description: "The term's definition (stored in the statement field)" },
6176
+ text: { type: "string", description: "Alias for --def (the definition)" },
6177
+ aliases: { type: "string", description: "Comma-separated synonyms \u2192 aliases[]" },
6178
+ section: { type: "string", description: "GLOSSARY.md layout bucket (Wave F)" },
6179
+ noBump: { type: "boolean", description: "revise: do not bump the envelope specVersion" },
6180
+ json: {
5709
6181
  type: "boolean",
5710
- description: "Fail (exit 1) if the committed GLOSSARY.md differs from the generated output"
5711
- },
5712
- json: { type: "boolean", description: "Emit a JSON result instead of the text summary" }
6182
+ description: "Author: { id, file } (or { domain, next_id } as an id query). list: the entries array. revise: { id, file, spec_version }."
6183
+ }
5713
6184
  },
5714
6185
  async run({ args }) {
5715
- const platformDir = resolvePlatform2(args.platformDir);
6186
+ const verb = args.name;
6187
+ const def = args.def ?? args.text;
5716
6188
  const json = Boolean(args.json);
5717
- if (args.migrate) {
5718
- await migrateGlossary(platformDir, json);
5719
- } else if (args.check) {
5720
- checkGlossary(platformDir, json);
5721
- } else {
5722
- await writeGlossary(platformDir, json);
6189
+ if (verb === "list") {
6190
+ await listTerms(args.platformDir, json);
6191
+ return;
5723
6192
  }
6193
+ if (verb === "revise") {
6194
+ await reviseTerm({
6195
+ id: args.platformDir ?? "",
6196
+ platformDirRaw: args.extra,
6197
+ def,
6198
+ noBump: Boolean(args.noBump),
6199
+ json
6200
+ });
6201
+ return;
6202
+ }
6203
+ if (verb === "confirm") {
6204
+ await confirmCitation({
6205
+ reqId: args.platformDir ?? "",
6206
+ termId: args.extra ?? "",
6207
+ platformDirRaw: args.extra2,
6208
+ json
6209
+ });
6210
+ return;
6211
+ }
6212
+ await authorOrQuery({
6213
+ name: verb,
6214
+ platformDirRaw: args.platformDir,
6215
+ def,
6216
+ aliasesRaw: args.aliases,
6217
+ section: args.section,
6218
+ json
6219
+ });
5724
6220
  }
5725
6221
  });
5726
6222
  });
@@ -5775,7 +6271,7 @@ __export(exports_supersede, {
5775
6271
  supersedeCommand: () => supersedeCommand
5776
6272
  });
5777
6273
  import { existsSync as existsSync12 } from "fs";
5778
- import { join as join14, resolve as resolve18 } from "path";
6274
+ import { join as join19, resolve as resolve18 } from "path";
5779
6275
  import { defineCommand as defineCommand13 } from "citty";
5780
6276
  async function resolveSupersedeTarget(id, platformDir) {
5781
6277
  if (!ID_RE.test(id)) {
@@ -5789,7 +6285,7 @@ async function resolveSupersedeTarget(id, platformDir) {
5789
6285
  }
5790
6286
  const key = id.slice(0, id.indexOf("-"));
5791
6287
  const relFile = `spec-engine/${key}/SPEC.json`;
5792
- const specPath = join14(platformDir, "spec-engine", key, "SPEC.json");
6288
+ const specPath = join19(platformDir, "spec-engine", key, "SPEC.json");
5793
6289
  if (!existsSync12(specPath)) {
5794
6290
  console.error(`spec supersede: no domain ${key} (expected ${relFile} under ${platformDir})`);
5795
6291
  process.exit(EXIT.USAGE);
@@ -5838,8 +6334,7 @@ function resolveSuccessorFields(args, req) {
5838
6334
  const predLives = Array.isArray(req.livesIn) && req.livesIn.length > 0 ? String(req.livesIn[0]) : "";
5839
6335
  const why = (args.why ?? predWhy).trim();
5840
6336
  const lives = (args.lives ?? predLives).trim();
5841
- const binds = (args.binds ?? "").trim();
5842
- return { why, lives, binds };
6337
+ return { why, lives };
5843
6338
  }
5844
6339
  function resolveSuccessorTermFields(args, req) {
5845
6340
  const predTerm = typeof req.term === "string" ? req.term : undefined;
@@ -5848,10 +6343,18 @@ function resolveSuccessorTermFields(args, req) {
5848
6343
  const aliases = typeof rawAliases === "string" ? rawAliases.split(",").map((s) => s.trim()).filter((s) => s !== "") : Array.isArray(req.aliases) ? req.aliases.map((a) => String(a)) : [];
5849
6344
  return { term, aliases };
5850
6345
  }
5851
- function applySupersedeEdit(target, newId, requirement, why, lives, noBump, termFields) {
5852
- const { domain: domain2, requirements, req } = target;
6346
+ function flipPredecessor(req, newId, issue) {
5853
6347
  req.status = "superseded";
5854
6348
  req.supersededBy = newId;
6349
+ if (issue) {
6350
+ const issues = Array.isArray(req.issues) ? req.issues : [];
6351
+ issues.push({ role: "supersedes-via", id: issue });
6352
+ req.issues = issues;
6353
+ }
6354
+ }
6355
+ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, termFields, issue) {
6356
+ const { domain: domain2, requirements, req } = target;
6357
+ flipPredecessor(req, newId, issue);
5855
6358
  const successor = {
5856
6359
  id: newId,
5857
6360
  status: "active",
@@ -5861,8 +6364,7 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
5861
6364
  supersededBy: null,
5862
6365
  relates: [],
5863
6366
  livesIn: lives === "" ? [] : [lives],
5864
- issues: [],
5865
- changedAtVersion: 1
6367
+ issues: issue ? [{ role: "created", id: issue }] : []
5866
6368
  };
5867
6369
  if (termFields !== null) {
5868
6370
  if (termFields.term !== undefined)
@@ -5871,20 +6373,25 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
5871
6373
  }
5872
6374
  requirements.push(successor);
5873
6375
  domain2.requirements = requirements;
5874
- let reportedVersion;
5875
- if (domain2.key === "TERM") {
5876
- const currentVersion = typeof domain2.specVersion === "number" ? domain2.specVersion : 1;
5877
- reportedVersion = noBump ? null : currentVersion + 1;
5878
- if (reportedVersion !== null)
5879
- domain2.specVersion = reportedVersion;
5880
- req.supersededAtVersion = reportedVersion ?? currentVersion;
5881
- } else {
5882
- reportedVersion = deriveDomainVersion(requirements);
5883
- req.supersededAtVersion = reportedVersion;
5884
- }
6376
+ const reportedVersion = applyVersionStage(target, successor, noBump);
5885
6377
  domain2.updated = localToday();
5886
6378
  return reportedVersion;
5887
6379
  }
6380
+ function applyVersionStage(target, successor, noBump) {
6381
+ const { domain: domain2, requirements, req } = target;
6382
+ if (domain2.key !== "TERM") {
6383
+ const derived = deriveDomainVersion(requirements);
6384
+ req.supersededAtVersion = derived;
6385
+ return derived;
6386
+ }
6387
+ const currentVersion = typeof domain2.specVersion === "number" ? domain2.specVersion : 1;
6388
+ const reported = noBump ? null : currentVersion + 1;
6389
+ if (reported !== null)
6390
+ domain2.specVersion = reported;
6391
+ req.supersededAtVersion = reported ?? currentVersion;
6392
+ successor.changedAtVersion = reported ?? currentVersion;
6393
+ return reported;
6394
+ }
5888
6395
  async function reindexAndCollectRetag(platformDir, id) {
5889
6396
  const tags = await reindexAndListTags(platformDir, id);
5890
6397
  return tags.map(({ req_id, repo, file, line, kind, level }) => ({
@@ -5900,10 +6407,11 @@ var supersedeCommand;
5900
6407
  var init_supersede = __esm(() => {
5901
6408
  init_src();
5902
6409
  init_domains();
6410
+ init_grammar2();
5903
6411
  init_constants();
5904
6412
  init_discover();
5905
6413
  init_domainJson();
5906
- init_grammar();
6414
+ init_grammar3();
5907
6415
  init__shared();
5908
6416
  init_req();
5909
6417
  supersedeCommand = defineCommand13({
@@ -5930,10 +6438,6 @@ var init_supersede = __esm(() => {
5930
6438
  type: "string",
5931
6439
  description: "Successor's Why it matters (default: copied from the old entry)"
5932
6440
  },
5933
- binds: {
5934
- type: "string",
5935
- description: "Binds value (validated for @-refs; not persisted in JSON \u2014 STOR-01)"
5936
- },
5937
6441
  lives: {
5938
6442
  type: "string",
5939
6443
  description: "Successor's Lives in (default: copied from the old entry)"
@@ -5950,6 +6454,10 @@ var init_supersede = __esm(() => {
5950
6454
  type: "boolean",
5951
6455
  description: "Do not bump the envelope specVersion"
5952
6456
  },
6457
+ issue: {
6458
+ type: "string",
6459
+ description: "Ticket that caused this supersession \u2014 recorded as supersedes-via on the predecessor and created on the successor (opaque provenance)"
6460
+ },
5953
6461
  json: {
5954
6462
  type: "boolean",
5955
6463
  description: "Emit { old_id, new_id, file, spec_version, retag } as JSON instead of the text summary"
@@ -5960,11 +6468,12 @@ var init_supersede = __esm(() => {
5960
6468
  const platformDir = resolve18(args.platformDir ?? process.cwd());
5961
6469
  const target = await resolveSupersedeTarget(id, platformDir);
5962
6470
  const requirement = await resolveSuccessorText(args, id);
5963
- const { why, lives, binds } = resolveSuccessorFields(args, target.req);
5964
- warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
6471
+ await enforceStatementGrammar(platformDir, target.key, requirement, "spec supersede");
6472
+ const { why, lives } = resolveSuccessorFields(args, target.req);
6473
+ warnUnresolvableRefs(platformDir, [requirement, why, lives]);
5965
6474
  const newId = await nextRequirementId(platformDir, target.key);
5966
6475
  const termFields = target.key === "TERM" ? resolveSuccessorTermFields(args, target.req) : null;
5967
- const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields);
6476
+ const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields, (args.issue ?? "").trim() || undefined);
5968
6477
  const res = await validateAndWrite(target.specPath, target.domain, target.relFile);
5969
6478
  if (!res.ok) {
5970
6479
  for (const diag of res.diagnostics) {
@@ -5998,18 +6507,133 @@ var init_supersede = __esm(() => {
5998
6507
  });
5999
6508
  });
6000
6509
 
6510
+ // src/commands/deprecate.ts
6511
+ var exports_deprecate = {};
6512
+ __export(exports_deprecate, {
6513
+ deprecateCommand: () => deprecateCommand
6514
+ });
6515
+ import { existsSync as existsSync13 } from "fs";
6516
+ import { join as join20, resolve as resolve19 } from "path";
6517
+ import { defineCommand as defineCommand14 } from "citty";
6518
+ async function resolveDeprecateTarget(platformDir, id) {
6519
+ const key = id.slice(0, id.indexOf("-"));
6520
+ const relFile = `spec-engine/${key}/SPEC.json`;
6521
+ const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
6522
+ if (!existsSync13(specPath)) {
6523
+ console.error(`spec deprecate: no domain ${key} (expected ${relFile} under ${platformDir})`);
6524
+ process.exit(EXIT.USAGE);
6525
+ }
6526
+ const domain2 = JSON.parse(await Bun.file(specPath).text());
6527
+ const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
6528
+ const req = requirements.find((r) => r?.id === id);
6529
+ if (req === undefined) {
6530
+ console.error(`spec deprecate: no entry ${id} in ${relFile}`);
6531
+ process.exit(EXIT.USAGE);
6532
+ }
6533
+ const statusLc = (typeof req.status === "string" ? req.status : "").toLowerCase();
6534
+ if (statusLc === "deprecated" || statusLc === "retired") {
6535
+ console.error(`spec deprecate: ${id} is already deprecated`);
6536
+ process.exit(EXIT.USAGE);
6537
+ }
6538
+ if (statusLc !== "active" && statusLc !== "draft") {
6539
+ console.error(`spec deprecate: ${id} is ${req.status} \u2014 only Active/Draft entries deprecate (a superseded entry is already history)`);
6540
+ process.exit(EXIT.USAGE);
6541
+ }
6542
+ return { domain: domain2, req, specPath, relFile };
6543
+ }
6544
+ var deprecateCommand;
6545
+ var init_deprecate = __esm(() => {
6546
+ init_src();
6547
+ init_constants();
6548
+ init_discover();
6549
+ init_grammar3();
6550
+ init__shared();
6551
+ deprecateCommand = defineCommand14({
6552
+ meta: {
6553
+ name: "deprecate",
6554
+ description: "Mark a requirement end-of-life with a recorded reason (a requirement is never deleted)"
6555
+ },
6556
+ args: {
6557
+ id: {
6558
+ type: "positional",
6559
+ required: true,
6560
+ description: "Requirement id to deprecate (KEY-NNN)"
6561
+ },
6562
+ platformDir: {
6563
+ type: "positional",
6564
+ required: false,
6565
+ description: "Platform directory (default: cwd)"
6566
+ },
6567
+ reason: {
6568
+ type: "string",
6569
+ description: "Why this requirement is end-of-life (required \u2014 the durable record)"
6570
+ },
6571
+ json: {
6572
+ type: "boolean",
6573
+ description: "Print { id, file, reason, sites } instead of text"
6574
+ }
6575
+ },
6576
+ async run({ args }) {
6577
+ const id = args.id;
6578
+ const platformDir = resolve19(args.platformDir ?? process.cwd());
6579
+ const reason = (args.reason ?? "").trim();
6580
+ if (!ID_RE.test(id)) {
6581
+ console.error(`spec deprecate: id must be a requirement id (KEY-NNN); got ${id}`);
6582
+ process.exit(EXIT.USAGE);
6583
+ }
6584
+ if (reason === "") {
6585
+ console.error("spec deprecate: --reason is required \u2014 the reason is the durable record of why this requirement ended");
6586
+ process.exit(EXIT.USAGE);
6587
+ }
6588
+ try {
6589
+ assertSpecPlatform(platformDir);
6590
+ } catch (e) {
6591
+ handleNotAPlatform(e);
6592
+ }
6593
+ const { domain: domain2, req, specPath, relFile } = await resolveDeprecateTarget(platformDir, id);
6594
+ req.status = "deprecated";
6595
+ req.deprecatedReason = reason;
6596
+ domain2.updated = localToday();
6597
+ const res = await validateAndWrite(specPath, domain2, relFile);
6598
+ if (!res.ok) {
6599
+ for (const diag of res.diagnostics)
6600
+ console.error(`spec deprecate: ${diag.detail}`);
6601
+ process.exit(EXIT.USAGE);
6602
+ }
6603
+ const tags = await reindexAndListTags(platformDir, id);
6604
+ const sites = tags.map(({ req_id, repo, file, line, kind, level }) => ({
6605
+ req_id,
6606
+ repo,
6607
+ file,
6608
+ line,
6609
+ kind,
6610
+ level: level ?? null
6611
+ }));
6612
+ if (args.json) {
6613
+ console.log(JSON.stringify({ id, file: relFile, reason, sites }));
6614
+ return;
6615
+ }
6616
+ console.log(`deprecated ${id} in ${relFile} \u2014 reason recorded`);
6617
+ if (sites.length > 0) {
6618
+ console.error(`${sites.length} code tag(s) still bind ${id} \u2014 remove or retag them (spec check reports each as DEPRECATED_REFERENCED):`);
6619
+ console.error(renderReqTags(sites, "text"));
6620
+ }
6621
+ }
6622
+ });
6623
+ });
6624
+
6001
6625
  // src/commands/move.ts
6002
6626
  var exports_move = {};
6003
6627
  __export(exports_move, {
6004
6628
  moveCommand: () => moveCommand
6005
6629
  });
6006
- import { existsSync as existsSync13 } from "fs";
6007
- import { join as join15, resolve as resolve19 } from "path";
6008
- import { defineCommand as defineCommand14 } from "citty";
6630
+ import { existsSync as existsSync14 } from "fs";
6631
+ import { join as join21, resolve as resolve20 } from "path";
6632
+ import { defineCommand as defineCommand15 } from "citty";
6009
6633
  async function loadDomainSide(platformDir, key) {
6010
6634
  const relFile = `spec-engine/${key}/SPEC.json`;
6011
- const specPath = join15(platformDir, "spec-engine", key, "SPEC.json");
6012
- if (!existsSync13(specPath))
6635
+ const specPath = join21(platformDir, "spec-engine", key, "SPEC.json");
6636
+ if (!existsSync14(specPath))
6013
6637
  return null;
6014
6638
  const domain2 = JSON.parse(await Bun.file(specPath).text());
6015
6639
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
@@ -6068,6 +6692,15 @@ function resolveSuccessorFields2(args, req) {
6068
6692
  lives: (args.lives ?? srcLives).trim()
6069
6693
  };
6070
6694
  }
6695
+ async function assertMovableStatement(platformDir, id, targetKey, statement, args) {
6696
+ if (statement === "") {
6697
+ console.error(`spec move: ${id} has an empty Requirement \u2014 cannot move a blank statement`);
6698
+ process.exit(EXIT.USAGE);
6699
+ }
6700
+ if (typeof args.text === "string") {
6701
+ await enforceStatementGrammar(platformDir, targetKey, statement, "spec move");
6702
+ }
6703
+ }
6071
6704
  function resolveMoveVersion(domain2, key, noBump) {
6072
6705
  domain2.updated = localToday();
6073
6706
  if (key !== "TERM") {
@@ -6123,15 +6756,16 @@ var moveCommand;
6123
6756
  var init_move = __esm(() => {
6124
6757
  init_src();
6125
6758
  init_domains();
6759
+ init_grammar2();
6126
6760
  init_constants();
6127
6761
  init_discover();
6128
6762
  init_pipeline();
6129
6763
  init_domainJson();
6130
- init_grammar();
6764
+ init_grammar3();
6131
6765
  init_sqlite();
6132
6766
  init__shared();
6133
6767
  init_req();
6134
- moveCommand = defineCommand14({
6768
+ moveCommand = defineCommand15({
6135
6769
  meta: {
6136
6770
  name: "move",
6137
6771
  description: "Move a requirement to another domain: mint the successor in <NEW-DOMAIN> carrying the source's fields, mark the source superseded, bump both specVersions, and emit the retag worklist."
@@ -6176,13 +6810,10 @@ var init_move = __esm(() => {
6176
6810
  async run({ args }) {
6177
6811
  const id = args.id;
6178
6812
  const rawTargetKey = args.newDomain;
6179
- const platformDir = resolve19(args.platformDir ?? process.cwd());
6813
+ const platformDir = resolve20(args.platformDir ?? process.cwd());
6180
6814
  const target = await resolveMoveTarget(id, rawTargetKey, platformDir);
6181
6815
  const fields = resolveSuccessorFields2(args, target.req);
6182
- if (fields.statement === "") {
6183
- console.error(`spec move: ${id} has an empty Requirement \u2014 cannot move a blank statement`);
6184
- process.exit(EXIT.USAGE);
6185
- }
6816
+ await assertMovableStatement(platformDir, id, target.target.key, fields.statement, args);
6186
6817
  warnUnresolvableRefs(platformDir, [fields.statement, fields.why, fields.lives]);
6187
6818
  const newId = await nextRequirementId(platformDir, target.target.key);
6188
6819
  const { sourceVersion, targetVersion } = applyMoveEdit(target, newId, fields, Boolean(args.noBump));
@@ -6234,17 +6865,17 @@ var exports_amend = {};
6234
6865
  __export(exports_amend, {
6235
6866
  amendCommand: () => amendCommand
6236
6867
  });
6237
- import { existsSync as existsSync14 } from "fs";
6238
- import { join as join16, resolve as resolve20 } from "path";
6239
- import { defineCommand as defineCommand15 } from "citty";
6868
+ import { existsSync as existsSync15 } from "fs";
6869
+ import { join as join22, resolve as resolve21 } from "path";
6870
+ import { defineCommand as defineCommand16 } from "citty";
6240
6871
  function validateAmendFields(args) {
6241
6872
  const hasText = typeof args.text === "string";
6873
+ const hasIssue = typeof args.issue === "string" && args.issue.trim() !== "";
6242
6874
  const hasWhy = typeof args.why === "string";
6243
6875
  const hasLives = typeof args.lives === "string";
6244
- const hasBinds = typeof args.binds === "string";
6245
6876
  const hasTerm = typeof args.term === "string";
6246
6877
  const hasAliases = typeof args.aliases === "string";
6247
- if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases) {
6878
+ if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases && !hasIssue) {
6248
6879
  console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --term / --aliases");
6249
6880
  process.exit(EXIT.USAGE);
6250
6881
  }
@@ -6256,13 +6887,13 @@ function validateAmendFields(args) {
6256
6887
  console.error("spec amend: --term must be a non-empty headword");
6257
6888
  process.exit(EXIT.USAGE);
6258
6889
  }
6259
- return { hasText, hasWhy, hasLives, hasBinds, hasTerm, hasAliases };
6890
+ return { hasText, hasWhy, hasLives, hasTerm, hasAliases, hasIssue };
6260
6891
  }
6261
6892
  async function locateAmendEntry(platformDir, id) {
6262
6893
  const key = id.slice(0, id.indexOf("-"));
6263
6894
  const relFile = `spec-engine/${key}/SPEC.json`;
6264
- const specPath = join16(platformDir, "spec-engine", key, "SPEC.json");
6265
- if (!existsSync14(specPath)) {
6895
+ const specPath = join22(platformDir, "spec-engine", key, "SPEC.json");
6896
+ if (!existsSync15(specPath)) {
6266
6897
  console.error(`spec amend: no domain ${key} (expected ${relFile} under ${platformDir})`);
6267
6898
  process.exit(EXIT.USAGE);
6268
6899
  }
@@ -6315,15 +6946,18 @@ function applyAmendMutations(req, args, fields) {
6315
6946
  fieldsChanged.push("lives");
6316
6947
  refValues.push(v);
6317
6948
  }
6318
- if (fields.hasBinds) {
6319
- refValues.push(args.binds.trim());
6320
- }
6321
6949
  if (fields.hasTerm) {
6322
6950
  const v = args.term.trim();
6323
6951
  req.term = v;
6324
6952
  fieldsChanged.push("term");
6325
6953
  refValues.push(v);
6326
6954
  }
6955
+ if (fields.hasIssue) {
6956
+ const issues = Array.isArray(req.issues) ? req.issues : [];
6957
+ issues.push({ role: "amends-via", id: args.issue.trim() });
6958
+ req.issues = issues;
6959
+ fieldsChanged.push("issue");
6960
+ }
6327
6961
  if (fields.hasAliases) {
6328
6962
  const raw = args.aliases.trim();
6329
6963
  req.aliases = raw === "" ? [] : raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
@@ -6334,12 +6968,13 @@ function applyAmendMutations(req, args, fields) {
6334
6968
  var amendCommand;
6335
6969
  var init_amend = __esm(() => {
6336
6970
  init_src();
6971
+ init_grammar2();
6337
6972
  init_constants();
6338
6973
  init_discover();
6339
- init_grammar();
6974
+ init_grammar3();
6340
6975
  init__shared();
6341
6976
  init_req();
6342
- amendCommand = defineCommand15({
6977
+ amendCommand = defineCommand16({
6343
6978
  meta: {
6344
6979
  name: "amend",
6345
6980
  description: "Revise an unshipped requirement's fields in place (same id, no specVersion bump). Supersede shipped truth instead."
@@ -6357,11 +6992,11 @@ var init_amend = __esm(() => {
6357
6992
  },
6358
6993
  text: { type: "string", description: "New Requirement (statement) field value" },
6359
6994
  why: { type: "string", description: "New Why it matters field value" },
6360
- binds: {
6995
+ lives: { type: "string", description: "New Lives in (livesIn) field value" },
6996
+ issue: {
6361
6997
  type: "string",
6362
- description: "Binds value (validated for @-refs; not persisted in JSON \u2014 STOR-01)"
6998
+ description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
6363
6999
  },
6364
- lives: { type: "string", description: "New Lives in (livesIn) field value" },
6365
7000
  term: { type: "string", description: "New TERM headword (term field; TERM ids)" },
6366
7001
  aliases: {
6367
7002
  type: "string",
@@ -6379,7 +7014,7 @@ var init_amend = __esm(() => {
6379
7014
  process.exit(EXIT.USAGE);
6380
7015
  return;
6381
7016
  }
6382
- const platformDir = resolve20(args.platformDir ?? process.cwd());
7017
+ const platformDir = resolve21(args.platformDir ?? process.cwd());
6383
7018
  try {
6384
7019
  assertSpecPlatform(platformDir);
6385
7020
  } catch (e) {
@@ -6392,6 +7027,10 @@ var init_amend = __esm(() => {
6392
7027
  if (statusLc === "active") {
6393
7028
  await assertUnshipped(platformDir, id);
6394
7029
  }
7030
+ if (fields.hasText) {
7031
+ const key = id.slice(0, id.indexOf("-"));
7032
+ await enforceStatementGrammar(platformDir, key, args.text.trim(), "spec amend");
7033
+ }
6395
7034
  const { fieldsChanged, refValues } = applyAmendMutations(req, args, fields);
6396
7035
  warnUnresolvableRefs(platformDir, refValues);
6397
7036
  domain2.updated = localToday();
@@ -6428,13 +7067,14 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
6428
7067
  opts.brief,
6429
7068
  "",
6430
7069
  "Steps:",
6431
- "1. One requirement per TESTABLE PROMISE \u2014 never one per ticket; a brief fans out.",
6432
- "2. Placement: concept-wins per the domain charter above; run `spec query <text>` first to dedup.",
6433
- "3. Draft each to the GUARD template: `<command/surface> <promise> when <condition>` \u2014 name the subject explicitly.",
6434
- "4. Cold-read self-check: the statement must stand alone (restate referenced rules; no sibling free-riding; glossary terms only); timeless; observable inputs\u2192outputs; the why carries the failure mode. See spec-engine/TAXONOMY.md \xA74.10 for the eight-point standard.",
6435
- "5. Present the full batch for approval BEFORE writing.",
6436
- '6. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026" --lives "\u2026"`.',
6437
- "7. The originating ticket is `Issues:` PROVENANCE ONLY \u2014 never a requirement id, never a code tag."
7070
+ "1. One requirement per behavior \u2014 never one per ticket; a brief fans out into several.",
7071
+ "2. Placement: the domain whose promise the requirement protects (see the charter above); run `spec query <text>` first \u2014 if a close requirement exists, amend or relate it instead of minting.",
7072
+ '3. Draft each statement in one of the five EARS shapes ("shall" separates who acts from the observable result): `The <system> shall <result>` / `When <trigger>, the <system> shall <result>` / `While <state>, \u2026` / `If <failure>, then \u2026` / `Where <feature>, \u2026`. Example: "When an unknown id is passed, spec resolve shall print [] and exit 0."',
7073
+ "4. Self-check: the statement makes sense read alone; it describes what the system does, not how the code does it; no expiration dates ('currently', removed things); the why names what breaks if the promise stops holding \u2014 never a restatement.",
7074
+ "5. Tie each draft to its source: quote the exact phrase of the brief it derives from; label anything you inferred as inferred.",
7075
+ "6. Present the full batch (statement, why, placement, source quote) for approval BEFORE writing.",
7076
+ '7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026" --lives "\u2026"`.',
7077
+ "8. The originating ticket is provenance only \u2014 never a requirement id, never a code tag."
6438
7078
  ].join(`
6439
7079
  `);
6440
7080
  }
@@ -6571,7 +7211,7 @@ var init_mcp = __esm(() => {
6571
7211
  init_domains();
6572
7212
  init_constants();
6573
7213
  init_pipeline();
6574
- init_grammar();
7214
+ init_grammar3();
6575
7215
  init_sqlite();
6576
7216
  });
6577
7217
 
@@ -6580,15 +7220,15 @@ var exports_mcp = {};
6580
7220
  __export(exports_mcp, {
6581
7221
  mcpCommand: () => mcpCommand
6582
7222
  });
6583
- import { resolve as resolve21 } from "path";
7223
+ import { resolve as resolve22 } from "path";
6584
7224
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6585
- import { defineCommand as defineCommand16 } from "citty";
7225
+ import { defineCommand as defineCommand17 } from "citty";
6586
7226
  var mcpCommand;
6587
7227
  var init_mcp2 = __esm(() => {
6588
7228
  init_discover();
6589
7229
  init_mcp();
6590
7230
  init__shared();
6591
- mcpCommand = defineCommand16({
7231
+ mcpCommand = defineCommand17({
6592
7232
  meta: {
6593
7233
  name: "mcp",
6594
7234
  description: "Serve Spec Engine as an MCP (Model Context Protocol) server over stdio \u2014 agent-native query/resolve/check/report tools."
@@ -6601,7 +7241,7 @@ var init_mcp2 = __esm(() => {
6601
7241
  }
6602
7242
  },
6603
7243
  async run({ args }) {
6604
- const platformDir = resolve21(args.platformDir ?? process.cwd());
7244
+ const platformDir = resolve22(args.platformDir ?? process.cwd());
6605
7245
  try {
6606
7246
  assertSpecPlatform(platformDir);
6607
7247
  } catch (e) {
@@ -6619,9 +7259,9 @@ var exports_resolve = {};
6619
7259
  __export(exports_resolve, {
6620
7260
  resolveCommand: () => resolveCommand
6621
7261
  });
6622
- import { statSync as statSync5 } from "fs";
6623
- import { join as join17, relative, resolve as resolve22, sep as sep3 } from "path";
6624
- import { defineCommand as defineCommand17 } from "citty";
7262
+ import { statSync as statSync6 } from "fs";
7263
+ import { join as join23, relative, resolve as resolve23, sep as sep3 } from "path";
7264
+ import { defineCommand as defineCommand18 } from "citty";
6625
7265
  function classifyArg(a) {
6626
7266
  if (a.startsWith("--")) {
6627
7267
  const eq = a.indexOf("=");
@@ -6656,10 +7296,10 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
6656
7296
  let lastIsPlatformDir = false;
6657
7297
  let lastIsDirWithoutSpecEngine = false;
6658
7298
  try {
6659
- const resolved = resolve22(last);
6660
- if (statSync5(resolved).isDirectory()) {
7299
+ const resolved = resolve23(last);
7300
+ if (statSync6(resolved).isDirectory()) {
6661
7301
  try {
6662
- if (statSync5(join17(resolved, "spec-engine")).isDirectory()) {
7302
+ if (statSync6(join23(resolved, "spec-engine")).isDirectory()) {
6663
7303
  lastIsPlatformDir = true;
6664
7304
  } else {
6665
7305
  lastIsDirWithoutSpecEngine = true;
@@ -6753,7 +7393,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
6753
7393
  }
6754
7394
  await runReverseQuery({
6755
7395
  reqId: reqArg,
6756
- platformDir: resolve22(lone ?? process.cwd()),
7396
+ platformDir: resolve23(lone ?? process.cwd()),
6757
7397
  outArg: args.out,
6758
7398
  json: Boolean(args.json),
6759
7399
  fresh: Boolean(args.fresh),
@@ -6763,7 +7403,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
6763
7403
  function normalizeFileInputs(files, platformDir) {
6764
7404
  const normalized = [];
6765
7405
  for (const input of files) {
6766
- const rel = relative(platformDir, resolve22(platformDir, input));
7406
+ const rel = relative(platformDir, resolve23(platformDir, input));
6767
7407
  const segments = rel.split(sep3);
6768
7408
  if (rel === "" || rel === "." || segments[0] === "..") {
6769
7409
  console.error(`spec resolve: file path must be inside platformDir: ${input}`);
@@ -6781,7 +7421,7 @@ var init_resolve2 = __esm(() => {
6781
7421
  init_prompt();
6782
7422
  init__shared();
6783
7423
  VALUE_FLAGS = new Set(["--out", "-o", "--platformDir", "--req"]);
6784
- resolveCommand = defineCommand17({
7424
+ resolveCommand = defineCommand18({
6785
7425
  meta: {
6786
7426
  name: "resolve",
6787
7427
  description: "Return the requirements tagged in the given files (RSLV-01). Accepts multiple positional file paths (e.g. `spec resolve api/src/renew.ts api/src/charge.ts`) and a comma-split fallback inside a single positional (`spec resolve a.ts,b.ts`). Paths may be platform-relative or absolute under the platform tree."
@@ -6836,7 +7476,7 @@ var init_resolve2 = __esm(() => {
6836
7476
  process.exit(EXIT.USAGE);
6837
7477
  return;
6838
7478
  }
6839
- const platformDir = resolve22(resolved.platformDir ?? process.cwd());
7479
+ const platformDir = resolve23(resolved.platformDir ?? process.cwd());
6840
7480
  const outArg = args.out;
6841
7481
  const dbPath = resolveDbPath(platformDir, outArg);
6842
7482
  if (outArg)
@@ -6905,6 +7545,18 @@ function classifyGate(input) {
6905
7545
  pinned_spec_version: pin
6906
7546
  };
6907
7547
  }
7548
+ if (req.status === "Deprecated") {
7549
+ return {
7550
+ pass: false,
7551
+ reason: "DEPRECATED",
7552
+ repo: requestedRepoName,
7553
+ req_id: requestedReqId,
7554
+ detail: `Requirement ${requestedReqId} is Deprecated \u2014 it is end-of-life with no successor; do not build against it`,
7555
+ status: req.status,
7556
+ changed_at_version: req.changed_at_version,
7557
+ pinned_spec_version: pin
7558
+ };
7559
+ }
6908
7560
  if (req.changed_at_version > pin) {
6909
7561
  return {
6910
7562
  pass: false,
@@ -6942,9 +7594,9 @@ var exports_gate = {};
6942
7594
  __export(exports_gate, {
6943
7595
  gateCommand: () => gateCommand
6944
7596
  });
6945
- import { existsSync as existsSync15, mkdirSync as mkdirSync7 } from "fs";
6946
- import { dirname as dirname10, join as join18, resolve as resolve23 } from "path";
6947
- import { defineCommand as defineCommand18 } from "citty";
7597
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
7598
+ import { dirname as dirname10, join as join24, resolve as resolve24 } from "path";
7599
+ import { defineCommand as defineCommand19 } from "citty";
6948
7600
  function isEpipe(e) {
6949
7601
  if (e === null || typeof e !== "object")
6950
7602
  return false;
@@ -6958,8 +7610,8 @@ function resolveGateArgs(args) {
6958
7610
  console.error("spec gate: <repo> and <reqId> are required (e.g., spec gate api BILLING-009)");
6959
7611
  process.exit(EXIT.USAGE);
6960
7612
  }
6961
- const platformDir = resolve23(args.platformDir ?? process.cwd());
6962
- const hasSpecEngineDir = existsSync15(join18(platformDir, "spec-engine"));
7613
+ const platformDir = resolve24(args.platformDir ?? process.cwd());
7614
+ const hasSpecEngineDir = existsSync16(join24(platformDir, "spec-engine"));
6963
7615
  if (!hasSpecEngineDir) {
6964
7616
  console.error(formatNotASpecPlatform(platformDir));
6965
7617
  process.exit(EXIT.USAGE);
@@ -6998,7 +7650,7 @@ var init_gate = __esm(() => {
6998
7650
  init_prompt();
6999
7651
  init_sqlite();
7000
7652
  init__shared();
7001
- gateCommand = defineCommand18({
7653
+ gateCommand = defineCommand19({
7002
7654
  meta: {
7003
7655
  name: "gate",
7004
7656
  description: "Rung-3 approval primitive: passes iff <reqId> is Active and <repo>'s pinned spec_version covers its changed_at_version. Exits 0 PASS / 1 any gate failure / 2 crash or bad args."
@@ -7084,26 +7736,8 @@ var init_gate = __esm(() => {
7084
7736
  });
7085
7737
  });
7086
7738
 
7087
- // src/guard/directives.ts
7088
- function scanApprovals(text) {
7089
- const out = [];
7090
- for (const line of text.split(`
7091
- `)) {
7092
- const m = APPROVE_RE.exec(line);
7093
- if (m !== null) {
7094
- out.push({ req_id: m[1], reason: m[2].trim() });
7095
- }
7096
- }
7097
- return out;
7098
- }
7099
- var APPROVE_RE;
7100
- var init_directives = __esm(() => {
7101
- APPROVE_RE = /@spec\s+approve\s+([A-Z][A-Z0-9]*-\d+)\s+(\S.*)$/;
7102
- });
7103
-
7104
7739
  // src/guard/collect.ts
7105
- import { existsSync as existsSync16 } from "fs";
7106
- import { join as join19 } from "path";
7740
+ import { join as join25 } from "path";
7107
7741
  function isIgnored(path) {
7108
7742
  return isPathIgnored(path);
7109
7743
  }
@@ -7160,7 +7794,7 @@ async function collectWorktreeReqs(canonicalDir) {
7160
7794
  const activeIds = new Set;
7161
7795
  const supersedesTargets = new Set;
7162
7796
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
7163
- const text = await Bun.file(join19(canonicalDir, rel)).text();
7797
+ const text = await Bun.file(join25(canonicalDir, rel)).text();
7164
7798
  for (const r of parseReqs(text)) {
7165
7799
  ids.add(r.id);
7166
7800
  if (r.status.toLowerCase() === "active")
@@ -7181,20 +7815,6 @@ function tallyWorktreeTags(storage2) {
7181
7815
  }
7182
7816
  return { implCount, verifyCount };
7183
7817
  }
7184
- async function collectApprovals(platformDir, changed) {
7185
- const approved = new Set;
7186
- for (const entry of changed) {
7187
- if (entry.status === "D")
7188
- continue;
7189
- const abs = join19(platformDir, entry.path);
7190
- if (!existsSync16(abs))
7191
- continue;
7192
- const text = await Bun.file(abs).text();
7193
- for (const a of scanApprovals(text))
7194
- approved.add(a.req_id);
7195
- }
7196
- return approved;
7197
- }
7198
7818
  async function collectFacts(platformDir, ref, storage2) {
7199
7819
  const changed = gitDiffNameStatus(platformDir, ref);
7200
7820
  const specPaths = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
@@ -7202,9 +7822,8 @@ async function collectFacts(platformDir, ref, storage2) {
7202
7822
  const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
7203
7823
  const base = collectBaseReqs2(platformDir, ref, specPaths);
7204
7824
  const baseTags = collectBaseTags(platformDir, ref, codePaths);
7205
- const worktree = await collectWorktreeReqs(join19(platformDir, CANONICAL_SPECS_DIR));
7825
+ const worktree = await collectWorktreeReqs(join25(platformDir, CANONICAL_SPECS_DIR));
7206
7826
  const counts = tallyWorktreeTags(storage2);
7207
- const approved = await collectApprovals(platformDir, changed);
7208
7827
  return {
7209
7828
  baseReqs: base.reqs,
7210
7829
  baseReqPath: base.path,
@@ -7215,7 +7834,6 @@ async function collectFacts(platformDir, ref, storage2) {
7215
7834
  worktreeSupersedesTargets: worktree.supersedesTargets,
7216
7835
  worktreeImplCount: counts.implCount,
7217
7836
  worktreeVerifyCount: counts.verifyCount,
7218
- approved,
7219
7837
  deletedSpecFiles
7220
7838
  };
7221
7839
  }
@@ -7225,7 +7843,6 @@ var init_collect = __esm(() => {
7225
7843
  init_constants();
7226
7844
  init_fs();
7227
7845
  init_tags();
7228
- init_directives();
7229
7846
  CODE_EXTS = new Set(DEFAULT_EXTS);
7230
7847
  });
7231
7848
 
@@ -7263,7 +7880,7 @@ function deletesClause(losses) {
7263
7880
  return joinClauses(parts);
7264
7881
  }
7265
7882
  function requirementBlock(reqId, losses) {
7266
- return `${STOP} spec-guard: ${reqId} is Active and this change deletes ${deletesClause(losses)}. ` + "Requirements are superseded, never deleted. " + `Either run \`spec supersede ${reqId}\` with a successor, ` + "or stop and ask the user whether this requirement should die.";
7883
+ return `${STOP} spec-guard: ${reqId} is Active and this change deletes ${deletesClause(losses)}. ` + "Requirements are superseded, never deleted. " + `Either run \`spec supersede ${reqId}\` with a successor, or run ` + `\`spec deprecate ${reqId} --reason "..."\` to end it with a recorded reason.`;
7267
7884
  }
7268
7885
  function fileBlock(loss) {
7269
7886
  return `${STOP} spec-guard: the canonical spec file ${loss.file} was deleted. ` + "Requirements are superseded, never deleted \u2014 restore the file or supersede " + "its requirements into another domain before removing it.";
@@ -7314,7 +7931,17 @@ function reqDeletedLoss(base, f) {
7314
7931
  req_id: base.id,
7315
7932
  file: f.baseReqPath.get(base.id) ?? "",
7316
7933
  line: 0,
7317
- detail: `${base.id} was Active at the ref but is absent from the working-tree spec with no approved supersession`
7934
+ detail: `${base.id} was Active at the ref but is absent from the working-tree spec and was not superseded \u2014 a requirement is never deleted`
7935
+ };
7936
+ }
7937
+ function historyRemovedLoss(base, f) {
7938
+ const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
7939
+ return {
7940
+ kind: "REQUIREMENT_REMOVED",
7941
+ req_id: base.id,
7942
+ file: f.baseReqPath.get(base.id) ?? "",
7943
+ line: 0,
7944
+ detail: `${base.id} was ${display} at the ref but is absent from the working-tree spec \u2014 a requirement is never deleted, and its history is the record`
7318
7945
  };
7319
7946
  }
7320
7947
  function implLostLoss(id, site) {
@@ -7345,18 +7972,16 @@ function specFileDeletedLoss(path) {
7345
7972
  };
7346
7973
  }
7347
7974
  function reqDeletedFor(base, f) {
7348
- if (!isActive(base.status))
7349
- return null;
7350
- if (f.approved.has(base.id))
7351
- return null;
7352
7975
  if (f.worktreeReqIds.has(base.id))
7353
7976
  return null;
7977
+ if (!isActive(base.status))
7978
+ return historyRemovedLoss(base, f);
7354
7979
  if (isSuperseded(base, f))
7355
7980
  return null;
7356
7981
  return reqDeletedLoss(base, f);
7357
7982
  }
7358
7983
  function lastTagGone(id, count, f) {
7359
- return !f.approved.has(id) && f.worktreeActiveIds.has(id) && count === 0;
7984
+ return f.worktreeActiveIds.has(id) && count === 0;
7360
7985
  }
7361
7986
  function classifyLosses(f) {
7362
7987
  const out = [];
@@ -7383,14 +8008,14 @@ var exports_guard = {};
7383
8008
  __export(exports_guard, {
7384
8009
  guardCommand: () => guardCommand
7385
8010
  });
7386
- import { resolve as resolve24 } from "path";
7387
- import { defineCommand as defineCommand19 } from "citty";
8011
+ import { resolve as resolve25 } from "path";
8012
+ import { defineCommand as defineCommand20 } from "citty";
7388
8013
  var guardCommand;
7389
8014
  var init_guard = __esm(() => {
7390
8015
  init_constants();
7391
8016
  init_collect();
7392
8017
  init__shared();
7393
- guardCommand = defineCommand19({
8018
+ guardCommand = defineCommand20({
7394
8019
  meta: {
7395
8020
  name: "guard",
7396
8021
  description: "Loss detection: diff the requirement derivation at a git ref (default HEAD) against the working tree and block requirements about to be steamrolled."
@@ -7411,7 +8036,7 @@ var init_guard = __esm(() => {
7411
8036
  }
7412
8037
  },
7413
8038
  async run({ args }) {
7414
- const platformDir = resolve24(args.platformDir ?? process.cwd());
8039
+ const platformDir = resolve25(args.platformDir ?? process.cwd());
7415
8040
  const ref = args.against ?? "HEAD";
7416
8041
  const jsonMode = Boolean(args.json);
7417
8042
  if (!gitRefResolves(platformDir, ref)) {
@@ -7482,6 +8107,9 @@ function link(href, key, label, active) {
7482
8107
  function comingSoon(label) {
7483
8108
  return html`<span class="sidebar-link disabled" aria-disabled="true"><span class="sidebar-dot"></span><span class="sidebar-label">${label}<span class="soon">coming soon</span></span></span>`;
7484
8109
  }
8110
+ function flagged(feature, href, key, label, active) {
8111
+ return featureEnabled(feature) ? link(href, key, label, active) : comingSoon(label);
8112
+ }
7485
8113
  function navBar(active = "") {
7486
8114
  return html`<nav class="sidebar">
7487
8115
  <a href="/" class="sidebar-brand">${clampMark}<span>Spec Engine</span></a>
@@ -7490,22 +8118,22 @@ function navBar(active = "") {
7490
8118
  <div class="sidebar-group-label">Registry</div>
7491
8119
  ${link("/", "coverage", "Coverage", active)}
7492
8120
  ${link("/requirements", "requirements", "Requirements", active)}
7493
- ${comingSoon("Glossary")}
8121
+ ${flagged("glossary", "/glossary", "glossary", "Glossary", active)}
7494
8122
  </div>
7495
8123
  <div class="sidebar-group">
7496
8124
  <div class="sidebar-group-label">Trace</div>
7497
- ${comingSoon("Relations")}
7498
- ${comingSoon("Provenance")}
8125
+ ${flagged("relations", "/relations", "relations", "Relations", active)}
8126
+ ${flagged("provenance", "/provenance", "provenance", "Provenance", active)}
7499
8127
  </div>
7500
8128
  <div class="sidebar-group">
7501
8129
  <div class="sidebar-group-label">Workspace</div>
7502
- ${comingSoon("Query")}
7503
- ${comingSoon("Editor")}
8130
+ ${flagged("query", "/query", "query", "Query", active)}
8131
+ ${flagged("editor", "/editor", "editor", "Editor", active)}
7504
8132
  </div>
7505
8133
  <div class="sidebar-group">
7506
8134
  <div class="sidebar-group-label">System</div>
7507
8135
  ${link("/setup", "setup", "Setup", active)}
7508
- ${comingSoon("Logs")}
8136
+ ${flagged("logs", "/logs", "logs", "Logs", active)}
7509
8137
  </div>
7510
8138
  </div>
7511
8139
  <div class="sidebar-footer">
@@ -7516,6 +8144,7 @@ function navBar(active = "") {
7516
8144
  }
7517
8145
  var clampMark;
7518
8146
  var init_nav = __esm(() => {
8147
+ init_src();
7519
8148
  clampMark = html`<svg width="22" height="22" viewBox="0 0 64 64" fill="none" aria-hidden="true">
7520
8149
  <path d="M25 13 H13 V51 H25" stroke="var(--fg)" stroke-width="6" />
7521
8150
  <path d="M39 13 H51 V51 H39" stroke="var(--fg)" stroke-width="6" />
@@ -9053,7 +9682,7 @@ var init_components = __esm(() => {
9053
9682
  Active: "ACTIVE",
9054
9683
  Superseded: "SUPERSEDED",
9055
9684
  Draft: "DRAFT",
9056
- Retired: "DEPRECATED"
9685
+ Deprecated: "DEPRECATED"
9057
9686
  };
9058
9687
  });
9059
9688
 
@@ -9184,8 +9813,8 @@ function mountCoverage(app) {
9184
9813
  const reqs = await apiJson(app, "/api/requirements");
9185
9814
  const titleOf = new Map(reqs.map((r) => [r.id, r.text]));
9186
9815
  const showAll = c.req.query("all") === "1";
9187
- const visible = showAll ? rows : rows.filter((r) => r.req_status !== "Superseded" && r.req_status !== "Retired");
9188
- const hiddenCount = new Set(rows.filter((r) => r.req_status === "Superseded" || r.req_status === "Retired").map((r) => r.req_id)).size;
9816
+ const visible = showAll ? rows : rows.filter((r) => r.req_status !== "Superseded" && r.req_status !== "Deprecated");
9817
+ const hiddenCount = new Set(rows.filter((r) => r.req_status === "Superseded" || r.req_status === "Deprecated").map((r) => r.req_id)).size;
9189
9818
  const { repos, domains } = pivot(visible);
9190
9819
  const reqCount = domains.reduce((n, d) => n + d.reqs.length, 0);
9191
9820
  const reportRows = await apiJson(app, "/api/report");
@@ -9216,7 +9845,7 @@ function mountCoverage(app) {
9216
9845
  <span class="legend-sep"></span>
9217
9846
  <span class="legend-item">${requirementStatusBadge("Active")}</span>
9218
9847
  ${showAll ? html4`<span class="legend-item">${requirementStatusBadge("Superseded")}</span>
9219
- <span class="legend-item">${requirementStatusBadge("Retired")}</span>` : ""}
9848
+ <span class="legend-item">${requirementStatusBadge("Deprecated")}</span>` : ""}
9220
9849
  <span class="panel-spacer"></span>
9221
9850
  <span class="req-controls">
9222
9851
  ${showAll ? html4`<a class="req-btn status-toggle" href="/">Hide superseded &amp; retired</a>` : html4`<a class="req-btn status-toggle" href="/?all=1">Show superseded &amp; retired${hiddenCount > 0 ? html4` (${hiddenCount})` : ""}</a>`}
@@ -9391,6 +10020,8 @@ async function renderResult(res) {
9391
10020
  }
9392
10021
  function mountEditor(app) {
9393
10022
  app.get("/editor", (c) => {
10023
+ if (!featureEnabled("editor"))
10024
+ return c.html(comingSoonDoc("editor", "Editor"));
9394
10025
  const body = html5`
9395
10026
  ${navBar("editor")}
9396
10027
  <div class="eyebrow">/ Editor</div>
@@ -9445,6 +10076,8 @@ function mountEditor(app) {
9445
10076
  return c.html(page("Editor", body));
9446
10077
  });
9447
10078
  app.post("/editor/create", editorBodyLimit, async (c) => {
10079
+ if (!featureEnabled("editor"))
10080
+ return c.html(rejectPage(featureDisabledMessage("editor")), 404);
9448
10081
  if (crossOriginRejected(c))
9449
10082
  return c.html(rejectPage("cross-origin request rejected"), 403);
9450
10083
  const form = await c.req.parseBody();
@@ -9467,6 +10100,8 @@ function mountEditor(app) {
9467
10100
  return c.html(await renderResult(res));
9468
10101
  });
9469
10102
  app.post("/editor/amend", editorBodyLimit, async (c) => {
10103
+ if (!featureEnabled("editor"))
10104
+ return c.html(rejectPage(featureDisabledMessage("editor")), 404);
9470
10105
  if (crossOriginRejected(c))
9471
10106
  return c.html(rejectPage("cross-origin request rejected"), 403);
9472
10107
  const form = await c.req.parseBody();
@@ -9489,6 +10124,7 @@ function mountEditor(app) {
9489
10124
  var styleTag4, MAX_EDITOR_BODY_BYTES, editorBodyLimit;
9490
10125
  var init_editor = __esm(() => {
9491
10126
  init_src();
10127
+ init_components();
9492
10128
  init_nav();
9493
10129
  init_styles();
9494
10130
  styleTag4 = raw4(`<style>${styles_default}</style>`);
@@ -9571,7 +10207,7 @@ var init_propagation3 = __esm(() => {
9571
10207
  import { html as html7, raw as raw6 } from "hono/html";
9572
10208
  function mountProvenance(app) {
9573
10209
  app.get("/provenance", async (c) => {
9574
- if (!PROVENANCE_ENABLED)
10210
+ if (!featureEnabled("provenance"))
9575
10211
  return c.html(comingSoonDoc("provenance", "Provenance matrix"));
9576
10212
  const text = await apiText(app, "/api/provenance?resolve=1");
9577
10213
  let body;
@@ -9604,8 +10240,9 @@ function mountProvenance(app) {
9604
10240
  </html>`);
9605
10241
  });
9606
10242
  }
9607
- var styleTag6, PROVENANCE_ENABLED = false;
10243
+ var styleTag6;
9608
10244
  var init_provenance2 = __esm(() => {
10245
+ init_src();
9609
10246
  init_components();
9610
10247
  init_data();
9611
10248
  init_nav();
@@ -9617,7 +10254,7 @@ var init_provenance2 = __esm(() => {
9617
10254
  import { html as html8, raw as raw7 } from "hono/html";
9618
10255
  function mountQuery(app) {
9619
10256
  app.get("/query", async (c) => {
9620
- if (!QUERY_ENABLED)
10257
+ if (!featureEnabled("query"))
9621
10258
  return c.html(comingSoonDoc("query", "Query"));
9622
10259
  const q = (c.req.query("q") ?? "").trim();
9623
10260
  const form = html8`
@@ -9703,8 +10340,9 @@ function mountQuery(app) {
9703
10340
  </html>`);
9704
10341
  });
9705
10342
  }
9706
- var styleTag7, QUERY_ENABLED = false;
10343
+ var styleTag7;
9707
10344
  var init_query2 = __esm(() => {
10345
+ init_src();
9708
10346
  init_components();
9709
10347
  init_data();
9710
10348
  init_nav();
@@ -9716,7 +10354,7 @@ var init_query2 = __esm(() => {
9716
10354
  import { html as html9, raw as raw8 } from "hono/html";
9717
10355
  function mountRelations(app) {
9718
10356
  app.get("/relations", async (c) => {
9719
- if (!RELATIONS_ENABLED)
10357
+ if (!featureEnabled("relations"))
9720
10358
  return c.html(comingSoonDoc("relations", "Relates graph"));
9721
10359
  const rows = await apiJson(app, "/api/relations");
9722
10360
  let body;
@@ -9754,8 +10392,9 @@ function mountRelations(app) {
9754
10392
  </html>`);
9755
10393
  });
9756
10394
  }
9757
- var styleTag8, RELATIONS_ENABLED = false;
10395
+ var styleTag8;
9758
10396
  var init_relations2 = __esm(() => {
10397
+ init_src();
9759
10398
  init_components();
9760
10399
  init_data();
9761
10400
  init_nav();
@@ -9829,7 +10468,7 @@ function verPill(r, versioned) {
9829
10468
  return html10`<span class="ver-pill ${versioned ? "versioned" : ""}" title="domain spec version (envelope)">v${r.spec_version}</span>`;
9830
10469
  }
9831
10470
  function chainVerLabel(v) {
9832
- const dead = v.status === "Superseded" || v.status === "Retired";
10471
+ const dead = v.status === "Superseded" || v.status === "Deprecated";
9833
10472
  if (dead && v.superseded_at_version != null) {
9834
10473
  return html10`<span class="chain-ver">superseded at v${v.superseded_at_version}</span>`;
9835
10474
  }
@@ -9972,8 +10611,8 @@ function mountRequirements(app) {
9972
10611
  const reqs = await apiJson(app, "/api/requirements");
9973
10612
  const coverage = await apiJson(app, "/api/coverage");
9974
10613
  const showAll = c.req.query("all") === "1";
9975
- const listed = showAll ? reqs : reqs.filter((r) => r.status !== "Superseded" && r.status !== "Retired");
9976
- const hiddenCount = reqs.filter((r) => r.status === "Superseded" || r.status === "Retired").length;
10614
+ const listed = showAll ? reqs : reqs.filter((r) => r.status !== "Superseded" && r.status !== "Deprecated");
10615
+ const hiddenCount = reqs.filter((r) => r.status === "Superseded" || r.status === "Deprecated").length;
9977
10616
  const tagLists = await Promise.all(listed.map((r) => apiJson(app, `/api/resolve?req=${encodeURIComponent(r.id)}`)));
9978
10617
  const tagsById = new Map(listed.map((r, i) => [r.id, tagLists[i] ?? []]));
9979
10618
  const { domains, defaultId } = buildDomains(reqs, listed, coverage, tagsById);
@@ -10249,7 +10888,7 @@ var init_server = __esm(() => {
10249
10888
 
10250
10889
  // src/server/api.ts
10251
10890
  import { existsSync as existsSync17 } from "fs";
10252
- import { join as join20 } from "path";
10891
+ import { join as join26 } from "path";
10253
10892
  function hasTraversalSegment(p) {
10254
10893
  return p.split(/[/\\]/).some((seg) => seg === "..");
10255
10894
  }
@@ -10356,7 +10995,7 @@ function resolveCreateTarget(c, platformDir, body) {
10356
10995
  return { ok: false, res: c.json({ error: "unknown domain key" }, 400) };
10357
10996
  }
10358
10997
  const relFile = `spec-engine/${key}/SPEC.json`;
10359
- const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
10998
+ const specPath = join26(platformDir, "spec-engine", key, "SPEC.json");
10360
10999
  if (!existsSync17(specPath))
10361
11000
  return { ok: false, res: c.json({ error: "not found" }, 404) };
10362
11001
  return { ok: true, key, relFile, specPath };
@@ -10376,7 +11015,7 @@ function buildRequirement(body, id) {
10376
11015
  }
10377
11016
  async function locateRequirement(platformDir, id) {
10378
11017
  for (const key of listDomainKeys(platformDir)) {
10379
- const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
11018
+ const specPath = join26(platformDir, "spec-engine", key, "SPEC.json");
10380
11019
  const relFile = `spec-engine/${key}/SPEC.json`;
10381
11020
  let domain2;
10382
11021
  try {
@@ -10406,6 +11045,40 @@ function applyAmendFields(req, body) {
10406
11045
  req.livesIn = toLivesIn(body.livesIn);
10407
11046
  return true;
10408
11047
  }
11048
+ function hasAmendFields(body) {
11049
+ return typeof body.statement === "string" || "why" in body || "livesIn" in body;
11050
+ }
11051
+ async function amendGateRejection(c, platformDir, storage2, id, req) {
11052
+ const rawStatus = typeof req.status === "string" ? req.status : "";
11053
+ const statusLc = rawStatus.toLowerCase();
11054
+ if (statusLc !== "active" && statusLc !== "draft") {
11055
+ return c.json({
11056
+ error: `${id} is ${rawStatus} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`
11057
+ }, 409);
11058
+ }
11059
+ if (statusLc === "active") {
11060
+ await runIndex({ platformDir, storage: storage2 });
11061
+ const bound = storage2.listTags({ req_id: id }).filter((t) => t.kind === "implements" || t.kind === "verifies");
11062
+ if (bound.length > 0) {
11063
+ const site = bound[0];
11064
+ return c.json({
11065
+ error: `${id} is shipped \u2014 ${bound.length} code tag(s) bind it (e.g. ${site.file}:${site.line}). A bound requirement is immutable; supersede it with a successor instead of amending in place.`
11066
+ }, 409);
11067
+ }
11068
+ }
11069
+ return null;
11070
+ }
11071
+ function searchFtsResponse(c, storage2, q, limit) {
11072
+ try {
11073
+ return c.json(storage2.searchFts(q, limit));
11074
+ } catch (e) {
11075
+ const msg = e instanceof Error ? e.message : String(e);
11076
+ if (msg.startsWith(FTS_SYNTAX_ERROR_PREFIX)) {
11077
+ return c.json({ error: "FTS5 grammar error; wrap phrases in double quotes" }, 400);
11078
+ }
11079
+ throw e;
11080
+ }
11081
+ }
10409
11082
  function parseQueryLimit(c, rawLimit) {
10410
11083
  if (!POSITIVE_INT_RE.test(rawLimit)) {
10411
11084
  return {
@@ -10449,6 +11122,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10449
11122
  return row ? c.json(row) : c.json({ error: "not found" }, 404);
10450
11123
  }));
10451
11124
  app.post("/api/requirements", guarded(async function createRequirement(c) {
11125
+ if (!featureEnabled("editor")) {
11126
+ return c.json({ error: featureDisabledMessage("editor") }, 404);
11127
+ }
10452
11128
  const originReject = rejectCrossOrigin(c);
10453
11129
  if (originReject)
10454
11130
  return originReject;
@@ -10483,6 +11159,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10483
11159
  });
10484
11160
  }));
10485
11161
  app.put("/api/requirements/:id", guarded(async function amendRequirement(c) {
11162
+ if (!featureEnabled("editor")) {
11163
+ return c.json({ error: featureDisabledMessage("editor") }, 404);
11164
+ }
10486
11165
  const originReject = rejectCrossOrigin(c);
10487
11166
  if (originReject)
10488
11167
  return originReject;
@@ -10501,9 +11180,13 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10501
11180
  if (found.kind === "invalid") {
10502
11181
  return c.json({ error: "INVALID_DOMAIN_FILE", detail: `${found.relFile} is not valid JSON` }, 400);
10503
11182
  }
10504
- if (!applyAmendFields(found.req, body)) {
11183
+ if (!hasAmendFields(body)) {
10505
11184
  return c.json({ error: "nothing to amend \u2014 provide statement, why, or livesIn" }, 400);
10506
11185
  }
11186
+ const gateReject = await amendGateRejection(c, platformDir, storage2, id, found.req);
11187
+ if (gateReject)
11188
+ return gateReject;
11189
+ applyAmendFields(found.req, body);
10507
11190
  found.domain.updated = localToday();
10508
11191
  const res = await validateAndWrite(found.specPath, found.domain, found.relFile);
10509
11192
  if (!res.ok) {
@@ -10515,6 +11198,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10515
11198
  }));
10516
11199
  app.get("/api/propagation/:id", guarded((c) => c.json(storage2.propagationFor(c.req.param("id") ?? ""))));
10517
11200
  app.get("/api/query", guarded((c) => {
11201
+ if (!featureEnabled("query")) {
11202
+ return c.json({ error: featureDisabledMessage("query") }, 404);
11203
+ }
10518
11204
  const q = (c.req.query("q") ?? "").trim();
10519
11205
  if (!q) {
10520
11206
  return c.json({ error: "q is required (non-empty FTS5 MATCH query)" }, 400);
@@ -10523,17 +11209,12 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10523
11209
  const parsedLimit = parseQueryLimit(c, rawLimit);
10524
11210
  if (!parsedLimit.ok)
10525
11211
  return parsedLimit.res;
10526
- try {
10527
- return c.json(storage2.searchFts(q, parsedLimit.limit));
10528
- } catch (e) {
10529
- const msg = e instanceof Error ? e.message : String(e);
10530
- if (msg.startsWith(FTS_SYNTAX_ERROR_PREFIX)) {
10531
- return c.json({ error: "FTS5 grammar error; wrap phrases in double quotes" }, 400);
10532
- }
10533
- throw e;
10534
- }
11212
+ return searchFtsResponse(c, storage2, q, parsedLimit.limit);
10535
11213
  }));
10536
11214
  app.get("/api/relations", guarded((c) => {
11215
+ if (!featureEnabled("relations")) {
11216
+ return c.json({ error: featureDisabledMessage("relations") }, 404);
11217
+ }
10537
11218
  const format = c.req.query("format");
10538
11219
  if (format !== undefined && format !== "mermaid") {
10539
11220
  return c.json({ error: 'format must be "mermaid" when provided' }, 400);
@@ -10545,6 +11226,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10545
11226
  return c.json(sortRelations(rows));
10546
11227
  }));
10547
11228
  app.get("/api/provenance", guarded(async (c) => {
11229
+ if (!featureEnabled("provenance")) {
11230
+ return c.json({ error: featureDisabledMessage("provenance") }, 404);
11231
+ }
10548
11232
  const resolveParam = c.req.query("resolve");
10549
11233
  if (resolveParam !== undefined && resolveParam !== "1") {
10550
11234
  return c.json({ error: 'resolve must be "1" when provided' }, 400);
@@ -10556,7 +11240,15 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10556
11240
  }
10557
11241
  return c.json(rows);
10558
11242
  }));
10559
- app.get("/api/provenance/:issue", guarded((c) => c.json(storage2.provenanceByIssue(c.req.param("issue") ?? ""))));
11243
+ app.get("/api/provenance/by-issue", guarded((c) => {
11244
+ if (!featureEnabled("provenance")) {
11245
+ return c.json({ error: featureDisabledMessage("provenance") }, 404);
11246
+ }
11247
+ const issue = (c.req.query("issue") ?? "").trim();
11248
+ if (issue === "")
11249
+ return c.json({ error: "issue is required (non-empty)" }, 400);
11250
+ return c.json(storage2.provenanceByIssue(issue));
11251
+ }));
10560
11252
  app.get("/api/resolve", guarded((c) => {
10561
11253
  const reqParam = c.req.query("req");
10562
11254
  if (reqParam !== undefined)
@@ -10597,8 +11289,8 @@ __export(exports_serve, {
10597
11289
  composeServeApp: () => composeServeApp
10598
11290
  });
10599
11291
  import { existsSync as existsSync18, mkdirSync as mkdirSync8 } from "fs";
10600
- import { dirname as dirname11, resolve as resolve25 } from "path";
10601
- import { defineCommand as defineCommand20 } from "citty";
11292
+ import { dirname as dirname11, resolve as resolve26 } from "path";
11293
+ import { defineCommand as defineCommand21 } from "citty";
10602
11294
  import { Hono as Hono2 } from "hono";
10603
11295
  function composeServeApp(storage2, platformDir = process.cwd()) {
10604
11296
  const app = new Hono2;
@@ -10646,7 +11338,7 @@ var init_serve = __esm(() => {
10646
11338
  init_api();
10647
11339
  init_sqlite();
10648
11340
  init__shared();
10649
- serveCommand = defineCommand20({
11341
+ serveCommand = defineCommand21({
10650
11342
  meta: {
10651
11343
  name: "serve",
10652
11344
  description: "Run the local webapp. Binds 127.0.0.1 on --port (loopback only \u2014 no --host). --probe is the SERV-04 asset-embedding smoke."
@@ -10692,7 +11384,7 @@ var init_serve = __esm(() => {
10692
11384
  process.exit(EXIT.USAGE);
10693
11385
  return;
10694
11386
  }
10695
- const platformDir = resolve25(args.platformDir ?? process.cwd());
11387
+ const platformDir = resolve26(args.platformDir ?? process.cwd());
10696
11388
  const outArg = args.out;
10697
11389
  const dbPath = resolveDbPath(platformDir, outArg);
10698
11390
  if (outArg)
@@ -10743,18 +11435,20 @@ var init_serve = __esm(() => {
10743
11435
  // src/commands/docs.ts
10744
11436
  var exports_docs = {};
10745
11437
  __export(exports_docs, {
11438
+ runProbe: () => runProbe2,
10746
11439
  resolveDocsRootFrom: () => resolveDocsRootFrom,
10747
11440
  resolveDocsRoot: () => resolveDocsRoot,
11441
+ exitNoDocsRoot: () => exitNoDocsRoot,
10748
11442
  docsCommand: () => docsCommand,
10749
11443
  createDocsFetchHandler: () => createDocsFetchHandler
10750
11444
  });
10751
- import { existsSync as existsSync19, statSync as statSync6 } from "fs";
10752
- import { join as join21, normalize, sep as sep4 } from "path";
11445
+ import { existsSync as existsSync19, statSync as statSync7 } from "fs";
11446
+ import { join as join27, normalize, sep as sep4 } from "path";
10753
11447
  import { fileURLToPath } from "url";
10754
- import { defineCommand as defineCommand21 } from "citty";
11448
+ import { defineCommand as defineCommand22 } from "citty";
10755
11449
  function resolveDocsRootFrom(candidates) {
10756
11450
  for (const candidate of candidates) {
10757
- if (existsSync19(join21(candidate, "index.html")))
11451
+ if (existsSync19(join27(candidate, "index.html")))
10758
11452
  return candidate;
10759
11453
  }
10760
11454
  return null;
@@ -10778,16 +11472,16 @@ function createDocsFetchHandler(root) {
10778
11472
  if (pathname.includes("\x00")) {
10779
11473
  return new Response("Bad request", { status: 400 });
10780
11474
  }
10781
- const target = normalize(join21(normalRoot, pathname));
11475
+ const target = normalize(join27(normalRoot, pathname));
10782
11476
  if (target !== normalRoot && !target.startsWith(rootPrefix)) {
10783
11477
  return new Response("Not found", { status: 404 });
10784
11478
  }
10785
- for (const candidate of [target, join21(target, "index.html"), `${target}.html`]) {
10786
- if (existsSync19(candidate) && statSync6(candidate).isFile()) {
11479
+ for (const candidate of [target, join27(target, "index.html"), `${target}.html`]) {
11480
+ if (existsSync19(candidate) && statSync7(candidate).isFile()) {
10787
11481
  return new Response(Bun.file(candidate));
10788
11482
  }
10789
11483
  }
10790
- const notFoundPage = join21(normalRoot, "404.html");
11484
+ const notFoundPage = join27(normalRoot, "404.html");
10791
11485
  if (existsSync19(notFoundPage)) {
10792
11486
  return new Response(Bun.file(notFoundPage), { status: 404 });
10793
11487
  }
@@ -10822,10 +11516,14 @@ async function runProbe2(root) {
10822
11516
  server.stop();
10823
11517
  process.exit(EXIT.OK);
10824
11518
  }
11519
+ function exitNoDocsRoot() {
11520
+ console.error("spec docs: no built documentation found. From a checkout run `bun run build:site` " + "(writes packages/site/dist); the published npm package ships it at dist/docs.");
11521
+ process.exit(EXIT.USAGE);
11522
+ }
10825
11523
  var docsCommand;
10826
11524
  var init_docs2 = __esm(() => {
10827
11525
  init_constants();
10828
- docsCommand = defineCommand21({
11526
+ docsCommand = defineCommand22({
10829
11527
  meta: {
10830
11528
  name: "docs",
10831
11529
  description: "Serve the bundled documentation site offline. Binds 127.0.0.1 on --port (loopback only \u2014 no --host). --probe is the boot-and-fetch smoke."
@@ -10844,8 +11542,7 @@ var init_docs2 = __esm(() => {
10844
11542
  async run({ args }) {
10845
11543
  const root = resolveDocsRoot();
10846
11544
  if (!root) {
10847
- console.error("spec docs: no built documentation found. From a checkout run `bun run build:site` " + "(writes packages/site/dist); the published npm package ships it at dist/docs.");
10848
- process.exit(EXIT.USAGE);
11545
+ exitNoDocsRoot();
10849
11546
  return;
10850
11547
  }
10851
11548
  if (args.probe) {
@@ -10887,10 +11584,10 @@ __export(exports___smoke, {
10887
11584
  schemaSmokeCommand: () => schemaSmokeCommand,
10888
11585
  schemaMismatchSmokeCommand: () => schemaMismatchSmokeCommand
10889
11586
  });
10890
- import { defineCommand as defineCommand22 } from "citty";
11587
+ import { defineCommand as defineCommand23 } from "citty";
10891
11588
  var schemaSmokeCommand, schemaMismatchSmokeCommand;
10892
11589
  var init___smoke = __esm(() => {
10893
- schemaSmokeCommand = defineCommand22({
11590
+ schemaSmokeCommand = defineCommand23({
10894
11591
  meta: {
10895
11592
  name: "__schema-smoke",
10896
11593
  description: "(hidden CI) open a fresh DB, exec DDL, assert the schema shape",
@@ -10933,7 +11630,7 @@ var init___smoke = __esm(() => {
10933
11630
  console.log("__schema-smoke OK");
10934
11631
  }
10935
11632
  });
10936
- schemaMismatchSmokeCommand = defineCommand22({
11633
+ schemaMismatchSmokeCommand = defineCommand23({
10937
11634
  meta: {
10938
11635
  name: "__schema-mismatch-smoke",
10939
11636
  description: "(hidden CI) corrupt _schema_version, re-open, assert silent rebuild",
@@ -10958,11 +11655,11 @@ var init___smoke = __esm(() => {
10958
11655
  });
10959
11656
 
10960
11657
  // src/cli.ts
10961
- import { defineCommand as defineCommand23, runMain, showUsage } from "citty";
11658
+ import { defineCommand as defineCommand24, runMain, showUsage } from "citty";
10962
11659
  // package.json
10963
11660
  var package_default = {
10964
11661
  name: "@spec-engine/spec-engine",
10965
- version: "0.1.1",
11662
+ version: "0.3.0",
10966
11663
  description: "Cross-repo spec engine \u2014 durable requirements with permanent IDs, bound to code via @spec tags, checked in CI. Ships the spec CLI, the local coverage webapp (spec serve), and offline docs (spec docs).",
10967
11664
  license: "MIT",
10968
11665
  type: "module",
@@ -10972,6 +11669,10 @@ var package_default = {
10972
11669
  files: [
10973
11670
  "dist"
10974
11671
  ],
11672
+ publishConfig: {
11673
+ access: "public",
11674
+ provenance: true
11675
+ },
10975
11676
  engines: {
10976
11677
  bun: ">=1.3.0"
10977
11678
  },
@@ -11010,7 +11711,7 @@ var package_default = {
11010
11711
  };
11011
11712
 
11012
11713
  // src/cli.ts
11013
- var main = defineCommand23({
11714
+ var main = defineCommand24({
11014
11715
  meta: {
11015
11716
  name: "spec",
11016
11717
  version: package_default.version,
@@ -11030,6 +11731,7 @@ var main = defineCommand23({
11030
11731
  term: () => Promise.resolve().then(() => (init_term(), exports_term)).then((m) => m.termCommand),
11031
11732
  glossary: () => Promise.resolve().then(() => (init_glossary(), exports_glossary)).then((m) => m.glossaryCommand),
11032
11733
  supersede: () => Promise.resolve().then(() => (init_supersede(), exports_supersede)).then((m) => m.supersedeCommand),
11734
+ deprecate: () => Promise.resolve().then(() => (init_deprecate(), exports_deprecate)).then((m) => m.deprecateCommand),
11033
11735
  move: () => Promise.resolve().then(() => (init_move(), exports_move)).then((m) => m.moveCommand),
11034
11736
  amend: () => Promise.resolve().then(() => (init_amend(), exports_amend)).then((m) => m.amendCommand),
11035
11737
  mcp: () => Promise.resolve().then(() => (init_mcp2(), exports_mcp)).then((m) => m.mcpCommand),