@spec-engine/spec-engine 0.7.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/docs/agent-reference/index.html +1 -1
  2. package/dist/docs/checks/index.html +11 -1
  3. package/dist/docs/commands/index.html +4 -4
  4. package/dist/docs/format/index.html +5 -1
  5. package/dist/docs/pagefind/fragment/en_24f76b6.pf_fragment +0 -0
  6. package/dist/docs/pagefind/fragment/en_2ee687a.pf_fragment +0 -0
  7. package/dist/docs/pagefind/fragment/en_4bd9de6.pf_fragment +0 -0
  8. package/dist/docs/pagefind/fragment/en_9cc1878.pf_fragment +0 -0
  9. package/dist/docs/pagefind/fragment/en_9cfc306.pf_fragment +0 -0
  10. package/dist/docs/pagefind/fragment/en_c3abf3d.pf_fragment +0 -0
  11. package/dist/docs/pagefind/index/en_59c3abf.pf_index +0 -0
  12. package/dist/docs/pagefind/index/en_a630d23.pf_index +0 -0
  13. package/dist/docs/pagefind/pagefind-entry.json +1 -1
  14. package/dist/docs/pagefind/pagefind.en_ced49c317e.pf_meta +0 -0
  15. package/dist/docs/platform-mapping/index.html +2 -2
  16. package/dist/docs/tags/index.html +47 -1
  17. package/dist/impl.js +408 -221
  18. package/package.json +1 -1
  19. package/dist/docs/pagefind/fragment/en_70e8a6f.pf_fragment +0 -0
  20. package/dist/docs/pagefind/fragment/en_907a67d.pf_fragment +0 -0
  21. package/dist/docs/pagefind/fragment/en_b2867b9.pf_fragment +0 -0
  22. package/dist/docs/pagefind/fragment/en_de9d958.pf_fragment +0 -0
  23. package/dist/docs/pagefind/fragment/en_e616a50.pf_fragment +0 -0
  24. package/dist/docs/pagefind/fragment/en_f77a622.pf_fragment +0 -0
  25. package/dist/docs/pagefind/index/en_19cbfc6.pf_index +0 -0
  26. package/dist/docs/pagefind/index/en_4be71c4.pf_index +0 -0
  27. package/dist/docs/pagefind/pagefind.en_f697eb692e.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.7.3",
23
+ version: "0.8.1",
24
24
  description: "Cross-repo spec engine \u2014 durable requirements with permanent IDs, bound to code via @spec tags, checked in CI. Ships the spec CLI, the local coverage webapp (spec serve), and offline docs (spec docs).",
25
25
  license: "MIT",
26
26
  type: "module",
@@ -73,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(() => {
@@ -91,6 +116,8 @@ var init_diagnostics = __esm(() => {
91
116
  DRIFT: "DRIFT",
92
117
  NO_SPEC_CONFIG: "NO_SPEC_CONFIG",
93
118
  BROKEN_FILE_REF: "BROKEN_FILE_REF",
119
+ UNIMPLEMENTED_IN_FILE: "UNIMPLEMENTED_IN_FILE",
120
+ UNVERIFIED_IN_FILE: "UNVERIFIED_IN_FILE",
94
121
  BROKEN_RELATES: "BROKEN_RELATES",
95
122
  RELATES_SUPERSEDED: "RELATES_SUPERSEDED",
96
123
  CYCLIC_SUPERSEDE: "CYCLIC_SUPERSEDE",
@@ -213,6 +240,7 @@ function orderDomain(d) {
213
240
  supersededBy: r.supersededBy ?? null,
214
241
  relates: r.relates,
215
242
  livesIn: r.livesIn,
243
+ verifiedIn: r.verifiedIn,
216
244
  issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
217
245
  changedAtVersion: r.changedAtVersion,
218
246
  supersededAtVersion: r.supersededAtVersion,
@@ -351,6 +379,7 @@ var init_domain = __esm(() => {
351
379
  supersededBy: z.string().nullable().optional(),
352
380
  relates: z.array(z.string()).optional().default([]),
353
381
  livesIn: z.array(z.string()).optional().default([]),
382
+ verifiedIn: z.array(z.string()).optional().default([]),
354
383
  issues: z.array(SpecIssueSchema).optional().default([]),
355
384
  changedAtVersion: z.number().int().optional(),
356
385
  supersededAtVersion: z.number().int().optional(),
@@ -1106,6 +1135,12 @@ function isPathIgnored(path, extra = []) {
1106
1135
  const anchored = `/${path}`;
1107
1136
  return IGNORE_SUBSTR.some((t) => anchored.includes(`/${t}`)) || extra.some((t) => path.startsWith(t.endsWith("/") ? t : `${t}/`));
1108
1137
  }
1138
+ function isHiddenEntry(name) {
1139
+ return name.startsWith(".") && name !== ".github";
1140
+ }
1141
+ function isHiddenPath(rel) {
1142
+ return rel.split("/").some(isHiddenEntry);
1143
+ }
1109
1144
  async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
1110
1145
  const absDir = relDir ? join2(rootDir, relDir) : rootDir;
1111
1146
  let entries;
@@ -1115,7 +1150,7 @@ async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
1115
1150
  return;
1116
1151
  }
1117
1152
  for (const ent of entries) {
1118
- if (ent.name.startsWith("."))
1153
+ if (isHiddenEntry(ent.name))
1119
1154
  continue;
1120
1155
  const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
1121
1156
  if (ent.isDirectory()) {
@@ -1147,8 +1182,11 @@ async function findDomainJsonFiles(canonicalDir) {
1147
1182
  async function findCodeFiles(repoDir, exts = DEFAULT_EXTS, extraIgnore = []) {
1148
1183
  return walkFiles(repoDir, (rel) => isTagSource(rel, exts), extraIgnore);
1149
1184
  }
1185
+ function isDocSource(rel) {
1186
+ return rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md");
1187
+ }
1150
1188
  async function findDocFiles(repoDir, extraIgnore = []) {
1151
- return walkFiles(repoDir, (rel) => rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md"), extraIgnore);
1189
+ return walkFiles(repoDir, isDocSource, extraIgnore);
1152
1190
  }
1153
1191
  var IGNORE_SUBSTR, DEFAULT_EXTS;
1154
1192
  var init_fs = __esm(() => {
@@ -1179,7 +1217,10 @@ var init_fs = __esm(() => {
1179
1217
  "tf",
1180
1218
  "toml",
1181
1219
  "sql",
1182
- "css"
1220
+ "css",
1221
+ "yml",
1222
+ "yaml",
1223
+ "astro"
1183
1224
  ];
1184
1225
  });
1185
1226
 
@@ -1405,8 +1446,9 @@ function toRepos(columns) {
1405
1446
  return columns.map((c) => ({ ...c.repo, dependency_depth: depth.get(c.repo.name) ?? 0 })).sort(byName);
1406
1447
  }
1407
1448
  function selfMember(absPlatform, platformVersion) {
1449
+ const dir = basename(absPlatform);
1408
1450
  return {
1409
- name: basename(absPlatform),
1451
+ name: dir === CANONICAL_SPECS_DIR ? "root" : dir,
1410
1452
  path: absPlatform,
1411
1453
  pinned_spec_version: platformVersion,
1412
1454
  dependency_depth: 0,
@@ -1446,7 +1488,7 @@ function packageDirs(repo) {
1446
1488
  function loneRootColumn(absPlatform, repo, platformVersion) {
1447
1489
  const dirs = packageDirs(repo);
1448
1490
  const { dependency_depth: _depth, ...self } = selfMember(absPlatform, platformVersion);
1449
- if (dirs === null || self.name === CANONICAL_SPECS_DIR)
1491
+ if (dirs === null)
1450
1492
  return [];
1451
1493
  return [{ repo: { ...self, ignore: dirs }, node: repo }];
1452
1494
  }
@@ -1525,14 +1567,15 @@ async function membersOf(reading, absPlatform, platformVersion) {
1525
1567
  const undeclared = mapped.diagnostics.filter((d) => d.code === "UNLISTED_REPO").map((d) => ({ name: d.subject, path: join3(absPlatform, d.subject) }));
1526
1568
  return { mode: "multi-repo", members: toRepos(columns), unpinned, undeclared, diagnostics: diagnostics2 };
1527
1569
  }
1528
- async function discoverRepos(platformDir) {
1570
+ async function discoverRepos(platformDir, opts = {}) {
1529
1571
  const absPlatform = resolve2(platformDir);
1530
1572
  const canonicalPath = join3(absPlatform, CANONICAL_SPECS_DIR);
1531
1573
  if (!isExistingDir(canonicalPath)) {
1532
1574
  throw new NotASpecPlatformError(absPlatform);
1533
1575
  }
1534
1576
  const platformVersion = await derivePlatformVersion(absPlatform);
1535
- warnIfRetiredManifest(absPlatform, platformVersion);
1577
+ if (!opts.quiet)
1578
+ warnIfRetiredManifest(absPlatform, platformVersion);
1536
1579
  const canonical = {
1537
1580
  name: CANONICAL_SPECS_DIR,
1538
1581
  path: canonicalPath,
@@ -1862,7 +1905,14 @@ ORDER BY tc.source_file, tc.line, tc.req_id
1862
1905
  `;
1863
1906
 
1864
1907
  // src/storage/sql/fts.ts
1865
- var FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status NOT IN ('Superseded', 'Deprecated') ORDER BY rank ASC LIMIT $limit`, RESOLVE_BY_FILES_SQL_PREFIX = "SELECT DISTINCT r.id, r.key, r.seq, r.status, r.superseded_by, r.text, r.why, r.source_file, r.line, r.spec_version, r.changed_at_version, r.superseded_at_version FROM tags t JOIN requirements r ON r.id = t.req_id WHERE t.file IN (", RESOLVE_BY_FILES_SQL_SUFFIX = ") ORDER BY r.key, r.seq";
1908
+ function quoteBareWords(text) {
1909
+ return text.split(/\s+/).map((token) => token === "" || FTS_OPERATORS.has(token) || FTS_SYNTAX.test(token) || /^[\p{L}\p{N}_]+$/u.test(token) ? token : `"${token}"`).join(" ");
1910
+ }
1911
+ var FTS_OPERATORS, FTS_SYNTAX, FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status NOT IN ('Superseded', 'Deprecated') ORDER BY rank ASC LIMIT $limit`, RESOLVE_BY_FILES_SQL_PREFIX = "SELECT DISTINCT r.id, r.key, r.seq, r.status, r.superseded_by, r.text, r.why, r.source_file, r.line, r.spec_version, r.changed_at_version, r.superseded_at_version FROM tags t JOIN requirements r ON r.id = t.req_id WHERE t.file IN (", RESOLVE_BY_FILES_SQL_SUFFIX = ") ORDER BY r.key, r.seq";
1912
+ var init_fts = __esm(() => {
1913
+ FTS_OPERATORS = new Set(["AND", "OR", "NOT", "NEAR"]);
1914
+ FTS_SYNTAX = /["()*:^]/;
1915
+ });
1866
1916
 
1867
1917
  // src/storage/sql/propagation.ts
1868
1918
  var PROP_REPO_STATES_SQL = `
@@ -2139,7 +2189,7 @@ class SqliteStorage {
2139
2189
  }
2140
2190
  searchFts(text, limit = 10) {
2141
2191
  try {
2142
- return this.#db.query(FTS_SEARCH_SQL).all({ query: text, limit });
2192
+ return this.#db.query(FTS_SEARCH_SQL).all({ query: quoteBareWords(text), limit });
2143
2193
  } catch (e) {
2144
2194
  const msg = e instanceof Error ? e.message : String(e);
2145
2195
  if (/fts5:|syntax error/i.test(msg)) {
@@ -2168,33 +2218,9 @@ class SqliteStorage {
2168
2218
  if (files.length > FILES_MAX) {
2169
2219
  throw new Error(`resolveByFiles: too many files (max ${FILES_MAX} per call; got ${files.length})`);
2170
2220
  }
2171
- const queryFiles = this.#expandSelfMemberFiles(files);
2172
- if (queryFiles.length > 32000) {
2173
- throw new Error(`resolveByFiles: expanded bind count ${queryFiles.length} would exceed ` + "SQLITE_MAX_VARIABLE_NUMBER (32766); lower FILES_MAX");
2174
- }
2175
- const placeholders = queryFiles.map(() => "?").join(",");
2221
+ const placeholders = files.map(() => "?").join(",");
2176
2222
  const sql = RESOLVE_BY_FILES_SQL_PREFIX + placeholders + RESOLVE_BY_FILES_SQL_SUFFIX;
2177
- return this.#db.query(sql).all(...queryFiles);
2178
- }
2179
- #selfMemberName() {
2180
- const canonical = this.getRepo("spec-engine");
2181
- if (!canonical)
2182
- return null;
2183
- const platformRoot = dirname2(canonical.path);
2184
- const row = this.#db.query("SELECT name FROM repos WHERE name != 'spec-engine' AND path = $path").get({ path: platformRoot });
2185
- return row?.name ?? null;
2186
- }
2187
- #expandSelfMemberFiles(files) {
2188
- const self = this.#selfMemberName();
2189
- if (!self)
2190
- return files;
2191
- const prefix = `${self}/`;
2192
- const expanded = [...files];
2193
- for (const f of files) {
2194
- if (!f.startsWith(prefix))
2195
- expanded.push(prefix + f);
2196
- }
2197
- return expanded;
2223
+ return this.#db.query(sql).all(...files);
2198
2224
  }
2199
2225
  withWriteTx(fn) {
2200
2226
  const wrapped = this.#db.transaction((arg) => fn(arg));
@@ -2433,6 +2459,7 @@ var BUSY_TIMEOUT_MS = 2000;
2433
2459
  var init_sqlite = __esm(() => {
2434
2460
  init_src();
2435
2461
  init_constants();
2462
+ init_fts();
2436
2463
  init_reads();
2437
2464
  });
2438
2465
 
@@ -2599,7 +2626,7 @@ var init_diagnostics2 = __esm(() => {
2599
2626
  });
2600
2627
 
2601
2628
  // src/indexer/pipeline.ts
2602
- import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2629
+ import { basename as basename2, dirname as dirname3, isAbsolute, join as join4, relative, resolve as resolve3, sep as sep2 } from "path";
2603
2630
  async function runIndex(opts) {
2604
2631
  const platformDir = resolve3(opts.platformDir);
2605
2632
  const {
@@ -2659,13 +2686,35 @@ function repoExtraIgnore(member) {
2659
2686
  const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
2660
2687
  return [...member.selfMember ? ["spec-engine/"] : [], ...repoIgnore];
2661
2688
  }
2689
+ function scannedTagFile(members, platformDir, platformRel) {
2690
+ const abs = resolve3(platformDir, platformRel);
2691
+ let owner = null;
2692
+ for (const member2 of members) {
2693
+ const rel2 = relative(member2.path, abs).split(sep2).join("/");
2694
+ if (rel2 === "" || rel2 === ".." || rel2.startsWith("../") || isAbsolute(rel2))
2695
+ continue;
2696
+ if (owner === null || member2.path.length > owner.member.path.length)
2697
+ owner = { member: member2, rel: rel2 };
2698
+ }
2699
+ if (owner === null)
2700
+ return null;
2701
+ const { member, rel } = owner;
2702
+ if (isHiddenPath(rel) || isPathIgnored(rel, repoExtraIgnore(member)))
2703
+ return null;
2704
+ if (!isTagSource(rel) && !isDocSource(rel))
2705
+ return null;
2706
+ return tagFile(member, rel);
2707
+ }
2708
+ function tagFile(member, rel) {
2709
+ return member.selfMember ? rel : `${member.name}/${rel}`;
2710
+ }
2662
2711
  async function scanMemberCode(member, extraIgnore) {
2663
2712
  const hitsOut = [];
2664
2713
  const codePaths = await findCodeFiles(member.path, DEFAULT_EXTS, extraIgnore);
2665
2714
  for (const rel of codePaths) {
2666
2715
  const absPath = join4(member.path, rel);
2667
2716
  const text = await Bun.file(absPath).text();
2668
- const platformRelative = `${member.name}/${rel}`;
2717
+ const platformRelative = tagFile(member, rel);
2669
2718
  const hits = scanTagsInFile(member.name, platformRelative, text);
2670
2719
  for (const h of hits)
2671
2720
  hitsOut.push(h);
@@ -2679,7 +2728,7 @@ async function scanMemberDocs(member, extraIgnore) {
2679
2728
  for (const rel of docPaths) {
2680
2729
  const absPath = join4(member.path, rel);
2681
2730
  const text = await Bun.file(absPath).text();
2682
- const platformRelative = `${member.name}/${rel}`;
2731
+ const platformRelative = tagFile(member, rel);
2683
2732
  const { tags, mentions } = scanDocFile(member.name, platformRelative, text);
2684
2733
  for (const t of tags)
2685
2734
  tagsOut.push(t);
@@ -2892,7 +2941,7 @@ var init__result = __esm(() => {
2892
2941
 
2893
2942
  // src/operations/init.ts
2894
2943
  import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "fs";
2895
- import { basename as basename3, join as join5, relative, resolve as resolve4, sep as sep2 } from "path";
2944
+ import { basename as basename3, join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "path";
2896
2945
  import {
2897
2946
  applyInit,
2898
2947
  applyLink,
@@ -2905,7 +2954,7 @@ function errMessage(err) {
2905
2954
  return err instanceof Error ? err.message : String(err);
2906
2955
  }
2907
2956
  function hasSpecEngineSegment(path) {
2908
- return path.split(sep2).filter((s) => s.length > 0).includes(CANONICAL_SPECS_DIR);
2957
+ return path.split(sep3).filter((s) => s.length > 0).includes(CANONICAL_SPECS_DIR);
2909
2958
  }
2910
2959
  function canonicalize(repoDir) {
2911
2960
  let repoStat;
@@ -2926,7 +2975,7 @@ function canonicalize(repoDir) {
2926
2975
  function judgeSpecTree(repoDir, canonical, root) {
2927
2976
  if (!hasSpecEngineSegment(canonical))
2928
2977
  return { ok: true };
2929
- const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative(root, canonical) : canonical;
2978
+ const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative2(root, canonical) : canonical;
2930
2979
  if (hasSpecEngineSegment(judged)) {
2931
2980
  return fail("usage", `${repoDir} resolves to ${canonical}, which is inside a spec-engine/ tree \u2014 refusing to scaffold there.`);
2932
2981
  }
@@ -3303,14 +3352,14 @@ function handleNotAPlatform(e) {
3303
3352
  }
3304
3353
  throw e;
3305
3354
  }
3306
- function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
3355
+ async function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
3307
3356
  if (!(e instanceof InvalidDomainFilesError))
3308
3357
  return;
3309
3358
  if (json)
3310
3359
  console.log(renderDiagnostics(e.diagnostics, "json"));
3311
3360
  else
3312
3361
  console.error(renderDiagnostics(e.diagnostics, "text"));
3313
- process.exit(code);
3362
+ await exit(code);
3314
3363
  }
3315
3364
  function cliFreshTags(platformDir) {
3316
3365
  const fresh = coldFreshTags(platformDir);
@@ -3318,7 +3367,7 @@ function cliFreshTags(platformDir) {
3318
3367
  try {
3319
3368
  return await fresh(reqId);
3320
3369
  } catch (e) {
3321
- handleInvalidDomainFiles(e, false, EXIT.USAGE);
3370
+ await handleInvalidDomainFiles(e, false, EXIT.USAGE);
3322
3371
  throw e;
3323
3372
  }
3324
3373
  };
@@ -3360,7 +3409,7 @@ async function withReadStorage(opts, fn) {
3360
3409
  });
3361
3410
  } catch (e) {
3362
3411
  handleStorageUnavailable(e, dbPath);
3363
- handleInvalidDomainFiles(e, json);
3412
+ await handleInvalidDomainFiles(e, json);
3364
3413
  handleNotAPlatform(e);
3365
3414
  }
3366
3415
  }
@@ -3369,6 +3418,7 @@ var init__shared = __esm(() => {
3369
3418
  init_constants();
3370
3419
  init_discover();
3371
3420
  init__index();
3421
+ init_stdout();
3372
3422
  init_errors();
3373
3423
  init_sqlite();
3374
3424
  });
@@ -3567,7 +3617,7 @@ function listFlag(rawArgs, name, parsed) {
3567
3617
  }
3568
3618
  return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
3569
3619
  }
3570
- var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, relatesArg;
3620
+ var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, testsArg, relatesArg;
3571
3621
  var init__args = __esm(() => {
3572
3622
  init_constants();
3573
3623
  platformDirArg = {
@@ -3592,6 +3642,10 @@ var init__args = __esm(() => {
3592
3642
  type: "string",
3593
3643
  description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
3594
3644
  };
3645
+ testsArg = {
3646
+ type: "string",
3647
+ description: "Verified in (verifiedIn) test paths: repeat the flag or comma-separate; an empty value clears the list"
3648
+ };
3595
3649
  relatesArg = {
3596
3650
  type: "string",
3597
3651
  description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
@@ -3670,7 +3724,7 @@ var init_commands = __esm(() => {
3670
3724
  console.error(formatNotASpecPlatform(err.platformDir));
3671
3725
  process.exit(EXIT.USAGE);
3672
3726
  }
3673
- handleInvalidDomainFiles(err, !!args.json);
3727
+ await handleInvalidDomainFiles(err, !!args.json);
3674
3728
  console.error("spec index FAILED:", err);
3675
3729
  process.exit(EXIT.FAILURE);
3676
3730
  } finally {
@@ -4049,67 +4103,117 @@ var init_filerefs = __esm(() => {
4049
4103
  });
4050
4104
 
4051
4105
  // src/check/filerefs.ts
4052
- async function readDomain(platformDir, key) {
4053
- const { abs, rel } = specPaths(platformDir, key);
4054
- let raw;
4055
- try {
4056
- raw = await Bun.file(abs).text();
4057
- } catch {
4058
- return null;
4059
- }
4060
- const parsed = parseDomainText(raw, rel);
4061
- return parsed.ok ? { raw, requirements: parsed.data.requirements } : null;
4062
- }
4063
- function rowsForRequirement(platformDir, key, reqId, livesIn, line) {
4064
- const rows = [];
4065
- for (const entry of livesIn) {
4066
- const ref = entry.startsWith("@") ? entry.slice(1) : entry;
4067
- if (ref.length === 0 || resolveFileRef(platformDir, ref))
4068
- continue;
4069
- rows.push({
4070
- code: DiagnosticCode.BROKEN_FILE_REF,
4071
- severity: "error",
4072
- repo: null,
4073
- source_file: specPaths(platformDir, key).rel,
4074
- line,
4075
- req_id: reqId,
4076
- detail: `livesIn entry ${entry} does not resolve to a file under the platform root`
4077
- });
4078
- }
4079
- return rows;
4080
- }
4081
- function rowsForDomain(platformDir, key, domain2) {
4082
- const rows = [];
4083
- const rawLines = domain2.raw.split(`
4106
+ function locateInDomain(raw, sourceFile, requirements, statuses) {
4107
+ const out = [];
4108
+ const rawLines = raw.split(`
4084
4109
  `);
4085
4110
  let lineCursor = 0;
4086
- for (const req of domain2.requirements) {
4111
+ for (const req of requirements) {
4087
4112
  const found = findRequirementIdLine(rawLines, req.id, lineCursor);
4088
4113
  if (found >= 0)
4089
4114
  lineCursor = found + 1;
4090
- if (!CHECKED_STATUSES.has(req.status.toLowerCase()))
4115
+ if (statuses.has(req.status.toLowerCase())) {
4116
+ out.push({ sourceFile, line: found >= 0 ? found + 1 : 0, req });
4117
+ }
4118
+ }
4119
+ return out;
4120
+ }
4121
+ async function locatedEntries(platformDir, statuses) {
4122
+ const out = [];
4123
+ for (const key of listDomainKeys(platformDir)) {
4124
+ const { abs, rel } = specPaths(platformDir, key);
4125
+ let raw;
4126
+ try {
4127
+ raw = await Bun.file(abs).text();
4128
+ } catch {
4091
4129
  continue;
4092
- rows.push(...rowsForRequirement(platformDir, key, req.id, req.livesIn, found >= 0 ? found + 1 : 0));
4130
+ }
4131
+ const parsed = parseDomainText(raw, rel);
4132
+ if (parsed.ok)
4133
+ out.push(...locateInDomain(raw, rel, parsed.data.requirements, statuses));
4093
4134
  }
4094
- return rows;
4135
+ return out;
4136
+ }
4137
+ function sortRows(rows) {
4138
+ 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));
4139
+ }
4140
+ function row(at, code, severity, detail) {
4141
+ return {
4142
+ code,
4143
+ severity,
4144
+ repo: null,
4145
+ source_file: at.sourceFile,
4146
+ line: at.line,
4147
+ req_id: at.req.id,
4148
+ detail
4149
+ };
4095
4150
  }
4096
4151
  async function brokenFileRefDiagnostics(platformDir) {
4097
4152
  const rows = [];
4098
- for (const key of listDomainKeys(platformDir)) {
4099
- const domain2 = await readDomain(platformDir, key);
4100
- if (domain2 !== null)
4101
- rows.push(...rowsForDomain(platformDir, key, domain2));
4153
+ for (const at of await locatedEntries(platformDir, FILE_REF_STATUSES)) {
4154
+ for (const [field, entries] of [
4155
+ ["livesIn", at.req.livesIn],
4156
+ ["verifiedIn", at.req.verifiedIn]
4157
+ ]) {
4158
+ for (const entry of entries) {
4159
+ const ref = refOf(entry);
4160
+ if (ref.length === 0 || resolveFileRef(platformDir, ref))
4161
+ continue;
4162
+ rows.push(row(at, DiagnosticCode.BROKEN_FILE_REF, "error", `${field} entry ${entry} does not resolve to a file under the platform root`));
4163
+ }
4164
+ }
4165
+ }
4166
+ return sortRows(rows);
4167
+ }
4168
+ function tagKeys(tags, kinds) {
4169
+ return new Set(tags.filter((t) => kinds.includes(t.kind)).map((t) => `${t.req_id} ${t.file}`));
4170
+ }
4171
+ function inFileRows(platformDir, members, at, check) {
4172
+ const rows = [];
4173
+ for (const entry of at.req[check.field]) {
4174
+ const ref = refOf(entry);
4175
+ if (ref.length === 0 || !resolveFileRef(platformDir, ref))
4176
+ continue;
4177
+ const file = scannedTagFile(members, platformDir, ref);
4178
+ if (file === null || check.keys.has(`${at.req.id} ${file}`))
4179
+ continue;
4180
+ rows.push(row(at, check.code, "warning", `${check.field} entry ${entry} holds no ${check.missing} tag for ${at.req.id}`));
4102
4181
  }
4103
- rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? "") || a.detail.localeCompare(b.detail));
4104
4182
  return rows;
4105
4183
  }
4106
- var CHECKED_STATUSES;
4184
+ async function inFileTagDiagnostics(platformDir, tags) {
4185
+ const { members } = await discoverRepos(platformDir, { quiet: true });
4186
+ const checks = [
4187
+ {
4188
+ field: "livesIn",
4189
+ code: DiagnosticCode.UNIMPLEMENTED_IN_FILE,
4190
+ keys: tagKeys(tags, ["implements", "documents"]),
4191
+ missing: "implementing or documenting"
4192
+ },
4193
+ {
4194
+ field: "verifiedIn",
4195
+ code: DiagnosticCode.UNVERIFIED_IN_FILE,
4196
+ keys: tagKeys(tags, ["verifies"]),
4197
+ missing: "verifying"
4198
+ }
4199
+ ];
4200
+ const rows = [];
4201
+ for (const at of await locatedEntries(platformDir, IN_FILE_STATUSES)) {
4202
+ for (const check of checks)
4203
+ rows.push(...inFileRows(platformDir, members, at, check));
4204
+ }
4205
+ return sortRows(rows);
4206
+ }
4207
+ var FILE_REF_STATUSES, IN_FILE_STATUSES, refOf = (entry) => entry.startsWith("@") ? entry.slice(1) : entry;
4107
4208
  var init_filerefs2 = __esm(() => {
4108
4209
  init_src();
4109
4210
  init_domains();
4110
4211
  init_filerefs();
4111
4212
  init_constants();
4112
- CHECKED_STATUSES = new Set(["active", "draft"]);
4213
+ init_discover();
4214
+ init_pipeline();
4215
+ FILE_REF_STATUSES = new Set(["active", "draft"]);
4216
+ IN_FILE_STATUSES = new Set(["active"]);
4113
4217
  });
4114
4218
 
4115
4219
  // src/check/grammar.ts
@@ -4130,7 +4234,7 @@ async function readDeclaredDomain(platformDir, key) {
4130
4234
  requirements: parsed.data.requirements
4131
4235
  };
4132
4236
  }
4133
- function rowsForDomain2(key, domain2) {
4237
+ function rowsForDomain(key, domain2) {
4134
4238
  const rows = [];
4135
4239
  const rawLines = domain2.raw.split(`
4136
4240
  `);
@@ -4139,7 +4243,7 @@ function rowsForDomain2(key, domain2) {
4139
4243
  const found = findRequirementIdLine(rawLines, req.id, lineCursor);
4140
4244
  if (found >= 0)
4141
4245
  lineCursor = found + 1;
4142
- if (!CHECKED_STATUSES2.has(req.status.toLowerCase()))
4246
+ if (!CHECKED_STATUSES.has(req.status.toLowerCase()))
4143
4247
  continue;
4144
4248
  const result = parseEarsStatement(req.statement);
4145
4249
  if (result.ok)
@@ -4163,17 +4267,17 @@ async function statementGrammarDiagnostics(platformDir) {
4163
4267
  continue;
4164
4268
  const domain2 = await readDeclaredDomain(platformDir, key);
4165
4269
  if (domain2 !== null)
4166
- rows.push(...rowsForDomain2(key, domain2));
4270
+ rows.push(...rowsForDomain(key, domain2));
4167
4271
  }
4168
4272
  rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
4169
4273
  return rows;
4170
4274
  }
4171
- var CHECKED_STATUSES2;
4275
+ var CHECKED_STATUSES;
4172
4276
  var init_grammar = __esm(() => {
4173
4277
  init_src();
4174
4278
  init_domains();
4175
4279
  init_constants();
4176
- CHECKED_STATUSES2 = new Set(["active", "draft"]);
4280
+ CHECKED_STATUSES = new Set(["active", "draft"]);
4177
4281
  });
4178
4282
 
4179
4283
  // src/check/propagation-teeth.ts
@@ -4250,12 +4354,12 @@ function readCheckConfig(platformDir) {
4250
4354
  return { ok: true, config: { severity: [] } };
4251
4355
  return parseCheckConfigText(readFileSync(path, "utf-8"), CHECK_CONFIG_REL);
4252
4356
  }
4253
- function specificity(rule, row) {
4254
- if (rule.code !== row.code)
4357
+ function specificity(rule, row2) {
4358
+ if (rule.code !== row2.code)
4255
4359
  return Specificity.none;
4256
4360
  if (rule.match === undefined)
4257
4361
  return Specificity.code;
4258
- const id = row.req_id;
4362
+ const id = row2.req_id;
4259
4363
  if (id === null)
4260
4364
  return Specificity.none;
4261
4365
  if (rule.match.includes(id))
@@ -4265,20 +4369,20 @@ function specificity(rule, row) {
4265
4369
  }
4266
4370
  function applySeverity(rows, rules) {
4267
4371
  const out = [];
4268
- for (const row of rows) {
4372
+ for (const row2 of rows) {
4269
4373
  let best;
4270
4374
  let bestScore = Specificity.none;
4271
4375
  for (const rule of rules) {
4272
- const score = specificity(rule, row);
4376
+ const score = specificity(rule, row2);
4273
4377
  if (score !== Specificity.none && score >= bestScore) {
4274
4378
  best = rule;
4275
4379
  bestScore = score;
4276
4380
  }
4277
4381
  }
4278
4382
  if (best === undefined)
4279
- out.push(row);
4383
+ out.push(row2);
4280
4384
  else if (best.severity !== "off")
4281
- out.push({ ...row, severity: best.severity });
4385
+ out.push({ ...row2, severity: best.severity });
4282
4386
  }
4283
4387
  return out;
4284
4388
  }
@@ -4797,6 +4901,7 @@ function successorEntry(id, pred, fields) {
4797
4901
  supersededBy: null,
4798
4902
  relates: fields.relates,
4799
4903
  livesIn: fields.livesIn,
4904
+ verifiedIn: fields.verifiedIn,
4800
4905
  issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
4801
4906
  aliases: [...pred.aliases],
4802
4907
  cites: pred.cites.map((c) => ({ ...c }))
@@ -4884,7 +4989,8 @@ async function mint(input) {
4884
4989
  const warnings = unresolvableRefWarnings(platformDir, [
4885
4990
  input.statement,
4886
4991
  input.why,
4887
- ...input.livesIn
4992
+ ...input.livesIn,
4993
+ ...input.verifiedIn ?? []
4888
4994
  ]);
4889
4995
  const verdict = await judgeStatementGrammar(platformDir, key, input.statement);
4890
4996
  if (!verdict.ok) {
@@ -4908,6 +5014,7 @@ async function mint(input) {
4908
5014
  supersededBy: null,
4909
5015
  relates: splitList(input.relates ?? []),
4910
5016
  livesIn: input.livesIn,
5017
+ verifiedIn: input.verifiedIn ?? [],
4911
5018
  issues: input.issue ? [{ role: "created", id: input.issue }] : [],
4912
5019
  aliases: [],
4913
5020
  cites: input.cites ?? []
@@ -4965,6 +5072,7 @@ async function mintTerm(input) {
4965
5072
  supersededBy: null,
4966
5073
  relates: [],
4967
5074
  livesIn: [],
5075
+ verifiedIn: [],
4968
5076
  issues: [],
4969
5077
  aliases: input.aliases,
4970
5078
  cites: [],
@@ -5151,6 +5259,7 @@ async function migrateGlossary(platformDir) {
5151
5259
  supersededBy: null,
5152
5260
  relates: [],
5153
5261
  livesIn: [],
5262
+ verifiedIn: [],
5154
5263
  issues: [],
5155
5264
  aliases: [],
5156
5265
  cites: [],
@@ -5508,6 +5617,7 @@ async function check(input, storage2) {
5508
5617
  }
5509
5618
  diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
5510
5619
  diagnostics2.push(...await brokenFileRefDiagnostics(platformDir));
5620
+ diagnostics2.push(...await inFileTagDiagnostics(platformDir, storage2.listTags()));
5511
5621
  diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
5512
5622
  const glossaryDrift = glossaryDriftDiagnostic(platformDir);
5513
5623
  if (glossaryDrift !== null)
@@ -5594,6 +5704,7 @@ var init_check2 = __esm(() => {
5594
5704
  init_prompt();
5595
5705
  init__index();
5596
5706
  init_check();
5707
+ init_stdout();
5597
5708
  init__args();
5598
5709
  init__shared();
5599
5710
  checkCommand = defineCommand3({
@@ -5667,7 +5778,7 @@ var init_check2 = __esm(() => {
5667
5778
  return result;
5668
5779
  });
5669
5780
  } catch (e) {
5670
- handleInvalidDomainFiles(e, jsonMode);
5781
+ await handleInvalidDomainFiles(e, jsonMode);
5671
5782
  exitAfterCrash(e);
5672
5783
  }
5673
5784
  if (outcome === undefined) {
@@ -5685,7 +5796,7 @@ var init_check2 = __esm(() => {
5685
5796
  console.log(renderDiagnostics(diagnostics2, jsonMode ? "json" : "text"));
5686
5797
  if (!jsonMode)
5687
5798
  console.log(`build_id: ${outcome.buildId}`);
5688
- process.exit(outcome.failing ? EXIT.FAILURE : EXIT.OK);
5799
+ await exit(outcome.failing ? EXIT.FAILURE : EXIT.OK);
5689
5800
  }
5690
5801
  });
5691
5802
  });
@@ -5935,12 +6046,12 @@ function sortMatrix(rows) {
5935
6046
  return a.repo.localeCompare(b.repo);
5936
6047
  });
5937
6048
  }
5938
- function cellStatus(row) {
5939
- if (row.implemented === 1 && row.verified === 1)
6049
+ function cellStatus(row2) {
6050
+ if (row2.implemented === 1 && row2.verified === 1)
5940
6051
  return "src+test";
5941
- if (row.implemented === 1)
6052
+ if (row2.implemented === 1)
5942
6053
  return "src";
5943
- if (row.verified === 1)
6054
+ if (row2.verified === 1)
5944
6055
  return "test";
5945
6056
  return EMPTY_CELL;
5946
6057
  }
@@ -5953,18 +6064,18 @@ function renderMatrix(rows, mode) {
5953
6064
  return "";
5954
6065
  const repos = Array.from(new Set(sorted.map((r) => r.repo))).sort((a, b) => a.localeCompare(b));
5955
6066
  const byReq = new Map;
5956
- for (const row of sorted) {
5957
- let entry = byReq.get(row.req_id);
6067
+ for (const row2 of sorted) {
6068
+ let entry = byReq.get(row2.req_id);
5958
6069
  if (!entry) {
5959
6070
  entry = {
5960
- domain_key: row.domain_key,
5961
- req_id: row.req_id,
5962
- req_status: row.req_status,
6071
+ domain_key: row2.domain_key,
6072
+ req_id: row2.req_id,
6073
+ req_status: row2.req_status,
5963
6074
  cells: new Map
5964
6075
  };
5965
- byReq.set(row.req_id, entry);
6076
+ byReq.set(row2.req_id, entry);
5966
6077
  }
5967
- entry.cells.set(row.repo, cellStatus(row));
6078
+ entry.cells.set(row2.repo, cellStatus(row2));
5968
6079
  }
5969
6080
  const header = ["DOMAIN", "REQUIREMENT", "STATUS", ...repos];
5970
6081
  const lines = [header];
@@ -5972,8 +6083,8 @@ function renderMatrix(rows, mode) {
5972
6083
  const cells = repos.map((r) => entry.cells.get(r) ?? EMPTY_CELL);
5973
6084
  lines.push([entry.domain_key, entry.req_id, entry.req_status, ...cells]);
5974
6085
  }
5975
- const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
5976
- return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6086
+ const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6087
+ return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
5977
6088
  `);
5978
6089
  }
5979
6090
  var EMPTY_CELL = "\u2014";
@@ -6109,11 +6220,11 @@ var init_map = __esm(() => {
6109
6220
 
6110
6221
  // src/operations/records.ts
6111
6222
  function getRecord(storage2, id) {
6112
- const row = storage2.getRequirement(id);
6223
+ const row2 = storage2.getRequirement(id);
6113
6224
  return {
6114
6225
  ok: true,
6115
- row,
6116
- platformEmpty: row === null && storage2.listRequirements().length === 0
6226
+ row: row2,
6227
+ platformEmpty: row2 === null && storage2.listRequirements().length === 0
6117
6228
  };
6118
6229
  }
6119
6230
  function listRecords(storage2, filter = {}) {
@@ -6150,8 +6261,8 @@ function renderRecords(rows, mode) {
6150
6261
  lines.push([r.id, r.status, r.key, String(r.seq), String(r.changed_at_version), r.text]);
6151
6262
  }
6152
6263
  const fixed = HEADER.length - 1;
6153
- const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
6154
- return lines.map((row) => row.map((cell, i) => i < fixed ? cell.padEnd(widths[i] ?? 0) : cell).join(" ").trimEnd()).join(`
6264
+ const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6265
+ return lines.map((row2) => row2.map((cell, i) => i < fixed ? cell.padEnd(widths[i] ?? 0) : cell).join(" ").trimEnd()).join(`
6155
6266
  `);
6156
6267
  }
6157
6268
  var HEADER;
@@ -6303,11 +6414,11 @@ function renderPropagation(rows, mode) {
6303
6414
  return "";
6304
6415
  const header = ["REPO", "STATE", "VIA", "DRIFT?"];
6305
6416
  const lines = [header];
6306
- for (const row of rows) {
6307
- lines.push([row.repo, row.state, row.via_req_id ?? EMPTY_CELL2, row.drifted ? "yes" : "no"]);
6417
+ for (const row2 of rows) {
6418
+ lines.push([row2.repo, row2.state, row2.via_req_id ?? EMPTY_CELL2, row2.drifted ? "yes" : "no"]);
6308
6419
  }
6309
- const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
6310
- return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6420
+ const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6421
+ return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6311
6422
  `);
6312
6423
  }
6313
6424
  var EMPTY_CELL2 = "\u2014";
@@ -6381,8 +6492,8 @@ function excerptOf(raw) {
6381
6492
  }
6382
6493
  function renderTable(header, rows) {
6383
6494
  const lines = [header, ...rows];
6384
- const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
6385
- return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6495
+ const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
6496
+ return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
6386
6497
  `);
6387
6498
  }
6388
6499
  function renderQuery(hits, mode) {
@@ -6637,17 +6748,17 @@ function compareReqIds2(a, b) {
6637
6748
  function sortProvenance(rows) {
6638
6749
  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);
6639
6750
  }
6640
- function backingTests(row) {
6751
+ function backingTests(row2) {
6641
6752
  let base;
6642
- if (row.implemented === 1 && row.verified === 1)
6753
+ if (row2.implemented === 1 && row2.verified === 1)
6643
6754
  base = "src+test";
6644
- else if (row.implemented === 1)
6755
+ else if (row2.implemented === 1)
6645
6756
  base = "src";
6646
- else if (row.verified === 1)
6757
+ else if (row2.verified === 1)
6647
6758
  base = "test";
6648
6759
  else
6649
6760
  base = "\u2014";
6650
- return row.test_levels ? `${base} (${row.test_levels})` : base;
6761
+ return row2.test_levels ? `${base} (${row2.test_levels})` : base;
6651
6762
  }
6652
6763
  function renderProvenance(rows, mode) {
6653
6764
  const sorted = sortProvenance(rows);
@@ -6657,12 +6768,12 @@ function renderProvenance(rows, mode) {
6657
6768
  if (sorted.length === 0)
6658
6769
  return "";
6659
6770
  const byReq = new Map;
6660
- for (const row of sorted) {
6661
- const bucket = byReq.get(row.req_id);
6771
+ for (const row2 of sorted) {
6772
+ const bucket = byReq.get(row2.req_id);
6662
6773
  if (bucket)
6663
- bucket.push(row);
6774
+ bucket.push(row2);
6664
6775
  else
6665
- byReq.set(row.req_id, [row]);
6776
+ byReq.set(row2.req_id, [row2]);
6666
6777
  }
6667
6778
  const lines = [];
6668
6779
  for (const [reqId, links] of byReq) {
@@ -6677,8 +6788,8 @@ function renderProvenance(rows, mode) {
6677
6788
  return lines.join(`
6678
6789
  `);
6679
6790
  }
6680
- function decorateRow(row, resolved) {
6681
- const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
6791
+ function decorateRow(row2, resolved) {
6792
+ const prefix = ` ${row2.role} ${row2.issue_id} ${row2.source_file}:${row2.line}`;
6682
6793
  if (resolved?.ok && resolved.title && resolved.url) {
6683
6794
  const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
6684
6795
  return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
@@ -6702,12 +6813,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
6702
6813
  if (sorted.length === 0)
6703
6814
  return "";
6704
6815
  const byReq = new Map;
6705
- for (const row of sorted) {
6706
- const bucket = byReq.get(row.req_id);
6816
+ for (const row2 of sorted) {
6817
+ const bucket = byReq.get(row2.req_id);
6707
6818
  if (bucket)
6708
- bucket.push(row);
6819
+ bucket.push(row2);
6709
6820
  else
6710
- byReq.set(row.req_id, [row]);
6821
+ byReq.set(row2.req_id, [row2]);
6711
6822
  }
6712
6823
  const lines = [];
6713
6824
  for (const [reqId, links] of byReq) {
@@ -7074,6 +7185,7 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
7074
7185
  statement,
7075
7186
  why: (args.why ?? "").trim(),
7076
7187
  livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
7188
+ verifiedIn: listFlag(rawArgs, "tests", args.tests) ?? [],
7077
7189
  relates: listFlag(rawArgs, "relates", args.relates) ?? [],
7078
7190
  issue: (args.issue ?? "").trim() || undefined,
7079
7191
  status: statusOf(args),
@@ -7153,6 +7265,7 @@ var init_req = __esm(() => {
7153
7265
  },
7154
7266
  why: { type: "string", description: "Why it matters field (only with --text)" },
7155
7267
  lives: livesArg,
7268
+ tests: testsArg,
7156
7269
  relates: relatesArg,
7157
7270
  issue: {
7158
7271
  type: "string",
@@ -7186,9 +7299,9 @@ var init_req = __esm(() => {
7186
7299
  await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
7187
7300
  return;
7188
7301
  }
7189
- const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.relates === "string" || typeof args.issue === "string";
7302
+ const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.tests === "string" || typeof args.relates === "string" || typeof args.issue === "string";
7190
7303
  if (hasFieldFlags) {
7191
- console.error("spec req: --why/--lives/--relates/--issue require --text (the Requirement field is mandatory)");
7304
+ console.error("spec req: --why/--lives/--tests/--relates/--issue require --text (the Requirement field is mandatory)");
7192
7305
  process.exit(EXIT.USAGE);
7193
7306
  return;
7194
7307
  }
@@ -7276,8 +7389,8 @@ async function listTermsCommand(platformDirRaw, json) {
7276
7389
  console.log(JSON.stringify(result.rows));
7277
7390
  return;
7278
7391
  }
7279
- for (const row of result.rows) {
7280
- console.log(`${row.id} ${row.term} ${row.status}`);
7392
+ for (const row2 of result.rows) {
7393
+ console.log(`${row2.id} ${row2.term} ${row2.status}`);
7281
7394
  }
7282
7395
  }
7283
7396
  async function reviseTermCommand(opts) {
@@ -7324,8 +7437,8 @@ async function confirmTermCommand(opts) {
7324
7437
  exitOnFailure("spec term confirm", result);
7325
7438
  await withIndex({ platformDir, build: "fresh" }, () => {
7326
7439
  return;
7327
- }).catch((e) => {
7328
- handleInvalidDomainFiles(e, false, EXIT.USAGE);
7440
+ }).catch(async (e) => {
7441
+ await handleInvalidDomainFiles(e, false, EXIT.USAGE);
7329
7442
  throw e;
7330
7443
  });
7331
7444
  const { pinned, file } = result;
@@ -7532,7 +7645,7 @@ async function generate(platformDir, json) {
7532
7645
  else
7533
7646
  console.log(`generated GLOSSARY.md from ${result.generated} terms`);
7534
7647
  }
7535
- function check2(platformDir, json) {
7648
+ async function check2(platformDir, json) {
7536
7649
  if (checkGlossary(platformDir).clean) {
7537
7650
  if (json)
7538
7651
  console.log(JSON.stringify({ ok: true }));
@@ -7544,13 +7657,14 @@ function check2(platformDir, json) {
7544
7657
  console.error("run `spec glossary .` to regenerate the human view from the store");
7545
7658
  if (json)
7546
7659
  console.log(JSON.stringify({ ok: false }));
7547
- process.exit(EXIT.FAILURE);
7660
+ await exit(EXIT.FAILURE);
7548
7661
  }
7549
7662
  var glossaryCommand;
7550
7663
  var init_glossary2 = __esm(() => {
7551
7664
  init_constants();
7552
7665
  init_discover();
7553
7666
  init_glossary();
7667
+ init_stdout();
7554
7668
  init__args();
7555
7669
  init__shared();
7556
7670
  glossaryCommand = defineCommand14({
@@ -7576,7 +7690,7 @@ var init_glossary2 = __esm(() => {
7576
7690
  if (args.migrate)
7577
7691
  await migrate(platformDir, json);
7578
7692
  else if (args.check)
7579
- check2(platformDir, json);
7693
+ await check2(platformDir, json);
7580
7694
  else
7581
7695
  await generate(platformDir, json);
7582
7696
  }
@@ -7602,6 +7716,7 @@ function successorFields(input, req) {
7602
7716
  return {
7603
7717
  why: (input.why ?? req.why ?? "").trim(),
7604
7718
  livesIn: input.livesIn ?? [...req.livesIn],
7719
+ verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
7605
7720
  relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
7606
7721
  };
7607
7722
  }
@@ -7646,14 +7761,15 @@ async function supersede(input, freshTags) {
7646
7761
  }
7647
7762
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
7648
7763
  }
7649
- const { why, livesIn, relates } = successorFields(input, req);
7650
- warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn]));
7764
+ const { why, livesIn, verifiedIn, relates } = successorFields(input, req);
7765
+ warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn, ...verifiedIn]));
7651
7766
  const newId = await nextRequirementId(platformDir, key);
7652
7767
  const issue = input.issue?.trim() || undefined;
7653
7768
  const successor = successorEntry(newId, req, {
7654
7769
  statement: input.statement,
7655
7770
  why,
7656
7771
  livesIn,
7772
+ verifiedIn,
7657
7773
  relates,
7658
7774
  issue
7659
7775
  });
@@ -7697,14 +7813,14 @@ function renderResolve(rows, mode) {
7697
7813
  return "";
7698
7814
  const header = ["REQ_ID", "STATUS", "TEXT"];
7699
7815
  const lines = [header];
7700
- for (const row of sorted) {
7701
- const raw = row.text ?? "";
7816
+ for (const row2 of sorted) {
7817
+ const raw = row2.text ?? "";
7702
7818
  const truncated = raw.length > TEXT_MAX ? raw.slice(0, TEXT_MAX) + ELLIPSIS2 : raw;
7703
7819
  const text = truncated.replace(/\s+/g, " ").trim();
7704
- lines.push([row.id, row.status, text || EMPTY_CELL4]);
7820
+ lines.push([row2.id, row2.status, text || EMPTY_CELL4]);
7705
7821
  }
7706
- const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
7707
- return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
7822
+ const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
7823
+ return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
7708
7824
  `);
7709
7825
  }
7710
7826
  function sortReqTags(rows) {
@@ -7719,8 +7835,8 @@ function renderReqTags(rows, mode) {
7719
7835
  return "";
7720
7836
  const header = ["REPO", "FILE", "LINE", "KIND", "LEVEL"];
7721
7837
  const lines = [header];
7722
- for (const row of sorted) {
7723
- lines.push([row.repo, row.file, String(row.line), row.kind, row.level ?? EMPTY_CELL4]);
7838
+ for (const row2 of sorted) {
7839
+ lines.push([row2.repo, row2.file, String(row2.line), row2.kind, row2.level ?? EMPTY_CELL4]);
7724
7840
  }
7725
7841
  const widths = header.map((_, col) => Math.max(...lines.map((l) => (l[col] ?? "").length)));
7726
7842
  return lines.map((l) => l.map((cell, col) => cell.padEnd(widths[col])).join(" ").trimEnd()).join(`
@@ -7767,6 +7883,9 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
7767
7883
  const livesIn = listFlag(rawArgs, "lives", args.lives);
7768
7884
  if (livesIn !== undefined)
7769
7885
  input.livesIn = livesIn;
7886
+ const verifiedIn = listFlag(rawArgs, "tests", args.tests);
7887
+ if (verifiedIn !== undefined)
7888
+ input.verifiedIn = verifiedIn;
7770
7889
  const relates = listFlag(rawArgs, "relates", args.relates);
7771
7890
  if (relates !== undefined)
7772
7891
  input.relates = relates;
@@ -7807,6 +7926,10 @@ var init_supersede2 = __esm(() => {
7807
7926
  type: "string",
7808
7927
  description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
7809
7928
  },
7929
+ tests: {
7930
+ type: "string",
7931
+ description: "Successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the old entry)"
7932
+ },
7810
7933
  relates: {
7811
7934
  type: "string",
7812
7935
  description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
@@ -8000,6 +8123,7 @@ function successorFields2(input, req) {
8000
8123
  statement: (input.statement ?? req.statement).trim(),
8001
8124
  why: (input.why ?? req.why ?? "").trim(),
8002
8125
  livesIn: input.livesIn ?? [...req.livesIn],
8126
+ verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
8003
8127
  relates: [...req.relates]
8004
8128
  };
8005
8129
  }
@@ -8044,7 +8168,12 @@ async function move(input, freshTags) {
8044
8168
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
8045
8169
  }
8046
8170
  }
8047
- warnings.push(...unresolvableRefWarnings(platformDir, [fields.statement, fields.why, ...fields.livesIn]));
8171
+ warnings.push(...unresolvableRefWarnings(platformDir, [
8172
+ fields.statement,
8173
+ fields.why,
8174
+ ...fields.livesIn,
8175
+ ...fields.verifiedIn
8176
+ ]));
8048
8177
  const newId = await nextRequirementId(platformDir, targetKey);
8049
8178
  const sourceCurrent = source.domain.specVersion ?? 1;
8050
8179
  req.status = "superseded";
@@ -8103,6 +8232,9 @@ function inputFromArgs2(args, rawArgs, platformDir, id, targetKey) {
8103
8232
  const livesIn = listFlag(rawArgs, "lives", args.lives);
8104
8233
  if (livesIn !== undefined)
8105
8234
  input.livesIn = livesIn;
8235
+ const verifiedIn = listFlag(rawArgs, "tests", args.tests);
8236
+ if (verifiedIn !== undefined)
8237
+ input.verifiedIn = verifiedIn;
8106
8238
  return input;
8107
8239
  }
8108
8240
  var moveArgs, moveCommand;
@@ -8138,6 +8270,10 @@ var init_move2 = __esm(() => {
8138
8270
  type: "string",
8139
8271
  description: "Rewrite the successor's Lives in: repeat or comma-separate; empty clears (default: copied from the source)"
8140
8272
  },
8273
+ tests: {
8274
+ type: "string",
8275
+ description: "Rewrite the successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the source)"
8276
+ },
8141
8277
  noBump: {
8142
8278
  type: "boolean",
8143
8279
  description: "Do not bump either envelope's specVersion"
@@ -8228,7 +8364,7 @@ function gateRejection(platformDir, relFile, req, fields) {
8228
8364
  const tag = releaseHolding(platformDir, relFile, id);
8229
8365
  if (tag === null)
8230
8366
  return null;
8231
- return fail("conflict", `${id} is released in ${tag} \u2014 its promise is immutable. ` + `Run \`spec supersede ${id}\` to change the statement or why; ` + "pass only --lives or --relates to amend its metadata in place.");
8367
+ 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.");
8232
8368
  }
8233
8369
  function applyFields(req, fields) {
8234
8370
  const fieldsChanged = [];
@@ -8248,6 +8384,11 @@ function applyFields(req, fields) {
8248
8384
  fieldsChanged.push("lives");
8249
8385
  refValues.push(...fields.livesIn);
8250
8386
  }
8387
+ if (fields.verifiedIn !== undefined) {
8388
+ req.verifiedIn = fields.verifiedIn;
8389
+ fieldsChanged.push("tests");
8390
+ refValues.push(...fields.verifiedIn);
8391
+ }
8251
8392
  if (fields.relates !== undefined) {
8252
8393
  req.relates = splitList(fields.relates);
8253
8394
  fieldsChanged.push("relates");
@@ -8317,9 +8458,9 @@ __export(exports_amend, {
8317
8458
  import { defineCommand as defineCommand18 } from "citty";
8318
8459
  function assertAmendFlags(args) {
8319
8460
  const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
8320
- const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
8461
+ const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.tests !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
8321
8462
  if (!hasField && !hasIssue) {
8322
- console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --relates / --term / --aliases");
8463
+ console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --tests / --relates / --term / --aliases");
8323
8464
  process.exit(EXIT.USAGE);
8324
8465
  }
8325
8466
  if (args.text !== undefined && args.text.trim() === "") {
@@ -8346,6 +8487,9 @@ function fieldsFromArgs(args, rawArgs) {
8346
8487
  const livesIn = listFlag(rawArgs, "lives", args.lives);
8347
8488
  if (livesIn !== undefined)
8348
8489
  fields.livesIn = livesIn;
8490
+ const verifiedIn = listFlag(rawArgs, "tests", args.tests);
8491
+ if (verifiedIn !== undefined)
8492
+ fields.verifiedIn = verifiedIn;
8349
8493
  const relates = listFlag(rawArgs, "relates", args.relates);
8350
8494
  if (relates !== undefined)
8351
8495
  fields.relates = relates;
@@ -8375,6 +8519,7 @@ var init_amend2 = __esm(() => {
8375
8519
  text: { type: "string", description: "New Requirement (statement) field value" },
8376
8520
  why: { type: "string", description: "New Why it matters field value" },
8377
8521
  lives: livesArg,
8522
+ tests: testsArg,
8378
8523
  relates: relatesArg,
8379
8524
  issue: {
8380
8525
  type: "string",
@@ -8390,7 +8535,7 @@ var init_amend2 = __esm(() => {
8390
8535
  amendCommand = defineCommand18({
8391
8536
  meta: {
8392
8537
  name: "amend",
8393
- description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --lives and --relates still amend."
8538
+ 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."
8394
8539
  },
8395
8540
  args: amendArgs,
8396
8541
  async run({ args, rawArgs }) {
@@ -8544,6 +8689,8 @@ function amendFieldsOf(args) {
8544
8689
  fields.why = args.why || null;
8545
8690
  if (args.lives !== undefined)
8546
8691
  fields.livesIn = args.lives;
8692
+ if (args.verified_in !== undefined)
8693
+ fields.verifiedIn = args.verified_in;
8547
8694
  if (args.relates !== undefined)
8548
8695
  fields.relates = args.relates;
8549
8696
  if (args.issue)
@@ -8621,8 +8768,8 @@ function buildMcpServer(platformDir) {
8621
8768
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8622
8769
  }
8623
8770
  return fresh((s) => {
8624
- const { row } = getRecord(s, req_id);
8625
- return jsonResult(row ?? []);
8771
+ const { row: row2 } = getRecord(s, req_id);
8772
+ return jsonResult(row2 ?? []);
8626
8773
  });
8627
8774
  });
8628
8775
  server.registerTool("spec_list", {
@@ -8653,14 +8800,24 @@ function buildMcpServer(platformDir) {
8653
8800
  statement: z4.string().min(1).describe("The successor's statement"),
8654
8801
  why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
8655
8802
  lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
8803
+ verified_in: z4.array(z4.string()).optional().describe("Successor's verifiedIn paths (default: copied from the predecessor)"),
8656
8804
  relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
8657
8805
  issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
8658
8806
  }
8659
- }, async ({ req_id, statement, why, lives_in, relates, issue }) => {
8807
+ }, async ({ req_id, statement, why, lives_in, verified_in, relates, issue }) => {
8660
8808
  if (!ID_RE.test(req_id)) {
8661
8809
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8662
8810
  }
8663
- const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, relates, issue }, coldFreshTags(platformDir));
8811
+ const r = await supersede({
8812
+ platformDir,
8813
+ id: req_id,
8814
+ statement,
8815
+ why,
8816
+ livesIn: lives_in,
8817
+ verifiedIn: verified_in,
8818
+ relates,
8819
+ issue
8820
+ }, coldFreshTags(platformDir));
8664
8821
  if (!r.ok)
8665
8822
  return errorResult(r.detail);
8666
8823
  for (const w of r.warnings)
@@ -8711,11 +8868,12 @@ function buildMcpServer(platformDir) {
8711
8868
  text: z4.string().trim().min(1).describe("The requirement statement"),
8712
8869
  why: z4.string().trim().default("").describe("What breaks without it"),
8713
8870
  lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
8871
+ verified_in: z4.array(z4.string()).default([]).describe("verifiedIn test paths"),
8714
8872
  relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
8715
8873
  issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
8716
8874
  draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
8717
8875
  }
8718
- }, async ({ domain: domain2, text, why, lives, relates, issue, draft }) => {
8876
+ }, async ({ domain: domain2, text, why, lives, verified_in, relates, issue, draft }) => {
8719
8877
  const allocated = await nextId(platformDir, domain2);
8720
8878
  if (!allocated.ok)
8721
8879
  return errorResult(allocated.detail);
@@ -8725,6 +8883,7 @@ function buildMcpServer(platformDir) {
8725
8883
  statement: text,
8726
8884
  why,
8727
8885
  livesIn: lives,
8886
+ verifiedIn: verified_in,
8728
8887
  relates,
8729
8888
  issue: issue || undefined,
8730
8889
  status: draft ? "draft" : "active"
@@ -8743,15 +8902,16 @@ function buildMcpServer(platformDir) {
8743
8902
  text: z4.string().trim().min(1).optional().describe("New statement"),
8744
8903
  why: z4.string().trim().optional().describe("New why; empty clears it"),
8745
8904
  lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
8905
+ verified_in: z4.array(z4.string()).optional().describe("Replacement verifiedIn list; empty clears"),
8746
8906
  relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
8747
8907
  issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
8748
8908
  }
8749
- }, async ({ id, text, why, lives, relates, issue }) => {
8909
+ }, async ({ id, text, why, lives, verified_in, relates, issue }) => {
8750
8910
  if (!ID_RE.test(id))
8751
8911
  return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8752
- const fields = amendFieldsOf({ text, why, lives, relates, issue });
8912
+ const fields = amendFieldsOf({ text, why, lives, verified_in, relates, issue });
8753
8913
  if (Object.keys(fields).length === 0) {
8754
- return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, relates");
8914
+ return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, verified_in, relates");
8755
8915
  }
8756
8916
  const r = await amend({ platformDir, id, fields });
8757
8917
  if (!r.ok)
@@ -8768,15 +8928,24 @@ function buildMcpServer(platformDir) {
8768
8928
  newDomain: z4.string().min(1).describe("The existing target domain key"),
8769
8929
  text: z4.string().optional().describe("Successor statement (default: copied)"),
8770
8930
  why: z4.string().optional().describe("Successor why (default: copied)"),
8771
- lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)")
8931
+ lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)"),
8932
+ verified_in: z4.array(z4.string()).optional().describe("Successor verifiedIn list; empty clears (default: copied)")
8772
8933
  }
8773
- }, async ({ id, newDomain: newDomain2, text, why, lives }) => {
8934
+ }, async ({ id, newDomain: newDomain2, text, why, lives, verified_in }) => {
8774
8935
  if (!ID_RE.test(id))
8775
8936
  return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8776
8937
  const targetKey = normalizeDomainKey(newDomain2);
8777
8938
  if (targetKey === "")
8778
8939
  return errorResult(`newDomain must be a domain key; got ${newDomain2}`);
8779
- const r = await move({ platformDir, id, targetKey, statement: text, why, livesIn: lives }, coldFreshTags(platformDir));
8940
+ const r = await move({
8941
+ platformDir,
8942
+ id,
8943
+ targetKey,
8944
+ statement: text,
8945
+ why,
8946
+ livesIn: lives,
8947
+ verifiedIn: verified_in
8948
+ }, coldFreshTags(platformDir));
8780
8949
  if (!r.ok)
8781
8950
  return errorResult(r.detail);
8782
8951
  for (const w of r.warnings)
@@ -8880,7 +9049,7 @@ __export(exports_resolve, {
8880
9049
  resolveCommand: () => resolveCommand
8881
9050
  });
8882
9051
  import { statSync as statSync5 } from "fs";
8883
- import { join as join14, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
9052
+ import { join as join14, relative as relative3, resolve as resolve13, sep as sep4 } from "path";
8884
9053
  import { defineCommand as defineCommand21 } from "citty";
8885
9054
  function classifyArg(a) {
8886
9055
  if (a.startsWith("--")) {
@@ -9016,8 +9185,8 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
9016
9185
  function normalizeFileInputs(files, platformDir) {
9017
9186
  const normalized = [];
9018
9187
  for (const input of files) {
9019
- const rel = relative2(platformDir, resolve13(platformDir, input));
9020
- const segments = rel.split(sep3);
9188
+ const rel = relative3(platformDir, resolve13(platformDir, input));
9189
+ const segments = rel.split(sep4);
9021
9190
  if (rel === "" || rel === "." || segments[0] === "..") {
9022
9191
  console.error(`spec resolve: file path must be inside platformDir: ${input}`);
9023
9192
  process.exit(EXIT.USAGE);
@@ -9233,7 +9402,7 @@ __export(exports_gate, {
9233
9402
  import { existsSync as existsSync13 } from "fs";
9234
9403
  import { join as join15 } from "path";
9235
9404
  import { defineCommand as defineCommand22 } from "citty";
9236
- function isEpipe(e) {
9405
+ function isEpipe2(e) {
9237
9406
  if (e === null || typeof e !== "object" || !("code" in e))
9238
9407
  return false;
9239
9408
  const code = e.code;
@@ -9243,7 +9412,7 @@ function writeStdoutTolerant(line) {
9243
9412
  try {
9244
9413
  console.log(line);
9245
9414
  } catch (writeErr) {
9246
- if (!isEpipe(writeErr))
9415
+ if (!isEpipe2(writeErr))
9247
9416
  throw writeErr;
9248
9417
  }
9249
9418
  }
@@ -9268,6 +9437,7 @@ var init_gate2 = __esm(() => {
9268
9437
  init_prompt();
9269
9438
  init__index();
9270
9439
  init_gate();
9440
+ init_stdout();
9271
9441
  init__args();
9272
9442
  init__shared();
9273
9443
  gateArgs = {
@@ -9328,7 +9498,7 @@ var init_gate2 = __esm(() => {
9328
9498
  writeStdoutTolerant(`build_id: ${gated.buildId}`);
9329
9499
  }
9330
9500
  } catch (e) {
9331
- handleInvalidDomainFiles(e, Boolean(args.json));
9501
+ await handleInvalidDomainFiles(e, Boolean(args.json));
9332
9502
  const msg = e instanceof Error ? e.message : String(e);
9333
9503
  console.error(`spec gate: crashed: ${msg}`);
9334
9504
  process.exit(EXIT.USAGE);
@@ -9338,7 +9508,7 @@ var init_gate2 = __esm(() => {
9338
9508
  process.exit(EXIT.USAGE);
9339
9509
  return;
9340
9510
  }
9341
- process.exit(gated.outcome.pass ? EXIT.OK : EXIT.FAILURE);
9511
+ await exit(gated.outcome.pass ? EXIT.OK : EXIT.FAILURE);
9342
9512
  }
9343
9513
  });
9344
9514
  });
@@ -9424,6 +9594,7 @@ var guardCommand;
9424
9594
  var init_guard2 = __esm(() => {
9425
9595
  init_constants();
9426
9596
  init_guard();
9597
+ init_stdout();
9427
9598
  init__args();
9428
9599
  init__shared();
9429
9600
  guardCommand = defineCommand23({
@@ -9449,14 +9620,14 @@ var init_guard2 = __esm(() => {
9449
9620
  if (!gitRefResolves(platformDir, ref)) {
9450
9621
  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");
9451
9622
  console.log(jsonMode ? "[]" : `\u2713 spec guard: no requirements about to be lost (non-git context)`);
9452
- process.exit(EXIT.OK);
9623
+ await exit(EXIT.OK);
9453
9624
  }
9454
9625
  let losses = [];
9455
9626
  await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
9456
9627
  losses = (await guard(storage2, platformDir, ref)).losses;
9457
9628
  });
9458
9629
  console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
9459
- process.exit(losses.length > 0 ? EXIT.FAILURE : EXIT.OK);
9630
+ await exit(losses.length > 0 ? EXIT.FAILURE : EXIT.OK);
9460
9631
  }
9461
9632
  });
9462
9633
  });
@@ -11058,9 +11229,9 @@ function requirementStatusBadge(status) {
11058
11229
  const variant = status === "Active" ? "active" : status === "Superseded" ? "superseded" : status === "Draft" ? "draft" : "retired";
11059
11230
  return badge(variant, STATUS_LABEL[status]);
11060
11231
  }
11061
- function propagationStateBadge(row) {
11232
+ function propagationStateBadge(row2) {
11062
11233
  let variant;
11063
- switch (row.state) {
11234
+ switch (row2.state) {
11064
11235
  case "MIGRATED_VERIFIED":
11065
11236
  variant = "verified";
11066
11237
  break;
@@ -11073,7 +11244,7 @@ function propagationStateBadge(row) {
11073
11244
  default:
11074
11245
  variant = "unbound";
11075
11246
  }
11076
- return badge(variant, row.state);
11247
+ return badge(variant, row2.state);
11077
11248
  }
11078
11249
  function coverageGlyph(impl, verif) {
11079
11250
  const [variant, char, title] = impl && verif ? ["both", "\u2713", "src + test"] : impl ? ["impl", "S", "src only"] : verif ? ["test", "T", "test only"] : ["none", "", "none"];
@@ -11207,18 +11378,18 @@ import { html as html4, raw as raw3 } from "hono/html";
11207
11378
  function pivot(rows) {
11208
11379
  const repos = [...new Set(rows.map((r) => r.repo))].sort();
11209
11380
  const byDomain = new Map;
11210
- for (const row of rows) {
11211
- let dom = byDomain.get(row.domain_key);
11381
+ for (const row2 of rows) {
11382
+ let dom = byDomain.get(row2.domain_key);
11212
11383
  if (!dom) {
11213
11384
  dom = new Map;
11214
- byDomain.set(row.domain_key, dom);
11385
+ byDomain.set(row2.domain_key, dom);
11215
11386
  }
11216
- let req = dom.get(row.req_id);
11387
+ let req = dom.get(row2.req_id);
11217
11388
  if (!req) {
11218
- req = { id: row.req_id, status: row.req_status, cells: new Map };
11219
- dom.set(row.req_id, req);
11389
+ req = { id: row2.req_id, status: row2.req_status, cells: new Map };
11390
+ dom.set(row2.req_id, req);
11220
11391
  }
11221
- req.cells.set(row.repo, { impl: row.implemented, verif: row.verified });
11392
+ req.cells.set(row2.repo, { impl: row2.implemented, verif: row2.verified });
11222
11393
  }
11223
11394
  const domains = [...byDomain.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, reqMap]) => ({
11224
11395
  key,
@@ -11469,6 +11640,10 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11469
11640
  Lives in (optional, single path)
11470
11641
  <input type="text" name="livesIn" placeholder="path/to/impl.ts" />
11471
11642
  </label>
11643
+ <label>
11644
+ Verified in (optional, single path)
11645
+ <input type="text" name="verifiedIn" placeholder="path/to/impl.test.ts" />
11646
+ </label>
11472
11647
  <label>
11473
11648
  Relates to (optional, comma-separated ids)
11474
11649
  <input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
@@ -11507,6 +11682,7 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11507
11682
  const statement = field(form, "statement");
11508
11683
  const why = field(form, "why");
11509
11684
  const livesIn = field(form, "livesIn");
11685
+ const verifiedIn = field(form, "verifiedIn");
11510
11686
  const relates = field(form, "relates");
11511
11687
  const payload = {
11512
11688
  key: field(form, "key"),
@@ -11516,6 +11692,8 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11516
11692
  payload.why = why;
11517
11693
  if (livesIn.length > 0)
11518
11694
  payload.livesIn = livesIn;
11695
+ if (verifiedIn.length > 0)
11696
+ payload.verifiedIn = verifiedIn;
11519
11697
  if (relates.length > 0)
11520
11698
  payload.relates = relates;
11521
11699
  const res = await app.request("/api/requirements", {
@@ -11832,15 +12010,15 @@ var init_relations2 = __esm(() => {
11832
12010
  import { html as html10, raw as raw9 } from "hono/html";
11833
12011
  function coverageRollups(coverage) {
11834
12012
  const cov = new Map;
11835
- for (const row of coverage) {
11836
- if (row.req_status !== "Active")
12013
+ for (const row2 of coverage) {
12014
+ if (row2.req_status !== "Active")
11837
12015
  continue;
11838
- const c = cov.get(row.req_id) ?? { impl: 0, verif: 0 };
11839
- if (row.implemented)
12016
+ const c = cov.get(row2.req_id) ?? { impl: 0, verif: 0 };
12017
+ if (row2.implemented)
11840
12018
  c.impl += 1;
11841
- if (row.verified)
12019
+ if (row2.verified)
11842
12020
  c.verif += 1;
11843
- cov.set(row.req_id, c);
12021
+ cov.set(row2.req_id, c);
11844
12022
  }
11845
12023
  return cov;
11846
12024
  }
@@ -12187,15 +12365,15 @@ function mountSetup(app) {
12187
12365
  const memberCount = members.length;
12188
12366
  const note = readingNote(mode, memberCount);
12189
12367
  const boundByRepo = new Map;
12190
- for (const row of cov) {
12191
- if (!row.implemented)
12368
+ for (const row2 of cov) {
12369
+ if (!row2.implemented)
12192
12370
  continue;
12193
- let set = boundByRepo.get(row.repo);
12371
+ let set = boundByRepo.get(row2.repo);
12194
12372
  if (!set) {
12195
12373
  set = new Set;
12196
- boundByRepo.set(row.repo, set);
12374
+ boundByRepo.set(row2.repo, set);
12197
12375
  }
12198
- set.add(row.req_id);
12376
+ set.add(row2.req_id);
12199
12377
  }
12200
12378
  const body = html11`
12201
12379
  ${navBar("setup")}
@@ -12433,6 +12611,7 @@ function mintFieldsFromBody(body) {
12433
12611
  statement: typeof body.statement === "string" ? body.statement : "",
12434
12612
  why: typeof body.why === "string" ? body.why : "",
12435
12613
  livesIn: toStringList(body.livesIn),
12614
+ verifiedIn: toStringList(body.verifiedIn),
12436
12615
  relates: toStringList(body.relates),
12437
12616
  issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
12438
12617
  status: body.status === "draft" ? "draft" : "active"
@@ -12442,6 +12621,7 @@ function successorFieldsFromBody(body) {
12442
12621
  return {
12443
12622
  why: typeof body.why === "string" ? body.why : undefined,
12444
12623
  livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
12624
+ verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined,
12445
12625
  relates: "relates" in body ? toStringList(body.relates) : undefined,
12446
12626
  issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
12447
12627
  };
@@ -12454,6 +12634,8 @@ function amendFieldsFromBody(body) {
12454
12634
  fields.why = typeof body.why === "string" ? body.why : null;
12455
12635
  if ("livesIn" in body)
12456
12636
  fields.livesIn = toStringList(body.livesIn);
12637
+ if ("verifiedIn" in body)
12638
+ fields.verifiedIn = toStringList(body.verifiedIn);
12457
12639
  if ("relates" in body)
12458
12640
  fields.relates = toStringList(body.relates);
12459
12641
  return Object.keys(fields).length === 0 ? null : fields;
@@ -12515,8 +12697,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12515
12697
  }));
12516
12698
  app.get("/api/requirements/:id", guarded((c) => {
12517
12699
  const id = c.req.param("id") ?? "";
12518
- const { row } = getRecord(storage2, id);
12519
- return row ? c.json(row) : c.json({ error: "not found" }, 404);
12700
+ const { row: row2 } = getRecord(storage2, id);
12701
+ return row2 ? c.json(row2) : c.json({ error: "not found" }, 404);
12520
12702
  }));
12521
12703
  app.post("/api/requirements", guarded(async function createRequirement(c) {
12522
12704
  if (!featureEnabled("editor")) {
@@ -12562,7 +12744,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12562
12744
  return withWriteLock(async () => {
12563
12745
  const fields = amendFieldsFromBody(body);
12564
12746
  if (fields === null) {
12565
- return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, or relates" }, 400);
12747
+ return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, verifiedIn, or relates" }, 400);
12566
12748
  }
12567
12749
  const r = await amend({ platformDir, id, fields });
12568
12750
  if (!r.ok)
@@ -12635,7 +12817,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12635
12817
  targetKey,
12636
12818
  statement: typeof body.text === "string" ? body.text : undefined,
12637
12819
  why: typeof body.why === "string" ? body.why : undefined,
12638
- livesIn: "lives" in body ? toStringList(body.lives) : undefined
12820
+ livesIn: "lives" in body ? toStringList(body.lives) : undefined,
12821
+ verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined
12639
12822
  }, async (reqId) => {
12640
12823
  await runIndex({ platformDir, storage: storage2 });
12641
12824
  return storage2.listTags({ req_id: reqId });
@@ -12727,12 +12910,12 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12727
12910
  if (!featureEnabled("relations")) {
12728
12911
  return c.json({ error: featureDisabledMessage("relations") }, 404);
12729
12912
  }
12730
- const format = c.req.query("format");
12731
- if (format !== undefined && format !== "mermaid") {
12913
+ const format2 = c.req.query("format");
12914
+ if (format2 !== undefined && format2 !== "mermaid") {
12732
12915
  return c.json({ error: 'format must be "mermaid" when provided' }, 400);
12733
12916
  }
12734
12917
  const rows = relations(storage2).rows;
12735
- if (format === "mermaid") {
12918
+ if (format2 === "mermaid") {
12736
12919
  return c.text(renderRelations(rows, "mermaid"));
12737
12920
  }
12738
12921
  return c.json(sortRelations(rows));
@@ -12987,7 +13170,7 @@ async function runProbe() {
12987
13170
  }
12988
13171
  console.log("serve --probe OK");
12989
13172
  server.stop();
12990
- process.exit(EXIT.OK);
13173
+ await exit(EXIT.OK);
12991
13174
  }
12992
13175
  var serveCommand;
12993
13176
  var init_serve = __esm(() => {
@@ -12999,6 +13182,7 @@ var init_serve = __esm(() => {
12999
13182
  init_prompt();
13000
13183
  init_api();
13001
13184
  init_lan();
13185
+ init_stdout();
13002
13186
  init_sqlite();
13003
13187
  init__args();
13004
13188
  init__shared();
@@ -13096,7 +13280,7 @@ var init_serve = __esm(() => {
13096
13280
  });
13097
13281
  } catch (err) {
13098
13282
  storage2.close();
13099
- handleInvalidDomainFiles(err, false);
13283
+ await handleInvalidDomainFiles(err, false);
13100
13284
  handleStorageUnavailable(err, dbPath);
13101
13285
  console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
13102
13286
  process.exit(EXIT.FAILURE);
@@ -13124,7 +13308,7 @@ __export(exports_docs, {
13124
13308
  createDocsFetchHandler: () => createDocsFetchHandler
13125
13309
  });
13126
13310
  import { existsSync as existsSync15, statSync as statSync6 } from "fs";
13127
- import { join as join16, normalize, sep as sep4 } from "path";
13311
+ import { join as join16, normalize, sep as sep5 } from "path";
13128
13312
  import { fileURLToPath } from "url";
13129
13313
  import { defineCommand as defineCommand25 } from "citty";
13130
13314
  function resolveDocsRootFrom(candidates) {
@@ -13142,7 +13326,7 @@ function resolveDocsRoot() {
13142
13326
  }
13143
13327
  function createDocsFetchHandler(root) {
13144
13328
  const normalRoot = normalize(root);
13145
- const rootPrefix = normalRoot.endsWith(sep4) ? normalRoot : normalRoot + sep4;
13329
+ const rootPrefix = normalRoot.endsWith(sep5) ? normalRoot : normalRoot + sep5;
13146
13330
  return (req) => {
13147
13331
  let pathname;
13148
13332
  try {
@@ -13195,7 +13379,7 @@ async function runProbe2(root) {
13195
13379
  }
13196
13380
  console.log("docs --probe OK");
13197
13381
  server.stop();
13198
- process.exit(EXIT.OK);
13382
+ await exit(EXIT.OK);
13199
13383
  }
13200
13384
  function exitNoDocsRoot() {
13201
13385
  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.");
@@ -13204,6 +13388,7 @@ function exitNoDocsRoot() {
13204
13388
  var docsCommand;
13205
13389
  var init_docs2 = __esm(() => {
13206
13390
  init_constants();
13391
+ init_stdout();
13207
13392
  docsCommand = defineCommand25({
13208
13393
  meta: {
13209
13394
  name: "docs",
@@ -13337,7 +13522,9 @@ var init___smoke = __esm(() => {
13337
13522
 
13338
13523
  // src/cli.ts
13339
13524
  init_package();
13525
+ init_stdout();
13340
13526
  import { defineCommand as defineCommand27, runMain, showUsage } from "citty";
13527
+ routeStdout();
13341
13528
  var main = defineCommand27({
13342
13529
  meta: {
13343
13530
  name: "spec",