@spec-engine/spec-engine 0.6.1 → 0.7.1

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.
package/dist/impl.js CHANGED
@@ -20,7 +20,7 @@ var package_default;
20
20
  var init_package = __esm(() => {
21
21
  package_default = {
22
22
  name: "@spec-engine/spec-engine",
23
- version: "0.6.1",
23
+ version: "0.7.1",
24
24
  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).",
25
25
  license: "MIT",
26
26
  type: "module",
@@ -73,18 +73,8 @@ var init_package = __esm(() => {
73
73
  };
74
74
  });
75
75
 
76
- // ../shared/src/config.ts
77
- import { z } from "zod";
78
- var SpecConfigSchema;
79
- var init_config = __esm(() => {
80
- SpecConfigSchema = z.object({
81
- specs: z.string().regex(/^spec-engine@\d+$/, "must be of the form spec-engine@N where N is an integer"),
82
- ignore: z.array(z.string().min(1, "ignore entries must be non-empty strings")).optional()
83
- });
84
- });
85
-
86
76
  // ../shared/src/diagnostics.ts
87
- var DiagnosticCode, NotASpecPlatformError;
77
+ var DiagnosticCode, NotASpecPlatformError, InvalidDomainFilesError;
88
78
  var init_diagnostics = __esm(() => {
89
79
  DiagnosticCode = {
90
80
  DUP_ID: "DUP_ID",
@@ -111,6 +101,9 @@ var init_diagnostics = __esm(() => {
111
101
  UNPROVEN_REQ: "UNPROVEN_REQ",
112
102
  PROOFS_UNCONFIRMED: "PROOFS_UNCONFIRMED",
113
103
  REQUIREMENT_REMOVED: "REQUIREMENT_REMOVED",
104
+ IMPL_LOST: "IMPL_LOST",
105
+ VERIFY_LOST: "VERIFY_LOST",
106
+ SPEC_FILE_DELETED: "SPEC_FILE_DELETED",
114
107
  UNAPPROVED_STATUS_FLIP: "UNAPPROVED_STATUS_FLIP",
115
108
  PARTIAL_PROPAGATION: "PARTIAL_PROPAGATION",
116
109
  MALFORMED_FILE: "MALFORMED_FILE",
@@ -135,10 +128,20 @@ var init_diagnostics = __esm(() => {
135
128
  Object.setPrototypeOf(this, NotASpecPlatformError.prototype);
136
129
  }
137
130
  };
131
+ InvalidDomainFilesError = class InvalidDomainFilesError extends Error {
132
+ diagnostics;
133
+ constructor(diagnostics) {
134
+ super(diagnostics.map((d) => `${d.source_file ?? ""}: ${d.detail}`).join(`
135
+ `));
136
+ this.name = "InvalidDomainFilesError";
137
+ this.diagnostics = diagnostics;
138
+ Object.setPrototypeOf(this, InvalidDomainFilesError.prototype);
139
+ }
140
+ };
138
141
  });
139
142
 
140
143
  // ../shared/src/domain.ts
141
- import { z as z2 } from "zod";
144
+ import { z } from "zod";
142
145
  function reqIdForIssuePath(input, path) {
143
146
  if (path.length < 2 || path[0] !== "requirements" || typeof path[1] !== "number") {
144
147
  return null;
@@ -221,62 +224,215 @@ function orderDomain(d) {
221
224
  }))
222
225
  };
223
226
  }
227
+ function isDefaultValue(v) {
228
+ return v === undefined || v === null || Array.isArray(v) && v.length === 0;
229
+ }
230
+ function patchFields(raw, before, next) {
231
+ for (const key of new Set([...Object.keys(before), ...Object.keys(next)])) {
232
+ if (key === "requirements")
233
+ continue;
234
+ const v = next[key];
235
+ if (sameValue(before[key], v))
236
+ continue;
237
+ if (v === undefined)
238
+ delete raw[key];
239
+ else if (!isDefaultValue(v) || key in raw)
240
+ raw[key] = v;
241
+ }
242
+ }
243
+ function shapeLike(template, next) {
244
+ if (template === undefined)
245
+ return next;
246
+ const out = {};
247
+ for (const key of Object.keys(template)) {
248
+ if (next[key] !== undefined)
249
+ out[key] = next[key];
250
+ }
251
+ for (const [key, v] of Object.entries(next)) {
252
+ if (!(key in out) && !isDefaultValue(v))
253
+ out[key] = v;
254
+ }
255
+ return out;
256
+ }
257
+ function preserveShape(raw, rawDomain, domain) {
258
+ const before = orderDomain(rawDomain);
259
+ const next = orderDomain(domain);
260
+ patchFields(raw, before, next);
261
+ const rawEntries = raw.requirements;
262
+ const byId = new Map;
263
+ rawDomain.requirements.forEach((r, i) => {
264
+ byId.set(r.id, [...byId.get(r.id) ?? [], i]);
265
+ });
266
+ const template = rawEntries[rawEntries.length - 1];
267
+ raw.requirements = next.requirements.map((entry, i) => {
268
+ const idx = byId.get(domain.requirements[i]?.id ?? "")?.shift();
269
+ if (idx === undefined)
270
+ return shapeLike(template, entry);
271
+ const rawEntry = rawEntries[idx];
272
+ patchFields(rawEntry, before.requirements[idx], entry);
273
+ return rawEntry;
274
+ });
275
+ return raw;
276
+ }
277
+ function verifyRoundTrip(serialized, domain, sourceFile) {
278
+ const reparsed = parseDomainText(serialized, sourceFile);
279
+ if (!reparsed.ok)
280
+ return { ok: false, diagnostics: reparsed.diagnostics };
281
+ if (sameValue(orderDomain(reparsed.data), orderDomain(domain)))
282
+ return { ok: true };
283
+ return {
284
+ ok: false,
285
+ diagnostics: [
286
+ {
287
+ code: DiagnosticCode.INVALID_DOMAIN_FILE,
288
+ source_file: sourceFile,
289
+ line: 0,
290
+ repo: null,
291
+ req_id: null,
292
+ detail: "the shape-preserving rewrite does not re-parse to the validated domain",
293
+ severity: "error"
294
+ }
295
+ ]
296
+ };
297
+ }
298
+ async function readExisting(path, sourceFile) {
299
+ const file = Bun.file(path);
300
+ if (!await file.exists())
301
+ return null;
302
+ let raw;
303
+ try {
304
+ raw = JSON.parse(await file.text());
305
+ } catch {
306
+ return null;
307
+ }
308
+ const validated = validateDomainFile(raw, sourceFile);
309
+ return validated.ok ? { raw, data: validated.data } : null;
310
+ }
224
311
  async function validateAndWrite(path, domain, sourceFile) {
225
- const validated = validateDomainFile(domain, sourceFile ?? path);
312
+ const src = sourceFile ?? path;
313
+ const validated = validateDomainFile(domain, src);
226
314
  if (!validated.ok) {
227
315
  return { ok: false, diagnostics: validated.diagnostics };
228
316
  }
229
- const serialized = `${JSON.stringify(orderDomain(validated.data), null, 2)}
317
+ const existing = await readExisting(path, src);
318
+ if (existing === null) {
319
+ await Bun.write(path, `${JSON.stringify(orderDomain(validated.data), null, 2)}
320
+ `);
321
+ return { ok: true };
322
+ }
323
+ const shaped = preserveShape(existing.raw, existing.data, validated.data);
324
+ const serialized = `${JSON.stringify(shaped, null, 2)}
230
325
  `;
326
+ const verdict = verifyRoundTrip(serialized, validated.data, src);
327
+ if (!verdict.ok)
328
+ return verdict;
231
329
  await Bun.write(path, serialized);
232
330
  return { ok: true };
233
331
  }
234
- var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, SpecRequirementSchema, SpecDomainSchema;
332
+ var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, SpecRequirementSchema, SpecDomainSchema, sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
235
333
  var init_domain = __esm(() => {
236
334
  init_diagnostics();
237
335
  ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
238
336
  KEY_RE = /^[A-Z][A-Z0-9]*$/;
239
- SpecIssueSchema = z2.object({
240
- role: z2.string().min(1),
241
- id: z2.string().min(1)
337
+ SpecIssueSchema = z.object({
338
+ role: z.string().min(1),
339
+ id: z.string().min(1)
340
+ }).strict();
341
+ SpecCiteSchema = z.object({
342
+ term: z.string(),
343
+ pinned: z.number().int()
242
344
  }).strict();
243
- SpecCiteSchema = z2.object({
244
- term: z2.string(),
245
- pinned: z2.number().int()
345
+ SpecRequirementSchema = z.object({
346
+ id: z.string().regex(ID_RE),
347
+ status: z.string().min(1),
348
+ statement: z.string().min(1),
349
+ why: z.string().nullable().optional(),
350
+ supersedes: z.string().nullable().optional(),
351
+ supersededBy: z.string().nullable().optional(),
352
+ relates: z.array(z.string()).optional().default([]),
353
+ livesIn: z.array(z.string()).optional().default([]),
354
+ issues: z.array(SpecIssueSchema).optional().default([]),
355
+ changedAtVersion: z.number().int().optional(),
356
+ supersededAtVersion: z.number().int().optional(),
357
+ deprecatedReason: z.string().optional(),
358
+ term: z.string().optional(),
359
+ aliases: z.array(z.string()).optional().default([]),
360
+ cites: z.array(SpecCiteSchema).optional().default([]),
361
+ section: z.string().nullable().optional()
246
362
  }).strict();
247
- SpecRequirementSchema = z2.object({
248
- id: z2.string().regex(ID_RE),
249
- status: z2.string().min(1),
250
- statement: z2.string().min(1),
251
- why: z2.string().nullable().optional(),
252
- supersedes: z2.string().nullable().optional(),
253
- supersededBy: z2.string().nullable().optional(),
254
- relates: z2.array(z2.string()).optional().default([]),
255
- livesIn: z2.array(z2.string()).optional().default([]),
256
- issues: z2.array(SpecIssueSchema).optional().default([]),
257
- changedAtVersion: z2.number().int().optional(),
258
- supersededAtVersion: z2.number().int().optional(),
259
- deprecatedReason: z2.string().optional(),
260
- term: z2.string().optional(),
261
- aliases: z2.array(z2.string()).optional().default([]),
262
- cites: z2.array(SpecCiteSchema).optional().default([]),
263
- section: z2.string().nullable().optional()
363
+ SpecDomainSchema = z.object({
364
+ key: z.string().regex(KEY_RE),
365
+ owner: z.string().nullable().optional(),
366
+ specVersion: z.number().int().positive().optional(),
367
+ updated: z.string(),
368
+ scope: z.string().nullable().optional(),
369
+ grammar: z.enum(["ears", "freeform"]).optional(),
370
+ grammarSeverity: z.enum(["warning", "error"]).optional(),
371
+ requirements: z.array(SpecRequirementSchema)
264
372
  }).strict();
265
- SpecDomainSchema = z2.object({
266
- key: z2.string().regex(KEY_RE),
267
- owner: z2.string().nullable().optional(),
268
- specVersion: z2.number().int().positive().optional(),
269
- updated: z2.string(),
270
- scope: z2.string().nullable().optional(),
271
- grammar: z2.enum(["ears", "freeform"]).optional(),
272
- grammarSeverity: z2.enum(["warning", "error"]).optional(),
273
- requirements: z2.array(SpecRequirementSchema)
373
+ });
374
+
375
+ // ../shared/src/checkConfig.ts
376
+ import { z as z2 } from "zod";
377
+ function parseCheckConfigText(text, sourceFile) {
378
+ let parsed;
379
+ try {
380
+ parsed = JSON.parse(text);
381
+ } catch (err) {
382
+ const msg = err instanceof Error ? err.message : String(err);
383
+ return { ok: false, detail: `${sourceFile}: not valid JSON: ${msg}` };
384
+ }
385
+ const result = CheckConfigSchema.safeParse(parsed);
386
+ if (result.success)
387
+ return { ok: true, config: result.data };
388
+ const issue = result.error.issues[0];
389
+ const path = issue && issue.path.length > 0 ? issue.path.join(".") : "(root)";
390
+ return { ok: false, detail: `${sourceFile}: ${path}: ${issue?.message ?? "invalid"}` };
391
+ }
392
+ var CODES, matchEntry, SeverityRuleSchema, CheckConfigSchema;
393
+ var init_checkConfig = __esm(() => {
394
+ init_diagnostics();
395
+ init_domain();
396
+ CODES = Object.values(DiagnosticCode);
397
+ matchEntry = z2.string().refine((s) => KEY_RE.test(s) || ID_RE.test(s), "must be a domain key or a requirement id");
398
+ SeverityRuleSchema = z2.object({
399
+ code: z2.enum(CODES),
400
+ severity: z2.enum(["error", "warning", "off"]),
401
+ match: z2.array(matchEntry).optional()
274
402
  }).strict();
403
+ CheckConfigSchema = z2.object({ severity: z2.array(SeverityRuleSchema) }).strict();
404
+ });
405
+
406
+ // ../shared/src/config.ts
407
+ import { z as z3 } from "zod";
408
+ var SpecConfigSchema;
409
+ var init_config = __esm(() => {
410
+ SpecConfigSchema = z3.object({
411
+ specs: z3.string().regex(/^spec-engine@\d+$/, "must be of the form spec-engine@N where N is an integer"),
412
+ ignore: z3.array(z3.string().min(1, "ignore entries must be non-empty strings")).optional()
413
+ });
275
414
  });
276
415
 
277
416
  // ../shared/src/ears.ts
278
- function reject(problem) {
279
- return { ok: false, problem, expected: EARS_EXPECTED };
417
+ function reject(problem, expected = SHAPE_EXPECTED) {
418
+ return { ok: false, problem, expected };
419
+ }
420
+ function onePromiseProblem(statement, response) {
421
+ if (/;\s/.test(response)) {
422
+ return 'joins a second clause with "; " after "shall" \u2014 split it into its own requirement';
423
+ }
424
+ if (/,\s+and\s+(?!shall\b)\S[^,;]*?\s+shall\b/i.test(response)) {
425
+ return 'joins a second clause with its own subject (", and <subject> shall") \u2014 split it into its own requirement';
426
+ }
427
+ const rationale = statement.match(RATIONALE_RE);
428
+ if (rationale) {
429
+ return `carries a rationale ("${rationale[1]}") \u2014 the reason belongs in the why field`;
430
+ }
431
+ return null;
432
+ }
433
+ function lint(clauses, statement) {
434
+ const problem = onePromiseProblem(statement, clauses.response);
435
+ return problem === null ? { ok: true, clauses } : reject(problem, ONE_PROMISE_EXPECTED);
280
436
  }
281
437
  function parseEarsStatement(raw) {
282
438
  const statement = raw.trim();
@@ -291,15 +447,12 @@ function parseEarsStatement(raw) {
291
447
  const m2 = statement.match(re);
292
448
  if (m2) {
293
449
  const [, condition = "", system = "", response = ""] = m2;
294
- return {
295
- ok: true,
296
- clauses: {
297
- pattern,
298
- condition: condition.trim(),
299
- system: system.trim().replace(/^the\s+/i, ""),
300
- response: response.trim()
301
- }
302
- };
450
+ return lint({
451
+ pattern,
452
+ condition: condition.trim(),
453
+ system: system.trim().replace(/^the\s+/i, ""),
454
+ response: response.trim()
455
+ }, statement);
303
456
  }
304
457
  if (keyword === "If" && !/,\s*then\s/.test(statement)) {
305
458
  return reject('starts with "If" but has no ", then" \u2014 the failure clause never hands over to the system');
@@ -312,19 +465,16 @@ function parseEarsStatement(raw) {
312
465
  if (/,/.test(system)) {
313
466
  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');
314
467
  }
315
- return {
316
- ok: true,
317
- clauses: {
318
- pattern: "always",
319
- condition: null,
320
- system: system.trim().replace(/^the\s+/i, ""),
321
- response: response.trim()
322
- }
323
- };
468
+ return lint({
469
+ pattern: "always",
470
+ condition: null,
471
+ system: system.trim().replace(/^the\s+/i, ""),
472
+ response: response.trim()
473
+ }, statement);
324
474
  }
325
475
  return reject('nothing follows "shall" \u2014 the observable result is missing');
326
476
  }
327
- var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE;
477
+ var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE, SHAPE_EXPECTED, ONE_PROMISE_EXPECTED = "Expected one promise per statement: a second behavior is its own requirement, and the reason goes in the why.", RATIONALE_RE;
328
478
  var init_ears = __esm(() => {
329
479
  EARS_EXPECTED = [
330
480
  'When <trigger>, the <system> shall <result> \u2014 e.g. "When an unknown id is passed, spec resolve shall print [] and exit 0."',
@@ -341,6 +491,9 @@ var init_ears = __esm(() => {
341
491
  { keyword: "Where", pattern: "where", re: /^Where\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s }
342
492
  ];
343
493
  ALWAYS_RE = /^(.+?)\s+shall\s+(.+)$/s;
494
+ SHAPE_EXPECTED = `Expected one of:
495
+ ${EARS_EXPECTED}`;
496
+ RATIONALE_RE = /\b(because|so that|in order to)\b/i;
344
497
  });
345
498
 
346
499
  // ../shared/src/flags.ts
@@ -531,7 +684,8 @@ CREATE INDEX IF NOT EXISTS idx_term_citations_term ON term_citations(term_id);
531
684
  -- supersedes-via | amends-via), validated at parse time, never as a DB
532
685
  -- constraint. Exactly one index, on req_id; never on issue_id, which would
533
686
  -- treat the opaque payload as a lookup key.
534
- -- @spec PROV-004
687
+ -- @spec PROV-005
688
+ -- @spec PROV-006
535
689
  CREATE TABLE IF NOT EXISTS provenance (
536
690
  id INTEGER PRIMARY KEY AUTOINCREMENT,
537
691
  req_id TEXT NOT NULL,
@@ -677,6 +831,7 @@ var init_storage = __esm(() => {
677
831
 
678
832
  // ../shared/src/index.ts
679
833
  var init_src = __esm(() => {
834
+ init_checkConfig();
680
835
  init_config();
681
836
  init_diagnostics();
682
837
  init_domain();
@@ -949,8 +1104,7 @@ import { readdir } from "fs/promises";
949
1104
  import { join as join2 } from "path";
950
1105
  function isPathIgnored(path, extra = []) {
951
1106
  const anchored = `/${path}`;
952
- const tokens = extra.length === 0 ? IGNORE_SUBSTR : [...IGNORE_SUBSTR, ...extra];
953
- return tokens.some((t) => anchored.includes(`/${t.endsWith("/") ? t : `${t}/`}`));
1107
+ return IGNORE_SUBSTR.some((t) => anchored.includes(`/${t}`)) || extra.some((t) => path.startsWith(t.endsWith("/") ? t : `${t}/`));
954
1108
  }
955
1109
  async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
956
1110
  const absDir = relDir ? join2(rootDir, relDir) : rootDir;
@@ -979,15 +1133,19 @@ async function walkFiles(rootDir, keep, extraIgnore) {
979
1133
  out.sort();
980
1134
  return out;
981
1135
  }
1136
+ function isDockerfile(name) {
1137
+ return name === "Dockerfile" || name.startsWith("Dockerfile.") && name.length > 11;
1138
+ }
1139
+ function isTagSource(rel, exts = DEFAULT_EXTS) {
1140
+ const name = rel.slice(rel.lastIndexOf("/") + 1);
1141
+ const dot = name.lastIndexOf(".");
1142
+ return isDockerfile(name) || dot > 0 && exts.includes(name.slice(dot + 1));
1143
+ }
982
1144
  async function findDomainJsonFiles(canonicalDir) {
983
1145
  return walkFiles(canonicalDir, (rel) => rel === "SPEC.json" || rel.endsWith("/SPEC.json"), []);
984
1146
  }
985
1147
  async function findCodeFiles(repoDir, exts = DEFAULT_EXTS, extraIgnore = []) {
986
- const extSet = new Set(exts);
987
- return walkFiles(repoDir, (rel) => {
988
- const dot = rel.lastIndexOf(".");
989
- return dot > 0 && extSet.has(rel.slice(dot + 1));
990
- }, extraIgnore);
1148
+ return walkFiles(repoDir, (rel) => isTagSource(rel, exts), extraIgnore);
991
1149
  }
992
1150
  async function findDocFiles(repoDir, extraIgnore = []) {
993
1151
  return walkFiles(repoDir, (rel) => rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md"), extraIgnore);
@@ -1017,7 +1175,11 @@ var init_fs = __esm(() => {
1017
1175
  "lua",
1018
1176
  "luau",
1019
1177
  "go",
1020
- "rs"
1178
+ "rs",
1179
+ "tf",
1180
+ "toml",
1181
+ "sql",
1182
+ "css"
1021
1183
  ];
1022
1184
  });
1023
1185
 
@@ -1548,9 +1710,13 @@ SELECT
1548
1710
  t.file AS source_file,
1549
1711
  t.line AS line,
1550
1712
  t.req_id AS req_id,
1551
- ('Tag references Draft requirement ' || t.req_id ||
1552
- ' \u2014 code is bound to a promise nobody approved yet') AS detail,
1553
- 'warning' AS severity
1713
+ CASE WHEN t.kind = 'verifies'
1714
+ THEN ('Test verifies Draft requirement ' || t.req_id ||
1715
+ ' \u2014 it asserts a promise nobody accepted yet')
1716
+ ELSE ('Tag references Draft requirement ' || t.req_id ||
1717
+ ' \u2014 code is bound to a promise nobody accepted yet')
1718
+ END AS detail,
1719
+ CASE WHEN t.kind = 'verifies' THEN 'error' ELSE 'warning' END AS severity
1554
1720
  FROM tags t
1555
1721
  JOIN requirements r ON r.id = t.req_id
1556
1722
  WHERE r.status = 'Draft'
@@ -1580,11 +1746,10 @@ SELECT
1580
1746
  'error' AS severity
1581
1747
  FROM requirements r
1582
1748
  WHERE r.status = 'Active'
1583
- -- code-coverage obligation \u2014 exclude the reserved TERM domain so a migrated
1584
- -- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
1585
- -- mirrors the coverage VIEW exclusion (WHERE r.key != 'TERM') in schema.ts.
1749
+ -- Must stay in step with the coverage view's TERM exclusion in schema.ts.
1586
1750
  -- @spec SCHM-017
1587
1751
  AND r.key != 'TERM'
1752
+ -- A doc paragraph citing a requirement is not an implementation.
1588
1753
  AND NOT EXISTS (
1589
1754
  SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
1590
1755
  )
@@ -1598,11 +1763,10 @@ SELECT
1598
1763
  r.id AS req_id,
1599
1764
  ('Requirement ' || r.id ||
1600
1765
  ' is implemented but has no verifies-kind tag') AS detail,
1601
- 'error' AS severity
1766
+ 'warning' AS severity
1602
1767
  FROM requirements r
1603
1768
  WHERE r.status = 'Active'
1604
- -- never implemented/verified by code, so it must never fire UNVERIFIED_REQ.
1605
- -- Literal 'TERM' only; mirrors Q4 + the coverage VIEW exclusion.
1769
+ -- Must stay in step with Q4 and the coverage view's TERM exclusion.
1606
1770
  AND r.key != 'TERM'
1607
1771
  AND EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'implements')
1608
1772
  AND NOT EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'verifies')
@@ -2445,7 +2609,7 @@ async function runIndex(opts) {
2445
2609
  unpinned,
2446
2610
  diagnostics: platformDiagnostics
2447
2611
  } = await discoverRepos(platformDir);
2448
- const { specs, structuralDiagnostics } = await readSpecs(canonical);
2612
+ const specs = await readSpecs(canonical);
2449
2613
  const { tagHits, docMentionCandidates } = await scanMembers(members);
2450
2614
  const rows = buildRows({
2451
2615
  canonical,
@@ -2453,7 +2617,6 @@ async function runIndex(opts) {
2453
2617
  members,
2454
2618
  specs,
2455
2619
  tagHits,
2456
- structuralDiagnostics,
2457
2620
  unpinned,
2458
2621
  platformDiagnostics
2459
2622
  });
@@ -2475,29 +2638,22 @@ async function runIndex(opts) {
2475
2638
  async function readSpecs(canonical) {
2476
2639
  const jsonPaths = await findDomainJsonFiles(canonical.path);
2477
2640
  const specs = [];
2478
- const structuralDiagnostics = [];
2641
+ const invalid = [];
2479
2642
  for (const rel of jsonPaths) {
2480
2643
  const absPath = join4(canonical.path, rel);
2481
2644
  const text = await Bun.file(absPath).text();
2482
2645
  const platformRelative = `${canonical.name}/${rel}`;
2483
2646
  const fallbackKey = basename2(dirname3(rel));
2484
2647
  const result = parseDomainJsonFile({ text, sourceFile: platformRelative, fallbackKey });
2485
- if (!result.ok) {
2486
- for (const d of result.diagnostics) {
2487
- structuralDiagnostics.push({
2488
- code: d.code,
2489
- source_file: d.source_file ?? platformRelative,
2490
- line: d.line ?? 0,
2491
- req_id: d.req_id,
2492
- detail: d.detail,
2493
- severity: d.severity
2494
- });
2495
- }
2496
- continue;
2497
- }
2498
- specs.push(result.spec);
2648
+ if (result.ok)
2649
+ specs.push(result.spec);
2650
+ else
2651
+ for (const d of result.diagnostics)
2652
+ invalid.push({ ...d, source_file: d.source_file ?? platformRelative });
2499
2653
  }
2500
- return { specs, structuralDiagnostics };
2654
+ if (invalid.length > 0)
2655
+ throw new InvalidDomainFilesError(invalid);
2656
+ return specs;
2501
2657
  }
2502
2658
  function repoExtraIgnore(member) {
2503
2659
  const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
@@ -2549,16 +2705,7 @@ async function scanMembers(members) {
2549
2705
  return { tagHits, docMentionCandidates };
2550
2706
  }
2551
2707
  function buildRows(input) {
2552
- const {
2553
- canonical,
2554
- platformVersion,
2555
- members,
2556
- specs,
2557
- tagHits,
2558
- structuralDiagnostics,
2559
- unpinned,
2560
- platformDiagnostics
2561
- } = input;
2708
+ const { canonical, platformVersion, members, specs, tagHits, unpinned, platformDiagnostics } = input;
2562
2709
  const sortedRepos = sortBy([
2563
2710
  {
2564
2711
  name: canonical.name,
@@ -2582,8 +2729,6 @@ function buildRows(input) {
2582
2729
  const { sortedTermAliases, sortedTermCitations } = buildTermRows(specs, flatRequirements);
2583
2730
  const sortedProvenance = sortBy(specs.flatMap((s) => s.provenance), (p) => `${p.req_id}\x00${p.role}\x00${p.issue_id}\x00${p.source_file}\x00${pad(p.line)}`);
2584
2731
  const diagnostics2 = validateStructure(specs);
2585
- for (const d of structuralDiagnostics)
2586
- diagnostics2.push(d);
2587
2732
  for (const s of unpinned) {
2588
2733
  diagnostics2.push({
2589
2734
  code: DiagnosticCode.NO_SPEC_CONFIG,
@@ -3017,6 +3162,49 @@ var init_init = __esm(() => {
3017
3162
  KNOWN_KEYS = new Set(SpecConfigSchema.keyof().options);
3018
3163
  });
3019
3164
 
3165
+ // src/check/format.ts
3166
+ function compareRepoNullsLast(a, b) {
3167
+ if (a === b)
3168
+ return 0;
3169
+ if (a === null)
3170
+ return 1;
3171
+ if (b === null)
3172
+ return -1;
3173
+ return a.localeCompare(b);
3174
+ }
3175
+ function compareDiagnostics(a, b) {
3176
+ const codeCmp = a.code.localeCompare(b.code);
3177
+ if (codeCmp !== 0)
3178
+ return codeCmp;
3179
+ const repoCmp = compareRepoNullsLast(a.repo, b.repo);
3180
+ if (repoCmp !== 0)
3181
+ return repoCmp;
3182
+ const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
3183
+ if (fileCmp !== 0)
3184
+ return fileCmp;
3185
+ const lineCmp = (a.line ?? 0) - (b.line ?? 0);
3186
+ if (lineCmp !== 0)
3187
+ return lineCmp;
3188
+ return (a.req_id ?? "").localeCompare(b.req_id ?? "");
3189
+ }
3190
+ function sortDiagnostics(rows) {
3191
+ return [...rows].sort(compareDiagnostics);
3192
+ }
3193
+ function renderDiagnostics(rows, mode) {
3194
+ const sorted = sortDiagnostics(rows);
3195
+ if (mode === "json") {
3196
+ return JSON.stringify(sorted);
3197
+ }
3198
+ return sorted.map((d) => {
3199
+ const repo = d.repo ?? "";
3200
+ const file = d.source_file ?? "";
3201
+ const line = d.line ?? "";
3202
+ const reqId = d.req_id ?? "";
3203
+ return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
3204
+ }).join(`
3205
+ `);
3206
+ }
3207
+
3020
3208
  // src/operations/_index.ts
3021
3209
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync3 } from "fs";
3022
3210
  import { dirname as dirname4, join as join6 } from "path";
@@ -3115,6 +3303,26 @@ function handleNotAPlatform(e) {
3115
3303
  }
3116
3304
  throw e;
3117
3305
  }
3306
+ function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
3307
+ if (!(e instanceof InvalidDomainFilesError))
3308
+ return;
3309
+ if (json)
3310
+ console.log(renderDiagnostics(e.diagnostics, "json"));
3311
+ else
3312
+ console.error(renderDiagnostics(e.diagnostics, "text"));
3313
+ process.exit(code);
3314
+ }
3315
+ function cliFreshTags(platformDir) {
3316
+ const fresh = coldFreshTags(platformDir);
3317
+ return async (reqId) => {
3318
+ try {
3319
+ return await fresh(reqId);
3320
+ } catch (e) {
3321
+ handleInvalidDomainFiles(e, false, EXIT.USAGE);
3322
+ throw e;
3323
+ }
3324
+ };
3325
+ }
3118
3326
  function handleStorageUnavailable(e, dbPath) {
3119
3327
  const info = describeStorageError(e);
3120
3328
  if (info === null)
@@ -3143,7 +3351,7 @@ function exitOnFailure(cmdName, failure) {
3143
3351
  process.exit(EXIT.USAGE);
3144
3352
  }
3145
3353
  async function withReadStorage(opts, fn) {
3146
- const { platformDir, dbPath, fresh } = opts;
3354
+ const { platformDir, dbPath, fresh, json = false } = opts;
3147
3355
  try {
3148
3356
  await withIndex({ platformDir, dbPath, build: fresh ? "fresh" : "missing" }, async (h) => {
3149
3357
  for (const w of h.warnings)
@@ -3152,6 +3360,7 @@ async function withReadStorage(opts, fn) {
3152
3360
  });
3153
3361
  } catch (e) {
3154
3362
  handleStorageUnavailable(e, dbPath);
3363
+ handleInvalidDomainFiles(e, json);
3155
3364
  handleNotAPlatform(e);
3156
3365
  }
3157
3366
  }
@@ -3358,7 +3567,7 @@ function listFlag(rawArgs, name, parsed) {
3358
3567
  }
3359
3568
  return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
3360
3569
  }
3361
- var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg;
3570
+ var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, relatesArg;
3362
3571
  var init__args = __esm(() => {
3363
3572
  init_constants();
3364
3573
  platformDirArg = {
@@ -3383,6 +3592,10 @@ var init__args = __esm(() => {
3383
3592
  type: "string",
3384
3593
  description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
3385
3594
  };
3595
+ relatesArg = {
3596
+ type: "string",
3597
+ description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
3598
+ };
3386
3599
  });
3387
3600
 
3388
3601
  // src/commands/index.ts
@@ -3457,6 +3670,7 @@ var init_commands = __esm(() => {
3457
3670
  console.error(formatNotASpecPlatform(err.platformDir));
3458
3671
  process.exit(EXIT.USAGE);
3459
3672
  }
3673
+ handleInvalidDomainFiles(err, !!args.json);
3460
3674
  console.error("spec index FAILED:", err);
3461
3675
  process.exit(EXIT.FAILURE);
3462
3676
  } finally {
@@ -3466,49 +3680,6 @@ var init_commands = __esm(() => {
3466
3680
  });
3467
3681
  });
3468
3682
 
3469
- // src/check/format.ts
3470
- function compareRepoNullsLast(a, b) {
3471
- if (a === b)
3472
- return 0;
3473
- if (a === null)
3474
- return 1;
3475
- if (b === null)
3476
- return -1;
3477
- return a.localeCompare(b);
3478
- }
3479
- function compareDiagnostics(a, b) {
3480
- const codeCmp = a.code.localeCompare(b.code);
3481
- if (codeCmp !== 0)
3482
- return codeCmp;
3483
- const repoCmp = compareRepoNullsLast(a.repo, b.repo);
3484
- if (repoCmp !== 0)
3485
- return repoCmp;
3486
- const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
3487
- if (fileCmp !== 0)
3488
- return fileCmp;
3489
- const lineCmp = (a.line ?? 0) - (b.line ?? 0);
3490
- if (lineCmp !== 0)
3491
- return lineCmp;
3492
- return (a.req_id ?? "").localeCompare(b.req_id ?? "");
3493
- }
3494
- function sortDiagnostics(rows) {
3495
- return [...rows].sort(compareDiagnostics);
3496
- }
3497
- function renderDiagnostics(rows, mode) {
3498
- const sorted = sortDiagnostics(rows);
3499
- if (mode === "json") {
3500
- return JSON.stringify(sorted);
3501
- }
3502
- return sorted.map((d) => {
3503
- const repo = d.repo ?? "";
3504
- const file = d.source_file ?? "";
3505
- const line = d.line ?? "";
3506
- const reqId = d.req_id ?? "";
3507
- return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
3508
- }).join(`
3509
- `);
3510
- }
3511
-
3512
3683
  // src/check/proven.ts
3513
3684
  function normalizeToSegments(path) {
3514
3685
  const out = [];
@@ -3634,7 +3805,7 @@ var init_proven = __esm(() => {
3634
3805
 
3635
3806
  // src/base/gitBase.ts
3636
3807
  function isSafeRef(ref) {
3637
- return /^[A-Za-z0-9._/-]+$/.test(ref);
3808
+ return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
3638
3809
  }
3639
3810
  function gitRepoPrefix(platformDir) {
3640
3811
  const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], { cwd: platformDir });
@@ -3672,7 +3843,7 @@ function gitLsTree(platformDir, ref, dir) {
3672
3843
  function gitDiffNameStatus(platformDir, ref) {
3673
3844
  if (!isSafeRef(ref))
3674
3845
  return [];
3675
- const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref], {
3846
+ const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], {
3676
3847
  cwd: platformDir
3677
3848
  });
3678
3849
  if (proc.exitCode !== 0)
@@ -3691,6 +3862,14 @@ function gitDiffNameStatus(platformDir, ref) {
3691
3862
  }
3692
3863
  return rows;
3693
3864
  }
3865
+ function newestReleaseTag(platformDir) {
3866
+ const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir });
3867
+ if (proc.exitCode !== 0)
3868
+ return null;
3869
+ const tag = proc.stdout.toString().split(`
3870
+ `)[0]?.trim() ?? "";
3871
+ return tag !== "" && isSafeRef(tag) ? tag : null;
3872
+ }
3694
3873
 
3695
3874
  // src/check/codeowners.ts
3696
3875
  function parseCodeowners(text) {
@@ -3972,8 +4151,7 @@ function rowsForDomain2(key, domain2) {
3972
4151
  source_file: specPaths("", key).rel,
3973
4152
  line: found >= 0 ? found + 1 : 0,
3974
4153
  req_id: req.id,
3975
- detail: `${result.problem}. Expected one of:
3976
- ${result.expected}`
4154
+ detail: `${result.problem}. ${result.expected}`
3977
4155
  });
3978
4156
  }
3979
4157
  return rows;
@@ -4063,6 +4241,60 @@ var init_removed = __esm(() => {
4063
4241
  init_src();
4064
4242
  });
4065
4243
 
4244
+ // src/check/severity.ts
4245
+ import { existsSync as existsSync7, readFileSync } from "fs";
4246
+ import { join as join8 } from "path";
4247
+ function readCheckConfig(platformDir) {
4248
+ const path = join8(platformDir, CHECK_CONFIG_REL);
4249
+ if (!existsSync7(path))
4250
+ return { ok: true, config: { severity: [] } };
4251
+ return parseCheckConfigText(readFileSync(path, "utf-8"), CHECK_CONFIG_REL);
4252
+ }
4253
+ function specificity(rule, row) {
4254
+ if (rule.code !== row.code)
4255
+ return Specificity.none;
4256
+ if (rule.match === undefined)
4257
+ return Specificity.code;
4258
+ const id = row.req_id;
4259
+ if (id === null)
4260
+ return Specificity.none;
4261
+ if (rule.match.includes(id))
4262
+ return Specificity.id;
4263
+ const key = id.slice(0, id.lastIndexOf("-"));
4264
+ return rule.match.includes(key) ? Specificity.domain : Specificity.none;
4265
+ }
4266
+ function applySeverity(rows, rules) {
4267
+ const out = [];
4268
+ for (const row of rows) {
4269
+ let best;
4270
+ let bestScore = Specificity.none;
4271
+ for (const rule of rules) {
4272
+ const score = specificity(rule, row);
4273
+ if (score !== Specificity.none && score >= bestScore) {
4274
+ best = rule;
4275
+ bestScore = score;
4276
+ }
4277
+ }
4278
+ if (best === undefined)
4279
+ out.push(row);
4280
+ else if (best.severity !== "off")
4281
+ out.push({ ...row, severity: best.severity });
4282
+ }
4283
+ return out;
4284
+ }
4285
+ var CHECK_CONFIG_REL, Specificity;
4286
+ var init_severity = __esm(() => {
4287
+ init_src();
4288
+ init_constants();
4289
+ CHECK_CONFIG_REL = `${CANONICAL_SPECS_DIR}/check.json`;
4290
+ Specificity = {
4291
+ none: -1,
4292
+ code: 0,
4293
+ domain: 1,
4294
+ id: 2
4295
+ };
4296
+ });
4297
+
4066
4298
  // src/check/sqlDiagnostics.ts
4067
4299
  function collectDiagnostics(storage2) {
4068
4300
  const semantic = storage2.listSemanticDiagnostics();
@@ -4501,7 +4733,7 @@ function localToday() {
4501
4733
  }
4502
4734
 
4503
4735
  // src/operations/_envelope.ts
4504
- import { existsSync as existsSync7 } from "fs";
4736
+ import { existsSync as existsSync8 } from "fs";
4505
4737
  function domainKeyOf(id) {
4506
4738
  return id.slice(0, id.indexOf("-"));
4507
4739
  }
@@ -4510,7 +4742,7 @@ function displayStatus(raw) {
4510
4742
  }
4511
4743
  async function readEnvelope(platformDir, key, missingDetail) {
4512
4744
  const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
4513
- if (!existsSync7(specPath)) {
4745
+ if (!existsSync8(specPath)) {
4514
4746
  return fail("not_found", missingDetail ?? `no domain ${key} (expected ${relFile} under ${platformDir})`);
4515
4747
  }
4516
4748
  const parsed = parseDomainText(await Bun.file(specPath).text(), relFile);
@@ -4526,32 +4758,58 @@ async function readEnvelope(platformDir, key, missingDetail) {
4526
4758
  const domain2 = parsed.data;
4527
4759
  return { ok: true, key, specPath, relFile, domain: domain2, requirements: domain2.requirements };
4528
4760
  }
4761
+ async function scanForEntry(platformDir, id, skipKey) {
4762
+ for (const key of listDomainKeys(platformDir)) {
4763
+ if (key === skipKey)
4764
+ continue;
4765
+ const file = await readEnvelope(platformDir, key);
4766
+ if (!file.ok)
4767
+ continue;
4768
+ const req = file.requirements.find((r) => r.id === id);
4769
+ if (req !== undefined)
4770
+ return { ...file, req };
4771
+ }
4772
+ return null;
4773
+ }
4529
4774
  async function locateEntry(platformDir, id, missingDomainDetail) {
4530
- const file = await readEnvelope(platformDir, domainKeyOf(id), missingDomainDetail);
4531
- if (!file.ok)
4775
+ const prefixKey = domainKeyOf(id);
4776
+ const file = await readEnvelope(platformDir, prefixKey, missingDomainDetail);
4777
+ if (!file.ok && file.reason !== "not_found")
4532
4778
  return file;
4533
- const req = file.requirements.find((r) => r.id === id);
4534
- if (req === undefined)
4535
- return fail("not_found", `no entry ${id} in ${file.relFile}`);
4536
- return { ...file, req };
4537
- }
4538
- function successorEntry(id, fields) {
4539
- return {
4779
+ const req = file.ok ? file.requirements.find((r) => r.id === id) : undefined;
4780
+ if (file.ok && req !== undefined)
4781
+ return { ...file, req };
4782
+ const scanned = await scanForEntry(platformDir, id, prefixKey);
4783
+ if (scanned !== null)
4784
+ return scanned;
4785
+ return file.ok ? fail("not_found", `no entry ${id} in ${file.relFile}`) : file;
4786
+ }
4787
+ function splitList(values) {
4788
+ return values.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
4789
+ }
4790
+ function successorEntry(id, pred, fields) {
4791
+ const entry = {
4540
4792
  id,
4541
4793
  status: "active",
4542
4794
  statement: fields.statement,
4543
4795
  why: fields.why === "" ? null : fields.why,
4544
4796
  supersedes: null,
4545
4797
  supersededBy: null,
4546
- relates: [],
4798
+ relates: fields.relates,
4547
4799
  livesIn: fields.livesIn,
4548
4800
  issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
4549
- aliases: [],
4550
- cites: []
4801
+ aliases: [...pred.aliases],
4802
+ cites: pred.cites.map((c) => ({ ...c }))
4551
4803
  };
4804
+ if (pred.term !== undefined)
4805
+ entry.term = pred.term;
4806
+ if (pred.section !== undefined)
4807
+ entry.section = pred.section;
4808
+ return entry;
4552
4809
  }
4553
4810
  var init__envelope = __esm(() => {
4554
4811
  init_src();
4812
+ init_domains();
4555
4813
  init_constants();
4556
4814
  init__result();
4557
4815
  });
@@ -4588,8 +4846,7 @@ async function judgeStatementGrammar(platformDir, key, statement) {
4588
4846
  severity: cfg.severity,
4589
4847
  key,
4590
4848
  message: `the statement does not match the domain's EARS shape \u2014 ${result.problem}.
4591
- ` + `Expected one of:
4592
- ${result.expected}`
4849
+ ${result.expected}`
4593
4850
  };
4594
4851
  }
4595
4852
  function grammarWarningText(cmdName, v) {
@@ -4606,7 +4863,7 @@ var init_grammar2 = __esm(() => {
4606
4863
  });
4607
4864
 
4608
4865
  // src/operations/mint.ts
4609
- import { existsSync as existsSync8 } from "fs";
4866
+ import { existsSync as existsSync9 } from "fs";
4610
4867
  function unresolvableRefWarnings(platformDir, fieldValues) {
4611
4868
  const out = [];
4612
4869
  for (const value of fieldValues) {
@@ -4621,7 +4878,7 @@ function unresolvableRefWarnings(platformDir, fieldValues) {
4621
4878
  async function mint(input) {
4622
4879
  const { platformDir, key } = input;
4623
4880
  const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
4624
- if (!existsSync8(specPath)) {
4881
+ if (!existsSync9(specPath)) {
4625
4882
  return fail("not_found", `no domain ${key} (expected ${relFile} under ${platformDir})`);
4626
4883
  }
4627
4884
  const warnings = unresolvableRefWarnings(platformDir, [
@@ -4649,7 +4906,7 @@ async function mint(input) {
4649
4906
  why: input.why || null,
4650
4907
  supersedes: null,
4651
4908
  supersededBy: null,
4652
- relates: input.relates ?? [],
4909
+ relates: splitList(input.relates ?? []),
4653
4910
  livesIn: input.livesIn,
4654
4911
  issues: input.issue ? [{ role: "created", id: input.issue }] : [],
4655
4912
  aliases: [],
@@ -4808,8 +5065,8 @@ var init_term = __esm(() => {
4808
5065
  });
4809
5066
 
4810
5067
  // src/operations/glossary.ts
4811
- import { existsSync as existsSync9, readFileSync } from "fs";
4812
- import { join as join8 } from "path";
5068
+ import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
5069
+ import { join as join9 } from "path";
4813
5070
  function parseGlossary(md) {
4814
5071
  const terms = [];
4815
5072
  let section = null;
@@ -4861,15 +5118,15 @@ function generateGlossary(terms) {
4861
5118
  }
4862
5119
  function readStoreTerms(platformDir) {
4863
5120
  const { abs: specPath, rel: relFile } = specPaths(platformDir, TERM_KEY);
4864
- if (!existsSync9(specPath))
5121
+ if (!existsSync10(specPath))
4865
5122
  return [];
4866
- const parsed = parseDomainText(readFileSync(specPath, "utf8"), relFile);
5123
+ const parsed = parseDomainText(readFileSync2(specPath, "utf8"), relFile);
4867
5124
  if (!parsed.ok)
4868
5125
  return [];
4869
5126
  return parsed.data.requirements.filter((r) => r.status.toLowerCase() === "active").sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((r) => ({ term: r.term ?? "", statement: r.statement, section: r.section ?? null }));
4870
5127
  }
4871
5128
  function glossaryPath(platformDir) {
4872
- return join8(platformDir, "GLOSSARY.md");
5129
+ return join9(platformDir, "GLOSSARY.md");
4873
5130
  }
4874
5131
  async function migrateGlossary(platformDir) {
4875
5132
  const store = await termStore(platformDir);
@@ -4880,10 +5137,10 @@ async function migrateGlossary(platformDir) {
4880
5137
  return { ok: true, skipped: true, existing: domain2.requirements.length };
4881
5138
  }
4882
5139
  const gPath = glossaryPath(platformDir);
4883
- if (!existsSync9(gPath)) {
5140
+ if (!existsSync10(gPath)) {
4884
5141
  return fail("not_found", `no GLOSSARY.md to migrate (expected ${gPath})`);
4885
5142
  }
4886
- const parsed = parseGlossary(readFileSync(gPath, "utf8"));
5143
+ const parsed = parseGlossary(readFileSync2(gPath, "utf8"));
4887
5144
  domain2.requirements = parsed.map((t, i) => ({
4888
5145
  id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
4889
5146
  status: "active",
@@ -4920,10 +5177,10 @@ function glossaryDriftDiagnostic(platformDir) {
4920
5177
  if (terms.length === 0)
4921
5178
  return null;
4922
5179
  const gPath = glossaryPath(platformDir);
4923
- if (!existsSync9(gPath))
5180
+ if (!existsSync10(gPath))
4924
5181
  return null;
4925
5182
  const generated = generateGlossary(terms);
4926
- const committed = readFileSync(gPath, "utf8");
5183
+ const committed = readFileSync2(gPath, "utf8");
4927
5184
  if (generated === committed)
4928
5185
  return null;
4929
5186
  return {
@@ -4939,7 +5196,7 @@ function glossaryDriftDiagnostic(platformDir) {
4939
5196
  function checkGlossary(platformDir) {
4940
5197
  const generated = generateGlossary(readStoreTerms(platformDir));
4941
5198
  const gPath = glossaryPath(platformDir);
4942
- const committed = existsSync9(gPath) ? readFileSync(gPath, "utf8") : "";
5199
+ const committed = existsSync10(gPath) ? readFileSync2(gPath, "utf8") : "";
4943
5200
  return { ok: true, clean: generated === committed };
4944
5201
  }
4945
5202
  var GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
@@ -4952,27 +5209,221 @@ var init_glossary = __esm(() => {
4952
5209
  init_term();
4953
5210
  });
4954
5211
 
4955
- // src/operations/check.ts
4956
- import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
4957
- import { join as join9 } from "path";
4958
- function readCodeownersText(platformDir) {
4959
- const path = join9(platformDir, ".github", "CODEOWNERS");
4960
- if (!existsSync10(path))
4961
- return null;
4962
- return readFileSync2(path, "utf-8");
5212
+ // src/guard/collect.ts
5213
+ import { join as join10 } from "path";
5214
+ function isIgnored(path) {
5215
+ return isPathIgnored(path);
4963
5216
  }
4964
- function domainReqsFromText(text, platformRel) {
4965
- const validated = parseDomainText(text, platformRel);
4966
- if (!validated.ok)
4967
- return { reqs: [], diagnostics: validated.diagnostics };
4968
- return { reqs: validated.data.requirements, diagnostics: [] };
5217
+ function isSpecFile(path) {
5218
+ return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
5219
+ }
5220
+ function isCodeFile(path) {
5221
+ return !isSpecFile(path) && !isIgnored(path) && isTagSource(path);
5222
+ }
5223
+ function parseReqs(text) {
5224
+ const v = parseDomainText(text, "");
5225
+ return v.ok ? v.data.requirements : [];
5226
+ }
5227
+ function collectBaseReqs(platformDir, ref, specPaths2) {
5228
+ const reqs = [];
5229
+ const path = new Map;
5230
+ for (const p of specPaths2) {
5231
+ const bytes = gitShow(platformDir, ref, p);
5232
+ if (bytes === null)
5233
+ continue;
5234
+ for (const r of parseReqs(bytes)) {
5235
+ reqs.push(r);
5236
+ path.set(r.id, p);
5237
+ }
5238
+ }
5239
+ return { reqs, path };
5240
+ }
5241
+ function collectBaseTags(platformDir, ref, codePaths) {
5242
+ const implSite = new Map;
5243
+ const verifySite = new Map;
5244
+ for (const p of codePaths) {
5245
+ const bytes = gitShow(platformDir, ref, p);
5246
+ if (bytes === null)
5247
+ continue;
5248
+ for (const tag of scanTagsInFile("", p, bytes)) {
5249
+ const target = tag.kind === "verifies" ? verifySite : implSite;
5250
+ if (!target.has(tag.req_id))
5251
+ target.set(tag.req_id, { file: tag.file, line: tag.line });
5252
+ }
5253
+ }
5254
+ return { implSite, verifySite };
5255
+ }
5256
+ async function collectWorktreeReqs(canonicalDir) {
5257
+ const ids = new Set;
5258
+ const activeIds = new Set;
5259
+ const supersedesTargets = new Set;
5260
+ for (const rel of await findDomainJsonFiles(canonicalDir)) {
5261
+ const text = await Bun.file(join10(canonicalDir, rel)).text();
5262
+ for (const r of parseReqs(text)) {
5263
+ ids.add(r.id);
5264
+ if (r.status.toLowerCase() === "active")
5265
+ activeIds.add(r.id);
5266
+ if (r.supersedes != null)
5267
+ supersedesTargets.add(r.supersedes);
5268
+ }
5269
+ }
5270
+ return { ids, activeIds, supersedesTargets };
5271
+ }
5272
+ function tallyWorktreeTags(storage2) {
5273
+ const implCount = new Map;
5274
+ const verifyCount = new Map;
5275
+ for (const t of storage2.listTags()) {
5276
+ const bucket = t.kind === "implements" ? implCount : t.kind === "verifies" ? verifyCount : null;
5277
+ if (bucket !== null)
5278
+ bucket.set(t.req_id, (bucket.get(t.req_id) ?? 0) + 1);
5279
+ }
5280
+ return { implCount, verifyCount };
5281
+ }
5282
+ async function collectFacts(platformDir, ref, storage2) {
5283
+ const changed = gitDiffNameStatus(platformDir, ref);
5284
+ const specPaths2 = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
5285
+ const codePaths = changed.filter((e) => isCodeFile(e.path)).map((e) => e.path);
5286
+ const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
5287
+ const base = collectBaseReqs(platformDir, ref, specPaths2);
5288
+ const baseTags = collectBaseTags(platformDir, ref, codePaths);
5289
+ const worktree = await collectWorktreeReqs(join10(platformDir, CANONICAL_SPECS_DIR));
5290
+ const counts = tallyWorktreeTags(storage2);
5291
+ return {
5292
+ baseReqs: base.reqs,
5293
+ baseReqPath: base.path,
5294
+ baseImplSite: baseTags.implSite,
5295
+ baseVerifySite: baseTags.verifySite,
5296
+ worktreeReqIds: worktree.ids,
5297
+ worktreeActiveIds: worktree.activeIds,
5298
+ worktreeSupersedesTargets: worktree.supersedesTargets,
5299
+ worktreeImplCount: counts.implCount,
5300
+ worktreeVerifyCount: counts.verifyCount,
5301
+ deletedSpecFiles
5302
+ };
5303
+ }
5304
+ var init_collect = __esm(() => {
5305
+ init_src();
5306
+ init_constants();
5307
+ init_fs();
5308
+ init_tags();
5309
+ });
5310
+
5311
+ // src/guard/losses.ts
5312
+ function isActive(status) {
5313
+ return status.toLowerCase() === "active";
5314
+ }
5315
+ function isSuperseded(base, f) {
5316
+ const forward = base.supersededBy != null && f.worktreeReqIds.has(base.supersededBy);
5317
+ return forward || f.worktreeSupersedesTargets.has(base.id);
5318
+ }
5319
+ function reqDeletedLoss(base, f) {
5320
+ return {
5321
+ kind: "REQUIREMENT_REMOVED",
5322
+ req_id: base.id,
5323
+ file: f.baseReqPath.get(base.id) ?? "",
5324
+ line: 0,
5325
+ 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`
5326
+ };
5327
+ }
5328
+ function historyRemovedLoss(base, f) {
5329
+ const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
5330
+ return {
5331
+ kind: "REQUIREMENT_REMOVED",
5332
+ req_id: base.id,
5333
+ file: f.baseReqPath.get(base.id) ?? "",
5334
+ line: 0,
5335
+ 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`
5336
+ };
5337
+ }
5338
+ function implLostLoss(id, site) {
5339
+ return {
5340
+ kind: "IMPL_LOST",
5341
+ req_id: id,
5342
+ file: site.file,
5343
+ line: site.line,
5344
+ detail: `${id} lost its last implementing @spec tag (was ${site.file}:${site.line})`
5345
+ };
5346
+ }
5347
+ function verifyLostLoss(id, site) {
5348
+ return {
5349
+ kind: "VERIFY_LOST",
5350
+ req_id: id,
5351
+ file: site.file,
5352
+ line: site.line,
5353
+ detail: `${id} lost its last verifying @spec tag (was ${site.file}:${site.line})`
5354
+ };
5355
+ }
5356
+ function specFileDeletedLoss(path) {
5357
+ return {
5358
+ kind: "SPEC_FILE_DELETED",
5359
+ req_id: null,
5360
+ file: path,
5361
+ line: 0,
5362
+ detail: `canonical spec file ${path} present at the ref is absent from the working tree`
5363
+ };
5364
+ }
5365
+ function reqDeletedFor(base, f) {
5366
+ if (f.worktreeReqIds.has(base.id))
5367
+ return null;
5368
+ if (!isActive(base.status))
5369
+ return historyRemovedLoss(base, f);
5370
+ if (isSuperseded(base, f))
5371
+ return null;
5372
+ return reqDeletedLoss(base, f);
5373
+ }
5374
+ function lastTagGone(id, count, f) {
5375
+ return f.worktreeActiveIds.has(id) && count === 0;
5376
+ }
5377
+ function classifyLosses(f) {
5378
+ const out = [];
5379
+ for (const base of f.baseReqs) {
5380
+ const loss = reqDeletedFor(base, f);
5381
+ if (loss !== null)
5382
+ out.push(loss);
5383
+ }
5384
+ for (const [id, site] of f.baseImplSite) {
5385
+ if (lastTagGone(id, f.worktreeImplCount.get(id) ?? 0, f))
5386
+ out.push(implLostLoss(id, site));
5387
+ }
5388
+ for (const [id, site] of f.baseVerifySite) {
5389
+ if (lastTagGone(id, f.worktreeVerifyCount.get(id) ?? 0, f))
5390
+ out.push(verifyLostLoss(id, site));
5391
+ }
5392
+ for (const path of f.deletedSpecFiles)
5393
+ out.push(specFileDeletedLoss(path));
5394
+ return out;
5395
+ }
5396
+
5397
+ // src/operations/guard.ts
5398
+ async function guard(storage2, platformDir, ref) {
5399
+ const losses = classifyLosses(await collectFacts(platformDir, ref, storage2));
5400
+ return { ok: true, ref, losses };
5401
+ }
5402
+ var init_guard = __esm(() => {
5403
+ init_collect();
5404
+ });
5405
+
5406
+ // src/operations/check.ts
5407
+ import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
5408
+ import { join as join11 } from "path";
5409
+ function readCodeownersText(platformDir) {
5410
+ const path = join11(platformDir, ".github", "CODEOWNERS");
5411
+ if (!existsSync11(path))
5412
+ return null;
5413
+ return readFileSync3(path, "utf-8");
5414
+ }
5415
+ function domainReqsFromText(text, platformRel) {
5416
+ const validated = parseDomainText(text, platformRel);
5417
+ if (!validated.ok)
5418
+ return { reqs: [], diagnostics: validated.diagnostics };
5419
+ return { reqs: validated.data.requirements, diagnostics: [] };
4969
5420
  }
4970
5421
  async function collectChangeReqs(canonicalDir) {
4971
5422
  const reqs = [];
4972
5423
  const relPathById = new Map;
4973
5424
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
4974
5425
  const platformRel = `${CANONICAL_SPECS_DIR}/${rel}`;
4975
- const text = await Bun.file(join9(canonicalDir, rel)).text();
5426
+ const text = await Bun.file(join11(canonicalDir, rel)).text();
4976
5427
  const parsed = domainReqsFromText(text, platformRel);
4977
5428
  for (const r of parsed.reqs) {
4978
5429
  reqs.push(r);
@@ -4981,7 +5432,7 @@ async function collectChangeReqs(canonicalDir) {
4981
5432
  }
4982
5433
  return { reqs, relPathById };
4983
5434
  }
4984
- function collectBaseReqs(platformDir, ref, diagnostics2) {
5435
+ function collectBaseReqs2(platformDir, ref, diagnostics2) {
4985
5436
  const reqs = [];
4986
5437
  const relPathById = new Map;
4987
5438
  for (const path of gitLsTree(platformDir, ref, CANONICAL_SPECS_DIR)) {
@@ -5013,8 +5464,8 @@ async function ingestResults(storage2, resultsPath, diagnostics2) {
5013
5464
  }
5014
5465
  async function governanceGate(input, ref, diagnostics2, results) {
5015
5466
  const { platformDir } = input;
5016
- const change = await collectChangeReqs(join9(platformDir, CANONICAL_SPECS_DIR));
5017
- const base = collectBaseReqs(platformDir, ref, diagnostics2);
5467
+ const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
5468
+ const base = collectBaseReqs2(platformDir, ref, diagnostics2);
5018
5469
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
5019
5470
  const codeowners = parseCodeowners(readCodeownersText(platformDir) ?? "");
5020
5471
  const approvedBy = (input.approvedBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -5023,12 +5474,33 @@ async function governanceGate(input, ref, diagnostics2, results) {
5023
5474
  diagnostics2.push(...partialPropagation(changedRules(base.reqs, change.reqs), results.verifyingTags, results.resultsParsed, (id) => change.relPathById.get(id) ?? null));
5024
5475
  }
5025
5476
  }
5477
+ async function lossDiagnostics(storage2, platformDir, ref) {
5478
+ const { losses } = await guard(storage2, platformDir, ref);
5479
+ const rows = [];
5480
+ for (const loss of losses) {
5481
+ if (loss.kind === "REQUIREMENT_REMOVED")
5482
+ continue;
5483
+ rows.push({
5484
+ code: loss.kind,
5485
+ severity: LOSS_SEVERITY[loss.kind],
5486
+ repo: null,
5487
+ source_file: loss.file === "" ? null : loss.file,
5488
+ line: loss.line,
5489
+ req_id: loss.req_id,
5490
+ detail: loss.detail
5491
+ });
5492
+ }
5493
+ return rows;
5494
+ }
5026
5495
  async function check(input, storage2) {
5027
5496
  const { platformDir } = input;
5028
5497
  if (input.base && !gitRefResolves(platformDir, input.base)) {
5029
5498
  return fail("usage", `--base ref '${input.base}' does not resolve in ${platformDir} ` + "(not a git repo, or the ref is unfetched/misspelled) \u2014 refusing to run the governance gate fail-open");
5030
5499
  }
5031
5500
  const result = await runIndex({ platformDir, storage: storage2 });
5501
+ const checkConfig2 = readCheckConfig(platformDir);
5502
+ if (!checkConfig2.ok)
5503
+ return fail("usage", checkConfig2.detail);
5032
5504
  const diagnostics2 = collectDiagnostics(storage2);
5033
5505
  if (input.unsourcedChange) {
5034
5506
  const superseded = storage2.listRequirements({ status: "Superseded" });
@@ -5043,20 +5515,24 @@ async function check(input, storage2) {
5043
5515
  const results = await ingestResults(storage2, input.resultsPath, diagnostics2);
5044
5516
  if (input.base) {
5045
5517
  await governanceGate(input, input.base, diagnostics2, results);
5518
+ diagnostics2.push(...await lossDiagnostics(storage2, platformDir, input.base));
5046
5519
  } else if (gitRefResolves(platformDir, "HEAD")) {
5047
- const change = await collectChangeReqs(join9(platformDir, CANONICAL_SPECS_DIR));
5520
+ const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
5048
5521
  const baseParseSink = [];
5049
- const base = collectBaseReqs(platformDir, "HEAD", baseParseSink);
5522
+ const base = collectBaseReqs2(platformDir, "HEAD", baseParseSink);
5050
5523
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
5524
+ diagnostics2.push(...await lossDiagnostics(storage2, platformDir, "HEAD"));
5051
5525
  }
5526
+ const graded = applySeverity(diagnostics2, checkConfig2.config.severity);
5052
5527
  return {
5053
5528
  ok: true,
5054
- diagnostics: diagnostics2,
5529
+ diagnostics: graded,
5055
5530
  buildId: result.build_id,
5056
- failing: diagnostics2.some((d) => d.severity === "error"),
5531
+ failing: graded.some((d) => d.severity === "error"),
5057
5532
  proofsUnconfirmed: !input.resultsPath
5058
5533
  };
5059
5534
  }
5535
+ var LOSS_SEVERITY;
5060
5536
  var init_check = __esm(() => {
5061
5537
  init_src();
5062
5538
  init_filerefs2();
@@ -5064,6 +5540,7 @@ var init_check = __esm(() => {
5064
5540
  init_propagation_teeth();
5065
5541
  init_proven();
5066
5542
  init_removed();
5543
+ init_severity();
5067
5544
  init_sqlDiagnostics();
5068
5545
  init_statusflip();
5069
5546
  init_supersedes();
@@ -5074,6 +5551,12 @@ var init_check = __esm(() => {
5074
5551
  init_fs();
5075
5552
  init__result();
5076
5553
  init_glossary();
5554
+ init_guard();
5555
+ LOSS_SEVERITY = {
5556
+ IMPL_LOST: "error",
5557
+ SPEC_FILE_DELETED: "error",
5558
+ VERIFY_LOST: "warning"
5559
+ };
5077
5560
  });
5078
5561
 
5079
5562
  // src/commands/check.ts
@@ -5123,17 +5606,17 @@ var init_check2 = __esm(() => {
5123
5606
  out: outArg,
5124
5607
  ci: {
5125
5608
  type: "boolean",
5126
- description: "Force cold rebuild: rm db+wal+shm BEFORE indexing (CHCK-01)"
5609
+ description: "Force cold rebuild: rm db+wal+shm BEFORE indexing"
5127
5610
  },
5128
5611
  json: jsonArg,
5129
5612
  noPrompt: noPromptArg,
5130
5613
  unsourcedChange: {
5131
5614
  type: "boolean",
5132
- description: "Opt-in: emit warning-severity UNSOURCED_CHANGE for Superseded requirements lacking a supersedes-via issue (OFF by default; USRC-02)"
5615
+ description: "Opt-in: emit warning-severity UNSOURCED_CHANGE for Superseded requirements lacking a supersedes-via issue (OFF by default)"
5133
5616
  },
5134
5617
  results: {
5135
5618
  type: "string",
5136
- description: "Ingest a JUnit XML results file; enforce the trusted-red PROVEN gate (GATE-01)"
5619
+ description: "Ingest a JUnit XML results file; enforce the trusted-red PROVEN gate"
5137
5620
  },
5138
5621
  base: {
5139
5622
  type: "string",
@@ -5184,6 +5667,7 @@ var init_check2 = __esm(() => {
5184
5667
  return result;
5185
5668
  });
5186
5669
  } catch (e) {
5670
+ handleInvalidDomainFiles(e, jsonMode);
5187
5671
  exitAfterCrash(e);
5188
5672
  }
5189
5673
  if (outcome === undefined) {
@@ -5207,8 +5691,8 @@ var init_check2 = __esm(() => {
5207
5691
  });
5208
5692
 
5209
5693
  // src/operations/domain.ts
5210
- import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "fs";
5211
- import { dirname as dirname6, join as join10, resolve as resolve9 } from "path";
5694
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4 } from "fs";
5695
+ import { dirname as dirname6, join as join12, resolve as resolve9 } from "path";
5212
5696
  function definedFields(options) {
5213
5697
  const out = {};
5214
5698
  if (options.scope !== undefined)
@@ -5228,9 +5712,9 @@ async function newDomain(platformDir, key, options = {}) {
5228
5712
  if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
5229
5713
  return fail("usage", `refusing to write outside platformDir (${resolvedDest})`);
5230
5714
  }
5231
- if (existsSync11(dest))
5715
+ if (existsSync12(dest))
5232
5716
  return fail("conflict", `refusing to overwrite ${dest}`);
5233
- if (existsSync11(join10(dirname6(dest), "SPEC.md"))) {
5717
+ if (existsSync12(join12(dirname6(dest), "SPEC.md"))) {
5234
5718
  return fail("conflict", `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)");
5235
5719
  }
5236
5720
  mkdirSync4(dirname6(dest), { recursive: true });
@@ -5247,10 +5731,34 @@ async function newDomain(platformDir, key, options = {}) {
5247
5731
  async function listDomains(platformDir) {
5248
5732
  return { ok: true, domains: await domainsWithScope(platformDir) };
5249
5733
  }
5734
+ async function setDomainGrammar(platformDir, key, options) {
5735
+ if (options.grammar === undefined && options.grammarSeverity === undefined) {
5736
+ return fail("usage", "pass --grammar, --severity, or both");
5737
+ }
5738
+ const file = await readEnvelope(platformDir, key);
5739
+ if (!file.ok)
5740
+ return file;
5741
+ const domain2 = { ...file.domain, ...definedFields(options) };
5742
+ const res = await validateAndWrite(file.specPath, domain2, file.relFile);
5743
+ if (!res.ok) {
5744
+ return fail("invalid_domain_file", res.diagnostics.map((d) => d.detail).join(`
5745
+ `), {
5746
+ diagnostics: res.diagnostics
5747
+ });
5748
+ }
5749
+ return {
5750
+ ok: true,
5751
+ key,
5752
+ grammar: domain2.grammar,
5753
+ grammarSeverity: domain2.grammarSeverity,
5754
+ file: file.relFile
5755
+ };
5756
+ }
5250
5757
  var init_domain2 = __esm(() => {
5251
5758
  init_src();
5252
5759
  init_domains();
5253
5760
  init_constants();
5761
+ init__envelope();
5254
5762
  init__result();
5255
5763
  });
5256
5764
 
@@ -5259,6 +5767,7 @@ var exports_domain = {};
5259
5767
  __export(exports_domain, {
5260
5768
  domainNewCommand: () => domainNewCommand,
5261
5769
  domainListCommand: () => domainListCommand,
5770
+ domainGrammarCommand: () => domainGrammarCommand,
5262
5771
  domainCommand: () => domainCommand
5263
5772
  });
5264
5773
  import { defineCommand as defineCommand4 } from "citty";
@@ -5269,7 +5778,15 @@ function exitOnNewFailure(failure) {
5269
5778
  }
5270
5779
  exitOnFailure("spec domain new", failure);
5271
5780
  }
5272
- var domainNewCommand, domainListCommand, domainCommand;
5781
+ function enumFlag(flag, value, allowed) {
5782
+ if (value === undefined)
5783
+ return;
5784
+ if (allowed.includes(value))
5785
+ return value;
5786
+ console.error(`spec domain grammar: --${flag} must be one of ${allowed.join(", ")}`);
5787
+ process.exit(EXIT.USAGE);
5788
+ }
5789
+ var domainNewCommand, domainListCommand, GRAMMARS, SEVERITIES, domainGrammarCommand, domainCommand;
5273
5790
  var init_domain3 = __esm(() => {
5274
5791
  init_domains();
5275
5792
  init_constants();
@@ -5337,6 +5854,55 @@ var init_domain3 = __esm(() => {
5337
5854
  }
5338
5855
  }
5339
5856
  });
5857
+ GRAMMARS = ["ears", "freeform"];
5858
+ SEVERITIES = ["warning", "error"];
5859
+ domainGrammarCommand = defineCommand4({
5860
+ meta: {
5861
+ name: "grammar",
5862
+ description: "Set a domain's statement grammar and its severity"
5863
+ },
5864
+ args: {
5865
+ name: {
5866
+ type: "positional",
5867
+ required: true,
5868
+ description: "Domain key (case-insensitive)"
5869
+ },
5870
+ platformDir: platformDirArg,
5871
+ grammar: {
5872
+ type: "string",
5873
+ description: "ears | freeform"
5874
+ },
5875
+ severity: {
5876
+ type: "string",
5877
+ description: "warning | error \u2014 how STATEMENT_GRAMMAR and write-time judging treat a miss"
5878
+ },
5879
+ json: {
5880
+ type: "boolean",
5881
+ description: "Print { key, grammar, grammar_severity, file }"
5882
+ }
5883
+ },
5884
+ async run({ args }) {
5885
+ const platformDir = resolvePlatformDir(args);
5886
+ const grammar = enumFlag("grammar", args.grammar, GRAMMARS);
5887
+ const grammarSeverity = enumFlag("severity", args.severity, SEVERITIES);
5888
+ const result = await setDomainGrammar(platformDir, normalizeDomainKey(args.name), {
5889
+ grammar,
5890
+ grammarSeverity
5891
+ });
5892
+ if (!result.ok)
5893
+ exitOnFailure("spec domain grammar", result);
5894
+ if (args.json) {
5895
+ console.log(JSON.stringify({
5896
+ key: result.key,
5897
+ grammar: result.grammar ?? null,
5898
+ grammar_severity: result.grammarSeverity ?? null,
5899
+ file: result.file
5900
+ }));
5901
+ return;
5902
+ }
5903
+ console.log(`${result.file}: grammar ${result.grammar ?? "freeform"}, severity ${result.grammarSeverity ?? "warning"}`);
5904
+ }
5905
+ });
5340
5906
  domainCommand = defineCommand4({
5341
5907
  meta: {
5342
5908
  name: "domain",
@@ -5344,7 +5910,8 @@ var init_domain3 = __esm(() => {
5344
5910
  },
5345
5911
  subCommands: {
5346
5912
  new: domainNewCommand,
5347
- list: domainListCommand
5913
+ list: domainListCommand,
5914
+ grammar: domainGrammarCommand
5348
5915
  }
5349
5916
  });
5350
5917
  });
@@ -5528,7 +6095,7 @@ var init_map = __esm(() => {
5528
6095
  noPrompt: args.noPrompt
5529
6096
  }
5530
6097
  });
5531
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
6098
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
5532
6099
  const { rows } = coverageMatrix(storage2);
5533
6100
  if (!args.json && rows.length === 0) {
5534
6101
  console.error(formatNoRequirementsIndexed(platformDir));
@@ -5641,7 +6208,7 @@ var init_get = __esm(() => {
5641
6208
  platformDir,
5642
6209
  args: { noPrompt: args.noPrompt }
5643
6210
  });
5644
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
6211
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
5645
6212
  const result = getRecord(storage2, reqId);
5646
6213
  if (result.row === null) {
5647
6214
  console.error(result.platformEmpty ? formatNoRequirementsIndexed(platformDir) : `spec get: no requirement ${reqId} in the index under ${platformDir} (check the id, or re-run with --fresh after editing specs)`);
@@ -5715,7 +6282,7 @@ var init_list = __esm(() => {
5715
6282
  platformDir,
5716
6283
  args: { noPrompt: args.noPrompt }
5717
6284
  });
5718
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
6285
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
5719
6286
  const result = listRecords(storage2, filter);
5720
6287
  if (!args.json && result.rows.length === 0) {
5721
6288
  console.error(result.platformEmpty ? formatNoRequirementsIndexed(platformDir) : `spec list: no requirements match under ${platformDir}`);
@@ -5792,7 +6359,7 @@ var init_propagation2 = __esm(() => {
5792
6359
  platformDir,
5793
6360
  args: { noPrompt: args.noPrompt }
5794
6361
  });
5795
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
6362
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
5796
6363
  const result = propagation2(storage2, reqId);
5797
6364
  if (!args.json && result.platformEmpty) {
5798
6365
  console.error(formatNoRequirementsIndexed(platformDir));
@@ -5915,7 +6482,7 @@ var init_query = __esm(() => {
5915
6482
  platformDir,
5916
6483
  args: { noPrompt: args.noPrompt }
5917
6484
  });
5918
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
6485
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
5919
6486
  const result = query(storage2, text, limit);
5920
6487
  if (!result.ok) {
5921
6488
  console.error(`spec query: ${result.detail}`);
@@ -6037,7 +6604,7 @@ var init_relations = __esm(() => {
6037
6604
  noPrompt: args.noPrompt
6038
6605
  }
6039
6606
  });
6040
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
6607
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
6041
6608
  const { rows } = relations(storage2);
6042
6609
  if (!args.json && rows.length === 0) {
6043
6610
  console.error(formatNoRelations(platformDir));
@@ -6162,7 +6729,7 @@ var init_adapter = () => {};
6162
6729
 
6163
6730
  // ../tracker/src/cache.ts
6164
6731
  import { mkdirSync as mkdirSync5 } from "fs";
6165
- import { dirname as dirname7, join as join11 } from "path";
6732
+ import { dirname as dirname7, join as join13 } from "path";
6166
6733
  function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
6167
6734
  const t = Date.parse(entry.fetched_at);
6168
6735
  if (Number.isNaN(t))
@@ -6176,7 +6743,7 @@ function isValidEntry(e) {
6176
6743
  return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
6177
6744
  }
6178
6745
  function sidecarPath(platformDir) {
6179
- return join11(platformDir, ".spec-engine", SIDECAR_FILE);
6746
+ return join13(platformDir, ".spec-engine", SIDECAR_FILE);
6180
6747
  }
6181
6748
  async function readCache(platformDir) {
6182
6749
  try {
@@ -6443,7 +7010,7 @@ var init_provenance = __esm(() => {
6443
7010
  noPrompt: args.noPrompt
6444
7011
  }
6445
7012
  });
6446
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, async (storage2) => {
7013
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, async (storage2) => {
6447
7014
  const { rows } = provenance(storage2, issueId || undefined);
6448
7015
  if (!args.json && rows.length === 0) {
6449
7016
  console.error(issueId ? formatNoProvenanceForIssue(issueId, platformDir) : formatNoProvenance(platformDir));
@@ -6507,6 +7074,7 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
6507
7074
  statement,
6508
7075
  why: (args.why ?? "").trim(),
6509
7076
  livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
7077
+ relates: listFlag(rawArgs, "relates", args.relates) ?? [],
6510
7078
  issue: (args.issue ?? "").trim() || undefined,
6511
7079
  status: statusOf(args),
6512
7080
  json: Boolean(args.json)
@@ -6585,6 +7153,7 @@ var init_req = __esm(() => {
6585
7153
  },
6586
7154
  why: { type: "string", description: "Why it matters field (only with --text)" },
6587
7155
  lives: livesArg,
7156
+ relates: relatesArg,
6588
7157
  issue: {
6589
7158
  type: "string",
6590
7159
  description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
@@ -6617,9 +7186,9 @@ var init_req = __esm(() => {
6617
7186
  await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
6618
7187
  return;
6619
7188
  }
6620
- const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.issue === "string";
7189
+ const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.relates === "string" || typeof args.issue === "string";
6621
7190
  if (hasFieldFlags) {
6622
- console.error("spec req: --why/--lives/--issue require --text (the Requirement field is mandatory)");
7191
+ console.error("spec req: --why/--lives/--relates/--issue require --text (the Requirement field is mandatory)");
6623
7192
  process.exit(EXIT.USAGE);
6624
7193
  return;
6625
7194
  }
@@ -6755,6 +7324,9 @@ async function confirmTermCommand(opts) {
6755
7324
  exitOnFailure("spec term confirm", result);
6756
7325
  await withIndex({ platformDir, build: "fresh" }, () => {
6757
7326
  return;
7327
+ }).catch((e) => {
7328
+ handleInvalidDomainFiles(e, false, EXIT.USAGE);
7329
+ throw e;
6758
7330
  });
6759
7331
  const { pinned, file } = result;
6760
7332
  if (opts.json) {
@@ -7027,14 +7599,18 @@ async function supersedeTarget(platformDir, id) {
7027
7599
  return located;
7028
7600
  }
7029
7601
  function successorFields(input, req) {
7030
- const predLives = (req.livesIn[0] ?? "").trim();
7031
- const why = (input.why ?? req.why ?? "").trim();
7032
- const livesIn = input.livesIn ?? (predLives === "" ? [] : [predLives]);
7033
- return { why, livesIn };
7602
+ return {
7603
+ why: (input.why ?? req.why ?? "").trim(),
7604
+ livesIn: input.livesIn ?? [...req.livesIn],
7605
+ relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
7606
+ };
7034
7607
  }
7035
- function successorTermFields(input, req) {
7036
- const term = (input.term ?? req.term)?.trim() || req.term;
7037
- return { term, aliases: input.aliases ?? req.aliases };
7608
+ function applyTermOverrides(successor, input) {
7609
+ const term = input.term?.trim();
7610
+ if (term)
7611
+ successor.term = term;
7612
+ if (input.aliases !== undefined)
7613
+ successor.aliases = input.aliases;
7038
7614
  }
7039
7615
  function flipPredecessor(req, newId, issue) {
7040
7616
  req.status = "superseded";
@@ -7070,18 +7646,19 @@ async function supersede(input, freshTags) {
7070
7646
  }
7071
7647
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
7072
7648
  }
7073
- const { why, livesIn } = successorFields(input, req);
7649
+ const { why, livesIn, relates } = successorFields(input, req);
7074
7650
  warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn]));
7075
7651
  const newId = await nextRequirementId(platformDir, key);
7076
7652
  const issue = input.issue?.trim() || undefined;
7653
+ const successor = successorEntry(newId, req, {
7654
+ statement: input.statement,
7655
+ why,
7656
+ livesIn,
7657
+ relates,
7658
+ issue
7659
+ });
7660
+ applyTermOverrides(successor, input);
7077
7661
  flipPredecessor(req, newId, issue);
7078
- const successor = successorEntry(newId, { statement: input.statement, why, livesIn, issue });
7079
- if (key === "TERM") {
7080
- const termFields = successorTermFields(input, req);
7081
- if (termFields.term !== undefined)
7082
- successor.term = termFields.term;
7083
- successor.aliases = termFields.aliases;
7084
- }
7085
7662
  requirements.push(successor);
7086
7663
  const specVersion = applyVersionStage(domain2, req, successor, Boolean(input.noBump));
7087
7664
  domain2.updated = localToday();
@@ -7190,6 +7767,9 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
7190
7767
  const livesIn = listFlag(rawArgs, "lives", args.lives);
7191
7768
  if (livesIn !== undefined)
7192
7769
  input.livesIn = livesIn;
7770
+ const relates = listFlag(rawArgs, "relates", args.relates);
7771
+ if (relates !== undefined)
7772
+ input.relates = relates;
7193
7773
  if (typeof args.term === "string")
7194
7774
  input.term = args.term;
7195
7775
  if (typeof args.aliases === "string")
@@ -7203,7 +7783,6 @@ var supersedeArgs, supersedeCommand;
7203
7783
  var init_supersede2 = __esm(() => {
7204
7784
  init_constants();
7205
7785
  init_discover();
7206
- init__index();
7207
7786
  init_supersede();
7208
7787
  init_grammar3();
7209
7788
  init__args();
@@ -7228,6 +7807,10 @@ var init_supersede2 = __esm(() => {
7228
7807
  type: "string",
7229
7808
  description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
7230
7809
  },
7810
+ relates: {
7811
+ type: "string",
7812
+ description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
7813
+ },
7231
7814
  term: {
7232
7815
  type: "string",
7233
7816
  description: "TERM successor's headword (default: copied from the old entry; TERM ids only)"
@@ -7268,7 +7851,7 @@ var init_supersede2 = __esm(() => {
7268
7851
  handleNotAPlatform(e);
7269
7852
  }
7270
7853
  const statement = await resolveSuccessorText(args, platformDir, id);
7271
- const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement), coldFreshTags(platformDir));
7854
+ const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement), cliFreshTags(platformDir));
7272
7855
  if (!result.ok)
7273
7856
  exitOnFailure("spec supersede", result);
7274
7857
  printWarnings("spec supersede", result.warnings);
@@ -7335,7 +7918,6 @@ var deprecateCommand;
7335
7918
  var init_deprecate2 = __esm(() => {
7336
7919
  init_constants();
7337
7920
  init_discover();
7338
- init__index();
7339
7921
  init_deprecate();
7340
7922
  init_grammar3();
7341
7923
  init__args();
@@ -7378,7 +7960,7 @@ var init_deprecate2 = __esm(() => {
7378
7960
  } catch (e) {
7379
7961
  handleNotAPlatform(e);
7380
7962
  }
7381
- const result = await deprecate({ platformDir, id, reason }, coldFreshTags(platformDir));
7963
+ const result = await deprecate({ platformDir, id, reason }, cliFreshTags(platformDir));
7382
7964
  if (!result.ok)
7383
7965
  exitOnFailure("spec deprecate", result);
7384
7966
  const { file, sites } = result;
@@ -7397,30 +7979,28 @@ var init_deprecate2 = __esm(() => {
7397
7979
 
7398
7980
  // src/operations/move.ts
7399
7981
  async function locateMove(platformDir, id, targetKey) {
7400
- const sourceKey = domainKeyOf(id);
7401
- if (sourceKey === targetKey) {
7982
+ const prefixKey = domainKeyOf(id);
7983
+ const located = await locateEntry(platformDir, id, `no domain ${prefixKey} (expected spec-engine/${prefixKey}/SPEC.json)`);
7984
+ if (!located.ok)
7985
+ return located;
7986
+ const { req, ...source } = located;
7987
+ if (source.key === targetKey) {
7402
7988
  return fail("usage", `${id} is already in ${targetKey} \u2014 use spec supersede for an in-domain revision`);
7403
7989
  }
7404
- const source = await readEnvelope(platformDir, sourceKey, `no domain ${sourceKey} (expected spec-engine/${sourceKey}/SPEC.json)`);
7405
- if (!source.ok)
7406
- return source;
7407
7990
  const target = await readEnvelope(platformDir, targetKey, `no target domain ${targetKey} \u2014 run \`spec domain new ${targetKey}\` first`);
7408
7991
  if (!target.ok)
7409
7992
  return target;
7410
- const req = source.requirements.find((r) => r.id === id);
7411
- if (req === undefined)
7412
- return fail("not_found", `no entry ${id} in ${source.relFile}`);
7413
7993
  if (req.status.toLowerCase() !== "active") {
7414
7994
  return fail("conflict", `${id} is ${req.status} \u2014 only Active requirements move (superseded/retired entries stay as history)`);
7415
7995
  }
7416
7996
  return { ok: true, source, target, req };
7417
7997
  }
7418
7998
  function successorFields2(input, req) {
7419
- const srcLives = (req.livesIn[0] ?? "").trim();
7420
7999
  return {
7421
8000
  statement: (input.statement ?? req.statement).trim(),
7422
8001
  why: (input.why ?? req.why ?? "").trim(),
7423
- livesIn: input.livesIn ?? (srcLives === "" ? [] : [srcLives])
8002
+ livesIn: input.livesIn ?? [...req.livesIn],
8003
+ relates: [...req.relates]
7424
8004
  };
7425
8005
  }
7426
8006
  function versionSide(domain2, key, noBump) {
@@ -7469,7 +8049,7 @@ async function move(input, freshTags) {
7469
8049
  const sourceCurrent = source.domain.specVersion ?? 1;
7470
8050
  req.status = "superseded";
7471
8051
  req.supersededBy = newId;
7472
- target.requirements.push(successorEntry(newId, fields));
8052
+ target.requirements.push(successorEntry(newId, req, fields));
7473
8053
  const noBump = Boolean(input.noBump);
7474
8054
  const sourceSpecVersion = versionSide(source.domain, source.key, noBump);
7475
8055
  const targetSpecVersion = versionSide(target.domain, target.key, noBump);
@@ -7530,7 +8110,6 @@ var init_move2 = __esm(() => {
7530
8110
  init_domains();
7531
8111
  init_constants();
7532
8112
  init_discover();
7533
- init__index();
7534
8113
  init_move();
7535
8114
  init_grammar3();
7536
8115
  init__args();
@@ -7592,7 +8171,7 @@ var init_move2 = __esm(() => {
7592
8171
  } catch (e) {
7593
8172
  handleNotAPlatform(e);
7594
8173
  }
7595
- const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey), coldFreshTags(platformDir));
8174
+ const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey), cliFreshTags(platformDir));
7596
8175
  if (!result.ok)
7597
8176
  exitOnFailure("spec move", result);
7598
8177
  printWarnings("spec move", result.warnings);
@@ -7626,18 +8205,30 @@ var init_move2 = __esm(() => {
7626
8205
  });
7627
8206
 
7628
8207
  // src/operations/amend.ts
7629
- async function gateRejection(id, req, freshTags) {
8208
+ function releaseHolding(platformDir, relFile, id) {
8209
+ const tag = newestReleaseTag(platformDir);
8210
+ if (tag === null)
8211
+ return null;
8212
+ const text = gitShow(platformDir, tag, relFile);
8213
+ if (text === null)
8214
+ return null;
8215
+ const parsed = parseDomainText(text, relFile);
8216
+ return parsed.ok && parsed.data.requirements.some((r) => r.id === id) ? tag : null;
8217
+ }
8218
+ function gateRejection(platformDir, relFile, req, fields) {
8219
+ const { id } = req;
7630
8220
  const statusLc = req.status.toLowerCase();
7631
8221
  if (statusLc !== "active" && statusLc !== "draft") {
7632
8222
  return fail("conflict", `${id} is ${displayStatus(req.status)} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`);
7633
8223
  }
7634
8224
  if (statusLc !== "active")
7635
8225
  return null;
7636
- const bound = (await freshTags(id)).filter((t) => t.kind === "implements" || t.kind === "verifies");
7637
- const site = bound[0];
7638
- if (site === undefined)
8226
+ if (!PROMISE_FIELDS.some((f) => fields[f] !== undefined))
8227
+ return null;
8228
+ const tag = releaseHolding(platformDir, relFile, id);
8229
+ if (tag === null)
7639
8230
  return null;
7640
- return fail("conflict", `${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.");
8231
+ return fail("conflict", `${id} is released in ${tag} \u2014 its promise is immutable. ` + `Run \`spec supersede ${id}\` to change the statement or why; ` + "pass only --lives or --relates to amend its metadata in place.");
7641
8232
  }
7642
8233
  function applyFields(req, fields) {
7643
8234
  const fieldsChanged = [];
@@ -7657,6 +8248,10 @@ function applyFields(req, fields) {
7657
8248
  fieldsChanged.push("lives");
7658
8249
  refValues.push(...fields.livesIn);
7659
8250
  }
8251
+ if (fields.relates !== undefined) {
8252
+ req.relates = splitList(fields.relates);
8253
+ fieldsChanged.push("relates");
8254
+ }
7660
8255
  if (fields.term !== undefined) {
7661
8256
  req.term = fields.term;
7662
8257
  fieldsChanged.push("term");
@@ -7672,18 +8267,17 @@ function applyFields(req, fields) {
7672
8267
  }
7673
8268
  return { fieldsChanged, refValues };
7674
8269
  }
7675
- async function amend(input, freshTags) {
8270
+ async function amend(input) {
7676
8271
  const { platformDir, id, fields } = input;
7677
8272
  const located = await locateEntry(platformDir, id);
7678
8273
  if (!located.ok)
7679
8274
  return located;
7680
- const { specPath, relFile, domain: domain2, req } = located;
7681
- const rejection = await gateRejection(id, req, freshTags);
8275
+ const { key, specPath, relFile, domain: domain2, req } = located;
8276
+ const rejection = gateRejection(platformDir, relFile, req, fields);
7682
8277
  if (rejection !== null)
7683
8278
  return rejection;
7684
8279
  const warnings = [];
7685
8280
  if (fields.statement !== undefined) {
7686
- const key = domainKeyOf(id);
7687
8281
  const verdict = await judgeStatementGrammar(platformDir, key, fields.statement);
7688
8282
  if (!verdict.ok) {
7689
8283
  if (verdict.severity === "error") {
@@ -7705,12 +8299,14 @@ async function amend(input, freshTags) {
7705
8299
  }
7706
8300
  return { ok: true, id, file: relFile, fieldsChanged: [...fieldsChanged].sort(), warnings };
7707
8301
  }
8302
+ var PROMISE_FIELDS;
7708
8303
  var init_amend = __esm(() => {
7709
8304
  init_src();
7710
8305
  init_grammar2();
7711
8306
  init__envelope();
7712
8307
  init__result();
7713
8308
  init_mint();
8309
+ PROMISE_FIELDS = ["statement", "why", "term", "aliases"];
7714
8310
  });
7715
8311
 
7716
8312
  // src/commands/amend.ts
@@ -7721,9 +8317,9 @@ __export(exports_amend, {
7721
8317
  import { defineCommand as defineCommand18 } from "citty";
7722
8318
  function assertAmendFlags(args) {
7723
8319
  const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
7724
- const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.term !== undefined || args.aliases !== undefined;
8320
+ const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
7725
8321
  if (!hasField && !hasIssue) {
7726
- console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --term / --aliases");
8322
+ console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --relates / --term / --aliases");
7727
8323
  process.exit(EXIT.USAGE);
7728
8324
  }
7729
8325
  if (args.text !== undefined && args.text.trim() === "") {
@@ -7750,6 +8346,9 @@ function fieldsFromArgs(args, rawArgs) {
7750
8346
  const livesIn = listFlag(rawArgs, "lives", args.lives);
7751
8347
  if (livesIn !== undefined)
7752
8348
  fields.livesIn = livesIn;
8349
+ const relates = listFlag(rawArgs, "relates", args.relates);
8350
+ if (relates !== undefined)
8351
+ fields.relates = relates;
7753
8352
  if (args.term !== undefined)
7754
8353
  fields.term = args.term.trim();
7755
8354
  if (args.issue !== undefined && args.issue.trim() !== "")
@@ -7762,7 +8361,6 @@ var amendArgs, amendCommand;
7762
8361
  var init_amend2 = __esm(() => {
7763
8362
  init_constants();
7764
8363
  init_discover();
7765
- init__index();
7766
8364
  init_amend();
7767
8365
  init_grammar3();
7768
8366
  init__args();
@@ -7777,6 +8375,7 @@ var init_amend2 = __esm(() => {
7777
8375
  text: { type: "string", description: "New Requirement (statement) field value" },
7778
8376
  why: { type: "string", description: "New Why it matters field value" },
7779
8377
  lives: livesArg,
8378
+ relates: relatesArg,
7780
8379
  issue: {
7781
8380
  type: "string",
7782
8381
  description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
@@ -7791,7 +8390,7 @@ var init_amend2 = __esm(() => {
7791
8390
  amendCommand = defineCommand18({
7792
8391
  meta: {
7793
8392
  name: "amend",
7794
- description: "Revise an unshipped requirement's fields in place (same id, no specVersion bump). Supersede shipped truth instead."
8393
+ description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --lives and --relates still amend."
7795
8394
  },
7796
8395
  args: amendArgs,
7797
8396
  async run({ args, rawArgs }) {
@@ -7808,7 +8407,7 @@ var init_amend2 = __esm(() => {
7808
8407
  handleNotAPlatform(e);
7809
8408
  }
7810
8409
  const fields = fieldsFromArgs(args, rawArgs);
7811
- const result = await amend({ platformDir, id, fields }, coldFreshTags(platformDir));
8410
+ const result = await amend({ platformDir, id, fields });
7812
8411
  if (!result.ok)
7813
8412
  exitOnFailure("spec amend", result);
7814
8413
  printWarnings("spec amend", result.warnings);
@@ -7930,13 +8529,27 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
7930
8529
 
7931
8530
  // src/server/mcp.ts
7932
8531
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7933
- import { z as z3 } from "zod";
8532
+ import { z as z4 } from "zod";
7934
8533
  function jsonResult(value) {
7935
8534
  return { content: [{ type: "text", text: JSON.stringify(value) }] };
7936
8535
  }
7937
8536
  function errorResult(message) {
7938
8537
  return { content: [{ type: "text", text: message }], isError: true };
7939
8538
  }
8539
+ function amendFieldsOf(args) {
8540
+ const fields = {};
8541
+ if (args.text !== undefined)
8542
+ fields.statement = args.text;
8543
+ if (args.why !== undefined)
8544
+ fields.why = args.why || null;
8545
+ if (args.lives !== undefined)
8546
+ fields.livesIn = args.lives;
8547
+ if (args.relates !== undefined)
8548
+ fields.relates = args.relates;
8549
+ if (args.issue)
8550
+ fields.issue = args.issue;
8551
+ return fields;
8552
+ }
7940
8553
  function buildMcpServer(platformDir) {
7941
8554
  const server = new McpServer({ name: "spec", version: package_default.version });
7942
8555
  const fresh = (fn) => withIndex({ platformDir, build: "fresh" }, (h) => fn(h.storage));
@@ -7944,8 +8557,8 @@ function buildMcpServer(platformDir) {
7944
8557
  title: "Full-text requirement retrieval",
7945
8558
  description: "Search requirements by full text (SQLite FTS5 MATCH syntax; wrap phrases in double quotes). Returns ranked hits \u2014 use this to load the requirements a task touches instead of re-asking the user.",
7946
8559
  inputSchema: {
7947
- text: z3.string().min(1).describe("FTS5 query text"),
7948
- limit: z3.number().int().min(1).max(LIMIT_MAX).optional().describe(`Max hits (default ${DEFAULT_QUERY_LIMIT})`)
8560
+ text: z4.string().min(1).describe("FTS5 query text"),
8561
+ limit: z4.number().int().min(1).max(LIMIT_MAX).optional().describe(`Max hits (default ${DEFAULT_QUERY_LIMIT})`)
7949
8562
  }
7950
8563
  }, async ({ text, limit }) => fresh((s) => {
7951
8564
  const r = query(s, text, limit ?? DEFAULT_QUERY_LIMIT);
@@ -7955,13 +8568,13 @@ function buildMcpServer(platformDir) {
7955
8568
  title: "Files \u2192 requirements",
7956
8569
  description: "The requirements tagged in the given files (platform-relative paths, e.g. api/src/renew.ts). Load these before changing the files.",
7957
8570
  inputSchema: {
7958
- files: z3.array(z3.string().min(1)).min(1).max(100).describe("Platform-relative file paths")
8571
+ files: z4.array(z4.string().min(1)).min(1).max(100).describe("Platform-relative file paths")
7959
8572
  }
7960
8573
  }, async ({ files }) => fresh((s) => jsonResult(resolveFiles(s, files).rows)));
7961
8574
  server.registerTool("spec_req_tags", {
7962
8575
  title: "Requirement \u2192 tag sites (reverse query)",
7963
8576
  description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis and retag worklists.",
7964
- inputSchema: { req_id: z3.string().describe("Requirement id (KEY-NNN)") }
8577
+ inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
7965
8578
  }, async ({ req_id }) => {
7966
8579
  if (!ID_RE.test(req_id)) {
7967
8580
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
@@ -7984,7 +8597,7 @@ function buildMcpServer(platformDir) {
7984
8597
  server.registerTool("spec_propagation", {
7985
8598
  title: "Cross-repo propagation for a superseded requirement",
7986
8599
  description: "Per member repo: migrated to the successor (verified or not), still on the predecessor, or unrelated \u2014 plus the drift flag.",
7987
- inputSchema: { req_id: z3.string().describe("Requirement id (KEY-NNN)") }
8600
+ inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
7988
8601
  }, async ({ req_id }) => {
7989
8602
  if (!ID_RE.test(req_id)) {
7990
8603
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
@@ -7994,7 +8607,7 @@ function buildMcpServer(platformDir) {
7994
8607
  server.registerTool("spec_next_id", {
7995
8608
  title: "Next unused requirement id",
7996
8609
  description: "Allocate-preview the next unused id in a domain (filesystem-derived; nothing is written). Use before authoring a new requirement.",
7997
- inputSchema: { domain: z3.string().min(1).describe("Domain key (e.g. BILLING)") }
8610
+ inputSchema: { domain: z4.string().min(1).describe("Domain key (e.g. BILLING)") }
7998
8611
  }, async ({ domain: domain2 }) => {
7999
8612
  const r = await nextId(platformDir, domain2);
8000
8613
  return r.ok ? jsonResult({ domain: r.key, next_id: r.nextId }) : errorResult(r.detail);
@@ -8002,7 +8615,7 @@ function buildMcpServer(platformDir) {
8002
8615
  server.registerTool("spec_get", {
8003
8616
  title: "One requirement's full record",
8004
8617
  description: "The full record for one requirement id: status, key, seq, statement, why, versions, and source location. An unknown id returns [] (the `spec get --json` shape).",
8005
- inputSchema: { req_id: z3.string().describe("Requirement id (KEY-NNN)") }
8618
+ inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
8006
8619
  }, async ({ req_id }) => {
8007
8620
  if (!ID_RE.test(req_id)) {
8008
8621
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
@@ -8016,8 +8629,8 @@ function buildMcpServer(platformDir) {
8016
8629
  title: "Every requirement record",
8017
8630
  description: "Every requirement record in (key, seq) order, superseded and deprecated history included. Optional domain and status filters. The `spec list --json` shape.",
8018
8631
  inputSchema: {
8019
- domain: z3.string().optional().describe("Only this domain key (e.g. BILLING)"),
8020
- status: z3.string().optional().describe(`Only this status: ${STATUS_WORDS.join(" | ")}`)
8632
+ domain: z4.string().optional().describe("Only this domain key (e.g. BILLING)"),
8633
+ status: z4.string().optional().describe(`Only this status: ${STATUS_WORDS.join(" | ")}`)
8021
8634
  }
8022
8635
  }, async ({ domain: domain2, status }) => {
8023
8636
  const filter = {};
@@ -8036,17 +8649,18 @@ function buildMcpServer(platformDir) {
8036
8649
  title: "Supersede a shipped requirement",
8037
8650
  description: "Flip an Active requirement to superseded, mint its successor in the same domain, and return the retag worklist (the tag sites spec check reports as SUPERSEDED_REFERENCED until retagged). The same JSON as `spec supersede --json`.",
8038
8651
  inputSchema: {
8039
- req_id: z3.string().describe("The Active requirement id to supersede (KEY-NNN)"),
8040
- statement: z3.string().min(1).describe("The successor's statement"),
8041
- why: z3.string().optional().describe("Successor's why (default: copied from the predecessor)"),
8042
- lives_in: z3.array(z3.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
8043
- issue: z3.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
8044
- }
8045
- }, async ({ req_id, statement, why, lives_in, issue }) => {
8652
+ req_id: z4.string().describe("The Active requirement id to supersede (KEY-NNN)"),
8653
+ statement: z4.string().min(1).describe("The successor's statement"),
8654
+ why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
8655
+ lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
8656
+ relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
8657
+ issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
8658
+ }
8659
+ }, async ({ req_id, statement, why, lives_in, relates, issue }) => {
8046
8660
  if (!ID_RE.test(req_id)) {
8047
8661
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8048
8662
  }
8049
- const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, issue }, coldFreshTags(platformDir));
8663
+ const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, relates, issue }, coldFreshTags(platformDir));
8050
8664
  if (!r.ok)
8051
8665
  return errorResult(r.detail);
8052
8666
  for (const w of r.warnings)
@@ -8063,8 +8677,8 @@ function buildMcpServer(platformDir) {
8063
8677
  title: "Deprecate a requirement",
8064
8678
  description: "Mark an Active or Draft requirement deprecated with a recorded reason and return the code tags still bound to it (each a DEPRECATED_REFERENCED error in spec check until removed). The same JSON as `spec deprecate --json`.",
8065
8679
  inputSchema: {
8066
- req_id: z3.string().describe("The requirement id to deprecate (KEY-NNN)"),
8067
- reason: z3.string().min(1).describe("Why the requirement ended; the durable record")
8680
+ req_id: z4.string().describe("The requirement id to deprecate (KEY-NNN)"),
8681
+ reason: z4.string().min(1).describe("Why the requirement ended; the durable record")
8068
8682
  }
8069
8683
  }, async ({ req_id, reason }) => {
8070
8684
  if (!ID_RE.test(req_id)) {
@@ -8080,21 +8694,109 @@ function buildMcpServer(platformDir) {
8080
8694
  title: "Accept a draft requirement",
8081
8695
  description: "Promote a Draft requirement to Active, leaving every other field untouched. The same JSON as `spec accept --json`.",
8082
8696
  inputSchema: {
8083
- req_id: z3.string().describe("The Draft requirement id to accept (KEY-NNN)")
8697
+ req_id: z4.string().describe("The Draft requirement id to accept (KEY-NNN)")
8698
+ }
8699
+ }, async ({ req_id }) => {
8700
+ if (!ID_RE.test(req_id)) {
8701
+ return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8702
+ }
8703
+ const r = await accept({ platformDir, id: req_id });
8704
+ return r.ok ? jsonResult({ id: r.id, file: r.file }) : errorResult(r.detail);
8705
+ });
8706
+ server.registerTool("spec_req", {
8707
+ title: "Author a requirement",
8708
+ description: "Append a new requirement to a domain (Active, or Draft with draft: true). The same JSON as `spec req --text \u2026 --json`: { id, file }, plus clauses when the statement parses as EARS.",
8709
+ inputSchema: {
8710
+ domain: z4.string().min(1).describe("Domain key or unique case-insensitive prefix"),
8711
+ text: z4.string().trim().min(1).describe("The requirement statement"),
8712
+ why: z4.string().trim().default("").describe("What breaks without it"),
8713
+ lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
8714
+ relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
8715
+ issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
8716
+ draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
8717
+ }
8718
+ }, async ({ domain: domain2, text, why, lives, relates, issue, draft }) => {
8719
+ const allocated = await nextId(platformDir, domain2);
8720
+ if (!allocated.ok)
8721
+ return errorResult(allocated.detail);
8722
+ const r = await mint({
8723
+ platformDir,
8724
+ key: allocated.key,
8725
+ statement: text,
8726
+ why,
8727
+ livesIn: lives,
8728
+ relates,
8729
+ issue: issue || undefined,
8730
+ status: draft ? "draft" : "active"
8731
+ });
8732
+ if (!r.ok)
8733
+ return errorResult(r.detail);
8734
+ for (const w of r.warnings)
8735
+ console.error(`spec_req: ${w.text}`);
8736
+ return jsonResult(r.clauses ? { id: r.id, file: r.file, clauses: r.clauses } : { id: r.id, file: r.file });
8737
+ });
8738
+ server.registerTool("spec_amend", {
8739
+ title: "Amend a requirement in place",
8740
+ description: "Revise an Active or Draft requirement's fields in place (same id, no version change); a shipped requirement's statement and why are superseded instead. The same JSON as `spec amend --json`.",
8741
+ inputSchema: {
8742
+ id: z4.string().describe("The requirement id to amend (KEY-NNN)"),
8743
+ text: z4.string().trim().min(1).optional().describe("New statement"),
8744
+ why: z4.string().trim().optional().describe("New why; empty clears it"),
8745
+ lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
8746
+ relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
8747
+ issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
8748
+ }
8749
+ }, async ({ id, text, why, lives, relates, issue }) => {
8750
+ if (!ID_RE.test(id))
8751
+ return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8752
+ const fields = amendFieldsOf({ text, why, lives, relates, issue });
8753
+ if (Object.keys(fields).length === 0) {
8754
+ return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, relates");
8084
8755
  }
8085
- }, async ({ req_id }) => {
8086
- if (!ID_RE.test(req_id)) {
8087
- return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8756
+ const r = await amend({ platformDir, id, fields });
8757
+ if (!r.ok)
8758
+ return errorResult(r.detail);
8759
+ for (const w of r.warnings)
8760
+ console.error(`spec_amend: ${w.text}`);
8761
+ return jsonResult({ id, file: r.file, fields_changed: r.fieldsChanged });
8762
+ });
8763
+ server.registerTool("spec_move", {
8764
+ title: "Move a requirement to another domain",
8765
+ description: "Mint the successor in newDomain carrying the source's fields, mark the source superseded, and return the retag worklist. The same JSON as `spec move --json`.",
8766
+ inputSchema: {
8767
+ id: z4.string().describe("The Active requirement id to move (KEY-NNN)"),
8768
+ newDomain: z4.string().min(1).describe("The existing target domain key"),
8769
+ text: z4.string().optional().describe("Successor statement (default: copied)"),
8770
+ why: z4.string().optional().describe("Successor why (default: copied)"),
8771
+ lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)")
8088
8772
  }
8089
- const r = await accept({ platformDir, id: req_id });
8090
- return r.ok ? jsonResult({ id: r.id, file: r.file }) : errorResult(r.detail);
8773
+ }, async ({ id, newDomain: newDomain2, text, why, lives }) => {
8774
+ if (!ID_RE.test(id))
8775
+ return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8776
+ const targetKey = normalizeDomainKey(newDomain2);
8777
+ if (targetKey === "")
8778
+ return errorResult(`newDomain must be a domain key; got ${newDomain2}`);
8779
+ const r = await move({ platformDir, id, targetKey, statement: text, why, livesIn: lives }, coldFreshTags(platformDir));
8780
+ if (!r.ok)
8781
+ return errorResult(r.detail);
8782
+ for (const w of r.warnings)
8783
+ console.error(`spec_move: ${w.text}`);
8784
+ return jsonResult({
8785
+ old_id: r.oldId,
8786
+ new_id: r.newId,
8787
+ from_file: r.fromFile,
8788
+ to_file: r.toFile,
8789
+ source_spec_version: r.sourceSpecVersion,
8790
+ target_spec_version: r.targetSpecVersion,
8791
+ retag: r.retag
8792
+ });
8091
8793
  });
8092
8794
  server.registerPrompt("author_requirements", {
8093
8795
  title: "Author requirements from a brief",
8094
8796
  description: "Turn a vague brief/ticket into well-formed Spec Engine requirements: one requirement per testable promise, placed by domain charter, deduped via spec query, drafted to the GUARD template + cold-read rubric, minted with spec req. The engine returns a static playbook template \u2014 your model runs it.",
8095
8797
  argsSchema: {
8096
- brief: z3.string().min(1).describe("The vague brief / ticket text to author from"),
8097
- domain: z3.string().optional().describe("Optional target domain KEY \u2014 its charter/scope is injected when given")
8798
+ brief: z4.string().min(1).describe("The vague brief / ticket text to author from"),
8799
+ domain: z4.string().optional().describe("Optional target domain KEY \u2014 its charter/scope is injected when given")
8098
8800
  }
8099
8801
  }, async ({ brief, domain: domain2 }) => {
8100
8802
  let resolvedDomain;
@@ -8125,8 +8827,11 @@ var init_mcp = __esm(() => {
8125
8827
  init_domains();
8126
8828
  init__index();
8127
8829
  init_accept();
8830
+ init_amend();
8128
8831
  init_check();
8129
8832
  init_deprecate();
8833
+ init_mint();
8834
+ init_move();
8130
8835
  init_nextId();
8131
8836
  init_reads2();
8132
8837
  init_records();
@@ -8175,7 +8880,7 @@ __export(exports_resolve, {
8175
8880
  resolveCommand: () => resolveCommand
8176
8881
  });
8177
8882
  import { statSync as statSync5 } from "fs";
8178
- import { join as join12, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
8883
+ import { join as join14, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
8179
8884
  import { defineCommand as defineCommand21 } from "citty";
8180
8885
  function classifyArg(a) {
8181
8886
  if (a.startsWith("--")) {
@@ -8214,7 +8919,7 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
8214
8919
  const resolved = resolve13(last);
8215
8920
  if (statSync5(resolved).isDirectory()) {
8216
8921
  try {
8217
- if (statSync5(join12(resolved, "spec-engine")).isDirectory()) {
8922
+ if (statSync5(join14(resolved, "spec-engine")).isDirectory()) {
8218
8923
  lastIsPlatformDir = true;
8219
8924
  } else {
8220
8925
  lastIsDirWithoutSpecEngine = true;
@@ -8254,7 +8959,7 @@ async function runReverseQuery(opts) {
8254
8959
  if (outArg2)
8255
8960
  assertContainedPath(dbPath, platformDir, "spec resolve: --out");
8256
8961
  await maybePromptForOnboarding({ platformDir, args: { noPrompt } });
8257
- await withReadStorage({ platformDir, dbPath, fresh }, (storage2) => {
8962
+ await withReadStorage({ platformDir, dbPath, fresh, json }, (storage2) => {
8258
8963
  const { rows, known } = reqTags(storage2, reqId);
8259
8964
  if (rows.length === 0) {
8260
8965
  if (!known) {
@@ -8343,7 +9048,7 @@ var init_resolve2 = __esm(() => {
8343
9048
  },
8344
9049
  req: {
8345
9050
  type: "string",
8346
- description: "Reverse query (T8): list every tag site (repo, file, line, kind, level) for the given requirement id instead of resolving files. Takes no file positionals."
9051
+ description: "Reverse query: list every tag site (repo, file, line, kind, level) for the given requirement id instead of resolving files. Takes no file positionals."
8347
9052
  },
8348
9053
  out: {
8349
9054
  type: "string",
@@ -8365,7 +9070,7 @@ var init_resolve2 = __esm(() => {
8365
9070
  resolveCommand = defineCommand21({
8366
9071
  meta: {
8367
9072
  name: "resolve",
8368
- 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."
9073
+ description: "Return the requirements tagged in the given files. 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."
8369
9074
  },
8370
9075
  args: resolveArgs,
8371
9076
  async run({ args, rawArgs }) {
@@ -8398,7 +9103,7 @@ var init_resolve2 = __esm(() => {
8398
9103
  noPrompt: args.noPrompt
8399
9104
  }
8400
9105
  });
8401
- await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
9106
+ await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
8402
9107
  const rows = storage2.resolveByFiles(normalized);
8403
9108
  if (!args.json && rows.length === 0 && storage2.listRequirements().length === 0) {
8404
9109
  console.error(formatNoRequirementsIndexed(platformDir));
@@ -8525,8 +9230,8 @@ var exports_gate = {};
8525
9230
  __export(exports_gate, {
8526
9231
  gateCommand: () => gateCommand
8527
9232
  });
8528
- import { existsSync as existsSync12 } from "fs";
8529
- import { join as join13 } from "path";
9233
+ import { existsSync as existsSync13 } from "fs";
9234
+ import { join as join15 } from "path";
8530
9235
  import { defineCommand as defineCommand22 } from "citty";
8531
9236
  function isEpipe(e) {
8532
9237
  if (e === null || typeof e !== "object" || !("code" in e))
@@ -8550,7 +9255,7 @@ function resolveGateArgs(args) {
8550
9255
  process.exit(EXIT.USAGE);
8551
9256
  }
8552
9257
  const platformDir = resolvePlatformDir(args);
8553
- if (!existsSync12(join13(platformDir, "spec-engine"))) {
9258
+ if (!existsSync13(join15(platformDir, "spec-engine"))) {
8554
9259
  console.error(formatNotASpecPlatform(platformDir));
8555
9260
  process.exit(EXIT.USAGE);
8556
9261
  }
@@ -8623,6 +9328,7 @@ var init_gate2 = __esm(() => {
8623
9328
  writeStdoutTolerant(`build_id: ${gated.buildId}`);
8624
9329
  }
8625
9330
  } catch (e) {
9331
+ handleInvalidDomainFiles(e, Boolean(args.json));
8626
9332
  const msg = e instanceof Error ? e.message : String(e);
8627
9333
  console.error(`spec gate: crashed: ${msg}`);
8628
9334
  process.exit(EXIT.USAGE);
@@ -8708,205 +9414,6 @@ function renderGuard(losses, mode, ref) {
8708
9414
  }
8709
9415
  var STOP = "\uD83D\uDED1";
8710
9416
 
8711
- // src/guard/collect.ts
8712
- import { join as join14 } from "path";
8713
- function isIgnored(path) {
8714
- return isPathIgnored(path);
8715
- }
8716
- function isSpecFile(path) {
8717
- return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
8718
- }
8719
- function isCodeFile(path) {
8720
- if (isSpecFile(path) || isIgnored(path))
8721
- return false;
8722
- const ext = path.includes(".") ? path.split(".").pop() ?? "" : "";
8723
- return CODE_EXTS.has(ext);
8724
- }
8725
- function parseReqs(text) {
8726
- const v = parseDomainText(text, "");
8727
- return v.ok ? v.data.requirements : [];
8728
- }
8729
- function collectBaseReqs2(platformDir, ref, specPaths2) {
8730
- const reqs = [];
8731
- const path = new Map;
8732
- for (const p of specPaths2) {
8733
- const bytes = gitShow(platformDir, ref, p);
8734
- if (bytes === null)
8735
- continue;
8736
- for (const r of parseReqs(bytes)) {
8737
- reqs.push(r);
8738
- path.set(r.id, p);
8739
- }
8740
- }
8741
- return { reqs, path };
8742
- }
8743
- function collectBaseTags(platformDir, ref, codePaths) {
8744
- const implSite = new Map;
8745
- const verifySite = new Map;
8746
- for (const p of codePaths) {
8747
- const bytes = gitShow(platformDir, ref, p);
8748
- if (bytes === null)
8749
- continue;
8750
- for (const tag of scanTagsInFile("", p, bytes)) {
8751
- const target = tag.kind === "verifies" ? verifySite : implSite;
8752
- if (!target.has(tag.req_id))
8753
- target.set(tag.req_id, { file: tag.file, line: tag.line });
8754
- }
8755
- }
8756
- return { implSite, verifySite };
8757
- }
8758
- async function collectWorktreeReqs(canonicalDir) {
8759
- const ids = new Set;
8760
- const activeIds = new Set;
8761
- const supersedesTargets = new Set;
8762
- for (const rel of await findDomainJsonFiles(canonicalDir)) {
8763
- const text = await Bun.file(join14(canonicalDir, rel)).text();
8764
- for (const r of parseReqs(text)) {
8765
- ids.add(r.id);
8766
- if (r.status.toLowerCase() === "active")
8767
- activeIds.add(r.id);
8768
- if (r.supersedes != null)
8769
- supersedesTargets.add(r.supersedes);
8770
- }
8771
- }
8772
- return { ids, activeIds, supersedesTargets };
8773
- }
8774
- function tallyWorktreeTags(storage2) {
8775
- const implCount = new Map;
8776
- const verifyCount = new Map;
8777
- for (const t of storage2.listTags()) {
8778
- const bucket = t.kind === "implements" ? implCount : t.kind === "verifies" ? verifyCount : null;
8779
- if (bucket !== null)
8780
- bucket.set(t.req_id, (bucket.get(t.req_id) ?? 0) + 1);
8781
- }
8782
- return { implCount, verifyCount };
8783
- }
8784
- async function collectFacts(platformDir, ref, storage2) {
8785
- const changed = gitDiffNameStatus(platformDir, ref);
8786
- const specPaths2 = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
8787
- const codePaths = changed.filter((e) => isCodeFile(e.path)).map((e) => e.path);
8788
- const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
8789
- const base = collectBaseReqs2(platformDir, ref, specPaths2);
8790
- const baseTags = collectBaseTags(platformDir, ref, codePaths);
8791
- const worktree = await collectWorktreeReqs(join14(platformDir, CANONICAL_SPECS_DIR));
8792
- const counts = tallyWorktreeTags(storage2);
8793
- return {
8794
- baseReqs: base.reqs,
8795
- baseReqPath: base.path,
8796
- baseImplSite: baseTags.implSite,
8797
- baseVerifySite: baseTags.verifySite,
8798
- worktreeReqIds: worktree.ids,
8799
- worktreeActiveIds: worktree.activeIds,
8800
- worktreeSupersedesTargets: worktree.supersedesTargets,
8801
- worktreeImplCount: counts.implCount,
8802
- worktreeVerifyCount: counts.verifyCount,
8803
- deletedSpecFiles
8804
- };
8805
- }
8806
- var CODE_EXTS;
8807
- var init_collect = __esm(() => {
8808
- init_src();
8809
- init_constants();
8810
- init_fs();
8811
- init_tags();
8812
- CODE_EXTS = new Set(DEFAULT_EXTS);
8813
- });
8814
-
8815
- // src/guard/losses.ts
8816
- function isActive(status) {
8817
- return status.toLowerCase() === "active";
8818
- }
8819
- function isSuperseded(base, f) {
8820
- const forward = base.supersededBy != null && f.worktreeReqIds.has(base.supersededBy);
8821
- return forward || f.worktreeSupersedesTargets.has(base.id);
8822
- }
8823
- function reqDeletedLoss(base, f) {
8824
- return {
8825
- kind: "REQUIREMENT_REMOVED",
8826
- req_id: base.id,
8827
- file: f.baseReqPath.get(base.id) ?? "",
8828
- line: 0,
8829
- 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`
8830
- };
8831
- }
8832
- function historyRemovedLoss(base, f) {
8833
- const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
8834
- return {
8835
- kind: "REQUIREMENT_REMOVED",
8836
- req_id: base.id,
8837
- file: f.baseReqPath.get(base.id) ?? "",
8838
- line: 0,
8839
- 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`
8840
- };
8841
- }
8842
- function implLostLoss(id, site) {
8843
- return {
8844
- kind: "IMPL_LOST",
8845
- req_id: id,
8846
- file: site.file,
8847
- line: site.line,
8848
- detail: `${id} lost its last implementing @spec tag (was ${site.file}:${site.line})`
8849
- };
8850
- }
8851
- function verifyLostLoss(id, site) {
8852
- return {
8853
- kind: "VERIFY_LOST",
8854
- req_id: id,
8855
- file: site.file,
8856
- line: site.line,
8857
- detail: `${id} lost its last verifying @spec tag (was ${site.file}:${site.line})`
8858
- };
8859
- }
8860
- function specFileDeletedLoss(path) {
8861
- return {
8862
- kind: "SPEC_FILE_DELETED",
8863
- req_id: null,
8864
- file: path,
8865
- line: 0,
8866
- detail: `canonical spec file ${path} present at the ref is absent from the working tree`
8867
- };
8868
- }
8869
- function reqDeletedFor(base, f) {
8870
- if (f.worktreeReqIds.has(base.id))
8871
- return null;
8872
- if (!isActive(base.status))
8873
- return historyRemovedLoss(base, f);
8874
- if (isSuperseded(base, f))
8875
- return null;
8876
- return reqDeletedLoss(base, f);
8877
- }
8878
- function lastTagGone(id, count, f) {
8879
- return f.worktreeActiveIds.has(id) && count === 0;
8880
- }
8881
- function classifyLosses(f) {
8882
- const out = [];
8883
- for (const base of f.baseReqs) {
8884
- const loss = reqDeletedFor(base, f);
8885
- if (loss !== null)
8886
- out.push(loss);
8887
- }
8888
- for (const [id, site] of f.baseImplSite) {
8889
- if (lastTagGone(id, f.worktreeImplCount.get(id) ?? 0, f))
8890
- out.push(implLostLoss(id, site));
8891
- }
8892
- for (const [id, site] of f.baseVerifySite) {
8893
- if (lastTagGone(id, f.worktreeVerifyCount.get(id) ?? 0, f))
8894
- out.push(verifyLostLoss(id, site));
8895
- }
8896
- for (const path of f.deletedSpecFiles)
8897
- out.push(specFileDeletedLoss(path));
8898
- return out;
8899
- }
8900
-
8901
- // src/operations/guard.ts
8902
- async function guard(storage2, platformDir, ref) {
8903
- const losses = classifyLosses(await collectFacts(platformDir, ref, storage2));
8904
- return { ok: true, ref, losses };
8905
- }
8906
- var init_guard = __esm(() => {
8907
- init_collect();
8908
- });
8909
-
8910
9417
  // src/commands/guard.ts
8911
9418
  var exports_guard = {};
8912
9419
  __export(exports_guard, {
@@ -8945,7 +9452,7 @@ var init_guard2 = __esm(() => {
8945
9452
  process.exit(EXIT.OK);
8946
9453
  }
8947
9454
  let losses = [];
8948
- await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true }, async (storage2) => {
9455
+ await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
8949
9456
  losses = (await guard(storage2, platformDir, ref)).losses;
8950
9457
  });
8951
9458
  console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
@@ -10962,6 +11469,10 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
10962
11469
  Lives in (optional, single path)
10963
11470
  <input type="text" name="livesIn" placeholder="path/to/impl.ts" />
10964
11471
  </label>
11472
+ <label>
11473
+ Relates to (optional, comma-separated ids)
11474
+ <input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
11475
+ </label>
10965
11476
  <button type="submit">Create</button>
10966
11477
  </form>
10967
11478
  </section>
@@ -10996,6 +11507,7 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
10996
11507
  const statement = field(form, "statement");
10997
11508
  const why = field(form, "why");
10998
11509
  const livesIn = field(form, "livesIn");
11510
+ const relates = field(form, "relates");
10999
11511
  const payload = {
11000
11512
  key: field(form, "key"),
11001
11513
  statement
@@ -11004,6 +11516,8 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11004
11516
  payload.why = why;
11005
11517
  if (livesIn.length > 0)
11006
11518
  payload.livesIn = livesIn;
11519
+ if (relates.length > 0)
11520
+ payload.relates = relates;
11007
11521
  const res = await app.request("/api/requirements", {
11008
11522
  method: "POST",
11009
11523
  headers: { "content-type": "application/json" },
@@ -11840,6 +12354,9 @@ function guarded(handler) {
11840
12354
  try {
11841
12355
  return await handler(c);
11842
12356
  } catch (err) {
12357
+ if (err instanceof InvalidDomainFilesError) {
12358
+ return c.json({ error: "invalid_domain_file", diagnostics: sortDiagnostics(err.diagnostics) }, 422);
12359
+ }
11843
12360
  const info = describeStorageError(err);
11844
12361
  if (info === null)
11845
12362
  throw err;
@@ -11904,7 +12421,7 @@ async function readJsonBody(c) {
11904
12421
  }
11905
12422
  return { ok: true, body: parsed };
11906
12423
  }
11907
- function toLivesIn(v) {
12424
+ function toStringList(v) {
11908
12425
  if (Array.isArray(v))
11909
12426
  return v.filter((x) => typeof x === "string");
11910
12427
  if (typeof v === "string" && v.length > 0)
@@ -11915,11 +12432,20 @@ function mintFieldsFromBody(body) {
11915
12432
  return {
11916
12433
  statement: typeof body.statement === "string" ? body.statement : "",
11917
12434
  why: typeof body.why === "string" ? body.why : "",
11918
- livesIn: toLivesIn(body.livesIn),
12435
+ livesIn: toStringList(body.livesIn),
12436
+ relates: toStringList(body.relates),
11919
12437
  issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
11920
12438
  status: body.status === "draft" ? "draft" : "active"
11921
12439
  };
11922
12440
  }
12441
+ function successorFieldsFromBody(body) {
12442
+ return {
12443
+ why: typeof body.why === "string" ? body.why : undefined,
12444
+ livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
12445
+ relates: "relates" in body ? toStringList(body.relates) : undefined,
12446
+ issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
12447
+ };
12448
+ }
11923
12449
  function amendFieldsFromBody(body) {
11924
12450
  const fields = {};
11925
12451
  if (typeof body.statement === "string")
@@ -11927,7 +12453,9 @@ function amendFieldsFromBody(body) {
11927
12453
  if ("why" in body)
11928
12454
  fields.why = typeof body.why === "string" ? body.why : null;
11929
12455
  if ("livesIn" in body)
11930
- fields.livesIn = toLivesIn(body.livesIn);
12456
+ fields.livesIn = toStringList(body.livesIn);
12457
+ if ("relates" in body)
12458
+ fields.relates = toStringList(body.relates);
11931
12459
  return Object.keys(fields).length === 0 ? null : fields;
11932
12460
  }
11933
12461
  function failureResponse(c, failure) {
@@ -12034,12 +12562,9 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12034
12562
  return withWriteLock(async () => {
12035
12563
  const fields = amendFieldsFromBody(body);
12036
12564
  if (fields === null) {
12037
- return c.json({ error: "nothing to amend \u2014 provide statement, why, or livesIn" }, 400);
12565
+ return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, or relates" }, 400);
12038
12566
  }
12039
- const r = await amend({ platformDir, id, fields }, async (reqId) => {
12040
- await runIndex({ platformDir, storage: storage2 });
12041
- return storage2.listTags({ req_id: reqId });
12042
- });
12567
+ const r = await amend({ platformDir, id, fields });
12043
12568
  if (!r.ok)
12044
12569
  return failureResponse(c, r);
12045
12570
  await runIndex({ platformDir, storage: storage2 });
@@ -12067,13 +12592,50 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12067
12592
  if (statement === "")
12068
12593
  return c.json({ error: "statement is required (non-empty)" }, 400);
12069
12594
  return withWriteLock(async () => {
12070
- const r = await supersede({
12595
+ const r = await supersede({ platformDir, id, statement, ...successorFieldsFromBody(body) }, async (reqId) => {
12596
+ await runIndex({ platformDir, storage: storage2 });
12597
+ return storage2.listTags({ req_id: reqId });
12598
+ });
12599
+ if (!r.ok)
12600
+ return failureResponse(c, r);
12601
+ return c.json({
12602
+ ok: true,
12603
+ old_id: r.oldId,
12604
+ new_id: r.newId,
12605
+ file: r.file,
12606
+ spec_version: r.specVersion,
12607
+ retag: r.retag
12608
+ }, 201);
12609
+ });
12610
+ }));
12611
+ app.post("/api/requirements/:id/move", guarded(async function moveRequirement(c) {
12612
+ if (!featureEnabled("editor")) {
12613
+ return c.json({ error: featureDisabledMessage("editor") }, 404);
12614
+ }
12615
+ const originReject = rejectCrossOrigin(c, bindHost);
12616
+ if (originReject)
12617
+ return originReject;
12618
+ const headerReject = rejectBadWriteHeaders(c);
12619
+ if (headerReject)
12620
+ return headerReject;
12621
+ const parsed = await readJsonBody(c);
12622
+ if (!parsed.ok)
12623
+ return parsed.res;
12624
+ const body = parsed.body;
12625
+ const id = c.req.param("id") ?? "";
12626
+ if (!ID_RE.test(id))
12627
+ return c.json({ error: "id must be a requirement id (KEY-NNN)" }, 400);
12628
+ const targetKey = normalizeDomainKey(typeof body.newDomain === "string" ? body.newDomain : "");
12629
+ if (targetKey === "")
12630
+ return c.json({ error: "newDomain is required (a domain key)" }, 400);
12631
+ return withWriteLock(async () => {
12632
+ const r = await move({
12071
12633
  platformDir,
12072
12634
  id,
12073
- statement,
12635
+ targetKey,
12636
+ statement: typeof body.text === "string" ? body.text : undefined,
12074
12637
  why: typeof body.why === "string" ? body.why : undefined,
12075
- livesIn: "livesIn" in body ? toLivesIn(body.livesIn) : undefined,
12076
- issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
12638
+ livesIn: "lives" in body ? toStringList(body.lives) : undefined
12077
12639
  }, async (reqId) => {
12078
12640
  await runIndex({ platformDir, storage: storage2 });
12079
12641
  return storage2.listTags({ req_id: reqId });
@@ -12084,8 +12646,10 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12084
12646
  ok: true,
12085
12647
  old_id: r.oldId,
12086
12648
  new_id: r.newId,
12087
- file: r.file,
12088
- spec_version: r.specVersion,
12649
+ from_file: r.fromFile,
12650
+ to_file: r.toFile,
12651
+ source_spec_version: r.sourceSpecVersion,
12652
+ target_spec_version: r.targetSpecVersion,
12089
12653
  retag: r.retag
12090
12654
  }, 201);
12091
12655
  });
@@ -12228,6 +12792,7 @@ var init_api = __esm(() => {
12228
12792
  init_amend();
12229
12793
  init_deprecate();
12230
12794
  init_mint();
12795
+ init_move();
12231
12796
  init_reads2();
12232
12797
  init_records();
12233
12798
  init_supersede();
@@ -12358,7 +12923,7 @@ __export(exports_serve, {
12358
12923
  resolveBind: () => resolveBind,
12359
12924
  composeServeApp: () => composeServeApp
12360
12925
  });
12361
- import { existsSync as existsSync13, mkdirSync as mkdirSync6 } from "fs";
12926
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
12362
12927
  import { dirname as dirname8 } from "path";
12363
12928
  import { defineCommand as defineCommand24 } from "citty";
12364
12929
  import { Hono as Hono2 } from "hono";
@@ -12411,7 +12976,7 @@ async function runProbe() {
12411
12976
  }
12412
12977
  const body = await res.text();
12413
12978
  if (!body.includes("Spec Engine \u2014 coming online")) {
12414
- console.error(`serve --probe FAILED: body did not contain the placeholder string (D-14)`);
12979
+ console.error(`serve --probe FAILED: body did not contain the placeholder string`);
12415
12980
  server.stop();
12416
12981
  process.exit(EXIT.FAILURE);
12417
12982
  }
@@ -12440,7 +13005,7 @@ var init_serve = __esm(() => {
12440
13005
  serveCommand = defineCommand24({
12441
13006
  meta: {
12442
13007
  name: "serve",
12443
- description: "Run the local webapp. Binds 127.0.0.1 on --port, or one private --host address behind an access token. --probe is the SERV-04 asset-embedding smoke."
13008
+ description: "Run the local webapp. Binds 127.0.0.1 on --port, or one private --host address behind an access token. --probe is the asset-embedding smoke."
12444
13009
  },
12445
13010
  args: {
12446
13011
  platformDir: platformDirArg,
@@ -12510,7 +13075,7 @@ var init_serve = __esm(() => {
12510
13075
  }
12511
13076
  });
12512
13077
  mkdirSync6(dirname8(dbPath), { recursive: true });
12513
- const needsIndex = !existsSync13(dbPath);
13078
+ const needsIndex = !existsSync14(dbPath);
12514
13079
  let storage2;
12515
13080
  try {
12516
13081
  storage2 = openStorage(dbPath);
@@ -12531,6 +13096,7 @@ var init_serve = __esm(() => {
12531
13096
  });
12532
13097
  } catch (err) {
12533
13098
  storage2.close();
13099
+ handleInvalidDomainFiles(err, false);
12534
13100
  handleStorageUnavailable(err, dbPath);
12535
13101
  console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
12536
13102
  process.exit(EXIT.FAILURE);
@@ -12557,13 +13123,13 @@ __export(exports_docs, {
12557
13123
  docsCommand: () => docsCommand,
12558
13124
  createDocsFetchHandler: () => createDocsFetchHandler
12559
13125
  });
12560
- import { existsSync as existsSync14, statSync as statSync6 } from "fs";
12561
- import { join as join15, normalize, sep as sep4 } from "path";
13126
+ import { existsSync as existsSync15, statSync as statSync6 } from "fs";
13127
+ import { join as join16, normalize, sep as sep4 } from "path";
12562
13128
  import { fileURLToPath } from "url";
12563
13129
  import { defineCommand as defineCommand25 } from "citty";
12564
13130
  function resolveDocsRootFrom(candidates) {
12565
13131
  for (const candidate of candidates) {
12566
- if (existsSync14(join15(candidate, "index.html")))
13132
+ if (existsSync15(join16(candidate, "index.html")))
12567
13133
  return candidate;
12568
13134
  }
12569
13135
  return null;
@@ -12587,17 +13153,17 @@ function createDocsFetchHandler(root) {
12587
13153
  if (pathname.includes("\x00")) {
12588
13154
  return new Response("Bad request", { status: 400 });
12589
13155
  }
12590
- const target = normalize(join15(normalRoot, pathname));
13156
+ const target = normalize(join16(normalRoot, pathname));
12591
13157
  if (target !== normalRoot && !target.startsWith(rootPrefix)) {
12592
13158
  return new Response("Not found", { status: 404 });
12593
13159
  }
12594
- for (const candidate of [target, join15(target, "index.html"), `${target}.html`]) {
12595
- if (existsSync14(candidate) && statSync6(candidate).isFile()) {
13160
+ for (const candidate of [target, join16(target, "index.html"), `${target}.html`]) {
13161
+ if (existsSync15(candidate) && statSync6(candidate).isFile()) {
12596
13162
  return new Response(Bun.file(candidate));
12597
13163
  }
12598
13164
  }
12599
- const notFoundPage = join15(normalRoot, "404.html");
12600
- if (existsSync14(notFoundPage)) {
13165
+ const notFoundPage = join16(normalRoot, "404.html");
13166
+ if (existsSync15(notFoundPage)) {
12601
13167
  return new Response(Bun.file(notFoundPage), { status: 404 });
12602
13168
  }
12603
13169
  return new Response("Not found", { status: 404 });
@@ -12647,7 +13213,7 @@ var init_docs2 = __esm(() => {
12647
13213
  port: {
12648
13214
  type: "string",
12649
13215
  default: "4100",
12650
- description: "Port (default 4100; 0 = ephemeral). Hostname is hardcoded 127.0.0.1 (T-1-01); no --host accepted."
13216
+ description: "Port (default 4100; 0 = ephemeral). Hostname is hardcoded 127.0.0.1; no --host accepted."
12651
13217
  },
12652
13218
  probe: {
12653
13219
  type: "boolean",