@spec-engine/spec-engine 0.7.2 → 0.8.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.
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.2",
23
+ version: "0.8.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",
@@ -59,11 +59,11 @@ var init_package = __esm(() => {
59
59
  prepack: "bun run build:npm"
60
60
  },
61
61
  dependencies: {
62
- "@modelcontextprotocol/sdk": "1.29.0",
62
+ "@modelcontextprotocol/sdk": "1.32.0",
63
63
  "@spec-engine/platform-map": "0.5.0",
64
64
  citty: "0.2.2",
65
65
  hono: "4.13.11",
66
- zod: "3.25.76"
66
+ zod: "4.4.3"
67
67
  },
68
68
  devDependencies: {
69
69
  "@spec-engine/shared": "workspace:*",
@@ -91,6 +91,8 @@ var init_diagnostics = __esm(() => {
91
91
  DRIFT: "DRIFT",
92
92
  NO_SPEC_CONFIG: "NO_SPEC_CONFIG",
93
93
  BROKEN_FILE_REF: "BROKEN_FILE_REF",
94
+ UNIMPLEMENTED_IN_FILE: "UNIMPLEMENTED_IN_FILE",
95
+ UNVERIFIED_IN_FILE: "UNVERIFIED_IN_FILE",
94
96
  BROKEN_RELATES: "BROKEN_RELATES",
95
97
  RELATES_SUPERSEDED: "RELATES_SUPERSEDED",
96
98
  CYCLIC_SUPERSEDE: "CYCLIC_SUPERSEDE",
@@ -213,6 +215,7 @@ function orderDomain(d) {
213
215
  supersededBy: r.supersededBy ?? null,
214
216
  relates: r.relates,
215
217
  livesIn: r.livesIn,
218
+ verifiedIn: r.verifiedIn,
216
219
  issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
217
220
  changedAtVersion: r.changedAtVersion,
218
221
  supersededAtVersion: r.supersededAtVersion,
@@ -351,6 +354,7 @@ var init_domain = __esm(() => {
351
354
  supersededBy: z.string().nullable().optional(),
352
355
  relates: z.array(z.string()).optional().default([]),
353
356
  livesIn: z.array(z.string()).optional().default([]),
357
+ verifiedIn: z.array(z.string()).optional().default([]),
354
358
  issues: z.array(SpecIssueSchema).optional().default([]),
355
359
  changedAtVersion: z.number().int().optional(),
356
360
  supersededAtVersion: z.number().int().optional(),
@@ -1106,6 +1110,12 @@ function isPathIgnored(path, extra = []) {
1106
1110
  const anchored = `/${path}`;
1107
1111
  return IGNORE_SUBSTR.some((t) => anchored.includes(`/${t}`)) || extra.some((t) => path.startsWith(t.endsWith("/") ? t : `${t}/`));
1108
1112
  }
1113
+ function isHiddenEntry(name) {
1114
+ return name.startsWith(".") && name !== ".github";
1115
+ }
1116
+ function isHiddenPath(rel) {
1117
+ return rel.split("/").some(isHiddenEntry);
1118
+ }
1109
1119
  async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
1110
1120
  const absDir = relDir ? join2(rootDir, relDir) : rootDir;
1111
1121
  let entries;
@@ -1115,7 +1125,7 @@ async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
1115
1125
  return;
1116
1126
  }
1117
1127
  for (const ent of entries) {
1118
- if (ent.name.startsWith("."))
1128
+ if (isHiddenEntry(ent.name))
1119
1129
  continue;
1120
1130
  const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
1121
1131
  if (ent.isDirectory()) {
@@ -1147,8 +1157,11 @@ async function findDomainJsonFiles(canonicalDir) {
1147
1157
  async function findCodeFiles(repoDir, exts = DEFAULT_EXTS, extraIgnore = []) {
1148
1158
  return walkFiles(repoDir, (rel) => isTagSource(rel, exts), extraIgnore);
1149
1159
  }
1160
+ function isDocSource(rel) {
1161
+ return rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md");
1162
+ }
1150
1163
  async function findDocFiles(repoDir, extraIgnore = []) {
1151
- return walkFiles(repoDir, (rel) => rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md"), extraIgnore);
1164
+ return walkFiles(repoDir, isDocSource, extraIgnore);
1152
1165
  }
1153
1166
  var IGNORE_SUBSTR, DEFAULT_EXTS;
1154
1167
  var init_fs = __esm(() => {
@@ -1179,7 +1192,10 @@ var init_fs = __esm(() => {
1179
1192
  "tf",
1180
1193
  "toml",
1181
1194
  "sql",
1182
- "css"
1195
+ "css",
1196
+ "yml",
1197
+ "yaml",
1198
+ "astro"
1183
1199
  ];
1184
1200
  });
1185
1201
 
@@ -1405,8 +1421,9 @@ function toRepos(columns) {
1405
1421
  return columns.map((c) => ({ ...c.repo, dependency_depth: depth.get(c.repo.name) ?? 0 })).sort(byName);
1406
1422
  }
1407
1423
  function selfMember(absPlatform, platformVersion) {
1424
+ const dir = basename(absPlatform);
1408
1425
  return {
1409
- name: basename(absPlatform),
1426
+ name: dir === CANONICAL_SPECS_DIR ? "root" : dir,
1410
1427
  path: absPlatform,
1411
1428
  pinned_spec_version: platformVersion,
1412
1429
  dependency_depth: 0,
@@ -1446,7 +1463,7 @@ function packageDirs(repo) {
1446
1463
  function loneRootColumn(absPlatform, repo, platformVersion) {
1447
1464
  const dirs = packageDirs(repo);
1448
1465
  const { dependency_depth: _depth, ...self } = selfMember(absPlatform, platformVersion);
1449
- if (dirs === null || self.name === CANONICAL_SPECS_DIR)
1466
+ if (dirs === null)
1450
1467
  return [];
1451
1468
  return [{ repo: { ...self, ignore: dirs }, node: repo }];
1452
1469
  }
@@ -1525,14 +1542,15 @@ async function membersOf(reading, absPlatform, platformVersion) {
1525
1542
  const undeclared = mapped.diagnostics.filter((d) => d.code === "UNLISTED_REPO").map((d) => ({ name: d.subject, path: join3(absPlatform, d.subject) }));
1526
1543
  return { mode: "multi-repo", members: toRepos(columns), unpinned, undeclared, diagnostics: diagnostics2 };
1527
1544
  }
1528
- async function discoverRepos(platformDir) {
1545
+ async function discoverRepos(platformDir, opts = {}) {
1529
1546
  const absPlatform = resolve2(platformDir);
1530
1547
  const canonicalPath = join3(absPlatform, CANONICAL_SPECS_DIR);
1531
1548
  if (!isExistingDir(canonicalPath)) {
1532
1549
  throw new NotASpecPlatformError(absPlatform);
1533
1550
  }
1534
1551
  const platformVersion = await derivePlatformVersion(absPlatform);
1535
- warnIfRetiredManifest(absPlatform, platformVersion);
1552
+ if (!opts.quiet)
1553
+ warnIfRetiredManifest(absPlatform, platformVersion);
1536
1554
  const canonical = {
1537
1555
  name: CANONICAL_SPECS_DIR,
1538
1556
  path: canonicalPath,
@@ -1862,7 +1880,14 @@ ORDER BY tc.source_file, tc.line, tc.req_id
1862
1880
  `;
1863
1881
 
1864
1882
  // 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";
1883
+ function quoteBareWords(text) {
1884
+ 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(" ");
1885
+ }
1886
+ 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";
1887
+ var init_fts = __esm(() => {
1888
+ FTS_OPERATORS = new Set(["AND", "OR", "NOT", "NEAR"]);
1889
+ FTS_SYNTAX = /["()*:^]/;
1890
+ });
1866
1891
 
1867
1892
  // src/storage/sql/propagation.ts
1868
1893
  var PROP_REPO_STATES_SQL = `
@@ -2139,7 +2164,7 @@ class SqliteStorage {
2139
2164
  }
2140
2165
  searchFts(text, limit = 10) {
2141
2166
  try {
2142
- return this.#db.query(FTS_SEARCH_SQL).all({ query: text, limit });
2167
+ return this.#db.query(FTS_SEARCH_SQL).all({ query: quoteBareWords(text), limit });
2143
2168
  } catch (e) {
2144
2169
  const msg = e instanceof Error ? e.message : String(e);
2145
2170
  if (/fts5:|syntax error/i.test(msg)) {
@@ -2433,6 +2458,7 @@ var BUSY_TIMEOUT_MS = 2000;
2433
2458
  var init_sqlite = __esm(() => {
2434
2459
  init_src();
2435
2460
  init_constants();
2461
+ init_fts();
2436
2462
  init_reads();
2437
2463
  });
2438
2464
 
@@ -2599,7 +2625,7 @@ var init_diagnostics2 = __esm(() => {
2599
2625
  });
2600
2626
 
2601
2627
  // src/indexer/pipeline.ts
2602
- import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2628
+ import { basename as basename2, dirname as dirname3, isAbsolute, join as join4, relative, resolve as resolve3, sep as sep2 } from "path";
2603
2629
  async function runIndex(opts) {
2604
2630
  const platformDir = resolve3(opts.platformDir);
2605
2631
  const {
@@ -2659,6 +2685,25 @@ function repoExtraIgnore(member) {
2659
2685
  const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
2660
2686
  return [...member.selfMember ? ["spec-engine/"] : [], ...repoIgnore];
2661
2687
  }
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}`;
2706
+ }
2662
2707
  async function scanMemberCode(member, extraIgnore) {
2663
2708
  const hitsOut = [];
2664
2709
  const codePaths = await findCodeFiles(member.path, DEFAULT_EXTS, extraIgnore);
@@ -2892,7 +2937,7 @@ var init__result = __esm(() => {
2892
2937
 
2893
2938
  // src/operations/init.ts
2894
2939
  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";
2940
+ import { basename as basename3, join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "path";
2896
2941
  import {
2897
2942
  applyInit,
2898
2943
  applyLink,
@@ -2905,7 +2950,7 @@ function errMessage(err) {
2905
2950
  return err instanceof Error ? err.message : String(err);
2906
2951
  }
2907
2952
  function hasSpecEngineSegment(path) {
2908
- return path.split(sep2).filter((s) => s.length > 0).includes(CANONICAL_SPECS_DIR);
2953
+ return path.split(sep3).filter((s) => s.length > 0).includes(CANONICAL_SPECS_DIR);
2909
2954
  }
2910
2955
  function canonicalize(repoDir) {
2911
2956
  let repoStat;
@@ -2926,7 +2971,7 @@ function canonicalize(repoDir) {
2926
2971
  function judgeSpecTree(repoDir, canonical, root) {
2927
2972
  if (!hasSpecEngineSegment(canonical))
2928
2973
  return { ok: true };
2929
- const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative(root, canonical) : canonical;
2974
+ const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative2(root, canonical) : canonical;
2930
2975
  if (hasSpecEngineSegment(judged)) {
2931
2976
  return fail("usage", `${repoDir} resolves to ${canonical}, which is inside a spec-engine/ tree \u2014 refusing to scaffold there.`);
2932
2977
  }
@@ -3567,7 +3612,7 @@ function listFlag(rawArgs, name, parsed) {
3567
3612
  }
3568
3613
  return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
3569
3614
  }
3570
- var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, relatesArg;
3615
+ var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, testsArg, relatesArg;
3571
3616
  var init__args = __esm(() => {
3572
3617
  init_constants();
3573
3618
  platformDirArg = {
@@ -3592,6 +3637,10 @@ var init__args = __esm(() => {
3592
3637
  type: "string",
3593
3638
  description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
3594
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
+ };
3595
3644
  relatesArg = {
3596
3645
  type: "string",
3597
3646
  description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
@@ -4049,67 +4098,117 @@ var init_filerefs = __esm(() => {
4049
4098
  });
4050
4099
 
4051
4100
  // 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(`
4101
+ function locateInDomain(raw, sourceFile, requirements, statuses) {
4102
+ const out = [];
4103
+ const rawLines = raw.split(`
4084
4104
  `);
4085
4105
  let lineCursor = 0;
4086
- for (const req of domain2.requirements) {
4106
+ for (const req of requirements) {
4087
4107
  const found = findRequirementIdLine(rawLines, req.id, lineCursor);
4088
4108
  if (found >= 0)
4089
4109
  lineCursor = found + 1;
4090
- if (!CHECKED_STATUSES.has(req.status.toLowerCase()))
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 {
4091
4124
  continue;
4092
- rows.push(...rowsForRequirement(platformDir, key, req.id, req.livesIn, found >= 0 ? found + 1 : 0));
4125
+ }
4126
+ const parsed = parseDomainText(raw, rel);
4127
+ if (parsed.ok)
4128
+ out.push(...locateInDomain(raw, rel, parsed.data.requirements, statuses));
4093
4129
  }
4094
- return rows;
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
+ };
4095
4145
  }
4096
4146
  async function brokenFileRefDiagnostics(platformDir) {
4097
4147
  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));
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}`));
4102
4176
  }
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
4177
  return rows;
4105
4178
  }
4106
- var CHECKED_STATUSES;
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);
4201
+ }
4202
+ var FILE_REF_STATUSES, IN_FILE_STATUSES, refOf = (entry) => entry.startsWith("@") ? entry.slice(1) : entry;
4107
4203
  var init_filerefs2 = __esm(() => {
4108
4204
  init_src();
4109
4205
  init_domains();
4110
4206
  init_filerefs();
4111
4207
  init_constants();
4112
- CHECKED_STATUSES = new Set(["active", "draft"]);
4208
+ init_discover();
4209
+ init_pipeline();
4210
+ FILE_REF_STATUSES = new Set(["active", "draft"]);
4211
+ IN_FILE_STATUSES = new Set(["active"]);
4113
4212
  });
4114
4213
 
4115
4214
  // src/check/grammar.ts
@@ -4130,7 +4229,7 @@ async function readDeclaredDomain(platformDir, key) {
4130
4229
  requirements: parsed.data.requirements
4131
4230
  };
4132
4231
  }
4133
- function rowsForDomain2(key, domain2) {
4232
+ function rowsForDomain(key, domain2) {
4134
4233
  const rows = [];
4135
4234
  const rawLines = domain2.raw.split(`
4136
4235
  `);
@@ -4139,7 +4238,7 @@ function rowsForDomain2(key, domain2) {
4139
4238
  const found = findRequirementIdLine(rawLines, req.id, lineCursor);
4140
4239
  if (found >= 0)
4141
4240
  lineCursor = found + 1;
4142
- if (!CHECKED_STATUSES2.has(req.status.toLowerCase()))
4241
+ if (!CHECKED_STATUSES.has(req.status.toLowerCase()))
4143
4242
  continue;
4144
4243
  const result = parseEarsStatement(req.statement);
4145
4244
  if (result.ok)
@@ -4163,17 +4262,17 @@ async function statementGrammarDiagnostics(platformDir) {
4163
4262
  continue;
4164
4263
  const domain2 = await readDeclaredDomain(platformDir, key);
4165
4264
  if (domain2 !== null)
4166
- rows.push(...rowsForDomain2(key, domain2));
4265
+ rows.push(...rowsForDomain(key, domain2));
4167
4266
  }
4168
4267
  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
4268
  return rows;
4170
4269
  }
4171
- var CHECKED_STATUSES2;
4270
+ var CHECKED_STATUSES;
4172
4271
  var init_grammar = __esm(() => {
4173
4272
  init_src();
4174
4273
  init_domains();
4175
4274
  init_constants();
4176
- CHECKED_STATUSES2 = new Set(["active", "draft"]);
4275
+ CHECKED_STATUSES = new Set(["active", "draft"]);
4177
4276
  });
4178
4277
 
4179
4278
  // src/check/propagation-teeth.ts
@@ -4250,12 +4349,12 @@ function readCheckConfig(platformDir) {
4250
4349
  return { ok: true, config: { severity: [] } };
4251
4350
  return parseCheckConfigText(readFileSync(path, "utf-8"), CHECK_CONFIG_REL);
4252
4351
  }
4253
- function specificity(rule, row) {
4254
- if (rule.code !== row.code)
4352
+ function specificity(rule, row2) {
4353
+ if (rule.code !== row2.code)
4255
4354
  return Specificity.none;
4256
4355
  if (rule.match === undefined)
4257
4356
  return Specificity.code;
4258
- const id = row.req_id;
4357
+ const id = row2.req_id;
4259
4358
  if (id === null)
4260
4359
  return Specificity.none;
4261
4360
  if (rule.match.includes(id))
@@ -4265,20 +4364,20 @@ function specificity(rule, row) {
4265
4364
  }
4266
4365
  function applySeverity(rows, rules) {
4267
4366
  const out = [];
4268
- for (const row of rows) {
4367
+ for (const row2 of rows) {
4269
4368
  let best;
4270
4369
  let bestScore = Specificity.none;
4271
4370
  for (const rule of rules) {
4272
- const score = specificity(rule, row);
4371
+ const score = specificity(rule, row2);
4273
4372
  if (score !== Specificity.none && score >= bestScore) {
4274
4373
  best = rule;
4275
4374
  bestScore = score;
4276
4375
  }
4277
4376
  }
4278
4377
  if (best === undefined)
4279
- out.push(row);
4378
+ out.push(row2);
4280
4379
  else if (best.severity !== "off")
4281
- out.push({ ...row, severity: best.severity });
4380
+ out.push({ ...row2, severity: best.severity });
4282
4381
  }
4283
4382
  return out;
4284
4383
  }
@@ -4797,6 +4896,7 @@ function successorEntry(id, pred, fields) {
4797
4896
  supersededBy: null,
4798
4897
  relates: fields.relates,
4799
4898
  livesIn: fields.livesIn,
4899
+ verifiedIn: fields.verifiedIn,
4800
4900
  issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
4801
4901
  aliases: [...pred.aliases],
4802
4902
  cites: pred.cites.map((c) => ({ ...c }))
@@ -4884,7 +4984,8 @@ async function mint(input) {
4884
4984
  const warnings = unresolvableRefWarnings(platformDir, [
4885
4985
  input.statement,
4886
4986
  input.why,
4887
- ...input.livesIn
4987
+ ...input.livesIn,
4988
+ ...input.verifiedIn ?? []
4888
4989
  ]);
4889
4990
  const verdict = await judgeStatementGrammar(platformDir, key, input.statement);
4890
4991
  if (!verdict.ok) {
@@ -4908,6 +5009,7 @@ async function mint(input) {
4908
5009
  supersededBy: null,
4909
5010
  relates: splitList(input.relates ?? []),
4910
5011
  livesIn: input.livesIn,
5012
+ verifiedIn: input.verifiedIn ?? [],
4911
5013
  issues: input.issue ? [{ role: "created", id: input.issue }] : [],
4912
5014
  aliases: [],
4913
5015
  cites: input.cites ?? []
@@ -4965,6 +5067,7 @@ async function mintTerm(input) {
4965
5067
  supersededBy: null,
4966
5068
  relates: [],
4967
5069
  livesIn: [],
5070
+ verifiedIn: [],
4968
5071
  issues: [],
4969
5072
  aliases: input.aliases,
4970
5073
  cites: [],
@@ -5151,6 +5254,7 @@ async function migrateGlossary(platformDir) {
5151
5254
  supersededBy: null,
5152
5255
  relates: [],
5153
5256
  livesIn: [],
5257
+ verifiedIn: [],
5154
5258
  issues: [],
5155
5259
  aliases: [],
5156
5260
  cites: [],
@@ -5508,6 +5612,7 @@ async function check(input, storage2) {
5508
5612
  }
5509
5613
  diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
5510
5614
  diagnostics2.push(...await brokenFileRefDiagnostics(platformDir));
5615
+ diagnostics2.push(...await inFileTagDiagnostics(platformDir, storage2.listTags()));
5511
5616
  diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
5512
5617
  const glossaryDrift = glossaryDriftDiagnostic(platformDir);
5513
5618
  if (glossaryDrift !== null)
@@ -5935,12 +6040,12 @@ function sortMatrix(rows) {
5935
6040
  return a.repo.localeCompare(b.repo);
5936
6041
  });
5937
6042
  }
5938
- function cellStatus(row) {
5939
- if (row.implemented === 1 && row.verified === 1)
6043
+ function cellStatus(row2) {
6044
+ if (row2.implemented === 1 && row2.verified === 1)
5940
6045
  return "src+test";
5941
- if (row.implemented === 1)
6046
+ if (row2.implemented === 1)
5942
6047
  return "src";
5943
- if (row.verified === 1)
6048
+ if (row2.verified === 1)
5944
6049
  return "test";
5945
6050
  return EMPTY_CELL;
5946
6051
  }
@@ -5953,18 +6058,18 @@ function renderMatrix(rows, mode) {
5953
6058
  return "";
5954
6059
  const repos = Array.from(new Set(sorted.map((r) => r.repo))).sort((a, b) => a.localeCompare(b));
5955
6060
  const byReq = new Map;
5956
- for (const row of sorted) {
5957
- let entry = byReq.get(row.req_id);
6061
+ for (const row2 of sorted) {
6062
+ let entry = byReq.get(row2.req_id);
5958
6063
  if (!entry) {
5959
6064
  entry = {
5960
- domain_key: row.domain_key,
5961
- req_id: row.req_id,
5962
- req_status: row.req_status,
6065
+ domain_key: row2.domain_key,
6066
+ req_id: row2.req_id,
6067
+ req_status: row2.req_status,
5963
6068
  cells: new Map
5964
6069
  };
5965
- byReq.set(row.req_id, entry);
6070
+ byReq.set(row2.req_id, entry);
5966
6071
  }
5967
- entry.cells.set(row.repo, cellStatus(row));
6072
+ entry.cells.set(row2.repo, cellStatus(row2));
5968
6073
  }
5969
6074
  const header = ["DOMAIN", "REQUIREMENT", "STATUS", ...repos];
5970
6075
  const lines = [header];
@@ -5972,8 +6077,8 @@ function renderMatrix(rows, mode) {
5972
6077
  const cells = repos.map((r) => entry.cells.get(r) ?? EMPTY_CELL);
5973
6078
  lines.push([entry.domain_key, entry.req_id, entry.req_status, ...cells]);
5974
6079
  }
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(`
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(`
5977
6082
  `);
5978
6083
  }
5979
6084
  var EMPTY_CELL = "\u2014";
@@ -6109,11 +6214,11 @@ var init_map = __esm(() => {
6109
6214
 
6110
6215
  // src/operations/records.ts
6111
6216
  function getRecord(storage2, id) {
6112
- const row = storage2.getRequirement(id);
6217
+ const row2 = storage2.getRequirement(id);
6113
6218
  return {
6114
6219
  ok: true,
6115
- row,
6116
- platformEmpty: row === null && storage2.listRequirements().length === 0
6220
+ row: row2,
6221
+ platformEmpty: row2 === null && storage2.listRequirements().length === 0
6117
6222
  };
6118
6223
  }
6119
6224
  function listRecords(storage2, filter = {}) {
@@ -6150,8 +6255,8 @@ function renderRecords(rows, mode) {
6150
6255
  lines.push([r.id, r.status, r.key, String(r.seq), String(r.changed_at_version), r.text]);
6151
6256
  }
6152
6257
  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(`
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(`
6155
6260
  `);
6156
6261
  }
6157
6262
  var HEADER;
@@ -6303,11 +6408,11 @@ function renderPropagation(rows, mode) {
6303
6408
  return "";
6304
6409
  const header = ["REPO", "STATE", "VIA", "DRIFT?"];
6305
6410
  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"]);
6411
+ for (const row2 of rows) {
6412
+ lines.push([row2.repo, row2.state, row2.via_req_id ?? EMPTY_CELL2, row2.drifted ? "yes" : "no"]);
6308
6413
  }
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(`
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(`
6311
6416
  `);
6312
6417
  }
6313
6418
  var EMPTY_CELL2 = "\u2014";
@@ -6381,8 +6486,8 @@ function excerptOf(raw) {
6381
6486
  }
6382
6487
  function renderTable(header, rows) {
6383
6488
  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(`
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(`
6386
6491
  `);
6387
6492
  }
6388
6493
  function renderQuery(hits, mode) {
@@ -6637,17 +6742,17 @@ function compareReqIds2(a, b) {
6637
6742
  function sortProvenance(rows) {
6638
6743
  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
6744
  }
6640
- function backingTests(row) {
6745
+ function backingTests(row2) {
6641
6746
  let base;
6642
- if (row.implemented === 1 && row.verified === 1)
6747
+ if (row2.implemented === 1 && row2.verified === 1)
6643
6748
  base = "src+test";
6644
- else if (row.implemented === 1)
6749
+ else if (row2.implemented === 1)
6645
6750
  base = "src";
6646
- else if (row.verified === 1)
6751
+ else if (row2.verified === 1)
6647
6752
  base = "test";
6648
6753
  else
6649
6754
  base = "\u2014";
6650
- return row.test_levels ? `${base} (${row.test_levels})` : base;
6755
+ return row2.test_levels ? `${base} (${row2.test_levels})` : base;
6651
6756
  }
6652
6757
  function renderProvenance(rows, mode) {
6653
6758
  const sorted = sortProvenance(rows);
@@ -6657,12 +6762,12 @@ function renderProvenance(rows, mode) {
6657
6762
  if (sorted.length === 0)
6658
6763
  return "";
6659
6764
  const byReq = new Map;
6660
- for (const row of sorted) {
6661
- const bucket = byReq.get(row.req_id);
6765
+ for (const row2 of sorted) {
6766
+ const bucket = byReq.get(row2.req_id);
6662
6767
  if (bucket)
6663
- bucket.push(row);
6768
+ bucket.push(row2);
6664
6769
  else
6665
- byReq.set(row.req_id, [row]);
6770
+ byReq.set(row2.req_id, [row2]);
6666
6771
  }
6667
6772
  const lines = [];
6668
6773
  for (const [reqId, links] of byReq) {
@@ -6677,8 +6782,8 @@ function renderProvenance(rows, mode) {
6677
6782
  return lines.join(`
6678
6783
  `);
6679
6784
  }
6680
- function decorateRow(row, resolved) {
6681
- const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
6785
+ function decorateRow(row2, resolved) {
6786
+ const prefix = ` ${row2.role} ${row2.issue_id} ${row2.source_file}:${row2.line}`;
6682
6787
  if (resolved?.ok && resolved.title && resolved.url) {
6683
6788
  const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
6684
6789
  return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
@@ -6702,12 +6807,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
6702
6807
  if (sorted.length === 0)
6703
6808
  return "";
6704
6809
  const byReq = new Map;
6705
- for (const row of sorted) {
6706
- const bucket = byReq.get(row.req_id);
6810
+ for (const row2 of sorted) {
6811
+ const bucket = byReq.get(row2.req_id);
6707
6812
  if (bucket)
6708
- bucket.push(row);
6813
+ bucket.push(row2);
6709
6814
  else
6710
- byReq.set(row.req_id, [row]);
6815
+ byReq.set(row2.req_id, [row2]);
6711
6816
  }
6712
6817
  const lines = [];
6713
6818
  for (const [reqId, links] of byReq) {
@@ -7074,6 +7179,7 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
7074
7179
  statement,
7075
7180
  why: (args.why ?? "").trim(),
7076
7181
  livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
7182
+ verifiedIn: listFlag(rawArgs, "tests", args.tests) ?? [],
7077
7183
  relates: listFlag(rawArgs, "relates", args.relates) ?? [],
7078
7184
  issue: (args.issue ?? "").trim() || undefined,
7079
7185
  status: statusOf(args),
@@ -7153,6 +7259,7 @@ var init_req = __esm(() => {
7153
7259
  },
7154
7260
  why: { type: "string", description: "Why it matters field (only with --text)" },
7155
7261
  lives: livesArg,
7262
+ tests: testsArg,
7156
7263
  relates: relatesArg,
7157
7264
  issue: {
7158
7265
  type: "string",
@@ -7186,9 +7293,9 @@ var init_req = __esm(() => {
7186
7293
  await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
7187
7294
  return;
7188
7295
  }
7189
- const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.relates === "string" || typeof args.issue === "string";
7296
+ const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.tests === "string" || typeof args.relates === "string" || typeof args.issue === "string";
7190
7297
  if (hasFieldFlags) {
7191
- console.error("spec req: --why/--lives/--relates/--issue require --text (the Requirement field is mandatory)");
7298
+ console.error("spec req: --why/--lives/--tests/--relates/--issue require --text (the Requirement field is mandatory)");
7192
7299
  process.exit(EXIT.USAGE);
7193
7300
  return;
7194
7301
  }
@@ -7276,8 +7383,8 @@ async function listTermsCommand(platformDirRaw, json) {
7276
7383
  console.log(JSON.stringify(result.rows));
7277
7384
  return;
7278
7385
  }
7279
- for (const row of result.rows) {
7280
- console.log(`${row.id} ${row.term} ${row.status}`);
7386
+ for (const row2 of result.rows) {
7387
+ console.log(`${row2.id} ${row2.term} ${row2.status}`);
7281
7388
  }
7282
7389
  }
7283
7390
  async function reviseTermCommand(opts) {
@@ -7602,6 +7709,7 @@ function successorFields(input, req) {
7602
7709
  return {
7603
7710
  why: (input.why ?? req.why ?? "").trim(),
7604
7711
  livesIn: input.livesIn ?? [...req.livesIn],
7712
+ verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
7605
7713
  relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
7606
7714
  };
7607
7715
  }
@@ -7646,14 +7754,15 @@ async function supersede(input, freshTags) {
7646
7754
  }
7647
7755
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
7648
7756
  }
7649
- const { why, livesIn, relates } = successorFields(input, req);
7650
- warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn]));
7757
+ const { why, livesIn, verifiedIn, relates } = successorFields(input, req);
7758
+ warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn, ...verifiedIn]));
7651
7759
  const newId = await nextRequirementId(platformDir, key);
7652
7760
  const issue = input.issue?.trim() || undefined;
7653
7761
  const successor = successorEntry(newId, req, {
7654
7762
  statement: input.statement,
7655
7763
  why,
7656
7764
  livesIn,
7765
+ verifiedIn,
7657
7766
  relates,
7658
7767
  issue
7659
7768
  });
@@ -7697,14 +7806,14 @@ function renderResolve(rows, mode) {
7697
7806
  return "";
7698
7807
  const header = ["REQ_ID", "STATUS", "TEXT"];
7699
7808
  const lines = [header];
7700
- for (const row of sorted) {
7701
- const raw = row.text ?? "";
7809
+ for (const row2 of sorted) {
7810
+ const raw = row2.text ?? "";
7702
7811
  const truncated = raw.length > TEXT_MAX ? raw.slice(0, TEXT_MAX) + ELLIPSIS2 : raw;
7703
7812
  const text = truncated.replace(/\s+/g, " ").trim();
7704
- lines.push([row.id, row.status, text || EMPTY_CELL4]);
7813
+ lines.push([row2.id, row2.status, text || EMPTY_CELL4]);
7705
7814
  }
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(`
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(`
7708
7817
  `);
7709
7818
  }
7710
7819
  function sortReqTags(rows) {
@@ -7719,8 +7828,8 @@ function renderReqTags(rows, mode) {
7719
7828
  return "";
7720
7829
  const header = ["REPO", "FILE", "LINE", "KIND", "LEVEL"];
7721
7830
  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]);
7831
+ for (const row2 of sorted) {
7832
+ lines.push([row2.repo, row2.file, String(row2.line), row2.kind, row2.level ?? EMPTY_CELL4]);
7724
7833
  }
7725
7834
  const widths = header.map((_, col) => Math.max(...lines.map((l) => (l[col] ?? "").length)));
7726
7835
  return lines.map((l) => l.map((cell, col) => cell.padEnd(widths[col])).join(" ").trimEnd()).join(`
@@ -7767,6 +7876,9 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
7767
7876
  const livesIn = listFlag(rawArgs, "lives", args.lives);
7768
7877
  if (livesIn !== undefined)
7769
7878
  input.livesIn = livesIn;
7879
+ const verifiedIn = listFlag(rawArgs, "tests", args.tests);
7880
+ if (verifiedIn !== undefined)
7881
+ input.verifiedIn = verifiedIn;
7770
7882
  const relates = listFlag(rawArgs, "relates", args.relates);
7771
7883
  if (relates !== undefined)
7772
7884
  input.relates = relates;
@@ -7807,6 +7919,10 @@ var init_supersede2 = __esm(() => {
7807
7919
  type: "string",
7808
7920
  description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
7809
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
+ },
7810
7926
  relates: {
7811
7927
  type: "string",
7812
7928
  description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
@@ -8000,6 +8116,7 @@ function successorFields2(input, req) {
8000
8116
  statement: (input.statement ?? req.statement).trim(),
8001
8117
  why: (input.why ?? req.why ?? "").trim(),
8002
8118
  livesIn: input.livesIn ?? [...req.livesIn],
8119
+ verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
8003
8120
  relates: [...req.relates]
8004
8121
  };
8005
8122
  }
@@ -8044,7 +8161,12 @@ async function move(input, freshTags) {
8044
8161
  warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
8045
8162
  }
8046
8163
  }
8047
- warnings.push(...unresolvableRefWarnings(platformDir, [fields.statement, fields.why, ...fields.livesIn]));
8164
+ warnings.push(...unresolvableRefWarnings(platformDir, [
8165
+ fields.statement,
8166
+ fields.why,
8167
+ ...fields.livesIn,
8168
+ ...fields.verifiedIn
8169
+ ]));
8048
8170
  const newId = await nextRequirementId(platformDir, targetKey);
8049
8171
  const sourceCurrent = source.domain.specVersion ?? 1;
8050
8172
  req.status = "superseded";
@@ -8103,6 +8225,9 @@ function inputFromArgs2(args, rawArgs, platformDir, id, targetKey) {
8103
8225
  const livesIn = listFlag(rawArgs, "lives", args.lives);
8104
8226
  if (livesIn !== undefined)
8105
8227
  input.livesIn = livesIn;
8228
+ const verifiedIn = listFlag(rawArgs, "tests", args.tests);
8229
+ if (verifiedIn !== undefined)
8230
+ input.verifiedIn = verifiedIn;
8106
8231
  return input;
8107
8232
  }
8108
8233
  var moveArgs, moveCommand;
@@ -8138,6 +8263,10 @@ var init_move2 = __esm(() => {
8138
8263
  type: "string",
8139
8264
  description: "Rewrite the successor's Lives in: repeat or comma-separate; empty clears (default: copied from the source)"
8140
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
+ },
8141
8270
  noBump: {
8142
8271
  type: "boolean",
8143
8272
  description: "Do not bump either envelope's specVersion"
@@ -8228,7 +8357,7 @@ function gateRejection(platformDir, relFile, req, fields) {
8228
8357
  const tag = releaseHolding(platformDir, relFile, id);
8229
8358
  if (tag === null)
8230
8359
  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.");
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.");
8232
8361
  }
8233
8362
  function applyFields(req, fields) {
8234
8363
  const fieldsChanged = [];
@@ -8248,6 +8377,11 @@ function applyFields(req, fields) {
8248
8377
  fieldsChanged.push("lives");
8249
8378
  refValues.push(...fields.livesIn);
8250
8379
  }
8380
+ if (fields.verifiedIn !== undefined) {
8381
+ req.verifiedIn = fields.verifiedIn;
8382
+ fieldsChanged.push("tests");
8383
+ refValues.push(...fields.verifiedIn);
8384
+ }
8251
8385
  if (fields.relates !== undefined) {
8252
8386
  req.relates = splitList(fields.relates);
8253
8387
  fieldsChanged.push("relates");
@@ -8317,9 +8451,9 @@ __export(exports_amend, {
8317
8451
  import { defineCommand as defineCommand18 } from "citty";
8318
8452
  function assertAmendFlags(args) {
8319
8453
  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;
8454
+ const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.tests !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
8321
8455
  if (!hasField && !hasIssue) {
8322
- console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --relates / --term / --aliases");
8456
+ console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --tests / --relates / --term / --aliases");
8323
8457
  process.exit(EXIT.USAGE);
8324
8458
  }
8325
8459
  if (args.text !== undefined && args.text.trim() === "") {
@@ -8346,6 +8480,9 @@ function fieldsFromArgs(args, rawArgs) {
8346
8480
  const livesIn = listFlag(rawArgs, "lives", args.lives);
8347
8481
  if (livesIn !== undefined)
8348
8482
  fields.livesIn = livesIn;
8483
+ const verifiedIn = listFlag(rawArgs, "tests", args.tests);
8484
+ if (verifiedIn !== undefined)
8485
+ fields.verifiedIn = verifiedIn;
8349
8486
  const relates = listFlag(rawArgs, "relates", args.relates);
8350
8487
  if (relates !== undefined)
8351
8488
  fields.relates = relates;
@@ -8375,6 +8512,7 @@ var init_amend2 = __esm(() => {
8375
8512
  text: { type: "string", description: "New Requirement (statement) field value" },
8376
8513
  why: { type: "string", description: "New Why it matters field value" },
8377
8514
  lives: livesArg,
8515
+ tests: testsArg,
8378
8516
  relates: relatesArg,
8379
8517
  issue: {
8380
8518
  type: "string",
@@ -8390,7 +8528,7 @@ var init_amend2 = __esm(() => {
8390
8528
  amendCommand = defineCommand18({
8391
8529
  meta: {
8392
8530
  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."
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."
8394
8532
  },
8395
8533
  args: amendArgs,
8396
8534
  async run({ args, rawArgs }) {
@@ -8544,6 +8682,8 @@ function amendFieldsOf(args) {
8544
8682
  fields.why = args.why || null;
8545
8683
  if (args.lives !== undefined)
8546
8684
  fields.livesIn = args.lives;
8685
+ if (args.verified_in !== undefined)
8686
+ fields.verifiedIn = args.verified_in;
8547
8687
  if (args.relates !== undefined)
8548
8688
  fields.relates = args.relates;
8549
8689
  if (args.issue)
@@ -8621,8 +8761,8 @@ function buildMcpServer(platformDir) {
8621
8761
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8622
8762
  }
8623
8763
  return fresh((s) => {
8624
- const { row } = getRecord(s, req_id);
8625
- return jsonResult(row ?? []);
8764
+ const { row: row2 } = getRecord(s, req_id);
8765
+ return jsonResult(row2 ?? []);
8626
8766
  });
8627
8767
  });
8628
8768
  server.registerTool("spec_list", {
@@ -8653,14 +8793,24 @@ function buildMcpServer(platformDir) {
8653
8793
  statement: z4.string().min(1).describe("The successor's statement"),
8654
8794
  why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
8655
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)"),
8656
8797
  relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
8657
8798
  issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
8658
8799
  }
8659
- }, async ({ req_id, statement, why, lives_in, relates, issue }) => {
8800
+ }, async ({ req_id, statement, why, lives_in, verified_in, relates, issue }) => {
8660
8801
  if (!ID_RE.test(req_id)) {
8661
8802
  return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
8662
8803
  }
8663
- const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, relates, issue }, coldFreshTags(platformDir));
8804
+ const r = await supersede({
8805
+ platformDir,
8806
+ id: req_id,
8807
+ statement,
8808
+ why,
8809
+ livesIn: lives_in,
8810
+ verifiedIn: verified_in,
8811
+ relates,
8812
+ issue
8813
+ }, coldFreshTags(platformDir));
8664
8814
  if (!r.ok)
8665
8815
  return errorResult(r.detail);
8666
8816
  for (const w of r.warnings)
@@ -8711,11 +8861,12 @@ function buildMcpServer(platformDir) {
8711
8861
  text: z4.string().trim().min(1).describe("The requirement statement"),
8712
8862
  why: z4.string().trim().default("").describe("What breaks without it"),
8713
8863
  lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
8864
+ verified_in: z4.array(z4.string()).default([]).describe("verifiedIn test paths"),
8714
8865
  relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
8715
8866
  issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
8716
8867
  draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
8717
8868
  }
8718
- }, async ({ domain: domain2, text, why, lives, relates, issue, draft }) => {
8869
+ }, async ({ domain: domain2, text, why, lives, verified_in, relates, issue, draft }) => {
8719
8870
  const allocated = await nextId(platformDir, domain2);
8720
8871
  if (!allocated.ok)
8721
8872
  return errorResult(allocated.detail);
@@ -8725,6 +8876,7 @@ function buildMcpServer(platformDir) {
8725
8876
  statement: text,
8726
8877
  why,
8727
8878
  livesIn: lives,
8879
+ verifiedIn: verified_in,
8728
8880
  relates,
8729
8881
  issue: issue || undefined,
8730
8882
  status: draft ? "draft" : "active"
@@ -8743,15 +8895,16 @@ function buildMcpServer(platformDir) {
8743
8895
  text: z4.string().trim().min(1).optional().describe("New statement"),
8744
8896
  why: z4.string().trim().optional().describe("New why; empty clears it"),
8745
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"),
8746
8899
  relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
8747
8900
  issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
8748
8901
  }
8749
- }, async ({ id, text, why, lives, relates, issue }) => {
8902
+ }, async ({ id, text, why, lives, verified_in, relates, issue }) => {
8750
8903
  if (!ID_RE.test(id))
8751
8904
  return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8752
- const fields = amendFieldsOf({ text, why, lives, relates, issue });
8905
+ const fields = amendFieldsOf({ text, why, lives, verified_in, relates, issue });
8753
8906
  if (Object.keys(fields).length === 0) {
8754
- return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, relates");
8907
+ return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, verified_in, relates");
8755
8908
  }
8756
8909
  const r = await amend({ platformDir, id, fields });
8757
8910
  if (!r.ok)
@@ -8768,15 +8921,24 @@ function buildMcpServer(platformDir) {
8768
8921
  newDomain: z4.string().min(1).describe("The existing target domain key"),
8769
8922
  text: z4.string().optional().describe("Successor statement (default: copied)"),
8770
8923
  why: z4.string().optional().describe("Successor why (default: copied)"),
8771
- lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (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)")
8772
8926
  }
8773
- }, async ({ id, newDomain: newDomain2, text, why, lives }) => {
8927
+ }, async ({ id, newDomain: newDomain2, text, why, lives, verified_in }) => {
8774
8928
  if (!ID_RE.test(id))
8775
8929
  return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8776
8930
  const targetKey = normalizeDomainKey(newDomain2);
8777
8931
  if (targetKey === "")
8778
8932
  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));
8933
+ const r = await move({
8934
+ platformDir,
8935
+ id,
8936
+ targetKey,
8937
+ statement: text,
8938
+ why,
8939
+ livesIn: lives,
8940
+ verifiedIn: verified_in
8941
+ }, coldFreshTags(platformDir));
8780
8942
  if (!r.ok)
8781
8943
  return errorResult(r.detail);
8782
8944
  for (const w of r.warnings)
@@ -8880,7 +9042,7 @@ __export(exports_resolve, {
8880
9042
  resolveCommand: () => resolveCommand
8881
9043
  });
8882
9044
  import { statSync as statSync5 } from "fs";
8883
- import { join as join14, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
9045
+ import { join as join14, relative as relative3, resolve as resolve13, sep as sep4 } from "path";
8884
9046
  import { defineCommand as defineCommand21 } from "citty";
8885
9047
  function classifyArg(a) {
8886
9048
  if (a.startsWith("--")) {
@@ -9016,8 +9178,8 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
9016
9178
  function normalizeFileInputs(files, platformDir) {
9017
9179
  const normalized = [];
9018
9180
  for (const input of files) {
9019
- const rel = relative2(platformDir, resolve13(platformDir, input));
9020
- const segments = rel.split(sep3);
9181
+ const rel = relative3(platformDir, resolve13(platformDir, input));
9182
+ const segments = rel.split(sep4);
9021
9183
  if (rel === "" || rel === "." || segments[0] === "..") {
9022
9184
  console.error(`spec resolve: file path must be inside platformDir: ${input}`);
9023
9185
  process.exit(EXIT.USAGE);
@@ -11058,9 +11220,9 @@ function requirementStatusBadge(status) {
11058
11220
  const variant = status === "Active" ? "active" : status === "Superseded" ? "superseded" : status === "Draft" ? "draft" : "retired";
11059
11221
  return badge(variant, STATUS_LABEL[status]);
11060
11222
  }
11061
- function propagationStateBadge(row) {
11223
+ function propagationStateBadge(row2) {
11062
11224
  let variant;
11063
- switch (row.state) {
11225
+ switch (row2.state) {
11064
11226
  case "MIGRATED_VERIFIED":
11065
11227
  variant = "verified";
11066
11228
  break;
@@ -11073,7 +11235,7 @@ function propagationStateBadge(row) {
11073
11235
  default:
11074
11236
  variant = "unbound";
11075
11237
  }
11076
- return badge(variant, row.state);
11238
+ return badge(variant, row2.state);
11077
11239
  }
11078
11240
  function coverageGlyph(impl, verif) {
11079
11241
  const [variant, char, title] = impl && verif ? ["both", "\u2713", "src + test"] : impl ? ["impl", "S", "src only"] : verif ? ["test", "T", "test only"] : ["none", "", "none"];
@@ -11207,18 +11369,18 @@ import { html as html4, raw as raw3 } from "hono/html";
11207
11369
  function pivot(rows) {
11208
11370
  const repos = [...new Set(rows.map((r) => r.repo))].sort();
11209
11371
  const byDomain = new Map;
11210
- for (const row of rows) {
11211
- let dom = byDomain.get(row.domain_key);
11372
+ for (const row2 of rows) {
11373
+ let dom = byDomain.get(row2.domain_key);
11212
11374
  if (!dom) {
11213
11375
  dom = new Map;
11214
- byDomain.set(row.domain_key, dom);
11376
+ byDomain.set(row2.domain_key, dom);
11215
11377
  }
11216
- let req = dom.get(row.req_id);
11378
+ let req = dom.get(row2.req_id);
11217
11379
  if (!req) {
11218
- req = { id: row.req_id, status: row.req_status, cells: new Map };
11219
- dom.set(row.req_id, req);
11380
+ req = { id: row2.req_id, status: row2.req_status, cells: new Map };
11381
+ dom.set(row2.req_id, req);
11220
11382
  }
11221
- req.cells.set(row.repo, { impl: row.implemented, verif: row.verified });
11383
+ req.cells.set(row2.repo, { impl: row2.implemented, verif: row2.verified });
11222
11384
  }
11223
11385
  const domains = [...byDomain.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, reqMap]) => ({
11224
11386
  key,
@@ -11469,6 +11631,10 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11469
11631
  Lives in (optional, single path)
11470
11632
  <input type="text" name="livesIn" placeholder="path/to/impl.ts" />
11471
11633
  </label>
11634
+ <label>
11635
+ Verified in (optional, single path)
11636
+ <input type="text" name="verifiedIn" placeholder="path/to/impl.test.ts" />
11637
+ </label>
11472
11638
  <label>
11473
11639
  Relates to (optional, comma-separated ids)
11474
11640
  <input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
@@ -11507,6 +11673,7 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11507
11673
  const statement = field(form, "statement");
11508
11674
  const why = field(form, "why");
11509
11675
  const livesIn = field(form, "livesIn");
11676
+ const verifiedIn = field(form, "verifiedIn");
11510
11677
  const relates = field(form, "relates");
11511
11678
  const payload = {
11512
11679
  key: field(form, "key"),
@@ -11516,6 +11683,8 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
11516
11683
  payload.why = why;
11517
11684
  if (livesIn.length > 0)
11518
11685
  payload.livesIn = livesIn;
11686
+ if (verifiedIn.length > 0)
11687
+ payload.verifiedIn = verifiedIn;
11519
11688
  if (relates.length > 0)
11520
11689
  payload.relates = relates;
11521
11690
  const res = await app.request("/api/requirements", {
@@ -11832,15 +12001,15 @@ var init_relations2 = __esm(() => {
11832
12001
  import { html as html10, raw as raw9 } from "hono/html";
11833
12002
  function coverageRollups(coverage) {
11834
12003
  const cov = new Map;
11835
- for (const row of coverage) {
11836
- if (row.req_status !== "Active")
12004
+ for (const row2 of coverage) {
12005
+ if (row2.req_status !== "Active")
11837
12006
  continue;
11838
- const c = cov.get(row.req_id) ?? { impl: 0, verif: 0 };
11839
- if (row.implemented)
12007
+ const c = cov.get(row2.req_id) ?? { impl: 0, verif: 0 };
12008
+ if (row2.implemented)
11840
12009
  c.impl += 1;
11841
- if (row.verified)
12010
+ if (row2.verified)
11842
12011
  c.verif += 1;
11843
- cov.set(row.req_id, c);
12012
+ cov.set(row2.req_id, c);
11844
12013
  }
11845
12014
  return cov;
11846
12015
  }
@@ -12187,15 +12356,15 @@ function mountSetup(app) {
12187
12356
  const memberCount = members.length;
12188
12357
  const note = readingNote(mode, memberCount);
12189
12358
  const boundByRepo = new Map;
12190
- for (const row of cov) {
12191
- if (!row.implemented)
12359
+ for (const row2 of cov) {
12360
+ if (!row2.implemented)
12192
12361
  continue;
12193
- let set = boundByRepo.get(row.repo);
12362
+ let set = boundByRepo.get(row2.repo);
12194
12363
  if (!set) {
12195
12364
  set = new Set;
12196
- boundByRepo.set(row.repo, set);
12365
+ boundByRepo.set(row2.repo, set);
12197
12366
  }
12198
- set.add(row.req_id);
12367
+ set.add(row2.req_id);
12199
12368
  }
12200
12369
  const body = html11`
12201
12370
  ${navBar("setup")}
@@ -12433,6 +12602,7 @@ function mintFieldsFromBody(body) {
12433
12602
  statement: typeof body.statement === "string" ? body.statement : "",
12434
12603
  why: typeof body.why === "string" ? body.why : "",
12435
12604
  livesIn: toStringList(body.livesIn),
12605
+ verifiedIn: toStringList(body.verifiedIn),
12436
12606
  relates: toStringList(body.relates),
12437
12607
  issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
12438
12608
  status: body.status === "draft" ? "draft" : "active"
@@ -12442,6 +12612,7 @@ function successorFieldsFromBody(body) {
12442
12612
  return {
12443
12613
  why: typeof body.why === "string" ? body.why : undefined,
12444
12614
  livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
12615
+ verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined,
12445
12616
  relates: "relates" in body ? toStringList(body.relates) : undefined,
12446
12617
  issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
12447
12618
  };
@@ -12454,6 +12625,8 @@ function amendFieldsFromBody(body) {
12454
12625
  fields.why = typeof body.why === "string" ? body.why : null;
12455
12626
  if ("livesIn" in body)
12456
12627
  fields.livesIn = toStringList(body.livesIn);
12628
+ if ("verifiedIn" in body)
12629
+ fields.verifiedIn = toStringList(body.verifiedIn);
12457
12630
  if ("relates" in body)
12458
12631
  fields.relates = toStringList(body.relates);
12459
12632
  return Object.keys(fields).length === 0 ? null : fields;
@@ -12515,8 +12688,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12515
12688
  }));
12516
12689
  app.get("/api/requirements/:id", guarded((c) => {
12517
12690
  const id = c.req.param("id") ?? "";
12518
- const { row } = getRecord(storage2, id);
12519
- return row ? c.json(row) : c.json({ error: "not found" }, 404);
12691
+ const { row: row2 } = getRecord(storage2, id);
12692
+ return row2 ? c.json(row2) : c.json({ error: "not found" }, 404);
12520
12693
  }));
12521
12694
  app.post("/api/requirements", guarded(async function createRequirement(c) {
12522
12695
  if (!featureEnabled("editor")) {
@@ -12562,7 +12735,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12562
12735
  return withWriteLock(async () => {
12563
12736
  const fields = amendFieldsFromBody(body);
12564
12737
  if (fields === null) {
12565
- return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, or relates" }, 400);
12738
+ return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, verifiedIn, or relates" }, 400);
12566
12739
  }
12567
12740
  const r = await amend({ platformDir, id, fields });
12568
12741
  if (!r.ok)
@@ -12635,7 +12808,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12635
12808
  targetKey,
12636
12809
  statement: typeof body.text === "string" ? body.text : undefined,
12637
12810
  why: typeof body.why === "string" ? body.why : undefined,
12638
- livesIn: "lives" in body ? toStringList(body.lives) : undefined
12811
+ livesIn: "lives" in body ? toStringList(body.lives) : undefined,
12812
+ verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined
12639
12813
  }, async (reqId) => {
12640
12814
  await runIndex({ platformDir, storage: storage2 });
12641
12815
  return storage2.listTags({ req_id: reqId });
@@ -13124,7 +13298,7 @@ __export(exports_docs, {
13124
13298
  createDocsFetchHandler: () => createDocsFetchHandler
13125
13299
  });
13126
13300
  import { existsSync as existsSync15, statSync as statSync6 } from "fs";
13127
- import { join as join16, normalize, sep as sep4 } from "path";
13301
+ import { join as join16, normalize, sep as sep5 } from "path";
13128
13302
  import { fileURLToPath } from "url";
13129
13303
  import { defineCommand as defineCommand25 } from "citty";
13130
13304
  function resolveDocsRootFrom(candidates) {
@@ -13142,7 +13316,7 @@ function resolveDocsRoot() {
13142
13316
  }
13143
13317
  function createDocsFetchHandler(root) {
13144
13318
  const normalRoot = normalize(root);
13145
- const rootPrefix = normalRoot.endsWith(sep4) ? normalRoot : normalRoot + sep4;
13319
+ const rootPrefix = normalRoot.endsWith(sep5) ? normalRoot : normalRoot + sep5;
13146
13320
  return (req) => {
13147
13321
  let pathname;
13148
13322
  try {