@ncoderz/awa 1.7.2 → 1.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.
Files changed (155) hide show
  1. package/LICENSE +23 -16
  2. package/README.md +25 -27
  3. package/dist/{chunk-OQZTQ5ZI.js → chunk-LRQWZCYL.js} +1 -4
  4. package/dist/chunk-LRQWZCYL.js.map +1 -0
  5. package/dist/chunk-WGGMDWBE.js +760 -0
  6. package/dist/chunk-WGGMDWBE.js.map +1 -0
  7. package/dist/{config-WL3SLSP6.js → config-EJIXC7D7.js} +2 -2
  8. package/dist/index.js +1252 -452
  9. package/dist/index.js.map +1 -1
  10. package/dist/renumber-ZCI2H5HZ.js +9 -0
  11. package/dist/renumber-ZCI2H5HZ.js.map +1 -0
  12. package/package.json +13 -6
  13. package/templates/awa/.agent/skills/spec-merge/SKILL.md +3 -0
  14. package/templates/awa/.agent/skills/spec-tidy/SKILL.md +3 -0
  15. package/templates/awa/.agent/workflows/spec-merge.md +3 -0
  16. package/templates/awa/.agent/workflows/spec-tidy.md +3 -0
  17. package/templates/awa/.agents/skills/spec-merge/SKILL.md +3 -0
  18. package/templates/awa/.agents/skills/spec-tidy/SKILL.md +3 -0
  19. package/templates/awa/.awa/.agent/schemas/ALIGN_REPORT.schema.yaml +1 -1
  20. package/templates/awa/.awa/.agent/schemas/API.schema.yaml +1 -1
  21. package/templates/awa/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +7 -0
  22. package/templates/awa/.awa/.agent/schemas/DESIGN.schema.yaml +1 -1
  23. package/templates/awa/.awa/.agent/schemas/{EXAMPLES.schema.yaml → EXAMPLE.schema.yaml} +4 -4
  24. package/templates/awa/.awa/.agent/schemas/FEAT.schema.yaml +8 -1
  25. package/templates/awa/.awa/.agent/schemas/PLAN.schema.yaml +1 -1
  26. package/templates/awa/.awa/.agent/schemas/README.schema.yaml +1 -1
  27. package/templates/awa/.awa/.agent/schemas/REQ.schema.yaml +1 -1
  28. package/templates/awa/.awa/.agent/schemas/TASK.schema.yaml +1 -1
  29. package/templates/awa/.claude/skills/spec-merge/SKILL.md +3 -0
  30. package/templates/awa/.claude/skills/spec-tidy/SKILL.md +3 -0
  31. package/templates/awa/.gemini/commands/spec-merge.md +3 -0
  32. package/templates/awa/.gemini/commands/spec-tidy.md +3 -0
  33. package/templates/awa/.gemini/skills/spec-merge/SKILL.md +3 -0
  34. package/templates/awa/.gemini/skills/spec-tidy/SKILL.md +3 -0
  35. package/templates/awa/.github/prompts/awa.spec-merge.prompt.md +8 -0
  36. package/templates/awa/.github/prompts/awa.spec-tidy.prompt.md +7 -0
  37. package/templates/awa/.github/skills/spec-merge/SKILL.md +3 -0
  38. package/templates/awa/.github/skills/spec-tidy/SKILL.md +3 -0
  39. package/templates/awa/.kilocode/skills/spec-merge/SKILL.md +3 -0
  40. package/templates/awa/.kilocode/skills/spec-tidy/SKILL.md +3 -0
  41. package/templates/awa/.kilocode/workflows/spec-merge.md +3 -0
  42. package/templates/awa/.kilocode/workflows/spec-tidy.md +3 -0
  43. package/templates/awa/.opencode/commands/spec-merge.md +3 -0
  44. package/templates/awa/.opencode/commands/spec-tidy.md +3 -0
  45. package/templates/awa/.opencode/skills/spec-merge/SKILL.md +3 -0
  46. package/templates/awa/.opencode/skills/spec-tidy/SKILL.md +3 -0
  47. package/templates/awa/.qwen/commands/spec-merge.md +3 -0
  48. package/templates/awa/.qwen/commands/spec-tidy.md +3 -0
  49. package/templates/awa/.qwen/skills/spec-merge/SKILL.md +3 -0
  50. package/templates/awa/.qwen/skills/spec-tidy/SKILL.md +3 -0
  51. package/templates/awa/.roo/skills/spec-merge/SKILL.md +3 -0
  52. package/templates/awa/.roo/skills/spec-tidy/SKILL.md +3 -0
  53. package/templates/awa/.windsurf/skills/spec-merge/SKILL.md +3 -0
  54. package/templates/awa/.windsurf/skills/spec-tidy/SKILL.md +3 -0
  55. package/templates/awa/_delete.txt +4 -0
  56. package/templates/awa/_partials/_cmd.spec-merge.md +6 -0
  57. package/templates/awa/_partials/_cmd.spec-tidy.md +5 -0
  58. package/templates/awa/_partials/_skill.spec-merge.md +6 -0
  59. package/templates/awa/_partials/_skill.spec-tidy.md +6 -0
  60. package/templates/awa/_partials/awa.align.md +1 -1
  61. package/templates/awa/_partials/awa.brainstorm.md +1 -1
  62. package/templates/awa/_partials/awa.code.md +1 -1
  63. package/templates/awa/_partials/awa.core.md +8 -4
  64. package/templates/awa/_partials/awa.design.md +3 -2
  65. package/templates/awa/_partials/awa.documentation.md +1 -1
  66. package/templates/awa/_partials/awa.examples.md +4 -4
  67. package/templates/awa/_partials/awa.feature.md +2 -1
  68. package/templates/awa/_partials/awa.plan.md +2 -2
  69. package/templates/awa/_partials/awa.requirements.md +4 -2
  70. package/templates/awa/_partials/awa.spec-merge.md +97 -0
  71. package/templates/awa/_partials/awa.spec.tidy.md +92 -0
  72. package/templates/awa/_partials/awa.tasks.md +1 -1
  73. package/templates/awa/_partials/awa.upgrade.md +3 -3
  74. package/templates/awa/_partials/awa.usage.md +74 -6
  75. package/templates/awa/_partials/awa.vibe.md +1 -1
  76. package/templates/awa/_tests/claude/.awa/.agent/awa.core.md +126 -0
  77. package/templates/awa/_tests/claude/.awa/.agent/schemas/ALIGN_REPORT.schema.yaml +83 -0
  78. package/templates/awa/_tests/claude/.awa/.agent/schemas/API.schema.yaml +7 -0
  79. package/templates/awa/_tests/claude/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +257 -0
  80. package/templates/awa/_tests/claude/.awa/.agent/schemas/DESIGN.schema.yaml +351 -0
  81. package/templates/awa/_tests/claude/.awa/.agent/schemas/EXAMPLE.schema.yaml +89 -0
  82. package/templates/awa/_tests/claude/.awa/.agent/schemas/FEAT.schema.yaml +142 -0
  83. package/templates/awa/_tests/claude/.awa/.agent/schemas/PLAN.schema.yaml +146 -0
  84. package/templates/awa/_tests/claude/.awa/.agent/schemas/README.schema.yaml +137 -0
  85. package/templates/awa/_tests/claude/.awa/.agent/schemas/REQ.schema.yaml +160 -0
  86. package/templates/awa/_tests/claude/.awa/.agent/schemas/TASK.schema.yaml +204 -0
  87. package/templates/awa/_tests/claude/.claude/agents/awa.md +137 -0
  88. package/templates/awa/_tests/claude/.claude/skills/awa-align/SKILL.md +67 -0
  89. package/templates/awa/_tests/claude/.claude/skills/awa-architecture/SKILL.md +50 -0
  90. package/templates/awa/_tests/claude/.claude/skills/awa-brainstorm/SKILL.md +57 -0
  91. package/templates/awa/_tests/claude/.claude/skills/awa-check/SKILL.md +79 -0
  92. package/templates/awa/_tests/claude/.claude/skills/awa-code/SKILL.md +179 -0
  93. package/templates/awa/_tests/claude/.claude/skills/awa-design/SKILL.md +62 -0
  94. package/templates/awa/_tests/claude/.claude/skills/awa-documentation/SKILL.md +91 -0
  95. package/templates/awa/_tests/claude/.claude/skills/awa-examples/SKILL.md +58 -0
  96. package/templates/awa/_tests/claude/.claude/skills/awa-feature/SKILL.md +56 -0
  97. package/templates/awa/_tests/claude/.claude/skills/awa-plan/SKILL.md +53 -0
  98. package/templates/awa/_tests/claude/.claude/skills/awa-refactor/SKILL.md +53 -0
  99. package/templates/awa/_tests/claude/.claude/skills/awa-requirements/SKILL.md +58 -0
  100. package/templates/awa/_tests/claude/.claude/skills/awa-tasks/SKILL.md +158 -0
  101. package/templates/awa/_tests/claude/.claude/skills/awa-upgrade/SKILL.md +68 -0
  102. package/templates/awa/_tests/claude/.claude/skills/awa-usage/SKILL.md +368 -0
  103. package/templates/awa/_tests/claude/.claude/skills/awa-vibe/SKILL.md +72 -0
  104. package/templates/awa/_tests/claude/.claude/skills/spec-merge/SKILL.md +102 -0
  105. package/templates/awa/_tests/claude/.claude/skills/spec-tidy/SKILL.md +97 -0
  106. package/templates/awa/_tests/claude/CLAUDE.md +132 -0
  107. package/templates/awa/_tests/copilot/.awa/.agent/awa.core.md +126 -0
  108. package/templates/awa/_tests/copilot/.awa/.agent/schemas/ALIGN_REPORT.schema.yaml +83 -0
  109. package/templates/awa/_tests/copilot/.awa/.agent/schemas/API.schema.yaml +7 -0
  110. package/templates/awa/_tests/copilot/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +257 -0
  111. package/templates/awa/_tests/copilot/.awa/.agent/schemas/DESIGN.schema.yaml +351 -0
  112. package/templates/awa/_tests/copilot/.awa/.agent/schemas/EXAMPLE.schema.yaml +89 -0
  113. package/templates/awa/_tests/copilot/.awa/.agent/schemas/FEAT.schema.yaml +142 -0
  114. package/templates/awa/_tests/copilot/.awa/.agent/schemas/PLAN.schema.yaml +146 -0
  115. package/templates/awa/_tests/copilot/.awa/.agent/schemas/README.schema.yaml +137 -0
  116. package/templates/awa/_tests/copilot/.awa/.agent/schemas/REQ.schema.yaml +160 -0
  117. package/templates/awa/_tests/copilot/.awa/.agent/schemas/TASK.schema.yaml +204 -0
  118. package/templates/awa/_tests/copilot/.github/agents/awa.agent.md +137 -0
  119. package/templates/awa/_tests/copilot/.github/prompts/awa.align.prompt.md +67 -0
  120. package/templates/awa/_tests/copilot/.github/prompts/awa.architecture.prompt.md +50 -0
  121. package/templates/awa/_tests/copilot/.github/prompts/awa.brainstorm.prompt.md +57 -0
  122. package/templates/awa/_tests/copilot/.github/prompts/awa.check.prompt.md +79 -0
  123. package/templates/awa/_tests/copilot/.github/prompts/awa.code.prompt.md +179 -0
  124. package/templates/awa/_tests/copilot/.github/prompts/awa.design.prompt.md +62 -0
  125. package/templates/awa/_tests/copilot/.github/prompts/awa.documentation.prompt.md +91 -0
  126. package/templates/awa/_tests/copilot/.github/prompts/awa.examples.prompt.md +58 -0
  127. package/templates/awa/_tests/copilot/.github/prompts/awa.feature.prompt.md +56 -0
  128. package/templates/awa/_tests/copilot/.github/prompts/awa.plan.prompt.md +53 -0
  129. package/templates/awa/_tests/copilot/.github/prompts/awa.refactor.prompt.md +53 -0
  130. package/templates/awa/_tests/copilot/.github/prompts/awa.requirements.prompt.md +58 -0
  131. package/templates/awa/_tests/copilot/.github/prompts/awa.spec-merge.prompt.md +102 -0
  132. package/templates/awa/_tests/copilot/.github/prompts/awa.spec-tidy.prompt.md +96 -0
  133. package/templates/awa/_tests/copilot/.github/prompts/awa.tasks.prompt.md +158 -0
  134. package/templates/awa/_tests/copilot/.github/prompts/awa.upgrade.prompt.md +68 -0
  135. package/templates/awa/_tests/copilot/.github/prompts/awa.vibe.prompt.md +72 -0
  136. package/templates/awa/_tests/copilot/.github/skills/awa-align/SKILL.md +67 -0
  137. package/templates/awa/_tests/copilot/.github/skills/awa-architecture/SKILL.md +50 -0
  138. package/templates/awa/_tests/copilot/.github/skills/awa-brainstorm/SKILL.md +57 -0
  139. package/templates/awa/_tests/copilot/.github/skills/awa-check/SKILL.md +79 -0
  140. package/templates/awa/_tests/copilot/.github/skills/awa-code/SKILL.md +179 -0
  141. package/templates/awa/_tests/copilot/.github/skills/awa-design/SKILL.md +62 -0
  142. package/templates/awa/_tests/copilot/.github/skills/awa-documentation/SKILL.md +91 -0
  143. package/templates/awa/_tests/copilot/.github/skills/awa-examples/SKILL.md +58 -0
  144. package/templates/awa/_tests/copilot/.github/skills/awa-feature/SKILL.md +56 -0
  145. package/templates/awa/_tests/copilot/.github/skills/awa-plan/SKILL.md +53 -0
  146. package/templates/awa/_tests/copilot/.github/skills/awa-refactor/SKILL.md +53 -0
  147. package/templates/awa/_tests/copilot/.github/skills/awa-requirements/SKILL.md +58 -0
  148. package/templates/awa/_tests/copilot/.github/skills/awa-tasks/SKILL.md +158 -0
  149. package/templates/awa/_tests/copilot/.github/skills/awa-upgrade/SKILL.md +68 -0
  150. package/templates/awa/_tests/copilot/.github/skills/awa-usage/SKILL.md +368 -0
  151. package/templates/awa/_tests/copilot/.github/skills/awa-vibe/SKILL.md +72 -0
  152. package/templates/awa/_tests/copilot/.github/skills/spec-merge/SKILL.md +102 -0
  153. package/templates/awa/_tests/copilot/.github/skills/spec-tidy/SKILL.md +97 -0
  154. package/dist/chunk-OQZTQ5ZI.js.map +0 -1
  155. /package/dist/{config-WL3SLSP6.js.map → config-EJIXC7D7.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,4 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_CHECK_CONFIG,
4
+ collectFiles,
5
+ matchSimpleGlob,
6
+ parseSpecs,
7
+ propagate,
8
+ renumberCommand,
9
+ scan,
10
+ scanMarkers
11
+ } from "./chunk-WGGMDWBE.js";
2
12
  import {
3
13
  ConfigError,
4
14
  DiffError,
@@ -16,7 +26,7 @@ import {
16
26
  rmDir,
17
27
  walkDirectory,
18
28
  writeTextFile
19
- } from "./chunk-OQZTQ5ZI.js";
29
+ } from "./chunk-LRQWZCYL.js";
20
30
 
21
31
  // src/cli/index.ts
22
32
  import { Command, Option } from "commander";
@@ -24,9 +34,9 @@ import { Command, Option } from "commander";
24
34
  // src/_generated/package_info.ts
25
35
  var PACKAGE_INFO = {
26
36
  "name": "@ncoderz/awa",
27
- "version": "1.7.2",
37
+ "version": "1.8.0",
28
38
  "author": "Richard Sewell <richard.sewell@ncoderz.com>",
29
- "license": "MIT",
39
+ "license": "BSD-3-Clause",
30
40
  "description": "awa is an Agent Workflow for AIs. It is also a CLI tool to powerfully manage agent workflow files using templates."
31
41
  };
32
42
 
@@ -201,154 +211,331 @@ function checkCodeAgainstSpec(markers, specs, config) {
201
211
  return { findings };
202
212
  }
203
213
 
204
- // src/core/check/marker-scanner.ts
205
- import { readFile } from "fs/promises";
214
+ // src/core/check/codes-fixer.ts
215
+ import { readFile as readFile2, writeFile } from "fs/promises";
216
+ import { basename as basename2 } from "path";
206
217
 
207
- // src/core/check/glob.ts
208
- import { glob } from "fs/promises";
209
- function matchSimpleGlob(path, pattern) {
210
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<GLOBSTAR>>").replace(/\*/g, "[^/]*").replace(/<<GLOBSTAR>>/g, ".*");
211
- return new RegExp(`(^|/)${regex}($|/)`).test(path);
212
- }
213
- async function collectFiles(globs, ignore) {
214
- const files = [];
215
- const dirPrefixes = ignore.filter((ig) => ig.endsWith("/**")).map((ig) => ig.slice(0, -3));
216
- for (const pattern of globs) {
217
- for await (const filePath of glob(pattern, {
218
- exclude: (p) => dirPrefixes.includes(p) || ignore.some((ig) => matchSimpleGlob(p, ig))
219
- })) {
220
- if (!ignore.some((ig) => matchSimpleGlob(filePath, ig))) {
221
- files.push(filePath);
218
+ // src/core/codes/scanner.ts
219
+ import { readFile } from "fs/promises";
220
+ import { basename } from "path";
221
+ async function scanCodes(specFiles, specGlobs, specIgnore) {
222
+ const codeMap = /* @__PURE__ */ new Map();
223
+ const featFiles = await collectFiles(
224
+ specGlobs.filter((g) => g.includes("FEAT-")),
225
+ specIgnore
226
+ );
227
+ for (const fp of featFiles) {
228
+ const fileName = basename(fp);
229
+ const match = /^FEAT-([A-Z][A-Z0-9]*)-(.+)\.md$/.exec(fileName);
230
+ if (!match?.[1]) continue;
231
+ const code = match[1];
232
+ const existing = codeMap.get(code);
233
+ if (existing) {
234
+ existing.feat = true;
235
+ } else {
236
+ codeMap.set(code, {
237
+ feature: match[2] ?? code,
238
+ reqCount: 0,
239
+ feat: true,
240
+ req: false,
241
+ design: false,
242
+ api: false,
243
+ example: false
244
+ });
245
+ }
246
+ }
247
+ for (const sf of specFiles) {
248
+ if (!sf.code) continue;
249
+ const fileName = basename(sf.filePath);
250
+ const prefix = fileName.split("-")[0];
251
+ const existing = codeMap.get(sf.code);
252
+ if (existing) {
253
+ if (prefix === "REQ") {
254
+ existing.req = true;
255
+ existing.reqCount += sf.requirementIds.length;
256
+ if (existing.feature === sf.code || !existing.feature) {
257
+ existing.feature = extractFeatureName(fileName);
258
+ }
259
+ } else if (prefix === "DESIGN") {
260
+ existing.design = true;
261
+ } else if (prefix === "API") {
262
+ existing.api = true;
263
+ } else if (prefix === "EXAMPLE") {
264
+ existing.example = true;
265
+ } else if (prefix === "FEAT") {
266
+ existing.feat = true;
222
267
  }
268
+ } else {
269
+ const feature = prefix === "REQ" ? extractFeatureName(fileName) : extractFeatureNameGeneric(fileName);
270
+ codeMap.set(sf.code, {
271
+ feature,
272
+ reqCount: prefix === "REQ" ? sf.requirementIds.length : 0,
273
+ feat: prefix === "FEAT",
274
+ req: prefix === "REQ",
275
+ design: prefix === "DESIGN",
276
+ api: prefix === "API",
277
+ example: prefix === "EXAMPLE"
278
+ });
223
279
  }
224
280
  }
225
- return [...new Set(files)];
226
- }
227
-
228
- // src/core/check/marker-scanner.ts
229
- var MARKER_TYPE_MAP = {
230
- "@awa-impl": "impl",
231
- "@awa-test": "test",
232
- "@awa-component": "component"
233
- };
234
- var IGNORE_FILE_RE = new RegExp("@awa-ignore-file\\b");
235
- var IGNORE_NEXT_LINE_RE = /@awa-ignore-next-line\b/;
236
- var IGNORE_LINE_RE = /@awa-ignore\b/;
237
- var IGNORE_START_RE = /@awa-ignore-start\b/;
238
- var IGNORE_END_RE = /@awa-ignore-end\b/;
239
- async function scanMarkers(config) {
240
- const files = await collectCodeFiles(config.codeGlobs, config.codeIgnore);
241
- const markers = [];
242
- const findings = [];
243
- for (const filePath of files) {
244
- if (config.ignoreMarkers.some((ig) => matchSimpleGlob(filePath, ig))) {
245
- continue;
281
+ const scopeMap = await extractScopeSummaries(specFiles, specGlobs, specIgnore);
282
+ const codes = [];
283
+ for (const [code, meta] of codeMap) {
284
+ const docs = {
285
+ feat: meta.feat,
286
+ req: meta.req,
287
+ design: meta.design,
288
+ api: meta.api,
289
+ example: meta.example
290
+ };
291
+ codes.push({
292
+ code,
293
+ feature: meta.feature,
294
+ reqCount: meta.reqCount,
295
+ scope: scopeMap.get(code) ?? "",
296
+ docs
297
+ });
298
+ }
299
+ codes.sort((a, b) => a.code.localeCompare(b.code));
300
+ return { codes };
301
+ }
302
+ function extractFeatureName(fileName) {
303
+ const name = basename(fileName, ".md");
304
+ const match = /^REQ-[A-Z][A-Z0-9]*-(.+)$/.exec(name);
305
+ return match?.[1] ?? name;
306
+ }
307
+ function extractFeatureNameGeneric(fileName) {
308
+ const name = basename(fileName).replace(/\.[^.]+$/, "");
309
+ const match = /^[A-Z]+-[A-Z][A-Z0-9]*-(.+)$/.exec(name);
310
+ return match?.[1] ?? name;
311
+ }
312
+ async function extractScopeSummaries(specFiles, specGlobs, specIgnore) {
313
+ const scopeMap = /* @__PURE__ */ new Map();
314
+ const featByCode = buildFileRefMapFromSpecFiles(specFiles, "FEAT");
315
+ const reqByCode = buildFileRefMapFromSpecFiles(specFiles, "REQ");
316
+ const designByCode = buildFileRefMapFromSpecFiles(specFiles, "DESIGN");
317
+ const scopeFeatFiles = await collectFiles(
318
+ specGlobs.filter((g) => g.includes("FEAT-")),
319
+ specIgnore
320
+ );
321
+ for (const fp of scopeFeatFiles) {
322
+ const name = basename(fp, ".md");
323
+ const match = /^FEAT-([A-Z][A-Z0-9]*)-/.exec(name);
324
+ if (match?.[1] && !featByCode.has(match[1])) {
325
+ featByCode.set(match[1], { filePath: fp });
246
326
  }
247
- const result = await scanFile(filePath, config.markers);
248
- markers.push(...result.markers);
249
- findings.push(...result.findings);
250
327
  }
251
- return { markers, findings };
328
+ const allCodes = /* @__PURE__ */ new Set();
329
+ for (const code of featByCode.keys()) allCodes.add(code);
330
+ for (const sf of specFiles) {
331
+ if (sf.code) {
332
+ allCodes.add(sf.code);
333
+ }
334
+ }
335
+ for (const code of allCodes) {
336
+ const scope = await resolveScope(code, featByCode, reqByCode, designByCode);
337
+ if (scope) {
338
+ scopeMap.set(code, scope);
339
+ }
340
+ }
341
+ return scopeMap;
252
342
  }
253
- function buildMarkerRegex(markerNames) {
254
- const escaped = markerNames.map((m) => m.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
255
- return new RegExp(`(${escaped.join("|")}):\\s*(.+)`, "g");
343
+ async function resolveScope(code, featByCode, reqByCode, designByCode) {
344
+ const featRef = featByCode.get(code);
345
+ if (featRef) {
346
+ const content = await readContent(featRef);
347
+ if (content) {
348
+ const scopeBoundary = extractScopeBoundary(content);
349
+ if (scopeBoundary) return scopeBoundary;
350
+ const firstParagraph = extractFirstParagraph(content);
351
+ if (firstParagraph) return firstParagraph;
352
+ }
353
+ }
354
+ const reqRef = reqByCode.get(code);
355
+ if (reqRef) {
356
+ const content = await readContent(reqRef);
357
+ if (content) {
358
+ const firstParagraph = extractFirstParagraph(content);
359
+ if (firstParagraph) return firstParagraph;
360
+ }
361
+ }
362
+ const designRef = designByCode.get(code);
363
+ if (designRef) {
364
+ const content = await readContent(designRef);
365
+ if (content) {
366
+ const firstParagraph = extractFirstParagraph(content);
367
+ if (firstParagraph) return firstParagraph;
368
+ }
369
+ }
370
+ return "";
256
371
  }
257
- var ID_TOKEN_RE = /^([A-Z][A-Z0-9]*(?:[-_][A-Za-z0-9]+)*(?:\.\d+)?(?:[-_][A-Za-z0-9]+)*)/;
258
- async function scanFile(filePath, markerNames) {
259
- let content;
372
+ async function readContent(ref) {
373
+ if (ref.content != null) return ref.content;
260
374
  try {
261
- content = await readFile(filePath, "utf-8");
375
+ return await readFile(ref.filePath, "utf-8");
262
376
  } catch {
263
- return { markers: [], findings: [] };
377
+ return void 0;
264
378
  }
265
- if (IGNORE_FILE_RE.test(content)) {
266
- return { markers: [], findings: [] };
379
+ }
380
+ function buildFileRefMapFromSpecFiles(specFiles, prefix) {
381
+ const map = /* @__PURE__ */ new Map();
382
+ for (const sf of specFiles) {
383
+ if (sf.code && basename(sf.filePath).startsWith(`${prefix}-`) && !map.has(sf.code)) {
384
+ map.set(sf.code, { filePath: sf.filePath, content: sf.content });
385
+ }
267
386
  }
268
- const regex = buildMarkerRegex(markerNames);
387
+ return map;
388
+ }
389
+ function extractScopeBoundary(content) {
269
390
  const lines = content.split("\n");
270
- const markers = [];
271
- const findings = [];
272
- let ignoreNextLine = false;
273
- let ignoreBlock = false;
274
- for (const [i, line] of lines.entries()) {
275
- if (IGNORE_START_RE.test(line)) {
276
- ignoreBlock = true;
277
- continue;
278
- }
279
- if (IGNORE_END_RE.test(line)) {
280
- ignoreBlock = false;
391
+ let inSection = false;
392
+ const paragraphLines = [];
393
+ for (const line of lines) {
394
+ if (!inSection) {
395
+ if (/^##\s+Scope Boundary\s*$/.test(line)) {
396
+ inSection = true;
397
+ }
281
398
  continue;
282
399
  }
283
- if (ignoreBlock) {
400
+ const trimmed = line.trim();
401
+ if (paragraphLines.length === 0 && trimmed === "") {
284
402
  continue;
285
403
  }
286
- if (ignoreNextLine) {
287
- ignoreNextLine = false;
288
- continue;
404
+ if (paragraphLines.length > 0 && (trimmed === "" || /^#/.test(trimmed))) {
405
+ break;
289
406
  }
290
- if (IGNORE_NEXT_LINE_RE.test(line)) {
291
- ignoreNextLine = true;
407
+ paragraphLines.push(trimmed);
408
+ }
409
+ const paragraph = paragraphLines.join(" ").trim();
410
+ if (paragraph.length > 120) {
411
+ return `${paragraph.slice(0, 117)}...`;
412
+ }
413
+ return paragraph;
414
+ }
415
+ function extractFirstParagraph(content) {
416
+ const lines = content.split("\n");
417
+ let foundHeading = false;
418
+ const paragraphLines = [];
419
+ for (const line of lines) {
420
+ if (!foundHeading) {
421
+ if (/^##\s/.test(line)) {
422
+ foundHeading = true;
423
+ }
292
424
  continue;
293
425
  }
294
- if (IGNORE_LINE_RE.test(line)) {
426
+ const trimmed = line.trim();
427
+ if (paragraphLines.length === 0 && trimmed === "") {
295
428
  continue;
296
429
  }
297
- regex.lastIndex = 0;
298
- let match = regex.exec(line);
299
- while (match !== null) {
300
- const markerName = match[1] ?? "";
301
- const idsRaw = match[2] ?? "";
302
- const type = resolveMarkerType(markerName, markerNames);
303
- const ids = idsRaw.split(",").map((id) => id.trim()).filter(Boolean);
304
- for (const id of ids) {
305
- const tokenMatch = ID_TOKEN_RE.exec(id);
306
- const cleanId = tokenMatch?.[1]?.trim() ?? "";
307
- if (cleanId && tokenMatch) {
308
- const remainder = id.slice(tokenMatch[0].length).trim();
309
- if (remainder) {
310
- findings.push({
311
- severity: "error",
312
- code: "marker-trailing-text",
313
- message: `Marker has trailing text after ID '${cleanId}': '${remainder}' \u2014 use comma-separated IDs only`,
314
- filePath,
315
- line: i + 1,
316
- id: cleanId
317
- });
318
- }
319
- markers.push({ type, id: cleanId, filePath, line: i + 1 });
320
- }
321
- }
322
- match = regex.exec(line);
430
+ if (paragraphLines.length > 0 && (trimmed === "" || /^#/.test(trimmed))) {
431
+ break;
323
432
  }
433
+ paragraphLines.push(trimmed);
324
434
  }
325
- return { markers, findings };
435
+ const paragraph = paragraphLines.join(" ").trim();
436
+ if (paragraph.length > 120) {
437
+ return `${paragraph.slice(0, 117)}...`;
438
+ }
439
+ return paragraph;
326
440
  }
327
- function resolveMarkerType(markerName, configuredMarkers) {
328
- const mapped = MARKER_TYPE_MAP[markerName];
329
- if (mapped) return mapped;
330
- const idx = configuredMarkers.indexOf(markerName);
331
- if (idx === 1) return "test";
332
- if (idx === 2) return "component";
333
- return "impl";
441
+
442
+ // src/core/check/codes-fixer.ts
443
+ function sanitizeCell(value) {
444
+ return value.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ").trim();
334
445
  }
335
- async function collectCodeFiles(codeGlobs, ignore) {
336
- return collectFiles(codeGlobs, ignore);
446
+ async function fixCodesTable(specs, config) {
447
+ const archFile = specs.specFiles.find((sf) => basename2(sf.filePath) === "ARCHITECTURE.md");
448
+ if (!archFile) {
449
+ return { filesFixed: 0, fileResults: [], emptyScopeCodes: [] };
450
+ }
451
+ let content;
452
+ if (archFile.content != null) {
453
+ content = archFile.content;
454
+ } else {
455
+ try {
456
+ content = await readFile2(archFile.filePath, "utf-8");
457
+ } catch {
458
+ return { filesFixed: 0, fileResults: [], emptyScopeCodes: [] };
459
+ }
460
+ }
461
+ const lines = content.split("\n");
462
+ const sectionStart = lines.findIndex((l) => /^##\s+Feature Codes\s*$/.test(l));
463
+ if (sectionStart === -1) {
464
+ return { filesFixed: 0, fileResults: [], emptyScopeCodes: [] };
465
+ }
466
+ const codesResult = await scanCodes(specs.specFiles, config.specGlobs, config.specIgnore);
467
+ const tableLines = [];
468
+ tableLines.push("");
469
+ tableLines.push(
470
+ "Run `awa spec codes` for the live inventory. The table below defines scope boundaries."
471
+ );
472
+ tableLines.push("");
473
+ tableLines.push("| Code | Feature | Scope Boundary |");
474
+ tableLines.push("|------|---------|----------------|");
475
+ const emptyScopeCodes = [];
476
+ for (const code of codesResult.codes) {
477
+ const scope = sanitizeCell(code.scope || "");
478
+ if (!scope) {
479
+ emptyScopeCodes.push(code.code);
480
+ }
481
+ tableLines.push(`| ${sanitizeCell(code.code)} | ${sanitizeCell(code.feature)} | ${scope} |`);
482
+ }
483
+ const newSection = tableLines.join("\n");
484
+ const newContent = replaceFeatureCodesSection(content, sectionStart, newSection);
485
+ if (newContent === content) {
486
+ return {
487
+ filesFixed: 0,
488
+ fileResults: [{ filePath: archFile.filePath, changed: false }],
489
+ emptyScopeCodes
490
+ };
491
+ }
492
+ await writeFile(archFile.filePath, newContent, "utf-8");
493
+ return {
494
+ filesFixed: 1,
495
+ fileResults: [{ filePath: archFile.filePath, changed: true }],
496
+ emptyScopeCodes
497
+ };
498
+ }
499
+ function replaceFeatureCodesSection(content, sectionStart, newSection) {
500
+ const lines = content.split("\n");
501
+ let sectionEnd = lines.length;
502
+ for (let i = sectionStart + 1; i < lines.length; i++) {
503
+ const line = lines[i];
504
+ if (line !== void 0 && /^##\s/.test(line)) {
505
+ sectionEnd = i;
506
+ break;
507
+ }
508
+ }
509
+ const before = lines.slice(0, sectionStart + 1);
510
+ const after = lines.slice(sectionEnd);
511
+ const result = [...before, newSection.trimEnd(), "", ...after];
512
+ return result.join("\n");
337
513
  }
338
514
 
339
515
  // src/core/check/matrix-fixer.ts
340
- import { readFile as readFile2, writeFile } from "fs/promises";
341
- import { basename } from "path";
516
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
517
+ import { basename as basename3 } from "path";
342
518
  async function fixMatrices(specs, crossRefPatterns) {
343
519
  const reqFileMaps = buildReqFileMaps(specs.specFiles);
344
520
  const fileResults = [];
345
521
  for (const specFile of specs.specFiles) {
346
- const fileName = basename(specFile.filePath);
522
+ const fileName = basename3(specFile.filePath);
347
523
  if (fileName.startsWith("DESIGN-")) {
348
- const changed = await fixDesignMatrix(specFile.filePath, reqFileMaps, crossRefPatterns);
524
+ const changed = await fixDesignMatrix(
525
+ specFile.filePath,
526
+ reqFileMaps,
527
+ crossRefPatterns,
528
+ specFile.content
529
+ );
349
530
  fileResults.push({ filePath: specFile.filePath, changed });
350
531
  } else if (fileName.startsWith("TASK-")) {
351
- const changed = await fixTaskMatrix(specFile.filePath, reqFileMaps, specs, crossRefPatterns);
532
+ const changed = await fixTaskMatrix(
533
+ specFile.filePath,
534
+ reqFileMaps,
535
+ specs,
536
+ crossRefPatterns,
537
+ specFile.content
538
+ );
352
539
  fileResults.push({ filePath: specFile.filePath, changed });
353
540
  }
354
541
  }
@@ -361,7 +548,7 @@ function buildReqFileMaps(specFiles) {
361
548
  const idToReqFile = /* @__PURE__ */ new Map();
362
549
  const codeToReqFilesSet = /* @__PURE__ */ new Map();
363
550
  for (const sf of specFiles) {
364
- const fileName = basename(sf.filePath);
551
+ const fileName = basename3(sf.filePath);
365
552
  if (!/\bREQ-/.test(fileName)) continue;
366
553
  for (const reqId of sf.requirementIds) {
367
554
  idToReqFile.set(reqId, fileName);
@@ -395,12 +582,16 @@ function resolveReqFile(id, maps) {
395
582
  }
396
583
  return void 0;
397
584
  }
398
- async function fixDesignMatrix(filePath, reqFileMaps, crossRefPatterns) {
585
+ async function fixDesignMatrix(filePath, reqFileMaps, crossRefPatterns, cachedContent) {
399
586
  let content;
400
- try {
401
- content = await readFile2(filePath, "utf-8");
402
- } catch {
403
- return false;
587
+ if (cachedContent != null) {
588
+ content = cachedContent;
589
+ } else {
590
+ try {
591
+ content = await readFile3(filePath, "utf-8");
592
+ } catch {
593
+ return false;
594
+ }
404
595
  }
405
596
  const { components, properties } = parseDesignFileData(content, crossRefPatterns);
406
597
  const acToComponents = /* @__PURE__ */ new Map();
@@ -424,7 +615,7 @@ async function fixDesignMatrix(filePath, reqFileMaps, crossRefPatterns) {
424
615
  const newSection = generateDesignSection(grouped, acToComponents, acToProperties);
425
616
  const newContent = replaceTraceabilitySection(content, newSection);
426
617
  if (newContent === content) return false;
427
- await writeFile(filePath, newContent, "utf-8");
618
+ await writeFile2(filePath, newContent, "utf-8");
428
619
  return true;
429
620
  }
430
621
  function parseDesignFileData(content, crossRefPatterns) {
@@ -495,12 +686,16 @@ function generateDesignSection(grouped, acToComponents, acToProperties) {
495
686
  }
496
687
  return lines.join("\n");
497
688
  }
498
- async function fixTaskMatrix(filePath, reqFileMaps, specs, crossRefPatterns) {
689
+ async function fixTaskMatrix(filePath, reqFileMaps, specs, crossRefPatterns, cachedContent) {
499
690
  let content;
500
- try {
501
- content = await readFile2(filePath, "utf-8");
502
- } catch {
503
- return false;
691
+ if (cachedContent != null) {
692
+ content = cachedContent;
693
+ } else {
694
+ try {
695
+ content = await readFile3(filePath, "utf-8");
696
+ } catch {
697
+ return false;
698
+ }
504
699
  }
505
700
  const { tasks, sourceDesigns } = parseTaskFileData(content, crossRefPatterns);
506
701
  const acToTasks = /* @__PURE__ */ new Map();
@@ -523,7 +718,7 @@ async function fixTaskMatrix(filePath, reqFileMaps, specs, crossRefPatterns) {
523
718
  for (const designName of sourceDesigns) {
524
719
  const designCode = extractCodeFromFileName(designName);
525
720
  for (const sf of specs.specFiles) {
526
- if (sf.code === designCode && /\bDESIGN-/.test(basename(sf.filePath))) {
721
+ if (sf.code === designCode && /\bDESIGN-/.test(basename3(sf.filePath))) {
527
722
  for (const propId of sf.propertyIds) sourcePropertyIds.add(propId);
528
723
  }
529
724
  }
@@ -533,7 +728,7 @@ async function fixTaskMatrix(filePath, reqFileMaps, specs, crossRefPatterns) {
533
728
  const newSection = generateTaskSection(grouped, acToTasks, idToTestTasks, sourcePropertyIds);
534
729
  const newContent = replaceTraceabilitySection(content, newSection);
535
730
  if (newContent === content) return false;
536
- await writeFile(filePath, newContent, "utf-8");
731
+ await writeFile2(filePath, newContent, "utf-8");
537
732
  return true;
538
733
  }
539
734
  function parseTaskFileData(content, crossRefPatterns) {
@@ -663,7 +858,7 @@ function extractIdsFromText(text) {
663
858
  return ids;
664
859
  }
665
860
  function extractCodeFromFileName(fileName) {
666
- const match = /^(?:REQ|DESIGN|FEAT|EXAMPLES|API|TASK)-([A-Z][A-Z0-9]*)-/.exec(fileName);
861
+ const match = /^(?:REQ|DESIGN|FEAT|EXAMPLE|API|TASK)-([A-Z][A-Z0-9]*)-/.exec(fileName);
667
862
  return match?.[1] ?? "";
668
863
  }
669
864
  function compareIds(a, b) {
@@ -745,7 +940,7 @@ function printRuleContext(f) {
745
940
  }
746
941
 
747
942
  // src/core/check/rule-loader.ts
748
- import { readFile as readFile3 } from "fs/promises";
943
+ import { readFile as readFile4 } from "fs/promises";
749
944
  import { join } from "path";
750
945
  import { parse as parseYaml } from "yaml";
751
946
  async function loadRules(schemaDir) {
@@ -766,7 +961,7 @@ function matchesTargetGlob(filePath, targetGlob) {
766
961
  async function loadRuleFile(filePath) {
767
962
  let content;
768
963
  try {
769
- content = await readFile3(filePath, "utf-8");
964
+ content = await readFile4(filePath, "utf-8");
770
965
  } catch {
771
966
  return null;
772
967
  }
@@ -958,7 +1153,7 @@ var RuleValidationError = class extends Error {
958
1153
  };
959
1154
 
960
1155
  // src/core/check/schema-checker.ts
961
- import { readFile as readFile4 } from "fs/promises";
1156
+ import { readFile as readFile5 } from "fs/promises";
962
1157
  import remarkGfm from "remark-gfm";
963
1158
  import remarkParse from "remark-parse";
964
1159
  import { unified } from "unified";
@@ -1000,14 +1195,18 @@ function formatProhibitedRule(pattern) {
1000
1195
  async function checkSchemasAsync(specFiles, ruleSets) {
1001
1196
  const findings = [];
1002
1197
  const parser = unified().use(remarkParse).use(remarkGfm);
1003
- for (const spec of specFiles) {
1004
- const matchingRules = ruleSets.filter((rs) => matchesTargetGlob(spec.filePath, rs.targetGlob));
1198
+ for (const spec2 of specFiles) {
1199
+ const matchingRules = ruleSets.filter((rs) => matchesTargetGlob(spec2.filePath, rs.targetGlob));
1005
1200
  if (matchingRules.length === 0) continue;
1006
1201
  let content;
1007
- try {
1008
- content = await readFile4(spec.filePath, "utf-8");
1009
- } catch {
1010
- continue;
1202
+ if (spec2.content != null) {
1203
+ content = spec2.content;
1204
+ } else {
1205
+ try {
1206
+ content = await readFile5(spec2.filePath, "utf-8");
1207
+ } catch {
1208
+ continue;
1209
+ }
1011
1210
  }
1012
1211
  const tree = parser.parse(content);
1013
1212
  const sectionTree = buildSectionTree(tree);
@@ -1021,7 +1220,7 @@ async function checkSchemasAsync(specFiles, ruleSets) {
1021
1220
  severity: "warning",
1022
1221
  code: "schema-line-limit",
1023
1222
  message: `File has ${lineCount} lines, exceeds limit of ${ruleSet.ruleFile["line-limit"]}`,
1024
- filePath: spec.filePath,
1223
+ filePath: spec2.filePath,
1025
1224
  ruleSource,
1026
1225
  rule: formatLineLimitRule(ruleSet.ruleFile["line-limit"])
1027
1226
  });
@@ -1031,7 +1230,7 @@ async function checkSchemasAsync(specFiles, ruleSets) {
1031
1230
  ...checkRulesAgainstSections(
1032
1231
  allSections,
1033
1232
  ruleSet.ruleFile.sections,
1034
- spec.filePath,
1233
+ spec2.filePath,
1035
1234
  ruleSource
1036
1235
  )
1037
1236
  );
@@ -1040,7 +1239,7 @@ async function checkSchemasAsync(specFiles, ruleSets) {
1040
1239
  ...checkProhibited(
1041
1240
  content,
1042
1241
  ruleSet.ruleFile["sections-prohibited"],
1043
- spec.filePath,
1242
+ spec2.filePath,
1044
1243
  ruleSource
1045
1244
  )
1046
1245
  );
@@ -1449,131 +1648,6 @@ function collectAllCodeBlocks(section) {
1449
1648
  return blocks;
1450
1649
  }
1451
1650
 
1452
- // src/core/check/spec-parser.ts
1453
- import { readFile as readFile5 } from "fs/promises";
1454
- import { basename as basename2 } from "path";
1455
- async function parseSpecs(config) {
1456
- const files = await collectSpecFiles(config.specGlobs, config.specIgnore);
1457
- const specFiles = [];
1458
- const requirementIds = /* @__PURE__ */ new Set();
1459
- const acIds = /* @__PURE__ */ new Set();
1460
- const propertyIds = /* @__PURE__ */ new Set();
1461
- const componentNames = /* @__PURE__ */ new Set();
1462
- const idLocations = /* @__PURE__ */ new Map();
1463
- for (const filePath of files) {
1464
- const specFile = await parseSpecFile(filePath, config.crossRefPatterns);
1465
- if (specFile) {
1466
- specFiles.push(specFile);
1467
- for (const id of specFile.requirementIds) requirementIds.add(id);
1468
- for (const id of specFile.acIds) acIds.add(id);
1469
- for (const id of specFile.propertyIds) propertyIds.add(id);
1470
- for (const name of specFile.componentNames) componentNames.add(name);
1471
- for (const [id, loc] of specFile.idLocations ?? []) {
1472
- idLocations.set(id, loc);
1473
- }
1474
- }
1475
- }
1476
- const allIds = /* @__PURE__ */ new Set([...requirementIds, ...acIds, ...propertyIds, ...componentNames]);
1477
- return { requirementIds, acIds, propertyIds, componentNames, allIds, specFiles, idLocations };
1478
- }
1479
- async function parseSpecFile(filePath, crossRefPatterns) {
1480
- let content;
1481
- try {
1482
- content = await readFile5(filePath, "utf-8");
1483
- } catch {
1484
- return null;
1485
- }
1486
- const code = extractCodePrefix(filePath);
1487
- const lines = content.split("\n");
1488
- const requirementIds = [];
1489
- const acIds = [];
1490
- const propertyIds = [];
1491
- const componentNames = [];
1492
- const crossRefs = [];
1493
- const idLocations = /* @__PURE__ */ new Map();
1494
- const componentImplements = /* @__PURE__ */ new Map();
1495
- const reqIdRegex = /^###\s+([A-Z][A-Z0-9]*-\d+(?:\.\d+)?)\s*:/;
1496
- const acIdRegex = /^-\s+(?:\[[ x]\]\s+)?([A-Z][A-Z0-9]*-\d+(?:\.\d+)?_AC-\d+)\s/;
1497
- const propIdRegex = /^-\s+([A-Z][A-Z0-9]*_P-\d+)\s/;
1498
- const componentRegex = /^###\s+([A-Z][A-Z0-9]*-[A-Za-z][A-Za-z0-9]*(?:[A-Z][a-z0-9]*)*)\s*$/;
1499
- let currentComponent = null;
1500
- for (const [i, line] of lines.entries()) {
1501
- const lineNum = i + 1;
1502
- const reqMatch = reqIdRegex.exec(line);
1503
- if (reqMatch?.[1]) {
1504
- requirementIds.push(reqMatch[1]);
1505
- idLocations.set(reqMatch[1], { filePath, line: lineNum });
1506
- }
1507
- const acMatch = acIdRegex.exec(line);
1508
- if (acMatch?.[1]) {
1509
- acIds.push(acMatch[1]);
1510
- idLocations.set(acMatch[1], { filePath, line: lineNum });
1511
- }
1512
- const propMatch = propIdRegex.exec(line);
1513
- if (propMatch?.[1]) {
1514
- propertyIds.push(propMatch[1]);
1515
- idLocations.set(propMatch[1], { filePath, line: lineNum });
1516
- }
1517
- const compMatch = componentRegex.exec(line);
1518
- if (compMatch?.[1]) {
1519
- if (!reqIdRegex.test(line)) {
1520
- componentNames.push(compMatch[1]);
1521
- idLocations.set(compMatch[1], { filePath, line: lineNum });
1522
- currentComponent = compMatch[1];
1523
- }
1524
- }
1525
- if (/^#{1,2}\s/.test(line) && !compMatch) {
1526
- currentComponent = null;
1527
- }
1528
- for (const pattern of crossRefPatterns) {
1529
- const patIdx = line.indexOf(pattern);
1530
- if (patIdx !== -1) {
1531
- const afterPattern = line.slice(patIdx + pattern.length);
1532
- const ids = extractIdsFromText2(afterPattern);
1533
- if (ids.length > 0) {
1534
- const type = pattern.toLowerCase().includes("implements") ? "implements" : "validates";
1535
- crossRefs.push({ type, ids, filePath, line: i + 1 });
1536
- if (type === "implements" && currentComponent) {
1537
- const existing = componentImplements.get(currentComponent) ?? [];
1538
- existing.push(...ids);
1539
- componentImplements.set(currentComponent, existing);
1540
- }
1541
- }
1542
- }
1543
- }
1544
- }
1545
- return {
1546
- filePath,
1547
- code,
1548
- requirementIds,
1549
- acIds,
1550
- propertyIds,
1551
- componentNames,
1552
- crossRefs,
1553
- idLocations,
1554
- componentImplements
1555
- };
1556
- }
1557
- function extractCodePrefix(filePath) {
1558
- const name = basename2(filePath, ".md");
1559
- const match = /^(?:REQ|DESIGN|FEAT|EXAMPLES|API)-([A-Z][A-Z0-9]*)-/.exec(name);
1560
- if (match?.[1]) return match[1];
1561
- return "";
1562
- }
1563
- function extractIdsFromText2(text) {
1564
- const idRegex = /[A-Z][A-Z0-9]*-\d+(?:\.\d+)?(?:_AC-\d+)?|[A-Z][A-Z0-9]*_P-\d+/g;
1565
- const ids = [];
1566
- let match = idRegex.exec(text);
1567
- while (match !== null) {
1568
- ids.push(match[0]);
1569
- match = idRegex.exec(text);
1570
- }
1571
- return ids;
1572
- }
1573
- async function collectSpecFiles(specGlobs, ignore) {
1574
- return collectFiles(specGlobs, ignore);
1575
- }
1576
-
1577
1651
  // src/core/check/spec-spec-checker.ts
1578
1652
  function checkSpecAgainstSpec(specs, markers, config) {
1579
1653
  const findings = [];
@@ -1658,44 +1732,6 @@ function checkSpecAgainstSpec(specs, markers, config) {
1658
1732
  return { findings };
1659
1733
  }
1660
1734
 
1661
- // src/core/check/types.ts
1662
- var DEFAULT_CHECK_CONFIG = {
1663
- specGlobs: [
1664
- ".awa/specs/ARCHITECTURE.md",
1665
- ".awa/specs/FEAT-*.md",
1666
- ".awa/specs/REQ-*.md",
1667
- ".awa/specs/DESIGN-*.md",
1668
- ".awa/specs/EXAMPLES-*.md",
1669
- ".awa/specs/API-*.tsp",
1670
- ".awa/tasks/TASK-*.md",
1671
- ".awa/plans/PLAN-*.md",
1672
- ".awa/align/ALIGN-*.md"
1673
- ],
1674
- codeGlobs: [
1675
- "**/*.{ts,js,tsx,jsx,mts,mjs,cjs,py,go,rs,java,kt,kts,cs,c,h,cpp,cc,cxx,hpp,hxx,swift,rb,php,scala,ex,exs,dart,lua,zig}"
1676
- ],
1677
- specIgnore: [],
1678
- codeIgnore: [
1679
- "node_modules/**",
1680
- "dist/**",
1681
- "vendor/**",
1682
- "target/**",
1683
- "build/**",
1684
- "out/**",
1685
- ".awa/**"
1686
- ],
1687
- ignoreMarkers: [],
1688
- markers: ["@awa-impl", "@awa-test", "@awa-component"],
1689
- idPattern: "([A-Z][A-Z0-9]*-\\d+(?:\\.\\d+)?(?:_AC-\\d+)?|[A-Z][A-Z0-9]*_P-\\d+)",
1690
- crossRefPatterns: ["IMPLEMENTS:", "VALIDATES:"],
1691
- format: "text",
1692
- schemaDir: ".awa/.agent/schemas",
1693
- schemaEnabled: true,
1694
- allowWarnings: false,
1695
- specOnly: false,
1696
- fix: true
1697
- };
1698
-
1699
1735
  // src/commands/check.ts
1700
1736
  async function checkCommand(cliOptions) {
1701
1737
  try {
@@ -1712,6 +1748,21 @@ async function checkCommand(cliOptions) {
1712
1748
  ]);
1713
1749
  const codeSpecResult = config.specOnly ? { findings: [] } : checkCodeAgainstSpec(markers, specs, config);
1714
1750
  const specSpecResult = checkSpecAgainstSpec(specs, markers, config);
1751
+ if (config.fix) {
1752
+ const fixResult = await fixMatrices(specs, config.crossRefPatterns);
1753
+ if (fixResult.filesFixed > 0) {
1754
+ logger.info(`Fixed traceability matrices in ${fixResult.filesFixed} file(s)`);
1755
+ }
1756
+ const codesFixResult = await fixCodesTable(specs, config);
1757
+ if (codesFixResult.filesFixed > 0) {
1758
+ logger.info("Fixed Feature Codes table in ARCHITECTURE.md");
1759
+ }
1760
+ if (codesFixResult.emptyScopeCodes.length > 0) {
1761
+ logger.warn(
1762
+ `Feature codes missing Scope Boundary: ${codesFixResult.emptyScopeCodes.join(", ")}`
1763
+ );
1764
+ }
1765
+ }
1715
1766
  const schemaResult = config.schemaEnabled && ruleSets.length > 0 ? await checkSchemasAsync(specs.specFiles, ruleSets) : { findings: [] };
1716
1767
  const combinedFindings = [
1717
1768
  ...markers.findings,
@@ -1730,12 +1781,6 @@ async function checkCommand(cliOptions) {
1730
1781
  } else {
1731
1782
  report(allFindings, config.format);
1732
1783
  }
1733
- if (config.fix) {
1734
- const fixResult = await fixMatrices(specs, config.crossRefPatterns);
1735
- if (fixResult.filesFixed > 0) {
1736
- logger.info(`Fixed traceability matrices in ${fixResult.filesFixed} file(s)`);
1737
- }
1738
- }
1739
1784
  const hasErrors = allFindings.some((f) => f.severity === "error");
1740
1785
  return hasErrors ? 1 : 0;
1741
1786
  } catch (error) {
@@ -1797,6 +1842,98 @@ function toStringArray(value) {
1797
1842
  return null;
1798
1843
  }
1799
1844
 
1845
+ // src/core/codes/reporter.ts
1846
+ import chalk2 from "chalk";
1847
+ function formatDocs(docs) {
1848
+ return [
1849
+ docs.feat ? "F" : "\xB7",
1850
+ docs.req ? "R" : "\xB7",
1851
+ docs.design ? "D" : "\xB7",
1852
+ docs.api ? "A" : "\xB7",
1853
+ docs.example ? "E" : "\xB7"
1854
+ ].join("");
1855
+ }
1856
+ function buildJsonOutput(result) {
1857
+ return {
1858
+ codes: result.codes.map((c) => ({
1859
+ code: c.code,
1860
+ feature: c.feature,
1861
+ reqCount: c.reqCount,
1862
+ docs: formatDocs(c.docs),
1863
+ scope: c.scope
1864
+ })),
1865
+ totalCodes: result.codes.length
1866
+ };
1867
+ }
1868
+ function formatJson(result) {
1869
+ return JSON.stringify(buildJsonOutput(result), null, 2);
1870
+ }
1871
+ function formatTable(result) {
1872
+ const { codes } = result;
1873
+ if (codes.length === 0) {
1874
+ return chalk2.yellow("No feature codes found.");
1875
+ }
1876
+ const codeWidth = Math.max(4, ...codes.map((c) => c.code.length));
1877
+ const featureWidth = Math.max(7, ...codes.map((c) => c.feature.length));
1878
+ const reqWidth = 4;
1879
+ const docsWidth = 5;
1880
+ const header = [
1881
+ "CODE".padEnd(codeWidth),
1882
+ "Feature".padEnd(featureWidth),
1883
+ "Reqs".padStart(reqWidth),
1884
+ "Docs".padEnd(docsWidth),
1885
+ "Scope"
1886
+ ].join(" ");
1887
+ const separator = [
1888
+ "\u2500".repeat(codeWidth),
1889
+ "\u2500".repeat(featureWidth),
1890
+ "\u2500".repeat(reqWidth),
1891
+ "\u2500".repeat(docsWidth),
1892
+ "\u2500".repeat(40)
1893
+ ].join(" ");
1894
+ const rows = codes.map((c) => {
1895
+ const docs = formatDocs(c.docs);
1896
+ return [
1897
+ chalk2.cyan(c.code.padEnd(codeWidth)),
1898
+ c.feature.padEnd(featureWidth),
1899
+ String(c.reqCount).padStart(reqWidth),
1900
+ docs,
1901
+ chalk2.dim(c.scope)
1902
+ ].join(" ");
1903
+ });
1904
+ return [chalk2.bold(`Feature codes (${codes.length}):
1905
+ `), header, separator, ...rows].join("\n");
1906
+ }
1907
+ function formatSummary(result) {
1908
+ return `codes: ${result.codes.length}`;
1909
+ }
1910
+
1911
+ // src/commands/codes.ts
1912
+ async function codesCommand(options) {
1913
+ try {
1914
+ const { specs, config } = await scan(options.config);
1915
+ const result = await scanCodes(specs.specFiles, config.specGlobs, config.specIgnore);
1916
+ if (options.summary) {
1917
+ process.stdout.write(`${formatSummary(result)}
1918
+ `);
1919
+ } else if (options.json) {
1920
+ process.stdout.write(`${formatJson(result)}
1921
+ `);
1922
+ } else {
1923
+ process.stdout.write(`${formatTable(result)}
1924
+ `);
1925
+ }
1926
+ return 0;
1927
+ } catch (err) {
1928
+ if (err instanceof Error) {
1929
+ logger.error(err.message);
1930
+ } else {
1931
+ logger.error(String(err));
1932
+ }
1933
+ return 2;
1934
+ }
1935
+ }
1936
+
1800
1937
  // src/commands/diff.ts
1801
1938
  import { intro, outro } from "@clack/prompts";
1802
1939
 
@@ -1905,7 +2042,7 @@ import { join as join3, relative } from "path";
1905
2042
  // src/core/resolver.ts
1906
2043
  import { MultiSelectPrompt } from "@clack/core";
1907
2044
  import { isCancel, multiselect } from "@clack/prompts";
1908
- import chalk2 from "chalk";
2045
+ import chalk3 from "chalk";
1909
2046
  var _unicode = process.platform !== "win32";
1910
2047
  var _s = (c, fb) => _unicode ? c : fb;
1911
2048
  var _CHECKED = _s("\u25FC", "[+]");
@@ -1915,20 +2052,20 @@ var _BAR = _s("\u2502", "|");
1915
2052
  var _BAR_END = _s("\u2514", "-");
1916
2053
  function _renderDeleteItem(opt, state) {
1917
2054
  const label = opt.label ?? opt.value;
1918
- const hint = opt.hint ? ` ${chalk2.dim(`(${opt.hint})`)}` : "";
2055
+ const hint = opt.hint ? ` ${chalk3.dim(`(${opt.hint})`)}` : "";
1919
2056
  switch (state) {
1920
2057
  case "active":
1921
- return `${chalk2.cyan(_UNCHECKED_A)} ${label}${hint}`;
2058
+ return `${chalk3.cyan(_UNCHECKED_A)} ${label}${hint}`;
1922
2059
  case "selected":
1923
- return `${chalk2.red(_CHECKED)} ${chalk2.dim(label)}${hint}`;
2060
+ return `${chalk3.red(_CHECKED)} ${chalk3.dim(label)}${hint}`;
1924
2061
  case "active-selected":
1925
- return `${chalk2.red(_CHECKED)} ${label}${hint}`;
2062
+ return `${chalk3.red(_CHECKED)} ${label}${hint}`;
1926
2063
  case "cancelled":
1927
- return chalk2.strikethrough(chalk2.dim(label));
2064
+ return chalk3.strikethrough(chalk3.dim(label));
1928
2065
  case "submitted":
1929
- return chalk2.dim(label);
2066
+ return chalk3.dim(label);
1930
2067
  default:
1931
- return `${chalk2.dim(_UNCHECKED)} ${chalk2.dim(label)}`;
2068
+ return `${chalk3.dim(_UNCHECKED)} ${chalk3.dim(label)}`;
1932
2069
  }
1933
2070
  }
1934
2071
  async function deleteMultiselect(opts) {
@@ -1939,8 +2076,8 @@ async function deleteMultiselect(opts) {
1939
2076
  required,
1940
2077
  render() {
1941
2078
  const self = this;
1942
- const header = `${chalk2.gray(_BAR)}
1943
- ${chalk2.cyan(_BAR)} ${message}
2079
+ const header = `${chalk3.gray(_BAR)}
2080
+ ${chalk3.cyan(_BAR)} ${message}
1944
2081
  `;
1945
2082
  const getState = (opt, idx) => {
1946
2083
  const active = idx === self.cursor;
@@ -1952,16 +2089,16 @@ ${chalk2.cyan(_BAR)} ${message}
1952
2089
  };
1953
2090
  switch (self.state) {
1954
2091
  case "submit":
1955
- return `${header}${chalk2.gray(_BAR)} ` + (self.options.filter((o) => self.value.includes(o.value)).map((o) => _renderDeleteItem(o, "submitted")).join(chalk2.dim(", ")) || chalk2.dim("none"));
2092
+ return `${header}${chalk3.gray(_BAR)} ` + (self.options.filter((o) => self.value.includes(o.value)).map((o) => _renderDeleteItem(o, "submitted")).join(chalk3.dim(", ")) || chalk3.dim("none"));
1956
2093
  case "cancel": {
1957
- const cancelled = self.options.filter((o) => self.value.includes(o.value)).map((o) => _renderDeleteItem(o, "cancelled")).join(chalk2.dim(", "));
1958
- return `${header}${chalk2.gray(_BAR)} ${cancelled.trim() ? `${cancelled}
1959
- ${chalk2.gray(_BAR)}` : chalk2.dim("none")}`;
2094
+ const cancelled = self.options.filter((o) => self.value.includes(o.value)).map((o) => _renderDeleteItem(o, "cancelled")).join(chalk3.dim(", "));
2095
+ return `${header}${chalk3.gray(_BAR)} ${cancelled.trim() ? `${cancelled}
2096
+ ${chalk3.gray(_BAR)}` : chalk3.dim("none")}`;
1960
2097
  }
1961
2098
  default:
1962
- return `${header}${chalk2.cyan(_BAR)} ` + self.options.map((o, i) => _renderDeleteItem(o, getState(o, i))).join(`
1963
- ${chalk2.cyan(_BAR)} `) + `
1964
- ${chalk2.cyan(_BAR_END)}
2099
+ return `${header}${chalk3.cyan(_BAR)} ` + self.options.map((o, i) => _renderDeleteItem(o, getState(o, i))).join(`
2100
+ ${chalk3.cyan(_BAR)} `) + `
2101
+ ${chalk3.cyan(_BAR_END)}
1965
2102
  `;
1966
2103
  }
1967
2104
  }
@@ -2057,11 +2194,9 @@ var EMPTY_FILE_MARKER = "<!-- AWA:EMPTY_FILE -->";
2057
2194
  var TemplateEngine = class {
2058
2195
  eta = null;
2059
2196
  templateDir = null;
2060
- compiledCache = /* @__PURE__ */ new Map();
2061
2197
  // @awa-impl: TPL-8_AC-1, TPL-8_AC-2, TPL-8_AC-3, TPL-8_AC-4
2062
2198
  configure(templateDir) {
2063
2199
  this.templateDir = templateDir;
2064
- this.compiledCache.clear();
2065
2200
  this.eta = new Eta({
2066
2201
  views: templateDir,
2067
2202
  cache: true,
@@ -2882,7 +3017,7 @@ async function diffCommand(cliOptions) {
2882
3017
  import { intro as intro2, outro as outro2 } from "@clack/prompts";
2883
3018
 
2884
3019
  // src/core/features/reporter.ts
2885
- import chalk3 from "chalk";
3020
+ import chalk4 from "chalk";
2886
3021
  var FeaturesReporter = class {
2887
3022
  // @awa-impl: DISC-6_AC-1, DISC-7_AC-1
2888
3023
  /** Render the features report to stdout. */
@@ -2917,25 +3052,25 @@ var FeaturesReporter = class {
2917
3052
  reportTable(scanResult, presets) {
2918
3053
  const { features, filesScanned } = scanResult;
2919
3054
  if (features.length === 0) {
2920
- console.log(chalk3.yellow("No feature flags found."));
2921
- console.log(chalk3.dim(`(${filesScanned} files scanned)`));
3055
+ console.log(chalk4.yellow("No feature flags found."));
3056
+ console.log(chalk4.dim(`(${filesScanned} files scanned)`));
2922
3057
  return;
2923
3058
  }
2924
- console.log(chalk3.bold(`Feature flags (${features.length} found):
3059
+ console.log(chalk4.bold(`Feature flags (${features.length} found):
2925
3060
  `));
2926
3061
  for (const feature of features) {
2927
- console.log(` ${chalk3.cyan(feature.name)}`);
3062
+ console.log(` ${chalk4.cyan(feature.name)}`);
2928
3063
  for (const file of feature.files) {
2929
- console.log(` ${chalk3.dim(file)}`);
3064
+ console.log(` ${chalk4.dim(file)}`);
2930
3065
  }
2931
3066
  }
2932
3067
  console.log("");
2933
- console.log(chalk3.dim(`${filesScanned} files scanned`));
3068
+ console.log(chalk4.dim(`${filesScanned} files scanned`));
2934
3069
  if (presets && Object.keys(presets).length > 0) {
2935
3070
  console.log("");
2936
- console.log(chalk3.bold("Presets (from .awa.toml):\n"));
3071
+ console.log(chalk4.bold("Presets (from .awa.toml):\n"));
2937
3072
  for (const [name, flags] of Object.entries(presets)) {
2938
- console.log(` ${chalk3.green(name)}: ${flags.join(", ")}`);
3073
+ console.log(` ${chalk4.green(name)}: ${flags.join(", ")}`);
2939
3074
  }
2940
3075
  }
2941
3076
  }
@@ -3168,25 +3303,665 @@ async function generateCommand(cliOptions) {
3168
3303
  }
3169
3304
  }
3170
3305
 
3306
+ // src/core/merge/content-merger.ts
3307
+ import { readFile as readFile7, rename, writeFile as writeFile3 } from "fs/promises";
3308
+ import { basename as basename4, dirname, extname, join as join8 } from "path";
3309
+ var MERGE_PREFIXES = ["FEAT", "REQ", "DESIGN", "API", "EXAMPLE", "TASK"];
3310
+ function resolveMovePath(sourceFilePath, prefix, sourceCode, targetCode, existingPaths, plannedPaths) {
3311
+ const dir = dirname(sourceFilePath);
3312
+ const name = basename4(sourceFilePath);
3313
+ const newName = name.replace(`${prefix}-${sourceCode}-`, `${prefix}-${targetCode}-`);
3314
+ const newPath = join8(dir, newName);
3315
+ if (!existingPaths.has(newPath) && !plannedPaths.has(newPath)) {
3316
+ return newPath;
3317
+ }
3318
+ const ext = extname(newName);
3319
+ const stem = newName.slice(0, -ext.length);
3320
+ for (let i = 1; i < 1e3; i++) {
3321
+ const indexed = join8(dir, `${stem}-${String(i).padStart(3, "0")}${ext}`);
3322
+ if (!existingPaths.has(indexed) && !plannedPaths.has(indexed)) {
3323
+ return indexed;
3324
+ }
3325
+ }
3326
+ throw new Error(`Cannot resolve conflict for ${sourceFilePath}`);
3327
+ }
3328
+ function updateHeading(content, sourceCode, targetCode) {
3329
+ const lines = content.split("\n");
3330
+ for (let i = 0; i < lines.length; i++) {
3331
+ const line = lines[i];
3332
+ if (/^#\s/.test(line)) {
3333
+ lines[i] = line.replaceAll(sourceCode, targetCode);
3334
+ break;
3335
+ }
3336
+ }
3337
+ return lines.join("\n");
3338
+ }
3339
+ async function executeMoves(sourceCode, targetCode, specFiles, dryRun) {
3340
+ const moves = [];
3341
+ const sourceFilePaths = /* @__PURE__ */ new Set();
3342
+ for (const sf of specFiles) {
3343
+ const name = basename4(sf.filePath);
3344
+ for (const prefix of MERGE_PREFIXES) {
3345
+ if (name.startsWith(`${prefix}-${sourceCode}-`)) {
3346
+ sourceFilePaths.add(sf.filePath);
3347
+ break;
3348
+ }
3349
+ }
3350
+ }
3351
+ const existingPaths = new Set(
3352
+ specFiles.map((sf) => sf.filePath).filter((p) => !sourceFilePaths.has(p))
3353
+ );
3354
+ const plannedPaths = /* @__PURE__ */ new Set();
3355
+ for (const sf of specFiles) {
3356
+ const name = basename4(sf.filePath);
3357
+ let matchedPrefix = "";
3358
+ for (const prefix of MERGE_PREFIXES) {
3359
+ if (name.startsWith(`${prefix}-${sourceCode}-`)) {
3360
+ matchedPrefix = prefix;
3361
+ break;
3362
+ }
3363
+ }
3364
+ if (!matchedPrefix) continue;
3365
+ const targetPath = resolveMovePath(
3366
+ sf.filePath,
3367
+ matchedPrefix,
3368
+ sourceCode,
3369
+ targetCode,
3370
+ existingPaths,
3371
+ plannedPaths
3372
+ );
3373
+ plannedPaths.add(targetPath);
3374
+ moves.push({
3375
+ sourceFile: sf.filePath,
3376
+ targetFile: targetPath,
3377
+ docType: matchedPrefix
3378
+ });
3379
+ if (!dryRun) {
3380
+ const content = await readFile7(sf.filePath, "utf-8");
3381
+ const updated = updateHeading(content, sourceCode, targetCode);
3382
+ await rename(sf.filePath, targetPath);
3383
+ if (updated !== content) {
3384
+ await writeFile3(targetPath, updated, "utf-8");
3385
+ }
3386
+ }
3387
+ }
3388
+ return moves;
3389
+ }
3390
+
3391
+ // src/core/merge/reporter.ts
3392
+ function formatText(result, dryRun) {
3393
+ const lines = [];
3394
+ if (dryRun) {
3395
+ lines.push("DRY RUN \u2014 no files were modified\n");
3396
+ }
3397
+ if (result.noChange) {
3398
+ lines.push(`${result.sourceCode} \u2192 ${result.targetCode}: nothing to merge`);
3399
+ return lines.join("\n");
3400
+ }
3401
+ lines.push(
3402
+ `${result.sourceCode} \u2192 ${result.targetCode}: ${result.map.entries.size} ID(s) recoded
3403
+ `
3404
+ );
3405
+ if (result.map.entries.size > 0) {
3406
+ lines.push(" Old ID \u2192 New ID");
3407
+ lines.push(` ${"\u2500".repeat(40)}`);
3408
+ for (const [oldId, newId] of result.map.entries) {
3409
+ lines.push(` ${oldId} \u2192 ${newId}`);
3410
+ }
3411
+ }
3412
+ if (result.moves.length > 0) {
3413
+ lines.push("");
3414
+ lines.push(` ${result.moves.length} file(s) moved:`);
3415
+ for (const m of result.moves) {
3416
+ lines.push(` ${m.sourceFile} \u2192 ${m.targetFile} (${m.docType})`);
3417
+ }
3418
+ }
3419
+ if (result.affectedFiles.length > 0) {
3420
+ lines.push("");
3421
+ lines.push(
3422
+ ` ${result.totalReplacements} replacement(s) in ${result.affectedFiles.length} file(s):`
3423
+ );
3424
+ for (const file of result.affectedFiles) {
3425
+ lines.push(` ${file.filePath} (${file.replacements.length})`);
3426
+ }
3427
+ }
3428
+ if (result.staleRefs.length > 0) {
3429
+ lines.push("");
3430
+ lines.push(` \u2716 ${result.staleRefs.length} file(s) still reference ${result.sourceCode}:`);
3431
+ for (const ref of result.staleRefs) {
3432
+ lines.push(` ${ref}`);
3433
+ }
3434
+ }
3435
+ return lines.join("\n");
3436
+ }
3437
+ function formatJson2(result) {
3438
+ const output = {
3439
+ sourceCode: result.sourceCode,
3440
+ targetCode: result.targetCode,
3441
+ noChange: result.noChange,
3442
+ map: Object.fromEntries(result.map.entries),
3443
+ moves: result.moves.map((m) => ({
3444
+ sourceFile: m.sourceFile,
3445
+ targetFile: m.targetFile,
3446
+ docType: m.docType
3447
+ })),
3448
+ staleRefs: result.staleRefs,
3449
+ affectedFiles: result.affectedFiles.map((f) => ({
3450
+ filePath: f.filePath,
3451
+ replacements: f.replacements.map((r) => ({
3452
+ line: r.line,
3453
+ oldId: r.oldId,
3454
+ newId: r.newId
3455
+ }))
3456
+ })),
3457
+ totalReplacements: result.totalReplacements
3458
+ };
3459
+ return JSON.stringify(output, null, 2);
3460
+ }
3461
+
3462
+ // src/core/merge/spec-mover.ts
3463
+ import { readFile as readFile8, rename as rename2 } from "fs/promises";
3464
+ import { basename as basename5, dirname as dirname2, join as join9 } from "path";
3465
+
3466
+ // src/core/merge/types.ts
3467
+ var MergeError = class extends Error {
3468
+ errorCode;
3469
+ constructor(errorCode, message) {
3470
+ super(message);
3471
+ this.name = "MergeError";
3472
+ this.errorCode = errorCode;
3473
+ }
3474
+ };
3475
+
3476
+ // src/core/merge/spec-mover.ts
3477
+ var CODE_PREFIXES = ["REQ", "DESIGN", "FEAT", "TASK", "EXAMPLE"];
3478
+ function resolveFeatureName(code, specFiles) {
3479
+ for (const sf of specFiles) {
3480
+ const name = basename5(sf.filePath, ".md");
3481
+ const match = new RegExp(`^REQ-${code}-(.+)$`).exec(name);
3482
+ if (match?.[1]) return match[1];
3483
+ }
3484
+ return void 0;
3485
+ }
3486
+ function planRenames(sourceCode, targetCode, specFiles) {
3487
+ const targetFeature = resolveFeatureName(targetCode, specFiles);
3488
+ const renames = [];
3489
+ for (const sf of specFiles) {
3490
+ const name = basename5(sf.filePath);
3491
+ for (const prefix of CODE_PREFIXES) {
3492
+ const oldPrefix = `${prefix}-${sourceCode}-`;
3493
+ if (name.startsWith(oldPrefix)) {
3494
+ const oldSuffix = name.slice(oldPrefix.length);
3495
+ const newSuffix = targetFeature ? replaceFeaturePart(oldSuffix, targetFeature) : oldSuffix;
3496
+ const newName = `${prefix}-${targetCode}-${newSuffix}`;
3497
+ const newPath = join9(dirname2(sf.filePath), newName);
3498
+ renames.push({ oldPath: sf.filePath, newPath });
3499
+ break;
3500
+ }
3501
+ }
3502
+ }
3503
+ return renames;
3504
+ }
3505
+ function replaceFeaturePart(suffix, targetFeature) {
3506
+ const numericMatch = /^(.+)-(\d+\.md)$/.exec(suffix);
3507
+ if (numericMatch) {
3508
+ return `${targetFeature}-${numericMatch[2]}`;
3509
+ }
3510
+ if (suffix.endsWith(".md")) {
3511
+ return `${targetFeature}.md`;
3512
+ }
3513
+ return suffix;
3514
+ }
3515
+ function detectConflicts(renames, specFiles) {
3516
+ const existingPaths = new Set(specFiles.map((sf) => sf.filePath));
3517
+ const conflicts = [];
3518
+ for (const r of renames) {
3519
+ if (existingPaths.has(r.newPath)) {
3520
+ conflicts.push(r.newPath);
3521
+ }
3522
+ }
3523
+ return conflicts;
3524
+ }
3525
+ function updateHeading2(content, sourceCode, targetCode) {
3526
+ const lines = content.split("\n");
3527
+ for (let i = 0; i < lines.length; i++) {
3528
+ const line = lines[i];
3529
+ if (/^#\s/.test(line)) {
3530
+ lines[i] = line.replaceAll(sourceCode, targetCode);
3531
+ break;
3532
+ }
3533
+ }
3534
+ return lines.join("\n");
3535
+ }
3536
+ async function executeRenames(renames, sourceCode, targetCode, dryRun) {
3537
+ if (dryRun) return renames;
3538
+ for (const r of renames) {
3539
+ const content = await readFile8(r.oldPath, "utf-8");
3540
+ const updated = updateHeading2(content, sourceCode, targetCode);
3541
+ await rename2(r.oldPath, r.newPath);
3542
+ if (updated !== content) {
3543
+ const { writeFile: writeFile5 } = await import("fs/promises");
3544
+ await writeFile5(r.newPath, updated, "utf-8");
3545
+ }
3546
+ }
3547
+ return renames;
3548
+ }
3549
+ async function findStaleRefs(sourceCode, specFiles) {
3550
+ const stale = [];
3551
+ const pattern = new RegExp(`\\b${sourceCode}-\\d`, "g");
3552
+ for (const sf of specFiles) {
3553
+ let content;
3554
+ try {
3555
+ content = await readFile8(sf.filePath, "utf-8");
3556
+ } catch {
3557
+ continue;
3558
+ }
3559
+ if (pattern.test(content)) {
3560
+ stale.push(sf.filePath);
3561
+ }
3562
+ pattern.lastIndex = 0;
3563
+ }
3564
+ return stale;
3565
+ }
3566
+ function validateMerge(sourceCode, targetCode) {
3567
+ if (sourceCode === targetCode) {
3568
+ throw new MergeError("SELF_MERGE", `Cannot merge a code into itself: ${sourceCode}`);
3569
+ }
3570
+ }
3571
+
3572
+ // src/core/recode/map-builder.ts
3573
+ import { basename as basename6 } from "path";
3574
+
3575
+ // src/core/recode/types.ts
3576
+ var RecodeError = class extends Error {
3577
+ errorCode;
3578
+ constructor(errorCode, message) {
3579
+ super(message);
3580
+ this.name = "RecodeError";
3581
+ this.errorCode = errorCode;
3582
+ }
3583
+ };
3584
+
3585
+ // src/core/recode/map-builder.ts
3586
+ function buildRecodeMap(sourceCode, targetCode, specs) {
3587
+ if (!hasAnySpecFile(specs.specFiles, sourceCode)) {
3588
+ throw new RecodeError("SOURCE_NOT_FOUND", `No spec files found for source code: ${sourceCode}`);
3589
+ }
3590
+ const entries = /* @__PURE__ */ new Map();
3591
+ const sourceReq = findSpecFile(specs.specFiles, sourceCode, "REQ");
3592
+ if (sourceReq) {
3593
+ const targetReq = findSpecFile(specs.specFiles, targetCode, "REQ");
3594
+ const reqOffset = targetReq ? findHighestReqNumber(targetReq) : 0;
3595
+ buildRequirementEntries(sourceCode, targetCode, sourceReq, reqOffset, entries);
3596
+ }
3597
+ const sourceDesign = findSpecFile(specs.specFiles, sourceCode, "DESIGN");
3598
+ const targetDesign = findSpecFile(specs.specFiles, targetCode, "DESIGN");
3599
+ if (sourceDesign) {
3600
+ const propOffset = targetDesign ? findHighestPropertyNumber(targetDesign) : 0;
3601
+ buildPropertyEntries(sourceCode, targetCode, sourceDesign, propOffset, entries);
3602
+ }
3603
+ if (sourceDesign) {
3604
+ buildComponentEntries(sourceCode, targetCode, sourceDesign, entries);
3605
+ }
3606
+ const noChange = entries.size === 0;
3607
+ const map = { code: sourceCode, entries };
3608
+ return { map, noChange };
3609
+ }
3610
+ function buildRequirementEntries(_sourceCode, targetCode, sourceReq, reqOffset, entries) {
3611
+ const topLevelReqs = [];
3612
+ const subReqsByParent = /* @__PURE__ */ new Map();
3613
+ for (const id of sourceReq.requirementIds) {
3614
+ if (id.includes(".")) {
3615
+ const dotIdx = id.lastIndexOf(".");
3616
+ const parent = id.slice(0, dotIdx);
3617
+ const subs = subReqsByParent.get(parent) ?? [];
3618
+ subs.push(id);
3619
+ subReqsByParent.set(parent, subs);
3620
+ } else {
3621
+ topLevelReqs.push(id);
3622
+ }
3623
+ }
3624
+ const reqNumberMap = /* @__PURE__ */ new Map();
3625
+ for (let i = 0; i < topLevelReqs.length; i++) {
3626
+ const oldId = topLevelReqs[i];
3627
+ const newNum = reqOffset + i + 1;
3628
+ const newId = `${targetCode}-${newNum}`;
3629
+ entries.set(oldId, newId);
3630
+ reqNumberMap.set(oldId, newNum);
3631
+ }
3632
+ for (const oldParentId of topLevelReqs) {
3633
+ const subs = subReqsByParent.get(oldParentId);
3634
+ if (!subs) continue;
3635
+ const newParentNum = reqNumberMap.get(oldParentId);
3636
+ if (newParentNum === void 0) continue;
3637
+ for (let j = 0; j < subs.length; j++) {
3638
+ const oldSubId = subs[j];
3639
+ const newSubNum = j + 1;
3640
+ const newSubId = `${targetCode}-${newParentNum}.${newSubNum}`;
3641
+ entries.set(oldSubId, newSubId);
3642
+ }
3643
+ }
3644
+ const acsByParent = /* @__PURE__ */ new Map();
3645
+ for (const acId of sourceReq.acIds) {
3646
+ const parent = acId.split("_AC-")[0];
3647
+ const acs = acsByParent.get(parent) ?? [];
3648
+ acs.push(acId);
3649
+ acsByParent.set(parent, acs);
3650
+ }
3651
+ for (const [oldParentId, acs] of acsByParent) {
3652
+ const newParentId = entries.get(oldParentId) ?? oldParentId;
3653
+ for (let k = 0; k < acs.length; k++) {
3654
+ const oldAcId = acs[k];
3655
+ const newAcNum = k + 1;
3656
+ const newAcId = `${newParentId}_AC-${newAcNum}`;
3657
+ entries.set(oldAcId, newAcId);
3658
+ }
3659
+ }
3660
+ }
3661
+ function buildPropertyEntries(_sourceCode, targetCode, sourceDesign, propOffset, entries) {
3662
+ for (let i = 0; i < sourceDesign.propertyIds.length; i++) {
3663
+ const oldId = sourceDesign.propertyIds[i];
3664
+ const newNum = propOffset + i + 1;
3665
+ const newId = `${targetCode}_P-${newNum}`;
3666
+ entries.set(oldId, newId);
3667
+ }
3668
+ }
3669
+ function buildComponentEntries(sourceCode, targetCode, sourceDesign, entries) {
3670
+ for (const compName of sourceDesign.componentNames) {
3671
+ const prefix = `${sourceCode}-`;
3672
+ if (compName.startsWith(prefix)) {
3673
+ const suffix = compName.slice(prefix.length);
3674
+ entries.set(compName, `${targetCode}-${suffix}`);
3675
+ }
3676
+ }
3677
+ }
3678
+ function findHighestReqNumber(reqFile) {
3679
+ let max = 0;
3680
+ for (const id of reqFile.requirementIds) {
3681
+ if (id.includes(".")) continue;
3682
+ const match = id.match(/-(\d+)$/);
3683
+ if (match) {
3684
+ const num = Number.parseInt(match[1], 10);
3685
+ if (num > max) max = num;
3686
+ }
3687
+ }
3688
+ return max;
3689
+ }
3690
+ function findHighestPropertyNumber(designFile) {
3691
+ let max = 0;
3692
+ for (const id of designFile.propertyIds) {
3693
+ const match = id.match(/_P-(\d+)$/);
3694
+ if (match) {
3695
+ const num = Number.parseInt(match[1], 10);
3696
+ if (num > max) max = num;
3697
+ }
3698
+ }
3699
+ return max;
3700
+ }
3701
+ function findSpecFile(specFiles, code, prefix) {
3702
+ return specFiles.find((sf) => {
3703
+ const name = basename6(sf.filePath);
3704
+ return name.startsWith(`${prefix}-${code}-`);
3705
+ });
3706
+ }
3707
+ var SPEC_PREFIXES = ["FEAT", "REQ", "DESIGN", "EXAMPLE", "API", "TASK"];
3708
+ function hasAnySpecFile(specFiles, code) {
3709
+ return specFiles.some((sf) => {
3710
+ const name = basename6(sf.filePath);
3711
+ return SPEC_PREFIXES.some((prefix) => name.startsWith(`${prefix}-${code}-`));
3712
+ });
3713
+ }
3714
+
3715
+ // src/commands/merge.ts
3716
+ async function mergeCommand(options) {
3717
+ try {
3718
+ validateMerge(options.sourceCode, options.targetCode);
3719
+ const { markers, specs } = await scan(options.config);
3720
+ const dryRun = options.dryRun === true;
3721
+ if (!hasAnySpecFile(specs.specFiles, options.targetCode)) {
3722
+ throw new MergeError(
3723
+ "TARGET_NOT_FOUND",
3724
+ `No spec files found for target code: ${options.targetCode}. Use \`awa spec recode\` to rename a code to a new one.`
3725
+ );
3726
+ }
3727
+ const { map, noChange: recodeNoChange } = buildRecodeMap(
3728
+ options.sourceCode,
3729
+ options.targetCode,
3730
+ specs
3731
+ );
3732
+ let affectedFiles = [];
3733
+ let totalReplacements = 0;
3734
+ if (!recodeNoChange) {
3735
+ const result2 = await propagate(map, specs, markers, dryRun);
3736
+ affectedFiles = result2.affectedFiles;
3737
+ totalReplacements = result2.totalReplacements;
3738
+ }
3739
+ const moves = await executeMoves(
3740
+ options.sourceCode,
3741
+ options.targetCode,
3742
+ specs.specFiles,
3743
+ dryRun
3744
+ );
3745
+ const movedSourcePaths = new Set(moves.map((m) => m.sourceFile));
3746
+ const propagatedPaths = new Set(affectedFiles.map((af) => af.filePath));
3747
+ const nonSourceFiles = specs.specFiles.filter(
3748
+ (sf) => !movedSourcePaths.has(sf.filePath) && !propagatedPaths.has(sf.filePath)
3749
+ );
3750
+ const staleRefs = await findStaleRefs(options.sourceCode, nonSourceFiles);
3751
+ const noChange = recodeNoChange && moves.length === 0;
3752
+ const result = {
3753
+ sourceCode: options.sourceCode,
3754
+ targetCode: options.targetCode,
3755
+ map,
3756
+ affectedFiles,
3757
+ totalReplacements,
3758
+ moves,
3759
+ staleRefs,
3760
+ noChange
3761
+ };
3762
+ outputResult(result, dryRun, options.json === true);
3763
+ if (staleRefs.length > 0) {
3764
+ return 2;
3765
+ }
3766
+ if (options.renumber && !dryRun && !noChange) {
3767
+ const { renumberCommand: renumberCommand2 } = await import("./renumber-ZCI2H5HZ.js");
3768
+ await renumberCommand2({
3769
+ code: options.targetCode,
3770
+ dryRun: false,
3771
+ json: options.json,
3772
+ config: options.config
3773
+ });
3774
+ }
3775
+ if (!dryRun && !noChange) {
3776
+ const { specs: freshSpecs, config: scanConfig } = await scan(options.config);
3777
+ await fixCodesTable(freshSpecs, scanConfig);
3778
+ }
3779
+ return noChange ? 0 : 1;
3780
+ } catch (err) {
3781
+ if (err instanceof MergeError) {
3782
+ logger.error(err.message);
3783
+ return 2;
3784
+ }
3785
+ if (err instanceof RecodeError) {
3786
+ logger.error(err.message);
3787
+ return 2;
3788
+ }
3789
+ if (err instanceof Error) {
3790
+ logger.error(err.message);
3791
+ } else {
3792
+ logger.error(String(err));
3793
+ }
3794
+ return 2;
3795
+ }
3796
+ }
3797
+ function outputResult(result, dryRun, json) {
3798
+ if (json) {
3799
+ process.stdout.write(`${formatJson2(result)}
3800
+ `);
3801
+ } else {
3802
+ const text = formatText(result, dryRun);
3803
+ logger.info(text);
3804
+ }
3805
+ }
3806
+
3807
+ // src/core/recode/reporter.ts
3808
+ function formatText2(result, dryRun) {
3809
+ const lines = [];
3810
+ if (dryRun) {
3811
+ lines.push("DRY RUN \u2014 no files were modified\n");
3812
+ }
3813
+ if (result.noChange) {
3814
+ lines.push(`${result.sourceCode} \u2192 ${result.targetCode}: no IDs to recode`);
3815
+ return lines.join("\n");
3816
+ }
3817
+ lines.push(
3818
+ `${result.sourceCode} \u2192 ${result.targetCode}: ${result.map.entries.size} ID(s) recoded
3819
+ `
3820
+ );
3821
+ lines.push(" Old ID \u2192 New ID");
3822
+ lines.push(` ${"\u2500".repeat(40)}`);
3823
+ for (const [oldId, newId] of result.map.entries) {
3824
+ lines.push(` ${oldId} \u2192 ${newId}`);
3825
+ }
3826
+ if (result.affectedFiles.length > 0) {
3827
+ lines.push("");
3828
+ lines.push(
3829
+ ` ${result.totalReplacements} replacement(s) in ${result.affectedFiles.length} file(s):`
3830
+ );
3831
+ for (const file of result.affectedFiles) {
3832
+ lines.push(` ${file.filePath} (${file.replacements.length})`);
3833
+ }
3834
+ }
3835
+ if (result.renames.length > 0) {
3836
+ lines.push("");
3837
+ lines.push(` ${result.renames.length} file(s) renamed:`);
3838
+ for (const r of result.renames) {
3839
+ lines.push(` ${r.oldPath} \u2192 ${r.newPath}`);
3840
+ }
3841
+ }
3842
+ if (result.staleRefs.length > 0) {
3843
+ lines.push("");
3844
+ lines.push(` \u2716 ${result.staleRefs.length} file(s) still reference ${result.sourceCode}:`);
3845
+ for (const ref of result.staleRefs) {
3846
+ lines.push(` ${ref}`);
3847
+ }
3848
+ }
3849
+ return lines.join("\n");
3850
+ }
3851
+ function formatJson3(result) {
3852
+ const output = {
3853
+ sourceCode: result.sourceCode,
3854
+ targetCode: result.targetCode,
3855
+ noChange: result.noChange,
3856
+ map: Object.fromEntries(result.map.entries),
3857
+ affectedFiles: result.affectedFiles.map((f) => ({
3858
+ filePath: f.filePath,
3859
+ replacements: f.replacements.map((r) => ({
3860
+ line: r.line,
3861
+ oldId: r.oldId,
3862
+ newId: r.newId
3863
+ }))
3864
+ })),
3865
+ totalReplacements: result.totalReplacements,
3866
+ renames: result.renames.map((r) => ({ oldPath: r.oldPath, newPath: r.newPath })),
3867
+ staleRefs: result.staleRefs
3868
+ };
3869
+ return JSON.stringify(output, null, 2);
3870
+ }
3871
+
3872
+ // src/commands/recode.ts
3873
+ async function recodeCommand(options) {
3874
+ try {
3875
+ const { markers, specs } = await scan(options.config);
3876
+ const dryRun = options.dryRun === true;
3877
+ const { map, noChange: recodeNoChange } = buildRecodeMap(
3878
+ options.sourceCode,
3879
+ options.targetCode,
3880
+ specs
3881
+ );
3882
+ let affectedFiles = [];
3883
+ let totalReplacements = 0;
3884
+ if (!recodeNoChange) {
3885
+ const result2 = await propagate(map, specs, markers, dryRun);
3886
+ affectedFiles = result2.affectedFiles;
3887
+ totalReplacements = result2.totalReplacements;
3888
+ }
3889
+ const renames = planRenames(options.sourceCode, options.targetCode, specs.specFiles);
3890
+ const conflicts = detectConflicts(renames, specs.specFiles);
3891
+ if (conflicts.length > 0) {
3892
+ throw new RecodeError(
3893
+ "RENAME_CONFLICT",
3894
+ `File rename conflict(s): ${conflicts.join(", ")}. Use \`awa spec merge\` to combine codes with overlapping files.`
3895
+ );
3896
+ }
3897
+ await executeRenames(renames, options.sourceCode, options.targetCode, dryRun);
3898
+ const propagatedPaths = new Set(affectedFiles.map((af) => af.filePath));
3899
+ const nonSourceFiles = specs.specFiles.filter(
3900
+ (sf) => !renames.some((r) => r.oldPath === sf.filePath) && !propagatedPaths.has(sf.filePath)
3901
+ );
3902
+ const staleRefs = await findStaleRefs(options.sourceCode, nonSourceFiles);
3903
+ const noChange = recodeNoChange && renames.length === 0;
3904
+ const result = {
3905
+ sourceCode: options.sourceCode,
3906
+ targetCode: options.targetCode,
3907
+ map,
3908
+ affectedFiles,
3909
+ totalReplacements,
3910
+ renames,
3911
+ staleRefs,
3912
+ noChange
3913
+ };
3914
+ outputResult2(result, dryRun, options.json === true);
3915
+ if (staleRefs.length > 0) {
3916
+ return 2;
3917
+ }
3918
+ if (!dryRun && !noChange) {
3919
+ const { specs: freshSpecs, config: scanConfig } = await scan(options.config);
3920
+ await fixCodesTable(freshSpecs, scanConfig);
3921
+ }
3922
+ return noChange ? 0 : 1;
3923
+ } catch (err) {
3924
+ if (err instanceof RecodeError) {
3925
+ logger.error(err.message);
3926
+ return 2;
3927
+ }
3928
+ if (err instanceof Error) {
3929
+ logger.error(err.message);
3930
+ } else {
3931
+ logger.error(String(err));
3932
+ }
3933
+ return 2;
3934
+ }
3935
+ }
3936
+ function outputResult2(result, dryRun, json) {
3937
+ if (json) {
3938
+ process.stdout.write(`${formatJson3(result)}
3939
+ `);
3940
+ } else {
3941
+ const text = formatText2(result, dryRun);
3942
+ logger.info(text);
3943
+ }
3944
+ }
3945
+
3171
3946
  // src/commands/test.ts
3172
3947
  import { intro as intro4, outro as outro4 } from "@clack/prompts";
3173
3948
 
3174
3949
  // src/core/template-test/fixture-loader.ts
3175
3950
  import { readdir as readdir2 } from "fs/promises";
3176
- import { basename as basename3, extname, join as join8 } from "path";
3951
+ import { basename as basename7, extname as extname2, join as join10 } from "path";
3177
3952
  import { parse } from "smol-toml";
3178
3953
  async function discoverFixtures(templatePath) {
3179
- const testsDir = join8(templatePath, "_tests");
3954
+ const testsDir = join10(templatePath, "_tests");
3180
3955
  let entries;
3181
3956
  try {
3182
3957
  const dirEntries = await readdir2(testsDir, { withFileTypes: true });
3183
- entries = dirEntries.filter((e) => e.isFile() && extname(e.name) === ".toml").map((e) => e.name).sort();
3958
+ entries = dirEntries.filter((e) => e.isFile() && extname2(e.name) === ".toml").map((e) => e.name).sort();
3184
3959
  } catch {
3185
3960
  return [];
3186
3961
  }
3187
3962
  const fixtures = [];
3188
3963
  for (const filename of entries) {
3189
- const filePath = join8(testsDir, filename);
3964
+ const filePath = join10(testsDir, filename);
3190
3965
  const fixture = await parseFixture(filePath);
3191
3966
  fixtures.push(fixture);
3192
3967
  }
@@ -3195,7 +3970,7 @@ async function discoverFixtures(templatePath) {
3195
3970
  async function parseFixture(filePath) {
3196
3971
  const content = await readTextFile(filePath);
3197
3972
  const parsed = parse(content);
3198
- const name = basename3(filePath, extname(filePath));
3973
+ const name = basename7(filePath, extname2(filePath));
3199
3974
  const features = toStringArray2(parsed.features) ?? [];
3200
3975
  const preset = toStringArray2(parsed.preset) ?? [];
3201
3976
  const removeFeatures = toStringArray2(parsed["remove-features"]) ?? [];
@@ -3217,7 +3992,7 @@ function toStringArray2(value) {
3217
3992
  }
3218
3993
 
3219
3994
  // src/core/template-test/reporter.ts
3220
- import chalk4 from "chalk";
3995
+ import chalk5 from "chalk";
3221
3996
  function report2(result, options) {
3222
3997
  if (options?.json) {
3223
3998
  reportJson2(result);
@@ -3228,11 +4003,11 @@ function report2(result, options) {
3228
4003
  reportFixture(fixture);
3229
4004
  }
3230
4005
  console.log("");
3231
- console.log(chalk4.bold("Test Summary:"));
4006
+ console.log(chalk5.bold("Test Summary:"));
3232
4007
  console.log(` Total: ${result.total}`);
3233
- console.log(chalk4.green(` Passed: ${result.passed}`));
4008
+ console.log(chalk5.green(` Passed: ${result.passed}`));
3234
4009
  if (result.failed > 0) {
3235
- console.log(chalk4.red(` Failed: ${result.failed}`));
4010
+ console.log(chalk5.red(` Failed: ${result.failed}`));
3236
4011
  }
3237
4012
  console.log("");
3238
4013
  }
@@ -3258,27 +4033,27 @@ function reportJson2(result) {
3258
4033
  console.log(JSON.stringify(output, null, 2));
3259
4034
  }
3260
4035
  function reportFixture(fixture) {
3261
- const icon = fixture.passed ? chalk4.green("\u2714") : chalk4.red("\u2716");
4036
+ const icon = fixture.passed ? chalk5.green("\u2714") : chalk5.red("\u2716");
3262
4037
  console.log(`${icon} ${fixture.name}`);
3263
4038
  if (fixture.error) {
3264
- console.log(chalk4.red(` Error: ${fixture.error}`));
4039
+ console.log(chalk5.red(` Error: ${fixture.error}`));
3265
4040
  return;
3266
4041
  }
3267
4042
  const missingFiles = fixture.fileResults.filter((r) => !r.found);
3268
4043
  for (const missing of missingFiles) {
3269
- console.log(chalk4.red(` Missing file: ${missing.path}`));
4044
+ console.log(chalk5.red(` Missing file: ${missing.path}`));
3270
4045
  }
3271
4046
  const snapshotFailures = fixture.snapshotResults.filter((r) => r.status !== "match");
3272
4047
  for (const failure of snapshotFailures) {
3273
4048
  switch (failure.status) {
3274
4049
  case "mismatch":
3275
- console.log(chalk4.yellow(` Snapshot mismatch: ${failure.path}`));
4050
+ console.log(chalk5.yellow(` Snapshot mismatch: ${failure.path}`));
3276
4051
  break;
3277
4052
  case "missing-snapshot":
3278
- console.log(chalk4.yellow(` Missing snapshot: ${failure.path}`));
4053
+ console.log(chalk5.yellow(` Missing snapshot: ${failure.path}`));
3279
4054
  break;
3280
4055
  case "extra-snapshot":
3281
- console.log(chalk4.yellow(` Extra snapshot (not in output): ${failure.path}`));
4056
+ console.log(chalk5.yellow(` Extra snapshot (not in output): ${failure.path}`));
3282
4057
  break;
3283
4058
  }
3284
4059
  }
@@ -3287,16 +4062,16 @@ function reportFixture(fixture) {
3287
4062
  // src/core/template-test/runner.ts
3288
4063
  import { mkdir as mkdir2, rm as rm3 } from "fs/promises";
3289
4064
  import { tmpdir as tmpdir3 } from "os";
3290
- import { join as join10 } from "path";
4065
+ import { join as join12 } from "path";
3291
4066
 
3292
4067
  // src/core/template-test/snapshot.ts
3293
4068
  import { mkdir, readdir as readdir3, rm as rm2 } from "fs/promises";
3294
- import { join as join9, relative as relative4 } from "path";
4069
+ import { join as join11, relative as relative4 } from "path";
3295
4070
  async function walkRelative(dir, base) {
3296
4071
  const results = [];
3297
4072
  const entries = await readdir3(dir, { withFileTypes: true });
3298
4073
  for (const entry of entries) {
3299
- const fullPath = join9(dir, entry.name);
4074
+ const fullPath = join11(dir, entry.name);
3300
4075
  if (entry.isDirectory()) {
3301
4076
  const sub = await walkRelative(fullPath, base);
3302
4077
  results.push(...sub);
@@ -3313,8 +4088,8 @@ async function compareSnapshots(renderedDir, snapshotDir) {
3313
4088
  const snapshotSet = new Set(snapshotFiles);
3314
4089
  const renderedSet = new Set(renderedFiles);
3315
4090
  for (const file of renderedFiles) {
3316
- const renderedPath = join9(renderedDir, file);
3317
- const snapshotPath = join9(snapshotDir, file);
4091
+ const renderedPath = join11(renderedDir, file);
4092
+ const snapshotPath = join11(snapshotDir, file);
3318
4093
  if (!snapshotSet.has(file)) {
3319
4094
  results.push({ path: file, status: "missing-snapshot" });
3320
4095
  continue;
@@ -3340,8 +4115,8 @@ async function updateSnapshots(renderedDir, snapshotDir) {
3340
4115
  await mkdir(snapshotDir, { recursive: true });
3341
4116
  const files = await walkRelative(renderedDir, renderedDir);
3342
4117
  for (const file of files) {
3343
- const srcPath = join9(renderedDir, file);
3344
- const destPath = join9(snapshotDir, file);
4118
+ const srcPath = join11(renderedDir, file);
4119
+ const destPath = join11(snapshotDir, file);
3345
4120
  const content = await readTextFile(srcPath);
3346
4121
  await writeTextFile(destPath, content);
3347
4122
  }
@@ -3349,7 +4124,7 @@ async function updateSnapshots(renderedDir, snapshotDir) {
3349
4124
 
3350
4125
  // src/core/template-test/runner.ts
3351
4126
  async function runFixture(fixture, templatePath, options, presetDefinitions = {}) {
3352
- const tempDir = join10(tmpdir3(), `awa-test-${fixture.name}-${Date.now()}`);
4127
+ const tempDir = join12(tmpdir3(), `awa-test-${fixture.name}-${Date.now()}`);
3353
4128
  try {
3354
4129
  await mkdir2(tempDir, { recursive: true });
3355
4130
  const features = featureResolver.resolve({
@@ -3368,12 +4143,12 @@ async function runFixture(fixture, templatePath, options, presetDefinitions = {}
3368
4143
  });
3369
4144
  const fileResults = [];
3370
4145
  for (const expectedFile of fixture.expectedFiles) {
3371
- const fullPath = join10(tempDir, expectedFile);
4146
+ const fullPath = join12(tempDir, expectedFile);
3372
4147
  const found = await pathExists(fullPath);
3373
4148
  fileResults.push({ path: expectedFile, found });
3374
4149
  }
3375
4150
  const missingFiles = fileResults.filter((r) => !r.found);
3376
- const snapshotDir = join10(templatePath, "_tests", fixture.name);
4151
+ const snapshotDir = join12(templatePath, "_tests", fixture.name);
3377
4152
  let snapshotResults = [];
3378
4153
  if (options.updateSnapshots) {
3379
4154
  await updateSnapshots(tempDir, snapshotDir);
@@ -3490,7 +4265,7 @@ async function testCommand(options) {
3490
4265
  }
3491
4266
 
3492
4267
  // src/core/trace/content-assembler.ts
3493
- import { readFile as readFile7 } from "fs/promises";
4268
+ import { readFile as readFile9 } from "fs/promises";
3494
4269
  var DEFAULT_BEFORE_CONTEXT = 5;
3495
4270
  var DEFAULT_AFTER_CONTEXT = 20;
3496
4271
  async function assembleContent(result, taskPath, contextOptions) {
@@ -3498,8 +4273,9 @@ async function assembleContent(result, taskPath, contextOptions) {
3498
4273
  const afterCtx = contextOptions?.afterContext ?? DEFAULT_AFTER_CONTEXT;
3499
4274
  const sections = [];
3500
4275
  const seen = /* @__PURE__ */ new Set();
4276
+ const fileCache = /* @__PURE__ */ new Map();
3501
4277
  if (taskPath) {
3502
- const content = await safeReadFile(taskPath);
4278
+ const content = await cachedReadFile(fileCache, taskPath);
3503
4279
  if (content) {
3504
4280
  const lineCount = content.split("\n").length;
3505
4281
  sections.push({
@@ -3518,6 +4294,7 @@ async function assembleContent(result, taskPath, contextOptions) {
3518
4294
  if (!seen.has(key)) {
3519
4295
  seen.add(key);
3520
4296
  const section = await extractSpecSection(
4297
+ fileCache,
3521
4298
  chain.requirement.location.filePath,
3522
4299
  chain.requirement.id,
3523
4300
  chain.requirement.location.line,
@@ -3533,6 +4310,7 @@ async function assembleContent(result, taskPath, contextOptions) {
3533
4310
  seen.add(key);
3534
4311
  if (!chain.requirement || ac.location.filePath !== chain.requirement.location.filePath) {
3535
4312
  const section = await extractSpecSection(
4313
+ fileCache,
3536
4314
  ac.location.filePath,
3537
4315
  ac.id,
3538
4316
  ac.location.line,
@@ -3548,6 +4326,7 @@ async function assembleContent(result, taskPath, contextOptions) {
3548
4326
  if (!seen.has(key)) {
3549
4327
  seen.add(key);
3550
4328
  const section = await extractSpecSection(
4329
+ fileCache,
3551
4330
  comp.location.filePath,
3552
4331
  comp.id,
3553
4332
  comp.location.line,
@@ -3562,6 +4341,7 @@ async function assembleContent(result, taskPath, contextOptions) {
3562
4341
  if (!seen.has(key)) {
3563
4342
  seen.add(key);
3564
4343
  const section = await extractCodeSection(
4344
+ fileCache,
3565
4345
  impl.location.filePath,
3566
4346
  impl.location.line,
3567
4347
  "implementation",
@@ -3577,6 +4357,7 @@ async function assembleContent(result, taskPath, contextOptions) {
3577
4357
  if (!seen.has(key)) {
3578
4358
  seen.add(key);
3579
4359
  const section = await extractCodeSection(
4360
+ fileCache,
3580
4361
  t.location.filePath,
3581
4362
  t.location.line,
3582
4363
  "test",
@@ -3592,6 +4373,7 @@ async function assembleContent(result, taskPath, contextOptions) {
3592
4373
  if (!seen.has(key)) {
3593
4374
  seen.add(key);
3594
4375
  const section = await extractSpecSection(
4376
+ fileCache,
3595
4377
  prop.location.filePath,
3596
4378
  prop.id,
3597
4379
  prop.location.line,
@@ -3605,8 +4387,8 @@ async function assembleContent(result, taskPath, contextOptions) {
3605
4387
  sections.sort((a, b) => a.priority - b.priority);
3606
4388
  return sections;
3607
4389
  }
3608
- async function extractSpecSection(filePath, _id, line, type, priority) {
3609
- const content = await safeReadFile(filePath);
4390
+ async function extractSpecSection(fileCache, filePath, _id, line, type, priority) {
4391
+ const content = await cachedReadFile(fileCache, filePath);
3610
4392
  if (!content) return null;
3611
4393
  const lines = content.split("\n");
3612
4394
  let startIdx = line - 1;
@@ -3630,8 +4412,8 @@ async function extractSpecSection(filePath, _id, line, type, priority) {
3630
4412
  priority
3631
4413
  };
3632
4414
  }
3633
- async function extractCodeSection(filePath, line, type, priority, beforeContext = DEFAULT_BEFORE_CONTEXT, afterContext = DEFAULT_AFTER_CONTEXT) {
3634
- const content = await safeReadFile(filePath);
4415
+ async function extractCodeSection(fileCache, filePath, line, type, priority, beforeContext = DEFAULT_BEFORE_CONTEXT, afterContext = DEFAULT_AFTER_CONTEXT) {
4416
+ const content = await cachedReadFile(fileCache, filePath);
3635
4417
  if (!content) return null;
3636
4418
  const lines = content.split("\n");
3637
4419
  const lineIdx = line - 1;
@@ -3687,12 +4469,16 @@ function findEnclosingBlock(lines, lineIdx, beforeContext = DEFAULT_BEFORE_CONTE
3687
4469
  }
3688
4470
  return { start, end };
3689
4471
  }
3690
- async function safeReadFile(filePath) {
4472
+ async function cachedReadFile(cache, filePath) {
4473
+ if (cache.has(filePath)) return cache.get(filePath) ?? null;
4474
+ let content;
3691
4475
  try {
3692
- return await readFile7(filePath, "utf-8");
4476
+ content = await readFile9(filePath, "utf-8");
3693
4477
  } catch {
3694
- return null;
4478
+ content = null;
3695
4479
  }
4480
+ cache.set(filePath, content);
4481
+ return content;
3696
4482
  }
3697
4483
 
3698
4484
  // src/core/trace/content-formatter.ts
@@ -3855,7 +4641,7 @@ function formatList(result) {
3855
4641
  }
3856
4642
  return [...paths].join("\n");
3857
4643
  }
3858
- function formatJson(result) {
4644
+ function formatJson4(result) {
3859
4645
  const output = {
3860
4646
  chains: result.chains.map((chain) => ({
3861
4647
  queryId: chain.queryId,
@@ -3921,10 +4707,6 @@ function buildTraceIndex(specs, markers) {
3921
4707
  ...specs.propertyIds,
3922
4708
  ...specs.componentNames
3923
4709
  ]);
3924
- const idLocations = /* @__PURE__ */ new Map();
3925
- for (const [id, loc] of specs.idLocations) {
3926
- idLocations.set(id, loc);
3927
- }
3928
4710
  return {
3929
4711
  reqToACs,
3930
4712
  acToDesignComponents,
@@ -3935,7 +4717,7 @@ function buildTraceIndex(specs, markers) {
3935
4717
  acToReq,
3936
4718
  componentToACs,
3937
4719
  propertyToACs,
3938
- idLocations,
4720
+ idLocations: specs.idLocations,
3939
4721
  allIds
3940
4722
  };
3941
4723
  }
@@ -4041,7 +4823,7 @@ function pushToMap(map, key, value) {
4041
4823
  }
4042
4824
 
4043
4825
  // src/core/trace/input-resolver.ts
4044
- import { readFile as readFile8 } from "fs/promises";
4826
+ import { readFile as readFile10 } from "fs/promises";
4045
4827
  function resolveIds(ids, index) {
4046
4828
  const resolved = [];
4047
4829
  const warnings = [];
@@ -4057,7 +4839,7 @@ function resolveIds(ids, index) {
4057
4839
  async function resolveTaskFile(taskPath, index) {
4058
4840
  let content;
4059
4841
  try {
4060
- content = await readFile8(taskPath, "utf-8");
4842
+ content = await readFile10(taskPath, "utf-8");
4061
4843
  } catch {
4062
4844
  return { ids: [], warnings: [`Task file not found: ${taskPath}`] };
4063
4845
  }
@@ -4108,7 +4890,7 @@ async function resolveTaskFile(taskPath, index) {
4108
4890
  async function resolveSourceFile(filePath, index) {
4109
4891
  let content;
4110
4892
  try {
4111
- content = await readFile8(filePath, "utf-8");
4893
+ content = await readFile10(filePath, "utf-8");
4112
4894
  } catch {
4113
4895
  return { ids: [], warnings: [`Source file not found: ${filePath}`] };
4114
4896
  }
@@ -4388,44 +5170,6 @@ function deduplicateNodes(nodes) {
4388
5170
  return result;
4389
5171
  }
4390
5172
 
4391
- // src/core/trace/scanner.ts
4392
- function buildScanConfig(fileConfig, overrides) {
4393
- const section = fileConfig?.check;
4394
- return {
4395
- specGlobs: toStringArray3(section?.["spec-globs"]) ?? [...DEFAULT_CHECK_CONFIG.specGlobs],
4396
- codeGlobs: toStringArray3(section?.["code-globs"]) ?? [...DEFAULT_CHECK_CONFIG.codeGlobs],
4397
- specIgnore: toStringArray3(section?.["spec-ignore"]) ?? [...DEFAULT_CHECK_CONFIG.specIgnore],
4398
- codeIgnore: toStringArray3(section?.["code-ignore"]) ?? [...DEFAULT_CHECK_CONFIG.codeIgnore],
4399
- ignoreMarkers: toStringArray3(section?.["ignore-markers"]) ?? [
4400
- ...DEFAULT_CHECK_CONFIG.ignoreMarkers
4401
- ],
4402
- markers: toStringArray3(section?.markers) ?? [...DEFAULT_CHECK_CONFIG.markers],
4403
- idPattern: typeof section?.["id-pattern"] === "string" ? section["id-pattern"] : DEFAULT_CHECK_CONFIG.idPattern,
4404
- crossRefPatterns: toStringArray3(section?.["cross-ref-patterns"]) ?? [
4405
- ...DEFAULT_CHECK_CONFIG.crossRefPatterns
4406
- ],
4407
- format: DEFAULT_CHECK_CONFIG.format,
4408
- schemaDir: DEFAULT_CHECK_CONFIG.schemaDir,
4409
- schemaEnabled: false,
4410
- allowWarnings: true,
4411
- specOnly: false,
4412
- fix: true,
4413
- ...overrides
4414
- };
4415
- }
4416
- async function scan(configPath) {
4417
- const fileConfig = await configLoader.load(configPath ?? null);
4418
- const config = buildScanConfig(fileConfig);
4419
- const [markers, specs] = await Promise.all([scanMarkers(config), parseSpecs(config)]);
4420
- return { markers, specs, config };
4421
- }
4422
- function toStringArray3(value) {
4423
- if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
4424
- return value;
4425
- }
4426
- return null;
4427
- }
4428
-
4429
5173
  // src/core/trace/token-estimator.ts
4430
5174
  function estimateTokens(text) {
4431
5175
  return Math.ceil(text.length / 4);
@@ -4543,7 +5287,7 @@ async function traceCommand(options) {
4543
5287
  } else if (options.list) {
4544
5288
  output = formatList(result);
4545
5289
  } else if (options.json) {
4546
- output = formatJson(result);
5290
+ output = formatJson4(result);
4547
5291
  } else {
4548
5292
  output = formatTree(result);
4549
5293
  }
@@ -4565,7 +5309,7 @@ async function traceCommand(options) {
4565
5309
  }
4566
5310
 
4567
5311
  // src/utils/update-check.ts
4568
- import chalk5 from "chalk";
5312
+ import chalk6 from "chalk";
4569
5313
  function compareSemver(a, b) {
4570
5314
  const partsA = a.split(".").map((s) => Number.parseInt(s, 10));
4571
5315
  const partsB = b.split(".").map((s) => Number.parseInt(s, 10));
@@ -4606,28 +5350,28 @@ function printUpdateWarning(log, result) {
4606
5350
  console.log("");
4607
5351
  if (result.isMajorBump) {
4608
5352
  log.warn(
4609
- chalk5.yellow(
5353
+ chalk6.yellow(
4610
5354
  `New major version available: ${result.current} \u2192 ${result.latest} (breaking changes)`
4611
5355
  )
4612
5356
  );
4613
- log.warn(chalk5.dim(" See https://github.com/ncoderz/awa/releases for details"));
5357
+ log.warn(chalk6.dim(" See https://github.com/ncoderz/awa/releases for details"));
4614
5358
  } else {
4615
- log.warn(chalk5.yellow(`Update available: ${result.current} \u2192 ${result.latest}`));
5359
+ log.warn(chalk6.yellow(`Update available: ${result.current} \u2192 ${result.latest}`));
4616
5360
  }
4617
- log.warn(chalk5.dim(" Run `npm install -g @ncoderz/awa` to update"));
5361
+ log.warn(chalk6.dim(" Run `npm install -g @ncoderz/awa` to update"));
4618
5362
  console.log("");
4619
5363
  }
4620
5364
 
4621
5365
  // src/utils/update-check-cache.ts
4622
- import { mkdir as mkdir3, readFile as readFile9, writeFile as writeFile2 } from "fs/promises";
5366
+ import { mkdir as mkdir3, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
4623
5367
  import { homedir } from "os";
4624
- import { dirname, join as join11 } from "path";
4625
- var CACHE_DIR = join11(homedir(), ".cache", "awa");
4626
- var CACHE_FILE = join11(CACHE_DIR, "update-check.json");
5368
+ import { dirname as dirname3, join as join13 } from "path";
5369
+ var CACHE_DIR = join13(homedir(), ".cache", "awa");
5370
+ var CACHE_FILE = join13(CACHE_DIR, "update-check.json");
4627
5371
  var DEFAULT_INTERVAL_MS = 864e5;
4628
5372
  async function shouldCheck(intervalMs = DEFAULT_INTERVAL_MS) {
4629
5373
  try {
4630
- const raw = await readFile9(CACHE_FILE, "utf-8");
5374
+ const raw = await readFile11(CACHE_FILE, "utf-8");
4631
5375
  const data = JSON.parse(raw);
4632
5376
  if (typeof data.timestamp !== "number" || typeof data.latestVersion !== "string") {
4633
5377
  return true;
@@ -4639,12 +5383,12 @@ async function shouldCheck(intervalMs = DEFAULT_INTERVAL_MS) {
4639
5383
  }
4640
5384
  async function writeCache(latestVersion) {
4641
5385
  try {
4642
- await mkdir3(dirname(CACHE_FILE), { recursive: true });
5386
+ await mkdir3(dirname3(CACHE_FILE), { recursive: true });
4643
5387
  const data = {
4644
5388
  timestamp: Date.now(),
4645
5389
  latestVersion
4646
5390
  };
4647
- await writeFile2(CACHE_FILE, JSON.stringify(data), "utf-8");
5391
+ await writeFile4(CACHE_FILE, JSON.stringify(data), "utf-8");
4648
5392
  } catch {
4649
5393
  }
4650
5394
  }
@@ -4771,29 +5515,85 @@ template.command("test").description("Run template test fixtures to verify expec
4771
5515
  process.exit(exitCode);
4772
5516
  });
4773
5517
  program.addCommand(template);
4774
- program.command("trace").description("Explore traceability chains and assemble context from specs, code, and tests").argument("[ids...]", "Traceability ID(s) to trace").option("--all", "Trace all known IDs in the project", false).option("--task <path>", "Resolve IDs from a task file").option("--file <path>", "Resolve IDs from a source file's markers").option("--content", "Output actual file sections instead of locations", false).option("--list", "Output file paths only (no content or tree)", false).option("--max-tokens <n>", "Cap content output size (implies --content)").option("--depth <n>", "Maximum traversal depth").option("--scope <code>", "Limit results to a feature code").option("--direction <dir>", "Traversal direction: both, forward, reverse", "both").option("--no-code", "Exclude source code (spec-only context)").option("--no-tests", "Exclude test files").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).option("-A <n>", "Lines of context after a code marker (--content only; default: 20)").option("-B <n>", "Lines of context before a code marker (--content only; default: 5)").option("-C <n>", "Lines of context before and after (--content only; overrides -A and -B)").option("-c, --config <path>", "Path to configuration file").action(async (ids, options) => {
4775
- const traceOptions = {
4776
- ids,
4777
- all: options.all,
4778
- task: options.task,
4779
- file: options.file,
4780
- content: options.content,
4781
- list: options.list,
5518
+ var spec = new Command("spec").description(
5519
+ "Spec operations (trace, renumber, recode, merge, codes)"
5520
+ );
5521
+ function configureTraceCommand(cmd) {
5522
+ return cmd.description("Explore traceability chains and assemble context from specs, code, and tests").argument("[ids...]", "Traceability ID(s) to trace").option("--all", "Trace all known IDs in the project", false).option("--task <path>", "Resolve IDs from a task file").option("--file <path>", "Resolve IDs from a source file's markers").option("--content", "Output actual file sections instead of locations", false).option("--list", "Output file paths only (no content or tree)", false).option("--max-tokens <n>", "Cap content output size (implies --content)").option("--depth <n>", "Maximum traversal depth").option("--scope <code>", "Limit results to a feature code").option("--direction <dir>", "Traversal direction: both, forward, reverse", "both").option("--no-code", "Exclude source code (spec-only context)").option("--no-tests", "Exclude test files").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).option("-A <n>", "Lines of context after a code marker (--content only; default: 20)").option("-B <n>", "Lines of context before a code marker (--content only; default: 5)").option("-C <n>", "Lines of context before and after (--content only; overrides -A and -B)").option("-c, --config <path>", "Path to configuration file").action(async (ids, options) => {
5523
+ const traceOptions = {
5524
+ ids,
5525
+ all: options.all,
5526
+ task: options.task,
5527
+ file: options.file,
5528
+ content: options.content,
5529
+ list: options.list,
5530
+ json: options.json,
5531
+ summary: options.summary,
5532
+ maxTokens: options.maxTokens !== void 0 ? Number(options.maxTokens) : void 0,
5533
+ depth: options.depth !== void 0 ? Number(options.depth) : void 0,
5534
+ scope: options.scope,
5535
+ direction: options.direction,
5536
+ noCode: options.code === false,
5537
+ noTests: options.tests === false,
5538
+ beforeContext: options.C !== void 0 ? Number(options.C) : options.B !== void 0 ? Number(options.B) : void 0,
5539
+ afterContext: options.C !== void 0 ? Number(options.C) : options.A !== void 0 ? Number(options.A) : void 0,
5540
+ config: options.config
5541
+ };
5542
+ const exitCode = await traceCommand(traceOptions);
5543
+ process.exit(exitCode);
5544
+ });
5545
+ }
5546
+ configureTraceCommand(spec.command("trace"));
5547
+ function configureRenumberCommand(cmd) {
5548
+ return cmd.description("Renumber traceability IDs to match document order").argument("[code]", "Feature code to renumber (e.g. CHK, TRC)").option("--all", "Renumber all feature codes", false).option("--dry-run", "Preview changes without modifying files", false).option("--json", "Output results as JSON", false).option("-c, --config <path>", "Path to configuration file").action(async (code, options) => {
5549
+ const renumberOptions = {
5550
+ code,
5551
+ all: options.all,
5552
+ dryRun: options.dryRun,
5553
+ json: options.json,
5554
+ config: options.config
5555
+ };
5556
+ const exitCode = await renumberCommand(renumberOptions);
5557
+ process.exit(exitCode);
5558
+ });
5559
+ }
5560
+ configureRenumberCommand(spec.command("renumber"));
5561
+ spec.command("recode").description("Recode traceability IDs from one feature code to another and rename spec files").argument("<source>", "Source feature code to recode from (e.g. CHK)").argument("<target>", "Target feature code to recode into (e.g. CLI)").option("--dry-run", "Preview changes without modifying files", false).option("--json", "Output results as JSON", false).option("--renumber", "Renumber target code after recode", false).option("-c, --config <path>", "Path to configuration file").action(async (source, target, options) => {
5562
+ const recodeOptions = {
5563
+ sourceCode: source,
5564
+ targetCode: target,
5565
+ dryRun: options.dryRun,
5566
+ json: options.json,
5567
+ renumber: options.renumber,
5568
+ config: options.config
5569
+ };
5570
+ const exitCode = await recodeCommand(recodeOptions);
5571
+ process.exit(exitCode);
5572
+ });
5573
+ spec.command("merge").description("Merge one feature code into another (recode + content merge + cleanup)").argument("<source>", "Source feature code to merge from (e.g. CHK)").argument("<target>", "Target feature code to merge into (e.g. CLI)").option("--dry-run", "Preview changes without modifying files", false).option("--json", "Output results as JSON", false).option("--renumber", "Renumber target code after merge", false).option("-c, --config <path>", "Path to configuration file").action(async (source, target, options) => {
5574
+ const mergeOptions = {
5575
+ sourceCode: source,
5576
+ targetCode: target,
5577
+ dryRun: options.dryRun,
5578
+ json: options.json,
5579
+ renumber: options.renumber,
5580
+ config: options.config
5581
+ };
5582
+ const exitCode = await mergeCommand(mergeOptions);
5583
+ process.exit(exitCode);
5584
+ });
5585
+ spec.command("codes").description("List all feature codes with requirement counts and scope summaries").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).option("-c, --config <path>", "Path to configuration file").action(async (options) => {
5586
+ const codesOptions = {
4782
5587
  json: options.json,
4783
5588
  summary: options.summary,
4784
- maxTokens: options.maxTokens !== void 0 ? Number(options.maxTokens) : void 0,
4785
- depth: options.depth !== void 0 ? Number(options.depth) : void 0,
4786
- scope: options.scope,
4787
- direction: options.direction,
4788
- noCode: options.code === false,
4789
- noTests: options.tests === false,
4790
- beforeContext: options.C !== void 0 ? Number(options.C) : options.B !== void 0 ? Number(options.B) : void 0,
4791
- afterContext: options.C !== void 0 ? Number(options.C) : options.A !== void 0 ? Number(options.A) : void 0,
4792
5589
  config: options.config
4793
5590
  };
4794
- const exitCode = await traceCommand(traceOptions);
5591
+ const exitCode = await codesCommand(codesOptions);
4795
5592
  process.exit(exitCode);
4796
5593
  });
5594
+ program.addCommand(spec);
5595
+ configureTraceCommand(program.command("trace"));
5596
+ configureRenumberCommand(program.command("renumber", { hidden: true }));
4797
5597
  var updateCheckPromise = null;
4798
5598
  var isJsonOrSummary = process.argv.includes("--json") || process.argv.includes("--summary");
4799
5599
  var isTTY = process.stdout.isTTY === true;
@@ -4801,7 +5601,7 @@ var isDisabledByEnv = !!process.env.NO_UPDATE_NOTIFIER;
4801
5601
  if (!isJsonOrSummary && isTTY && !isDisabledByEnv) {
4802
5602
  updateCheckPromise = (async () => {
4803
5603
  try {
4804
- const { configLoader: configLoader2 } = await import("./config-WL3SLSP6.js");
5604
+ const { configLoader: configLoader2 } = await import("./config-EJIXC7D7.js");
4805
5605
  const configPath = process.argv.indexOf("-c") !== -1 ? process.argv[process.argv.indexOf("-c") + 1] : process.argv.indexOf("--config") !== -1 ? process.argv[process.argv.indexOf("--config") + 1] : void 0;
4806
5606
  const fileConfig = await configLoader2.load(configPath ?? null);
4807
5607
  const updateCheckConfig = fileConfig?.["update-check"];
@@ -4830,5 +5630,5 @@ program.hook("postAction", async () => {
4830
5630
  } catch {
4831
5631
  }
4832
5632
  });
4833
- program.parseAsync();
5633
+ void program.parseAsync();
4834
5634
  //# sourceMappingURL=index.js.map