@spec-engine/spec-engine 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/AGENTS.md +678 -0
  3. package/dist/agent/mcp.json +11 -0
  4. package/dist/agent/pre-commit +3 -0
  5. package/dist/agent/skills/req-author/SKILL.md +102 -0
  6. package/dist/docs/agent-reference/index.html +1280 -33
  7. package/dist/docs/architecture/index.html +6 -6
  8. package/dist/docs/checks/index.html +19 -266
  9. package/dist/docs/commands/index.html +32 -164
  10. package/dist/docs/format/index.html +2 -9
  11. package/dist/docs/getting-started/index.html +3 -3
  12. package/dist/docs/guides/platform-setup/index.html +2 -2
  13. package/dist/docs/pagefind/fragment/en_10e3da6.pf_fragment +0 -0
  14. package/dist/docs/pagefind/fragment/en_37cae5e.pf_fragment +0 -0
  15. package/dist/docs/pagefind/fragment/en_7fc2fcd.pf_fragment +0 -0
  16. package/dist/docs/pagefind/fragment/en_96d3bc1.pf_fragment +0 -0
  17. package/dist/docs/pagefind/fragment/en_a26aa07.pf_fragment +0 -0
  18. package/dist/docs/pagefind/fragment/en_b7e68ef.pf_fragment +0 -0
  19. package/dist/docs/pagefind/fragment/en_d8a5bcd.pf_fragment +0 -0
  20. package/dist/docs/pagefind/fragment/en_de67937.pf_fragment +0 -0
  21. package/dist/docs/pagefind/fragment/en_fc3c55b.pf_fragment +0 -0
  22. package/dist/docs/pagefind/index/en_1664b45.pf_index +0 -0
  23. package/dist/docs/pagefind/index/en_a8b8bb6.pf_index +0 -0
  24. package/dist/docs/pagefind/pagefind-entry.json +1 -1
  25. package/dist/docs/pagefind/pagefind.en_bc3d4a7766.pf_meta +0 -0
  26. package/dist/docs/platform-mapping/index.html +2 -1
  27. package/dist/docs/tags/index.html +9 -92
  28. package/dist/impl.js +2011 -703
  29. package/package.json +1 -1
  30. package/dist/docs/pagefind/fragment/en_1f98c2d.pf_fragment +0 -0
  31. package/dist/docs/pagefind/fragment/en_24f76b6.pf_fragment +0 -0
  32. package/dist/docs/pagefind/fragment/en_4bd9de6.pf_fragment +0 -0
  33. package/dist/docs/pagefind/fragment/en_9cc1878.pf_fragment +0 -0
  34. package/dist/docs/pagefind/fragment/en_9cfc306.pf_fragment +0 -0
  35. package/dist/docs/pagefind/fragment/en_b2867b9.pf_fragment +0 -0
  36. package/dist/docs/pagefind/fragment/en_b81dc52.pf_fragment +0 -0
  37. package/dist/docs/pagefind/fragment/en_c3abf3d.pf_fragment +0 -0
  38. package/dist/docs/pagefind/fragment/en_c3e92c6.pf_fragment +0 -0
  39. package/dist/docs/pagefind/index/en_6e50ed4.pf_index +0 -0
  40. package/dist/docs/pagefind/index/en_afc98e5.pf_index +0 -0
  41. package/dist/docs/pagefind/pagefind.en_d0b5c328bc.pf_meta +0 -0
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.8.0",
23
+ version: "0.9.0",
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,6 +73,31 @@ var init_package = __esm(() => {
73
73
  };
74
74
  });
75
75
 
76
+ // src/stdout.ts
77
+ import { format } from "util";
78
+ function isEpipe(e) {
79
+ return typeof e === "object" && e !== null && "code" in e && (e.code === "EPIPE" || e.code === "ERR_STREAM_DESTROYED");
80
+ }
81
+ function routeStdout() {
82
+ process.stdout.on("error", (e) => {
83
+ if (!isEpipe(e))
84
+ throw e;
85
+ });
86
+ console.log = (...args) => {
87
+ const text = `${format(...args)}
88
+ `;
89
+ lastWrite = new Promise((written) => process.stdout.write(text, () => written()));
90
+ };
91
+ }
92
+ async function exit(code) {
93
+ await lastWrite;
94
+ process.exit(code);
95
+ }
96
+ var lastWrite;
97
+ var init_stdout = __esm(() => {
98
+ lastWrite = Promise.resolve();
99
+ });
100
+
76
101
  // ../shared/src/diagnostics.ts
77
102
  var DiagnosticCode, NotASpecPlatformError, InvalidDomainFilesError;
78
103
  var init_diagnostics = __esm(() => {
@@ -84,15 +109,13 @@ var init_diagnostics = __esm(() => {
84
109
  SUPERSEDED_REFERENCED: "SUPERSEDED_REFERENCED",
85
110
  DEPRECATED_REFERENCED: "DEPRECATED_REFERENCED",
86
111
  STATEMENT_GRAMMAR: "STATEMENT_GRAMMAR",
112
+ STATEMENT_RULES: "STATEMENT_RULES",
87
113
  DRAFT_REFERENCED: "DRAFT_REFERENCED",
88
114
  GLOSSARY_DRIFT: "GLOSSARY_DRIFT",
89
115
  ORPHAN_REQ: "ORPHAN_REQ",
90
116
  UNVERIFIED_REQ: "UNVERIFIED_REQ",
91
117
  DRIFT: "DRIFT",
92
118
  NO_SPEC_CONFIG: "NO_SPEC_CONFIG",
93
- BROKEN_FILE_REF: "BROKEN_FILE_REF",
94
- UNIMPLEMENTED_IN_FILE: "UNIMPLEMENTED_IN_FILE",
95
- UNVERIFIED_IN_FILE: "UNVERIFIED_IN_FILE",
96
119
  BROKEN_RELATES: "BROKEN_RELATES",
97
120
  RELATES_SUPERSEDED: "RELATES_SUPERSEDED",
98
121
  CYCLIC_SUPERSEDE: "CYCLIC_SUPERSEDE",
@@ -100,6 +123,7 @@ var init_diagnostics = __esm(() => {
100
123
  UNKNOWN_ROLE: "UNKNOWN_ROLE",
101
124
  UNSOURCED_CHANGE: "UNSOURCED_CHANGE",
102
125
  INVALID_DOMAIN_FILE: "INVALID_DOMAIN_FILE",
126
+ MIGRATION_PENDING: "MIGRATION_PENDING",
103
127
  UNPROVEN_REQ: "UNPROVEN_REQ",
104
128
  PROOFS_UNCONFIRMED: "PROOFS_UNCONFIRMED",
105
129
  REQUIREMENT_REMOVED: "REQUIREMENT_REMOVED",
@@ -214,8 +238,6 @@ function orderDomain(d) {
214
238
  supersedes: r.supersedes ?? null,
215
239
  supersededBy: r.supersededBy ?? null,
216
240
  relates: r.relates,
217
- livesIn: r.livesIn,
218
- verifiedIn: r.verifiedIn,
219
241
  issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
220
242
  changedAtVersion: r.changedAtVersion,
221
243
  supersededAtVersion: r.supersededAtVersion,
@@ -262,6 +284,10 @@ function preserveShape(raw, rawDomain, domain) {
262
284
  const next = orderDomain(domain);
263
285
  patchFields(raw, before, next);
264
286
  const rawEntries = raw.requirements;
287
+ for (const entry of rawEntries) {
288
+ for (const key of LEGACY_ENTRY_KEYS)
289
+ delete entry[key];
290
+ }
265
291
  const byId = new Map;
266
292
  rawDomain.requirements.forEach((r, i) => {
267
293
  byId.set(r.id, [...byId.get(r.id) ?? [], i]);
@@ -332,7 +358,7 @@ async function validateAndWrite(path, domain, sourceFile) {
332
358
  await Bun.write(path, serialized);
333
359
  return { ok: true };
334
360
  }
335
- var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, SpecRequirementSchema, SpecDomainSchema, sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
361
+ var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, LEGACY_ENTRY_KEYS, SpecRequirementSchema, SpecDomainSchema, sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
336
362
  var init_domain = __esm(() => {
337
363
  init_diagnostics();
338
364
  ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
@@ -345,6 +371,7 @@ var init_domain = __esm(() => {
345
371
  term: z.string(),
346
372
  pinned: z.number().int()
347
373
  }).strict();
374
+ LEGACY_ENTRY_KEYS = ["livesIn", "verifiedIn"];
348
375
  SpecRequirementSchema = z.object({
349
376
  id: z.string().regex(ID_RE),
350
377
  status: z.string().min(1),
@@ -353,8 +380,8 @@ var init_domain = __esm(() => {
353
380
  supersedes: z.string().nullable().optional(),
354
381
  supersededBy: z.string().nullable().optional(),
355
382
  relates: z.array(z.string()).optional().default([]),
356
- livesIn: z.array(z.string()).optional().default([]),
357
- verifiedIn: z.array(z.string()).optional().default([]),
383
+ livesIn: z.unknown().optional(),
384
+ verifiedIn: z.unknown().optional(),
358
385
  issues: z.array(SpecIssueSchema).optional().default([]),
359
386
  changedAtVersion: z.number().int().optional(),
360
387
  supersededAtVersion: z.number().int().optional(),
@@ -363,7 +390,7 @@ var init_domain = __esm(() => {
363
390
  aliases: z.array(z.string()).optional().default([]),
364
391
  cites: z.array(SpecCiteSchema).optional().default([]),
365
392
  section: z.string().nullable().optional()
366
- }).strict();
393
+ }).strict().transform(({ livesIn: _l, verifiedIn: _v, ...entry }) => entry);
367
394
  SpecDomainSchema = z.object({
368
395
  key: z.string().regex(KEY_RE),
369
396
  owner: z.string().nullable().optional(),
@@ -376,6 +403,159 @@ var init_domain = __esm(() => {
376
403
  }).strict();
377
404
  });
378
405
 
406
+ // ../shared/src/statementRules.ts
407
+ function jargonPatterns(words) {
408
+ if (words.length === 0)
409
+ return [];
410
+ return [new RegExp(`\\b(?:${words.map(escapeRegExp).join("|")})\\b`, "i")];
411
+ }
412
+ function withoutExemptNames(statement, names) {
413
+ let out = statement;
414
+ for (const name of names) {
415
+ const tail = name.endsWith("/") ? "[^\\s`]*" : "(?![\\w-])";
416
+ out = out.replace(new RegExp(`(?<![\\w.-])${escapeRegExp(name)}${tail}`, "g"), "X");
417
+ }
418
+ return out;
419
+ }
420
+ function sentenceDetail(statement) {
421
+ if (/[\r\n]/.test(statement))
422
+ return "contains a line break";
423
+ const prose = statement.replace(QUOTED, "Q").replace(NON_TERMINAL, "N");
424
+ const terminals = prose.match(TERMINAL)?.length ?? 0;
425
+ if (terminals === 1 && /[.?!]$/.test(prose.trimEnd()))
426
+ return null;
427
+ return `${terminals} sentence terminators; exactly one, at the end`;
428
+ }
429
+ function hitsDetail(text, patterns) {
430
+ const hits = patterns.map((pattern) => text.match(pattern)?.[0]).filter((hit) => hit !== undefined);
431
+ return hits.length > 0 ? hits.map((hit) => `"${hit}"`).join(", ") : null;
432
+ }
433
+ function statementFindings(statement, config = {}) {
434
+ const maxLength = config.maxLength ?? DEFAULT_MAX_STATEMENT_LENGTH;
435
+ const details = {
436
+ "one-sentence": sentenceDetail(statement),
437
+ length: statement.length > maxLength ? `${statement.length} characters; at most ${maxLength}` : null,
438
+ "no-file-path": hitsDetail(withoutExemptNames(statement, config.exemptNames ?? []), FILE_PATH),
439
+ "no-test-narration": null,
440
+ "no-toolchain-layout": null,
441
+ "no-history": null,
442
+ "no-jargon": hitsDetail(statement, jargonPatterns(config.jargon ?? DEFAULT_JARGON)),
443
+ "no-tool-statement": hitsDetail(statement, TOOL_STATEMENT)
444
+ };
445
+ for (const { rule, patterns } of FIXED_RULES)
446
+ details[rule] = hitsDetail(statement, patterns);
447
+ const findings = [];
448
+ for (const rule of STATEMENT_RULES) {
449
+ const detail = details[rule];
450
+ if (detail !== null && config.severity?.[rule] !== "off") {
451
+ findings.push({ rule, detail, reason: STATEMENT_RULE_REASONS[rule] });
452
+ }
453
+ }
454
+ return findings;
455
+ }
456
+ function formatStatementFinding(f) {
457
+ return `${f.rule}: ${f.detail} \u2014 ${f.reason}`;
458
+ }
459
+ var RULE_ZERO = "A requirement is one rule of the product's behavior, stated from the user's point of view.", STATEMENT_RULES, DEFAULT_MAX_STATEMENT_LENGTH = 280, DEFAULT_JARGON, STATEMENT_RULE_REASONS, EXT = "ts|tsx|js|mjs|json|yml|yaml|sql|md|toml|css|astro|sh", FILE_PATH, FIXED_RULES, TOOL_STATEMENT, escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), QUOTED, NON_TERMINAL, TERMINAL;
460
+ var init_statementRules = __esm(() => {
461
+ STATEMENT_RULES = [
462
+ "one-sentence",
463
+ "length",
464
+ "no-file-path",
465
+ "no-test-narration",
466
+ "no-toolchain-layout",
467
+ "no-history",
468
+ "no-jargon",
469
+ "no-tool-statement"
470
+ ];
471
+ DEFAULT_JARGON = [
472
+ "seam",
473
+ "seams",
474
+ "rung",
475
+ "rungs",
476
+ "residue",
477
+ "matrix entry",
478
+ "stage runner",
479
+ "the stage"
480
+ ];
481
+ STATEMENT_RULE_REASONS = {
482
+ "one-sentence": "a statement is one testable sentence",
483
+ length: "a sentence a reader holds at once",
484
+ "no-file-path": "which file holds what is layout, not behaviour",
485
+ "no-test-narration": "how a test is written is not what the product does",
486
+ "no-toolchain-layout": "how CI and tooling are arranged is not a product behaviour",
487
+ "no-history": "a statement says what holds now, not how it got there",
488
+ "no-jargon": "a reader without the codebase cannot parse project jargon",
489
+ "no-tool-statement": "naming the tool in use is a decision record, not a requirement"
490
+ };
491
+ FILE_PATH = [
492
+ /(?<![\w/@-])(?:apps|packages|tooling|modules|spec-engine|\.github)\/[\w.@/-]*/i,
493
+ /`(?![@/<])(?![^`\s]*:\/\/)(?![^`\s./]*\/[^`\s./]*`)[^`\s]*\/[^`\s]*`/,
494
+ new RegExp(`\\b(?!(?:next|node|postgres)\\.js\\b|auth\\.md\\b)[\\w-]+(?:\\.[\\w-]+)*\\.(?:${EXT})\\b`, "i")
495
+ ];
496
+ FIXED_RULES = [
497
+ {
498
+ rule: "no-test-narration",
499
+ patterns: [
500
+ /\b(?:describe|it|test)\(/,
501
+ /\.(?:test|spec)\./i,
502
+ /\btest files?\b/i,
503
+ /\bthe tests?\b/i,
504
+ /\bvitest\b/i,
505
+ /\bplaywright\b/i,
506
+ /\bfixtures?\b(?! (?:accounts?|seed))/i,
507
+ /\bplanted\b/i
508
+ ]
509
+ },
510
+ {
511
+ rule: "no-toolchain-layout",
512
+ patterns: [
513
+ /\bworkflows?\b/i,
514
+ /\b(?:test|full|CI|job|build|RLS|per-table) matrix\b|\bmatrix (?:entry|entries|job)\b/i,
515
+ /\brunners?\b/i,
516
+ /\bGitHub Actions\b/i,
517
+ /\bci\.yml\b/i,
518
+ /(?<!`)\bpnpm\s/i,
519
+ /\bturbo\b/i,
520
+ /\bbiome\b/i,
521
+ /\bdrizzle-kit\b/i,
522
+ /\btsx\s/i,
523
+ /\bbun\s/i,
524
+ /\bShellCheck\b/i,
525
+ /\bcoverage thresholds?\b/i,
526
+ /`(?:pnpm )?ci:/i,
527
+ /@spec(?![\w-])/i,
528
+ /\btag(?:ged)? in\b/i
529
+ ]
530
+ },
531
+ {
532
+ rule: "no-history",
533
+ patterns: [
534
+ /\bamended\b/i,
535
+ /\bsupersed\w*(?: by)? `?[A-Z][A-Z0-9]*-\d/,
536
+ /\breplaced by\b|\breplace[sd] `?(?:ADR-|[A-Z][A-Z0-9]*-\d)/i,
537
+ /\bpreviously\b/i,
538
+ /\bformerly\b/i,
539
+ /\bused to\b/i,
540
+ /\bsince `?\d/i,
541
+ /\bmigration \d/i,
542
+ /\b0\d{3}_/,
543
+ /\bPORT-T/,
544
+ /\bADR-/
545
+ ]
546
+ }
547
+ ];
548
+ TOOL_STATEMENT = [
549
+ /\bwe use\b/i,
550
+ /\bis used for\b/i,
551
+ /\bis the tool\b/i,
552
+ /\bis pinned (?:to|at) (?:`?[\w@./-]+`? )?v?\d/i
553
+ ];
554
+ QUOTED = /`[^`]*`|"[^"]*"|\u201C[^\u201D]*\u201D/g;
555
+ NON_TERMINAL = /\b(?:e\.g|i\.e|etc|vs|cf)\.|\d+(?:\.\d+)+/gi;
556
+ TERMINAL = /[.?!](?=\s|$)/g;
557
+ });
558
+
379
559
  // ../shared/src/checkConfig.ts
380
560
  import { z as z2 } from "zod";
381
561
  function parseCheckConfigText(text, sourceFile) {
@@ -393,10 +573,11 @@ function parseCheckConfigText(text, sourceFile) {
393
573
  const path = issue && issue.path.length > 0 ? issue.path.join(".") : "(root)";
394
574
  return { ok: false, detail: `${sourceFile}: ${path}: ${issue?.message ?? "invalid"}` };
395
575
  }
396
- var CODES, matchEntry, SeverityRuleSchema, CheckConfigSchema;
576
+ var CODES, matchEntry, SeverityRuleSchema, StatementRulesConfigSchema, CheckConfigSchema;
397
577
  var init_checkConfig = __esm(() => {
398
578
  init_diagnostics();
399
579
  init_domain();
580
+ init_statementRules();
400
581
  CODES = Object.values(DiagnosticCode);
401
582
  matchEntry = z2.string().refine((s) => KEY_RE.test(s) || ID_RE.test(s), "must be a domain key or a requirement id");
402
583
  SeverityRuleSchema = z2.object({
@@ -404,7 +585,16 @@ var init_checkConfig = __esm(() => {
404
585
  severity: z2.enum(["error", "warning", "off"]),
405
586
  match: z2.array(matchEntry).optional()
406
587
  }).strict();
407
- CheckConfigSchema = z2.object({ severity: z2.array(SeverityRuleSchema) }).strict();
588
+ StatementRulesConfigSchema = z2.object({
589
+ maxLength: z2.number().int().positive().optional(),
590
+ jargon: z2.array(z2.string().min(1)).optional(),
591
+ exemptNames: z2.array(z2.string().min(1)).optional(),
592
+ severity: z2.partialRecord(z2.enum(STATEMENT_RULES), z2.enum(["error", "warning", "off"])).optional()
593
+ }).strict();
594
+ CheckConfigSchema = z2.object({
595
+ severity: z2.array(SeverityRuleSchema).default([]),
596
+ statementRules: StatementRulesConfigSchema.optional()
597
+ }).strict();
408
598
  });
409
599
 
410
600
  // ../shared/src/config.ts
@@ -844,6 +1034,7 @@ var init_src = __esm(() => {
844
1034
  init_net();
845
1035
  init_propagation();
846
1036
  init_schema();
1037
+ init_statementRules();
847
1038
  init_storage();
848
1039
  });
849
1040
 
@@ -1113,9 +1304,6 @@ function isPathIgnored(path, extra = []) {
1113
1304
  function isHiddenEntry(name) {
1114
1305
  return name.startsWith(".") && name !== ".github";
1115
1306
  }
1116
- function isHiddenPath(rel) {
1117
- return rel.split("/").some(isHiddenEntry);
1118
- }
1119
1307
  async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
1120
1308
  const absDir = relDir ? join2(rootDir, relDir) : rootDir;
1121
1309
  let entries;
@@ -2193,33 +2381,9 @@ class SqliteStorage {
2193
2381
  if (files.length > FILES_MAX) {
2194
2382
  throw new Error(`resolveByFiles: too many files (max ${FILES_MAX} per call; got ${files.length})`);
2195
2383
  }
2196
- const queryFiles = this.#expandSelfMemberFiles(files);
2197
- if (queryFiles.length > 32000) {
2198
- throw new Error(`resolveByFiles: expanded bind count ${queryFiles.length} would exceed ` + "SQLITE_MAX_VARIABLE_NUMBER (32766); lower FILES_MAX");
2199
- }
2200
- const placeholders = queryFiles.map(() => "?").join(",");
2384
+ const placeholders = files.map(() => "?").join(",");
2201
2385
  const sql = RESOLVE_BY_FILES_SQL_PREFIX + placeholders + RESOLVE_BY_FILES_SQL_SUFFIX;
2202
- return this.#db.query(sql).all(...queryFiles);
2203
- }
2204
- #selfMemberName() {
2205
- const canonical = this.getRepo("spec-engine");
2206
- if (!canonical)
2207
- return null;
2208
- const platformRoot = dirname2(canonical.path);
2209
- const row = this.#db.query("SELECT name FROM repos WHERE name != 'spec-engine' AND path = $path").get({ path: platformRoot });
2210
- return row?.name ?? null;
2211
- }
2212
- #expandSelfMemberFiles(files) {
2213
- const self = this.#selfMemberName();
2214
- if (!self)
2215
- return files;
2216
- const prefix = `${self}/`;
2217
- const expanded = [...files];
2218
- for (const f of files) {
2219
- if (!f.startsWith(prefix))
2220
- expanded.push(prefix + f);
2221
- }
2222
- return expanded;
2386
+ return this.#db.query(sql).all(...files);
2223
2387
  }
2224
2388
  withWriteTx(fn) {
2225
2389
  const wrapped = this.#db.transaction((arg) => fn(arg));
@@ -2685,24 +2849,8 @@ function repoExtraIgnore(member) {
2685
2849
  const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
2686
2850
  return [...member.selfMember ? ["spec-engine/"] : [], ...repoIgnore];
2687
2851
  }
2688
- function scannedTagFile(members, platformDir, platformRel) {
2689
- const abs = resolve3(platformDir, platformRel);
2690
- let owner = null;
2691
- for (const member2 of members) {
2692
- const rel2 = relative(member2.path, abs).split(sep2).join("/");
2693
- if (rel2 === "" || rel2 === ".." || rel2.startsWith("../") || isAbsolute(rel2))
2694
- continue;
2695
- if (owner === null || member2.path.length > owner.member.path.length)
2696
- owner = { member: member2, rel: rel2 };
2697
- }
2698
- if (owner === null)
2699
- return null;
2700
- const { member, rel } = owner;
2701
- if (isHiddenPath(rel) || isPathIgnored(rel, repoExtraIgnore(member)))
2702
- return null;
2703
- if (!isTagSource(rel) && !isDocSource(rel))
2704
- return null;
2705
- return `${member.name}/${rel}`;
2852
+ function tagFile(member, rel) {
2853
+ return member.selfMember ? rel : `${member.name}/${rel}`;
2706
2854
  }
2707
2855
  async function scanMemberCode(member, extraIgnore) {
2708
2856
  const hitsOut = [];
@@ -2710,7 +2858,7 @@ async function scanMemberCode(member, extraIgnore) {
2710
2858
  for (const rel of codePaths) {
2711
2859
  const absPath = join4(member.path, rel);
2712
2860
  const text = await Bun.file(absPath).text();
2713
- const platformRelative = `${member.name}/${rel}`;
2861
+ const platformRelative = tagFile(member, rel);
2714
2862
  const hits = scanTagsInFile(member.name, platformRelative, text);
2715
2863
  for (const h of hits)
2716
2864
  hitsOut.push(h);
@@ -2724,7 +2872,7 @@ async function scanMemberDocs(member, extraIgnore) {
2724
2872
  for (const rel of docPaths) {
2725
2873
  const absPath = join4(member.path, rel);
2726
2874
  const text = await Bun.file(absPath).text();
2727
- const platformRelative = `${member.name}/${rel}`;
2875
+ const platformRelative = tagFile(member, rel);
2728
2876
  const { tags, mentions } = scanDocFile(member.name, platformRelative, text);
2729
2877
  for (const t of tags)
2730
2878
  tagsOut.push(t);
@@ -2915,6 +3063,944 @@ var init_pipeline = __esm(() => {
2915
3063
  init_discover();
2916
3064
  });
2917
3065
 
3066
+ // ../../.claude/skills/req-author/SKILL.md
3067
+ var SKILL_default = `---
3068
+ name: req-author
3069
+ description: Turn a rough brief or ticket into Spec Engine requirements \u2014 draft them in the fixed EARS shape, tie every draft to the exact words it came from, present the batch for human approval, and only then mint via the real \`spec\` CLI.
3070
+ ---
3071
+
3072
+ # req-author
3073
+
3074
+ > A requirement is one rule of the product's behavior, stated from the user's point of view.
3075
+
3076
+ The full requirement authoring standard is the "Requirement authoring
3077
+ standard" section of the agent reference
3078
+ (\`packages/site/src/content/docs/agent-reference.md\`); this skill applies it.
3079
+
3080
+ This skill drives the real \`spec\` CLI (\`spec req\`, \`spec query\`,
3081
+ \`spec domain list\`) and never reimplements minting. It is the front half of
3082
+ the lifecycle: brief in, approved requirements out; the route \u2192 tag \u2192 check
3083
+ loop consumes what it produces.
3084
+
3085
+ **Two non-negotiable rules:**
3086
+
3087
+ 1. **Never write without approval.** Draft, self-check, present the batch \u2014
3088
+ then STOP. No \`spec req\` runs until the human explicitly approves.
3089
+ 2. **Every draft quotes its source.** Each drafted requirement carries the
3090
+ exact phrase from the brief it derives from, verbatim. A requirement you
3091
+ cannot anchor to the author's words is labeled **inferred** so the human
3092
+ can accept or reject the inference knowingly. This exists because
3093
+ agent-drafted requirements have shipped that the product owner could not
3094
+ trace back to anything they said.
3095
+
3096
+ ## Procedure
3097
+
3098
+ 1. **Read the brief.** A ticket is temporary work; the requirements outlive
3099
+ it. One brief usually splits into several requirements \u2014 one per behavior,
3100
+ never one per ticket.
3101
+
3102
+ 2. **Draft each requirement in the fixed shape.** Every statement uses one of
3103
+ the five EARS shapes (the word "shall" separates who acts from the
3104
+ observable result):
3105
+
3106
+ | Situation | Shape |
3107
+ |---|---|
3108
+ | Always true | The \`<system>\` shall \`<result>\` |
3109
+ | Triggered by an event | When \`<trigger>\`, the \`<system>\` shall \`<result>\` |
3110
+ | Only in a certain state | While \`<state>\`, the \`<system>\` shall \`<result>\` |
3111
+ | Handling something going wrong | If \`<failure>\`, then the \`<system>\` shall \`<result>\` |
3112
+ | Only when a feature is present | Where \`<feature>\`, the \`<system>\` shall \`<result>\` |
3113
+
3114
+ Example: "When an unknown requirement id is passed, spec resolve shall
3115
+ print [] and exit 0." If a draft fits no row, you have not yet decided
3116
+ what triggers the behavior or what observably happens \u2014 decide, or mark
3117
+ the draft as a question for the human.
3118
+
3119
+ One promise per statement: a result chaining two behaviors with \`and\` is
3120
+ two drafts. Prefer \`When\`, \`If \u2026 then\`, and the bare \`shall\` form; \`While\`
3121
+ and \`Where\` only when a state or feature gates the behavior. Rationale
3122
+ belongs in the why, never in the statement.
3123
+
3124
+ The statement rules are checked the same way: one sentence ending in a
3125
+ period, at most 280 characters, and no repository file or folder, test
3126
+ mechanics, CI or tooling layout, history, project jargon, or tool choice.
3127
+ \`spec req\` refuses a draft that breaks one; \`spec lint <KEY-NNN> .\` lists
3128
+ the findings for an existing requirement.
3129
+
3130
+ 3. **Write a why that names what breaks.** The \`why\` answers "what goes wrong
3131
+ if this stops being true" \u2014 never a restatement of the statement in
3132
+ different words.
3133
+
3134
+ 4. **Place and dedup.** \`spec domain list --json\` shows every domain's
3135
+ charter; put each requirement in the domain whose promise it protects (not
3136
+ where the code file sits). Then \`spec query "<phrase>" . --json\` \u2014 if a
3137
+ close requirement already exists, propose amending or relating it instead
3138
+ of a new id.
3139
+
3140
+ 5. **Present the batch and STOP.** For each candidate show: the domain, the
3141
+ statement, the why, and the **source quote** (or the
3142
+ \`inferred\` label). Write nothing until the human approves. This gate is
3143
+ mandatory.
3144
+
3145
+ 6. **On approval, mint via the real CLI:**
3146
+
3147
+ \`\`\`
3148
+ spec req <domain> --text "<statement>" --why "<what breaks>"
3149
+ \`\`\`
3150
+
3151
+ (or the equivalent \`spec mcp\` tools). \`spec req\` validates through the one
3152
+ shared schema \u2014 in a domain that declares the ears grammar it also checks
3153
+ the statement shape at write time, and with \`--json\` it returns the parsed
3154
+ clauses.
3155
+
3156
+ 7. **Record the ticket as provenance only.** The originating ticket id is
3157
+ history, never a requirement id and never a code \`@spec\` tag.
3158
+
3159
+ 8. **Verify.** \`spec index . && spec check . --ci\` (exit 0) and
3160
+ \`spec guard .\` (no loss) before the change ships.
3161
+
3162
+ ## Invocation note
3163
+
3164
+ The repo's \`.mcp.json\` registers the local \`spec mcp\` server (stdio,
3165
+ loopback), so the tools and the \`author_requirements\` prompt are callable
3166
+ natively from the harness. From a source checkout the server runs as
3167
+ \`bun packages/engine/src/cli.ts mcp .\`; a compiled \`spec\` binary on PATH runs
3168
+ as \`spec mcp .\`.
3169
+ `;
3170
+ var init_SKILL = () => {};
3171
+
3172
+ // ../site/src/content/docs/agent-reference.md
3173
+ var agent_reference_default = `---
3174
+ title: Agent Reference
3175
+ description: The model, the authoring standard, and the exact contract of the spec CLI, for coding agents and the people who direct them
3176
+ ---
3177
+
3178
+ The reference for a coding agent working in a repository that uses Spec Engine. It has three parts: the model and the working loop, the standard every requirement is written against, and the exact contract of every \`spec\` command.
3179
+
3180
+ ## What Spec Engine is
3181
+
3182
+ Spec Engine helps developers, product teams, and agents write better specs and keep them current, so the software changes accurately.
3183
+
3184
+ It works with four things:
3185
+
3186
+ | Noun | What it is | Where it lives | Authored or derived |
3187
+ | --- | --- | --- | --- |
3188
+ | Domain | A named subject area of requirements with an UPPERCASE key (\`BILLING\`), an owner, and a charter that says what belongs in it | \`spec-engine/<KEY>/SPEC.json\`, one file per domain | Authored |
3189
+ | Requirement | One durable rule of the product's behavior, addressed by a permanent id \`KEY-NNN\` (\`BILLING-009\`), with a statement, a why, and a status | An entry in its domain's \`SPEC.json\` | Authored |
3190
+ | Tag | A comment \`@spec KEY-NNN\` that binds a line of code or a test to a requirement | Source and test files, in comments | Authored in code |
3191
+ | Index | A SQLite database of every domain, requirement, and tag, which the read and check commands query | \`.spec-engine/index.sqlite\` at the platform root | Derived |
3192
+
3193
+ - The \`SPEC.json\` files are the only authored truth about what the product promises. Write them through the \`spec\` commands. An id is never reused or renumbered: a changed promise is superseded by a new id, and an ended one is deprecated.
3194
+ - Tags in code are the only truth about where a requirement is implemented and where it is verified. A tag in implementation code implements; a tag in a test file verifies. The kind comes from the file path, never from the tag text.
3195
+ - The index is derived and disposable. Every command rebuilds it from the \`SPEC.json\` files and the tags, and deleting \`.spec-engine/\` is always safe.
3196
+ - Requirements are not tickets. One ticket usually becomes several requirements. A ticket id is provenance only: it goes in \`--issue\`, never in a requirement id or a tag.
3197
+
3198
+ ## The loop
3199
+
3200
+ \`\`\`shell
3201
+ spec query "renewal charge" . --json # 1. route: requirements on the topic
3202
+ spec resolve src/billing/renew.ts . --json # or the ones tagged in a file
3203
+ # 2. implement the change
3204
+ # 3. tag the function that produces the behavior and the test that proves it
3205
+ spec check . --ci --json # 4. exit 1: fix before the PR
3206
+ spec guard . --json # 5. exit 1: the change loses a requirement
3207
+ \`\`\`
3208
+
3209
+ When \`spec guard\` blocks, relay its \`\uD83D\uDED1 spec-guard:\` block verbatim to the user. Then either \`spec supersede\` the requirement with a successor or get the user's explicit decision to \`spec deprecate\` it. Never delete a requirement to make the gate pass.
3210
+
3211
+ ## What to tag
3212
+
3213
+ \`\`\`ts
3214
+ export function renew() { /* \u2026 */ } // @spec BILLING-009
3215
+ it("charges current price", () => {}) // @spec BILLING-009 unit
3216
+ \`\`\`
3217
+
3218
+ - A tag binds the one function that produces the behavior and the one test that proves it.
3219
+ - Helpers, glue code, and configuration carry no tag.
3220
+ - If you cannot name the behavior a line produces, do not tag it.
3221
+ - A test tag may end with a level: \`unit\`, \`integration\`, or \`e2e\`.
3222
+
3223
+ ## In CI
3224
+
3225
+ \`\`\`shell
3226
+ spec check . --ci --base "$(git merge-base HEAD origin/main)"
3227
+ \`\`\`
3228
+
3229
+ Without \`--base\`, \`check\` diffs against \`HEAD\`, which in a CI checkout already holds the change, so the removal and loss checks find nothing to report.
3230
+
3231
+ ## Requirement authoring standard
3232
+
3233
+ <!-- @spec CHRT-024 -->
3234
+
3235
+ The rules every new requirement is written against. The statement shapes and the statement rules are also checked at write time and by \`spec check\`.
3236
+
3237
+ 0. **A requirement is one rule of the product's behavior, stated from the user's point of view.** The statement rules below are the mechanical half of this rule: \`spec req\`, \`amend\`, \`supersede\`, \`move\`, and \`accept\` refuse a statement that breaks one, \`spec check\` reports an Active statement that breaks one as \`STATEMENT_RULES\`, and \`spec lint\` lists every finding. The judgment half stays with the author.
3238
+ 1. **One behavior per requirement.** A ticket usually splits into several requirements. Never mint one requirement per ticket.
3239
+ 2. **Write the statement in a fixed shape.** Pick the sentence shape that matches the situation. The word "shall" separates who acts from the observable result:
3240
+
3241
+ | Situation | Shape | Example |
3242
+ | --- | --- | --- |
3243
+ | Always true | The \`<system>\` shall \`<result>\` | The spec CLI shall write JSON output to stdout only. |
3244
+ | Triggered by an event | When \`<trigger>\`, the \`<system>\` shall \`<result>\` | When an unknown requirement id is passed, spec resolve shall print \`[]\` and exit 0. |
3245
+ | Only in a certain state | While \`<state>\`, the \`<system>\` shall \`<result>\` | While the index file is missing, read commands shall rebuild it before answering. |
3246
+ | Handling something going wrong | If \`<failure>\`, then the \`<system>\` shall \`<result>\` | If the results file cannot be parsed, then spec check shall report the file and exit 2. |
3247
+ | Only when a feature is present | Where \`<feature>\`, the \`<system>\` shall \`<result>\` | Where a member config lists ignore directories, the scanner shall skip them. |
3248
+
3249
+ A statement that fits no row has not yet decided what triggers the behavior or what observably happens.
3250
+
3251
+ A statement may end with a trailing \`so <observable consequence>\` clause ("\u2026shall report NO_SPEC_CONFIG at warning severity, so a lone warning never fails the gate") when the consequence is itself observable. The reason the behavior exists belongs in the why, never in the statement.
3252
+
3253
+ One promise per statement. A result that chains two behaviors with \`and\`, or that sets two severities, is two requirements: a single test must be able to prove the whole statement. Prefer \`When\`, \`If \u2026 then\`, and the bare \`shall\` form. Use \`While\` and \`Where\` only when a state or a feature is what gates the behavior.
3254
+ 3. **It must make sense on its own.** A reader who sees only this one statement, in search results or a report, understands it. A statement that says "the normalization" or "that prefix" without naming the subject fails.
3255
+ 4. **Describe what it does, not how.** Say what a user or calling agent observes. If renaming an internal function would make the statement wrong, it is written at the wrong level.
3256
+ 5. **No expiration dates.** No "currently", no "the new behavior", no references to things that were removed.
3257
+ 6. **The why states what breaks.** Answer "what goes wrong if this stops being true". Never restate the requirement in other words.
3258
+ 7. **Three link types, three jobs.** Tests prove it. Code implements it. Tickets are only history: never an id, never proof.
3259
+ 8. **File it where the promise lives, and search first.** Pick the domain whose charter covers the behavior, and run \`spec query\` before minting. If something close exists, amend it instead.
3260
+
3261
+ ### Requirement id format
3262
+
3263
+ <!-- @spec CHRT-023 -->
3264
+
3265
+ - Ids are \`KEY-NNN\`: the domain key, a hyphen, and a three-digit zero-padded number (\`BILLING-010\`).
3266
+ - Ids are permanent. A requirement is superseded or deprecated, never renumbered and never deleted.
3267
+
3268
+ ### The one-promise lint
3269
+
3270
+ A domain envelope may declare \`grammar: "ears"\` with \`grammarSeverity: "warning" | "error"\` (default warning). Set both with \`spec domain grammar <KEY>\`, never by hand. In such a domain a statement must fit one of the five shapes, and a statement that fits a shape still fails when:
3271
+
3272
+ | Rule | Fails | Passes |
3273
+ | --- | --- | --- |
3274
+ | Semicolon clause | \`; \` anywhere after the first \`shall\` | \`When a; b holds, X shall \u2026\` |
3275
+ | Second subject | \`, and <subject> shall\` after the first \`shall\` | \`X shall A, and shall B\` |
3276
+ | Rationale | \`because\`, \`so that\`, \`in order to\` anywhere | a trailing \`, so <observable consequence>\` |
3277
+
3278
+ - \`spec check\` reports a nonconforming Active or Draft statement as \`STATEMENT_GRAMMAR\` at the declared severity, one row per statement, the detail naming the first failed shape or rule.
3279
+ - \`spec req\`, \`amend\`, \`supersede\`, and \`move\` judge new text at write time: they warn and write under warning, and refuse (exit 2) under error.
3280
+ - The lint is a pattern, not a reading: a statement that passes can still chain two behaviors, and the reviewer still owns that call.
3281
+ - The TERM domain is exempt.
3282
+
3283
+ ### The statement rules
3284
+
3285
+ Every domain is judged by these rules, whatever its grammar. \`spec req\`, \`amend\`, \`supersede\`, \`move\` (with new text), and \`accept\` refuse a statement that breaks a rule graded \`error\` (the default): exit 2, one \`<rule>: <detail> \u2014 <reason>\` line per finding, rule 0 last. A rule graded \`warning\` writes the statement and prints the same finding lines on stderr. There is no override flag; the \`statementRules\` block of \`check.json\` grades a rule, tunes it, or turns it off. \`spec check\` reports an Active statement that breaks one as \`STATEMENT_RULES\`, one row per rule per statement. The TERM domain is never judged.
3286
+
3287
+ | Rule | Reports | Printed reason |
3288
+ | --- | --- | --- |
3289
+ | \`one-sentence\` | more than one sentence, no closing period, or a line break | a statement is one testable sentence |
3290
+ | \`length\` | over \`maxLength\` characters (280 by default) | a sentence a reader holds at once |
3291
+ | \`no-file-path\` | a repository path or file name | which file holds what is layout, not behaviour |
3292
+ | \`no-test-narration\` | how a test is written | how a test is written is not what the product does |
3293
+ | \`no-toolchain-layout\` | CI or tooling layout | how CI and tooling are arranged is not a product behaviour |
3294
+ | \`no-history\` | how the requirement changed | a statement says what holds now, not how it got there |
3295
+ | \`no-jargon\` | a word from the jargon list | a reader without the codebase cannot parse project jargon |
3296
+ | \`no-tool-statement\` | which tool is in use | naming the tool in use is a decision record, not a requirement |
3297
+
3298
+ ## Authoring requirements (brief \u2192 mint)
3299
+
3300
+ <!-- @spec AUTHOR-013 -->
3301
+
3302
+ <!-- @spec AUTHOR-012 -->
3303
+
3304
+ The procedure for turning a brief or ticket into requirements. Each draft is written to the requirement authoring standard above.
3305
+
3306
+ 1. **Split the brief.** One requirement per behavior, never one per ticket.
3307
+ 2. **Place each.** \`spec domain list --json\` prints every charter. File the requirement where the promise lives.
3308
+ 3. **Dedup.** \`spec query "<phrase>" . --json\` before minting. Overlap means \`spec amend\` or relate the existing requirement.
3309
+ 4. **Draft to the standard.** Every draft quotes the phrase of the brief it derives from.
3310
+ 5. **Mint.** \`spec req <domain> --text "<statement>" --why "<what breaks>" [--issue <ticket>]\`.
3311
+ 6. **Provenance, not identity.** The ticket goes in \`--issue\`, never in an id or a tag.
3312
+ 7. **Verify.** \`spec index . && spec check . --ci\` (exit 0) and \`spec guard .\` before the PR.
3313
+
3314
+ ## Command reference
3315
+
3316
+ ### Invocation
3317
+
3318
+ \`\`\`shell
3319
+ spec <command> [...] # after: bun add -g @spec-engine/spec-engine
3320
+ bunx @spec-engine/spec-engine <command> [...] # without a global install (requires Bun)
3321
+ \`\`\`
3322
+
3323
+ - Almost every command takes an optional trailing \`platformDir\` positional (default: cwd). The platform dir is the directory that contains \`spec-engine/\`.
3324
+ - Every command is non-interactive when stdin is not a TTY.
3325
+ - \`--json\` goes to **stdout only**. Guidance, warnings, and diagnostics chrome go to **stderr**. Parse stdout, surface stderr.
3326
+
3327
+ ### Platforms, members, and pins
3328
+
3329
+ - A platform is the directory holding \`spec-engine/\`. When declared, its platform file \`platform-map.json\` is \`{ name, members, ignore }\`.
3330
+ - A member is a repository named in that file, carrying a \`{ platform, member }\` marker. A monorepo's coverage columns are its workspace packages, named by path (\`packages/engine\`). A lone repository with no platform file is one column or its packages. A monorepo's files outside every package are one more column, named by the member or, for a lone monorepo, the directory.
3331
+ - \`spec-engine.member.json\` adds one fact: the member's pin, \`spec-engine@N\`. \`spec init <name>\` declares and pins in one step.
3332
+ - A requirement domain's version is derived from its supersede history. The platform version is the max domain version. Drift compares a member's pin against each referenced requirement's \`changed_at_version\`. A stray \`spec-engine.platform.json\` is ignored with a warning.
3333
+ - A member's \`dependsOn\` (the platform's own package names in its manifest) orders \`spec propagation\`: providers before consumers.
3334
+
3335
+ ### Tags
3336
+
3337
+ - The tag is \`@spec KEY-NNN\`, optionally followed by a level token \`unit\` | \`integration\` | \`e2e\` on a test tag. Never write "implements" or "verifies" in the tag.
3338
+ - The scanner reads \`.ts\`, \`.tsx\`, \`.js\`, \`.jsx\`, \`.mjs\`, \`.sh\`, \`.lua\`, \`.luau\`, \`.go\`, \`.rs\`, \`.tf\`, \`.toml\`, \`.sql\`, \`.css\`, \`.yml\`, \`.yaml\`, and \`.astro\` files, any file named \`Dockerfile\` or \`Dockerfile.<suffix>\`, and \`.md\` documents, whose tags document.
3339
+ - The tag regex ignores the comment leader, so \`--\`, \`//\`, \`#\`, \`<!-- -->\`, and \`/* \u2026 */\` all work.
3340
+ - Dot-prefixed directories are skipped except \`.github/\`. \`fixtures/\` directories are ignored everywhere.
3341
+ - A tag in implementation code implements; a tag in a test path verifies. A test path contains \`.test.\`, \`.spec.\`, \`__tests__/\`, \`/tests/\`, \`/e2e/\`, or \`.e2e.\`, or ends in \`_test.go\`, \`_test.rs\`, \`_spec.lua\`, or \`_test.lua\`, or lives under \`spec/\` (busted).
3342
+ - Rust tests inline under \`#[cfg(test)]\` sit in the implementation file and count as implements; put a verifying tag in \`tests/\` or a \`*_test.rs\` file.
3343
+
3344
+ ### Exit code contract
3345
+
3346
+ | Code | Meaning |
3347
+ | --- | --- |
3348
+ | 0 | Success. Empty results are success (\`[]\` on stdout, guidance on stderr). |
3349
+ | 1 | Data-level failure: \`check\` found an error-severity diagnostic; \`guard\` found a loss; \`gate\` failed; \`index\` crashed mid-build. |
3350
+ | 2 | Usage/environment error: bad args, not a Spec Engine platform (no \`spec-engine/\`), path-containment violation, invalid \`--limit\`/\`--port\`, FTS5 syntax error. |
3351
+
3352
+ Branch on exit codes, not on output text.
3353
+
3354
+ ### Commands
3355
+
3356
+ | Command | Synopsis | \`--json\` | Exits |
3357
+ | --- | --- | --- | --- |
3358
+ | \`spec index [platformDir]\` | Build or refresh the derived index | yes (IndexResult) | 0 / 1 / 2 |
3359
+ | \`spec check [platformDir] [--ci]\` | Integrity + coverage + drift diagnostics | yes (array) | 0 / 1 / 2 |
3360
+ | \`spec guard [platformDir] [--against <ref>]\` | Block a change about to lose a requirement, its last implementation, or its last test | yes (array) | 0 / 1 / 2 |
3361
+ | \`spec map [platformDir]\` | Requirement \xD7 repo coverage matrix | yes (array) | 0 / 2 |
3362
+ | \`spec get <KEY-NNN> [platformDir]\` | One requirement's full record and its tag sites; unknown id is \`[]\` | yes (object, or \`[]\`) | 0 / 2 |
3363
+ | \`spec list [platformDir] [--domain KEY] [--status S]\` | Every requirement record in (key, seq) order, history included | yes (array) | 0 / 2 |
3364
+ | \`spec query <text> [platformDir]\` | Full-text retrieval over requirements | yes (array) | 0 / 2 |
3365
+ | \`spec relations [platformDir]\` | Mermaid graph of \`relates\` links | yes (array) | 0 / 2 |
3366
+ | \`spec provenance [issueId] [platformDir]\` | Per-requirement provenance matrix | yes (array) | 0 / 2 |
3367
+ | \`spec resolve <files\u2026> [platformDir]\` | Requirements tagged in the given files | yes (array) | 0 / 2 |
3368
+ | \`spec propagation <KEY-NNN> [platformDir]\` | Per-repo migrated/drifted state for a superseded requirement | yes (array) | 0 / 2 |
3369
+ | \`spec gate <repo> <KEY-NNN> [platformDir]\` | Approval gate: pass iff Active and the pin covers it | yes (object) | 0 / 1 / 2 |
3370
+ | \`spec init [repo]\` | Declare a member through platform-map and write its \`spec-engine.member.json\` pin | yes (object) | 0 / 2 |
3371
+ | \`spec domain new <KEY>\` / \`spec domain list\` / \`spec domain grammar <KEY>\` | Scaffold / list \`spec-engine/<KEY>/SPEC.json\` / set its statement grammar and severity | \`list\`: yes (array); \`grammar\`: yes (object) | 0 / 2 |
3372
+ | \`spec migrate [platformDir]\` | Strip the retired \`livesIn\` and \`verifiedIn\` fields from every \`SPEC.json\` (idempotent) | yes (object) | 0 / 2 |
3373
+ | \`spec req <domain-prefix> [platformDir]\` | Piped: next unused id. \`--text\`: author non-interactively | yes (object) | 0 / 2 |
3374
+ | \`spec term \u2026\` | Author, list, revise, and confirm glossary terms | yes | 0 / 2 |
3375
+ | \`spec glossary [platformDir]\` | Generate \`GLOSSARY.md\` from the TERM store; \`--migrate\`, \`--check\` | yes (object) | 0 / 1 / 2 |
3376
+ | \`spec deprecate <KEY-NNN> --reason "\u2026" [platformDir]\` | End an Active/Draft requirement with a recorded reason | yes (object) | 0 / 2 |
3377
+ | \`spec supersede <KEY-NNN> [platformDir]\` | Flip to superseded, mint the successor, retag every site to it | yes (object) | 0 / 2 |
3378
+ | \`spec move <KEY-NNN> <NEW-DOMAIN> [platformDir]\` | Cross-domain supersede | yes (object) | 0 / 2 |
3379
+ | \`spec amend <KEY-NNN> [platformDir]\` | Revise an unshipped entry in place | yes (object) | 0 / 2 |
3380
+ | \`spec accept <KEY-NNN> [platformDir]\` | Promote a Draft requirement to Active | yes (object) | 0 / 2 |
3381
+ | \`spec lint [KEY \\| KEY-NNN] [platformDir] [--all]\` | Every statement-rule finding for the platform, a domain, or a requirement | yes (array) | 0 / 1 / 2 |
3382
+ | \`spec serve [platformDir] [--port N] [--host IP]\` | Local webapp + \`/api/*\` over the index | n/a (HTTP) | 0 / 1 / 2 |
3383
+ | \`spec docs [--port N]\` | Serve the bundled docs site offline | n/a (HTTP) | 0 / 1 / 2 |
3384
+ | \`spec mcp [platformDir]\` | MCP server over stdio | n/a (JSON-RPC) | 0 / 2 |
3385
+
3386
+ Common flags on the index-touching commands (\`index\`, \`check\`, \`map\`, \`get\`, \`list\`, \`query\`, \`relations\`, \`resolve\`, \`propagation\`, \`provenance\`, \`gate\`, \`serve\`):
3387
+
3388
+ | Flag | Effect |
3389
+ | --- | --- |
3390
+ | \`--out <path>\` | Index DB path override. Must resolve inside \`platformDir\` (exit 2 otherwise). |
3391
+ | \`--no-prompt\` | Suppress the member-onboarding prompt. |
3392
+ | \`--json\` | Machine output (all except \`serve\`). |
3393
+ | \`--fresh\` | Read commands only: delete the DB plus WAL/SHM and reindex before answering. |
3394
+
3395
+ \`init\`, \`domain list\`, \`req\` register \`--json\` only. \`domain new\` registers none.
3396
+
3397
+ **Index freshness.** Read commands (\`map\`, \`get\`, \`list\`, \`query\`, \`relations\`, \`resolve\`, \`propagation\`, \`provenance\`) build the index when it is missing and otherwise trust it. A warm index older than a changed \`SPEC.json\` produces a stderr warning. Tag-only code edits are not detected. \`gate\` always rebuilds cold, and so does \`check --ci\`. After editing specs or tags, pass \`--fresh\`.
3398
+
3399
+ ### spec index
3400
+
3401
+ Build or refresh the derived index.
3402
+
3403
+ \`--json\`: \`{ build_id, repos, domains, requirements, tags, diagnostics }\`.
3404
+
3405
+ Exit: 0 built / 1 an invalid SPEC.json refused the build, or the build crashed / 2 not a platform or bad args.
3406
+
3407
+ - \`build_id\` is deterministic for identical inputs (cold-rebuild equivalence).
3408
+ - A \`SPEC.json\` that is not JSON or fails the shared schema refuses the whole build, and the index on disk is left as it was. Every file is parsed first, so one run reports every invalid file. Under \`--json\` stdout holds only the \`INVALID_DOMAIN_FILE\` rows in the \`spec check --json\` row shape; text mode prints only those rows on stderr. Every other index-building command prints only the same rows and exits 1 (\`check\`, \`gate\`, \`guard\`, \`serve\`, and the read commands); a lifecycle command that reindexes for its retag (\`supersede\`, \`amend\`, \`deprecate\`, \`move\`, \`term confirm\`) exits 2 after its write.
3409
+ - Also writes \`.spec-engine/doctor.md\`, a list of doc lines that mention a requirement id without an explicit \`<!-- @spec ID -->\` binding.
3410
+
3411
+ ### spec check
3412
+
3413
+ Integrity, coverage, drift, and governance diagnostics.
3414
+
3415
+ | Flag | Effect |
3416
+ | --- | --- |
3417
+ | \`--ci\` | Delete the DB first. Correctness never trusts a warm index. |
3418
+ | \`--results <junit.xml>\` | The trusted-red gate: a verifying tag counts only when its test passed. Path resolves inside \`platformDir\`. |
3419
+ | \`--base <ref>\` | Diff the requirement set against a git ref (deletion and status-flip governance). |
3420
+ | \`--approved-by <list>\` | Approver set for the status-flip check. |
3421
+ | \`--require-owner-approval\` | Escalate \`UNAPPROVED_STATUS_FLIP\` from warning to error. |
3422
+ | \`--unsourced-change\` | Also warn on superseded requirements with no \`supersedes-via\` issue. |
3423
+
3424
+ \`--json\` rows: \`{ code, severity, repo, source_file, line, req_id, detail }\`, nullable except \`code\`, \`severity\`, \`detail\`, sorted deterministically.
3425
+
3426
+ Exit: 0 clean or warnings only / 1 any error-severity row / 2 usage.
3427
+
3428
+ <!-- @spec CHCK-053 -->
3429
+
3430
+ Codes, in \`DiagnosticCode\` order:
3431
+
3432
+ | Code | Severity | Condition |
3433
+ | --- | --- | --- |
3434
+ | \`DUP_ID\` | error | The same requirement id appears twice |
3435
+ | \`BROKEN_SUPERSEDE\` | error | A \`supersededBy\` names a missing id, or a \`supersedes\` names a missing id or the entry itself |
3436
+ | \`BAD_STATUS\` | error | An entry status outside \`active\` / \`draft\` / \`superseded\` / \`deprecated\` |
3437
+ | \`DANGLING_TAG\` | error | A tag references no requirement |
3438
+ | \`SUPERSEDED_REFERENCED\` | error | Code still tags a superseded requirement (retag to the successor) |
3439
+ | \`DEPRECATED_REFERENCED\` | error | Code still tags a deprecated requirement |
3440
+ | \`STATEMENT_GRAMMAR\` | per domain, default warning | A statement fails its domain's declared grammar |
3441
+ | \`STATEMENT_RULES\` | error; per rule from \`statementRules.severity\` | An Active statement breaks a statement rule: one row per rule per statement, the detail \`<rule>: <detail> \u2014 <reason>\` |
3442
+ | \`DRAFT_REFERENCED\` | error on a verifying tag, warning on an implementing or documenting tag | A tag references a Draft requirement |
3443
+ | \`GLOSSARY_DRIFT\` | warning | The committed glossary differs from the one generated from the term store |
3444
+ | \`ORPHAN_REQ\` | error | An Active requirement has no implementing tag |
3445
+ | \`UNVERIFIED_REQ\` | warning | An Active requirement is implemented but has no verifying tag |
3446
+ | \`DRIFT\` | error | A member is pinned behind the version at which a requirement it references changed |
3447
+ | \`NO_SPEC_CONFIG\` | warning | A declared member is present on disk with no \`spec-engine.member.json\` |
3448
+ | \`BROKEN_RELATES\` | warning | A \`relates\` entry names a nonexistent requirement |
3449
+ | \`RELATES_SUPERSEDED\` | warning | A \`relates\` entry names a superseded requirement |
3450
+ | \`CYCLIC_SUPERSEDE\` | error | A circular supersession chain |
3451
+ | \`SELF_RELATES\` | warning | A \`relates\` entry names its own requirement (dropped, but surfaced) |
3452
+ | \`UNKNOWN_ROLE\` | warning | An \`issues\` entry uses a role outside \`created\` / \`supersedes-via\` / \`amends-via\` (surfaced and dropped, never stored) |
3453
+ | \`UNSOURCED_CHANGE\` | warning, only with \`--unsourced-change\` | A superseded requirement carries no \`supersedes-via\` issue |
3454
+ | \`INVALID_DOMAIN_FILE\` | error | A \`SPEC.json\` is not JSON or fails the shared schema; one row per failing field |
3455
+ | \`MIGRATION_PENDING\` | warning | A \`SPEC.json\` still carries the retired \`livesIn\` or \`verifiedIn\` fields; run \`spec migrate\` |
3456
+ | \`UNPROVEN_REQ\` | error, only with \`--results\` | An Active requirement has verifying tags but no passing correlated test |
3457
+ | \`PROOFS_UNCONFIRMED\` | warning, only without \`--results\` | The proof gate fell back to presence-only mode |
3458
+ | \`REQUIREMENT_REMOVED\` | error | An Active requirement present at the base ref is gone with no same-change supersession |
3459
+ | \`IMPL_LOST\` | error, when a base ref resolves | The change removes the last implementing tag of a surviving Active requirement |
3460
+ | \`VERIFY_LOST\` | warning, when a base ref resolves | The change removes the last verifying tag of a surviving Active requirement |
3461
+ | \`SPEC_FILE_DELETED\` | error, when a base ref resolves | A \`SPEC.json\` present at the base ref is gone |
3462
+ | \`UNAPPROVED_STATUS_FLIP\` | warning; error with \`--require-owner-approval\` | A flip to superseded or deprecated whose CODEOWNERS domain owner is absent from \`--approved-by\` |
3463
+ | \`PARTIAL_PROPAGATION\` | error, needs \`--base\` and \`--results\` | A changed requirement where some bound tests pass and some fail |
3464
+ | \`MALFORMED_FILE\` | platform-map's severity: error, or warning for a manifest | A platform file, a package or workspace manifest, or platform-map's per-user file failed to parse or validate |
3465
+ | \`MEMBER_MISSING\` | warning | A declared member is not on this machine; it is not scanned |
3466
+ | \`MARKER_MISSING\` | warning | A declared member has no marker |
3467
+ | \`MARKER_MISMATCH\` | error | A member's marker names a different platform |
3468
+ | \`UNLISTED_REPO\` | warning (platform-map info, promoted) | A repository in the platform folder is not a member; its tags are not scanned |
3469
+ | \`PLATFORM_NOT_LOCATED\` | warning | A marker names a platform this machine cannot find |
3470
+ | \`UNDECLARED_PLATFORM\` | warning (platform-map info, promoted) | A folder of repositories with no platform file; nothing in it is a member |
3471
+ | \`SCAN_TRUNCATED\` | warning | A workspace walk hit its depth or entry cap |
3472
+ | \`UNDEFINED_TERM\` | error | A \`cites\` entry resolves to no glossary term |
3473
+ | \`ORPHAN_TERM\` | warning | An Active term that nothing cites |
3474
+ | \`TERM_DRIFT\` | warning | A citation's pin is behind the term's current version |
3475
+ | \`SUPERSEDED_TERM_REFERENCED\` | error | A citation names a superseded term |
3476
+
3477
+ The four TERM codes are the last four rows. Only \`error\` rows drive exit 1.
3478
+
3479
+ The eight codes before them are platform-map's, surfaced with the platform-map code: every platform-map error and warning keeps its severity; \`UNLISTED_REPO\` and \`UNDECLARED_PLATFORM\` are promoted from info to warning because they mean a repository's tags are not scanned; \`UNMATCHED_PATTERN\` and \`AMBIGUOUS_ECOSYSTEM\` are not surfaced. \`repo\` is the diagnostic's subject when it names a member; \`source_file\` is the subject. \`NO_SPEC_CONFIG\` stays Spec Engine's own code: a declared member present on disk with no \`spec-engine.member.json\`.
3480
+
3481
+ - Deletion detection runs by default: with no \`--base\`, the working tree is diffed against \`HEAD\` whenever git resolves. An Active requirement absent from the tree without a same-change supersession is \`REQUIREMENT_REMOVED\`. History (superseded and deprecated entries) may never be deleted. Outside git the check stays quiet.
3482
+ - Without \`--results\`, \`check\` emits \`PROOFS_UNCONFIRMED\` and skips proof enforcement. The results file is never hashed into \`build_id\`. Generate it with the test runner's JUnit reporter, for example \`bun test --reporter=junit --reporter-outfile=.spec-engine/results.xml\`.
3483
+ - Term citations: \`UNDEFINED_TERM\`, \`SUPERSEDED_TERM_REFERENCED\`, and \`TERM_DRIFT\` clear with \`spec term confirm\`.
3484
+ - Losses: whenever a base ref resolves (\`--base\`, else \`HEAD\`), \`check\` reports the \`IMPL_LOST\`, \`VERIFY_LOST\`, and \`SPEC_FILE_DELETED\` rows \`spec guard\` would report against that ref. Guard's \`REQUIREMENT_REMOVED\` rows are dropped; the removal check already reports that id.
3485
+ - Under \`--ci\` with no \`--base\`, a working tree whose tracked files equal \`HEAD\` makes every loss check pass vacuously, so \`check\` warns on stderr with the line to run instead: \`spec check . --ci --base "$(git merge-base HEAD origin/<default>)"\`. The exit code and stdout are unchanged. <!-- @spec GUARD-031 -->
3486
+ - Coverage counts only Active requirements: \`ORPHAN_REQ\` and \`UNVERIFIED_REQ\` fire for Active entries only. <!-- @spec CHCK-032 -->
3487
+
3488
+ #### check.json
3489
+
3490
+ \`spec-engine/check.json\` regrades diagnostics. Absent means no rules.
3491
+
3492
+ \`\`\`json
3493
+ { "severity": [
3494
+ { "code": "UNVERIFIED_REQ", "severity": "error" },
3495
+ { "code": "UNVERIFIED_REQ", "severity": "warning", "match": ["LEGACY", "BILLING-004"] }
3496
+ ] }
3497
+ \`\`\`
3498
+
3499
+ - \`code\` is any diagnostic code; \`severity\` is \`error\`, \`warning\`, or \`off\`.
3500
+ - \`match\` lists domain keys and requirement ids. A scoped rule applies only to rows whose \`req_id\` is listed or carries a listed key.
3501
+ - The most specific rule wins: an id match, then a domain match, then an unscoped rule. Among equals, the later rule wins.
3502
+ - \`off\` drops the row from every output.
3503
+ - Grading runs inside the operation, so the CLI, \`spec_check\`, and every other surface agree, and exit 1 follows the graded severities.
3504
+ - A file that fails the schema (unknown code or key, a bad severity, a malformed match entry) exits 2 with \`spec-engine/check.json: <path>: <reason>\`.
3505
+
3506
+ \`statementRules\` configures the statement rules on every surface: the write-time refusal, \`STATEMENT_RULES\`, and \`spec lint\`.
3507
+
3508
+ \`\`\`json
3509
+ { "severity": [],
3510
+ "statementRules": {
3511
+ "maxLength": 280,
3512
+ "jargon": ["seam", "rung"],
3513
+ "exemptNames": ["SPEC.json", "spec-engine/"],
3514
+ "severity": { "no-jargon": "off", "length": "error" }
3515
+ } }
3516
+ \`\`\`
3517
+
3518
+ - \`maxLength\` defaults to 280. \`jargon\` replaces the default list (\`seam\`, \`seams\`, \`rung\`, \`rungs\`, \`residue\`, \`matrix entry\`, \`stage runner\`, \`the stage\`).
3519
+ - \`exemptNames\` are product names \`no-file-path\` never reports. A name ending in \`/\` exempts every path under it.
3520
+ - \`severity\` maps a rule to \`error\`, \`warning\`, or \`off\`. A rule with no entry is \`error\`. \`error\` refuses the write and grades the \`STATEMENT_RULES\` row error; \`warning\` writes, prints the finding on stderr, and grades the row warning; \`off\` turns the rule off everywhere. The \`severity\` array regrades by code after it.
3521
+ - The rules: \`one-sentence\`, \`length\`, \`no-file-path\`, \`no-test-narration\`, \`no-toolchain-layout\`, \`no-history\`, \`no-jargon\`, \`no-tool-statement\`. Unknown keys exit 2.
3522
+
3523
+ ### spec guard
3524
+
3525
+ The pre-commit loss gate. Diffs the requirement derivation at \`--against <ref>\` (default \`HEAD\`) against the working tree, scoped to the files in \`git diff\`. Rebuilds the worktree index cold every run.
3526
+
3527
+ \`--json\` rows: \`{ kind, req_id, file, line, detail }\`, sorted by \`(req_id, kind, file, line)\`. \`kind\` is one of \`REQUIREMENT_REMOVED\`, \`IMPL_LOST\`, \`VERIFY_LOST\`, \`SPEC_FILE_DELETED\` (\`req_id\` null).
3528
+
3529
+ Exit: 0 clean / 1 any loss / 2 usage.
3530
+
3531
+ - A loss is suppressed only by ending the requirement in the same change: supersede it (either direction) or \`spec deprecate\` it. There is no override comment.
3532
+ - A non-git tree, a repo with no \`HEAD\`, or an unknown ref prints \`NOT_A_GIT_REPO\` to stderr and exits 0.
3533
+ - A ref that begins with \`-\` or holds a character outside \`A-Z a-z 0-9 . _ / -\` is refused before git runs: \`guard\` treats it as an unknown ref, and \`check --base\` exits 2.
3534
+ - Text mode prints a \`\uD83D\uDED1 spec-guard:\` block per requirement, written in the second person. Relay it verbatim to the user before proceeding.
3535
+
3536
+ ### spec map
3537
+
3538
+ \`--json\` rows keyed \`(req_id, repo)\`: \`{ req_id, domain_key, req_status, req_spec_version, req_changed_at_version, repo, repo_pin, implemented: 0|1, verified: 0|1, test_levels }\`. Text mode renders \`src\`, \`test\`, \`src+test\`, \`\u2014\`.
3539
+
3540
+ Exit: 0 / 2.
3541
+
3542
+ ### spec get
3543
+
3544
+ One requirement's full record from the derived index, with the code sites its tags name. Tags are the only record of where a requirement is implemented and verified.
3545
+
3546
+ \`--json\`: the \`Requirement\` row \`{ id, key, seq, status, superseded_by, text, why, source_file, line, spec_version, changed_at_version, superseded_at_version }\` plus \`implemented_in: [{ repo, file, line, kind }]\` (implementing and documenting tags) and \`verified_in: [{ repo, file, line, level }]\` (verifying tags), each sorted by (repo, file, line). The same object is served by \`GET /api/requirements/:id\` and \`spec_get\`, and shown on the webapp \`/requirements/:id\` page. Text mode is a one-row table (\`ID STATUS KEY SEQ CHANGED_AT TEXT\`) followed by an \`implemented in:\` and a \`verified in:\` list of \`file:line\`.
3547
+
3548
+ Exit: 0 / 2. An unknown id prints \`[]\` on stdout, guidance on stderr, and exits 0. A malformed id exits 2.
3549
+
3550
+ ### spec list
3551
+
3552
+ Every requirement record in (key, seq) order. Unlike \`query\`, superseded and deprecated rows are included and nothing is ranked.
3553
+
3554
+ | Flag | Effect |
3555
+ | --- | --- |
3556
+ | \`--domain KEY\` | Only that domain (case-insensitive key). |
3557
+ | \`--status S\` | Only that status: \`active\`, \`draft\`, \`superseded\`, \`deprecated\`. Any other word exits 2. |
3558
+
3559
+ \`--json\`: an array of \`Requirement\` rows, the same rows \`GET /api/requirements\` serves. Text mode is the \`ID STATUS KEY SEQ CHANGED_AT TEXT\` table.
3560
+
3561
+ Exit: 0 / 2. An empty result is \`[]\` under \`--json\`, guidance on stderr in text mode, exit 0.
3562
+
3563
+ ### spec query
3564
+
3565
+ FTS5 \`MATCH\` syntax. \`--limit N\` (default 10, max 1000).
3566
+
3567
+ Exit: 0 / 2 (bare \`AND\` or \`OR\` is an FTS5 grammar error).
3568
+
3569
+ - Wrap multi-word phrases in double quotes inside the shell-quoted arg.
3570
+ - Results rank ascending (best first). Superseded requirements are excluded.
3571
+ - Glossary term definitions ride the same index. A matching term appears in a separate Terms group in text mode, discriminated by \`key\` under \`--json\`.
3572
+
3573
+ ### spec relations
3574
+
3575
+ Text mode: mermaid \`graph LR\`, one node per requirement in a \`relates\` field, one undirected deduped edge per pair. \`--json\` rows: \`{ from_id, to_id, source_file, line }\`, sorted by (from, to).
3576
+
3577
+ Exit: 0 / 2. Empty graph is \`[]\` and exit 0.
3578
+
3579
+ ### spec provenance
3580
+
3581
+ Per requirement: the creating issue, the revising issues (\`supersedes-via\`, \`amends-via\`), the backing tests, and the \`source_file:line\` pointer.
3582
+
3583
+ | Flag | Effect |
3584
+ | --- | --- |
3585
+ | \`[issueId]\` positional | Display-only reverse filter. Never a join key. |
3586
+ | \`--resolve-issues\` | Overlay tracker title/status/URL. Needs \`SPEC_TRACKER_TOKEN\`; degrades to bare ids without it. |
3587
+
3588
+ \`--json\` rows: \`{ req_id, role, issue_id, source_file, line, req_status, implemented, verified, test_levels }\`, sorted on the full composite key.
3589
+
3590
+ Exit: 0 / 2. Empty matrix is \`[]\` and exit 0.
3591
+
3592
+ ### spec resolve
3593
+
3594
+ Accepts multiple positionals, comma-split inside one positional, and absolute paths under the platform dir.
3595
+
3596
+ | Flag | Effect |
3597
+ | --- | --- |
3598
+ | \`--req KEY-NNN\` | Reverse query: every tag site for that requirement. Takes no file positionals. |
3599
+
3600
+ \`--json\` rows (\`--req\`): \`{ req_id, repo, file, line, kind, level }\` sorted by (repo, file, line). \`kind\` is \`implements\`, \`verifies\`, or \`documents\`.
3601
+
3602
+ Exit: 0 / 2. Empty result is \`[]\` and exit 0. Unknown id under \`--req\` is \`[]\`.
3603
+
3604
+ Path rules. \`tags.file\` is platform-relative: \`<member>/<path>\` for a file inside a declared member or a workspace package, and the bare \`<path>\` for a file a lone repository owns itself (its directory-named or \`root\` column). The key never carries the checkout's folder name.
3605
+
3606
+ - Multi-repo platform: pass \`<repo>/<path>\`, e.g. \`spec resolve api/src/renew.ts .\`
3607
+ - Single repo: pass the path from the platform root, e.g. \`spec resolve src/orders.ts .\` or \`spec resolve .github/workflows/ci.yml .\`
3608
+ - Absolute paths are normalized against \`platformDir\`. A path outside it exits 2.
3609
+
3610
+ ### spec propagation
3611
+
3612
+ One row per member repo: \`{ repo, state, via_req_id, drifted }\`. \`state\` is one of \`MIGRATED_VERIFIED\`, \`MIGRATED_UNVERIFIED\`, \`ON_PREDECESSOR\`, \`ON_OTHER_DOMAIN_REQ\`, \`NO_DOMAIN_REFERENCE\`.
3613
+
3614
+ Rows are in dependency order: a member comes after every member it depends on, read from platform-map's \`dependsOn\` (the platform's own package names in a member's manifest), and members at the same depth come by name. The same order is served by \`/api/propagation/:id\`, \`spec_propagation\`, and the webapp's \`/propagation/:id\` page. A platform with no in-platform dependencies lists members by name.
3615
+
3616
+ Exit: 0 / 2.
3617
+
3618
+ ### spec gate
3619
+
3620
+ \`--json\`: \`{ pass, reason, repo, req_id, detail, status, changed_at_version, pinned_spec_version }\`. \`reason\` is one of \`PASS\`, \`NOT_FOUND\`, \`DRAFT\`, \`SUPERSEDED\`, \`DEPRECATED\`, \`VERSION_PIN\`, decided in that order. Pin equality passes.
3621
+
3622
+ Exit: 0 pass / 1 fail / 2 unknown repo name or bad args.
3623
+
3624
+ ### spec init
3625
+
3626
+ Declares a member through platform-map and writes \`spec-engine.member.json\`.
3627
+
3628
+ | Flag | Effect |
3629
+ | --- | --- |
3630
+ | \`--specs spec-engine@N\` | Override the pin. |
3631
+ | \`--force\` | Rewrite an existing config, preserving \`ignore\`. Any other key refuses, as does a malformed \`ignore\`. |
3632
+ | \`--platform <dir>\` | The platform root, for a declared member checked out outside the platform folder. Its location is recorded in platform-map's per-user file first. |
3633
+
3634
+ \`--json\`: \`{ action: "wrote", path, pin, source, declared, linked, agent_surface }\`, \`{ action: "already-configured", path, pin, extra_fields, declared, linked, agent_surface }\`, or, on a platform directory, \`{ action: "surface-only", agent_surface }\`. \`declared\` lists the platform-map files written (the platform file, the marker), empty when the member was already declared; \`linked\` is the per-user file written under \`--platform\`, else null; \`agent_surface\` lists the agent-surface files written or changed, \`[]\` on a re-run.
3635
+
3636
+ Exit: 0 wrote or already configured / 2. Branch on \`action\`, not the exit code.
3637
+
3638
+ - Where the target sits decides what is written:
3639
+ - a repository directly under a platform folder (a directory holding \`spec-engine/\`) that the platform file does not list is declared: a \`platform-map.json\` platform file entry at the root (the file is created when absent) and a \`{ platform, member }\` marker in the repository, then the pin. platform-map declares only a directory with a \`.git\` entry or a \`package.json\`; a plain folder is refused with exit 2;
3640
+ - a declared member only gets the pin (a missing marker is written back);
3641
+ - a workspace package of a monorepo gets a nested pin and no declaration; a repository inside a lone monorepo that is not a workspace package is refused;
3642
+ - a directory under no platform gets the \`spec-engine@1\` fallback pin;
3643
+ - a platform directory (holding \`spec-engine/\`) gets the agent surface and no pin.
3644
+ - Every action then installs the agent surface into the repository (the member; the monorepo root for a workspace package), bundled in the npm package under \`dist/agent/\`. Hints for anything left alone go to stderr.
3645
+ - \`AGENTS.md\`: the agent reference between a \`spec-engine:agent-reference:begin\` and a \`spec-engine:agent-reference:end\` HTML comment marker, appended or created, replaced in place when the markers exist; text outside them is never touched. A \`CLAUDE.md\` that does not mention \`AGENTS.md\` gets a hint only.
3646
+ - \`.claude/skills/req-author/\`: written when absent; a differing copy is replaced only under \`--force\`.
3647
+ - \`.mcp.json\`: a \`spec\` server entry (\`spec mcp <platform>\`) merged in; other keys and an existing \`spec\` entry are kept.
3648
+ - \`pre-commit\`: \`spec guard <platform>\`, written to \`.githooks/\` or else \`.git/hooks/\` when no hook exists there; an existing hook gets a hint.
3649
+ - With no platform located, the MCP entry and the hook are skipped.
3650
+ - The platform root is platform-map's \`locate()\`: the nearest \`platform-map.json\` or \`.git\` boundary, a marker resolved through the parent directory or the per-user file; the parent directory when it holds \`spec-engine/\`. A \`.git\` boundary between the target and an outer \`spec-engine/\` hides that platform.
3651
+ - Default pin: \`--specs\`, else the derived platform version, else \`spec-engine@1\` with a printed note.
3652
+ - A resolved path with a \`spec-engine\` segment is refused, except on the engine's own checkout (root package named \`@spec-engine/spec-engine\`), where only the path below the platform root is judged.
3653
+ - \`ignore: ["dir", \u2026]\` excludes repo-relative directory prefixes from that repo's scans: \`vendor\` hides \`vendor/x.ts\` but not \`src/vendor/x.ts\`. A monorepo root column excludes its workspace package paths the same way. A monorepo's columns are its workspace manifest's packages.
3654
+
3655
+ ### spec domain
3656
+
3657
+ \`spec domain new <KEY>\` scaffolds \`spec-engine/<KEY>/SPEC.json\`. \`spec domain list\` prints one key per line; \`--json\` prints a sorted array of \`{ key, scope }\` (scope is the charter sentence or null), read from the filesystem.
3658
+
3659
+ \`spec domain grammar <KEY> [platformDir] [--grammar ears|freeform] [--severity warning|error]\` sets the envelope's \`grammar\` and/or \`grammarSeverity\` through the validating write path; every other field is written back as read. \`--json\` prints \`{ key, grammar, grammar_severity, file }\` (an unset field is null). An unknown domain, neither flag, or a value outside the enums exits 2. CLI only: no MCP tool or HTTP route.
3660
+
3661
+ \`\`\`shell
3662
+ spec domain grammar CHCK --severity error
3663
+ \`\`\`
3664
+
3665
+ Exit: 0 / 2.
3666
+
3667
+ ### spec migrate
3668
+
3669
+ Rewrites every \`SPEC.json\` that still carries the retired \`livesIn\` or \`verifiedIn\` fields through the write seam, which drops them. A file without them is not touched, so a second run writes nothing. Until a file is migrated, the fields are read and ignored, and the next write of a file drops them.
3670
+
3671
+ \`--json\`: \`{ files }\`, the platform-relative paths rewritten.
3672
+
3673
+ Exit: 0 / 2.
3674
+
3675
+ ### spec req
3676
+
3677
+ | Mode | Behavior |
3678
+ | --- | --- |
3679
+ | stdin not a TTY, no \`--text\` | Print the bare next unused id (e.g. \`BILLING-010\`), exit 0. No prompts, no writes. |
3680
+ | \`--json\`, no \`--text\` | Print \`{ domain, next_id }\`. Same zero-write contract, even on a TTY. |
3681
+ | \`--text "<statement>"\` | Append an Active entry with zero prompts. \`--why\`, \`--relates\`, \`--issue <ticket>\` fill the other fields and error without \`--text\`. \`--relates\` repeats and comma-separates. |
3682
+ | \`--draft\` | Author the entry as Draft (with \`--text\`, or on a TTY). Off a TTY without \`--text\` it exits 2. |
3683
+ | TTY, no flags | Interactive authoring. |
3684
+
3685
+ \`--json\` with \`--text\`: \`{ id, file }\`, plus \`clauses: { pattern, condition, system, response }\` when the statement parses as EARS.
3686
+
3687
+ Exit: 0 / 2 (ambiguous prefix, unknown domain, grammar refusal, statement-rule refusal).
3688
+
3689
+ - The domain prefix is case-insensitive: \`bil\` resolves to \`BILLING\`.
3690
+ - The resolved domain's charter is echoed to stderr on every authoring path.
3691
+ - Unresolvable \`@<path>\` refs warn on stderr and never block.
3692
+
3693
+ ### spec term
3694
+
3695
+ A term is a requirement row in the reserved \`TERM\` domain: definition in \`statement\`, headword in \`term\`, synonyms in \`aliases\`.
3696
+
3697
+ | Form | Effect |
3698
+ | --- | --- |
3699
+ | \`spec term "<name>" --def "<definition>" [--aliases a,b] [--section s]\` | Append a \`TERM-NNN\`. \`--json\`: \`{ id, file }\`. |
3700
+ | \`spec term\` with no \`--def\` | Pure id query, mirrors \`spec req\`: bare next \`TERM\` id, or \`{ domain, next_id }\`. |
3701
+ | \`spec term list\` | \`id name status\` per line, or \`[{ id, term, status }]\`. Reads the filesystem. |
3702
+ | \`spec term revise <TERM-NNN> --def "\u2026"\` | Rewrite the definition in place and bump the TERM envelope's \`specVersion\`. \`--no-bump\` opts out. \`--json\`: \`{ id, file, spec_version }\`. |
3703
+ | \`spec term confirm <KEY-NNN> <TERM-NNN>\` | Re-pin a citation to the term's current version (clears \`TERM_DRIFT\`); re-point to the successor when the term is superseded (clears \`SUPERSEDED_TERM_REFERENCED\`). \`--json\`: \`{ req_id, term_id, pinned, file }\`. |
3704
+
3705
+ \`spec supersede TERM-NNN\` and \`spec amend TERM-NNN --term/--aliases\` work on term ids as on requirements.
3706
+
3707
+ Exit: 0 / 2.
3708
+
3709
+ ### spec glossary
3710
+
3711
+ | Form | Effect |
3712
+ | --- | --- |
3713
+ | \`spec glossary\` | Overwrite \`GLOSSARY.md\` from the TERM store. Byte-stable: fixed header, terms in id order, \`## {section}\` once per change, one \`- **{term}** \u2014 {statement}\` bullet each. |
3714
+ | \`--migrate\` | One-time parse of an existing \`GLOSSARY.md\` into \`TERM-001..N\`. Skips when the TERM domain already has entries. |
3715
+ | \`--check\` | Regenerate into a buffer and diff against the committed file. Exit 1 on drift. |
3716
+
3717
+ \`--json\`: \`{ generated }\`, \`{ migrated }\`, or \`{ ok }\`.
3718
+
3719
+ Exit: 0 / 1 (\`--check\` drift) / 2.
3720
+
3721
+ ### spec deprecate
3722
+
3723
+ Marks an Active/Draft requirement \`deprecated\`, records \`--reason\` on the entry, and removes every \`@spec\` tag of the id from the code. A line that holds only the tag (and its comment leader) is deleted; on a line with other text, only the tag and its level token go. Text mode prints the removed sites.
3724
+
3725
+ \`--json\`: \`{ id, file, reason, removed: [{ req_id, repo, file, line, kind, level }] }\`, the sites as they stood before the removal.
3726
+
3727
+ Tag sites come from the freshly built index, so \`fixtures/\` and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
3728
+
3729
+ Exit: 0 / 2.
3730
+
3731
+ ### spec supersede
3732
+
3733
+ Requires an Active target. Flips it to \`superseded by NEW\`, mints the successor in the domain file that holds the target, reindexes fresh, and rewrites every \`@spec OLD\` tag in the code to \`@spec NEW\`, keeping the level token and the rest of the line. Text mode prints the rewritten sites. Tag sites come from the freshly built index, so \`fixtures/\` and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
3734
+
3735
+ The successor copies the predecessor's \`why\`, \`relates\`, \`cites\`, \`aliases\`, \`term\`, and \`section\` unless a flag overrides the field. Its \`issues\` hold only the \`created\` row from \`--issue\`.
3736
+
3737
+ | Flag | Effect |
3738
+ | --- | --- |
3739
+ | \`--text\` | The successor statement. Required when not a TTY. |
3740
+ | \`--why\`, \`--relates\` | Successor fields. Default to copies of the predecessor's. \`--relates\` repeats or comma-separates and replaces the whole list; an empty value clears it. |
3741
+ | \`--issue <ticket>\` | Recorded as \`supersedes-via\` on the predecessor and \`created\` on the successor. |
3742
+ | \`--no-bump\` | TERM domain only: keep the authored \`specVersion\`. No-op elsewhere. |
3743
+
3744
+ \`--json\`: \`{ old_id, new_id, file, spec_version, retagged: [{ req_id, repo, file, line, kind, level }] }\`, the rewritten sites (\`req_id\` is the old id). On a requirement domain \`spec_version\` is the derived domain version after the supersession, and the same number is stamped as the predecessor's \`supersededAtVersion\`.
3745
+
3746
+ Exit: 0 / 2. All guards run before the first byte is written.
3747
+
3748
+ ### spec move
3749
+
3750
+ Cross-domain supersede. Mints the successor as the next id in \`<NEW-DOMAIN>\` (which must exist), copies \`statement\`, \`why\`, \`relates\`, \`cites\`, \`aliases\`, \`term\`, and \`section\` (\`--text\` and \`--why\` override), flips the source to superseded with a cross-domain \`supersededBy\`, and rewrites every tag of the source to the successor as \`spec supersede\` does. The successor carries no \`issues\`.
3751
+
3752
+ \`--json\`: \`{ old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged: [...] }\`.
3753
+
3754
+ Exit: 0 / 2 (non-Active source, or the target is the source's own domain).
3755
+
3756
+ - Both envelopes validate before either is written.
3757
+ - The source version advances; the target version is unchanged.
3758
+
3759
+ ### spec amend
3760
+
3761
+ Revise an Active or Draft entry in place. Same id, no version change.
3762
+
3763
+ | Flag | Effect |
3764
+ | --- | --- |
3765
+ | \`--text\`, \`--why\`, \`--relates\` | Field to change. At least one flag is required. Untouched fields stay byte-identical. \`--relates\` repeats or comma-separates and replaces the whole list (the \`--aliases\` idiom); an empty value clears it. |
3766
+ | \`--issue <ticket>\` | Append \`amends-via\` provenance. |
3767
+ | \`--term\`, \`--aliases\` | TERM ids only. |
3768
+
3769
+ \`--json\`: \`{ id, file, fields_changed }\`.
3770
+
3771
+ A released requirement is an Active entry present in its domain file at the newest \`v*\` tag merged into HEAD. Its promise does not change in place: a \`--text\`, \`--why\`, \`--term\`, or \`--aliases\` amend refuses with exit 2, naming the tag. Two paths stay open: \`spec supersede\` changes the promise, and an amend of only \`--relates\` or \`--issue\` writes in place. With no \`v*\` tag merged into HEAD, or outside git, every entry is unreleased. A Draft entry always amends.
3772
+
3773
+ Exit: 0 / 2 (superseded or deprecated target; a released entry's promise fields).
3774
+
3775
+ ### spec accept
3776
+
3777
+ Promotes a Draft requirement to Active. Every other field stays byte-identical and the domain version does not move. Reword a draft with \`spec amend\` first; end one with \`spec deprecate\`.
3778
+
3779
+ \`--json\`: \`{ id, file }\`.
3780
+
3781
+ Exit: 0 / 2 (the target is not Draft: nothing is written).
3782
+
3783
+ - A Draft never raises \`ORPHAN_REQ\` or \`UNVERIFIED_REQ\` and is not counted in the coverage report. A code tag on one is \`DRAFT_REFERENCED\` (warning).
3784
+
3785
+ ### spec lint
3786
+
3787
+ Every statement-rule finding. Reads \`SPEC.json\` directly, not the index.
3788
+
3789
+ | Form | Scope |
3790
+ | --- | --- |
3791
+ | \`spec lint [platformDir]\` | the platform |
3792
+ | \`spec lint <KEY> [platformDir]\` | one domain |
3793
+ | \`spec lint <KEY-NNN> [platformDir]\` | one requirement |
3794
+ | \`--all\` | Draft requirements as well as Active ones |
3795
+
3796
+ Text mode prints \`<id> <rule>: <detail> \u2014 <reason>\` per finding on stdout, then rule 0 on stderr. \`--json\` prints a sorted array of \`{ req_id, rule, detail, reason, source_file, line }\`. The TERM domain is never judged. A lone positional that holds \`spec-engine/\` is the platform dir. CLI only: no MCP tool or HTTP route.
3797
+
3798
+ Exit: 0 clean / 1 any finding / 2 usage, unknown domain or id, or a bad \`check.json\`.
3799
+
3800
+ ### spec serve
3801
+
3802
+ Binds one address: \`127.0.0.1\`, or \`--host <ip>\` (a literal loopback, RFC 1918, or \`100.64/10\` address only; anything else exits 2). \`--port 0\` (default) picks a free port. \`--probe\` boots on loopback, fetches \`/\`, and exits 0 or 1. The startup line, \`/api/platform\` (\`name\`), and every page's title and sidebar name the platform (platform file \`name\`, else the directory name), never a path; \`spec mcp\` logs the same name on stderr.
3803
+
3804
+ A non-loopback \`--host\` is LAN mode: stdout prints \`http://<ip>:<port>/?token=\u2026\`; every request needs that token (the query parameter is exchanged for an \`HttpOnly; SameSite=Strict\` cookie, then a 302 drops it), else 401. A \`Host\` other than loopback or the bound address is 403. Read-only unless \`--allow-writes\`: non-GET requests, \`/provenance\`, and \`/api/provenance?resolve=1\` answer 403. \`--token\` / \`SPEC_SERVE_TOKEN\` pin the token (\u2265 16 of \`A-Z a-z 0-9 _ -\`); otherwise it is 256 random bits.
3805
+
3806
+ When the reindex after a write refuses on an invalid \`SPEC.json\` elsewhere on the platform, the route answers 422 \`{ error: "invalid_domain_file", diagnostics: [...] }\`, the rows in the \`check --json\` shape (the CLI exits 2 with the same rows).
3807
+
3808
+ Always-on routes: \`/\`, \`/requirements[/:id]\`, \`/propagation/:id\`, \`/api/coverage\`, \`/api/report\`, \`/api/repos\`, \`/api/platform\`, \`/api/requirements[/:id]\`, \`/api/propagation/:id\`, \`/api/resolve?files=\u2026\`.
3809
+
3810
+ Flag-gated, 404 until enabled. \`SPEC_FLAGS\` is comma-separated; unknown keys are ignored.
3811
+
3812
+ | \`SPEC_FLAGS\` key | Routes |
3813
+ | --- | --- |
3814
+ | \`query\` | \`/query\`, \`/api/query?q=&limit=\` |
3815
+ | \`relations\` | \`/relations\`, \`/api/relations[?format=mermaid]\` |
3816
+ | \`provenance\` | \`/provenance\`, \`/api/provenance[?resolve=1]\`, \`/api/provenance/by-issue?issue=\` |
3817
+ | \`editor\` | \`/editor\`, \`POST\` and \`PUT /api/requirements\`, \`POST /api/requirements/:id/supersede\`, \`POST /api/requirements/:id/move\`, \`POST /api/requirements/:id/deprecate\`, \`POST /api/requirements/:id/accept\` |
3818
+ | \`glossary\`, \`logs\` | Placeholder pages |
3819
+
3820
+ Exit: 0 / 1 / 2.
3821
+
3822
+ ### spec docs
3823
+
3824
+ Serves the prebuilt docs site on \`127.0.0.1\`, fully offline.
3825
+
3826
+ - Takes no platform dir. Registers none of \`--json\`, \`--out\`, \`--no-prompt\`.
3827
+ - The npm artifact ships the site at \`dist/docs/\`. A source checkout serves \`packages/site/dist\`; run \`bun run build:site\` first, else exit 2.
3828
+ - \`--probe\` mirrors \`serve --probe\`.
3829
+
3830
+ Exit: 0 / 1 / 2.
3831
+
3832
+ ### spec mcp
3833
+
3834
+ MCP server over stdio. Register it as \`{ "command": "spec", "args": ["mcp", "<platformDir>"] }\`.
3835
+
3836
+ | Tool | Returns |
3837
+ | --- | --- |
3838
+ | \`spec_query\` | FTS hits, the \`spec query --json\` shape |
3839
+ | \`spec_resolve\` | files \u2192 requirements |
3840
+ | \`spec_req_tags\` | requirement \u2192 tag sites |
3841
+ | \`spec_coverage_report\` | per-domain rollup \`{ domain, active, implemented, verified, orphans, unverified }\` |
3842
+ | \`spec_check\` | diagnostic rows |
3843
+ | \`spec_propagation\` | per-repo migration state |
3844
+ | \`spec_next_id\` | \`{ domain, next_id }\`, nothing written |
3845
+ | \`spec_get\` | one \`Requirement\` row with \`implemented_in\` and \`verified_in\`, or \`[]\` for an unknown id; the \`spec get --json\` shape |
3846
+ | \`spec_list\` | \`Requirement\` rows in (key, seq) order, \`domain\` and \`status\` filters; the \`spec list --json\` shape |
3847
+ | \`spec_supersede\` | \`{ old_id, new_id, file, spec_version, retagged }\`, the \`spec supersede --json\` shape; optional \`why\`, \`relates\`, \`issue\` arguments |
3848
+ | \`spec_deprecate\` | \`{ id, file, reason, removed }\`, the \`spec deprecate --json\` shape |
3849
+ | \`spec_accept\` | \`{ id, file }\`, the \`spec accept --json\` shape |
3850
+ | \`spec_req\` | \`{ id, file, clauses? }\`, the \`spec req --text --json\` shape; \`domain\` (key or prefix) and \`text\` required, optional \`why\`, \`relates\`, \`issue\`, \`draft\` |
3851
+ | \`spec_amend\` | \`{ id, file, fields_changed }\`, the \`spec amend --json\` shape; \`id\` required, optional \`text\`, \`why\`, \`relates\`, \`issue\` |
3852
+ | \`spec_move\` | \`{ old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged }\`, the \`spec move --json\` shape; \`id\` and \`newDomain\` required, optional \`text\`, \`why\` |
3853
+
3854
+ Every tool call reindexes fresh. stdout is the protocol channel; chrome goes to stderr. One prompt, \`author_requirements\` (\`brief\` required, \`domain\` optional), returns a static authoring playbook with the brief and the domain's charter substituted. The engine runs no model.
3855
+
3856
+ Exit: 0 / 2.
3857
+
3858
+ ### Gotchas
3859
+
3860
+ - **\`fixtures/\` is globally ignored by the tag scanner.** Tags under any \`fixtures/\` folder are test data, not coverage claims.
3861
+ - **The index owns nothing.** Never edit \`.spec-engine/index.sqlite\`. Edit the \`SPEC.json\` source and re-run \`spec index\`.
3862
+ - **\`storage_unavailable\` / \`SQLITE_IOERR*\` means your environment.** The index runs in WAL mode and needs real file locks. A sandboxed process gets \`SQLITE_IOERR_VNODE\`; re-run unsandboxed or grant write and lock access to \`<platformDir>/.spec-engine/\`. \`SQLITE_BUSY\` means another \`spec\` process holds the index.
3863
+ - **Empty is not an error.** Read commands exit 0 with \`[]\` and guidance on stderr.
3864
+ - **Determinism is contractual.** JSON outputs are sorted and byte-stable.
3865
+ - A directory passed as the last positional is \`platformDir\` only if it contains \`spec-engine/\`. Otherwise it is treated as a file, with a warning.
3866
+ `;
3867
+ var init_agent_reference = () => {};
3868
+
3869
+ // src/operations/agentSurface.ts
3870
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
3871
+ import { dirname as dirname4, join as join5, relative as relative2 } from "path";
3872
+ function mcpTemplate(platformArg) {
3873
+ const body = { mcpServers: { spec: { command: "spec", args: ["mcp", platformArg] } } };
3874
+ return `${JSON.stringify(body, null, 2)}
3875
+ `;
3876
+ }
3877
+ function shellArg(arg) {
3878
+ return /^[\w./-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", "'\\''")}'`;
3879
+ }
3880
+ function writeIfChanged(path, content, out) {
3881
+ if (existsSync3(path) && readFileSync(path, "utf8") === content)
3882
+ return;
3883
+ mkdirSync2(dirname4(path), { recursive: true });
3884
+ writeFileSync(path, content);
3885
+ out.written.push(path);
3886
+ }
3887
+ function writeReferenceBlock(dir, out) {
3888
+ const path = join5(dir, "AGENTS.md");
3889
+ const block = `${AGENT_BLOCK_BEGIN}
3890
+ ${AGENT_REFERENCE.trimEnd()}
3891
+ ${AGENT_BLOCK_END}`;
3892
+ const text = existsSync3(path) ? readFileSync(path, "utf8") : "";
3893
+ const begin = text.indexOf(AGENT_BLOCK_BEGIN);
3894
+ const end = text.indexOf(AGENT_BLOCK_END, begin);
3895
+ let next;
3896
+ if (begin !== -1 && end !== -1) {
3897
+ next = text.slice(0, begin) + block + text.slice(end + AGENT_BLOCK_END.length);
3898
+ } else if (begin !== -1 || text.includes(AGENT_BLOCK_END)) {
3899
+ out.hints.push(`${path} holds only one agent-reference marker; restore the pair to let spec init refresh the block`);
3900
+ return;
3901
+ } else {
3902
+ const gap = text === "" ? "" : text.endsWith(`
3903
+ `) ? `
3904
+ ` : `
3905
+
3906
+ `;
3907
+ next = `${text}${gap}${block}
3908
+ `;
3909
+ }
3910
+ writeIfChanged(path, next, out);
3911
+ const claude = join5(dir, "CLAUDE.md");
3912
+ if (existsSync3(claude) && !readFileSync(claude, "utf8").includes("AGENTS.md")) {
3913
+ out.hints.push(`${claude} does not reference AGENTS.md; add the line @AGENTS.md so Claude Code loads the spec reference`);
3914
+ }
3915
+ }
3916
+ function writeSkill(dir, force, out) {
3917
+ for (const [rel, content] of Object.entries(SKILL_FILES)) {
3918
+ const path = join5(dir, ".claude", "skills", rel);
3919
+ if (existsSync3(path) && !force && readFileSync(path, "utf8") !== content) {
3920
+ out.hints.push(`${path} differs from the bundled skill; re-run with --force to replace it`);
3921
+ continue;
3922
+ }
3923
+ writeIfChanged(path, content, out);
3924
+ }
3925
+ }
3926
+ function isRecord(value) {
3927
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3928
+ }
3929
+ function writeMcpEntry(dir, platformArg, out) {
3930
+ const path = join5(dir, ".mcp.json");
3931
+ const entry = { command: "spec", args: ["mcp", platformArg] };
3932
+ const wanted = `add to its mcpServers: "spec": ${JSON.stringify(entry)}`;
3933
+ if (!existsSync3(path)) {
3934
+ writeIfChanged(path, mcpTemplate(platformArg), out);
3935
+ return;
3936
+ }
3937
+ let config2;
3938
+ try {
3939
+ config2 = JSON.parse(readFileSync(path, "utf8"));
3940
+ } catch {
3941
+ out.hints.push(`${path} is not valid JSON; left unchanged. ${wanted}`);
3942
+ return;
3943
+ }
3944
+ if (!isRecord(config2) || config2.mcpServers !== undefined && !isRecord(config2.mcpServers)) {
3945
+ out.hints.push(`${path} is not an MCP server config object; left unchanged. ${wanted}`);
3946
+ return;
3947
+ }
3948
+ const servers = config2.mcpServers ?? {};
3949
+ if (servers.spec !== undefined) {
3950
+ if (JSON.stringify(servers.spec) !== JSON.stringify(entry)) {
3951
+ out.hints.push(`${path} already has a spec server entry; left unchanged. Expected ${wanted}`);
3952
+ }
3953
+ return;
3954
+ }
3955
+ config2.mcpServers = { ...servers, spec: entry };
3956
+ writeIfChanged(path, `${JSON.stringify(config2, null, 2)}
3957
+ `, out);
3958
+ }
3959
+ function writeHook(dir, platformArg, out) {
3960
+ const command = `spec guard ${shellArg(platformArg)}`;
3961
+ const hooksDir = [join5(dir, ".githooks"), join5(dir, ".git", "hooks")].find(isExistingDir);
3962
+ if (hooksDir === undefined) {
3963
+ out.hints.push(`${dir} has no hooks directory; add a pre-commit hook that runs: ${command}`);
3964
+ return;
3965
+ }
3966
+ const path = join5(hooksDir, "pre-commit");
3967
+ if (existsSync3(path)) {
3968
+ if (!readFileSync(path, "utf8").includes(command)) {
3969
+ out.hints.push(`${path} already exists; left unchanged. Add this line to it: ${command}`);
3970
+ }
3971
+ return;
3972
+ }
3973
+ writeFileSync(path, PRE_COMMIT.replace("spec guard .", command), { mode: 493 });
3974
+ out.written.push(path);
3975
+ if (hooksDir.endsWith(".githooks")) {
3976
+ out.hints.push(`git runs ${path} only after: git config core.hooksPath .githooks`);
3977
+ }
3978
+ }
3979
+ function writeAgentSurface(input) {
3980
+ const out = { written: [], hints: [] };
3981
+ writeReferenceBlock(input.dir, out);
3982
+ writeSkill(input.dir, input.force, out);
3983
+ if (input.platformDir === null) {
3984
+ out.hints.push("no platform was located, so the MCP server and the pre-commit hook were skipped; re-run spec init once the repository belongs to a platform");
3985
+ return out;
3986
+ }
3987
+ const platformArg = relative2(input.dir, input.platformDir) || ".";
3988
+ writeMcpEntry(input.dir, platformArg, out);
3989
+ writeHook(input.dir, platformArg, out);
3990
+ return out;
3991
+ }
3992
+ var AGENT_BLOCK_BEGIN = "<!-- spec-engine:agent-reference:begin -->", AGENT_BLOCK_END = "<!-- spec-engine:agent-reference:end -->", AGENT_REFERENCE, SKILL_FILES, PRE_COMMIT = `#!/bin/sh
3993
+ spec guard .
3994
+ exit $?
3995
+ `;
3996
+ var init_agentSurface = __esm(() => {
3997
+ init_SKILL();
3998
+ init_agent_reference();
3999
+ init_constants();
4000
+ AGENT_REFERENCE = agent_reference_default.replace(/^---\n[\s\S]*?\n---\n+/, "").replace(/^<!-- @spec [A-Z][A-Z0-9]*-\d+ -->\n\n?/gm, "").replace(/ <!-- @spec [A-Z][A-Z0-9]*-\d+ -->/g, "");
4001
+ SKILL_FILES = { "req-author/SKILL.md": SKILL_default };
4002
+ });
4003
+
2918
4004
  // src/operations/_result.ts
2919
4005
  function fail(reason, detail, extra = {}) {
2920
4006
  const out = { ok: false, reason, detail };
@@ -2936,8 +4022,8 @@ var init__result = __esm(() => {
2936
4022
  });
2937
4023
 
2938
4024
  // src/operations/init.ts
2939
- import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "fs";
2940
- import { basename as basename3, join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "path";
4025
+ import { existsSync as existsSync4, realpathSync, statSync as statSync2 } from "fs";
4026
+ import { basename as basename3, join as join6, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
2941
4027
  import {
2942
4028
  applyInit,
2943
4029
  applyLink,
@@ -2971,7 +4057,7 @@ function canonicalize(repoDir) {
2971
4057
  function judgeSpecTree(repoDir, canonical, root) {
2972
4058
  if (!hasSpecEngineSegment(canonical))
2973
4059
  return { ok: true };
2974
- const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative2(root, canonical) : canonical;
4060
+ const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative3(root, canonical) : canonical;
2975
4061
  if (hasSpecEngineSegment(judged)) {
2976
4062
  return fail("usage", `${repoDir} resolves to ${canonical}, which is inside a spec-engine/ tree \u2014 refusing to scaffold there.`);
2977
4063
  }
@@ -3019,7 +4105,7 @@ function place(repoDir, canonical) {
3019
4105
  return fail("usage", `${canonical} is neither a declared member nor a workspace package of the platform at ${root}`);
3020
4106
  }
3021
4107
  function link(canonical, platformRoot) {
3022
- if (!existsSync3(join5(canonical, PLATFORM_MAP_FILENAME))) {
4108
+ if (!existsSync4(join6(canonical, PLATFORM_MAP_FILENAME))) {
3023
4109
  return fail("usage", `${canonical} carries no platform-map.json marker, so it is not a declared member; declare it from the platform folder with \`spec init <name>\``);
3024
4110
  }
3025
4111
  const plan = planLink(canonical, { root: resolve4(platformRoot) });
@@ -3034,7 +4120,7 @@ async function resolveMemberPin(input) {
3034
4120
  if (!canon.ok)
3035
4121
  return canon;
3036
4122
  const { canonical } = canon;
3037
- if (isExistingDir(join5(canonical, CANONICAL_SPECS_DIR))) {
4123
+ if (isExistingDir(join6(canonical, CANONICAL_SPECS_DIR))) {
3038
4124
  return fail("usage", `${canonical} is a platform dir (contains spec-engine/) \u2014 pass a member subdir or cd into one`);
3039
4125
  }
3040
4126
  let linked = null;
@@ -3084,7 +4170,7 @@ function planDeclare(platformDir, name) {
3084
4170
  }
3085
4171
  const candidate = plan.candidates.find((c) => c.name === name);
3086
4172
  if (candidate === undefined) {
3087
- return fail("usage", `${join5(platformDir, name)} has no .git entry or package.json, so platform-map cannot declare it as a member`);
4173
+ return fail("usage", `${join6(platformDir, name)} has no .git entry or package.json, so platform-map cannot declare it as a member`);
3088
4174
  }
3089
4175
  if (candidate.marker !== undefined && candidate.marker !== plan.platformName) {
3090
4176
  return fail("conflict", `${name} carries a platform-map.json marker for platform "${candidate.marker}", not "${plan.platformName}"`);
@@ -3127,8 +4213,8 @@ async function write(configPath, specs, ignore) {
3127
4213
  }
3128
4214
  async function writeMemberConfig(input) {
3129
4215
  const { canonical, pin } = input;
3130
- const configPath = join5(canonical, MEMBER_CONFIG_FILENAME);
3131
- if (!existsSync3(configPath)) {
4216
+ const configPath = join6(canonical, MEMBER_CONFIG_FILENAME);
4217
+ if (!existsSync4(configPath)) {
3132
4218
  await write(configPath, pin, input.ignore);
3133
4219
  return { ok: true, action: "wrote", path: configPath, pin };
3134
4220
  }
@@ -3251,16 +4337,16 @@ function renderDiagnostics(rows, mode) {
3251
4337
  }
3252
4338
 
3253
4339
  // src/operations/_index.ts
3254
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync3 } from "fs";
3255
- import { dirname as dirname4, join as join6 } from "path";
4340
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, statSync as statSync3 } from "fs";
4341
+ import { dirname as dirname5, join as join7 } from "path";
3256
4342
  async function withIndex(opts, fn) {
3257
4343
  const { platformDir, build } = opts;
3258
4344
  const dbPath = opts.dbPath ?? defaultIndexPath(platformDir);
3259
4345
  assertSpecPlatform(platformDir);
3260
- mkdirSync2(dirname4(dbPath), { recursive: true });
4346
+ mkdirSync3(dirname5(dbPath), { recursive: true });
3261
4347
  if (build === "fresh" || build === "reset")
3262
4348
  coldResetDb(dbPath);
3263
- const wasMissing = !existsSync4(dbPath);
4349
+ const wasMissing = !existsSync5(dbPath);
3264
4350
  const storage2 = openStorage(dbPath);
3265
4351
  try {
3266
4352
  const warnings = [];
@@ -3281,12 +4367,12 @@ async function withIndex(opts, fn) {
3281
4367
  function staleIndexWarning(platformDir, dbPath) {
3282
4368
  try {
3283
4369
  const dbMtime = statSync3(dbPath).mtimeMs;
3284
- const specsDir = join6(platformDir, CANONICAL_SPECS_DIR);
4370
+ const specsDir = join7(platformDir, CANONICAL_SPECS_DIR);
3285
4371
  for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
3286
4372
  if (!entry.isDirectory())
3287
4373
  continue;
3288
- const specPath = join6(specsDir, entry.name, SPEC_FILENAME);
3289
- if (!existsSync4(specPath))
4374
+ const specPath = join7(specsDir, entry.name, SPEC_FILENAME);
4375
+ if (!existsSync5(specPath))
3290
4376
  continue;
3291
4377
  if (statSync3(specPath).mtimeMs > dbMtime) {
3292
4378
  return `spec: warning \u2014 ${CANONICAL_SPECS_DIR}/${entry.name}/${SPEC_FILENAME} changed after the index was built; results may be stale. Pass --fresh to rebuild.`;
@@ -3348,14 +4434,14 @@ function handleNotAPlatform(e) {
3348
4434
  }
3349
4435
  throw e;
3350
4436
  }
3351
- function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
4437
+ async function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
3352
4438
  if (!(e instanceof InvalidDomainFilesError))
3353
4439
  return;
3354
4440
  if (json)
3355
4441
  console.log(renderDiagnostics(e.diagnostics, "json"));
3356
4442
  else
3357
4443
  console.error(renderDiagnostics(e.diagnostics, "text"));
3358
- process.exit(code);
4444
+ await exit(code);
3359
4445
  }
3360
4446
  function cliFreshTags(platformDir) {
3361
4447
  const fresh = coldFreshTags(platformDir);
@@ -3363,7 +4449,7 @@ function cliFreshTags(platformDir) {
3363
4449
  try {
3364
4450
  return await fresh(reqId);
3365
4451
  } catch (e) {
3366
- handleInvalidDomainFiles(e, false, EXIT.USAGE);
4452
+ await handleInvalidDomainFiles(e, false, EXIT.USAGE);
3367
4453
  throw e;
3368
4454
  }
3369
4455
  };
@@ -3379,7 +4465,7 @@ function printWarnings(cmdName, warnings) {
3379
4465
  for (const w of warnings ?? []) {
3380
4466
  if (w.kind === "ref")
3381
4467
  console.error(`spec req: warning \u2014 ${w.text}`);
3382
- else if (w.kind === "grammar")
4468
+ else if (w.kind === "grammar" || w.kind === "statement")
3383
4469
  console.error(`${cmdName}: ${w.text}`);
3384
4470
  else
3385
4471
  console.error(w.text);
@@ -3405,7 +4491,7 @@ async function withReadStorage(opts, fn) {
3405
4491
  });
3406
4492
  } catch (e) {
3407
4493
  handleStorageUnavailable(e, dbPath);
3408
- handleInvalidDomainFiles(e, json);
4494
+ await handleInvalidDomainFiles(e, json);
3409
4495
  handleNotAPlatform(e);
3410
4496
  }
3411
4497
  }
@@ -3414,6 +4500,7 @@ var init__shared = __esm(() => {
3414
4500
  init_constants();
3415
4501
  init_discover();
3416
4502
  init__index();
4503
+ init_stdout();
3417
4504
  init_errors();
3418
4505
  init_sqlite();
3419
4506
  });
@@ -3424,7 +4511,7 @@ __export(exports_init, {
3424
4511
  runInit: () => runInit,
3425
4512
  initCommand: () => initCommand
3426
4513
  });
3427
- import { resolve as resolve5 } from "path";
4514
+ import { dirname as dirname6, join as join8, resolve as resolve5 } from "path";
3428
4515
  import { defineCommand } from "citty";
3429
4516
  function errMessage2(err) {
3430
4517
  return err instanceof Error ? err.message : String(err);
@@ -3456,19 +4543,54 @@ function sourceText(result, json) {
3456
4543
  }
3457
4544
  return "fallback @1";
3458
4545
  }
4546
+ function printSurface(surface, json) {
4547
+ for (const hint of surface.hints)
4548
+ console.error(`spec init: ${hint}`);
4549
+ if (json)
4550
+ return;
4551
+ for (const file of surface.written)
4552
+ console.log(` agent surface: ${file}`);
4553
+ }
4554
+ function surfaceFor(result, force) {
4555
+ const { placement } = result;
4556
+ return writeAgentSurface({
4557
+ dir: placement.kind === "package" ? placement.platformDir : dirname6(result.path),
4558
+ platformDir: placement.kind === "loose" ? null : placement.platformDir,
4559
+ force
4560
+ });
4561
+ }
4562
+ function isPlatformDir(dir) {
4563
+ try {
4564
+ return isExistingDir(join8(dir, CANONICAL_SPECS_DIR));
4565
+ } catch {
4566
+ return false;
4567
+ }
4568
+ }
4569
+ function initPlatformDir(dir, force, json) {
4570
+ const surface = writeAgentSurface({ dir, platformDir: dir, force });
4571
+ if (json) {
4572
+ console.log(JSON.stringify({ action: "surface-only", agent_surface: surface.written }));
4573
+ } else {
4574
+ console.log(`spec init: ${dir} is a platform directory; wrote no member config`);
4575
+ }
4576
+ printSurface(surface, json);
4577
+ }
3459
4578
  async function runInit(args) {
3460
4579
  const json = Boolean(args.json);
4580
+ const force = Boolean(args.force);
3461
4581
  const override = typeof args.specs === "string" ? validateOverride(args.specs) : undefined;
3462
4582
  const result = await initMember({
3463
4583
  repoDir: resolveRepoArg(args.repo),
3464
4584
  override,
3465
4585
  platformRoot: typeof args.platform === "string" ? args.platform : undefined,
3466
- force: Boolean(args.force)
4586
+ force
3467
4587
  });
3468
4588
  if (!result.ok)
3469
4589
  exitOnFailure("spec init", result);
3470
4590
  const source = sourceText(result, json);
3471
4591
  const { declared, linked } = result;
4592
+ const surface = surfaceFor(result, force);
4593
+ const agent_surface = surface.written;
3472
4594
  if (result.action === "already-configured") {
3473
4595
  if (json) {
3474
4596
  console.log(JSON.stringify({
@@ -3477,8 +4599,10 @@ async function runInit(args) {
3477
4599
  pin: result.pin,
3478
4600
  extra_fields: result.extraFields,
3479
4601
  declared,
3480
- linked
4602
+ linked,
4603
+ agent_surface
3481
4604
  }));
4605
+ printSurface(surface, json);
3482
4606
  return;
3483
4607
  }
3484
4608
  console.log("spec init: already configured");
@@ -3491,6 +4615,7 @@ async function runInit(args) {
3491
4615
  console.log(` declared: ${file}`);
3492
4616
  if (linked !== null)
3493
4617
  console.log(` linked: ${linked}`);
4618
+ printSurface(surface, json);
3494
4619
  return;
3495
4620
  }
3496
4621
  if (json) {
@@ -3500,8 +4625,10 @@ async function runInit(args) {
3500
4625
  pin: result.pin,
3501
4626
  source,
3502
4627
  declared,
3503
- linked
4628
+ linked,
4629
+ agent_surface
3504
4630
  }));
4631
+ printSurface(surface, json);
3505
4632
  return;
3506
4633
  }
3507
4634
  console.log(`spec init: wrote ${result.path}`);
@@ -3511,17 +4638,19 @@ async function runInit(args) {
3511
4638
  console.log(` declared: ${file}`);
3512
4639
  if (linked !== null)
3513
4640
  console.log(` linked: ${linked}`);
4641
+ printSurface(surface, json);
3514
4642
  }
3515
4643
  var initCommand;
3516
4644
  var init_init2 = __esm(() => {
3517
4645
  init_src();
3518
4646
  init_constants();
4647
+ init_agentSurface();
3519
4648
  init_init();
3520
4649
  init__shared();
3521
4650
  initCommand = defineCommand({
3522
4651
  meta: {
3523
4652
  name: "init",
3524
- description: "Declare a member repo and scaffold its spec-engine.member.json"
4653
+ description: "Declare a member repo, scaffold its spec-engine.member.json, and install the agent surface"
3525
4654
  },
3526
4655
  args: {
3527
4656
  repo: {
@@ -3539,7 +4668,7 @@ var init_init2 = __esm(() => {
3539
4668
  },
3540
4669
  force: {
3541
4670
  type: "boolean",
3542
- description: "Overwrite an existing spec-engine.member.json (refuses if existing has extra keys)."
4671
+ description: "Overwrite an existing spec-engine.member.json (refuses if existing has extra keys) and a differing req-author skill."
3543
4672
  },
3544
4673
  json: {
3545
4674
  type: "boolean",
@@ -3547,7 +4676,11 @@ var init_init2 = __esm(() => {
3547
4676
  }
3548
4677
  },
3549
4678
  async run({ args }) {
3550
- await runInit(args);
4679
+ const repoDir = resolveRepoArg(args.repo);
4680
+ if (isPlatformDir(repoDir))
4681
+ initPlatformDir(repoDir, Boolean(args.force), Boolean(args.json));
4682
+ else
4683
+ await runInit(args);
3551
4684
  }
3552
4685
  });
3553
4686
  });
@@ -3612,7 +4745,7 @@ function listFlag(rawArgs, name, parsed) {
3612
4745
  }
3613
4746
  return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
3614
4747
  }
3615
- var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, testsArg, relatesArg;
4748
+ var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, relatesArg;
3616
4749
  var init__args = __esm(() => {
3617
4750
  init_constants();
3618
4751
  platformDirArg = {
@@ -3633,14 +4766,6 @@ var init__args = __esm(() => {
3633
4766
  type: "boolean",
3634
4767
  description: "Suppress the interactive onboarding prompt for siblings missing spec-engine.member.json (they surface as NO_SPEC_CONFIG warnings)"
3635
4768
  };
3636
- livesArg = {
3637
- type: "string",
3638
- description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
3639
- };
3640
- testsArg = {
3641
- type: "string",
3642
- description: "Verified in (verifiedIn) test paths: repeat the flag or comma-separate; an empty value clears the list"
3643
- };
3644
4769
  relatesArg = {
3645
4770
  type: "string",
3646
4771
  description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
@@ -3652,8 +4777,8 @@ var exports_commands = {};
3652
4777
  __export(exports_commands, {
3653
4778
  indexCommand: () => indexCommand
3654
4779
  });
3655
- import { mkdirSync as mkdirSync3 } from "fs";
3656
- import { dirname as dirname5 } from "path";
4780
+ import { mkdirSync as mkdirSync4 } from "fs";
4781
+ import { dirname as dirname7 } from "path";
3657
4782
  import { defineCommand as defineCommand2 } from "citty";
3658
4783
  var indexCommand;
3659
4784
  var init_commands = __esm(() => {
@@ -3700,7 +4825,7 @@ var init_commands = __esm(() => {
3700
4825
  let storage2;
3701
4826
  try {
3702
4827
  assertSpecPlatform(platformDir);
3703
- mkdirSync3(dirname5(dbPath), { recursive: true });
4828
+ mkdirSync4(dirname7(dbPath), { recursive: true });
3704
4829
  storage2 = openStorage(dbPath);
3705
4830
  const result = await runIndex({ platformDir, storage: storage2 });
3706
4831
  if (args.json) {
@@ -3719,7 +4844,7 @@ var init_commands = __esm(() => {
3719
4844
  console.error(formatNotASpecPlatform(err.platformDir));
3720
4845
  process.exit(EXIT.USAGE);
3721
4846
  }
3722
- handleInvalidDomainFiles(err, !!args.json);
4847
+ await handleInvalidDomainFiles(err, !!args.json);
3723
4848
  console.error("spec index FAILED:", err);
3724
4849
  process.exit(EXIT.FAILURE);
3725
4850
  } finally {
@@ -3729,6 +4854,87 @@ var init_commands = __esm(() => {
3729
4854
  });
3730
4855
  });
3731
4856
 
4857
+ // src/base/gitBase.ts
4858
+ function gitEnv() {
4859
+ return Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_") || k.startsWith("GIT_CONFIG")));
4860
+ }
4861
+ function isSafeRef(ref) {
4862
+ return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
4863
+ }
4864
+ function gitRepoPrefix(platformDir) {
4865
+ const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], {
4866
+ cwd: platformDir,
4867
+ env: gitEnv()
4868
+ });
4869
+ if (proc.exitCode !== 0)
4870
+ return "";
4871
+ return proc.stdout.toString().trim();
4872
+ }
4873
+ function gitRefResolves(platformDir, ref) {
4874
+ if (!isSafeRef(ref))
4875
+ return false;
4876
+ const proc = Bun.spawnSync(["git", "rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
4877
+ cwd: platformDir,
4878
+ env: gitEnv()
4879
+ });
4880
+ return proc.exitCode === 0;
4881
+ }
4882
+ function gitShow(platformDir, ref, relPath) {
4883
+ if (!isSafeRef(ref))
4884
+ return null;
4885
+ const full = `${gitRepoPrefix(platformDir)}${relPath}`;
4886
+ const proc = Bun.spawnSync(["git", "show", `${ref}:${full}`], {
4887
+ cwd: platformDir,
4888
+ env: gitEnv()
4889
+ });
4890
+ if (proc.exitCode !== 0)
4891
+ return null;
4892
+ return proc.stdout.toString();
4893
+ }
4894
+ function gitLsTree(platformDir, ref, dir) {
4895
+ if (!isSafeRef(ref))
4896
+ return [];
4897
+ const prefix = gitRepoPrefix(platformDir);
4898
+ const proc = Bun.spawnSync(["git", "ls-tree", "-r", "--name-only", ref, "--", `${prefix}${dir}`], { cwd: platformDir, env: gitEnv() });
4899
+ if (proc.exitCode !== 0)
4900
+ return [];
4901
+ return proc.stdout.toString().split(`
4902
+ `).map((l) => l.trim()).filter((l) => l.length > 0).map((l) => prefix.length > 0 && l.startsWith(prefix) ? l.slice(prefix.length) : l);
4903
+ }
4904
+ function gitTreeMatchesHead(platformDir) {
4905
+ return Bun.spawnSync(["git", "diff", "--quiet", "HEAD", "--", ".", ":!.spec-engine"], {
4906
+ cwd: platformDir
4907
+ }).exitCode === 0;
4908
+ }
4909
+ function gitDiffNameStatus(platformDir, ref) {
4910
+ if (!isSafeRef(ref))
4911
+ return [];
4912
+ const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], { cwd: platformDir, env: gitEnv() });
4913
+ if (proc.exitCode !== 0)
4914
+ return [];
4915
+ const rows = [];
4916
+ for (const line of proc.stdout.toString().split(`
4917
+ `)) {
4918
+ if (line.length === 0)
4919
+ continue;
4920
+ const parts = line.split("\t");
4921
+ const status = (parts[0] ?? "").trim().toUpperCase();
4922
+ const path = (parts[parts.length - 1] ?? "").trim();
4923
+ if (status.length === 0 || path.length === 0)
4924
+ continue;
4925
+ rows.push({ status, path });
4926
+ }
4927
+ return rows;
4928
+ }
4929
+ function newestReleaseTag(platformDir) {
4930
+ const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir, env: gitEnv() });
4931
+ if (proc.exitCode !== 0)
4932
+ return null;
4933
+ const tag = proc.stdout.toString().split(`
4934
+ `)[0]?.trim() ?? "";
4935
+ return tag !== "" && isSafeRef(tag) ? tag : null;
4936
+ }
4937
+
3732
4938
  // src/check/proven.ts
3733
4939
  function normalizeToSegments(path) {
3734
4940
  const out = [];
@@ -3852,74 +5058,6 @@ var init_proven = __esm(() => {
3852
5058
  init_src();
3853
5059
  });
3854
5060
 
3855
- // src/base/gitBase.ts
3856
- function isSafeRef(ref) {
3857
- return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
3858
- }
3859
- function gitRepoPrefix(platformDir) {
3860
- const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], { cwd: platformDir });
3861
- if (proc.exitCode !== 0)
3862
- return "";
3863
- return proc.stdout.toString().trim();
3864
- }
3865
- function gitRefResolves(platformDir, ref) {
3866
- if (!isSafeRef(ref))
3867
- return false;
3868
- const proc = Bun.spawnSync(["git", "rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
3869
- cwd: platformDir
3870
- });
3871
- return proc.exitCode === 0;
3872
- }
3873
- function gitShow(platformDir, ref, relPath) {
3874
- if (!isSafeRef(ref))
3875
- return null;
3876
- const full = `${gitRepoPrefix(platformDir)}${relPath}`;
3877
- const proc = Bun.spawnSync(["git", "show", `${ref}:${full}`], { cwd: platformDir });
3878
- if (proc.exitCode !== 0)
3879
- return null;
3880
- return proc.stdout.toString();
3881
- }
3882
- function gitLsTree(platformDir, ref, dir) {
3883
- if (!isSafeRef(ref))
3884
- return [];
3885
- const prefix = gitRepoPrefix(platformDir);
3886
- const proc = Bun.spawnSync(["git", "ls-tree", "-r", "--name-only", ref, "--", `${prefix}${dir}`], { cwd: platformDir });
3887
- if (proc.exitCode !== 0)
3888
- return [];
3889
- return proc.stdout.toString().split(`
3890
- `).map((l) => l.trim()).filter((l) => l.length > 0).map((l) => prefix.length > 0 && l.startsWith(prefix) ? l.slice(prefix.length) : l);
3891
- }
3892
- function gitDiffNameStatus(platformDir, ref) {
3893
- if (!isSafeRef(ref))
3894
- return [];
3895
- const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], {
3896
- cwd: platformDir
3897
- });
3898
- if (proc.exitCode !== 0)
3899
- return [];
3900
- const rows = [];
3901
- for (const line of proc.stdout.toString().split(`
3902
- `)) {
3903
- if (line.length === 0)
3904
- continue;
3905
- const parts = line.split("\t");
3906
- const status = (parts[0] ?? "").trim().toUpperCase();
3907
- const path = (parts[parts.length - 1] ?? "").trim();
3908
- if (status.length === 0 || path.length === 0)
3909
- continue;
3910
- rows.push({ status, path });
3911
- }
3912
- return rows;
3913
- }
3914
- function newestReleaseTag(platformDir) {
3915
- const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir });
3916
- if (proc.exitCode !== 0)
3917
- return null;
3918
- const tag = proc.stdout.toString().split(`
3919
- `)[0]?.trim() ?? "";
3920
- return tag !== "" && isSafeRef(tag) ? tag : null;
3921
- }
3922
-
3923
5061
  // src/check/codeowners.ts
3924
5062
  function parseCodeowners(text) {
3925
5063
  const rules = [];
@@ -3997,18 +5135,18 @@ function matchesSegment(pat, seg) {
3997
5135
  }
3998
5136
 
3999
5137
  // src/authoring/domains.ts
4000
- import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
4001
- import { join as join7 } from "path";
5138
+ import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
5139
+ import { join as join9 } from "path";
4002
5140
  function normalizeDomainKey(raw) {
4003
5141
  return raw.toUpperCase().replace(/\s+/g, "");
4004
5142
  }
4005
5143
  function listDomainKeys(platformDir) {
4006
- const specsDir = join7(platformDir, "spec-engine");
4007
- return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(join7(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
5144
+ const specsDir = join9(platformDir, "spec-engine");
5145
+ return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync6(join9(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
4008
5146
  }
4009
5147
  async function domainScope(platformDir, key) {
4010
5148
  const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
4011
- if (!existsSync5(jsonPath))
5149
+ if (!existsSync6(jsonPath))
4012
5150
  return null;
4013
5151
  let text;
4014
5152
  try {
@@ -4049,166 +5187,28 @@ function requirementsOf(text, sourceFile) {
4049
5187
  }
4050
5188
  async function headMaxSeq(platformDir, key) {
4051
5189
  try {
4052
- const proc = Bun.spawn(["git", "-C", join7(platformDir, "spec-engine", key), "show", "HEAD:./SPEC.json"], { stdout: "pipe", stderr: "ignore" });
5190
+ const proc = Bun.spawn(["git", "-C", join9(platformDir, "spec-engine", key), "show", "HEAD:./SPEC.json"], { stdout: "pipe", stderr: "ignore", env: gitEnv() });
4053
5191
  const text = await new Response(proc.stdout).text();
4054
5192
  if (await proc.exited !== 0)
4055
- return 0;
4056
- return maxSeqOf(requirementsOf(text, specPaths(platformDir, key).rel));
4057
- } catch {
4058
- return 0;
4059
- }
4060
- }
4061
- async function nextRequirementId(platformDir, key) {
4062
- const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
4063
- if (!existsSync5(jsonPath))
4064
- return `${key}-001`;
4065
- const requirements = requirementsOf(await Bun.file(jsonPath).text(), relFile);
4066
- const maxSeq = Math.max(maxSeqOf(requirements), await headMaxSeq(platformDir, key));
4067
- const next = String(maxSeq + 1).padStart(3, "0");
4068
- return `${key}-${next}`;
4069
- }
4070
- var init_domains = __esm(() => {
4071
- init_src();
4072
- init_constants();
4073
- init_src();
4074
- });
4075
-
4076
- // src/authoring/filerefs.ts
4077
- import { existsSync as existsSync6 } from "fs";
4078
- import { resolve as resolve7 } from "path";
4079
- function extractRefsFromText(s) {
4080
- const out = [];
4081
- for (const m of s.matchAll(FILE_REF_RE)) {
4082
- const candidate = (m[1] ?? "").replace(/\.+$/, "");
4083
- if (candidate.includes("/"))
4084
- out.push(candidate);
4085
- }
4086
- return out;
4087
- }
4088
- function resolveFileRef(platformDir, ref) {
4089
- const root = resolve7(platformDir);
4090
- const abs = resolve7(root, ref);
4091
- if (!(abs === root || abs.startsWith(`${root}/`)))
4092
- return false;
4093
- return existsSync6(abs);
4094
- }
4095
- var FILE_REF_RE;
4096
- var init_filerefs = __esm(() => {
4097
- FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
4098
- });
4099
-
4100
- // src/check/filerefs.ts
4101
- function locateInDomain(raw, sourceFile, requirements, statuses) {
4102
- const out = [];
4103
- const rawLines = raw.split(`
4104
- `);
4105
- let lineCursor = 0;
4106
- for (const req of requirements) {
4107
- const found = findRequirementIdLine(rawLines, req.id, lineCursor);
4108
- if (found >= 0)
4109
- lineCursor = found + 1;
4110
- if (statuses.has(req.status.toLowerCase())) {
4111
- out.push({ sourceFile, line: found >= 0 ? found + 1 : 0, req });
4112
- }
4113
- }
4114
- return out;
4115
- }
4116
- async function locatedEntries(platformDir, statuses) {
4117
- const out = [];
4118
- for (const key of listDomainKeys(platformDir)) {
4119
- const { abs, rel } = specPaths(platformDir, key);
4120
- let raw;
4121
- try {
4122
- raw = await Bun.file(abs).text();
4123
- } catch {
4124
- continue;
4125
- }
4126
- const parsed = parseDomainText(raw, rel);
4127
- if (parsed.ok)
4128
- out.push(...locateInDomain(raw, rel, parsed.data.requirements, statuses));
4129
- }
4130
- return out;
4131
- }
4132
- function sortRows(rows) {
4133
- return rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? "") || a.code.localeCompare(b.code) || a.detail.localeCompare(b.detail));
4134
- }
4135
- function row(at, code, severity, detail) {
4136
- return {
4137
- code,
4138
- severity,
4139
- repo: null,
4140
- source_file: at.sourceFile,
4141
- line: at.line,
4142
- req_id: at.req.id,
4143
- detail
4144
- };
4145
- }
4146
- async function brokenFileRefDiagnostics(platformDir) {
4147
- const rows = [];
4148
- for (const at of await locatedEntries(platformDir, FILE_REF_STATUSES)) {
4149
- for (const [field, entries] of [
4150
- ["livesIn", at.req.livesIn],
4151
- ["verifiedIn", at.req.verifiedIn]
4152
- ]) {
4153
- for (const entry of entries) {
4154
- const ref = refOf(entry);
4155
- if (ref.length === 0 || resolveFileRef(platformDir, ref))
4156
- continue;
4157
- rows.push(row(at, DiagnosticCode.BROKEN_FILE_REF, "error", `${field} entry ${entry} does not resolve to a file under the platform root`));
4158
- }
4159
- }
4160
- }
4161
- return sortRows(rows);
4162
- }
4163
- function tagKeys(tags, kinds) {
4164
- return new Set(tags.filter((t) => kinds.includes(t.kind)).map((t) => `${t.req_id} ${t.file}`));
4165
- }
4166
- function inFileRows(platformDir, members, at, check) {
4167
- const rows = [];
4168
- for (const entry of at.req[check.field]) {
4169
- const ref = refOf(entry);
4170
- if (ref.length === 0 || !resolveFileRef(platformDir, ref))
4171
- continue;
4172
- const file = scannedTagFile(members, platformDir, ref);
4173
- if (file === null || check.keys.has(`${at.req.id} ${file}`))
4174
- continue;
4175
- rows.push(row(at, check.code, "warning", `${check.field} entry ${entry} holds no ${check.missing} tag for ${at.req.id}`));
5193
+ return 0;
5194
+ return maxSeqOf(requirementsOf(text, specPaths(platformDir, key).rel));
5195
+ } catch {
5196
+ return 0;
4176
5197
  }
4177
- return rows;
4178
5198
  }
4179
- async function inFileTagDiagnostics(platformDir, tags) {
4180
- const { members } = await discoverRepos(platformDir, { quiet: true });
4181
- const checks = [
4182
- {
4183
- field: "livesIn",
4184
- code: DiagnosticCode.UNIMPLEMENTED_IN_FILE,
4185
- keys: tagKeys(tags, ["implements", "documents"]),
4186
- missing: "implementing or documenting"
4187
- },
4188
- {
4189
- field: "verifiedIn",
4190
- code: DiagnosticCode.UNVERIFIED_IN_FILE,
4191
- keys: tagKeys(tags, ["verifies"]),
4192
- missing: "verifying"
4193
- }
4194
- ];
4195
- const rows = [];
4196
- for (const at of await locatedEntries(platformDir, IN_FILE_STATUSES)) {
4197
- for (const check of checks)
4198
- rows.push(...inFileRows(platformDir, members, at, check));
4199
- }
4200
- return sortRows(rows);
5199
+ async function nextRequirementId(platformDir, key) {
5200
+ const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
5201
+ if (!existsSync6(jsonPath))
5202
+ return `${key}-001`;
5203
+ const requirements = requirementsOf(await Bun.file(jsonPath).text(), relFile);
5204
+ const maxSeq = Math.max(maxSeqOf(requirements), await headMaxSeq(platformDir, key));
5205
+ const next = String(maxSeq + 1).padStart(3, "0");
5206
+ return `${key}-${next}`;
4201
5207
  }
4202
- var FILE_REF_STATUSES, IN_FILE_STATUSES, refOf = (entry) => entry.startsWith("@") ? entry.slice(1) : entry;
4203
- var init_filerefs2 = __esm(() => {
5208
+ var init_domains = __esm(() => {
4204
5209
  init_src();
4205
- init_domains();
4206
- init_filerefs();
4207
5210
  init_constants();
4208
- init_discover();
4209
- init_pipeline();
4210
- FILE_REF_STATUSES = new Set(["active", "draft"]);
4211
- IN_FILE_STATUSES = new Set(["active"]);
5211
+ init_src();
4212
5212
  });
4213
5213
 
4214
5214
  // src/check/grammar.ts
@@ -4341,20 +5341,20 @@ var init_removed = __esm(() => {
4341
5341
  });
4342
5342
 
4343
5343
  // src/check/severity.ts
4344
- import { existsSync as existsSync7, readFileSync } from "fs";
4345
- import { join as join8 } from "path";
5344
+ import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
5345
+ import { join as join10 } from "path";
4346
5346
  function readCheckConfig(platformDir) {
4347
- const path = join8(platformDir, CHECK_CONFIG_REL);
5347
+ const path = join10(platformDir, CHECK_CONFIG_REL);
4348
5348
  if (!existsSync7(path))
4349
5349
  return { ok: true, config: { severity: [] } };
4350
- return parseCheckConfigText(readFileSync(path, "utf-8"), CHECK_CONFIG_REL);
5350
+ return parseCheckConfigText(readFileSync2(path, "utf-8"), CHECK_CONFIG_REL);
4351
5351
  }
4352
- function specificity(rule, row2) {
4353
- if (rule.code !== row2.code)
5352
+ function specificity(rule, row) {
5353
+ if (rule.code !== row.code)
4354
5354
  return Specificity.none;
4355
5355
  if (rule.match === undefined)
4356
5356
  return Specificity.code;
4357
- const id = row2.req_id;
5357
+ const id = row.req_id;
4358
5358
  if (id === null)
4359
5359
  return Specificity.none;
4360
5360
  if (rule.match.includes(id))
@@ -4364,20 +5364,20 @@ function specificity(rule, row2) {
4364
5364
  }
4365
5365
  function applySeverity(rows, rules) {
4366
5366
  const out = [];
4367
- for (const row2 of rows) {
5367
+ for (const row of rows) {
4368
5368
  let best;
4369
5369
  let bestScore = Specificity.none;
4370
5370
  for (const rule of rules) {
4371
- const score = specificity(rule, row2);
5371
+ const score = specificity(rule, row);
4372
5372
  if (score !== Specificity.none && score >= bestScore) {
4373
5373
  best = rule;
4374
5374
  bestScore = score;
4375
5375
  }
4376
5376
  }
4377
5377
  if (best === undefined)
4378
- out.push(row2);
5378
+ out.push(row);
4379
5379
  else if (best.severity !== "off")
4380
- out.push({ ...row2, severity: best.severity });
5380
+ out.push({ ...row, severity: best.severity });
4381
5381
  }
4382
5382
  return out;
4383
5383
  }
@@ -4424,6 +5424,60 @@ var init_sqlDiagnostics = __esm(() => {
4424
5424
  ]);
4425
5425
  });
4426
5426
 
5427
+ // src/check/statementRules.ts
5428
+ async function domainRows(platformDir, key, config2, scope) {
5429
+ const { abs, rel } = specPaths(platformDir, key);
5430
+ const raw = await Bun.file(abs).text().catch(() => null);
5431
+ if (raw === null)
5432
+ return [];
5433
+ const parsed = parseDomainText(raw, rel);
5434
+ if (!parsed.ok)
5435
+ return [];
5436
+ const rawLines = raw.split(`
5437
+ `);
5438
+ const rows = [];
5439
+ let lineCursor = 0;
5440
+ for (const req of parsed.data.requirements) {
5441
+ const found = findRequirementIdLine(rawLines, req.id, lineCursor);
5442
+ if (found >= 0)
5443
+ lineCursor = found + 1;
5444
+ if (scope.id !== undefined && req.id !== scope.id)
5445
+ continue;
5446
+ if (!scope.statuses.has(req.status.toLowerCase()))
5447
+ continue;
5448
+ for (const f of statementFindings(req.statement, config2)) {
5449
+ rows.push({ req_id: req.id, ...f, source_file: rel, line: found >= 0 ? found + 1 : 0 });
5450
+ }
5451
+ }
5452
+ return rows;
5453
+ }
5454
+ async function statementRuleRows(platformDir, config2, scope) {
5455
+ const rows = [];
5456
+ for (const key of scope.keys ?? listDomainKeys(platformDir)) {
5457
+ if (key !== "TERM")
5458
+ rows.push(...await domainRows(platformDir, key, config2, scope));
5459
+ }
5460
+ rows.sort((a, b) => a.source_file.localeCompare(b.source_file) || a.line - b.line || a.req_id.localeCompare(b.req_id));
5461
+ return rows;
5462
+ }
5463
+ async function statementRulesDiagnostics(platformDir, config2) {
5464
+ const rows = await statementRuleRows(platformDir, config2, { statuses: new Set(["active"]) });
5465
+ return rows.map((r) => ({
5466
+ code: DiagnosticCode.STATEMENT_RULES,
5467
+ severity: config2?.severity?.[r.rule] === "warning" ? "warning" : "error",
5468
+ repo: null,
5469
+ source_file: r.source_file,
5470
+ line: r.line,
5471
+ req_id: r.req_id,
5472
+ detail: formatStatementFinding(r)
5473
+ }));
5474
+ }
5475
+ var init_statementRules2 = __esm(() => {
5476
+ init_src();
5477
+ init_domains();
5478
+ init_constants();
5479
+ });
5480
+
4427
5481
  // src/check/statusflip.ts
4428
5482
  function normalizeHandle(h) {
4429
5483
  return (h.startsWith("@") ? h.slice(1) : h).toLowerCase();
@@ -4895,8 +5949,6 @@ function successorEntry(id, pred, fields) {
4895
5949
  supersedes: null,
4896
5950
  supersededBy: null,
4897
5951
  relates: fields.relates,
4898
- livesIn: fields.livesIn,
4899
- verifiedIn: fields.verifiedIn,
4900
5952
  issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
4901
5953
  aliases: [...pred.aliases],
4902
5954
  cites: pred.cites.map((c) => ({ ...c }))
@@ -4914,6 +5966,30 @@ var init__envelope = __esm(() => {
4914
5966
  init__result();
4915
5967
  });
4916
5968
 
5969
+ // src/authoring/filerefs.ts
5970
+ import { existsSync as existsSync9 } from "fs";
5971
+ import { resolve as resolve7 } from "path";
5972
+ function extractRefsFromText(s) {
5973
+ const out = [];
5974
+ for (const m of s.matchAll(FILE_REF_RE)) {
5975
+ const candidate = (m[1] ?? "").replace(/\.+$/, "");
5976
+ if (candidate.includes("/"))
5977
+ out.push(candidate);
5978
+ }
5979
+ return out;
5980
+ }
5981
+ function resolveFileRef(platformDir, ref) {
5982
+ const root = resolve7(platformDir);
5983
+ const abs = resolve7(root, ref);
5984
+ if (!(abs === root || abs.startsWith(`${root}/`)))
5985
+ return false;
5986
+ return existsSync9(abs);
5987
+ }
5988
+ var FILE_REF_RE;
5989
+ var init_filerefs = __esm(() => {
5990
+ FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
5991
+ });
5992
+
4917
5993
  // src/authoring/grammar.ts
4918
5994
  async function grammarConfig(platformDir, key) {
4919
5995
  const freeform = { grammar: "freeform", severity: "warning" };
@@ -4962,8 +6038,35 @@ var init_grammar2 = __esm(() => {
4962
6038
  init_constants();
4963
6039
  });
4964
6040
 
6041
+ // src/authoring/statementRules.ts
6042
+ function statementRulesVerdict(platformDir, key, statement) {
6043
+ if (key === "TERM" || statement.trim() === "")
6044
+ return { refusal: null, warnings: [] };
6045
+ const config2 = readCheckConfig(platformDir);
6046
+ if (!config2.ok)
6047
+ return { refusal: config2.detail, warnings: [] };
6048
+ const rules = config2.config.statementRules;
6049
+ const findings = statementFindings(statement, rules);
6050
+ const errors = findings.filter((f) => (rules?.severity?.[f.rule] ?? "error") === "error");
6051
+ if (errors.length > 0) {
6052
+ return { refusal: [...errors.map(formatStatementFinding), RULE_ZERO].join(`
6053
+ `), warnings: [] };
6054
+ }
6055
+ return {
6056
+ refusal: null,
6057
+ warnings: findings.map((f) => ({
6058
+ kind: "statement",
6059
+ text: `warning \u2014 ${formatStatementFinding(f)}`
6060
+ }))
6061
+ };
6062
+ }
6063
+ var init_statementRules3 = __esm(() => {
6064
+ init_src();
6065
+ init_severity();
6066
+ });
6067
+
4965
6068
  // src/operations/mint.ts
4966
- import { existsSync as existsSync9 } from "fs";
6069
+ import { existsSync as existsSync10 } from "fs";
4967
6070
  function unresolvableRefWarnings(platformDir, fieldValues) {
4968
6071
  const out = [];
4969
6072
  for (const value of fieldValues) {
@@ -4978,15 +6081,10 @@ function unresolvableRefWarnings(platformDir, fieldValues) {
4978
6081
  async function mint(input) {
4979
6082
  const { platformDir, key } = input;
4980
6083
  const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
4981
- if (!existsSync9(specPath)) {
6084
+ if (!existsSync10(specPath)) {
4982
6085
  return fail("not_found", `no domain ${key} (expected ${relFile} under ${platformDir})`);
4983
6086
  }
4984
- const warnings = unresolvableRefWarnings(platformDir, [
4985
- input.statement,
4986
- input.why,
4987
- ...input.livesIn,
4988
- ...input.verifiedIn ?? []
4989
- ]);
6087
+ const warnings = unresolvableRefWarnings(platformDir, [input.statement, input.why]);
4990
6088
  const verdict = await judgeStatementGrammar(platformDir, key, input.statement);
4991
6089
  if (!verdict.ok) {
4992
6090
  if (verdict.severity === "error") {
@@ -4994,6 +6092,10 @@ async function mint(input) {
4994
6092
  }
4995
6093
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
4996
6094
  }
6095
+ const rules = statementRulesVerdict(platformDir, key, input.statement);
6096
+ if (rules.refusal !== null)
6097
+ return fail("usage", rules.refusal, { warnings });
6098
+ warnings.push(...rules.warnings);
4997
6099
  const file = await readEnvelope(platformDir, key);
4998
6100
  if (!file.ok) {
4999
6101
  return fail(file.reason, file.detail, { diagnostics: file.diagnostics, warnings });
@@ -5008,8 +6110,6 @@ async function mint(input) {
5008
6110
  supersedes: null,
5009
6111
  supersededBy: null,
5010
6112
  relates: splitList(input.relates ?? []),
5011
- livesIn: input.livesIn,
5012
- verifiedIn: input.verifiedIn ?? [],
5013
6113
  issues: input.issue ? [{ role: "created", id: input.issue }] : [],
5014
6114
  aliases: [],
5015
6115
  cites: input.cites ?? []
@@ -5031,6 +6131,7 @@ var init_mint = __esm(() => {
5031
6131
  init_domains();
5032
6132
  init_filerefs();
5033
6133
  init_grammar2();
6134
+ init_statementRules3();
5034
6135
  init_constants();
5035
6136
  init__envelope();
5036
6137
  init__result();
@@ -5066,8 +6167,6 @@ async function mintTerm(input) {
5066
6167
  supersedes: null,
5067
6168
  supersededBy: null,
5068
6169
  relates: [],
5069
- livesIn: [],
5070
- verifiedIn: [],
5071
6170
  issues: [],
5072
6171
  aliases: input.aliases,
5073
6172
  cites: [],
@@ -5168,8 +6267,8 @@ var init_term = __esm(() => {
5168
6267
  });
5169
6268
 
5170
6269
  // src/operations/glossary.ts
5171
- import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
5172
- import { join as join9 } from "path";
6270
+ import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
6271
+ import { join as join11 } from "path";
5173
6272
  function parseGlossary(md) {
5174
6273
  const terms = [];
5175
6274
  let section = null;
@@ -5221,15 +6320,15 @@ function generateGlossary(terms) {
5221
6320
  }
5222
6321
  function readStoreTerms(platformDir) {
5223
6322
  const { abs: specPath, rel: relFile } = specPaths(platformDir, TERM_KEY);
5224
- if (!existsSync10(specPath))
6323
+ if (!existsSync11(specPath))
5225
6324
  return [];
5226
- const parsed = parseDomainText(readFileSync2(specPath, "utf8"), relFile);
6325
+ const parsed = parseDomainText(readFileSync3(specPath, "utf8"), relFile);
5227
6326
  if (!parsed.ok)
5228
6327
  return [];
5229
6328
  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 }));
5230
6329
  }
5231
6330
  function glossaryPath(platformDir) {
5232
- return join9(platformDir, "GLOSSARY.md");
6331
+ return join11(platformDir, "GLOSSARY.md");
5233
6332
  }
5234
6333
  async function migrateGlossary(platformDir) {
5235
6334
  const store = await termStore(platformDir);
@@ -5240,10 +6339,10 @@ async function migrateGlossary(platformDir) {
5240
6339
  return { ok: true, skipped: true, existing: domain2.requirements.length };
5241
6340
  }
5242
6341
  const gPath = glossaryPath(platformDir);
5243
- if (!existsSync10(gPath)) {
6342
+ if (!existsSync11(gPath)) {
5244
6343
  return fail("not_found", `no GLOSSARY.md to migrate (expected ${gPath})`);
5245
6344
  }
5246
- const parsed = parseGlossary(readFileSync2(gPath, "utf8"));
6345
+ const parsed = parseGlossary(readFileSync3(gPath, "utf8"));
5247
6346
  domain2.requirements = parsed.map((t, i) => ({
5248
6347
  id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
5249
6348
  status: "active",
@@ -5253,8 +6352,6 @@ async function migrateGlossary(platformDir) {
5253
6352
  supersedes: null,
5254
6353
  supersededBy: null,
5255
6354
  relates: [],
5256
- livesIn: [],
5257
- verifiedIn: [],
5258
6355
  issues: [],
5259
6356
  aliases: [],
5260
6357
  cites: [],
@@ -5281,10 +6378,10 @@ function glossaryDriftDiagnostic(platformDir) {
5281
6378
  if (terms.length === 0)
5282
6379
  return null;
5283
6380
  const gPath = glossaryPath(platformDir);
5284
- if (!existsSync10(gPath))
6381
+ if (!existsSync11(gPath))
5285
6382
  return null;
5286
6383
  const generated = generateGlossary(terms);
5287
- const committed = readFileSync2(gPath, "utf8");
6384
+ const committed = readFileSync3(gPath, "utf8");
5288
6385
  if (generated === committed)
5289
6386
  return null;
5290
6387
  return {
@@ -5300,7 +6397,7 @@ function glossaryDriftDiagnostic(platformDir) {
5300
6397
  function checkGlossary(platformDir) {
5301
6398
  const generated = generateGlossary(readStoreTerms(platformDir));
5302
6399
  const gPath = glossaryPath(platformDir);
5303
- const committed = existsSync10(gPath) ? readFileSync2(gPath, "utf8") : "";
6400
+ const committed = existsSync11(gPath) ? readFileSync3(gPath, "utf8") : "";
5304
6401
  return { ok: true, clean: generated === committed };
5305
6402
  }
5306
6403
  var GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
@@ -5314,7 +6411,7 @@ var init_glossary = __esm(() => {
5314
6411
  });
5315
6412
 
5316
6413
  // src/guard/collect.ts
5317
- import { join as join10 } from "path";
6414
+ import { join as join12 } from "path";
5318
6415
  function isIgnored(path) {
5319
6416
  return isPathIgnored(path);
5320
6417
  }
@@ -5362,7 +6459,7 @@ async function collectWorktreeReqs(canonicalDir) {
5362
6459
  const activeIds = new Set;
5363
6460
  const supersedesTargets = new Set;
5364
6461
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
5365
- const text = await Bun.file(join10(canonicalDir, rel)).text();
6462
+ const text = await Bun.file(join12(canonicalDir, rel)).text();
5366
6463
  for (const r of parseReqs(text)) {
5367
6464
  ids.add(r.id);
5368
6465
  if (r.status.toLowerCase() === "active")
@@ -5390,7 +6487,7 @@ async function collectFacts(platformDir, ref, storage2) {
5390
6487
  const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
5391
6488
  const base = collectBaseReqs(platformDir, ref, specPaths2);
5392
6489
  const baseTags = collectBaseTags(platformDir, ref, codePaths);
5393
- const worktree = await collectWorktreeReqs(join10(platformDir, CANONICAL_SPECS_DIR));
6490
+ const worktree = await collectWorktreeReqs(join12(platformDir, CANONICAL_SPECS_DIR));
5394
6491
  const counts = tallyWorktreeTags(storage2);
5395
6492
  return {
5396
6493
  baseReqs: base.reqs,
@@ -5507,14 +6604,63 @@ var init_guard = __esm(() => {
5507
6604
  init_collect();
5508
6605
  });
5509
6606
 
6607
+ // src/operations/migrate.ts
6608
+ async function migrationPendingDiagnostics(platformDir) {
6609
+ const rows = [];
6610
+ for (const key of listDomainKeys(platformDir)) {
6611
+ const { abs, rel } = specPaths(platformDir, key);
6612
+ const raw = await Bun.file(abs).text().catch(() => "");
6613
+ if (!LEGACY_KEY_RE.test(raw))
6614
+ continue;
6615
+ rows.push({
6616
+ code: DiagnosticCode.MIGRATION_PENDING,
6617
+ severity: "warning",
6618
+ repo: null,
6619
+ source_file: rel,
6620
+ line: null,
6621
+ req_id: null,
6622
+ detail: `${rel} still carries the retired livesIn or verifiedIn fields; run spec migrate to strip them`
6623
+ });
6624
+ }
6625
+ return rows;
6626
+ }
6627
+ async function migrate(platformDir) {
6628
+ const files = [];
6629
+ for (const key of listDomainKeys(platformDir)) {
6630
+ const file = await readEnvelope(platformDir, key);
6631
+ if (!file.ok)
6632
+ return file;
6633
+ if (!LEGACY_KEY_RE.test(await Bun.file(file.specPath).text()))
6634
+ continue;
6635
+ const res = await validateAndWrite(file.specPath, file.domain, file.relFile);
6636
+ if (!res.ok) {
6637
+ return fail("invalid_domain_file", res.diagnostics.map((d) => d.detail).join(`
6638
+ `), {
6639
+ diagnostics: res.diagnostics
6640
+ });
6641
+ }
6642
+ files.push(file.relFile);
6643
+ }
6644
+ return { ok: true, files };
6645
+ }
6646
+ var LEGACY_KEY_RE;
6647
+ var init_migrate = __esm(() => {
6648
+ init_src();
6649
+ init_domains();
6650
+ init_constants();
6651
+ init__envelope();
6652
+ init__result();
6653
+ LEGACY_KEY_RE = /(?<!\\)"(?:livesIn|verifiedIn)"\s*:/;
6654
+ });
6655
+
5510
6656
  // src/operations/check.ts
5511
- import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
5512
- import { join as join11 } from "path";
6657
+ import { existsSync as existsSync12, readFileSync as readFileSync4 } from "fs";
6658
+ import { join as join13 } from "path";
5513
6659
  function readCodeownersText(platformDir) {
5514
- const path = join11(platformDir, ".github", "CODEOWNERS");
5515
- if (!existsSync11(path))
6660
+ const path = join13(platformDir, ".github", "CODEOWNERS");
6661
+ if (!existsSync12(path))
5516
6662
  return null;
5517
- return readFileSync3(path, "utf-8");
6663
+ return readFileSync4(path, "utf-8");
5518
6664
  }
5519
6665
  function domainReqsFromText(text, platformRel) {
5520
6666
  const validated = parseDomainText(text, platformRel);
@@ -5527,7 +6673,7 @@ async function collectChangeReqs(canonicalDir) {
5527
6673
  const relPathById = new Map;
5528
6674
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
5529
6675
  const platformRel = `${CANONICAL_SPECS_DIR}/${rel}`;
5530
- const text = await Bun.file(join11(canonicalDir, rel)).text();
6676
+ const text = await Bun.file(join13(canonicalDir, rel)).text();
5531
6677
  const parsed = domainReqsFromText(text, platformRel);
5532
6678
  for (const r of parsed.reqs) {
5533
6679
  reqs.push(r);
@@ -5568,7 +6714,7 @@ async function ingestResults(storage2, resultsPath, diagnostics2) {
5568
6714
  }
5569
6715
  async function governanceGate(input, ref, diagnostics2, results) {
5570
6716
  const { platformDir } = input;
5571
- const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
6717
+ const change = await collectChangeReqs(join13(platformDir, CANONICAL_SPECS_DIR));
5572
6718
  const base = collectBaseReqs2(platformDir, ref, diagnostics2);
5573
6719
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
5574
6720
  const codeowners = parseCodeowners(readCodeownersText(platformDir) ?? "");
@@ -5611,9 +6757,9 @@ async function check(input, storage2) {
5611
6757
  diagnostics2.push(...unsourcedChanges(superseded, storage2.listProvenance()));
5612
6758
  }
5613
6759
  diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
5614
- diagnostics2.push(...await brokenFileRefDiagnostics(platformDir));
5615
- diagnostics2.push(...await inFileTagDiagnostics(platformDir, storage2.listTags()));
6760
+ diagnostics2.push(...await statementRulesDiagnostics(platformDir, checkConfig2.config.statementRules));
5616
6761
  diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
6762
+ diagnostics2.push(...await migrationPendingDiagnostics(platformDir));
5617
6763
  const glossaryDrift = glossaryDriftDiagnostic(platformDir);
5618
6764
  if (glossaryDrift !== null)
5619
6765
  diagnostics2.push(glossaryDrift);
@@ -5622,7 +6768,7 @@ async function check(input, storage2) {
5622
6768
  await governanceGate(input, input.base, diagnostics2, results);
5623
6769
  diagnostics2.push(...await lossDiagnostics(storage2, platformDir, input.base));
5624
6770
  } else if (gitRefResolves(platformDir, "HEAD")) {
5625
- const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
6771
+ const change = await collectChangeReqs(join13(platformDir, CANONICAL_SPECS_DIR));
5626
6772
  const baseParseSink = [];
5627
6773
  const base = collectBaseReqs2(platformDir, "HEAD", baseParseSink);
5628
6774
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
@@ -5640,13 +6786,13 @@ async function check(input, storage2) {
5640
6786
  var LOSS_SEVERITY;
5641
6787
  var init_check = __esm(() => {
5642
6788
  init_src();
5643
- init_filerefs2();
5644
6789
  init_grammar();
5645
6790
  init_propagation_teeth();
5646
6791
  init_proven();
5647
6792
  init_removed();
5648
6793
  init_severity();
5649
6794
  init_sqlDiagnostics();
6795
+ init_statementRules2();
5650
6796
  init_statusflip();
5651
6797
  init_supersedes();
5652
6798
  init_unsourced();
@@ -5657,6 +6803,7 @@ var init_check = __esm(() => {
5657
6803
  init__result();
5658
6804
  init_glossary();
5659
6805
  init_guard();
6806
+ init_migrate();
5660
6807
  LOSS_SEVERITY = {
5661
6808
  IMPL_LOST: "error",
5662
6809
  SPEC_FILE_DELETED: "error",
@@ -5667,7 +6814,8 @@ var init_check = __esm(() => {
5667
6814
  // src/commands/check.ts
5668
6815
  var exports_check = {};
5669
6816
  __export(exports_check, {
5670
- checkCommand: () => checkCommand
6817
+ checkCommand: () => checkCommand,
6818
+ CI_BASE_WARNING: () => CI_BASE_WARNING
5671
6819
  });
5672
6820
  import { resolve as resolve8 } from "path";
5673
6821
  import { defineCommand as defineCommand3 } from "citty";
@@ -5690,7 +6838,7 @@ function exitAfterCrash(e) {
5690
6838
  console.error(`spec check: crashed: ${msg}`);
5691
6839
  process.exit(EXIT.USAGE);
5692
6840
  }
5693
- var checkCommand;
6841
+ var CI_BASE_WARNING = 'spec check --ci: losses not checked, the working tree equals HEAD. In CI run: spec check . --ci --base "$(git merge-base HEAD origin/<default>)"', checkCommand;
5694
6842
  var init_check2 = __esm(() => {
5695
6843
  init_src();
5696
6844
  init_proven();
@@ -5699,6 +6847,7 @@ var init_check2 = __esm(() => {
5699
6847
  init_prompt();
5700
6848
  init__index();
5701
6849
  init_check();
6850
+ init_stdout();
5702
6851
  init__args();
5703
6852
  init__shared();
5704
6853
  checkCommand = defineCommand3({
@@ -5772,13 +6921,16 @@ var init_check2 = __esm(() => {
5772
6921
  return result;
5773
6922
  });
5774
6923
  } catch (e) {
5775
- handleInvalidDomainFiles(e, jsonMode);
6924
+ await handleInvalidDomainFiles(e, jsonMode);
5776
6925
  exitAfterCrash(e);
5777
6926
  }
5778
6927
  if (outcome === undefined) {
5779
6928
  process.exit(EXIT.USAGE);
5780
6929
  return;
5781
6930
  }
6931
+ if (args.ci && !args.base && gitTreeMatchesHead(platformDir)) {
6932
+ console.error(CI_BASE_WARNING);
6933
+ }
5782
6934
  const diagnostics2 = [...outcome.diagnostics];
5783
6935
  if (outcome.proofsUnconfirmed) {
5784
6936
  if (jsonMode) {
@@ -5790,14 +6942,14 @@ var init_check2 = __esm(() => {
5790
6942
  console.log(renderDiagnostics(diagnostics2, jsonMode ? "json" : "text"));
5791
6943
  if (!jsonMode)
5792
6944
  console.log(`build_id: ${outcome.buildId}`);
5793
- process.exit(outcome.failing ? EXIT.FAILURE : EXIT.OK);
6945
+ await exit(outcome.failing ? EXIT.FAILURE : EXIT.OK);
5794
6946
  }
5795
6947
  });
5796
6948
  });
5797
6949
 
5798
6950
  // src/operations/domain.ts
5799
- import { existsSync as existsSync12, mkdirSync as mkdirSync4 } from "fs";
5800
- import { dirname as dirname6, join as join12, resolve as resolve9 } from "path";
6951
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5 } from "fs";
6952
+ import { dirname as dirname8, join as join14, resolve as resolve9 } from "path";
5801
6953
  function definedFields(options) {
5802
6954
  const out = {};
5803
6955
  if (options.scope !== undefined)
@@ -5817,12 +6969,12 @@ async function newDomain(platformDir, key, options = {}) {
5817
6969
  if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
5818
6970
  return fail("usage", `refusing to write outside platformDir (${resolvedDest})`);
5819
6971
  }
5820
- if (existsSync12(dest))
6972
+ if (existsSync13(dest))
5821
6973
  return fail("conflict", `refusing to overwrite ${dest}`);
5822
- if (existsSync12(join12(dirname6(dest), "SPEC.md"))) {
6974
+ if (existsSync13(join14(dirname8(dest), "SPEC.md"))) {
5823
6975
  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)");
5824
6976
  }
5825
- mkdirSync4(dirname6(dest), { recursive: true });
6977
+ mkdirSync5(dirname8(dest), { recursive: true });
5826
6978
  const envelope = { ...scaffoldDomainObject(key, localToday()), ...definedFields(options) };
5827
6979
  const res = await validateAndWrite(dest, envelope, relFile);
5828
6980
  if (!res.ok) {
@@ -6040,12 +7192,12 @@ function sortMatrix(rows) {
6040
7192
  return a.repo.localeCompare(b.repo);
6041
7193
  });
6042
7194
  }
6043
- function cellStatus(row2) {
6044
- if (row2.implemented === 1 && row2.verified === 1)
7195
+ function cellStatus(row) {
7196
+ if (row.implemented === 1 && row.verified === 1)
6045
7197
  return "src+test";
6046
- if (row2.implemented === 1)
7198
+ if (row.implemented === 1)
6047
7199
  return "src";
6048
- if (row2.verified === 1)
7200
+ if (row.verified === 1)
6049
7201
  return "test";
6050
7202
  return EMPTY_CELL;
6051
7203
  }
@@ -6058,18 +7210,18 @@ function renderMatrix(rows, mode) {
6058
7210
  return "";
6059
7211
  const repos = Array.from(new Set(sorted.map((r) => r.repo))).sort((a, b) => a.localeCompare(b));
6060
7212
  const byReq = new Map;
6061
- for (const row2 of sorted) {
6062
- let entry = byReq.get(row2.req_id);
7213
+ for (const row of sorted) {
7214
+ let entry = byReq.get(row.req_id);
6063
7215
  if (!entry) {
6064
7216
  entry = {
6065
- domain_key: row2.domain_key,
6066
- req_id: row2.req_id,
6067
- req_status: row2.req_status,
7217
+ domain_key: row.domain_key,
7218
+ req_id: row.req_id,
7219
+ req_status: row.req_status,
6068
7220
  cells: new Map
6069
7221
  };
6070
- byReq.set(row2.req_id, entry);
7222
+ byReq.set(row.req_id, entry);
6071
7223
  }
6072
- entry.cells.set(row2.repo, cellStatus(row2));
7224
+ entry.cells.set(row.repo, cellStatus(row));
6073
7225
  }
6074
7226
  const header = ["DOMAIN", "REQUIREMENT", "STATUS", ...repos];
6075
7227
  const lines = [header];
@@ -6077,8 +7229,8 @@ function renderMatrix(rows, mode) {
6077
7229
  const cells = repos.map((r) => entry.cells.get(r) ?? EMPTY_CELL);
6078
7230
  lines.push([entry.domain_key, entry.req_id, entry.req_status, ...cells]);
6079
7231
  }
6080
- const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6081
- return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
7232
+ const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
7233
+ return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6082
7234
  `);
6083
7235
  }
6084
7236
  var EMPTY_CELL = "\u2014";
@@ -6214,12 +7366,19 @@ var init_map = __esm(() => {
6214
7366
 
6215
7367
  // src/operations/records.ts
6216
7368
  function getRecord(storage2, id) {
6217
- const row2 = storage2.getRequirement(id);
6218
- return {
6219
- ok: true,
6220
- row: row2,
6221
- platformEmpty: row2 === null && storage2.listRequirements().length === 0
6222
- };
7369
+ const req = storage2.getRequirement(id);
7370
+ if (req === null) {
7371
+ return { ok: true, row: null, platformEmpty: storage2.listRequirements().length === 0 };
7372
+ }
7373
+ const implemented_in = [];
7374
+ const verified_in = [];
7375
+ for (const { repo, file, line, kind, level } of storage2.listTags({ req_id: id })) {
7376
+ if (kind === "verifies")
7377
+ verified_in.push({ repo, file, line, level: level ?? null });
7378
+ else
7379
+ implemented_in.push({ repo, file, line, kind });
7380
+ }
7381
+ return { ok: true, row: { ...req, implemented_in, verified_in }, platformEmpty: false };
6223
7382
  }
6224
7383
  function listRecords(storage2, filter = {}) {
6225
7384
  const rows = storage2.listRequirements(filter);
@@ -6255,8 +7414,18 @@ function renderRecords(rows, mode) {
6255
7414
  lines.push([r.id, r.status, r.key, String(r.seq), String(r.changed_at_version), r.text]);
6256
7415
  }
6257
7416
  const fixed = HEADER.length - 1;
6258
- const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6259
- return lines.map((row2) => row2.map((cell, i) => i < fixed ? cell.padEnd(widths[i] ?? 0) : cell).join(" ").trimEnd()).join(`
7417
+ const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
7418
+ return lines.map((row) => row.map((cell, i) => i < fixed ? cell.padEnd(widths[i] ?? 0) : cell).join(" ").trimEnd()).join(`
7419
+ `);
7420
+ }
7421
+ function renderRecord(record) {
7422
+ const sites = (label, list) => list.length === 0 ? [`${label}: none`] : [`${label}:`, ...list.map((s) => ` ${s.file}:${s.line}`)];
7423
+ return [
7424
+ renderRecords([record], "text"),
7425
+ "",
7426
+ ...sites("implemented in", record.implemented_in),
7427
+ ...sites("verified in", record.verified_in)
7428
+ ].join(`
6260
7429
  `);
6261
7430
  }
6262
7431
  var HEADER;
@@ -6283,7 +7452,7 @@ var init_get = __esm(() => {
6283
7452
  getCommand = defineCommand6({
6284
7453
  meta: {
6285
7454
  name: "get",
6286
- description: "Print one requirement's full record (id, status, key, seq, versions, statement) from the derived index."
7455
+ description: "Print one requirement's full record (id, status, key, seq, versions, statement) and the file:line of every tag that implements or verifies it, from the derived index."
6287
7456
  },
6288
7457
  args: {
6289
7458
  reqId: {
@@ -6320,7 +7489,7 @@ var init_get = __esm(() => {
6320
7489
  console.log("[]");
6321
7490
  return;
6322
7491
  }
6323
- console.log(args.json ? JSON.stringify(result.row) : renderRecords([result.row], "text"));
7492
+ console.log(args.json ? JSON.stringify(result.row) : renderRecord(result.row));
6324
7493
  });
6325
7494
  }
6326
7495
  });
@@ -6408,11 +7577,11 @@ function renderPropagation(rows, mode) {
6408
7577
  return "";
6409
7578
  const header = ["REPO", "STATE", "VIA", "DRIFT?"];
6410
7579
  const lines = [header];
6411
- for (const row2 of rows) {
6412
- lines.push([row2.repo, row2.state, row2.via_req_id ?? EMPTY_CELL2, row2.drifted ? "yes" : "no"]);
7580
+ for (const row of rows) {
7581
+ lines.push([row.repo, row.state, row.via_req_id ?? EMPTY_CELL2, row.drifted ? "yes" : "no"]);
6413
7582
  }
6414
- const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6415
- return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
7583
+ const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
7584
+ return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6416
7585
  `);
6417
7586
  }
6418
7587
  var EMPTY_CELL2 = "\u2014";
@@ -6486,8 +7655,8 @@ function excerptOf(raw) {
6486
7655
  }
6487
7656
  function renderTable(header, rows) {
6488
7657
  const lines = [header, ...rows];
6489
- const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6490
- return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
7658
+ const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
7659
+ return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6491
7660
  `);
6492
7661
  }
6493
7662
  function renderQuery(hits, mode) {
@@ -6742,17 +7911,17 @@ function compareReqIds2(a, b) {
6742
7911
  function sortProvenance(rows) {
6743
7912
  return [...rows].sort((a, b) => compareReqIds2(a.req_id, b.req_id) || a.role.localeCompare(b.role) || a.issue_id.localeCompare(b.issue_id) || a.source_file.localeCompare(b.source_file) || a.line - b.line);
6744
7913
  }
6745
- function backingTests(row2) {
7914
+ function backingTests(row) {
6746
7915
  let base;
6747
- if (row2.implemented === 1 && row2.verified === 1)
7916
+ if (row.implemented === 1 && row.verified === 1)
6748
7917
  base = "src+test";
6749
- else if (row2.implemented === 1)
7918
+ else if (row.implemented === 1)
6750
7919
  base = "src";
6751
- else if (row2.verified === 1)
7920
+ else if (row.verified === 1)
6752
7921
  base = "test";
6753
7922
  else
6754
7923
  base = "\u2014";
6755
- return row2.test_levels ? `${base} (${row2.test_levels})` : base;
7924
+ return row.test_levels ? `${base} (${row.test_levels})` : base;
6756
7925
  }
6757
7926
  function renderProvenance(rows, mode) {
6758
7927
  const sorted = sortProvenance(rows);
@@ -6762,12 +7931,12 @@ function renderProvenance(rows, mode) {
6762
7931
  if (sorted.length === 0)
6763
7932
  return "";
6764
7933
  const byReq = new Map;
6765
- for (const row2 of sorted) {
6766
- const bucket = byReq.get(row2.req_id);
7934
+ for (const row of sorted) {
7935
+ const bucket = byReq.get(row.req_id);
6767
7936
  if (bucket)
6768
- bucket.push(row2);
7937
+ bucket.push(row);
6769
7938
  else
6770
- byReq.set(row2.req_id, [row2]);
7939
+ byReq.set(row.req_id, [row]);
6771
7940
  }
6772
7941
  const lines = [];
6773
7942
  for (const [reqId, links] of byReq) {
@@ -6782,8 +7951,8 @@ function renderProvenance(rows, mode) {
6782
7951
  return lines.join(`
6783
7952
  `);
6784
7953
  }
6785
- function decorateRow(row2, resolved) {
6786
- const prefix = ` ${row2.role} ${row2.issue_id} ${row2.source_file}:${row2.line}`;
7954
+ function decorateRow(row, resolved) {
7955
+ const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
6787
7956
  if (resolved?.ok && resolved.title && resolved.url) {
6788
7957
  const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
6789
7958
  return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
@@ -6807,12 +7976,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
6807
7976
  if (sorted.length === 0)
6808
7977
  return "";
6809
7978
  const byReq = new Map;
6810
- for (const row2 of sorted) {
6811
- const bucket = byReq.get(row2.req_id);
7979
+ for (const row of sorted) {
7980
+ const bucket = byReq.get(row.req_id);
6812
7981
  if (bucket)
6813
- bucket.push(row2);
7982
+ bucket.push(row);
6814
7983
  else
6815
- byReq.set(row2.req_id, [row2]);
7984
+ byReq.set(row.req_id, [row]);
6816
7985
  }
6817
7986
  const lines = [];
6818
7987
  for (const [reqId, links] of byReq) {
@@ -6833,8 +8002,8 @@ var TOKEN_HINT = "set SPEC_TRACKER_TOKEN to resolve issue titles";
6833
8002
  var init_adapter = () => {};
6834
8003
 
6835
8004
  // ../tracker/src/cache.ts
6836
- import { mkdirSync as mkdirSync5 } from "fs";
6837
- import { dirname as dirname7, join as join13 } from "path";
8005
+ import { mkdirSync as mkdirSync6 } from "fs";
8006
+ import { dirname as dirname9, join as join15 } from "path";
6838
8007
  function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
6839
8008
  const t = Date.parse(entry.fetched_at);
6840
8009
  if (Number.isNaN(t))
@@ -6848,7 +8017,7 @@ function isValidEntry(e) {
6848
8017
  return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
6849
8018
  }
6850
8019
  function sidecarPath(platformDir) {
6851
- return join13(platformDir, ".spec-engine", SIDECAR_FILE);
8020
+ return join15(platformDir, ".spec-engine", SIDECAR_FILE);
6852
8021
  }
6853
8022
  async function readCache(platformDir) {
6854
8023
  try {
@@ -6872,7 +8041,7 @@ async function readCache(platformDir) {
6872
8041
  async function writeCache(platformDir, cache) {
6873
8042
  try {
6874
8043
  const path = sidecarPath(platformDir);
6875
- mkdirSync5(dirname7(path), { recursive: true });
8044
+ mkdirSync6(dirname9(path), { recursive: true });
6876
8045
  await Bun.write(path, `${JSON.stringify(cache, null, 2)}
6877
8046
  `);
6878
8047
  return true;
@@ -7178,8 +8347,6 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
7178
8347
  await mintAndReport(platformDir, key, {
7179
8348
  statement,
7180
8349
  why: (args.why ?? "").trim(),
7181
- livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
7182
- verifiedIn: listFlag(rawArgs, "tests", args.tests) ?? [],
7183
8350
  relates: listFlag(rawArgs, "relates", args.relates) ?? [],
7184
8351
  issue: (args.issue ?? "").trim() || undefined,
7185
8352
  status: statusOf(args),
@@ -7199,11 +8366,9 @@ async function mintInteractively(platformDir, key, id, status) {
7199
8366
  return;
7200
8367
  }
7201
8368
  const why = (await askLine("Why it matters: ")).trim();
7202
- const livesIn = listFlag([], "lives", await askLine("Lives in: ")) ?? [];
7203
8369
  await mintAndReport(platformDir, key, {
7204
8370
  statement: requirement,
7205
8371
  why,
7206
- livesIn,
7207
8372
  issue: undefined,
7208
8373
  status,
7209
8374
  json: false
@@ -7258,8 +8423,6 @@ var init_req = __esm(() => {
7258
8423
  description: "Non-interactive authoring: the Requirement field. When set, the entry appends with zero prompts."
7259
8424
  },
7260
8425
  why: { type: "string", description: "Why it matters field (only with --text)" },
7261
- lives: livesArg,
7262
- tests: testsArg,
7263
8426
  relates: relatesArg,
7264
8427
  issue: {
7265
8428
  type: "string",
@@ -7293,9 +8456,9 @@ var init_req = __esm(() => {
7293
8456
  await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
7294
8457
  return;
7295
8458
  }
7296
- const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.tests === "string" || typeof args.relates === "string" || typeof args.issue === "string";
8459
+ const hasFieldFlags = typeof args.why === "string" || typeof args.relates === "string" || typeof args.issue === "string";
7297
8460
  if (hasFieldFlags) {
7298
- console.error("spec req: --why/--lives/--tests/--relates/--issue require --text (the Requirement field is mandatory)");
8461
+ console.error("spec req: --why/--relates/--issue require --text (the Requirement field is mandatory)");
7299
8462
  process.exit(EXIT.USAGE);
7300
8463
  return;
7301
8464
  }
@@ -7383,8 +8546,8 @@ async function listTermsCommand(platformDirRaw, json) {
7383
8546
  console.log(JSON.stringify(result.rows));
7384
8547
  return;
7385
8548
  }
7386
- for (const row2 of result.rows) {
7387
- console.log(`${row2.id} ${row2.term} ${row2.status}`);
8549
+ for (const row of result.rows) {
8550
+ console.log(`${row.id} ${row.term} ${row.status}`);
7388
8551
  }
7389
8552
  }
7390
8553
  async function reviseTermCommand(opts) {
@@ -7431,8 +8594,8 @@ async function confirmTermCommand(opts) {
7431
8594
  exitOnFailure("spec term confirm", result);
7432
8595
  await withIndex({ platformDir, build: "fresh" }, () => {
7433
8596
  return;
7434
- }).catch((e) => {
7435
- handleInvalidDomainFiles(e, false, EXIT.USAGE);
8597
+ }).catch(async (e) => {
8598
+ await handleInvalidDomainFiles(e, false, EXIT.USAGE);
7436
8599
  throw e;
7437
8600
  });
7438
8601
  const { pinned, file } = result;
@@ -7615,7 +8778,7 @@ function resolvePlatform2(platformDirRaw) {
7615
8778
  }
7616
8779
  return platformDir;
7617
8780
  }
7618
- async function migrate(platformDir, json) {
8781
+ async function migrate2(platformDir, json) {
7619
8782
  const result = await migrateGlossary(platformDir);
7620
8783
  if (!result.ok)
7621
8784
  exitOnFailure("spec glossary", result);
@@ -7639,7 +8802,7 @@ async function generate(platformDir, json) {
7639
8802
  else
7640
8803
  console.log(`generated GLOSSARY.md from ${result.generated} terms`);
7641
8804
  }
7642
- function check2(platformDir, json) {
8805
+ async function check2(platformDir, json) {
7643
8806
  if (checkGlossary(platformDir).clean) {
7644
8807
  if (json)
7645
8808
  console.log(JSON.stringify({ ok: true }));
@@ -7651,13 +8814,14 @@ function check2(platformDir, json) {
7651
8814
  console.error("run `spec glossary .` to regenerate the human view from the store");
7652
8815
  if (json)
7653
8816
  console.log(JSON.stringify({ ok: false }));
7654
- process.exit(EXIT.FAILURE);
8817
+ await exit(EXIT.FAILURE);
7655
8818
  }
7656
8819
  var glossaryCommand;
7657
8820
  var init_glossary2 = __esm(() => {
7658
8821
  init_constants();
7659
8822
  init_discover();
7660
8823
  init_glossary();
8824
+ init_stdout();
7661
8825
  init__args();
7662
8826
  init__shared();
7663
8827
  glossaryCommand = defineCommand14({
@@ -7681,15 +8845,69 @@ var init_glossary2 = __esm(() => {
7681
8845
  const platformDir = resolvePlatform2(args.platformDir);
7682
8846
  const json = Boolean(args.json);
7683
8847
  if (args.migrate)
7684
- await migrate(platformDir, json);
8848
+ await migrate2(platformDir, json);
7685
8849
  else if (args.check)
7686
- check2(platformDir, json);
8850
+ await check2(platformDir, json);
7687
8851
  else
7688
8852
  await generate(platformDir, json);
7689
8853
  }
7690
8854
  });
7691
8855
  });
7692
8856
 
8857
+ // src/operations/_retag.ts
8858
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
8859
+ import { join as join16 } from "path";
8860
+ function retagLine(line, oldId, newId, doc) {
8861
+ const cr = line.endsWith("\r") ? "\r" : "";
8862
+ const body = cr ? line.slice(0, -1) : line;
8863
+ const re = doc ? DOC_BINDING_RE : SPEC_TAG_RE;
8864
+ if (newId !== null) {
8865
+ return body.replace(re, (m, id) => id === oldId ? m.replace(id, newId) : m) + cr;
8866
+ }
8867
+ const stripped = body.replace(new RegExp(`\\s*(?:${re.source})`, "g"), (m, id, level) => {
8868
+ if (id !== oldId)
8869
+ return m;
8870
+ return typeof level === "string" && !LEVELS.has(level) ? m.slice(m.search(TRAILING_WORD)) : "";
8871
+ });
8872
+ if (stripped === body)
8873
+ return line;
8874
+ const rest = stripped.replace(EMPTY_COMMENT_TAIL, "");
8875
+ const residue = rest.trim();
8876
+ return residue === "" || residue === "*" ? null : rest + cr;
8877
+ }
8878
+ async function applyRetag(platformDir, oldId, newId, freshTags) {
8879
+ const sites = toReqTagRows(await freshTags(oldId));
8880
+ const byFile = new Map;
8881
+ for (const site of sites) {
8882
+ const entry = byFile.get(site.file) ?? { doc: site.kind === "documents", lines: new Set };
8883
+ entry.lines.add(site.line);
8884
+ byFile.set(site.file, entry);
8885
+ }
8886
+ for (const [file, { doc, lines }] of byFile) {
8887
+ const abs = join16(platformDir, file);
8888
+ const out = [];
8889
+ readFileSync5(abs, "utf8").split(`
8890
+ `).forEach((line, i) => {
8891
+ const next = lines.has(i + 1) ? retagLine(line, oldId, newId, doc) : line;
8892
+ if (next !== null)
8893
+ out.push(next);
8894
+ });
8895
+ writeFileSync2(abs, out.join(`
8896
+ `));
8897
+ }
8898
+ if (sites.length > 0)
8899
+ await freshTags(oldId);
8900
+ return sites;
8901
+ }
8902
+ var EMPTY_COMMENT_TAIL, TRAILING_WORD;
8903
+ var init__retag = __esm(() => {
8904
+ init_docs();
8905
+ init_tags();
8906
+ init_reads2();
8907
+ EMPTY_COMMENT_TAIL = /\s*(?:\/\/|#|--|\/\*+\s*\*+\/|<!--\s*-->)\s*$/;
8908
+ TRAILING_WORD = /\s+[a-z][a-z0-9]*$/;
8909
+ });
8910
+
7693
8911
  // src/operations/supersede.ts
7694
8912
  async function supersedeTarget(platformDir, id) {
7695
8913
  const located = await locateEntry(platformDir, id);
@@ -7708,8 +8926,6 @@ async function supersedeTarget(platformDir, id) {
7708
8926
  function successorFields(input, req) {
7709
8927
  return {
7710
8928
  why: (input.why ?? req.why ?? "").trim(),
7711
- livesIn: input.livesIn ?? [...req.livesIn],
7712
- verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
7713
8929
  relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
7714
8930
  };
7715
8931
  }
@@ -7754,15 +8970,17 @@ async function supersede(input, freshTags) {
7754
8970
  }
7755
8971
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
7756
8972
  }
7757
- const { why, livesIn, verifiedIn, relates } = successorFields(input, req);
7758
- warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn, ...verifiedIn]));
8973
+ const rules = statementRulesVerdict(platformDir, key, input.statement);
8974
+ if (rules.refusal !== null)
8975
+ return fail("usage", rules.refusal);
8976
+ warnings.push(...rules.warnings);
8977
+ const { why, relates } = successorFields(input, req);
8978
+ warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why]));
7759
8979
  const newId = await nextRequirementId(platformDir, key);
7760
8980
  const issue = input.issue?.trim() || undefined;
7761
8981
  const successor = successorEntry(newId, req, {
7762
8982
  statement: input.statement,
7763
8983
  why,
7764
- livesIn,
7765
- verifiedIn,
7766
8984
  relates,
7767
8985
  issue
7768
8986
  });
@@ -7779,18 +8997,19 @@ async function supersede(input, freshTags) {
7779
8997
  warnings
7780
8998
  });
7781
8999
  }
7782
- const retag = toReqTagRows(await freshTags(id));
7783
- return { ok: true, oldId: id, newId, file: relFile, specVersion, retag, warnings };
9000
+ const retagged = await applyRetag(platformDir, id, newId, freshTags);
9001
+ return { ok: true, oldId: id, newId, file: relFile, specVersion, retagged, warnings };
7784
9002
  }
7785
9003
  var init_supersede = __esm(() => {
7786
9004
  init_src();
7787
9005
  init_domains();
7788
9006
  init_grammar2();
9007
+ init_statementRules3();
7789
9008
  init_domainJson();
7790
9009
  init__envelope();
7791
9010
  init__result();
9011
+ init__retag();
7792
9012
  init_mint();
7793
- init_reads2();
7794
9013
  });
7795
9014
 
7796
9015
  // src/resolve/format.ts
@@ -7806,14 +9025,14 @@ function renderResolve(rows, mode) {
7806
9025
  return "";
7807
9026
  const header = ["REQ_ID", "STATUS", "TEXT"];
7808
9027
  const lines = [header];
7809
- for (const row2 of sorted) {
7810
- const raw = row2.text ?? "";
9028
+ for (const row of sorted) {
9029
+ const raw = row.text ?? "";
7811
9030
  const truncated = raw.length > TEXT_MAX ? raw.slice(0, TEXT_MAX) + ELLIPSIS2 : raw;
7812
9031
  const text = truncated.replace(/\s+/g, " ").trim();
7813
- lines.push([row2.id, row2.status, text || EMPTY_CELL4]);
9032
+ lines.push([row.id, row.status, text || EMPTY_CELL4]);
7814
9033
  }
7815
- const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
7816
- return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
9034
+ const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
9035
+ return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
7817
9036
  `);
7818
9037
  }
7819
9038
  function sortReqTags(rows) {
@@ -7828,8 +9047,8 @@ function renderReqTags(rows, mode) {
7828
9047
  return "";
7829
9048
  const header = ["REPO", "FILE", "LINE", "KIND", "LEVEL"];
7830
9049
  const lines = [header];
7831
- for (const row2 of sorted) {
7832
- lines.push([row2.repo, row2.file, String(row2.line), row2.kind, row2.level ?? EMPTY_CELL4]);
9050
+ for (const row of sorted) {
9051
+ lines.push([row.repo, row.file, String(row.line), row.kind, row.level ?? EMPTY_CELL4]);
7833
9052
  }
7834
9053
  const widths = header.map((_, col) => Math.max(...lines.map((l) => (l[col] ?? "").length)));
7835
9054
  return lines.map((l) => l.map((cell, col) => cell.padEnd(widths[col])).join(" ").trimEnd()).join(`
@@ -7873,12 +9092,6 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
7873
9092
  const input = { platformDir, id, statement, noBump: Boolean(args.noBump) };
7874
9093
  if (typeof args.why === "string")
7875
9094
  input.why = args.why;
7876
- const livesIn = listFlag(rawArgs, "lives", args.lives);
7877
- if (livesIn !== undefined)
7878
- input.livesIn = livesIn;
7879
- const verifiedIn = listFlag(rawArgs, "tests", args.tests);
7880
- if (verifiedIn !== undefined)
7881
- input.verifiedIn = verifiedIn;
7882
9095
  const relates = listFlag(rawArgs, "relates", args.relates);
7883
9096
  if (relates !== undefined)
7884
9097
  input.relates = relates;
@@ -7915,14 +9128,6 @@ var init_supersede2 = __esm(() => {
7915
9128
  type: "string",
7916
9129
  description: "Successor's Why it matters (default: copied from the old entry)"
7917
9130
  },
7918
- lives: {
7919
- type: "string",
7920
- description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
7921
- },
7922
- tests: {
7923
- type: "string",
7924
- description: "Successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the old entry)"
7925
- },
7926
9131
  relates: {
7927
9132
  type: "string",
7928
9133
  description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
@@ -7945,13 +9150,13 @@ var init_supersede2 = __esm(() => {
7945
9150
  },
7946
9151
  json: {
7947
9152
  type: "boolean",
7948
- description: "Emit { old_id, new_id, file, spec_version, retag } as JSON instead of the text summary"
9153
+ description: "Emit { old_id, new_id, file, spec_version, retagged } as JSON instead of the text summary"
7949
9154
  }
7950
9155
  };
7951
9156
  supersedeCommand = defineCommand15({
7952
9157
  meta: {
7953
9158
  name: "supersede",
7954
- description: "Supersede a shipped requirement: flip it to superseded, mint the successor, bump specVersion, and emit the retag worklist."
9159
+ description: "Supersede a shipped requirement: flip it to superseded, mint the successor, bump specVersion, and retag every site of it to the successor."
7955
9160
  },
7956
9161
  args: supersedeArgs,
7957
9162
  async run({ args, rawArgs }) {
@@ -7971,21 +9176,18 @@ var init_supersede2 = __esm(() => {
7971
9176
  if (!result.ok)
7972
9177
  exitOnFailure("spec supersede", result);
7973
9178
  printWarnings("spec supersede", result.warnings);
7974
- const { newId, file, specVersion, retag } = result;
9179
+ const { newId, file, specVersion, retagged } = result;
7975
9180
  if (args.json) {
7976
- console.log(JSON.stringify({ old_id: id, new_id: newId, file, spec_version: specVersion, retag }));
9181
+ console.log(JSON.stringify({ old_id: id, new_id: newId, file, spec_version: specVersion, retagged }));
7977
9182
  } else {
7978
9183
  console.log(`superseded ${id} \u2192 ${newId} in ${file}`);
7979
9184
  if (specVersion !== null)
7980
9185
  console.log(`specVersion bumped to ${specVersion}`);
7981
- if (retag.length > 0) {
7982
- console.log(`retag ${retag.length} site(s) from ${id} to ${newId}:`);
7983
- console.log(renderReqTags(retag, "text"));
9186
+ if (retagged.length > 0) {
9187
+ console.log(`retagged ${retagged.length} site(s) from ${id} to ${newId}:`);
9188
+ console.log(renderReqTags(retagged, "text"));
7984
9189
  }
7985
9190
  }
7986
- if (retag.length > 0) {
7987
- console.error(`note: spec check will report SUPERSEDED_REFERENCED at each remaining ${id} tag until the sites are retagged to ${newId}`);
7988
- }
7989
9191
  }
7990
9192
  });
7991
9193
  });
@@ -8014,14 +9216,14 @@ async function deprecate(input, freshTags) {
8014
9216
  diagnostics: res.diagnostics
8015
9217
  });
8016
9218
  }
8017
- const sites = toReqTagRows(await freshTags(id));
8018
- return { ok: true, id, file: relFile, reason, sites };
9219
+ const removed = await applyRetag(platformDir, id, null, freshTags);
9220
+ return { ok: true, id, file: relFile, reason, removed };
8019
9221
  }
8020
9222
  var init_deprecate = __esm(() => {
8021
9223
  init_src();
8022
9224
  init__envelope();
8023
9225
  init__result();
8024
- init_reads2();
9226
+ init__retag();
8025
9227
  });
8026
9228
 
8027
9229
  // src/commands/deprecate.ts
@@ -8056,7 +9258,7 @@ var init_deprecate2 = __esm(() => {
8056
9258
  },
8057
9259
  json: {
8058
9260
  type: "boolean",
8059
- description: "Print { id, file, reason, sites } instead of text"
9261
+ description: "Print { id, file, reason, removed } instead of text"
8060
9262
  }
8061
9263
  },
8062
9264
  async run({ args }) {
@@ -8079,15 +9281,15 @@ var init_deprecate2 = __esm(() => {
8079
9281
  const result = await deprecate({ platformDir, id, reason }, cliFreshTags(platformDir));
8080
9282
  if (!result.ok)
8081
9283
  exitOnFailure("spec deprecate", result);
8082
- const { file, sites } = result;
9284
+ const { file, removed } = result;
8083
9285
  if (args.json) {
8084
- console.log(JSON.stringify({ id, file, reason, sites }));
9286
+ console.log(JSON.stringify({ id, file, reason, removed }));
8085
9287
  return;
8086
9288
  }
8087
9289
  console.log(`deprecated ${id} in ${file} \u2014 reason recorded`);
8088
- if (sites.length > 0) {
8089
- console.error(`${sites.length} code tag(s) still bind ${id} \u2014 remove or retag them (spec check reports each as DEPRECATED_REFERENCED):`);
8090
- console.error(renderReqTags(sites, "text"));
9290
+ if (removed.length > 0) {
9291
+ console.log(`removed ${removed.length} tag(s) of ${id}:`);
9292
+ console.log(renderReqTags(removed, "text"));
8091
9293
  }
8092
9294
  }
8093
9295
  });
@@ -8115,8 +9317,6 @@ function successorFields2(input, req) {
8115
9317
  return {
8116
9318
  statement: (input.statement ?? req.statement).trim(),
8117
9319
  why: (input.why ?? req.why ?? "").trim(),
8118
- livesIn: input.livesIn ?? [...req.livesIn],
8119
- verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
8120
9320
  relates: [...req.relates]
8121
9321
  };
8122
9322
  }
@@ -8160,13 +9360,12 @@ async function move(input, freshTags) {
8160
9360
  }
8161
9361
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
8162
9362
  }
9363
+ const rules = statementRulesVerdict(platformDir, targetKey, fields.statement);
9364
+ if (rules.refusal !== null)
9365
+ return fail("usage", rules.refusal);
9366
+ warnings.push(...rules.warnings);
8163
9367
  }
8164
- warnings.push(...unresolvableRefWarnings(platformDir, [
8165
- fields.statement,
8166
- fields.why,
8167
- ...fields.livesIn,
8168
- ...fields.verifiedIn
8169
- ]));
9368
+ warnings.push(...unresolvableRefWarnings(platformDir, [fields.statement, fields.why]));
8170
9369
  const newId = await nextRequirementId(platformDir, targetKey);
8171
9370
  const sourceCurrent = source.domain.specVersion ?? 1;
8172
9371
  req.status = "superseded";
@@ -8186,7 +9385,7 @@ async function move(input, freshTags) {
8186
9385
  }
8187
9386
  await validateAndWrite(target.specPath, target.domain, target.relFile);
8188
9387
  await validateAndWrite(source.specPath, source.domain, source.relFile);
8189
- const retag = toReqTagRows(await freshTags(id));
9388
+ const retagged = await applyRetag(platformDir, id, newId, freshTags);
8190
9389
  return {
8191
9390
  ok: true,
8192
9391
  oldId: id,
@@ -8195,7 +9394,7 @@ async function move(input, freshTags) {
8195
9394
  toFile: target.relFile,
8196
9395
  sourceSpecVersion,
8197
9396
  targetSpecVersion,
8198
- retag,
9397
+ retagged,
8199
9398
  warnings
8200
9399
  };
8201
9400
  }
@@ -8203,11 +9402,12 @@ var init_move = __esm(() => {
8203
9402
  init_src();
8204
9403
  init_domains();
8205
9404
  init_grammar2();
9405
+ init_statementRules3();
8206
9406
  init_domainJson();
8207
9407
  init__envelope();
8208
9408
  init__result();
9409
+ init__retag();
8209
9410
  init_mint();
8210
- init_reads2();
8211
9411
  });
8212
9412
 
8213
9413
  // src/commands/move.ts
@@ -8216,18 +9416,12 @@ __export(exports_move, {
8216
9416
  moveCommand: () => moveCommand
8217
9417
  });
8218
9418
  import { defineCommand as defineCommand17 } from "citty";
8219
- function inputFromArgs2(args, rawArgs, platformDir, id, targetKey) {
9419
+ function inputFromArgs2(args, platformDir, id, targetKey) {
8220
9420
  const input = { platformDir, id, targetKey, noBump: Boolean(args.noBump) };
8221
9421
  if (typeof args.text === "string")
8222
9422
  input.statement = args.text;
8223
9423
  if (typeof args.why === "string")
8224
9424
  input.why = args.why;
8225
- const livesIn = listFlag(rawArgs, "lives", args.lives);
8226
- if (livesIn !== undefined)
8227
- input.livesIn = livesIn;
8228
- const verifiedIn = listFlag(rawArgs, "tests", args.tests);
8229
- if (verifiedIn !== undefined)
8230
- input.verifiedIn = verifiedIn;
8231
9425
  return input;
8232
9426
  }
8233
9427
  var moveArgs, moveCommand;
@@ -8259,30 +9453,22 @@ var init_move2 = __esm(() => {
8259
9453
  type: "string",
8260
9454
  description: "Rewrite the successor's Why (default: copied from the source)"
8261
9455
  },
8262
- lives: {
8263
- type: "string",
8264
- description: "Rewrite the successor's Lives in: repeat or comma-separate; empty clears (default: copied from the source)"
8265
- },
8266
- tests: {
8267
- type: "string",
8268
- description: "Rewrite the successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the source)"
8269
- },
8270
9456
  noBump: {
8271
9457
  type: "boolean",
8272
9458
  description: "Do not bump either envelope's specVersion"
8273
9459
  },
8274
9460
  json: {
8275
9461
  type: "boolean",
8276
- description: "Emit { old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retag } as JSON"
9462
+ description: "Emit { old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged } as JSON"
8277
9463
  }
8278
9464
  };
8279
9465
  moveCommand = defineCommand17({
8280
9466
  meta: {
8281
9467
  name: "move",
8282
- description: "Move a requirement to another domain: mint the successor in <NEW-DOMAIN> carrying the source's fields, mark the source superseded, bump both specVersions, and emit the retag worklist."
9468
+ description: "Move a requirement to another domain: mint the successor in <NEW-DOMAIN> carrying the source's fields, mark the source superseded, bump both specVersions, and retag every site of it to the successor."
8283
9469
  },
8284
9470
  args: moveArgs,
8285
- async run({ args, rawArgs }) {
9471
+ async run({ args }) {
8286
9472
  const id = args.id;
8287
9473
  const rawTargetKey = args.newDomain;
8288
9474
  const platformDir = resolvePlatformDir(args);
@@ -8300,11 +9486,11 @@ var init_move2 = __esm(() => {
8300
9486
  } catch (e) {
8301
9487
  handleNotAPlatform(e);
8302
9488
  }
8303
- const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey), cliFreshTags(platformDir));
9489
+ const result = await move(inputFromArgs2(args, platformDir, id, targetKey), cliFreshTags(platformDir));
8304
9490
  if (!result.ok)
8305
9491
  exitOnFailure("spec move", result);
8306
9492
  printWarnings("spec move", result.warnings);
8307
- const { newId, fromFile, toFile, sourceSpecVersion, targetSpecVersion, retag } = result;
9493
+ const { newId, fromFile, toFile, sourceSpecVersion, targetSpecVersion, retagged } = result;
8308
9494
  const sourceKey = id.slice(0, id.indexOf("-"));
8309
9495
  if (args.json) {
8310
9496
  console.log(JSON.stringify({
@@ -8314,21 +9500,18 @@ var init_move2 = __esm(() => {
8314
9500
  to_file: toFile,
8315
9501
  source_spec_version: sourceSpecVersion,
8316
9502
  target_spec_version: targetSpecVersion,
8317
- retag
9503
+ retagged
8318
9504
  }));
8319
9505
  } else {
8320
9506
  console.log(`moved ${id} \u2192 ${newId} (${sourceKey} \u2192 ${targetKey})`);
8321
9507
  if (sourceSpecVersion !== null || targetSpecVersion !== null) {
8322
9508
  console.log(`specVersion: ${sourceKey}\u2192${sourceSpecVersion}, ${targetKey}\u2192${targetSpecVersion}`);
8323
9509
  }
8324
- if (retag.length > 0) {
8325
- console.log(`retag ${retag.length} site(s) from ${id} to ${newId}:`);
8326
- console.log(renderReqTags(retag, "text"));
9510
+ if (retagged.length > 0) {
9511
+ console.log(`retagged ${retagged.length} site(s) from ${id} to ${newId}:`);
9512
+ console.log(renderReqTags(retagged, "text"));
8327
9513
  }
8328
9514
  }
8329
- if (retag.length > 0) {
8330
- console.error(`note: spec check will report SUPERSEDED_REFERENCED at each remaining ${id} tag until the sites are retagged to ${newId}`);
8331
- }
8332
9515
  }
8333
9516
  });
8334
9517
  });
@@ -8357,7 +9540,7 @@ function gateRejection(platformDir, relFile, req, fields) {
8357
9540
  const tag = releaseHolding(platformDir, relFile, id);
8358
9541
  if (tag === null)
8359
9542
  return null;
8360
- 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, --tests, or --relates to amend its metadata in place.");
9543
+ 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 --relates or --issue to amend its metadata in place.");
8361
9544
  }
8362
9545
  function applyFields(req, fields) {
8363
9546
  const fieldsChanged = [];
@@ -8372,16 +9555,6 @@ function applyFields(req, fields) {
8372
9555
  fieldsChanged.push("why");
8373
9556
  refValues.push(fields.why ?? "");
8374
9557
  }
8375
- if (fields.livesIn !== undefined) {
8376
- req.livesIn = fields.livesIn;
8377
- fieldsChanged.push("lives");
8378
- refValues.push(...fields.livesIn);
8379
- }
8380
- if (fields.verifiedIn !== undefined) {
8381
- req.verifiedIn = fields.verifiedIn;
8382
- fieldsChanged.push("tests");
8383
- refValues.push(...fields.verifiedIn);
8384
- }
8385
9558
  if (fields.relates !== undefined) {
8386
9559
  req.relates = splitList(fields.relates);
8387
9560
  fieldsChanged.push("relates");
@@ -8419,6 +9592,10 @@ async function amend(input) {
8419
9592
  }
8420
9593
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
8421
9594
  }
9595
+ const rules = statementRulesVerdict(platformDir, key, fields.statement);
9596
+ if (rules.refusal !== null)
9597
+ return fail("usage", rules.refusal);
9598
+ warnings.push(...rules.warnings);
8422
9599
  }
8423
9600
  const { fieldsChanged, refValues } = applyFields(req, fields);
8424
9601
  warnings.push(...unresolvableRefWarnings(platformDir, refValues));
@@ -8437,6 +9614,7 @@ var PROMISE_FIELDS;
8437
9614
  var init_amend = __esm(() => {
8438
9615
  init_src();
8439
9616
  init_grammar2();
9617
+ init_statementRules3();
8440
9618
  init__envelope();
8441
9619
  init__result();
8442
9620
  init_mint();
@@ -8451,9 +9629,9 @@ __export(exports_amend, {
8451
9629
  import { defineCommand as defineCommand18 } from "citty";
8452
9630
  function assertAmendFlags(args) {
8453
9631
  const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
8454
- const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.tests !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
9632
+ const hasField = args.text !== undefined || args.why !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
8455
9633
  if (!hasField && !hasIssue) {
8456
- console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --tests / --relates / --term / --aliases");
9634
+ console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --relates / --term / --aliases");
8457
9635
  process.exit(EXIT.USAGE);
8458
9636
  }
8459
9637
  if (args.text !== undefined && args.text.trim() === "") {
@@ -8477,12 +9655,6 @@ function fieldsFromArgs(args, rawArgs) {
8477
9655
  const v = args.why.trim();
8478
9656
  fields.why = v === "" ? null : v;
8479
9657
  }
8480
- const livesIn = listFlag(rawArgs, "lives", args.lives);
8481
- if (livesIn !== undefined)
8482
- fields.livesIn = livesIn;
8483
- const verifiedIn = listFlag(rawArgs, "tests", args.tests);
8484
- if (verifiedIn !== undefined)
8485
- fields.verifiedIn = verifiedIn;
8486
9658
  const relates = listFlag(rawArgs, "relates", args.relates);
8487
9659
  if (relates !== undefined)
8488
9660
  fields.relates = relates;
@@ -8511,8 +9683,6 @@ var init_amend2 = __esm(() => {
8511
9683
  platformDir: platformDirArg,
8512
9684
  text: { type: "string", description: "New Requirement (statement) field value" },
8513
9685
  why: { type: "string", description: "New Why it matters field value" },
8514
- lives: livesArg,
8515
- tests: testsArg,
8516
9686
  relates: relatesArg,
8517
9687
  issue: {
8518
9688
  type: "string",
@@ -8528,7 +9698,7 @@ var init_amend2 = __esm(() => {
8528
9698
  amendCommand = defineCommand18({
8529
9699
  meta: {
8530
9700
  name: "amend",
8531
- description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --lives, --tests, and --relates still amend."
9701
+ description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --relates and --issue still amend."
8532
9702
  },
8533
9703
  args: amendArgs,
8534
9704
  async run({ args, rawArgs }) {
@@ -8568,6 +9738,9 @@ async function accept(input) {
8568
9738
  if (req.status.toLowerCase() !== "draft") {
8569
9739
  return fail("conflict", `${id} is ${displayStatus(req.status)} \u2014 only a Draft entry accepts`);
8570
9740
  }
9741
+ const rules = statementRulesVerdict(platformDir, located.key, req.statement);
9742
+ if (rules.refusal !== null)
9743
+ return fail("usage", rules.refusal);
8571
9744
  req.status = "active";
8572
9745
  domain2.updated = localToday();
8573
9746
  const res = await validateAndWrite(specPath, domain2, relFile);
@@ -8577,10 +9750,11 @@ async function accept(input) {
8577
9750
  diagnostics: res.diagnostics
8578
9751
  });
8579
9752
  }
8580
- return { ok: true, id, file: relFile };
9753
+ return { ok: true, id, file: relFile, warnings: rules.warnings };
8581
9754
  }
8582
9755
  var init_accept = __esm(() => {
8583
9756
  init_src();
9757
+ init_statementRules3();
8584
9758
  init__envelope();
8585
9759
  init__result();
8586
9760
  });
@@ -8629,6 +9803,7 @@ var init_accept2 = __esm(() => {
8629
9803
  const result = await accept({ platformDir, id });
8630
9804
  if (!result.ok)
8631
9805
  exitOnFailure("spec accept", result);
9806
+ printWarnings("spec accept", result.warnings);
8632
9807
  if (args.json) {
8633
9808
  console.log(JSON.stringify({ id, file: result.file }));
8634
9809
  return;
@@ -8638,6 +9813,158 @@ var init_accept2 = __esm(() => {
8638
9813
  });
8639
9814
  });
8640
9815
 
9816
+ // src/operations/lint.ts
9817
+ import { existsSync as existsSync14 } from "fs";
9818
+ async function lint2(input) {
9819
+ const { platformDir, target } = input;
9820
+ const config2 = readCheckConfig(platformDir);
9821
+ if (!config2.ok)
9822
+ return fail("usage", config2.detail);
9823
+ const statuses = new Set(input.all ? ["active", "draft"] : ["active"]);
9824
+ let keys;
9825
+ let id;
9826
+ if (target !== undefined && ID_RE.test(target)) {
9827
+ const located = await locateEntry(platformDir, target);
9828
+ if (!located.ok)
9829
+ return located;
9830
+ keys = [located.key];
9831
+ id = target;
9832
+ } else if (target !== undefined && KEY_RE.test(target)) {
9833
+ const { abs, rel } = specPaths(platformDir, target);
9834
+ if (!existsSync14(abs))
9835
+ return fail("not_found", `no domain ${target} (expected ${rel})`);
9836
+ keys = [target];
9837
+ } else if (target !== undefined) {
9838
+ return fail("usage", `${target} is neither a domain key nor a requirement id`);
9839
+ }
9840
+ const findings = await statementRuleRows(platformDir, config2.config.statementRules, {
9841
+ keys,
9842
+ id,
9843
+ statuses
9844
+ });
9845
+ return { ok: true, findings };
9846
+ }
9847
+ var init_lint = __esm(() => {
9848
+ init_src();
9849
+ init_severity();
9850
+ init_statementRules2();
9851
+ init_constants();
9852
+ init__envelope();
9853
+ init__result();
9854
+ });
9855
+
9856
+ // src/commands/lint.ts
9857
+ var exports_lint = {};
9858
+ __export(exports_lint, {
9859
+ lintCommand: () => lintCommand
9860
+ });
9861
+ import { existsSync as existsSync15 } from "fs";
9862
+ import { join as join17, resolve as resolve13 } from "path";
9863
+ import { defineCommand as defineCommand20 } from "citty";
9864
+ function splitPositionals(args) {
9865
+ const { target, platformDir } = args;
9866
+ if (target !== undefined && platformDir === undefined && existsSync15(join17(resolve13(target), CANONICAL_SPECS_DIR))) {
9867
+ return { target: undefined, platformDir: resolve13(target) };
9868
+ }
9869
+ return { target, platformDir: resolvePlatformDir({ platformDir }) };
9870
+ }
9871
+ var lintCommand;
9872
+ var init_lint2 = __esm(() => {
9873
+ init_src();
9874
+ init_constants();
9875
+ init_discover();
9876
+ init_lint();
9877
+ init_stdout();
9878
+ init__args();
9879
+ init__shared();
9880
+ lintCommand = defineCommand20({
9881
+ meta: {
9882
+ name: "lint",
9883
+ description: "Print every statement-rule finding for the platform, one domain (KEY), or one requirement (KEY-NNN); exit 1 on any"
9884
+ },
9885
+ args: {
9886
+ target: {
9887
+ type: "positional",
9888
+ required: false,
9889
+ description: "Domain key or requirement id (default: the whole platform)"
9890
+ },
9891
+ platformDir: platformDirArg,
9892
+ all: { type: "boolean", description: "Judge Draft requirements as well as Active ones" },
9893
+ json: jsonArg
9894
+ },
9895
+ async run({ args }) {
9896
+ const { target, platformDir } = splitPositionals(args);
9897
+ try {
9898
+ assertSpecPlatform(platformDir);
9899
+ } catch (e) {
9900
+ handleNotAPlatform(e);
9901
+ }
9902
+ const result = await lint2({ platformDir, target, all: !!args.all });
9903
+ if (!result.ok)
9904
+ exitOnFailure("spec lint", result);
9905
+ if (args.json) {
9906
+ console.log(JSON.stringify(result.findings.map(({ req_id, rule, detail, reason, source_file, line }) => ({
9907
+ req_id,
9908
+ rule,
9909
+ detail,
9910
+ reason,
9911
+ source_file,
9912
+ line
9913
+ }))));
9914
+ } else {
9915
+ for (const f of result.findings)
9916
+ console.log(`${f.req_id} ${formatStatementFinding(f)}`);
9917
+ if (result.findings.length > 0)
9918
+ console.error(RULE_ZERO);
9919
+ }
9920
+ await exit(result.findings.length > 0 ? EXIT.FAILURE : EXIT.OK);
9921
+ }
9922
+ });
9923
+ });
9924
+
9925
+ // src/commands/migrate.ts
9926
+ var exports_migrate = {};
9927
+ __export(exports_migrate, {
9928
+ migrateCommand: () => migrateCommand
9929
+ });
9930
+ import { defineCommand as defineCommand21 } from "citty";
9931
+ var migrateCommand;
9932
+ var init_migrate2 = __esm(() => {
9933
+ init_discover();
9934
+ init_migrate();
9935
+ init__args();
9936
+ init__shared();
9937
+ migrateCommand = defineCommand21({
9938
+ meta: {
9939
+ name: "migrate",
9940
+ description: "Remove the retired livesIn and verifiedIn fields from every SPEC.json, rewriting only the files that carry one"
9941
+ },
9942
+ args: {
9943
+ platformDir: platformDirArg,
9944
+ json: jsonArg
9945
+ },
9946
+ async run({ args }) {
9947
+ const platformDir = resolvePlatformDir(args);
9948
+ try {
9949
+ assertSpecPlatform(platformDir);
9950
+ } catch (e) {
9951
+ handleNotAPlatform(e);
9952
+ }
9953
+ const result = await migrate(platformDir);
9954
+ if (!result.ok)
9955
+ exitOnFailure("spec migrate", result);
9956
+ if (args.json) {
9957
+ console.log(JSON.stringify({ files: result.files }));
9958
+ return;
9959
+ }
9960
+ if (result.files.length === 0)
9961
+ console.error("spec migrate: nothing to migrate");
9962
+ for (const f of result.files)
9963
+ console.log(`migrated ${f}`);
9964
+ }
9965
+ });
9966
+ });
9967
+
8641
9968
  // src/server/authorPrompt.ts
8642
9969
  function renderAuthorPrompt(opts) {
8643
9970
  const charterBlock = opts.charter ? `
@@ -8647,6 +9974,8 @@ Domain charter (scope): ${opts.charter}
8647
9974
  Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement carefully)
8648
9975
  ` : "\nNo target domain given \u2014 determine placement from `spec domain list`.\n";
8649
9976
  return [
9977
+ RULE_ZERO,
9978
+ "",
8650
9979
  "You are authoring Spec Engine requirements from a brief. Follow this playbook exactly.",
8651
9980
  charterBlock,
8652
9981
  "Brief:",
@@ -8656,14 +9985,17 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
8656
9985
  "1. One requirement per behavior \u2014 never one per ticket; a brief fans out into several.",
8657
9986
  "2. Placement: the domain whose promise the requirement protects (see the charter above); run `spec query <text>` first \u2014 if a close requirement exists, amend or relate it instead of minting.",
8658
9987
  '3. Draft each statement in one of the five EARS shapes ("shall" separates who acts from the observable result): `The <system> shall <result>` / `When <trigger>, the <system> shall <result>` / `While <state>, \u2026` / `If <failure>, then \u2026` / `Where <feature>, \u2026`. Example: "When an unknown id is passed, spec resolve shall print [] and exit 0."',
8659
- "4. Self-check: the statement makes sense read alone; it describes what the system does, not how the code does it; no expiration dates ('currently', removed things); the why names what breaks if the promise stops holding \u2014 never a restatement.",
9988
+ "4. Self-check: the statement is one sentence of at most 280 characters that names no repository file or folder, test mechanics, CI or tooling layout, history, project jargon, or the tool in use (spec req refuses it otherwise; `spec lint` lists the findings); it makes sense read alone; it describes what the system does, not how the code does it; no expiration dates ('currently', removed things); the why names what breaks if the promise stops holding \u2014 never a restatement.",
8660
9989
  "5. Tie each draft to its source: quote the exact phrase of the brief it derives from; label anything you inferred as inferred.",
8661
9990
  "6. Present the full batch (statement, why, placement, source quote) for approval BEFORE writing.",
8662
- '7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026" --lives "\u2026"`.',
9991
+ '7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026"`.',
8663
9992
  "8. The originating ticket is provenance only \u2014 never a requirement id, never a code tag."
8664
9993
  ].join(`
8665
9994
  `);
8666
9995
  }
9996
+ var init_authorPrompt = __esm(() => {
9997
+ init_src();
9998
+ });
8667
9999
 
8668
10000
  // src/server/mcp.ts
8669
10001
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -8680,10 +10012,6 @@ function amendFieldsOf(args) {
8680
10012
  fields.statement = args.text;
8681
10013
  if (args.why !== undefined)
8682
10014
  fields.why = args.why || null;
8683
- if (args.lives !== undefined)
8684
- fields.livesIn = args.lives;
8685
- if (args.verified_in !== undefined)
8686
- fields.verifiedIn = args.verified_in;
8687
10015
  if (args.relates !== undefined)
8688
10016
  fields.relates = args.relates;
8689
10017
  if (args.issue)
@@ -8713,7 +10041,7 @@ function buildMcpServer(platformDir) {
8713
10041
  }, async ({ files }) => fresh((s) => jsonResult(resolveFiles(s, files).rows)));
8714
10042
  server.registerTool("spec_req_tags", {
8715
10043
  title: "Requirement \u2192 tag sites (reverse query)",
8716
- description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis and retag worklists.",
10044
+ description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis.",
8717
10045
  inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
8718
10046
  }, async ({ req_id }) => {
8719
10047
  if (!ID_RE.test(req_id)) {
@@ -8754,15 +10082,15 @@ function buildMcpServer(platformDir) {
8754
10082
  });
8755
10083
  server.registerTool("spec_get", {
8756
10084
  title: "One requirement's full record",
8757
- 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).",
10085
+ description: "The full record for one requirement id: status, key, seq, statement, why, versions, source location, and the implemented_in and verified_in tag sites. An unknown id returns [] (the `spec get --json` shape).",
8758
10086
  inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
8759
10087
  }, async ({ req_id }) => {
8760
10088
  if (!ID_RE.test(req_id)) {
8761
10089
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8762
10090
  }
8763
10091
  return fresh((s) => {
8764
- const { row: row2 } = getRecord(s, req_id);
8765
- return jsonResult(row2 ?? []);
10092
+ const { row } = getRecord(s, req_id);
10093
+ return jsonResult(row ?? []);
8766
10094
  });
8767
10095
  });
8768
10096
  server.registerTool("spec_list", {
@@ -8787,17 +10115,15 @@ function buildMcpServer(platformDir) {
8787
10115
  });
8788
10116
  server.registerTool("spec_supersede", {
8789
10117
  title: "Supersede a shipped requirement",
8790
- 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`.",
10118
+ description: "Flip an Active requirement to superseded, mint its successor in the same domain, rewrite every tag of the predecessor to the successor, and return the rewritten sites. The same JSON as `spec supersede --json`.",
8791
10119
  inputSchema: {
8792
10120
  req_id: z4.string().describe("The Active requirement id to supersede (KEY-NNN)"),
8793
10121
  statement: z4.string().min(1).describe("The successor's statement"),
8794
10122
  why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
8795
- lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
8796
- verified_in: z4.array(z4.string()).optional().describe("Successor's verifiedIn paths (default: copied from the predecessor)"),
8797
10123
  relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
8798
10124
  issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
8799
10125
  }
8800
- }, async ({ req_id, statement, why, lives_in, verified_in, relates, issue }) => {
10126
+ }, async ({ req_id, statement, why, relates, issue }) => {
8801
10127
  if (!ID_RE.test(req_id)) {
8802
10128
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8803
10129
  }
@@ -8806,8 +10132,6 @@ function buildMcpServer(platformDir) {
8806
10132
  id: req_id,
8807
10133
  statement,
8808
10134
  why,
8809
- livesIn: lives_in,
8810
- verifiedIn: verified_in,
8811
10135
  relates,
8812
10136
  issue
8813
10137
  }, coldFreshTags(platformDir));
@@ -8820,12 +10144,12 @@ function buildMcpServer(platformDir) {
8820
10144
  new_id: r.newId,
8821
10145
  file: r.file,
8822
10146
  spec_version: r.specVersion,
8823
- retag: r.retag
10147
+ retagged: r.retagged
8824
10148
  });
8825
10149
  });
8826
10150
  server.registerTool("spec_deprecate", {
8827
10151
  title: "Deprecate a requirement",
8828
- 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`.",
10152
+ description: "Mark an Active or Draft requirement deprecated with a recorded reason, remove every tag of it from the code, and return the removed sites. The same JSON as `spec deprecate --json`.",
8829
10153
  inputSchema: {
8830
10154
  req_id: z4.string().describe("The requirement id to deprecate (KEY-NNN)"),
8831
10155
  reason: z4.string().min(1).describe("Why the requirement ended; the durable record")
@@ -8838,7 +10162,7 @@ function buildMcpServer(platformDir) {
8838
10162
  if (trimmed === "")
8839
10163
  return errorResult("reason must be non-empty");
8840
10164
  const r = await deprecate({ platformDir, id: req_id, reason: trimmed }, coldFreshTags(platformDir));
8841
- return r.ok ? jsonResult({ id: r.id, file: r.file, reason: r.reason, sites: r.sites }) : errorResult(r.detail);
10165
+ return r.ok ? jsonResult({ id: r.id, file: r.file, reason: r.reason, removed: r.removed }) : errorResult(r.detail);
8842
10166
  });
8843
10167
  server.registerTool("spec_accept", {
8844
10168
  title: "Accept a draft requirement",
@@ -8851,7 +10175,11 @@ function buildMcpServer(platformDir) {
8851
10175
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8852
10176
  }
8853
10177
  const r = await accept({ platformDir, id: req_id });
8854
- return r.ok ? jsonResult({ id: r.id, file: r.file }) : errorResult(r.detail);
10178
+ if (!r.ok)
10179
+ return errorResult(r.detail);
10180
+ for (const w of r.warnings)
10181
+ console.error(`spec_accept: ${w.text}`);
10182
+ return jsonResult({ id: r.id, file: r.file });
8855
10183
  });
8856
10184
  server.registerTool("spec_req", {
8857
10185
  title: "Author a requirement",
@@ -8860,13 +10188,11 @@ function buildMcpServer(platformDir) {
8860
10188
  domain: z4.string().min(1).describe("Domain key or unique case-insensitive prefix"),
8861
10189
  text: z4.string().trim().min(1).describe("The requirement statement"),
8862
10190
  why: z4.string().trim().default("").describe("What breaks without it"),
8863
- lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
8864
- verified_in: z4.array(z4.string()).default([]).describe("verifiedIn test paths"),
8865
10191
  relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
8866
10192
  issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
8867
10193
  draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
8868
10194
  }
8869
- }, async ({ domain: domain2, text, why, lives, verified_in, relates, issue, draft }) => {
10195
+ }, async ({ domain: domain2, text, why, relates, issue, draft }) => {
8870
10196
  const allocated = await nextId(platformDir, domain2);
8871
10197
  if (!allocated.ok)
8872
10198
  return errorResult(allocated.detail);
@@ -8875,8 +10201,6 @@ function buildMcpServer(platformDir) {
8875
10201
  key: allocated.key,
8876
10202
  statement: text,
8877
10203
  why,
8878
- livesIn: lives,
8879
- verifiedIn: verified_in,
8880
10204
  relates,
8881
10205
  issue: issue || undefined,
8882
10206
  status: draft ? "draft" : "active"
@@ -8894,17 +10218,15 @@ function buildMcpServer(platformDir) {
8894
10218
  id: z4.string().describe("The requirement id to amend (KEY-NNN)"),
8895
10219
  text: z4.string().trim().min(1).optional().describe("New statement"),
8896
10220
  why: z4.string().trim().optional().describe("New why; empty clears it"),
8897
- lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
8898
- verified_in: z4.array(z4.string()).optional().describe("Replacement verifiedIn list; empty clears"),
8899
10221
  relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
8900
10222
  issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
8901
10223
  }
8902
- }, async ({ id, text, why, lives, verified_in, relates, issue }) => {
10224
+ }, async ({ id, text, why, relates, issue }) => {
8903
10225
  if (!ID_RE.test(id))
8904
10226
  return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8905
- const fields = amendFieldsOf({ text, why, lives, verified_in, relates, issue });
10227
+ const fields = amendFieldsOf({ text, why, relates, issue });
8906
10228
  if (Object.keys(fields).length === 0) {
8907
- return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, verified_in, relates");
10229
+ return errorResult("nothing to amend \u2014 pass at least one of text, why, relates, issue");
8908
10230
  }
8909
10231
  const r = await amend({ platformDir, id, fields });
8910
10232
  if (!r.ok)
@@ -8915,16 +10237,14 @@ function buildMcpServer(platformDir) {
8915
10237
  });
8916
10238
  server.registerTool("spec_move", {
8917
10239
  title: "Move a requirement to another domain",
8918
- 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`.",
10240
+ description: "Mint the successor in newDomain carrying the source's fields, mark the source superseded, rewrite every tag of the source to the successor, and return the rewritten sites. The same JSON as `spec move --json`.",
8919
10241
  inputSchema: {
8920
10242
  id: z4.string().describe("The Active requirement id to move (KEY-NNN)"),
8921
10243
  newDomain: z4.string().min(1).describe("The existing target domain key"),
8922
10244
  text: z4.string().optional().describe("Successor statement (default: copied)"),
8923
- why: z4.string().optional().describe("Successor why (default: copied)"),
8924
- lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)"),
8925
- verified_in: z4.array(z4.string()).optional().describe("Successor verifiedIn list; empty clears (default: copied)")
10245
+ why: z4.string().optional().describe("Successor why (default: copied)")
8926
10246
  }
8927
- }, async ({ id, newDomain: newDomain2, text, why, lives, verified_in }) => {
10247
+ }, async ({ id, newDomain: newDomain2, text, why }) => {
8928
10248
  if (!ID_RE.test(id))
8929
10249
  return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8930
10250
  const targetKey = normalizeDomainKey(newDomain2);
@@ -8935,9 +10255,7 @@ function buildMcpServer(platformDir) {
8935
10255
  id,
8936
10256
  targetKey,
8937
10257
  statement: text,
8938
- why,
8939
- livesIn: lives,
8940
- verifiedIn: verified_in
10258
+ why
8941
10259
  }, coldFreshTags(platformDir));
8942
10260
  if (!r.ok)
8943
10261
  return errorResult(r.detail);
@@ -8950,7 +10268,7 @@ function buildMcpServer(platformDir) {
8950
10268
  to_file: r.toFile,
8951
10269
  source_spec_version: r.sourceSpecVersion,
8952
10270
  target_spec_version: r.targetSpecVersion,
8953
- retag: r.retag
10271
+ retagged: r.retagged
8954
10272
  });
8955
10273
  });
8956
10274
  server.registerPrompt("author_requirements", {
@@ -8999,6 +10317,7 @@ var init_mcp = __esm(() => {
8999
10317
  init_records();
9000
10318
  init_supersede();
9001
10319
  init_grammar3();
10320
+ init_authorPrompt();
9002
10321
  });
9003
10322
 
9004
10323
  // src/commands/mcp.ts
@@ -9007,14 +10326,14 @@ __export(exports_mcp, {
9007
10326
  mcpCommand: () => mcpCommand
9008
10327
  });
9009
10328
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9010
- import { defineCommand as defineCommand20 } from "citty";
10329
+ import { defineCommand as defineCommand22 } from "citty";
9011
10330
  var mcpCommand;
9012
10331
  var init_mcp2 = __esm(() => {
9013
10332
  init_discover();
9014
10333
  init_mcp();
9015
10334
  init__args();
9016
10335
  init__shared();
9017
- mcpCommand = defineCommand20({
10336
+ mcpCommand = defineCommand22({
9018
10337
  meta: {
9019
10338
  name: "mcp",
9020
10339
  description: "Serve Spec Engine as an MCP (Model Context Protocol) server over stdio \u2014 agent-native query/resolve/check/report tools."
@@ -9042,8 +10361,8 @@ __export(exports_resolve, {
9042
10361
  resolveCommand: () => resolveCommand
9043
10362
  });
9044
10363
  import { statSync as statSync5 } from "fs";
9045
- import { join as join14, relative as relative3, resolve as resolve13, sep as sep4 } from "path";
9046
- import { defineCommand as defineCommand21 } from "citty";
10364
+ import { join as join18, relative as relative4, resolve as resolve14, sep as sep4 } from "path";
10365
+ import { defineCommand as defineCommand23 } from "citty";
9047
10366
  function classifyArg(a) {
9048
10367
  if (a.startsWith("--")) {
9049
10368
  const eq = a.indexOf("=");
@@ -9078,10 +10397,10 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
9078
10397
  let lastIsPlatformDir = false;
9079
10398
  let lastIsDirWithoutSpecEngine = false;
9080
10399
  try {
9081
- const resolved = resolve13(last);
10400
+ const resolved = resolve14(last);
9082
10401
  if (statSync5(resolved).isDirectory()) {
9083
10402
  try {
9084
- if (statSync5(join14(resolved, "spec-engine")).isDirectory()) {
10403
+ if (statSync5(join18(resolved, "spec-engine")).isDirectory()) {
9085
10404
  lastIsPlatformDir = true;
9086
10405
  } else {
9087
10406
  lastIsDirWithoutSpecEngine = true;
@@ -9168,7 +10487,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
9168
10487
  }
9169
10488
  await runReverseQuery({
9170
10489
  reqId: reqArg,
9171
- platformDir: resolve13(lone ?? process.cwd()),
10490
+ platformDir: resolve14(lone ?? process.cwd()),
9172
10491
  outArg: args.out,
9173
10492
  json: Boolean(args.json),
9174
10493
  fresh: Boolean(args.fresh),
@@ -9178,7 +10497,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
9178
10497
  function normalizeFileInputs(files, platformDir) {
9179
10498
  const normalized = [];
9180
10499
  for (const input of files) {
9181
- const rel = relative3(platformDir, resolve13(platformDir, input));
10500
+ const rel = relative4(platformDir, resolve14(platformDir, input));
9182
10501
  const segments = rel.split(sep4);
9183
10502
  if (rel === "" || rel === "." || segments[0] === "..") {
9184
10503
  console.error(`spec resolve: file path must be inside platformDir: ${input}`);
@@ -9229,7 +10548,7 @@ var init_resolve2 = __esm(() => {
9229
10548
  description: "Suppress interactive onboarding prompt for siblings missing spec-engine.member.json (defaults to NO_SPEC_CONFIG warning)"
9230
10549
  }
9231
10550
  };
9232
- resolveCommand = defineCommand21({
10551
+ resolveCommand = defineCommand23({
9233
10552
  meta: {
9234
10553
  name: "resolve",
9235
10554
  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."
@@ -9253,7 +10572,7 @@ var init_resolve2 = __esm(() => {
9253
10572
  process.exit(EXIT.USAGE);
9254
10573
  return;
9255
10574
  }
9256
- const platformDir = resolve13(resolved.platformDir ?? process.cwd());
10575
+ const platformDir = resolve14(resolved.platformDir ?? process.cwd());
9257
10576
  const outArg2 = args.out;
9258
10577
  const dbPath = resolveDbPath(platformDir, outArg2);
9259
10578
  if (outArg2)
@@ -9392,10 +10711,10 @@ var exports_gate = {};
9392
10711
  __export(exports_gate, {
9393
10712
  gateCommand: () => gateCommand
9394
10713
  });
9395
- import { existsSync as existsSync13 } from "fs";
9396
- import { join as join15 } from "path";
9397
- import { defineCommand as defineCommand22 } from "citty";
9398
- function isEpipe(e) {
10714
+ import { existsSync as existsSync16 } from "fs";
10715
+ import { join as join19 } from "path";
10716
+ import { defineCommand as defineCommand24 } from "citty";
10717
+ function isEpipe2(e) {
9399
10718
  if (e === null || typeof e !== "object" || !("code" in e))
9400
10719
  return false;
9401
10720
  const code = e.code;
@@ -9405,7 +10724,7 @@ function writeStdoutTolerant(line) {
9405
10724
  try {
9406
10725
  console.log(line);
9407
10726
  } catch (writeErr) {
9408
- if (!isEpipe(writeErr))
10727
+ if (!isEpipe2(writeErr))
9409
10728
  throw writeErr;
9410
10729
  }
9411
10730
  }
@@ -9417,7 +10736,7 @@ function resolveGateArgs(args) {
9417
10736
  process.exit(EXIT.USAGE);
9418
10737
  }
9419
10738
  const platformDir = resolvePlatformDir(args);
9420
- if (!existsSync13(join15(platformDir, "spec-engine"))) {
10739
+ if (!existsSync16(join19(platformDir, "spec-engine"))) {
9421
10740
  console.error(formatNotASpecPlatform(platformDir));
9422
10741
  process.exit(EXIT.USAGE);
9423
10742
  }
@@ -9430,6 +10749,7 @@ var init_gate2 = __esm(() => {
9430
10749
  init_prompt();
9431
10750
  init__index();
9432
10751
  init_gate();
10752
+ init_stdout();
9433
10753
  init__args();
9434
10754
  init__shared();
9435
10755
  gateArgs = {
@@ -9457,7 +10777,7 @@ var init_gate2 = __esm(() => {
9457
10777
  description: "Suppress interactive onboarding prompt for siblings missing spec-engine.member.json (defaults to NO_SPEC_CONFIG warning)"
9458
10778
  }
9459
10779
  };
9460
- gateCommand = defineCommand22({
10780
+ gateCommand = defineCommand24({
9461
10781
  meta: {
9462
10782
  name: "gate",
9463
10783
  description: "Rung-3 approval primitive: passes iff <reqId> is Active and <repo>'s pinned spec_version covers its changed_at_version. Exits 0 PASS / 1 any gate failure / 2 crash or bad args."
@@ -9490,7 +10810,7 @@ var init_gate2 = __esm(() => {
9490
10810
  writeStdoutTolerant(`build_id: ${gated.buildId}`);
9491
10811
  }
9492
10812
  } catch (e) {
9493
- handleInvalidDomainFiles(e, Boolean(args.json));
10813
+ await handleInvalidDomainFiles(e, Boolean(args.json));
9494
10814
  const msg = e instanceof Error ? e.message : String(e);
9495
10815
  console.error(`spec gate: crashed: ${msg}`);
9496
10816
  process.exit(EXIT.USAGE);
@@ -9500,7 +10820,7 @@ var init_gate2 = __esm(() => {
9500
10820
  process.exit(EXIT.USAGE);
9501
10821
  return;
9502
10822
  }
9503
- process.exit(gated.outcome.pass ? EXIT.OK : EXIT.FAILURE);
10823
+ await exit(gated.outcome.pass ? EXIT.OK : EXIT.FAILURE);
9504
10824
  }
9505
10825
  });
9506
10826
  });
@@ -9581,14 +10901,15 @@ var exports_guard = {};
9581
10901
  __export(exports_guard, {
9582
10902
  guardCommand: () => guardCommand
9583
10903
  });
9584
- import { defineCommand as defineCommand23 } from "citty";
10904
+ import { defineCommand as defineCommand25 } from "citty";
9585
10905
  var guardCommand;
9586
10906
  var init_guard2 = __esm(() => {
9587
10907
  init_constants();
9588
10908
  init_guard();
10909
+ init_stdout();
9589
10910
  init__args();
9590
10911
  init__shared();
9591
- guardCommand = defineCommand23({
10912
+ guardCommand = defineCommand25({
9592
10913
  meta: {
9593
10914
  name: "guard",
9594
10915
  description: "Loss detection: diff the requirement derivation at a git ref (default HEAD) against the working tree and block requirements about to be steamrolled."
@@ -9611,14 +10932,14 @@ var init_guard2 = __esm(() => {
9611
10932
  if (!gitRefResolves(platformDir, ref)) {
9612
10933
  console.error(`spec guard: NOT_A_GIT_REPO \u2014 ref '${ref}' does not resolve in ${platformDir} ` + "(not a git repo, a fresh repo with no commit, or an unfetched/misspelled ref); " + "skipping loss detection");
9613
10934
  console.log(jsonMode ? "[]" : `\u2713 spec guard: no requirements about to be lost (non-git context)`);
9614
- process.exit(EXIT.OK);
10935
+ await exit(EXIT.OK);
9615
10936
  }
9616
10937
  let losses = [];
9617
10938
  await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
9618
10939
  losses = (await guard(storage2, platformDir, ref)).losses;
9619
10940
  });
9620
10941
  console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
9621
- process.exit(losses.length > 0 ? EXIT.FAILURE : EXIT.OK);
10942
+ await exit(losses.length > 0 ? EXIT.FAILURE : EXIT.OK);
9622
10943
  }
9623
10944
  });
9624
10945
  });
@@ -11220,9 +12541,9 @@ function requirementStatusBadge(status) {
11220
12541
  const variant = status === "Active" ? "active" : status === "Superseded" ? "superseded" : status === "Draft" ? "draft" : "retired";
11221
12542
  return badge(variant, STATUS_LABEL[status]);
11222
12543
  }
11223
- function propagationStateBadge(row2) {
12544
+ function propagationStateBadge(row) {
11224
12545
  let variant;
11225
- switch (row2.state) {
12546
+ switch (row.state) {
11226
12547
  case "MIGRATED_VERIFIED":
11227
12548
  variant = "verified";
11228
12549
  break;
@@ -11235,7 +12556,7 @@ function propagationStateBadge(row2) {
11235
12556
  default:
11236
12557
  variant = "unbound";
11237
12558
  }
11238
- return badge(variant, row2.state);
12559
+ return badge(variant, row.state);
11239
12560
  }
11240
12561
  function coverageGlyph(impl, verif) {
11241
12562
  const [variant, char, title] = impl && verif ? ["both", "\u2713", "src + test"] : impl ? ["impl", "S", "src only"] : verif ? ["test", "T", "test only"] : ["none", "", "none"];
@@ -11369,18 +12690,18 @@ import { html as html4, raw as raw3 } from "hono/html";
11369
12690
  function pivot(rows) {
11370
12691
  const repos = [...new Set(rows.map((r) => r.repo))].sort();
11371
12692
  const byDomain = new Map;
11372
- for (const row2 of rows) {
11373
- let dom = byDomain.get(row2.domain_key);
12693
+ for (const row of rows) {
12694
+ let dom = byDomain.get(row.domain_key);
11374
12695
  if (!dom) {
11375
12696
  dom = new Map;
11376
- byDomain.set(row2.domain_key, dom);
12697
+ byDomain.set(row.domain_key, dom);
11377
12698
  }
11378
- let req = dom.get(row2.req_id);
12699
+ let req = dom.get(row.req_id);
11379
12700
  if (!req) {
11380
- req = { id: row2.req_id, status: row2.req_status, cells: new Map };
11381
- dom.set(row2.req_id, req);
12701
+ req = { id: row.req_id, status: row.req_status, cells: new Map };
12702
+ dom.set(row.req_id, req);
11382
12703
  }
11383
- req.cells.set(row2.repo, { impl: row2.implemented, verif: row2.verified });
12704
+ req.cells.set(row.repo, { impl: row.implemented, verif: row.verified });
11384
12705
  }
11385
12706
  const domains = [...byDomain.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, reqMap]) => ({
11386
12707
  key,
@@ -11627,14 +12948,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11627
12948
  Why (optional)
11628
12949
  <textarea name="why" rows="2"></textarea>
11629
12950
  </label>
11630
- <label>
11631
- Lives in (optional, single path)
11632
- <input type="text" name="livesIn" placeholder="path/to/impl.ts" />
11633
- </label>
11634
- <label>
11635
- Verified in (optional, single path)
11636
- <input type="text" name="verifiedIn" placeholder="path/to/impl.test.ts" />
11637
- </label>
11638
12951
  <label>
11639
12952
  Relates to (optional, comma-separated ids)
11640
12953
  <input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
@@ -11672,8 +12985,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11672
12985
  const form = await c.req.parseBody();
11673
12986
  const statement = field(form, "statement");
11674
12987
  const why = field(form, "why");
11675
- const livesIn = field(form, "livesIn");
11676
- const verifiedIn = field(form, "verifiedIn");
11677
12988
  const relates = field(form, "relates");
11678
12989
  const payload = {
11679
12990
  key: field(form, "key"),
@@ -11681,10 +12992,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11681
12992
  };
11682
12993
  if (why.length > 0)
11683
12994
  payload.why = why;
11684
- if (livesIn.length > 0)
11685
- payload.livesIn = livesIn;
11686
- if (verifiedIn.length > 0)
11687
- payload.verifiedIn = verifiedIn;
11688
12995
  if (relates.length > 0)
11689
12996
  payload.relates = relates;
11690
12997
  const res = await app.request("/api/requirements", {
@@ -12001,15 +13308,15 @@ var init_relations2 = __esm(() => {
12001
13308
  import { html as html10, raw as raw9 } from "hono/html";
12002
13309
  function coverageRollups(coverage) {
12003
13310
  const cov = new Map;
12004
- for (const row2 of coverage) {
12005
- if (row2.req_status !== "Active")
13311
+ for (const row of coverage) {
13312
+ if (row.req_status !== "Active")
12006
13313
  continue;
12007
- const c = cov.get(row2.req_id) ?? { impl: 0, verif: 0 };
12008
- if (row2.implemented)
13314
+ const c = cov.get(row.req_id) ?? { impl: 0, verif: 0 };
13315
+ if (row.implemented)
12009
13316
  c.impl += 1;
12010
- if (row2.verified)
13317
+ if (row.verified)
12011
13318
  c.verif += 1;
12012
- cov.set(row2.req_id, c);
13319
+ cov.set(row.req_id, c);
12013
13320
  }
12014
13321
  return cov;
12015
13322
  }
@@ -12253,6 +13560,9 @@ function mountRequirements(app) {
12253
13560
  </html>`);
12254
13561
  }
12255
13562
  const r = await res.json();
13563
+ const sites = (list) => list.length === 0 ? "\u2014" : html10`<ul>
13564
+ ${list.map((s) => html10`<li><code>${s.file}:${s.line}</code></li>`)}
13565
+ </ul>`;
12256
13566
  const body = html10`
12257
13567
  ${navBar("requirements")}
12258
13568
  <div class="eyebrow">/ Requirement</div>
@@ -12272,6 +13582,8 @@ function mountRequirements(app) {
12272
13582
  <dt>line</dt><dd>${r.line}</dd>
12273
13583
  <dt>text</dt><dd>${r.text}</dd>
12274
13584
  <dt>why</dt><dd>${r.why ?? "\u2014"}</dd>
13585
+ <dt>implemented_in</dt><dd>${sites(r.implemented_in)}</dd>
13586
+ <dt>verified_in</dt><dd>${sites(r.verified_in)}</dd>
12275
13587
  </dl>
12276
13588
  `;
12277
13589
  return c.html(html10`<!doctype html>
@@ -12356,15 +13668,15 @@ function mountSetup(app) {
12356
13668
  const memberCount = members.length;
12357
13669
  const note = readingNote(mode, memberCount);
12358
13670
  const boundByRepo = new Map;
12359
- for (const row2 of cov) {
12360
- if (!row2.implemented)
13671
+ for (const row of cov) {
13672
+ if (!row.implemented)
12361
13673
  continue;
12362
- let set = boundByRepo.get(row2.repo);
13674
+ let set = boundByRepo.get(row.repo);
12363
13675
  if (!set) {
12364
13676
  set = new Set;
12365
- boundByRepo.set(row2.repo, set);
13677
+ boundByRepo.set(row.repo, set);
12366
13678
  }
12367
- set.add(row2.req_id);
13679
+ set.add(row.req_id);
12368
13680
  }
12369
13681
  const body = html11`
12370
13682
  ${navBar("setup")}
@@ -12601,8 +13913,6 @@ function mintFieldsFromBody(body) {
12601
13913
  return {
12602
13914
  statement: typeof body.statement === "string" ? body.statement : "",
12603
13915
  why: typeof body.why === "string" ? body.why : "",
12604
- livesIn: toStringList(body.livesIn),
12605
- verifiedIn: toStringList(body.verifiedIn),
12606
13916
  relates: toStringList(body.relates),
12607
13917
  issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
12608
13918
  status: body.status === "draft" ? "draft" : "active"
@@ -12611,8 +13921,6 @@ function mintFieldsFromBody(body) {
12611
13921
  function successorFieldsFromBody(body) {
12612
13922
  return {
12613
13923
  why: typeof body.why === "string" ? body.why : undefined,
12614
- livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
12615
- verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined,
12616
13924
  relates: "relates" in body ? toStringList(body.relates) : undefined,
12617
13925
  issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
12618
13926
  };
@@ -12623,10 +13931,6 @@ function amendFieldsFromBody(body) {
12623
13931
  fields.statement = body.statement;
12624
13932
  if ("why" in body)
12625
13933
  fields.why = typeof body.why === "string" ? body.why : null;
12626
- if ("livesIn" in body)
12627
- fields.livesIn = toStringList(body.livesIn);
12628
- if ("verifiedIn" in body)
12629
- fields.verifiedIn = toStringList(body.verifiedIn);
12630
13934
  if ("relates" in body)
12631
13935
  fields.relates = toStringList(body.relates);
12632
13936
  return Object.keys(fields).length === 0 ? null : fields;
@@ -12688,8 +13992,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12688
13992
  }));
12689
13993
  app.get("/api/requirements/:id", guarded((c) => {
12690
13994
  const id = c.req.param("id") ?? "";
12691
- const { row: row2 } = getRecord(storage2, id);
12692
- return row2 ? c.json(row2) : c.json({ error: "not found" }, 404);
13995
+ const { row } = getRecord(storage2, id);
13996
+ return row ? c.json(row) : c.json({ error: "not found" }, 404);
12693
13997
  }));
12694
13998
  app.post("/api/requirements", guarded(async function createRequirement(c) {
12695
13999
  if (!featureEnabled("editor")) {
@@ -12735,7 +14039,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12735
14039
  return withWriteLock(async () => {
12736
14040
  const fields = amendFieldsFromBody(body);
12737
14041
  if (fields === null) {
12738
- return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, verifiedIn, or relates" }, 400);
14042
+ return c.json({ error: "nothing to amend \u2014 provide statement, why, or relates" }, 400);
12739
14043
  }
12740
14044
  const r = await amend({ platformDir, id, fields });
12741
14045
  if (!r.ok)
@@ -12777,7 +14081,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12777
14081
  new_id: r.newId,
12778
14082
  file: r.file,
12779
14083
  spec_version: r.specVersion,
12780
- retag: r.retag
14084
+ retagged: r.retagged
12781
14085
  }, 201);
12782
14086
  });
12783
14087
  }));
@@ -12807,9 +14111,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12807
14111
  id,
12808
14112
  targetKey,
12809
14113
  statement: typeof body.text === "string" ? body.text : undefined,
12810
- why: typeof body.why === "string" ? body.why : undefined,
12811
- livesIn: "lives" in body ? toStringList(body.lives) : undefined,
12812
- verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined
14114
+ why: typeof body.why === "string" ? body.why : undefined
12813
14115
  }, async (reqId) => {
12814
14116
  await runIndex({ platformDir, storage: storage2 });
12815
14117
  return storage2.listTags({ req_id: reqId });
@@ -12824,7 +14126,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12824
14126
  to_file: r.toFile,
12825
14127
  source_spec_version: r.sourceSpecVersion,
12826
14128
  target_spec_version: r.targetSpecVersion,
12827
- retag: r.retag
14129
+ retagged: r.retagged
12828
14130
  }, 201);
12829
14131
  });
12830
14132
  }));
@@ -12855,7 +14157,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12855
14157
  });
12856
14158
  if (!r.ok)
12857
14159
  return failureResponse(c, r);
12858
- return c.json({ ok: true, id: r.id, file: r.file, reason: r.reason, sites: r.sites }, 200);
14160
+ return c.json({ ok: true, id: r.id, file: r.file, reason: r.reason, removed: r.removed }, 200);
12859
14161
  });
12860
14162
  }));
12861
14163
  app.post("/api/requirements/:id/accept", guarded(async function acceptRequirement(c) {
@@ -12901,12 +14203,12 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12901
14203
  if (!featureEnabled("relations")) {
12902
14204
  return c.json({ error: featureDisabledMessage("relations") }, 404);
12903
14205
  }
12904
- const format = c.req.query("format");
12905
- if (format !== undefined && format !== "mermaid") {
14206
+ const format2 = c.req.query("format");
14207
+ if (format2 !== undefined && format2 !== "mermaid") {
12906
14208
  return c.json({ error: 'format must be "mermaid" when provided' }, 400);
12907
14209
  }
12908
14210
  const rows = relations(storage2).rows;
12909
- if (format === "mermaid") {
14211
+ if (format2 === "mermaid") {
12910
14212
  return c.text(renderRelations(rows, "mermaid"));
12911
14213
  }
12912
14214
  return c.json(sortRelations(rows));
@@ -13097,9 +14399,9 @@ __export(exports_serve, {
13097
14399
  resolveBind: () => resolveBind,
13098
14400
  composeServeApp: () => composeServeApp
13099
14401
  });
13100
- import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
13101
- import { dirname as dirname8 } from "path";
13102
- import { defineCommand as defineCommand24 } from "citty";
14402
+ import { existsSync as existsSync17, mkdirSync as mkdirSync7 } from "fs";
14403
+ import { dirname as dirname10 } from "path";
14404
+ import { defineCommand as defineCommand26 } from "citty";
13103
14405
  import { Hono as Hono2 } from "hono";
13104
14406
  function composeServeApp(storage2, platformDir = process.cwd(), bindHost = DEFAULT_BIND_HOST) {
13105
14407
  const app = new Hono2;
@@ -13161,7 +14463,7 @@ async function runProbe() {
13161
14463
  }
13162
14464
  console.log("serve --probe OK");
13163
14465
  server.stop();
13164
- process.exit(EXIT.OK);
14466
+ await exit(EXIT.OK);
13165
14467
  }
13166
14468
  var serveCommand;
13167
14469
  var init_serve = __esm(() => {
@@ -13173,10 +14475,11 @@ var init_serve = __esm(() => {
13173
14475
  init_prompt();
13174
14476
  init_api();
13175
14477
  init_lan();
14478
+ init_stdout();
13176
14479
  init_sqlite();
13177
14480
  init__args();
13178
14481
  init__shared();
13179
- serveCommand = defineCommand24({
14482
+ serveCommand = defineCommand26({
13180
14483
  meta: {
13181
14484
  name: "serve",
13182
14485
  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."
@@ -13248,8 +14551,8 @@ var init_serve = __esm(() => {
13248
14551
  noPrompt: args.noPrompt
13249
14552
  }
13250
14553
  });
13251
- mkdirSync6(dirname8(dbPath), { recursive: true });
13252
- const needsIndex = !existsSync14(dbPath);
14554
+ mkdirSync7(dirname10(dbPath), { recursive: true });
14555
+ const needsIndex = !existsSync17(dbPath);
13253
14556
  let storage2;
13254
14557
  try {
13255
14558
  storage2 = openStorage(dbPath);
@@ -13270,7 +14573,7 @@ var init_serve = __esm(() => {
13270
14573
  });
13271
14574
  } catch (err) {
13272
14575
  storage2.close();
13273
- handleInvalidDomainFiles(err, false);
14576
+ await handleInvalidDomainFiles(err, false);
13274
14577
  handleStorageUnavailable(err, dbPath);
13275
14578
  console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
13276
14579
  process.exit(EXIT.FAILURE);
@@ -13297,13 +14600,13 @@ __export(exports_docs, {
13297
14600
  docsCommand: () => docsCommand,
13298
14601
  createDocsFetchHandler: () => createDocsFetchHandler
13299
14602
  });
13300
- import { existsSync as existsSync15, statSync as statSync6 } from "fs";
13301
- import { join as join16, normalize, sep as sep5 } from "path";
14603
+ import { existsSync as existsSync18, statSync as statSync6 } from "fs";
14604
+ import { join as join20, normalize, sep as sep5 } from "path";
13302
14605
  import { fileURLToPath } from "url";
13303
- import { defineCommand as defineCommand25 } from "citty";
14606
+ import { defineCommand as defineCommand27 } from "citty";
13304
14607
  function resolveDocsRootFrom(candidates) {
13305
14608
  for (const candidate of candidates) {
13306
- if (existsSync15(join16(candidate, "index.html")))
14609
+ if (existsSync18(join20(candidate, "index.html")))
13307
14610
  return candidate;
13308
14611
  }
13309
14612
  return null;
@@ -13327,17 +14630,17 @@ function createDocsFetchHandler(root) {
13327
14630
  if (pathname.includes("\x00")) {
13328
14631
  return new Response("Bad request", { status: 400 });
13329
14632
  }
13330
- const target = normalize(join16(normalRoot, pathname));
14633
+ const target = normalize(join20(normalRoot, pathname));
13331
14634
  if (target !== normalRoot && !target.startsWith(rootPrefix)) {
13332
14635
  return new Response("Not found", { status: 404 });
13333
14636
  }
13334
- for (const candidate of [target, join16(target, "index.html"), `${target}.html`]) {
13335
- if (existsSync15(candidate) && statSync6(candidate).isFile()) {
14637
+ for (const candidate of [target, join20(target, "index.html"), `${target}.html`]) {
14638
+ if (existsSync18(candidate) && statSync6(candidate).isFile()) {
13336
14639
  return new Response(Bun.file(candidate));
13337
14640
  }
13338
14641
  }
13339
- const notFoundPage = join16(normalRoot, "404.html");
13340
- if (existsSync15(notFoundPage)) {
14642
+ const notFoundPage = join20(normalRoot, "404.html");
14643
+ if (existsSync18(notFoundPage)) {
13341
14644
  return new Response(Bun.file(notFoundPage), { status: 404 });
13342
14645
  }
13343
14646
  return new Response("Not found", { status: 404 });
@@ -13369,7 +14672,7 @@ async function runProbe2(root) {
13369
14672
  }
13370
14673
  console.log("docs --probe OK");
13371
14674
  server.stop();
13372
- process.exit(EXIT.OK);
14675
+ await exit(EXIT.OK);
13373
14676
  }
13374
14677
  function exitNoDocsRoot() {
13375
14678
  console.error("spec docs: no built documentation found. From a checkout run `bun run build:site` " + "(writes packages/site/dist); the published npm package ships it at dist/docs.");
@@ -13378,7 +14681,8 @@ function exitNoDocsRoot() {
13378
14681
  var docsCommand;
13379
14682
  var init_docs2 = __esm(() => {
13380
14683
  init_constants();
13381
- docsCommand = defineCommand25({
14684
+ init_stdout();
14685
+ docsCommand = defineCommand27({
13382
14686
  meta: {
13383
14687
  name: "docs",
13384
14688
  description: "Serve the bundled documentation site offline. Binds 127.0.0.1 on --port (loopback only \u2014 no --host). --probe is the boot-and-fetch smoke."
@@ -13439,10 +14743,10 @@ __export(exports___smoke, {
13439
14743
  schemaSmokeCommand: () => schemaSmokeCommand,
13440
14744
  schemaMismatchSmokeCommand: () => schemaMismatchSmokeCommand
13441
14745
  });
13442
- import { defineCommand as defineCommand26 } from "citty";
14746
+ import { defineCommand as defineCommand28 } from "citty";
13443
14747
  var schemaSmokeCommand, schemaMismatchSmokeCommand;
13444
14748
  var init___smoke = __esm(() => {
13445
- schemaSmokeCommand = defineCommand26({
14749
+ schemaSmokeCommand = defineCommand28({
13446
14750
  meta: {
13447
14751
  name: "__schema-smoke",
13448
14752
  description: "(hidden CI) open a fresh DB, exec DDL, assert the schema shape",
@@ -13485,7 +14789,7 @@ var init___smoke = __esm(() => {
13485
14789
  console.log("__schema-smoke OK");
13486
14790
  }
13487
14791
  });
13488
- schemaMismatchSmokeCommand = defineCommand26({
14792
+ schemaMismatchSmokeCommand = defineCommand28({
13489
14793
  meta: {
13490
14794
  name: "__schema-mismatch-smoke",
13491
14795
  description: "(hidden CI) corrupt _schema_version, re-open, assert silent rebuild",
@@ -13511,8 +14815,10 @@ var init___smoke = __esm(() => {
13511
14815
 
13512
14816
  // src/cli.ts
13513
14817
  init_package();
13514
- import { defineCommand as defineCommand27, runMain, showUsage } from "citty";
13515
- var main = defineCommand27({
14818
+ init_stdout();
14819
+ import { defineCommand as defineCommand29, runMain, showUsage } from "citty";
14820
+ routeStdout();
14821
+ var main = defineCommand29({
13516
14822
  meta: {
13517
14823
  name: "spec",
13518
14824
  version: package_default.version,
@@ -13538,6 +14844,8 @@ var main = defineCommand27({
13538
14844
  move: () => Promise.resolve().then(() => (init_move2(), exports_move)).then((m) => m.moveCommand),
13539
14845
  amend: () => Promise.resolve().then(() => (init_amend2(), exports_amend)).then((m) => m.amendCommand),
13540
14846
  accept: () => Promise.resolve().then(() => (init_accept2(), exports_accept)).then((m) => m.acceptCommand),
14847
+ lint: () => Promise.resolve().then(() => (init_lint2(), exports_lint)).then((m) => m.lintCommand),
14848
+ migrate: () => Promise.resolve().then(() => (init_migrate2(), exports_migrate)).then((m) => m.migrateCommand),
13541
14849
  mcp: () => Promise.resolve().then(() => (init_mcp2(), exports_mcp)).then((m) => m.mcpCommand),
13542
14850
  resolve: () => Promise.resolve().then(() => (init_resolve2(), exports_resolve)).then((m) => m.resolveCommand),
13543
14851
  gate: () => Promise.resolve().then(() => (init_gate2(), exports_gate)).then((m) => m.gateCommand),