@spec-engine/spec-engine 0.1.0 → 0.2.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 (44) 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 +9 -9
  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 +11 -10
  8. package/dist/docs/format/index.html +10 -10
  9. package/dist/docs/getting-started/index.html +35 -18
  10. package/dist/docs/guides/adoption/index.html +9 -9
  11. package/dist/docs/guides/platform-setup/index.html +9 -8
  12. package/dist/docs/guides/single-repo/index.html +9 -8
  13. package/dist/docs/index.html +2 -2
  14. package/dist/docs/overview/index.html +31 -43
  15. package/dist/docs/pagefind/fragment/en_2d1adec.pf_fragment +0 -0
  16. package/dist/docs/pagefind/fragment/en_3b7d939.pf_fragment +0 -0
  17. package/dist/docs/pagefind/fragment/en_435dca1.pf_fragment +0 -0
  18. package/dist/docs/pagefind/fragment/en_4bdd17f.pf_fragment +0 -0
  19. package/dist/docs/pagefind/fragment/en_523ebff.pf_fragment +0 -0
  20. package/dist/docs/pagefind/fragment/en_649983a.pf_fragment +0 -0
  21. package/dist/docs/pagefind/fragment/en_6c144c1.pf_fragment +0 -0
  22. package/dist/docs/pagefind/fragment/en_8d65739.pf_fragment +0 -0
  23. package/dist/docs/pagefind/fragment/en_8ef15c5.pf_fragment +0 -0
  24. package/dist/docs/pagefind/fragment/en_93176f1.pf_fragment +0 -0
  25. package/dist/docs/pagefind/index/en_d1196c8.pf_index +0 -0
  26. package/dist/docs/pagefind/pagefind-entry.json +1 -1
  27. package/dist/docs/pagefind/pagefind.en_f3d65f37ce.pf_meta +0 -0
  28. package/dist/docs/pagefind/wasm.en.pagefind +0 -0
  29. package/dist/docs/pagefind/wasm.unknown.pagefind +0 -0
  30. package/dist/docs/tags/index.html +8 -8
  31. package/dist/impl.js +1201 -638
  32. package/package.json +5 -1
  33. package/dist/docs/pagefind/fragment/en_3f97d08.pf_fragment +0 -0
  34. package/dist/docs/pagefind/fragment/en_51433aa.pf_fragment +0 -0
  35. package/dist/docs/pagefind/fragment/en_629887a.pf_fragment +0 -0
  36. package/dist/docs/pagefind/fragment/en_6339f38.pf_fragment +0 -0
  37. package/dist/docs/pagefind/fragment/en_74b2c3a.pf_fragment +0 -0
  38. package/dist/docs/pagefind/fragment/en_a4de4f3.pf_fragment +0 -0
  39. package/dist/docs/pagefind/fragment/en_ae342c4.pf_fragment +0 -0
  40. package/dist/docs/pagefind/fragment/en_b571467.pf_fragment +0 -0
  41. package/dist/docs/pagefind/fragment/en_cc9f632.pf_fragment +0 -0
  42. package/dist/docs/pagefind/fragment/en_dc8d1bd.pf_fragment +0 -0
  43. package/dist/docs/pagefind/index/en_4f59892.pf_index +0 -0
  44. package/dist/docs/pagefind/pagefind.en_c8da4a195d.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();
@@ -772,7 +883,8 @@ var init_domainJson = __esm(() => {
772
883
  active: "Active",
773
884
  draft: "Draft",
774
885
  superseded: "Superseded",
775
- retired: "Retired"
886
+ deprecated: "Deprecated",
887
+ retired: "Deprecated"
776
888
  });
777
889
  VALID_ROLES = new Set(["created", "supersedes-via", "amends-via"]);
778
890
  });
@@ -1292,6 +1404,8 @@ class SqliteStorage {
1292
1404
  for (const sql of [
1293
1405
  Q1_DANGLING_TAG_SQL,
1294
1406
  Q2_SUPERSEDED_REFERENCED_SQL,
1407
+ Q2B_DEPRECATED_REFERENCED_SQL,
1408
+ Q2D_DRAFT_REFERENCED_SQL,
1295
1409
  Q3_DRIFT_SQL,
1296
1410
  Q4_ORPHAN_REQ_SQL,
1297
1411
  Q5_UNVERIFIED_REQ_SQL,
@@ -1651,6 +1765,34 @@ FROM tags t
1651
1765
  JOIN requirements r ON r.id = t.req_id
1652
1766
  WHERE r.status = 'Superseded'
1653
1767
  ORDER BY t.repo, t.file, t.line
1768
+ `, Q2B_DEPRECATED_REFERENCED_SQL = `
1769
+ SELECT
1770
+ 'DEPRECATED_REFERENCED' AS code,
1771
+ t.repo AS repo,
1772
+ t.file AS source_file,
1773
+ t.line AS line,
1774
+ t.req_id AS req_id,
1775
+ ('Tag references deprecated requirement ' || t.req_id ||
1776
+ ' \u2014 it is end-of-life with no successor') AS detail,
1777
+ 'error' AS severity
1778
+ FROM tags t
1779
+ JOIN requirements r ON r.id = t.req_id
1780
+ WHERE r.status = 'Deprecated'
1781
+ ORDER BY t.repo, t.file, t.line
1782
+ `, Q2D_DRAFT_REFERENCED_SQL = `
1783
+ SELECT
1784
+ 'DRAFT_REFERENCED' AS code,
1785
+ t.repo AS repo,
1786
+ t.file AS source_file,
1787
+ t.line AS line,
1788
+ t.req_id AS req_id,
1789
+ ('Tag references Draft requirement ' || t.req_id ||
1790
+ ' \u2014 code is bound to a promise nobody approved yet') AS detail,
1791
+ 'warning' AS severity
1792
+ FROM tags t
1793
+ JOIN requirements r ON r.id = t.req_id
1794
+ WHERE r.status = 'Draft'
1795
+ ORDER BY t.repo, t.file, t.line
1654
1796
  `, Q3_DRIFT_SQL = `
1655
1797
  SELECT
1656
1798
  'DRIFT' AS code,
@@ -1680,6 +1822,7 @@ WHERE r.status = 'Active'
1680
1822
  -- code-coverage obligation \u2014 exclude the reserved TERM domain so a migrated
1681
1823
  -- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
1682
1824
  -- mirrors the coverage VIEW exclusion (WHERE r.key != 'TERM') in schema.ts.
1825
+ -- @spec SCHM-017
1683
1826
  AND r.key != 'TERM'
1684
1827
  AND NOT EXISTS (
1685
1828
  SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
@@ -1853,7 +1996,7 @@ SELECT
1853
1996
  FROM repos
1854
1997
  WHERE repos.name != 'spec-engine'
1855
1998
  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
1999
+ `, 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_TAGS_SQL = `SELECT id, req_id, repo, file, line, kind, level FROM tags
1857
2000
  WHERE ($repo IS NULL OR repo = $repo)
1858
2001
  AND ($req_id IS NULL OR req_id = $req_id)
1859
2002
  AND ($file IS NULL OR file = $file)
@@ -2659,8 +2802,8 @@ var init_errors = __esm(() => {
2659
2802
  });
2660
2803
 
2661
2804
  // src/commands/_shared.ts
2662
- import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
2663
- import { dirname as dirname5 } from "path";
2805
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync4 } from "fs";
2806
+ import { dirname as dirname5, join as join7 } from "path";
2664
2807
  function assertContainedPath(resolved, platformDir, subject) {
2665
2808
  if (!isContainedPath(resolved, platformDir)) {
2666
2809
  console.error(`${subject} path must be inside platformDir (resolved to ${resolved})`);
@@ -2692,6 +2835,23 @@ async function reindexAndListTags(platformDir, reqId) {
2692
2835
  storage2.close();
2693
2836
  }
2694
2837
  }
2838
+ function warnIfIndexStale(platformDir, dbPath) {
2839
+ try {
2840
+ const dbMtime = statSync4(dbPath).mtimeMs;
2841
+ const specsDir = join7(platformDir, "spec-engine");
2842
+ for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
2843
+ if (!entry.isDirectory())
2844
+ continue;
2845
+ const specPath = join7(specsDir, entry.name, "SPEC.json");
2846
+ if (!existsSync4(specPath))
2847
+ continue;
2848
+ if (statSync4(specPath).mtimeMs > dbMtime) {
2849
+ 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.`);
2850
+ return;
2851
+ }
2852
+ }
2853
+ } catch {}
2854
+ }
2695
2855
  async function withReadStorage(opts, fn) {
2696
2856
  const { platformDir, dbPath, fresh } = opts;
2697
2857
  try {
@@ -2704,6 +2864,8 @@ async function withReadStorage(opts, fn) {
2704
2864
  try {
2705
2865
  if (needsIndex || storage2.listRepos().length === 0) {
2706
2866
  await runIndex({ platformDir, storage: storage2 });
2867
+ } else {
2868
+ warnIfIndexStale(platformDir, dbPath);
2707
2869
  }
2708
2870
  await fn(storage2);
2709
2871
  } finally {
@@ -2987,6 +3149,147 @@ function renderDiagnostics(rows, mode) {
2987
3149
  `);
2988
3150
  }
2989
3151
 
3152
+ // src/authoring/domains.ts
3153
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
3154
+ import { join as join8 } from "path";
3155
+ function normalizeDomainKey(raw) {
3156
+ return raw.toUpperCase().replace(/\s+/g, "");
3157
+ }
3158
+ function listDomainKeys(platformDir) {
3159
+ const specsDir = join8(platformDir, "spec-engine");
3160
+ return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(join8(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
3161
+ }
3162
+ async function domainScope(platformDir, key) {
3163
+ const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
3164
+ if (!existsSync5(jsonPath))
3165
+ return null;
3166
+ try {
3167
+ const env = JSON.parse(await Bun.file(jsonPath).text());
3168
+ if (typeof env.scope !== "string" || env.scope.trim() === "")
3169
+ return null;
3170
+ return env.scope;
3171
+ } catch (e) {
3172
+ if (e instanceof SyntaxError)
3173
+ return null;
3174
+ console.error(`spec: could not read scope for ${key}: ${e.message}`);
3175
+ return null;
3176
+ }
3177
+ }
3178
+ async function domainsWithScope(platformDir) {
3179
+ const keys = listDomainKeys(platformDir);
3180
+ return Promise.all(keys.map(async (key) => ({ key, scope: await domainScope(platformDir, key) })));
3181
+ }
3182
+ function scaffoldDomainObject(key, today) {
3183
+ return {
3184
+ key,
3185
+ owner: null,
3186
+ ...key === "TERM" ? { specVersion: 1 } : {},
3187
+ updated: today,
3188
+ requirements: []
3189
+ };
3190
+ }
3191
+ function maxSeqOf(domain2) {
3192
+ const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
3193
+ return reqs.reduce((m, r) => {
3194
+ const id = typeof r?.id === "string" ? r.id : "";
3195
+ const seq = Number(id.split("-")[1]);
3196
+ return Number.isFinite(seq) ? Math.max(m, seq) : m;
3197
+ }, 0);
3198
+ }
3199
+ async function headMaxSeq(platformDir, key) {
3200
+ try {
3201
+ const proc = Bun.spawn(["git", "-C", join8(platformDir, "spec-engine", key), "show", "HEAD:./SPEC.json"], { stdout: "pipe", stderr: "ignore" });
3202
+ const text = await new Response(proc.stdout).text();
3203
+ if (await proc.exited !== 0)
3204
+ return 0;
3205
+ return maxSeqOf(JSON.parse(text));
3206
+ } catch {
3207
+ return 0;
3208
+ }
3209
+ }
3210
+ async function nextRequirementId(platformDir, key) {
3211
+ const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
3212
+ if (!existsSync5(jsonPath))
3213
+ return `${key}-001`;
3214
+ const domain2 = JSON.parse(await Bun.file(jsonPath).text());
3215
+ const maxSeq = Math.max(maxSeqOf(domain2), await headMaxSeq(platformDir, key));
3216
+ const next = String(maxSeq + 1).padStart(3, "0");
3217
+ return `${key}-${next}`;
3218
+ }
3219
+ var init_domains = __esm(() => {
3220
+ init_src();
3221
+ });
3222
+
3223
+ // src/check/grammar.ts
3224
+ import { join as join9 } from "path";
3225
+ function lineOf(rawText, id) {
3226
+ const lines = rawText.split(`
3227
+ `);
3228
+ const needle = `"id": "${id}"`;
3229
+ for (let i = 0;i < lines.length; i++) {
3230
+ if (lines[i].includes(needle))
3231
+ return i + 1;
3232
+ }
3233
+ return 0;
3234
+ }
3235
+ async function readDeclaredDomain(platformDir, key) {
3236
+ try {
3237
+ const raw = await Bun.file(join9(platformDir, "spec-engine", key, "SPEC.json")).text();
3238
+ const doc = JSON.parse(raw);
3239
+ if (doc.grammar !== "ears")
3240
+ return null;
3241
+ return {
3242
+ raw,
3243
+ severity: doc.grammarSeverity === "error" ? "error" : "warning",
3244
+ requirements: Array.isArray(doc.requirements) ? doc.requirements : []
3245
+ };
3246
+ } catch {
3247
+ return null;
3248
+ }
3249
+ }
3250
+ function rowsForDomain(key, domain2) {
3251
+ const rows = [];
3252
+ for (const req of domain2.requirements) {
3253
+ if (typeof req?.id !== "string" || typeof req.statement !== "string")
3254
+ continue;
3255
+ const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
3256
+ if (!CHECKED_STATUSES.has(status))
3257
+ continue;
3258
+ const result = parseEarsStatement(req.statement);
3259
+ if (result.ok)
3260
+ continue;
3261
+ rows.push({
3262
+ code: DiagnosticCode.STATEMENT_GRAMMAR,
3263
+ severity: domain2.severity,
3264
+ repo: null,
3265
+ source_file: `spec-engine/${key}/SPEC.json`,
3266
+ line: lineOf(domain2.raw, req.id),
3267
+ req_id: req.id,
3268
+ detail: `${result.problem}. Expected one of:
3269
+ ${result.expected}`
3270
+ });
3271
+ }
3272
+ return rows;
3273
+ }
3274
+ async function statementGrammarDiagnostics(platformDir) {
3275
+ const rows = [];
3276
+ for (const key of listDomainKeys(platformDir)) {
3277
+ if (key === "TERM")
3278
+ continue;
3279
+ const domain2 = await readDeclaredDomain(platformDir, key);
3280
+ if (domain2 !== null)
3281
+ rows.push(...rowsForDomain(key, domain2));
3282
+ }
3283
+ rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
3284
+ return rows;
3285
+ }
3286
+ var CHECKED_STATUSES;
3287
+ var init_grammar = __esm(() => {
3288
+ init_src();
3289
+ init_domains();
3290
+ CHECKED_STATUSES = new Set(["active", "draft"]);
3291
+ });
3292
+
2990
3293
  // src/check/proven.ts
2991
3294
  function normalizeToSegments(path) {
2992
3295
  const out = [];
@@ -3154,7 +3457,8 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
3154
3457
  for (const b of baseReqs) {
3155
3458
  if (changeIds.has(b.id))
3156
3459
  continue;
3157
- const exempt = b.supersededBy != null && changeIds.has(b.supersededBy) || changeReqs.some((y) => y.supersedes === b.id);
3460
+ const activeAtBase = (b.status ?? "").toLowerCase() === "active";
3461
+ const exempt = activeAtBase && (b.supersededBy != null && changeIds.has(b.supersededBy) || changeReqs.some((y) => y.supersedes === b.id));
3158
3462
  if (exempt)
3159
3463
  continue;
3160
3464
  out.push({
@@ -3163,7 +3467,7 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
3163
3467
  line: 0,
3164
3468
  repo: null,
3165
3469
  req_id: b.id,
3166
- detail: `${b.id} was present in the base ref but is absent from the change with no approved supersession`,
3470
+ 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
3471
  severity: "error"
3168
3472
  });
3169
3473
  }
@@ -3193,11 +3497,11 @@ function normalizeHandle(h) {
3193
3497
  return (h.startsWith("@") ? h.slice(1) : h).toLowerCase();
3194
3498
  }
3195
3499
  function isNewTerminalFlip(base, change) {
3196
- if (change !== "superseded" && change !== "retired")
3500
+ if (!TERMINAL_STATUSES.has(change))
3197
3501
  return false;
3198
3502
  if (base === undefined)
3199
3503
  return false;
3200
- if (base === "superseded" || base === "retired")
3504
+ if (TERMINAL_STATUSES.has(base))
3201
3505
  return false;
3202
3506
  return base !== change;
3203
3507
  }
@@ -3226,8 +3530,10 @@ function unapprovedStatusFlip(baseReqs, changeReqs, codeowners, approvedBy, relP
3226
3530
  }
3227
3531
  return out;
3228
3532
  }
3533
+ var TERMINAL_STATUSES;
3229
3534
  var init_statusflip = __esm(() => {
3230
3535
  init_src();
3536
+ TERMINAL_STATUSES = new Set(["superseded", "deprecated", "retired"]);
3231
3537
  });
3232
3538
 
3233
3539
  // src/check/unsourced.ts
@@ -3521,76 +3827,307 @@ var init_junit = __esm(() => {
3521
3827
  ]);
3522
3828
  });
3523
3829
 
3524
- // src/commands/check.ts
3525
- var exports_check = {};
3526
- __export(exports_check, {
3527
- checkCommand: () => checkCommand
3830
+ // src/authoring/edit.ts
3831
+ function localToday() {
3832
+ const d = new Date;
3833
+ return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
3834
+ }
3835
+
3836
+ // src/commands/glossary.ts
3837
+ var exports_glossary = {};
3838
+ __export(exports_glossary, {
3839
+ parseGlossary: () => parseGlossary,
3840
+ glossaryDriftDiagnostic: () => glossaryDriftDiagnostic,
3841
+ glossaryCommand: () => glossaryCommand,
3842
+ generateGlossary: () => generateGlossary
3528
3843
  });
3529
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync } from "fs";
3530
- import { dirname as dirname7, join as join7, resolve as resolve7 } from "path";
3844
+ import { existsSync as existsSync6, readFileSync } from "fs";
3845
+ import { join as join10, resolve as resolve7 } from "path";
3531
3846
  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);
3542
- } catch (err) {
3543
- const msg = err instanceof Error ? err.message : String(err);
3544
- return {
3545
- reqs: [],
3546
- diagnostics: [
3547
- {
3548
- code: DiagnosticCode.INVALID_DOMAIN_FILE,
3549
- source_file: platformRel,
3550
- line: 0,
3551
- repo: null,
3552
- req_id: null,
3553
- detail: `not valid JSON: ${msg}`,
3554
- severity: "error"
3555
- }
3556
- ]
3557
- };
3847
+ function parseGlossary(md) {
3848
+ const terms = [];
3849
+ let section = null;
3850
+ let buf = null;
3851
+ const flush = () => {
3852
+ if (buf === null)
3853
+ return;
3854
+ const m = buf.match(/^\*\*(.+?)\*\*/);
3855
+ if (m) {
3856
+ const remainder = buf.slice(m[0].length);
3857
+ const statement = remainder.replace(/^ \u2014 /, "").trim();
3858
+ terms.push({ term: m[1], statement, section });
3859
+ }
3860
+ buf = null;
3861
+ };
3862
+ for (const line of md.split(`
3863
+ `)) {
3864
+ if (line.startsWith("## ")) {
3865
+ flush();
3866
+ section = line.slice(3).trim();
3867
+ } else if (line.startsWith("- ")) {
3868
+ flush();
3869
+ buf = line.slice(2);
3870
+ } else if (buf !== null) {
3871
+ if (line.trim() === "")
3872
+ flush();
3873
+ else
3874
+ buf += ` ${line.trim()}`;
3875
+ }
3558
3876
  }
3559
- const validated = validateDomainFile(parsed, platformRel);
3560
- if (!validated.ok)
3561
- return { reqs: [], diagnostics: validated.diagnostics };
3562
- return { reqs: validated.data.requirements, diagnostics: [] };
3877
+ flush();
3878
+ return terms;
3563
3879
  }
3564
- async function ingestResults(storage2, platformDir, resultsArg, jsonMode, diagnostics2) {
3565
- if (!resultsArg) {
3566
- if (jsonMode) {
3567
- console.error("spec check: no --results supplied; proofs unconfirmed (PROOFS_UNCONFIRMED) \u2014 run with --results <junit.xml> to enforce trusted-red");
3568
- } else {
3569
- diagnostics2.push(proofsUnconfirmedWarning());
3880
+ function generateGlossary(terms) {
3881
+ const parts = ["# Glossary", "", GLOSSARY_INTRO];
3882
+ let section;
3883
+ for (const t of terms) {
3884
+ if (t.section !== section) {
3885
+ section = t.section;
3886
+ if (section !== null && section !== undefined) {
3887
+ parts.push("", `## ${section}`, "");
3888
+ }
3570
3889
  }
3571
- return { resultsParsed: undefined, verifyingTags: undefined };
3890
+ parts.push(`- **${t.term}** \u2014 ${t.statement}`);
3572
3891
  }
3573
- const resultsPath = resolve7(platformDir, resultsArg);
3574
- if (!isContainedPath(resultsPath, platformDir)) {
3575
- console.error(`spec check: --results path must be inside platformDir (resolved to ${resultsPath})`);
3576
- try {
3577
- storage2.close();
3578
- } catch {}
3579
- process.exit(EXIT.USAGE);
3892
+ return `${parts.join(`
3893
+ `)}
3894
+ `;
3895
+ }
3896
+ function resolvePlatform(platformDirRaw) {
3897
+ const platformDir = resolve7(platformDirRaw ?? process.cwd());
3898
+ try {
3899
+ assertSpecPlatform(platformDir);
3900
+ } catch (e) {
3901
+ handleNotAPlatform(e);
3580
3902
  }
3581
- const xml = await Bun.file(resultsPath).text();
3582
- const resultsParsed = parseJUnit(xml);
3583
- const verifyingTags = storage2.listTags().filter((t) => t.kind === "verifies");
3584
- const active = storage2.listRequirements({ status: "Active" });
3585
- diagnostics2.push(...provenDetermination(active, verifyingTags, resultsParsed));
3586
- return { resultsParsed, verifyingTags };
3903
+ return platformDir;
3904
+ }
3905
+ function readStoreTerms(platformDir) {
3906
+ const specPath = join10(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
3907
+ if (!existsSync6(specPath))
3908
+ return [];
3909
+ const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
3910
+ const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
3911
+ 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) => ({
3912
+ term: String(r.term ?? ""),
3913
+ statement: String(r.statement ?? ""),
3914
+ section: r.section ?? null
3915
+ }));
3916
+ }
3917
+ function glossaryPath(platformDir) {
3918
+ return join10(platformDir, "GLOSSARY.md");
3919
+ }
3920
+ async function migrateGlossary(platformDir, json) {
3921
+ const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
3922
+ const specPath = join10(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
3923
+ if (!existsSync6(specPath)) {
3924
+ console.error(`spec glossary: no TERM domain (expected ${relFile} under ${platformDir})`);
3925
+ process.exit(EXIT.USAGE);
3926
+ }
3927
+ const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
3928
+ const existing = Array.isArray(domain2.requirements) ? domain2.requirements : [];
3929
+ if (existing.length > 0) {
3930
+ console.error(`spec glossary: TERM domain already holds ${existing.length} entries \u2014 migration skipped`);
3931
+ if (json)
3932
+ console.log(JSON.stringify({ migrated: 0, skipped: true }));
3933
+ else
3934
+ console.log("migration skipped (TERM domain not empty)");
3935
+ return;
3936
+ }
3937
+ const gPath = glossaryPath(platformDir);
3938
+ if (!existsSync6(gPath)) {
3939
+ console.error(`spec glossary: no GLOSSARY.md to migrate (expected ${gPath})`);
3940
+ process.exit(EXIT.USAGE);
3941
+ }
3942
+ const parsed = parseGlossary(readFileSync(gPath, "utf8"));
3943
+ domain2.requirements = parsed.map((t, i) => ({
3944
+ id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
3945
+ status: "active",
3946
+ statement: t.statement,
3947
+ term: t.term,
3948
+ why: null,
3949
+ supersedes: null,
3950
+ supersededBy: null,
3951
+ relates: [],
3952
+ livesIn: [],
3953
+ issues: [],
3954
+ aliases: [],
3955
+ cites: [],
3956
+ changedAtVersion: 1,
3957
+ section: t.section
3958
+ }));
3959
+ domain2.updated = localToday();
3960
+ const res = await validateAndWrite(specPath, domain2, relFile);
3961
+ if (!res.ok) {
3962
+ for (const diag of res.diagnostics)
3963
+ console.error(`spec glossary: ${diag.detail}`);
3964
+ process.exit(EXIT.USAGE);
3965
+ }
3966
+ const count = parsed.length;
3967
+ if (json)
3968
+ console.log(JSON.stringify({ migrated: count, file: relFile }));
3969
+ else
3970
+ console.log(`migrated ${count} terms into ${relFile}`);
3971
+ }
3972
+ async function writeGlossary(platformDir, json) {
3973
+ const terms = readStoreTerms(platformDir);
3974
+ await Bun.write(glossaryPath(platformDir), generateGlossary(terms));
3975
+ if (json)
3976
+ console.log(JSON.stringify({ generated: terms.length, file: "GLOSSARY.md" }));
3977
+ else
3978
+ console.log(`generated GLOSSARY.md from ${terms.length} terms`);
3979
+ }
3980
+ function glossaryDriftDiagnostic(platformDir) {
3981
+ const terms = readStoreTerms(platformDir);
3982
+ if (terms.length === 0)
3983
+ return null;
3984
+ const gPath = glossaryPath(platformDir);
3985
+ if (!existsSync6(gPath))
3986
+ return null;
3987
+ const generated = generateGlossary(terms);
3988
+ const committed = readFileSync(gPath, "utf8");
3989
+ if (generated === committed)
3990
+ return null;
3991
+ return {
3992
+ code: DiagnosticCode.GLOSSARY_DRIFT,
3993
+ severity: "warning",
3994
+ repo: null,
3995
+ source_file: "GLOSSARY.md",
3996
+ line: 0,
3997
+ req_id: null,
3998
+ detail: "GLOSSARY.md does not match the TERM store \u2014 run `spec glossary .` to regenerate the human view"
3999
+ };
4000
+ }
4001
+ function checkGlossary(platformDir, json) {
4002
+ const generated = generateGlossary(readStoreTerms(platformDir));
4003
+ const gPath = glossaryPath(platformDir);
4004
+ const committed = existsSync6(gPath) ? readFileSync(gPath, "utf8") : "";
4005
+ if (generated === committed) {
4006
+ if (json)
4007
+ console.log(JSON.stringify({ ok: true }));
4008
+ else
4009
+ console.error("glossary round-trip: OK (committed GLOSSARY.md == generated)");
4010
+ return;
4011
+ }
4012
+ console.error("glossary drift: committed GLOSSARY.md != generated from the TERM store");
4013
+ console.error("run `spec glossary .` to regenerate the human view from the store");
4014
+ if (json)
4015
+ console.log(JSON.stringify({ ok: false }));
4016
+ process.exit(EXIT.FAILURE);
4017
+ }
4018
+ var TERM_KEY = "TERM", GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
4019
+ docs need one of these ideas, use the term below \u2014 not a synonym. Terms are ordered
4020
+ by the data model, outside-in.`, glossaryCommand;
4021
+ var init_glossary = __esm(() => {
4022
+ init_src();
4023
+ init_constants();
4024
+ init_discover();
4025
+ init__shared();
4026
+ glossaryCommand = defineCommand3({
4027
+ meta: {
4028
+ name: "glossary",
4029
+ description: "Generate GLOSSARY.md from the TERM store (byte-stable); --migrate parses GLOSSARY.md into TERM-001..N; --check fails on drift (committed != generated)"
4030
+ },
4031
+ args: {
4032
+ platformDir: {
4033
+ type: "positional",
4034
+ required: false,
4035
+ description: "Platform directory containing spec-engine/ (default: cwd)"
4036
+ },
4037
+ migrate: {
4038
+ type: "boolean",
4039
+ description: "One-time: parse the committed GLOSSARY.md into TERM-001..N (idempotent-skip)"
4040
+ },
4041
+ check: {
4042
+ type: "boolean",
4043
+ description: "Fail (exit 1) if the committed GLOSSARY.md differs from the generated output"
4044
+ },
4045
+ json: { type: "boolean", description: "Emit a JSON result instead of the text summary" }
4046
+ },
4047
+ async run({ args }) {
4048
+ const platformDir = resolvePlatform(args.platformDir);
4049
+ const json = Boolean(args.json);
4050
+ if (args.migrate) {
4051
+ await migrateGlossary(platformDir, json);
4052
+ } else if (args.check) {
4053
+ checkGlossary(platformDir, json);
4054
+ } else {
4055
+ await writeGlossary(platformDir, json);
4056
+ }
4057
+ }
4058
+ });
4059
+ });
4060
+
4061
+ // src/commands/check.ts
4062
+ var exports_check = {};
4063
+ __export(exports_check, {
4064
+ checkCommand: () => checkCommand
4065
+ });
4066
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
4067
+ import { dirname as dirname7, join as join11, resolve as resolve8 } from "path";
4068
+ import { defineCommand as defineCommand4 } from "citty";
4069
+ function readCodeownersText(platformDir) {
4070
+ const path = join11(platformDir, ".github", "CODEOWNERS");
4071
+ if (!existsSync7(path))
4072
+ return null;
4073
+ return readFileSync2(path, "utf-8");
4074
+ }
4075
+ function domainReqsFromText(text, platformRel) {
4076
+ let parsed;
4077
+ try {
4078
+ parsed = JSON.parse(text);
4079
+ } catch (err) {
4080
+ const msg = err instanceof Error ? err.message : String(err);
4081
+ return {
4082
+ reqs: [],
4083
+ diagnostics: [
4084
+ {
4085
+ code: DiagnosticCode.INVALID_DOMAIN_FILE,
4086
+ source_file: platformRel,
4087
+ line: 0,
4088
+ repo: null,
4089
+ req_id: null,
4090
+ detail: `not valid JSON: ${msg}`,
4091
+ severity: "error"
4092
+ }
4093
+ ]
4094
+ };
4095
+ }
4096
+ const validated = validateDomainFile(parsed, platformRel);
4097
+ if (!validated.ok)
4098
+ return { reqs: [], diagnostics: validated.diagnostics };
4099
+ return { reqs: validated.data.requirements, diagnostics: [] };
4100
+ }
4101
+ async function ingestResults(storage2, platformDir, resultsArg, jsonMode, diagnostics2) {
4102
+ if (!resultsArg) {
4103
+ if (jsonMode) {
4104
+ console.error("spec check: no --results supplied; proofs unconfirmed (PROOFS_UNCONFIRMED) \u2014 run with --results <junit.xml> to enforce trusted-red");
4105
+ } else {
4106
+ diagnostics2.push(proofsUnconfirmedWarning());
4107
+ }
4108
+ return { resultsParsed: undefined, verifyingTags: undefined };
4109
+ }
4110
+ const resultsPath = resolve8(platformDir, resultsArg);
4111
+ if (!isContainedPath(resultsPath, platformDir)) {
4112
+ console.error(`spec check: --results path must be inside platformDir (resolved to ${resultsPath})`);
4113
+ try {
4114
+ storage2.close();
4115
+ } catch {}
4116
+ process.exit(EXIT.USAGE);
4117
+ }
4118
+ const xml = await Bun.file(resultsPath).text();
4119
+ const resultsParsed = parseJUnit(xml);
4120
+ const verifyingTags = storage2.listTags().filter((t) => t.kind === "verifies");
4121
+ const active = storage2.listRequirements({ status: "Active" });
4122
+ diagnostics2.push(...provenDetermination(active, verifyingTags, resultsParsed));
4123
+ return { resultsParsed, verifyingTags };
3587
4124
  }
3588
4125
  async function collectChangeReqs(canonicalDir) {
3589
4126
  const reqs = [];
3590
4127
  const relPathById = new Map;
3591
4128
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
3592
4129
  const platformRel = `spec-engine/${rel}`;
3593
- const text = await Bun.file(join7(canonicalDir, rel)).text();
4130
+ const text = await Bun.file(join11(canonicalDir, rel)).text();
3594
4131
  const parsed = domainReqsFromText(text, platformRel);
3595
4132
  for (const r of parsed.reqs) {
3596
4133
  reqs.push(r);
@@ -3630,7 +4167,7 @@ async function runGovernanceGate(storage2, platformDir, args, diagnostics2, resu
3630
4167
  } catch {}
3631
4168
  process.exit(EXIT.USAGE);
3632
4169
  }
3633
- const canonicalDir = join7(platformDir, "spec-engine");
4170
+ const canonicalDir = join11(platformDir, "spec-engine");
3634
4171
  const change = await collectChangeReqs(canonicalDir);
3635
4172
  const base = collectBaseReqs(platformDir, ref, diagnostics2);
3636
4173
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
@@ -3656,6 +4193,7 @@ function exitAfterCrash(e, storage2) {
3656
4193
  var checkCommand;
3657
4194
  var init_check = __esm(() => {
3658
4195
  init_src();
4196
+ init_grammar();
3659
4197
  init_propagation_teeth();
3660
4198
  init_proven();
3661
4199
  init_removed();
@@ -3669,7 +4207,8 @@ var init_check = __esm(() => {
3669
4207
  init_fs();
3670
4208
  init_sqlite();
3671
4209
  init__shared();
3672
- checkCommand = defineCommand3({
4210
+ init_glossary();
4211
+ checkCommand = defineCommand4({
3673
4212
  meta: {
3674
4213
  name: "check",
3675
4214
  description: "Cross-repo integrity check. --ci rebuilds the index cold (rm db+wal+shm). Exits 1 on any error-severity diagnostic."
@@ -3718,7 +4257,7 @@ var init_check = __esm(() => {
3718
4257
  }
3719
4258
  },
3720
4259
  async run({ args }) {
3721
- const platformDir = resolve7(args.platformDir ?? process.cwd());
4260
+ const platformDir = resolve8(args.platformDir ?? process.cwd());
3722
4261
  const outArg = args.out;
3723
4262
  const dbPath = resolveDbPath(platformDir, outArg);
3724
4263
  if (outArg)
@@ -3747,12 +4286,22 @@ var init_check = __esm(() => {
3747
4286
  const provenance = storage2.listProvenance();
3748
4287
  diagnostics2.push(...unsourcedChanges(superseded, provenance));
3749
4288
  }
4289
+ diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
4290
+ const glossaryDrift = glossaryDriftDiagnostic(platformDir);
4291
+ if (glossaryDrift !== null)
4292
+ diagnostics2.push(glossaryDrift);
3750
4293
  const results = await ingestResults(storage2, platformDir, args.results, !!args.json, diagnostics2);
3751
4294
  await runGovernanceGate(storage2, platformDir, {
3752
4295
  base: args.base,
3753
4296
  approvedBy: args.approvedBy,
3754
4297
  requireOwnerApproval: args.requireOwnerApproval
3755
4298
  }, diagnostics2, results);
4299
+ if (!args.base && gitRefResolves(platformDir, "HEAD")) {
4300
+ const change = await collectChangeReqs(join11(platformDir, "spec-engine"));
4301
+ const baseParseSink = [];
4302
+ const base = collectBaseReqs(platformDir, "HEAD", baseParseSink);
4303
+ diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
4304
+ }
3756
4305
  const output = renderDiagnostics(diagnostics2, args.json ? "json" : "text");
3757
4306
  console.log(output);
3758
4307
  if (!args.json) {
@@ -3768,63 +4317,6 @@ var init_check = __esm(() => {
3768
4317
  });
3769
4318
  });
3770
4319
 
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
4320
  // src/commands/domain.ts
3829
4321
  var exports_domain = {};
3830
4322
  __export(exports_domain, {
@@ -3832,9 +4324,9 @@ __export(exports_domain, {
3832
4324
  domainListCommand: () => domainListCommand,
3833
4325
  domainCommand: () => domainCommand
3834
4326
  });
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";
4327
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
4328
+ import { dirname as dirname8, join as join12, resolve as resolve9 } from "path";
4329
+ import { defineCommand as defineCommand5 } from "citty";
3838
4330
  var domainNewCommand, domainListCommand, domainCommand;
3839
4331
  var init_domain2 = __esm(() => {
3840
4332
  init_src();
@@ -3842,7 +4334,7 @@ var init_domain2 = __esm(() => {
3842
4334
  init_constants();
3843
4335
  init_discover();
3844
4336
  init__shared();
3845
- domainNewCommand = defineCommand4({
4337
+ domainNewCommand = defineCommand5({
3846
4338
  meta: {
3847
4339
  name: "new",
3848
4340
  description: "Scaffold a fresh spec-engine/<KEY>/SPEC.json (input normalized)"
@@ -3861,7 +4353,7 @@ var init_domain2 = __esm(() => {
3861
4353
  },
3862
4354
  async run({ args }) {
3863
4355
  const raw = args.name;
3864
- const platformDir = resolve8(args.platformDir ?? process.cwd());
4356
+ const platformDir = resolve9(args.platformDir ?? process.cwd());
3865
4357
  const key = normalizeDomainKey(raw);
3866
4358
  if (key !== raw) {
3867
4359
  console.log(`spec domain: normalized "${raw}" \u2192 ${key}`);
@@ -3871,21 +4363,21 @@ var init_domain2 = __esm(() => {
3871
4363
  process.exit(EXIT.USAGE);
3872
4364
  return;
3873
4365
  }
3874
- const dest = join9(platformDir, "spec-engine", key, "SPEC.json");
3875
- const resolvedDest = resolve8(dest);
3876
- const resolvedRoot = resolve8(platformDir);
4366
+ const dest = join12(platformDir, "spec-engine", key, "SPEC.json");
4367
+ const resolvedDest = resolve9(dest);
4368
+ const resolvedRoot = resolve9(platformDir);
3877
4369
  if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
3878
4370
  console.error(`spec domain new: refusing to write outside platformDir (${resolvedDest})`);
3879
4371
  process.exit(EXIT.USAGE);
3880
4372
  return;
3881
4373
  }
3882
- if (existsSync7(dest)) {
4374
+ if (existsSync8(dest)) {
3883
4375
  console.error(`refusing to overwrite ${dest}`);
3884
4376
  process.exit(EXIT.USAGE);
3885
4377
  return;
3886
4378
  }
3887
- const mdSibling = join9(dirname8(dest), "SPEC.md");
3888
- if (existsSync7(mdSibling)) {
4379
+ const mdSibling = join12(dirname8(dest), "SPEC.md");
4380
+ if (existsSync8(mdSibling)) {
3889
4381
  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
4382
  process.exit(EXIT.USAGE);
3891
4383
  return;
@@ -3905,7 +4397,7 @@ var init_domain2 = __esm(() => {
3905
4397
  console.log(`created ${relFile}`);
3906
4398
  }
3907
4399
  });
3908
- domainListCommand = defineCommand4({
4400
+ domainListCommand = defineCommand5({
3909
4401
  meta: {
3910
4402
  name: "list",
3911
4403
  description: "List domain keys (sorted, read from the filesystem)"
@@ -3922,7 +4414,7 @@ var init_domain2 = __esm(() => {
3922
4414
  }
3923
4415
  },
3924
4416
  async run({ args }) {
3925
- const platformDir = resolve8(args.platformDir ?? process.cwd());
4417
+ const platformDir = resolve9(args.platformDir ?? process.cwd());
3926
4418
  try {
3927
4419
  assertSpecPlatform(platformDir);
3928
4420
  } catch (e) {
@@ -3937,7 +4429,7 @@ var init_domain2 = __esm(() => {
3937
4429
  }
3938
4430
  }
3939
4431
  });
3940
- domainCommand = defineCommand4({
4432
+ domainCommand = defineCommand5({
3941
4433
  meta: {
3942
4434
  name: "domain",
3943
4435
  description: "Manage spec domains (spec-engine/<KEY>/)"
@@ -4016,15 +4508,15 @@ var exports_map = {};
4016
4508
  __export(exports_map, {
4017
4509
  mapCommand: () => mapCommand
4018
4510
  });
4019
- import { resolve as resolve9 } from "path";
4020
- import { defineCommand as defineCommand5 } from "citty";
4511
+ import { resolve as resolve10 } from "path";
4512
+ import { defineCommand as defineCommand6 } from "citty";
4021
4513
  var mapCommand;
4022
4514
  var init_map = __esm(() => {
4023
4515
  init_constants();
4024
4516
  init_discover();
4025
4517
  init_prompt();
4026
4518
  init__shared();
4027
- mapCommand = defineCommand5({
4519
+ mapCommand = defineCommand6({
4028
4520
  meta: {
4029
4521
  name: "map",
4030
4522
  description: "Render the cross-repo coverage matrix from the coverage VIEW. --json emits a deterministically sorted JSON array."
@@ -4053,7 +4545,7 @@ var init_map = __esm(() => {
4053
4545
  }
4054
4546
  },
4055
4547
  async run({ args }) {
4056
- const platformDir = resolve9(args.platformDir ?? process.cwd());
4548
+ const platformDir = resolve10(args.platformDir ?? process.cwd());
4057
4549
  const outArg = args.out;
4058
4550
  const dbPath = resolveDbPath(platformDir, outArg);
4059
4551
  if (outArg)
@@ -4103,15 +4595,15 @@ var exports_propagation = {};
4103
4595
  __export(exports_propagation, {
4104
4596
  propagationCommand: () => propagationCommand
4105
4597
  });
4106
- import { resolve as resolve10 } from "path";
4107
- import { defineCommand as defineCommand6 } from "citty";
4598
+ import { resolve as resolve11 } from "path";
4599
+ import { defineCommand as defineCommand7 } from "citty";
4108
4600
  var propagationCommand;
4109
4601
  var init_propagation2 = __esm(() => {
4110
4602
  init_constants();
4111
4603
  init_discover();
4112
4604
  init_prompt();
4113
4605
  init__shared();
4114
- propagationCommand = defineCommand6({
4606
+ propagationCommand = defineCommand7({
4115
4607
  meta: {
4116
4608
  name: "propagation",
4117
4609
  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 +4643,7 @@ var init_propagation2 = __esm(() => {
4151
4643
  process.exit(EXIT.USAGE);
4152
4644
  return;
4153
4645
  }
4154
- const platformDir = resolve10(args.platformDir ?? process.cwd());
4646
+ const platformDir = resolve11(args.platformDir ?? process.cwd());
4155
4647
  const outArg = args.out;
4156
4648
  const dbPath = resolveDbPath(platformDir, outArg);
4157
4649
  if (outArg)
@@ -4225,8 +4717,8 @@ var exports_query = {};
4225
4717
  __export(exports_query, {
4226
4718
  queryCommand: () => queryCommand
4227
4719
  });
4228
- import { resolve as resolve11 } from "path";
4229
- import { defineCommand as defineCommand7 } from "citty";
4720
+ import { resolve as resolve12 } from "path";
4721
+ import { defineCommand as defineCommand8 } from "citty";
4230
4722
  var queryCommand;
4231
4723
  var init_query = __esm(() => {
4232
4724
  init_src();
@@ -4234,7 +4726,7 @@ var init_query = __esm(() => {
4234
4726
  init_discover();
4235
4727
  init_prompt();
4236
4728
  init__shared();
4237
- queryCommand = defineCommand7({
4729
+ queryCommand = defineCommand8({
4238
4730
  meta: {
4239
4731
  name: "query",
4240
4732
  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 +4783,7 @@ var init_query = __esm(() => {
4291
4783
  process.exit(EXIT.USAGE);
4292
4784
  return;
4293
4785
  }
4294
- const platformDir = resolve11(args.platformDir ?? process.cwd());
4786
+ const platformDir = resolve12(args.platformDir ?? process.cwd());
4295
4787
  const outArg = args.out;
4296
4788
  const dbPath = resolveDbPath(platformDir, outArg);
4297
4789
  if (outArg)
@@ -4380,8 +4872,8 @@ var exports_relations = {};
4380
4872
  __export(exports_relations, {
4381
4873
  relationsCommand: () => relationsCommand
4382
4874
  });
4383
- import { resolve as resolve12 } from "path";
4384
- import { defineCommand as defineCommand8 } from "citty";
4875
+ import { resolve as resolve13 } from "path";
4876
+ import { defineCommand as defineCommand9 } from "citty";
4385
4877
  function formatNoRelations(platformDir) {
4386
4878
  return [
4387
4879
  `No Relates links indexed under ${platformDir}.`,
@@ -4394,7 +4886,7 @@ var init_relations = __esm(() => {
4394
4886
  init_constants();
4395
4887
  init_prompt();
4396
4888
  init__shared();
4397
- relationsCommand = defineCommand8({
4889
+ relationsCommand = defineCommand9({
4398
4890
  meta: {
4399
4891
  name: "relations",
4400
4892
  description: "Render the Relates links between requirements as a mermaid entity diagram. --json emits the sorted relation rows."
@@ -4423,7 +4915,7 @@ var init_relations = __esm(() => {
4423
4915
  }
4424
4916
  },
4425
4917
  async run({ args }) {
4426
- const platformDir = resolve12(args.platformDir ?? process.cwd());
4918
+ const platformDir = resolve13(args.platformDir ?? process.cwd());
4427
4919
  const outArg = args.out;
4428
4920
  const dbPath = resolveDbPath(platformDir, outArg);
4429
4921
  if (outArg)
@@ -4510,7 +5002,8 @@ function renderProvenance(rows, mode) {
4510
5002
  function decorateRow(row, resolved) {
4511
5003
  const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
4512
5004
  if (resolved?.ok && resolved.title && resolved.url) {
4513
- return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}`;
5005
+ const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
5006
+ return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
4514
5007
  }
4515
5008
  return `${prefix} (${TOKEN_HINT})`;
4516
5009
  }
@@ -4519,7 +5012,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
4519
5012
  if (mode === "json") {
4520
5013
  return JSON.stringify(sorted.map((r) => {
4521
5014
  const hit = resolved.get(r.issue_id);
4522
- const resolvedField = hit?.ok === true ? { title: hit.title ?? "", status: hit.status ?? "", url: hit.url ?? "" } : null;
5015
+ const resolvedField = hit?.ok === true ? {
5016
+ title: hit.title ?? "",
5017
+ status: hit.status ?? "",
5018
+ url: hit.url ?? "",
5019
+ stale: hit.stale === true
5020
+ } : null;
4523
5021
  return { ...r, resolved: resolvedField };
4524
5022
  }));
4525
5023
  }
@@ -4553,7 +5051,7 @@ var init_adapter = () => {};
4553
5051
 
4554
5052
  // ../tracker/src/cache.ts
4555
5053
  import { mkdirSync as mkdirSync6 } from "fs";
4556
- import { dirname as dirname9, join as join10 } from "path";
5054
+ import { dirname as dirname9, join as join13 } from "path";
4557
5055
  function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
4558
5056
  const t = Date.parse(entry.fetched_at);
4559
5057
  if (Number.isNaN(t))
@@ -4567,7 +5065,7 @@ function isValidEntry(e) {
4567
5065
  return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
4568
5066
  }
4569
5067
  function sidecarPath(platformDir) {
4570
- return join10(platformDir, ".spec-engine", SIDECAR_FILE);
5068
+ return join13(platformDir, ".spec-engine", SIDECAR_FILE);
4571
5069
  }
4572
5070
  async function readCache(platformDir) {
4573
5071
  try {
@@ -4735,7 +5233,13 @@ async function resolveAndCache(rows, platformDir, adapter = makeLinearAdapter(),
4735
5233
  const out = new Map;
4736
5234
  for (const id of uniqueIds) {
4737
5235
  const entry = next[id];
4738
- out.set(id, entry ? { ok: true, title: entry.title, status: entry.status, url: entry.url } : { ok: false });
5236
+ out.set(id, entry ? {
5237
+ ok: true,
5238
+ title: entry.title,
5239
+ status: entry.status,
5240
+ url: entry.url,
5241
+ stale: !isFresh(entry, now)
5242
+ } : { ok: false });
4739
5243
  }
4740
5244
  return out;
4741
5245
  }
@@ -4748,9 +5252,9 @@ var exports_provenance = {};
4748
5252
  __export(exports_provenance, {
4749
5253
  provenanceCommand: () => provenanceCommand
4750
5254
  });
4751
- import { statSync as statSync4 } from "fs";
4752
- import { resolve as resolve13 } from "path";
4753
- import { defineCommand as defineCommand9 } from "citty";
5255
+ import { statSync as statSync5 } from "fs";
5256
+ import { resolve as resolve14 } from "path";
5257
+ import { defineCommand as defineCommand10 } from "citty";
4754
5258
  function formatNoProvenance(platformDir) {
4755
5259
  return [
4756
5260
  `No provenance links indexed under ${platformDir}.`,
@@ -4767,7 +5271,7 @@ var init_provenance = __esm(() => {
4767
5271
  init_prompt();
4768
5272
  init_resolve();
4769
5273
  init__shared();
4770
- provenanceCommand = defineCommand9({
5274
+ provenanceCommand = defineCommand10({
4771
5275
  meta: {
4772
5276
  name: "provenance",
4773
5277
  description: "Render the per-requirement provenance matrix (creating/revising issues + backing tests + git pointer). --json emits the deterministically-sorted matrix rows."
@@ -4810,7 +5314,7 @@ var init_provenance = __esm(() => {
4810
5314
  if (issueId && platformArg === undefined) {
4811
5315
  let isDir = false;
4812
5316
  try {
4813
- isDir = statSync4(resolve13(issueId)).isDirectory();
5317
+ isDir = statSync5(resolve14(issueId)).isDirectory();
4814
5318
  } catch {
4815
5319
  isDir = false;
4816
5320
  }
@@ -4819,7 +5323,7 @@ var init_provenance = __esm(() => {
4819
5323
  issueId = "";
4820
5324
  }
4821
5325
  }
4822
- const platformDir = resolve13(platformArg ?? process.cwd());
5326
+ const platformDir = resolve14(platformArg ?? process.cwd());
4823
5327
  const outArg = args.out;
4824
5328
  const dbPath = resolveDbPath(platformDir, outArg);
4825
5329
  if (outArg)
@@ -4844,15 +5348,9 @@ var init_provenance = __esm(() => {
4844
5348
  });
4845
5349
  });
4846
5350
 
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
5351
  // src/authoring/filerefs.ts
4854
- import { existsSync as existsSync8 } from "fs";
4855
- import { resolve as resolve14 } from "path";
5352
+ import { existsSync as existsSync9 } from "fs";
5353
+ import { resolve as resolve15 } from "path";
4856
5354
  function extractRefsFromText(s) {
4857
5355
  const out = [];
4858
5356
  for (const m of s.matchAll(FILE_REF_RE)) {
@@ -4863,17 +5361,55 @@ function extractRefsFromText(s) {
4863
5361
  return out;
4864
5362
  }
4865
5363
  function resolveFileRef(platformDir, ref) {
4866
- const root = resolve14(platformDir);
4867
- const abs = resolve14(root, ref);
5364
+ const root = resolve15(platformDir);
5365
+ const abs = resolve15(root, ref);
4868
5366
  if (!(abs === root || abs.startsWith(`${root}/`)))
4869
5367
  return false;
4870
- return existsSync8(abs);
5368
+ return existsSync9(abs);
4871
5369
  }
4872
5370
  var FILE_REF_RE;
4873
5371
  var init_filerefs = __esm(() => {
4874
5372
  FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
4875
5373
  });
4876
5374
 
5375
+ // src/authoring/grammar.ts
5376
+ import { join as join14 } from "path";
5377
+ async function grammarConfig(platformDir, key) {
5378
+ try {
5379
+ const doc = JSON.parse(await Bun.file(join14(platformDir, "spec-engine", key, "SPEC.json")).text());
5380
+ return {
5381
+ grammar: doc.grammar === "ears" ? "ears" : "freeform",
5382
+ severity: doc.grammarSeverity === "error" ? "error" : "warning"
5383
+ };
5384
+ } catch {
5385
+ return { grammar: "freeform", severity: "warning" };
5386
+ }
5387
+ }
5388
+ async function enforceStatementGrammar(platformDir, key, statement, cmdName) {
5389
+ if (key === "TERM")
5390
+ return;
5391
+ const cfg = await grammarConfig(platformDir, key);
5392
+ if (cfg.grammar !== "ears")
5393
+ return;
5394
+ const result = parseEarsStatement(statement);
5395
+ if (result.ok)
5396
+ return;
5397
+ const message = `${cmdName}: the statement does not match the domain's EARS shape \u2014 ${result.problem}.
5398
+ ` + `Expected one of:
5399
+ ${result.expected}`;
5400
+ if (cfg.severity === "error") {
5401
+ console.error(`${message}
5402
+ (${key} sets grammarSeverity: "error", so the write is refused)`);
5403
+ process.exit(EXIT.USAGE);
5404
+ }
5405
+ console.error(`${message}
5406
+ (warning \u2014 written anyway; ${key} sets grammar: "ears")`);
5407
+ }
5408
+ var init_grammar2 = __esm(() => {
5409
+ init_src();
5410
+ init_constants();
5411
+ });
5412
+
4877
5413
  // src/commands/req.ts
4878
5414
  var exports_req = {};
4879
5415
  __export(exports_req, {
@@ -4882,10 +5418,10 @@ __export(exports_req, {
4882
5418
  askLine: () => askLine,
4883
5419
  appendEntry: () => appendEntry
4884
5420
  });
4885
- import { existsSync as existsSync9 } from "fs";
4886
- import { join as join11, resolve as resolve15 } from "path";
5421
+ import { existsSync as existsSync10 } from "fs";
5422
+ import { join as join15, resolve as resolve16 } from "path";
4887
5423
  import { createInterface as createInterface2 } from "readline";
4888
- import { defineCommand as defineCommand10 } from "citty";
5424
+ import { defineCommand as defineCommand11 } from "citty";
4889
5425
  function resolveDomainKey(input, platformDir) {
4890
5426
  const prefix = normalizeDomainKey(input);
4891
5427
  const keys = listDomainKeys(platformDir);
@@ -4919,10 +5455,12 @@ async function authorFromFieldFlags(platformDir, key, nextId, textFlag, opts) {
4919
5455
  requirement,
4920
5456
  why,
4921
5457
  binds,
4922
- lives
5458
+ lives,
5459
+ issue: (opts.issue ?? "").trim() || undefined
4923
5460
  });
4924
5461
  if (opts.json) {
4925
- console.log(JSON.stringify({ id: nextId, file: relFile }));
5462
+ const parsed = parseEarsStatement(requirement);
5463
+ console.log(JSON.stringify(parsed.ok ? { id: nextId, file: relFile, clauses: parsed.clauses } : { id: nextId, file: relFile }));
4926
5464
  } else {
4927
5465
  console.log(`appended ${nextId} to ${relFile}`);
4928
5466
  }
@@ -4946,8 +5484,9 @@ function warnUnresolvableRefs(platformDir, fieldValues) {
4946
5484
  }
4947
5485
  async function appendEntry(platformDir, key, id, fields) {
4948
5486
  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;
5487
+ const specPath = join15(platformDir, "spec-engine", key, "SPEC.json");
5488
+ await enforceStatementGrammar(platformDir, key, fields.requirement, "spec req");
5489
+ const domain2 = existsSync10(specPath) ? JSON.parse(await Bun.file(specPath).text()) : null;
4951
5490
  if (domain2 === null) {
4952
5491
  console.error(`spec req: no domain ${key} (expected ${relFile} under ${platformDir})`);
4953
5492
  process.exit(EXIT.USAGE);
@@ -4962,7 +5501,7 @@ async function appendEntry(platformDir, key, id, fields) {
4962
5501
  supersededBy: null,
4963
5502
  relates: [],
4964
5503
  livesIn: fields.lives ? [fields.lives] : [],
4965
- issues: []
5504
+ issues: fields.issue ? [{ role: "created", id: fields.issue }] : []
4966
5505
  });
4967
5506
  domain2.requirements = requirements;
4968
5507
  domain2.updated = localToday();
@@ -4988,10 +5527,11 @@ var init_req = __esm(() => {
4988
5527
  init_src();
4989
5528
  init_domains();
4990
5529
  init_filerefs();
5530
+ init_grammar2();
4991
5531
  init_constants();
4992
5532
  init_discover();
4993
5533
  init__shared();
4994
- reqCommand = defineCommand10({
5534
+ reqCommand = defineCommand11({
4995
5535
  meta: {
4996
5536
  name: "req",
4997
5537
  description: "Author a new requirement interactively (TTY) or print the next unused id (non-TTY)"
@@ -5026,11 +5566,15 @@ var init_req = __esm(() => {
5026
5566
  lives: {
5027
5567
  type: "string",
5028
5568
  description: "Lives in field (only with --text)"
5569
+ },
5570
+ issue: {
5571
+ type: "string",
5572
+ description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
5029
5573
  }
5030
5574
  },
5031
5575
  async run({ args }) {
5032
5576
  const input = args.domainPrefix;
5033
- const platformDir = resolve15(args.platformDir ?? process.cwd());
5577
+ const platformDir = resolve16(args.platformDir ?? process.cwd());
5034
5578
  try {
5035
5579
  assertSpecPlatform(platformDir);
5036
5580
  } catch (e) {
@@ -5039,9 +5583,9 @@ var init_req = __esm(() => {
5039
5583
  const key = resolveDomainKey(input, platformDir);
5040
5584
  const nextId = await nextRequirementId(platformDir, key);
5041
5585
  const textFlag = args.text;
5042
- const hasFieldFlags = typeof args.why === "string" || typeof args.binds === "string" || typeof args.lives === "string";
5586
+ const hasFieldFlags = typeof args.why === "string" || typeof args.binds === "string" || typeof args.lives === "string" || typeof args.issue === "string";
5043
5587
  if (textFlag === undefined && hasFieldFlags) {
5044
- console.error("spec req: --why/--binds/--lives require --text (the Requirement field is mandatory)");
5588
+ console.error("spec req: --why/--binds/--lives/--issue require --text (the Requirement field is mandatory)");
5045
5589
  process.exit(EXIT.USAGE);
5046
5590
  return;
5047
5591
  }
@@ -5050,6 +5594,7 @@ var init_req = __esm(() => {
5050
5594
  why: args.why,
5051
5595
  binds: args.binds,
5052
5596
  lives: args.lives,
5597
+ issue: args.issue,
5053
5598
  json: Boolean(args.json)
5054
5599
  });
5055
5600
  return;
@@ -5086,7 +5631,7 @@ var init_req = __esm(() => {
5086
5631
  });
5087
5632
 
5088
5633
  // src/parser/grammar.ts
5089
- var init_grammar = __esm(() => {
5634
+ var init_grammar3 = __esm(() => {
5090
5635
  init_src();
5091
5636
  });
5092
5637
 
@@ -5098,16 +5643,16 @@ __export(exports_term, {
5098
5643
  termConfirmCommand: () => termConfirmCommand,
5099
5644
  termCommand: () => termCommand
5100
5645
  });
5101
- import { existsSync as existsSync10 } from "fs";
5102
- import { join as join12, resolve as resolve16 } from "path";
5103
- import { defineCommand as defineCommand11 } from "citty";
5646
+ import { existsSync as existsSync11 } from "fs";
5647
+ import { join as join16, resolve as resolve17 } from "path";
5648
+ import { defineCommand as defineCommand12 } from "citty";
5104
5649
  function parseAliases(raw) {
5105
5650
  if (typeof raw !== "string" || raw.trim() === "")
5106
5651
  return [];
5107
5652
  return raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
5108
5653
  }
5109
- function resolvePlatform(platformDirRaw) {
5110
- const platformDir = resolve16(platformDirRaw ?? process.cwd());
5654
+ function resolvePlatform2(platformDirRaw) {
5655
+ const platformDir = resolve17(platformDirRaw ?? process.cwd());
5111
5656
  try {
5112
5657
  assertSpecPlatform(platformDir);
5113
5658
  } catch (e) {
@@ -5116,16 +5661,16 @@ function resolvePlatform(platformDirRaw) {
5116
5661
  return platformDir;
5117
5662
  }
5118
5663
  async function readTermEnvelope(platformDir, cmd) {
5119
- const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
5120
- if (!existsSync10(specPath)) {
5664
+ const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5665
+ if (!existsSync11(specPath)) {
5121
5666
  console.error(`spec ${cmd}: no TERM domain (expected spec-engine/TERM/SPEC.json under ${platformDir})`);
5122
5667
  process.exit(EXIT.USAGE);
5123
5668
  }
5124
5669
  return JSON.parse(await Bun.file(specPath).text());
5125
5670
  }
5126
5671
  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");
5672
+ const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
5673
+ const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5129
5674
  const domain2 = await readTermEnvelope(platformDir, "term");
5130
5675
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5131
5676
  const entry = {
@@ -5158,11 +5703,11 @@ async function appendTermEntry(platformDir, id, fields) {
5158
5703
  return relFile;
5159
5704
  }
5160
5705
  async function authorOrQuery(opts) {
5161
- const platformDir = resolvePlatform(opts.platformDirRaw);
5162
- const nextId = await nextRequirementId(platformDir, TERM_KEY);
5706
+ const platformDir = resolvePlatform2(opts.platformDirRaw);
5707
+ const nextId = await nextRequirementId(platformDir, TERM_KEY2);
5163
5708
  if (opts.def === undefined) {
5164
5709
  if (opts.json) {
5165
- console.log(JSON.stringify({ domain: TERM_KEY, next_id: nextId }));
5710
+ console.log(JSON.stringify({ domain: TERM_KEY2, next_id: nextId }));
5166
5711
  } else {
5167
5712
  console.log(nextId);
5168
5713
  }
@@ -5188,9 +5733,9 @@ async function authorOrQuery(opts) {
5188
5733
  }
5189
5734
  }
5190
5735
  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: [] };
5736
+ const platformDir = resolvePlatform2(platformDirRaw);
5737
+ const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5738
+ const domain2 = existsSync11(specPath) ? JSON.parse(await Bun.file(specPath).text()) : { requirements: [] };
5194
5739
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5195
5740
  const rows = requirements.map((r) => ({
5196
5741
  id: typeof r.id === "string" ? r.id : "",
@@ -5210,18 +5755,18 @@ async function resolveReviseTarget(id, platformDirRaw, defRaw) {
5210
5755
  console.error(`spec term revise: id must be a requirement id (TERM-NNN); got ${id}`);
5211
5756
  process.exit(EXIT.USAGE);
5212
5757
  }
5213
- if (id.slice(0, id.indexOf("-")) !== TERM_KEY) {
5758
+ if (id.slice(0, id.indexOf("-")) !== TERM_KEY2) {
5214
5759
  console.error(`spec term revise: ${id} is not a TERM id \u2014 revise operates on the TERM store`);
5215
5760
  process.exit(EXIT.USAGE);
5216
5761
  }
5217
- const platformDir = resolvePlatform(platformDirRaw);
5762
+ const platformDir = resolvePlatform2(platformDirRaw);
5218
5763
  const def = (defRaw ?? "").trim();
5219
5764
  if (def === "") {
5220
5765
  console.error("spec term revise: --def <definition> is required (a non-empty revision)");
5221
5766
  process.exit(EXIT.USAGE);
5222
5767
  }
5223
- const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
5224
- const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
5768
+ const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
5769
+ const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5225
5770
  const domain2 = await readTermEnvelope(platformDir, "term revise");
5226
5771
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5227
5772
  const req = requirements.find((r) => r?.id === id);
@@ -5279,7 +5824,7 @@ async function resolveConfirmTarget(platformDir, termId) {
5279
5824
  const termReqs = Array.isArray(termDomain.requirements) ? termDomain.requirements : [];
5280
5825
  const term = termReqs.find((r) => r?.id === termId);
5281
5826
  if (term === undefined) {
5282
- console.error(`spec term confirm: no term ${termId} in spec-engine/${TERM_KEY}/SPEC.json`);
5827
+ console.error(`spec term confirm: no term ${termId} in spec-engine/${TERM_KEY2}/SPEC.json`);
5283
5828
  process.exit(EXIT.USAGE);
5284
5829
  }
5285
5830
  const targetPin = typeof termDomain.specVersion === "number" ? termDomain.specVersion : 1;
@@ -5297,8 +5842,8 @@ async function resolveConfirmTarget(platformDir, termId) {
5297
5842
  async function locateCitation(platformDir, reqId, termId) {
5298
5843
  const citKey = reqId.slice(0, reqId.indexOf("-"));
5299
5844
  const relFile = `spec-engine/${citKey}/SPEC.json`;
5300
- const specPath = join12(platformDir, "spec-engine", citKey, "SPEC.json");
5301
- if (!existsSync10(specPath)) {
5845
+ const specPath = join16(platformDir, "spec-engine", citKey, "SPEC.json");
5846
+ if (!existsSync11(specPath)) {
5302
5847
  console.error(`spec term confirm: no domain ${citKey} (expected ${relFile} under ${platformDir})`);
5303
5848
  process.exit(EXIT.USAGE);
5304
5849
  }
@@ -5309,6 +5854,12 @@ async function locateCitation(platformDir, reqId, termId) {
5309
5854
  console.error(`spec term confirm: no entry ${reqId} in ${relFile}`);
5310
5855
  process.exit(EXIT.USAGE);
5311
5856
  }
5857
+ const rawStatus = typeof citReq.status === "string" ? citReq.status : "";
5858
+ const statusLc = rawStatus.toLowerCase();
5859
+ if (statusLc !== "active" && statusLc !== "draft") {
5860
+ console.error(`spec term confirm: ${reqId} is ${rawStatus} \u2014 only Active/Draft entries re-pin a citation (a superseded entry is history)`);
5861
+ process.exit(EXIT.USAGE);
5862
+ }
5312
5863
  const cites = Array.isArray(citReq.cites) ? citReq.cites : [];
5313
5864
  const cite = cites.find((c) => c?.term === termId);
5314
5865
  if (cite === undefined) {
@@ -5323,11 +5874,11 @@ async function confirmCitation(opts) {
5323
5874
  console.error(`spec term confirm: req id must be a requirement id (KEY-NNN); got ${reqId}`);
5324
5875
  process.exit(EXIT.USAGE);
5325
5876
  }
5326
- if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !== TERM_KEY) {
5877
+ if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !== TERM_KEY2) {
5327
5878
  console.error(`spec term confirm: ${termId} is not a TERM id \u2014 confirm re-pins a TERM citation`);
5328
5879
  process.exit(EXIT.USAGE);
5329
5880
  }
5330
- const platformDir = resolvePlatform(opts.platformDirRaw);
5881
+ const platformDir = resolvePlatform2(opts.platformDirRaw);
5331
5882
  const { targetId, targetPin } = await resolveConfirmTarget(platformDir, termId);
5332
5883
  const { citDomain, specPath, relFile, cite } = await locateCitation(platformDir, reqId, termId);
5333
5884
  cite.term = targetId;
@@ -5347,18 +5898,18 @@ async function confirmCitation(opts) {
5347
5898
  console.log(`confirmed ${reqId} cites ${targetId} @${targetPin} in ${relFile}`);
5348
5899
  }
5349
5900
  }
5350
- var TERM_KEY = "TERM", termListCommand, termReviseCommand, termConfirmCommand, termCommand;
5901
+ var TERM_KEY2 = "TERM", termListCommand, termReviseCommand, termConfirmCommand, termCommand;
5351
5902
  var init_term = __esm(() => {
5352
5903
  init_src();
5353
5904
  init_domains();
5354
5905
  init_constants();
5355
5906
  init_discover();
5356
5907
  init_pipeline();
5357
- init_grammar();
5908
+ init_grammar3();
5358
5909
  init_sqlite();
5359
5910
  init__shared();
5360
5911
  init_req();
5361
- termListCommand = defineCommand11({
5912
+ termListCommand = defineCommand12({
5362
5913
  meta: {
5363
5914
  name: "list",
5364
5915
  description: "List the glossary TERM entries (id, name, status), sorted by id"
@@ -5378,7 +5929,7 @@ var init_term = __esm(() => {
5378
5929
  await listTerms(args.platformDir, Boolean(args.json));
5379
5930
  }
5380
5931
  });
5381
- termReviseCommand = defineCommand11({
5932
+ termReviseCommand = defineCommand12({
5382
5933
  meta: {
5383
5934
  name: "revise",
5384
5935
  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 +5963,7 @@ var init_term = __esm(() => {
5412
5963
  });
5413
5964
  }
5414
5965
  });
5415
- termConfirmCommand = defineCommand11({
5966
+ termConfirmCommand = defineCommand12({
5416
5967
  meta: {
5417
5968
  name: "confirm",
5418
5969
  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 +5998,7 @@ var init_term = __esm(() => {
5447
5998
  });
5448
5999
  }
5449
6000
  });
5450
- termCommand = defineCommand11({
6001
+ termCommand = defineCommand12({
5451
6002
  meta: {
5452
6003
  name: "term",
5453
6004
  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)"
@@ -5456,271 +6007,68 @@ var init_term = __esm(() => {
5456
6007
  name: {
5457
6008
  type: "positional",
5458
6009
  required: true,
5459
- description: "The term's headword, or the literal `list` / `revise` verb. Without --def (author form), prints the next unused TERM id."
5460
- },
5461
- platformDir: {
5462
- type: "positional",
5463
- required: false,
5464
- description: "Platform directory (author/list), or \u2014 after `revise` \u2014 the TERM-NNN id to revise, or \u2014 after `confirm` \u2014 the citing KEY-NNN id"
5465
- },
5466
- extra: {
5467
- type: "positional",
5468
- 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: {
6010
+ description: "The term's headword, or the literal `list` / `revise` verb. Without --def (author form), prints the next unused TERM id."
6011
+ },
5699
6012
  platformDir: {
5700
6013
  type: "positional",
5701
6014
  required: false,
5702
- description: "Platform directory containing spec-engine/ (default: cwd)"
6015
+ description: "Platform directory (author/list), or \u2014 after `revise` \u2014 the TERM-NNN id to revise, or \u2014 after `confirm` \u2014 the citing KEY-NNN id"
5703
6016
  },
5704
- migrate: {
5705
- type: "boolean",
5706
- description: "One-time: parse the committed GLOSSARY.md into TERM-001..N (idempotent-skip)"
6017
+ extra: {
6018
+ type: "positional",
6019
+ required: false,
6020
+ description: "After `revise <TERM-NNN>`: the platform directory (default: cwd). After `confirm <KEY-NNN>`: the TERM-NNN id."
5707
6021
  },
5708
- check: {
5709
- type: "boolean",
5710
- description: "Fail (exit 1) if the committed GLOSSARY.md differs from the generated output"
6022
+ extra2: {
6023
+ type: "positional",
6024
+ required: false,
6025
+ description: "After `confirm <KEY-NNN> <TERM-NNN>`: the platform directory (default: cwd)"
5711
6026
  },
5712
- json: { type: "boolean", description: "Emit a JSON result instead of the text summary" }
6027
+ def: { type: "string", description: "The term's definition (stored in the statement field)" },
6028
+ text: { type: "string", description: "Alias for --def (the definition)" },
6029
+ aliases: { type: "string", description: "Comma-separated synonyms \u2192 aliases[]" },
6030
+ section: { type: "string", description: "GLOSSARY.md layout bucket (Wave F)" },
6031
+ noBump: { type: "boolean", description: "revise: do not bump the envelope specVersion" },
6032
+ json: {
6033
+ type: "boolean",
6034
+ description: "Author: { id, file } (or { domain, next_id } as an id query). list: the entries array. revise: { id, file, spec_version }."
6035
+ }
5713
6036
  },
5714
6037
  async run({ args }) {
5715
- const platformDir = resolvePlatform2(args.platformDir);
6038
+ const verb = args.name;
6039
+ const def = args.def ?? args.text;
5716
6040
  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);
6041
+ if (verb === "list") {
6042
+ await listTerms(args.platformDir, json);
6043
+ return;
6044
+ }
6045
+ if (verb === "revise") {
6046
+ await reviseTerm({
6047
+ id: args.platformDir ?? "",
6048
+ platformDirRaw: args.extra,
6049
+ def,
6050
+ noBump: Boolean(args.noBump),
6051
+ json
6052
+ });
6053
+ return;
6054
+ }
6055
+ if (verb === "confirm") {
6056
+ await confirmCitation({
6057
+ reqId: args.platformDir ?? "",
6058
+ termId: args.extra ?? "",
6059
+ platformDirRaw: args.extra2,
6060
+ json
6061
+ });
6062
+ return;
5723
6063
  }
6064
+ await authorOrQuery({
6065
+ name: verb,
6066
+ platformDirRaw: args.platformDir,
6067
+ def,
6068
+ aliasesRaw: args.aliases,
6069
+ section: args.section,
6070
+ json
6071
+ });
5724
6072
  }
5725
6073
  });
5726
6074
  });
@@ -5775,7 +6123,7 @@ __export(exports_supersede, {
5775
6123
  supersedeCommand: () => supersedeCommand
5776
6124
  });
5777
6125
  import { existsSync as existsSync12 } from "fs";
5778
- import { join as join14, resolve as resolve18 } from "path";
6126
+ import { join as join17, resolve as resolve18 } from "path";
5779
6127
  import { defineCommand as defineCommand13 } from "citty";
5780
6128
  async function resolveSupersedeTarget(id, platformDir) {
5781
6129
  if (!ID_RE.test(id)) {
@@ -5789,7 +6137,7 @@ async function resolveSupersedeTarget(id, platformDir) {
5789
6137
  }
5790
6138
  const key = id.slice(0, id.indexOf("-"));
5791
6139
  const relFile = `spec-engine/${key}/SPEC.json`;
5792
- const specPath = join14(platformDir, "spec-engine", key, "SPEC.json");
6140
+ const specPath = join17(platformDir, "spec-engine", key, "SPEC.json");
5793
6141
  if (!existsSync12(specPath)) {
5794
6142
  console.error(`spec supersede: no domain ${key} (expected ${relFile} under ${platformDir})`);
5795
6143
  process.exit(EXIT.USAGE);
@@ -5848,10 +6196,18 @@ function resolveSuccessorTermFields(args, req) {
5848
6196
  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
6197
  return { term, aliases };
5850
6198
  }
5851
- function applySupersedeEdit(target, newId, requirement, why, lives, noBump, termFields) {
5852
- const { domain: domain2, requirements, req } = target;
6199
+ function flipPredecessor(req, newId, issue) {
5853
6200
  req.status = "superseded";
5854
6201
  req.supersededBy = newId;
6202
+ if (issue) {
6203
+ const issues = Array.isArray(req.issues) ? req.issues : [];
6204
+ issues.push({ role: "supersedes-via", id: issue });
6205
+ req.issues = issues;
6206
+ }
6207
+ }
6208
+ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, termFields, issue) {
6209
+ const { domain: domain2, requirements, req } = target;
6210
+ flipPredecessor(req, newId, issue);
5855
6211
  const successor = {
5856
6212
  id: newId,
5857
6213
  status: "active",
@@ -5861,7 +6217,7 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
5861
6217
  supersededBy: null,
5862
6218
  relates: [],
5863
6219
  livesIn: lives === "" ? [] : [lives],
5864
- issues: [],
6220
+ issues: issue ? [{ role: "created", id: issue }] : [],
5865
6221
  changedAtVersion: 1
5866
6222
  };
5867
6223
  if (termFields !== null) {
@@ -5900,10 +6256,11 @@ var supersedeCommand;
5900
6256
  var init_supersede = __esm(() => {
5901
6257
  init_src();
5902
6258
  init_domains();
6259
+ init_grammar2();
5903
6260
  init_constants();
5904
6261
  init_discover();
5905
6262
  init_domainJson();
5906
- init_grammar();
6263
+ init_grammar3();
5907
6264
  init__shared();
5908
6265
  init_req();
5909
6266
  supersedeCommand = defineCommand13({
@@ -5950,6 +6307,10 @@ var init_supersede = __esm(() => {
5950
6307
  type: "boolean",
5951
6308
  description: "Do not bump the envelope specVersion"
5952
6309
  },
6310
+ issue: {
6311
+ type: "string",
6312
+ description: "Ticket that caused this supersession \u2014 recorded as supersedes-via on the predecessor and created on the successor (opaque provenance)"
6313
+ },
5953
6314
  json: {
5954
6315
  type: "boolean",
5955
6316
  description: "Emit { old_id, new_id, file, spec_version, retag } as JSON instead of the text summary"
@@ -5960,11 +6321,12 @@ var init_supersede = __esm(() => {
5960
6321
  const platformDir = resolve18(args.platformDir ?? process.cwd());
5961
6322
  const target = await resolveSupersedeTarget(id, platformDir);
5962
6323
  const requirement = await resolveSuccessorText(args, id);
6324
+ await enforceStatementGrammar(platformDir, target.key, requirement, "spec supersede");
5963
6325
  const { why, lives, binds } = resolveSuccessorFields(args, target.req);
5964
6326
  warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
5965
6327
  const newId = await nextRequirementId(platformDir, target.key);
5966
6328
  const termFields = target.key === "TERM" ? resolveSuccessorTermFields(args, target.req) : null;
5967
- const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields);
6329
+ const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields, (args.issue ?? "").trim() || undefined);
5968
6330
  const res = await validateAndWrite(target.specPath, target.domain, target.relFile);
5969
6331
  if (!res.ok) {
5970
6332
  for (const diag of res.diagnostics) {
@@ -5998,18 +6360,133 @@ var init_supersede = __esm(() => {
5998
6360
  });
5999
6361
  });
6000
6362
 
6363
+ // src/commands/deprecate.ts
6364
+ var exports_deprecate = {};
6365
+ __export(exports_deprecate, {
6366
+ deprecateCommand: () => deprecateCommand
6367
+ });
6368
+ import { existsSync as existsSync13 } from "fs";
6369
+ import { join as join18, resolve as resolve19 } from "path";
6370
+ import { defineCommand as defineCommand14 } from "citty";
6371
+ async function resolveDeprecateTarget(platformDir, id) {
6372
+ const key = id.slice(0, id.indexOf("-"));
6373
+ const relFile = `spec-engine/${key}/SPEC.json`;
6374
+ const specPath = join18(platformDir, "spec-engine", key, "SPEC.json");
6375
+ if (!existsSync13(specPath)) {
6376
+ console.error(`spec deprecate: no domain ${key} (expected ${relFile} under ${platformDir})`);
6377
+ process.exit(EXIT.USAGE);
6378
+ }
6379
+ const domain2 = JSON.parse(await Bun.file(specPath).text());
6380
+ const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
6381
+ const req = requirements.find((r) => r?.id === id);
6382
+ if (req === undefined) {
6383
+ console.error(`spec deprecate: no entry ${id} in ${relFile}`);
6384
+ process.exit(EXIT.USAGE);
6385
+ }
6386
+ const statusLc = (typeof req.status === "string" ? req.status : "").toLowerCase();
6387
+ if (statusLc === "deprecated" || statusLc === "retired") {
6388
+ console.error(`spec deprecate: ${id} is already deprecated`);
6389
+ process.exit(EXIT.USAGE);
6390
+ }
6391
+ if (statusLc !== "active" && statusLc !== "draft") {
6392
+ console.error(`spec deprecate: ${id} is ${req.status} \u2014 only Active/Draft entries deprecate (a superseded entry is already history)`);
6393
+ process.exit(EXIT.USAGE);
6394
+ }
6395
+ return { domain: domain2, req, specPath, relFile };
6396
+ }
6397
+ var deprecateCommand;
6398
+ var init_deprecate = __esm(() => {
6399
+ init_src();
6400
+ init_constants();
6401
+ init_discover();
6402
+ init_grammar3();
6403
+ init__shared();
6404
+ deprecateCommand = defineCommand14({
6405
+ meta: {
6406
+ name: "deprecate",
6407
+ description: "Mark a requirement end-of-life with a recorded reason (a requirement is never deleted)"
6408
+ },
6409
+ args: {
6410
+ id: {
6411
+ type: "positional",
6412
+ required: true,
6413
+ description: "Requirement id to deprecate (KEY-NNN)"
6414
+ },
6415
+ platformDir: {
6416
+ type: "positional",
6417
+ required: false,
6418
+ description: "Platform directory (default: cwd)"
6419
+ },
6420
+ reason: {
6421
+ type: "string",
6422
+ description: "Why this requirement is end-of-life (required \u2014 the durable record)"
6423
+ },
6424
+ json: {
6425
+ type: "boolean",
6426
+ description: "Print { id, file, reason, sites } instead of text"
6427
+ }
6428
+ },
6429
+ async run({ args }) {
6430
+ const id = args.id;
6431
+ const platformDir = resolve19(args.platformDir ?? process.cwd());
6432
+ const reason = (args.reason ?? "").trim();
6433
+ if (!ID_RE.test(id)) {
6434
+ console.error(`spec deprecate: id must be a requirement id (KEY-NNN); got ${id}`);
6435
+ process.exit(EXIT.USAGE);
6436
+ }
6437
+ if (reason === "") {
6438
+ console.error("spec deprecate: --reason is required \u2014 the reason is the durable record of why this requirement ended");
6439
+ process.exit(EXIT.USAGE);
6440
+ }
6441
+ try {
6442
+ assertSpecPlatform(platformDir);
6443
+ } catch (e) {
6444
+ handleNotAPlatform(e);
6445
+ }
6446
+ const { domain: domain2, req, specPath, relFile } = await resolveDeprecateTarget(platformDir, id);
6447
+ req.status = "deprecated";
6448
+ req.deprecatedReason = reason;
6449
+ domain2.updated = localToday();
6450
+ const res = await validateAndWrite(specPath, domain2, relFile);
6451
+ if (!res.ok) {
6452
+ for (const diag of res.diagnostics)
6453
+ console.error(`spec deprecate: ${diag.detail}`);
6454
+ process.exit(EXIT.USAGE);
6455
+ }
6456
+ const tags = await reindexAndListTags(platformDir, id);
6457
+ const sites = tags.map(({ req_id, repo, file, line, kind, level }) => ({
6458
+ req_id,
6459
+ repo,
6460
+ file,
6461
+ line,
6462
+ kind,
6463
+ level: level ?? null
6464
+ }));
6465
+ if (args.json) {
6466
+ console.log(JSON.stringify({ id, file: relFile, reason, sites }));
6467
+ return;
6468
+ }
6469
+ console.log(`deprecated ${id} in ${relFile} \u2014 reason recorded`);
6470
+ if (sites.length > 0) {
6471
+ console.error(`${sites.length} code tag(s) still bind ${id} \u2014 remove or retag them (spec check reports each as DEPRECATED_REFERENCED):`);
6472
+ console.error(renderReqTags(sites, "text"));
6473
+ }
6474
+ }
6475
+ });
6476
+ });
6477
+
6001
6478
  // src/commands/move.ts
6002
6479
  var exports_move = {};
6003
6480
  __export(exports_move, {
6004
6481
  moveCommand: () => moveCommand
6005
6482
  });
6006
- import { existsSync as existsSync13 } from "fs";
6007
- import { join as join15, resolve as resolve19 } from "path";
6008
- import { defineCommand as defineCommand14 } from "citty";
6483
+ import { existsSync as existsSync14 } from "fs";
6484
+ import { join as join19, resolve as resolve20 } from "path";
6485
+ import { defineCommand as defineCommand15 } from "citty";
6009
6486
  async function loadDomainSide(platformDir, key) {
6010
6487
  const relFile = `spec-engine/${key}/SPEC.json`;
6011
- const specPath = join15(platformDir, "spec-engine", key, "SPEC.json");
6012
- if (!existsSync13(specPath))
6488
+ const specPath = join19(platformDir, "spec-engine", key, "SPEC.json");
6489
+ if (!existsSync14(specPath))
6013
6490
  return null;
6014
6491
  const domain2 = JSON.parse(await Bun.file(specPath).text());
6015
6492
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
@@ -6068,6 +6545,15 @@ function resolveSuccessorFields2(args, req) {
6068
6545
  lives: (args.lives ?? srcLives).trim()
6069
6546
  };
6070
6547
  }
6548
+ async function assertMovableStatement(platformDir, id, targetKey, statement, args) {
6549
+ if (statement === "") {
6550
+ console.error(`spec move: ${id} has an empty Requirement \u2014 cannot move a blank statement`);
6551
+ process.exit(EXIT.USAGE);
6552
+ }
6553
+ if (typeof args.text === "string") {
6554
+ await enforceStatementGrammar(platformDir, targetKey, statement, "spec move");
6555
+ }
6556
+ }
6071
6557
  function resolveMoveVersion(domain2, key, noBump) {
6072
6558
  domain2.updated = localToday();
6073
6559
  if (key !== "TERM") {
@@ -6123,15 +6609,16 @@ var moveCommand;
6123
6609
  var init_move = __esm(() => {
6124
6610
  init_src();
6125
6611
  init_domains();
6612
+ init_grammar2();
6126
6613
  init_constants();
6127
6614
  init_discover();
6128
6615
  init_pipeline();
6129
6616
  init_domainJson();
6130
- init_grammar();
6617
+ init_grammar3();
6131
6618
  init_sqlite();
6132
6619
  init__shared();
6133
6620
  init_req();
6134
- moveCommand = defineCommand14({
6621
+ moveCommand = defineCommand15({
6135
6622
  meta: {
6136
6623
  name: "move",
6137
6624
  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 +6663,10 @@ var init_move = __esm(() => {
6176
6663
  async run({ args }) {
6177
6664
  const id = args.id;
6178
6665
  const rawTargetKey = args.newDomain;
6179
- const platformDir = resolve19(args.platformDir ?? process.cwd());
6666
+ const platformDir = resolve20(args.platformDir ?? process.cwd());
6180
6667
  const target = await resolveMoveTarget(id, rawTargetKey, platformDir);
6181
6668
  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
- }
6669
+ await assertMovableStatement(platformDir, id, target.target.key, fields.statement, args);
6186
6670
  warnUnresolvableRefs(platformDir, [fields.statement, fields.why, fields.lives]);
6187
6671
  const newId = await nextRequirementId(platformDir, target.target.key);
6188
6672
  const { sourceVersion, targetVersion } = applyMoveEdit(target, newId, fields, Boolean(args.noBump));
@@ -6234,17 +6718,18 @@ var exports_amend = {};
6234
6718
  __export(exports_amend, {
6235
6719
  amendCommand: () => amendCommand
6236
6720
  });
6237
- import { existsSync as existsSync14 } from "fs";
6238
- import { join as join16, resolve as resolve20 } from "path";
6239
- import { defineCommand as defineCommand15 } from "citty";
6721
+ import { existsSync as existsSync15 } from "fs";
6722
+ import { join as join20, resolve as resolve21 } from "path";
6723
+ import { defineCommand as defineCommand16 } from "citty";
6240
6724
  function validateAmendFields(args) {
6241
6725
  const hasText = typeof args.text === "string";
6726
+ const hasIssue = typeof args.issue === "string" && args.issue.trim() !== "";
6242
6727
  const hasWhy = typeof args.why === "string";
6243
6728
  const hasLives = typeof args.lives === "string";
6244
6729
  const hasBinds = typeof args.binds === "string";
6245
6730
  const hasTerm = typeof args.term === "string";
6246
6731
  const hasAliases = typeof args.aliases === "string";
6247
- if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases) {
6732
+ if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases && !hasIssue) {
6248
6733
  console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --term / --aliases");
6249
6734
  process.exit(EXIT.USAGE);
6250
6735
  }
@@ -6256,13 +6741,13 @@ function validateAmendFields(args) {
6256
6741
  console.error("spec amend: --term must be a non-empty headword");
6257
6742
  process.exit(EXIT.USAGE);
6258
6743
  }
6259
- return { hasText, hasWhy, hasLives, hasBinds, hasTerm, hasAliases };
6744
+ return { hasText, hasWhy, hasLives, hasBinds, hasTerm, hasAliases, hasIssue };
6260
6745
  }
6261
6746
  async function locateAmendEntry(platformDir, id) {
6262
6747
  const key = id.slice(0, id.indexOf("-"));
6263
6748
  const relFile = `spec-engine/${key}/SPEC.json`;
6264
- const specPath = join16(platformDir, "spec-engine", key, "SPEC.json");
6265
- if (!existsSync14(specPath)) {
6749
+ const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
6750
+ if (!existsSync15(specPath)) {
6266
6751
  console.error(`spec amend: no domain ${key} (expected ${relFile} under ${platformDir})`);
6267
6752
  process.exit(EXIT.USAGE);
6268
6753
  }
@@ -6324,6 +6809,12 @@ function applyAmendMutations(req, args, fields) {
6324
6809
  fieldsChanged.push("term");
6325
6810
  refValues.push(v);
6326
6811
  }
6812
+ if (fields.hasIssue) {
6813
+ const issues = Array.isArray(req.issues) ? req.issues : [];
6814
+ issues.push({ role: "amends-via", id: args.issue.trim() });
6815
+ req.issues = issues;
6816
+ fieldsChanged.push("issue");
6817
+ }
6327
6818
  if (fields.hasAliases) {
6328
6819
  const raw = args.aliases.trim();
6329
6820
  req.aliases = raw === "" ? [] : raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
@@ -6334,12 +6825,13 @@ function applyAmendMutations(req, args, fields) {
6334
6825
  var amendCommand;
6335
6826
  var init_amend = __esm(() => {
6336
6827
  init_src();
6828
+ init_grammar2();
6337
6829
  init_constants();
6338
6830
  init_discover();
6339
- init_grammar();
6831
+ init_grammar3();
6340
6832
  init__shared();
6341
6833
  init_req();
6342
- amendCommand = defineCommand15({
6834
+ amendCommand = defineCommand16({
6343
6835
  meta: {
6344
6836
  name: "amend",
6345
6837
  description: "Revise an unshipped requirement's fields in place (same id, no specVersion bump). Supersede shipped truth instead."
@@ -6362,6 +6854,10 @@ var init_amend = __esm(() => {
6362
6854
  description: "Binds value (validated for @-refs; not persisted in JSON \u2014 STOR-01)"
6363
6855
  },
6364
6856
  lives: { type: "string", description: "New Lives in (livesIn) field value" },
6857
+ issue: {
6858
+ type: "string",
6859
+ description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
6860
+ },
6365
6861
  term: { type: "string", description: "New TERM headword (term field; TERM ids)" },
6366
6862
  aliases: {
6367
6863
  type: "string",
@@ -6379,7 +6875,7 @@ var init_amend = __esm(() => {
6379
6875
  process.exit(EXIT.USAGE);
6380
6876
  return;
6381
6877
  }
6382
- const platformDir = resolve20(args.platformDir ?? process.cwd());
6878
+ const platformDir = resolve21(args.platformDir ?? process.cwd());
6383
6879
  try {
6384
6880
  assertSpecPlatform(platformDir);
6385
6881
  } catch (e) {
@@ -6392,6 +6888,10 @@ var init_amend = __esm(() => {
6392
6888
  if (statusLc === "active") {
6393
6889
  await assertUnshipped(platformDir, id);
6394
6890
  }
6891
+ if (fields.hasText) {
6892
+ const key = id.slice(0, id.indexOf("-"));
6893
+ await enforceStatementGrammar(platformDir, key, args.text.trim(), "spec amend");
6894
+ }
6395
6895
  const { fieldsChanged, refValues } = applyAmendMutations(req, args, fields);
6396
6896
  warnUnresolvableRefs(platformDir, refValues);
6397
6897
  domain2.updated = localToday();
@@ -6428,13 +6928,14 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
6428
6928
  opts.brief,
6429
6929
  "",
6430
6930
  "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."
6931
+ "1. One requirement per behavior \u2014 never one per ticket; a brief fans out into several.",
6932
+ "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.",
6933
+ '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."',
6934
+ "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.",
6935
+ "5. Tie each draft to its source: quote the exact phrase of the brief it derives from; label anything you inferred as inferred.",
6936
+ "6. Present the full batch (statement, why, placement, source quote) for approval BEFORE writing.",
6937
+ '7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026" --lives "\u2026"`.',
6938
+ "8. The originating ticket is provenance only \u2014 never a requirement id, never a code tag."
6438
6939
  ].join(`
6439
6940
  `);
6440
6941
  }
@@ -6571,7 +7072,7 @@ var init_mcp = __esm(() => {
6571
7072
  init_domains();
6572
7073
  init_constants();
6573
7074
  init_pipeline();
6574
- init_grammar();
7075
+ init_grammar3();
6575
7076
  init_sqlite();
6576
7077
  });
6577
7078
 
@@ -6580,15 +7081,15 @@ var exports_mcp = {};
6580
7081
  __export(exports_mcp, {
6581
7082
  mcpCommand: () => mcpCommand
6582
7083
  });
6583
- import { resolve as resolve21 } from "path";
7084
+ import { resolve as resolve22 } from "path";
6584
7085
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6585
- import { defineCommand as defineCommand16 } from "citty";
7086
+ import { defineCommand as defineCommand17 } from "citty";
6586
7087
  var mcpCommand;
6587
7088
  var init_mcp2 = __esm(() => {
6588
7089
  init_discover();
6589
7090
  init_mcp();
6590
7091
  init__shared();
6591
- mcpCommand = defineCommand16({
7092
+ mcpCommand = defineCommand17({
6592
7093
  meta: {
6593
7094
  name: "mcp",
6594
7095
  description: "Serve Spec Engine as an MCP (Model Context Protocol) server over stdio \u2014 agent-native query/resolve/check/report tools."
@@ -6601,7 +7102,7 @@ var init_mcp2 = __esm(() => {
6601
7102
  }
6602
7103
  },
6603
7104
  async run({ args }) {
6604
- const platformDir = resolve21(args.platformDir ?? process.cwd());
7105
+ const platformDir = resolve22(args.platformDir ?? process.cwd());
6605
7106
  try {
6606
7107
  assertSpecPlatform(platformDir);
6607
7108
  } catch (e) {
@@ -6619,9 +7120,9 @@ var exports_resolve = {};
6619
7120
  __export(exports_resolve, {
6620
7121
  resolveCommand: () => resolveCommand
6621
7122
  });
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";
7123
+ import { statSync as statSync6 } from "fs";
7124
+ import { join as join21, relative, resolve as resolve23, sep as sep3 } from "path";
7125
+ import { defineCommand as defineCommand18 } from "citty";
6625
7126
  function classifyArg(a) {
6626
7127
  if (a.startsWith("--")) {
6627
7128
  const eq = a.indexOf("=");
@@ -6656,10 +7157,10 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
6656
7157
  let lastIsPlatformDir = false;
6657
7158
  let lastIsDirWithoutSpecEngine = false;
6658
7159
  try {
6659
- const resolved = resolve22(last);
6660
- if (statSync5(resolved).isDirectory()) {
7160
+ const resolved = resolve23(last);
7161
+ if (statSync6(resolved).isDirectory()) {
6661
7162
  try {
6662
- if (statSync5(join17(resolved, "spec-engine")).isDirectory()) {
7163
+ if (statSync6(join21(resolved, "spec-engine")).isDirectory()) {
6663
7164
  lastIsPlatformDir = true;
6664
7165
  } else {
6665
7166
  lastIsDirWithoutSpecEngine = true;
@@ -6753,7 +7254,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
6753
7254
  }
6754
7255
  await runReverseQuery({
6755
7256
  reqId: reqArg,
6756
- platformDir: resolve22(lone ?? process.cwd()),
7257
+ platformDir: resolve23(lone ?? process.cwd()),
6757
7258
  outArg: args.out,
6758
7259
  json: Boolean(args.json),
6759
7260
  fresh: Boolean(args.fresh),
@@ -6763,7 +7264,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
6763
7264
  function normalizeFileInputs(files, platformDir) {
6764
7265
  const normalized = [];
6765
7266
  for (const input of files) {
6766
- const rel = relative(platformDir, resolve22(platformDir, input));
7267
+ const rel = relative(platformDir, resolve23(platformDir, input));
6767
7268
  const segments = rel.split(sep3);
6768
7269
  if (rel === "" || rel === "." || segments[0] === "..") {
6769
7270
  console.error(`spec resolve: file path must be inside platformDir: ${input}`);
@@ -6781,7 +7282,7 @@ var init_resolve2 = __esm(() => {
6781
7282
  init_prompt();
6782
7283
  init__shared();
6783
7284
  VALUE_FLAGS = new Set(["--out", "-o", "--platformDir", "--req"]);
6784
- resolveCommand = defineCommand17({
7285
+ resolveCommand = defineCommand18({
6785
7286
  meta: {
6786
7287
  name: "resolve",
6787
7288
  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 +7337,7 @@ var init_resolve2 = __esm(() => {
6836
7337
  process.exit(EXIT.USAGE);
6837
7338
  return;
6838
7339
  }
6839
- const platformDir = resolve22(resolved.platformDir ?? process.cwd());
7340
+ const platformDir = resolve23(resolved.platformDir ?? process.cwd());
6840
7341
  const outArg = args.out;
6841
7342
  const dbPath = resolveDbPath(platformDir, outArg);
6842
7343
  if (outArg)
@@ -6905,6 +7406,18 @@ function classifyGate(input) {
6905
7406
  pinned_spec_version: pin
6906
7407
  };
6907
7408
  }
7409
+ if (req.status === "Deprecated") {
7410
+ return {
7411
+ pass: false,
7412
+ reason: "DEPRECATED",
7413
+ repo: requestedRepoName,
7414
+ req_id: requestedReqId,
7415
+ detail: `Requirement ${requestedReqId} is Deprecated \u2014 it is end-of-life with no successor; do not build against it`,
7416
+ status: req.status,
7417
+ changed_at_version: req.changed_at_version,
7418
+ pinned_spec_version: pin
7419
+ };
7420
+ }
6908
7421
  if (req.changed_at_version > pin) {
6909
7422
  return {
6910
7423
  pass: false,
@@ -6942,9 +7455,9 @@ var exports_gate = {};
6942
7455
  __export(exports_gate, {
6943
7456
  gateCommand: () => gateCommand
6944
7457
  });
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";
7458
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
7459
+ import { dirname as dirname10, join as join22, resolve as resolve24 } from "path";
7460
+ import { defineCommand as defineCommand19 } from "citty";
6948
7461
  function isEpipe(e) {
6949
7462
  if (e === null || typeof e !== "object")
6950
7463
  return false;
@@ -6958,8 +7471,8 @@ function resolveGateArgs(args) {
6958
7471
  console.error("spec gate: <repo> and <reqId> are required (e.g., spec gate api BILLING-009)");
6959
7472
  process.exit(EXIT.USAGE);
6960
7473
  }
6961
- const platformDir = resolve23(args.platformDir ?? process.cwd());
6962
- const hasSpecEngineDir = existsSync15(join18(platformDir, "spec-engine"));
7474
+ const platformDir = resolve24(args.platformDir ?? process.cwd());
7475
+ const hasSpecEngineDir = existsSync16(join22(platformDir, "spec-engine"));
6963
7476
  if (!hasSpecEngineDir) {
6964
7477
  console.error(formatNotASpecPlatform(platformDir));
6965
7478
  process.exit(EXIT.USAGE);
@@ -6998,7 +7511,7 @@ var init_gate = __esm(() => {
6998
7511
  init_prompt();
6999
7512
  init_sqlite();
7000
7513
  init__shared();
7001
- gateCommand = defineCommand18({
7514
+ gateCommand = defineCommand19({
7002
7515
  meta: {
7003
7516
  name: "gate",
7004
7517
  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 +7597,8 @@ var init_gate = __esm(() => {
7084
7597
  });
7085
7598
  });
7086
7599
 
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
7600
  // src/guard/collect.ts
7105
- import { existsSync as existsSync16 } from "fs";
7106
- import { join as join19 } from "path";
7601
+ import { join as join23 } from "path";
7107
7602
  function isIgnored(path) {
7108
7603
  return isPathIgnored(path);
7109
7604
  }
@@ -7160,7 +7655,7 @@ async function collectWorktreeReqs(canonicalDir) {
7160
7655
  const activeIds = new Set;
7161
7656
  const supersedesTargets = new Set;
7162
7657
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
7163
- const text = await Bun.file(join19(canonicalDir, rel)).text();
7658
+ const text = await Bun.file(join23(canonicalDir, rel)).text();
7164
7659
  for (const r of parseReqs(text)) {
7165
7660
  ids.add(r.id);
7166
7661
  if (r.status.toLowerCase() === "active")
@@ -7181,20 +7676,6 @@ function tallyWorktreeTags(storage2) {
7181
7676
  }
7182
7677
  return { implCount, verifyCount };
7183
7678
  }
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
7679
  async function collectFacts(platformDir, ref, storage2) {
7199
7680
  const changed = gitDiffNameStatus(platformDir, ref);
7200
7681
  const specPaths = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
@@ -7202,9 +7683,8 @@ async function collectFacts(platformDir, ref, storage2) {
7202
7683
  const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
7203
7684
  const base = collectBaseReqs2(platformDir, ref, specPaths);
7204
7685
  const baseTags = collectBaseTags(platformDir, ref, codePaths);
7205
- const worktree = await collectWorktreeReqs(join19(platformDir, CANONICAL_SPECS_DIR));
7686
+ const worktree = await collectWorktreeReqs(join23(platformDir, CANONICAL_SPECS_DIR));
7206
7687
  const counts = tallyWorktreeTags(storage2);
7207
- const approved = await collectApprovals(platformDir, changed);
7208
7688
  return {
7209
7689
  baseReqs: base.reqs,
7210
7690
  baseReqPath: base.path,
@@ -7215,7 +7695,6 @@ async function collectFacts(platformDir, ref, storage2) {
7215
7695
  worktreeSupersedesTargets: worktree.supersedesTargets,
7216
7696
  worktreeImplCount: counts.implCount,
7217
7697
  worktreeVerifyCount: counts.verifyCount,
7218
- approved,
7219
7698
  deletedSpecFiles
7220
7699
  };
7221
7700
  }
@@ -7225,7 +7704,6 @@ var init_collect = __esm(() => {
7225
7704
  init_constants();
7226
7705
  init_fs();
7227
7706
  init_tags();
7228
- init_directives();
7229
7707
  CODE_EXTS = new Set(DEFAULT_EXTS);
7230
7708
  });
7231
7709
 
@@ -7263,7 +7741,7 @@ function deletesClause(losses) {
7263
7741
  return joinClauses(parts);
7264
7742
  }
7265
7743
  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.";
7744
+ 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
7745
  }
7268
7746
  function fileBlock(loss) {
7269
7747
  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 +7792,17 @@ function reqDeletedLoss(base, f) {
7314
7792
  req_id: base.id,
7315
7793
  file: f.baseReqPath.get(base.id) ?? "",
7316
7794
  line: 0,
7317
- detail: `${base.id} was Active at the ref but is absent from the working-tree spec with no approved supersession`
7795
+ 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`
7796
+ };
7797
+ }
7798
+ function historyRemovedLoss(base, f) {
7799
+ const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
7800
+ return {
7801
+ kind: "REQUIREMENT_REMOVED",
7802
+ req_id: base.id,
7803
+ file: f.baseReqPath.get(base.id) ?? "",
7804
+ line: 0,
7805
+ 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
7806
  };
7319
7807
  }
7320
7808
  function implLostLoss(id, site) {
@@ -7345,18 +7833,16 @@ function specFileDeletedLoss(path) {
7345
7833
  };
7346
7834
  }
7347
7835
  function reqDeletedFor(base, f) {
7348
- if (!isActive(base.status))
7349
- return null;
7350
- if (f.approved.has(base.id))
7351
- return null;
7352
7836
  if (f.worktreeReqIds.has(base.id))
7353
7837
  return null;
7838
+ if (!isActive(base.status))
7839
+ return historyRemovedLoss(base, f);
7354
7840
  if (isSuperseded(base, f))
7355
7841
  return null;
7356
7842
  return reqDeletedLoss(base, f);
7357
7843
  }
7358
7844
  function lastTagGone(id, count, f) {
7359
- return !f.approved.has(id) && f.worktreeActiveIds.has(id) && count === 0;
7845
+ return f.worktreeActiveIds.has(id) && count === 0;
7360
7846
  }
7361
7847
  function classifyLosses(f) {
7362
7848
  const out = [];
@@ -7383,14 +7869,14 @@ var exports_guard = {};
7383
7869
  __export(exports_guard, {
7384
7870
  guardCommand: () => guardCommand
7385
7871
  });
7386
- import { resolve as resolve24 } from "path";
7387
- import { defineCommand as defineCommand19 } from "citty";
7872
+ import { resolve as resolve25 } from "path";
7873
+ import { defineCommand as defineCommand20 } from "citty";
7388
7874
  var guardCommand;
7389
7875
  var init_guard = __esm(() => {
7390
7876
  init_constants();
7391
7877
  init_collect();
7392
7878
  init__shared();
7393
- guardCommand = defineCommand19({
7879
+ guardCommand = defineCommand20({
7394
7880
  meta: {
7395
7881
  name: "guard",
7396
7882
  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 +7897,7 @@ var init_guard = __esm(() => {
7411
7897
  }
7412
7898
  },
7413
7899
  async run({ args }) {
7414
- const platformDir = resolve24(args.platformDir ?? process.cwd());
7900
+ const platformDir = resolve25(args.platformDir ?? process.cwd());
7415
7901
  const ref = args.against ?? "HEAD";
7416
7902
  const jsonMode = Boolean(args.json);
7417
7903
  if (!gitRefResolves(platformDir, ref)) {
@@ -7482,6 +7968,9 @@ function link(href, key, label, active) {
7482
7968
  function comingSoon(label) {
7483
7969
  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
7970
  }
7971
+ function flagged(feature, href, key, label, active) {
7972
+ return featureEnabled(feature) ? link(href, key, label, active) : comingSoon(label);
7973
+ }
7485
7974
  function navBar(active = "") {
7486
7975
  return html`<nav class="sidebar">
7487
7976
  <a href="/" class="sidebar-brand">${clampMark}<span>Spec Engine</span></a>
@@ -7490,22 +7979,22 @@ function navBar(active = "") {
7490
7979
  <div class="sidebar-group-label">Registry</div>
7491
7980
  ${link("/", "coverage", "Coverage", active)}
7492
7981
  ${link("/requirements", "requirements", "Requirements", active)}
7493
- ${comingSoon("Glossary")}
7982
+ ${flagged("glossary", "/glossary", "glossary", "Glossary", active)}
7494
7983
  </div>
7495
7984
  <div class="sidebar-group">
7496
7985
  <div class="sidebar-group-label">Trace</div>
7497
- ${comingSoon("Relations")}
7498
- ${comingSoon("Provenance")}
7986
+ ${flagged("relations", "/relations", "relations", "Relations", active)}
7987
+ ${flagged("provenance", "/provenance", "provenance", "Provenance", active)}
7499
7988
  </div>
7500
7989
  <div class="sidebar-group">
7501
7990
  <div class="sidebar-group-label">Workspace</div>
7502
- ${comingSoon("Query")}
7503
- ${comingSoon("Editor")}
7991
+ ${flagged("query", "/query", "query", "Query", active)}
7992
+ ${flagged("editor", "/editor", "editor", "Editor", active)}
7504
7993
  </div>
7505
7994
  <div class="sidebar-group">
7506
7995
  <div class="sidebar-group-label">System</div>
7507
7996
  ${link("/setup", "setup", "Setup", active)}
7508
- ${comingSoon("Logs")}
7997
+ ${flagged("logs", "/logs", "logs", "Logs", active)}
7509
7998
  </div>
7510
7999
  </div>
7511
8000
  <div class="sidebar-footer">
@@ -7516,6 +8005,7 @@ function navBar(active = "") {
7516
8005
  }
7517
8006
  var clampMark;
7518
8007
  var init_nav = __esm(() => {
8008
+ init_src();
7519
8009
  clampMark = html`<svg width="22" height="22" viewBox="0 0 64 64" fill="none" aria-hidden="true">
7520
8010
  <path d="M25 13 H13 V51 H25" stroke="var(--fg)" stroke-width="6" />
7521
8011
  <path d="M39 13 H51 V51 H39" stroke="var(--fg)" stroke-width="6" />
@@ -9053,7 +9543,7 @@ var init_components = __esm(() => {
9053
9543
  Active: "ACTIVE",
9054
9544
  Superseded: "SUPERSEDED",
9055
9545
  Draft: "DRAFT",
9056
- Retired: "DEPRECATED"
9546
+ Deprecated: "DEPRECATED"
9057
9547
  };
9058
9548
  });
9059
9549
 
@@ -9184,8 +9674,8 @@ function mountCoverage(app) {
9184
9674
  const reqs = await apiJson(app, "/api/requirements");
9185
9675
  const titleOf = new Map(reqs.map((r) => [r.id, r.text]));
9186
9676
  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;
9677
+ const visible = showAll ? rows : rows.filter((r) => r.req_status !== "Superseded" && r.req_status !== "Deprecated");
9678
+ const hiddenCount = new Set(rows.filter((r) => r.req_status === "Superseded" || r.req_status === "Deprecated").map((r) => r.req_id)).size;
9189
9679
  const { repos, domains } = pivot(visible);
9190
9680
  const reqCount = domains.reduce((n, d) => n + d.reqs.length, 0);
9191
9681
  const reportRows = await apiJson(app, "/api/report");
@@ -9216,7 +9706,7 @@ function mountCoverage(app) {
9216
9706
  <span class="legend-sep"></span>
9217
9707
  <span class="legend-item">${requirementStatusBadge("Active")}</span>
9218
9708
  ${showAll ? html4`<span class="legend-item">${requirementStatusBadge("Superseded")}</span>
9219
- <span class="legend-item">${requirementStatusBadge("Retired")}</span>` : ""}
9709
+ <span class="legend-item">${requirementStatusBadge("Deprecated")}</span>` : ""}
9220
9710
  <span class="panel-spacer"></span>
9221
9711
  <span class="req-controls">
9222
9712
  ${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 +9881,8 @@ async function renderResult(res) {
9391
9881
  }
9392
9882
  function mountEditor(app) {
9393
9883
  app.get("/editor", (c) => {
9884
+ if (!featureEnabled("editor"))
9885
+ return c.html(comingSoonDoc("editor", "Editor"));
9394
9886
  const body = html5`
9395
9887
  ${navBar("editor")}
9396
9888
  <div class="eyebrow">/ Editor</div>
@@ -9445,6 +9937,8 @@ function mountEditor(app) {
9445
9937
  return c.html(page("Editor", body));
9446
9938
  });
9447
9939
  app.post("/editor/create", editorBodyLimit, async (c) => {
9940
+ if (!featureEnabled("editor"))
9941
+ return c.html(rejectPage(featureDisabledMessage("editor")), 404);
9448
9942
  if (crossOriginRejected(c))
9449
9943
  return c.html(rejectPage("cross-origin request rejected"), 403);
9450
9944
  const form = await c.req.parseBody();
@@ -9467,6 +9961,8 @@ function mountEditor(app) {
9467
9961
  return c.html(await renderResult(res));
9468
9962
  });
9469
9963
  app.post("/editor/amend", editorBodyLimit, async (c) => {
9964
+ if (!featureEnabled("editor"))
9965
+ return c.html(rejectPage(featureDisabledMessage("editor")), 404);
9470
9966
  if (crossOriginRejected(c))
9471
9967
  return c.html(rejectPage("cross-origin request rejected"), 403);
9472
9968
  const form = await c.req.parseBody();
@@ -9489,6 +9985,7 @@ function mountEditor(app) {
9489
9985
  var styleTag4, MAX_EDITOR_BODY_BYTES, editorBodyLimit;
9490
9986
  var init_editor = __esm(() => {
9491
9987
  init_src();
9988
+ init_components();
9492
9989
  init_nav();
9493
9990
  init_styles();
9494
9991
  styleTag4 = raw4(`<style>${styles_default}</style>`);
@@ -9571,7 +10068,7 @@ var init_propagation3 = __esm(() => {
9571
10068
  import { html as html7, raw as raw6 } from "hono/html";
9572
10069
  function mountProvenance(app) {
9573
10070
  app.get("/provenance", async (c) => {
9574
- if (!PROVENANCE_ENABLED)
10071
+ if (!featureEnabled("provenance"))
9575
10072
  return c.html(comingSoonDoc("provenance", "Provenance matrix"));
9576
10073
  const text = await apiText(app, "/api/provenance?resolve=1");
9577
10074
  let body;
@@ -9604,8 +10101,9 @@ function mountProvenance(app) {
9604
10101
  </html>`);
9605
10102
  });
9606
10103
  }
9607
- var styleTag6, PROVENANCE_ENABLED = false;
10104
+ var styleTag6;
9608
10105
  var init_provenance2 = __esm(() => {
10106
+ init_src();
9609
10107
  init_components();
9610
10108
  init_data();
9611
10109
  init_nav();
@@ -9617,7 +10115,7 @@ var init_provenance2 = __esm(() => {
9617
10115
  import { html as html8, raw as raw7 } from "hono/html";
9618
10116
  function mountQuery(app) {
9619
10117
  app.get("/query", async (c) => {
9620
- if (!QUERY_ENABLED)
10118
+ if (!featureEnabled("query"))
9621
10119
  return c.html(comingSoonDoc("query", "Query"));
9622
10120
  const q = (c.req.query("q") ?? "").trim();
9623
10121
  const form = html8`
@@ -9703,8 +10201,9 @@ function mountQuery(app) {
9703
10201
  </html>`);
9704
10202
  });
9705
10203
  }
9706
- var styleTag7, QUERY_ENABLED = false;
10204
+ var styleTag7;
9707
10205
  var init_query2 = __esm(() => {
10206
+ init_src();
9708
10207
  init_components();
9709
10208
  init_data();
9710
10209
  init_nav();
@@ -9716,7 +10215,7 @@ var init_query2 = __esm(() => {
9716
10215
  import { html as html9, raw as raw8 } from "hono/html";
9717
10216
  function mountRelations(app) {
9718
10217
  app.get("/relations", async (c) => {
9719
- if (!RELATIONS_ENABLED)
10218
+ if (!featureEnabled("relations"))
9720
10219
  return c.html(comingSoonDoc("relations", "Relates graph"));
9721
10220
  const rows = await apiJson(app, "/api/relations");
9722
10221
  let body;
@@ -9754,8 +10253,9 @@ function mountRelations(app) {
9754
10253
  </html>`);
9755
10254
  });
9756
10255
  }
9757
- var styleTag8, RELATIONS_ENABLED = false;
10256
+ var styleTag8;
9758
10257
  var init_relations2 = __esm(() => {
10258
+ init_src();
9759
10259
  init_components();
9760
10260
  init_data();
9761
10261
  init_nav();
@@ -9829,7 +10329,7 @@ function verPill(r, versioned) {
9829
10329
  return html10`<span class="ver-pill ${versioned ? "versioned" : ""}" title="domain spec version (envelope)">v${r.spec_version}</span>`;
9830
10330
  }
9831
10331
  function chainVerLabel(v) {
9832
- const dead = v.status === "Superseded" || v.status === "Retired";
10332
+ const dead = v.status === "Superseded" || v.status === "Deprecated";
9833
10333
  if (dead && v.superseded_at_version != null) {
9834
10334
  return html10`<span class="chain-ver">superseded at v${v.superseded_at_version}</span>`;
9835
10335
  }
@@ -9972,8 +10472,8 @@ function mountRequirements(app) {
9972
10472
  const reqs = await apiJson(app, "/api/requirements");
9973
10473
  const coverage = await apiJson(app, "/api/coverage");
9974
10474
  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;
10475
+ const listed = showAll ? reqs : reqs.filter((r) => r.status !== "Superseded" && r.status !== "Deprecated");
10476
+ const hiddenCount = reqs.filter((r) => r.status === "Superseded" || r.status === "Deprecated").length;
9977
10477
  const tagLists = await Promise.all(listed.map((r) => apiJson(app, `/api/resolve?req=${encodeURIComponent(r.id)}`)));
9978
10478
  const tagsById = new Map(listed.map((r, i) => [r.id, tagLists[i] ?? []]));
9979
10479
  const { domains, defaultId } = buildDomains(reqs, listed, coverage, tagsById);
@@ -10249,7 +10749,7 @@ var init_server = __esm(() => {
10249
10749
 
10250
10750
  // src/server/api.ts
10251
10751
  import { existsSync as existsSync17 } from "fs";
10252
- import { join as join20 } from "path";
10752
+ import { join as join24 } from "path";
10253
10753
  function hasTraversalSegment(p) {
10254
10754
  return p.split(/[/\\]/).some((seg) => seg === "..");
10255
10755
  }
@@ -10356,7 +10856,7 @@ function resolveCreateTarget(c, platformDir, body) {
10356
10856
  return { ok: false, res: c.json({ error: "unknown domain key" }, 400) };
10357
10857
  }
10358
10858
  const relFile = `spec-engine/${key}/SPEC.json`;
10359
- const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
10859
+ const specPath = join24(platformDir, "spec-engine", key, "SPEC.json");
10360
10860
  if (!existsSync17(specPath))
10361
10861
  return { ok: false, res: c.json({ error: "not found" }, 404) };
10362
10862
  return { ok: true, key, relFile, specPath };
@@ -10376,7 +10876,7 @@ function buildRequirement(body, id) {
10376
10876
  }
10377
10877
  async function locateRequirement(platformDir, id) {
10378
10878
  for (const key of listDomainKeys(platformDir)) {
10379
- const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
10879
+ const specPath = join24(platformDir, "spec-engine", key, "SPEC.json");
10380
10880
  const relFile = `spec-engine/${key}/SPEC.json`;
10381
10881
  let domain2;
10382
10882
  try {
@@ -10406,6 +10906,40 @@ function applyAmendFields(req, body) {
10406
10906
  req.livesIn = toLivesIn(body.livesIn);
10407
10907
  return true;
10408
10908
  }
10909
+ function hasAmendFields(body) {
10910
+ return typeof body.statement === "string" || "why" in body || "livesIn" in body;
10911
+ }
10912
+ async function amendGateRejection(c, platformDir, storage2, id, req) {
10913
+ const rawStatus = typeof req.status === "string" ? req.status : "";
10914
+ const statusLc = rawStatus.toLowerCase();
10915
+ if (statusLc !== "active" && statusLc !== "draft") {
10916
+ return c.json({
10917
+ error: `${id} is ${rawStatus} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`
10918
+ }, 409);
10919
+ }
10920
+ if (statusLc === "active") {
10921
+ await runIndex({ platformDir, storage: storage2 });
10922
+ const bound = storage2.listTags({ req_id: id }).filter((t) => t.kind === "implements" || t.kind === "verifies");
10923
+ if (bound.length > 0) {
10924
+ const site = bound[0];
10925
+ return c.json({
10926
+ 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.`
10927
+ }, 409);
10928
+ }
10929
+ }
10930
+ return null;
10931
+ }
10932
+ function searchFtsResponse(c, storage2, q, limit) {
10933
+ try {
10934
+ return c.json(storage2.searchFts(q, limit));
10935
+ } catch (e) {
10936
+ const msg = e instanceof Error ? e.message : String(e);
10937
+ if (msg.startsWith(FTS_SYNTAX_ERROR_PREFIX)) {
10938
+ return c.json({ error: "FTS5 grammar error; wrap phrases in double quotes" }, 400);
10939
+ }
10940
+ throw e;
10941
+ }
10942
+ }
10409
10943
  function parseQueryLimit(c, rawLimit) {
10410
10944
  if (!POSITIVE_INT_RE.test(rawLimit)) {
10411
10945
  return {
@@ -10449,6 +10983,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10449
10983
  return row ? c.json(row) : c.json({ error: "not found" }, 404);
10450
10984
  }));
10451
10985
  app.post("/api/requirements", guarded(async function createRequirement(c) {
10986
+ if (!featureEnabled("editor")) {
10987
+ return c.json({ error: featureDisabledMessage("editor") }, 404);
10988
+ }
10452
10989
  const originReject = rejectCrossOrigin(c);
10453
10990
  if (originReject)
10454
10991
  return originReject;
@@ -10483,6 +11020,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10483
11020
  });
10484
11021
  }));
10485
11022
  app.put("/api/requirements/:id", guarded(async function amendRequirement(c) {
11023
+ if (!featureEnabled("editor")) {
11024
+ return c.json({ error: featureDisabledMessage("editor") }, 404);
11025
+ }
10486
11026
  const originReject = rejectCrossOrigin(c);
10487
11027
  if (originReject)
10488
11028
  return originReject;
@@ -10501,9 +11041,13 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10501
11041
  if (found.kind === "invalid") {
10502
11042
  return c.json({ error: "INVALID_DOMAIN_FILE", detail: `${found.relFile} is not valid JSON` }, 400);
10503
11043
  }
10504
- if (!applyAmendFields(found.req, body)) {
11044
+ if (!hasAmendFields(body)) {
10505
11045
  return c.json({ error: "nothing to amend \u2014 provide statement, why, or livesIn" }, 400);
10506
11046
  }
11047
+ const gateReject = await amendGateRejection(c, platformDir, storage2, id, found.req);
11048
+ if (gateReject)
11049
+ return gateReject;
11050
+ applyAmendFields(found.req, body);
10507
11051
  found.domain.updated = localToday();
10508
11052
  const res = await validateAndWrite(found.specPath, found.domain, found.relFile);
10509
11053
  if (!res.ok) {
@@ -10515,6 +11059,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10515
11059
  }));
10516
11060
  app.get("/api/propagation/:id", guarded((c) => c.json(storage2.propagationFor(c.req.param("id") ?? ""))));
10517
11061
  app.get("/api/query", guarded((c) => {
11062
+ if (!featureEnabled("query")) {
11063
+ return c.json({ error: featureDisabledMessage("query") }, 404);
11064
+ }
10518
11065
  const q = (c.req.query("q") ?? "").trim();
10519
11066
  if (!q) {
10520
11067
  return c.json({ error: "q is required (non-empty FTS5 MATCH query)" }, 400);
@@ -10523,17 +11070,12 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10523
11070
  const parsedLimit = parseQueryLimit(c, rawLimit);
10524
11071
  if (!parsedLimit.ok)
10525
11072
  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
- }
11073
+ return searchFtsResponse(c, storage2, q, parsedLimit.limit);
10535
11074
  }));
10536
11075
  app.get("/api/relations", guarded((c) => {
11076
+ if (!featureEnabled("relations")) {
11077
+ return c.json({ error: featureDisabledMessage("relations") }, 404);
11078
+ }
10537
11079
  const format = c.req.query("format");
10538
11080
  if (format !== undefined && format !== "mermaid") {
10539
11081
  return c.json({ error: 'format must be "mermaid" when provided' }, 400);
@@ -10545,6 +11087,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10545
11087
  return c.json(sortRelations(rows));
10546
11088
  }));
10547
11089
  app.get("/api/provenance", guarded(async (c) => {
11090
+ if (!featureEnabled("provenance")) {
11091
+ return c.json({ error: featureDisabledMessage("provenance") }, 404);
11092
+ }
10548
11093
  const resolveParam = c.req.query("resolve");
10549
11094
  if (resolveParam !== undefined && resolveParam !== "1") {
10550
11095
  return c.json({ error: 'resolve must be "1" when provided' }, 400);
@@ -10556,7 +11101,15 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
10556
11101
  }
10557
11102
  return c.json(rows);
10558
11103
  }));
10559
- app.get("/api/provenance/:issue", guarded((c) => c.json(storage2.provenanceByIssue(c.req.param("issue") ?? ""))));
11104
+ app.get("/api/provenance/by-issue", guarded((c) => {
11105
+ if (!featureEnabled("provenance")) {
11106
+ return c.json({ error: featureDisabledMessage("provenance") }, 404);
11107
+ }
11108
+ const issue = (c.req.query("issue") ?? "").trim();
11109
+ if (issue === "")
11110
+ return c.json({ error: "issue is required (non-empty)" }, 400);
11111
+ return c.json(storage2.provenanceByIssue(issue));
11112
+ }));
10560
11113
  app.get("/api/resolve", guarded((c) => {
10561
11114
  const reqParam = c.req.query("req");
10562
11115
  if (reqParam !== undefined)
@@ -10597,8 +11150,8 @@ __export(exports_serve, {
10597
11150
  composeServeApp: () => composeServeApp
10598
11151
  });
10599
11152
  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";
11153
+ import { dirname as dirname11, resolve as resolve26 } from "path";
11154
+ import { defineCommand as defineCommand21 } from "citty";
10602
11155
  import { Hono as Hono2 } from "hono";
10603
11156
  function composeServeApp(storage2, platformDir = process.cwd()) {
10604
11157
  const app = new Hono2;
@@ -10646,7 +11199,7 @@ var init_serve = __esm(() => {
10646
11199
  init_api();
10647
11200
  init_sqlite();
10648
11201
  init__shared();
10649
- serveCommand = defineCommand20({
11202
+ serveCommand = defineCommand21({
10650
11203
  meta: {
10651
11204
  name: "serve",
10652
11205
  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 +11245,7 @@ var init_serve = __esm(() => {
10692
11245
  process.exit(EXIT.USAGE);
10693
11246
  return;
10694
11247
  }
10695
- const platformDir = resolve25(args.platformDir ?? process.cwd());
11248
+ const platformDir = resolve26(args.platformDir ?? process.cwd());
10696
11249
  const outArg = args.out;
10697
11250
  const dbPath = resolveDbPath(platformDir, outArg);
10698
11251
  if (outArg)
@@ -10743,18 +11296,20 @@ var init_serve = __esm(() => {
10743
11296
  // src/commands/docs.ts
10744
11297
  var exports_docs = {};
10745
11298
  __export(exports_docs, {
11299
+ runProbe: () => runProbe2,
10746
11300
  resolveDocsRootFrom: () => resolveDocsRootFrom,
10747
11301
  resolveDocsRoot: () => resolveDocsRoot,
11302
+ exitNoDocsRoot: () => exitNoDocsRoot,
10748
11303
  docsCommand: () => docsCommand,
10749
11304
  createDocsFetchHandler: () => createDocsFetchHandler
10750
11305
  });
10751
- import { existsSync as existsSync19, statSync as statSync6 } from "fs";
10752
- import { join as join21, normalize, sep as sep4 } from "path";
11306
+ import { existsSync as existsSync19, statSync as statSync7 } from "fs";
11307
+ import { join as join25, normalize, sep as sep4 } from "path";
10753
11308
  import { fileURLToPath } from "url";
10754
- import { defineCommand as defineCommand21 } from "citty";
11309
+ import { defineCommand as defineCommand22 } from "citty";
10755
11310
  function resolveDocsRootFrom(candidates) {
10756
11311
  for (const candidate of candidates) {
10757
- if (existsSync19(join21(candidate, "index.html")))
11312
+ if (existsSync19(join25(candidate, "index.html")))
10758
11313
  return candidate;
10759
11314
  }
10760
11315
  return null;
@@ -10778,16 +11333,16 @@ function createDocsFetchHandler(root) {
10778
11333
  if (pathname.includes("\x00")) {
10779
11334
  return new Response("Bad request", { status: 400 });
10780
11335
  }
10781
- const target = normalize(join21(normalRoot, pathname));
11336
+ const target = normalize(join25(normalRoot, pathname));
10782
11337
  if (target !== normalRoot && !target.startsWith(rootPrefix)) {
10783
11338
  return new Response("Not found", { status: 404 });
10784
11339
  }
10785
- for (const candidate of [target, join21(target, "index.html"), `${target}.html`]) {
10786
- if (existsSync19(candidate) && statSync6(candidate).isFile()) {
11340
+ for (const candidate of [target, join25(target, "index.html"), `${target}.html`]) {
11341
+ if (existsSync19(candidate) && statSync7(candidate).isFile()) {
10787
11342
  return new Response(Bun.file(candidate));
10788
11343
  }
10789
11344
  }
10790
- const notFoundPage = join21(normalRoot, "404.html");
11345
+ const notFoundPage = join25(normalRoot, "404.html");
10791
11346
  if (existsSync19(notFoundPage)) {
10792
11347
  return new Response(Bun.file(notFoundPage), { status: 404 });
10793
11348
  }
@@ -10822,10 +11377,14 @@ async function runProbe2(root) {
10822
11377
  server.stop();
10823
11378
  process.exit(EXIT.OK);
10824
11379
  }
11380
+ function exitNoDocsRoot() {
11381
+ 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.");
11382
+ process.exit(EXIT.USAGE);
11383
+ }
10825
11384
  var docsCommand;
10826
11385
  var init_docs2 = __esm(() => {
10827
11386
  init_constants();
10828
- docsCommand = defineCommand21({
11387
+ docsCommand = defineCommand22({
10829
11388
  meta: {
10830
11389
  name: "docs",
10831
11390
  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 +11403,7 @@ var init_docs2 = __esm(() => {
10844
11403
  async run({ args }) {
10845
11404
  const root = resolveDocsRoot();
10846
11405
  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);
11406
+ exitNoDocsRoot();
10849
11407
  return;
10850
11408
  }
10851
11409
  if (args.probe) {
@@ -10887,10 +11445,10 @@ __export(exports___smoke, {
10887
11445
  schemaSmokeCommand: () => schemaSmokeCommand,
10888
11446
  schemaMismatchSmokeCommand: () => schemaMismatchSmokeCommand
10889
11447
  });
10890
- import { defineCommand as defineCommand22 } from "citty";
11448
+ import { defineCommand as defineCommand23 } from "citty";
10891
11449
  var schemaSmokeCommand, schemaMismatchSmokeCommand;
10892
11450
  var init___smoke = __esm(() => {
10893
- schemaSmokeCommand = defineCommand22({
11451
+ schemaSmokeCommand = defineCommand23({
10894
11452
  meta: {
10895
11453
  name: "__schema-smoke",
10896
11454
  description: "(hidden CI) open a fresh DB, exec DDL, assert the schema shape",
@@ -10933,7 +11491,7 @@ var init___smoke = __esm(() => {
10933
11491
  console.log("__schema-smoke OK");
10934
11492
  }
10935
11493
  });
10936
- schemaMismatchSmokeCommand = defineCommand22({
11494
+ schemaMismatchSmokeCommand = defineCommand23({
10937
11495
  meta: {
10938
11496
  name: "__schema-mismatch-smoke",
10939
11497
  description: "(hidden CI) corrupt _schema_version, re-open, assert silent rebuild",
@@ -10958,11 +11516,11 @@ var init___smoke = __esm(() => {
10958
11516
  });
10959
11517
 
10960
11518
  // src/cli.ts
10961
- import { defineCommand as defineCommand23, runMain, showUsage } from "citty";
11519
+ import { defineCommand as defineCommand24, runMain, showUsage } from "citty";
10962
11520
  // package.json
10963
11521
  var package_default = {
10964
11522
  name: "@spec-engine/spec-engine",
10965
- version: "0.1.0",
11523
+ version: "0.2.0",
10966
11524
  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
11525
  license: "MIT",
10968
11526
  type: "module",
@@ -10972,6 +11530,10 @@ var package_default = {
10972
11530
  files: [
10973
11531
  "dist"
10974
11532
  ],
11533
+ publishConfig: {
11534
+ access: "public",
11535
+ provenance: true
11536
+ },
10975
11537
  engines: {
10976
11538
  bun: ">=1.3.0"
10977
11539
  },
@@ -11010,7 +11572,7 @@ var package_default = {
11010
11572
  };
11011
11573
 
11012
11574
  // src/cli.ts
11013
- var main = defineCommand23({
11575
+ var main = defineCommand24({
11014
11576
  meta: {
11015
11577
  name: "spec",
11016
11578
  version: package_default.version,
@@ -11030,6 +11592,7 @@ var main = defineCommand23({
11030
11592
  term: () => Promise.resolve().then(() => (init_term(), exports_term)).then((m) => m.termCommand),
11031
11593
  glossary: () => Promise.resolve().then(() => (init_glossary(), exports_glossary)).then((m) => m.glossaryCommand),
11032
11594
  supersede: () => Promise.resolve().then(() => (init_supersede(), exports_supersede)).then((m) => m.supersedeCommand),
11595
+ deprecate: () => Promise.resolve().then(() => (init_deprecate(), exports_deprecate)).then((m) => m.deprecateCommand),
11033
11596
  move: () => Promise.resolve().then(() => (init_move(), exports_move)).then((m) => m.moveCommand),
11034
11597
  amend: () => Promise.resolve().then(() => (init_amend(), exports_amend)).then((m) => m.amendCommand),
11035
11598
  mcp: () => Promise.resolve().then(() => (init_mcp2(), exports_mcp)).then((m) => m.mcpCommand),