@ncoderz/awa 1.7.1 → 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.
- package/LICENSE +23 -16
- package/README.md +25 -27
- package/dist/{chunk-OQZTQ5ZI.js → chunk-LRQWZCYL.js} +1 -4
- package/dist/chunk-LRQWZCYL.js.map +1 -0
- package/dist/chunk-WGGMDWBE.js +760 -0
- package/dist/chunk-WGGMDWBE.js.map +1 -0
- package/dist/{config-WL3SLSP6.js → config-EJIXC7D7.js} +2 -2
- package/dist/index.js +1285 -466
- package/dist/index.js.map +1 -1
- package/dist/renumber-ZCI2H5HZ.js +9 -0
- package/dist/renumber-ZCI2H5HZ.js.map +1 -0
- package/package.json +13 -6
- package/templates/awa/.agent/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.agent/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.agent/workflows/spec-merge.md +3 -0
- package/templates/awa/.agent/workflows/spec-tidy.md +3 -0
- package/templates/awa/.agents/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.agents/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.awa/.agent/schemas/ALIGN_REPORT.schema.yaml +1 -1
- package/templates/awa/.awa/.agent/schemas/API.schema.yaml +1 -1
- package/templates/awa/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +7 -0
- package/templates/awa/.awa/.agent/schemas/DESIGN.schema.yaml +1 -1
- package/templates/awa/.awa/.agent/schemas/{EXAMPLES.schema.yaml → EXAMPLE.schema.yaml} +4 -4
- package/templates/awa/.awa/.agent/schemas/FEAT.schema.yaml +8 -1
- package/templates/awa/.awa/.agent/schemas/PLAN.schema.yaml +1 -1
- package/templates/awa/.awa/.agent/schemas/README.schema.yaml +1 -1
- package/templates/awa/.awa/.agent/schemas/REQ.schema.yaml +1 -1
- package/templates/awa/.awa/.agent/schemas/TASK.schema.yaml +1 -1
- package/templates/awa/.claude/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.claude/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.gemini/commands/spec-merge.md +3 -0
- package/templates/awa/.gemini/commands/spec-tidy.md +3 -0
- package/templates/awa/.gemini/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.gemini/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.github/prompts/awa.spec-merge.prompt.md +8 -0
- package/templates/awa/.github/prompts/awa.spec-tidy.prompt.md +7 -0
- package/templates/awa/.github/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.github/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.kilocode/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.kilocode/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.kilocode/workflows/spec-merge.md +3 -0
- package/templates/awa/.kilocode/workflows/spec-tidy.md +3 -0
- package/templates/awa/.opencode/commands/spec-merge.md +3 -0
- package/templates/awa/.opencode/commands/spec-tidy.md +3 -0
- package/templates/awa/.opencode/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.opencode/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.qwen/commands/spec-merge.md +3 -0
- package/templates/awa/.qwen/commands/spec-tidy.md +3 -0
- package/templates/awa/.qwen/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.qwen/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.roo/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.roo/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/.windsurf/skills/spec-merge/SKILL.md +3 -0
- package/templates/awa/.windsurf/skills/spec-tidy/SKILL.md +3 -0
- package/templates/awa/_delete.txt +4 -0
- package/templates/awa/_partials/_cmd.spec-merge.md +6 -0
- package/templates/awa/_partials/_cmd.spec-tidy.md +5 -0
- package/templates/awa/_partials/_skill.spec-merge.md +6 -0
- package/templates/awa/_partials/_skill.spec-tidy.md +6 -0
- package/templates/awa/_partials/awa.align.md +1 -1
- package/templates/awa/_partials/awa.brainstorm.md +1 -1
- package/templates/awa/_partials/awa.code.md +1 -1
- package/templates/awa/_partials/awa.core.md +8 -4
- package/templates/awa/_partials/awa.design.md +3 -2
- package/templates/awa/_partials/awa.documentation.md +1 -1
- package/templates/awa/_partials/awa.examples.md +4 -4
- package/templates/awa/_partials/awa.feature.md +2 -1
- package/templates/awa/_partials/awa.plan.md +2 -2
- package/templates/awa/_partials/awa.requirements.md +4 -2
- package/templates/awa/_partials/awa.spec-merge.md +97 -0
- package/templates/awa/_partials/awa.spec.tidy.md +92 -0
- package/templates/awa/_partials/awa.tasks.md +1 -1
- package/templates/awa/_partials/awa.upgrade.md +3 -3
- package/templates/awa/_partials/awa.usage.md +74 -6
- package/templates/awa/_partials/awa.vibe.md +1 -1
- package/templates/awa/_tests/claude/.awa/.agent/awa.core.md +126 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/ALIGN_REPORT.schema.yaml +83 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/API.schema.yaml +7 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +257 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/DESIGN.schema.yaml +351 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/EXAMPLE.schema.yaml +89 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/FEAT.schema.yaml +142 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/PLAN.schema.yaml +146 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/README.schema.yaml +137 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/REQ.schema.yaml +160 -0
- package/templates/awa/_tests/claude/.awa/.agent/schemas/TASK.schema.yaml +204 -0
- package/templates/awa/_tests/claude/.claude/agents/awa.md +137 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-align/SKILL.md +67 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-architecture/SKILL.md +50 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-brainstorm/SKILL.md +57 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-check/SKILL.md +79 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-code/SKILL.md +179 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-design/SKILL.md +62 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-documentation/SKILL.md +91 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-examples/SKILL.md +58 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-feature/SKILL.md +56 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-plan/SKILL.md +53 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-refactor/SKILL.md +53 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-requirements/SKILL.md +58 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-tasks/SKILL.md +158 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-upgrade/SKILL.md +68 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-usage/SKILL.md +368 -0
- package/templates/awa/_tests/claude/.claude/skills/awa-vibe/SKILL.md +72 -0
- package/templates/awa/_tests/claude/.claude/skills/spec-merge/SKILL.md +102 -0
- package/templates/awa/_tests/claude/.claude/skills/spec-tidy/SKILL.md +97 -0
- package/templates/awa/_tests/claude/CLAUDE.md +132 -0
- package/templates/awa/_tests/copilot/.awa/.agent/awa.core.md +126 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/ALIGN_REPORT.schema.yaml +83 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/API.schema.yaml +7 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +257 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/DESIGN.schema.yaml +351 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/EXAMPLE.schema.yaml +89 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/FEAT.schema.yaml +142 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/PLAN.schema.yaml +146 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/README.schema.yaml +137 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/REQ.schema.yaml +160 -0
- package/templates/awa/_tests/copilot/.awa/.agent/schemas/TASK.schema.yaml +204 -0
- package/templates/awa/_tests/copilot/.github/agents/awa.agent.md +137 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.align.prompt.md +67 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.architecture.prompt.md +50 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.brainstorm.prompt.md +57 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.check.prompt.md +79 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.code.prompt.md +179 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.design.prompt.md +62 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.documentation.prompt.md +91 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.examples.prompt.md +58 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.feature.prompt.md +56 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.plan.prompt.md +53 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.refactor.prompt.md +53 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.requirements.prompt.md +58 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.spec-merge.prompt.md +102 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.spec-tidy.prompt.md +96 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.tasks.prompt.md +158 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.upgrade.prompt.md +68 -0
- package/templates/awa/_tests/copilot/.github/prompts/awa.vibe.prompt.md +72 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-align/SKILL.md +67 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-architecture/SKILL.md +50 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-brainstorm/SKILL.md +57 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-check/SKILL.md +79 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-code/SKILL.md +179 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-design/SKILL.md +62 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-documentation/SKILL.md +91 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-examples/SKILL.md +58 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-feature/SKILL.md +56 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-plan/SKILL.md +53 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-refactor/SKILL.md +53 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-requirements/SKILL.md +58 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-tasks/SKILL.md +158 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-upgrade/SKILL.md +68 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-usage/SKILL.md +368 -0
- package/templates/awa/_tests/copilot/.github/skills/awa-vibe/SKILL.md +72 -0
- package/templates/awa/_tests/copilot/.github/skills/spec-merge/SKILL.md +102 -0
- package/templates/awa/_tests/copilot/.github/skills/spec-tidy/SKILL.md +97 -0
- package/dist/chunk-OQZTQ5ZI.js.map +0 -1
- /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-
|
|
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.
|
|
37
|
+
"version": "1.8.0",
|
|
28
38
|
"author": "Richard Sewell <richard.sewell@ncoderz.com>",
|
|
29
|
-
"license": "
|
|
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,158 +211,330 @@ function checkCodeAgainstSpec(markers, specs, config) {
|
|
|
201
211
|
return { findings };
|
|
202
212
|
}
|
|
203
213
|
|
|
204
|
-
// src/core/check/
|
|
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/
|
|
208
|
-
import {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
for (const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
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);
|
|
246
333
|
}
|
|
247
|
-
const result = await scanFile(filePath, config.markers);
|
|
248
|
-
markers.push(...result.markers);
|
|
249
|
-
findings.push(...result.findings);
|
|
250
334
|
}
|
|
251
|
-
|
|
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
|
|
254
|
-
const
|
|
255
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
let content;
|
|
372
|
+
async function readContent(ref) {
|
|
373
|
+
if (ref.content != null) return ref.content;
|
|
260
374
|
try {
|
|
261
|
-
|
|
375
|
+
return await readFile(ref.filePath, "utf-8");
|
|
262
376
|
} catch {
|
|
263
|
-
return
|
|
377
|
+
return void 0;
|
|
264
378
|
}
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
387
|
+
return map;
|
|
388
|
+
}
|
|
389
|
+
function extractScopeBoundary(content) {
|
|
269
390
|
const lines = content.split("\n");
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
400
|
+
const trimmed = line.trim();
|
|
401
|
+
if (paragraphLines.length === 0 && trimmed === "") {
|
|
284
402
|
continue;
|
|
285
403
|
}
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
continue;
|
|
404
|
+
if (paragraphLines.length > 0 && (trimmed === "" || /^#/.test(trimmed))) {
|
|
405
|
+
break;
|
|
289
406
|
}
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
426
|
+
const trimmed = line.trim();
|
|
427
|
+
if (paragraphLines.length === 0 && trimmed === "") {
|
|
295
428
|
continue;
|
|
296
429
|
}
|
|
297
|
-
|
|
298
|
-
|
|
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);
|
|
434
|
+
}
|
|
435
|
+
const paragraph = paragraphLines.join(" ").trim();
|
|
436
|
+
if (paragraph.length > 120) {
|
|
437
|
+
return `${paragraph.slice(0, 117)}...`;
|
|
324
438
|
}
|
|
325
|
-
return
|
|
439
|
+
return paragraph;
|
|
326
440
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
336
|
-
|
|
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
|
|
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
|
-
const
|
|
519
|
+
const reqFileMaps = buildReqFileMaps(specs.specFiles);
|
|
344
520
|
const fileResults = [];
|
|
345
521
|
for (const specFile of specs.specFiles) {
|
|
346
|
-
const fileName =
|
|
522
|
+
const fileName = basename3(specFile.filePath);
|
|
347
523
|
if (fileName.startsWith("DESIGN-")) {
|
|
348
|
-
const changed = await fixDesignMatrix(
|
|
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
532
|
const changed = await fixTaskMatrix(
|
|
352
533
|
specFile.filePath,
|
|
353
|
-
|
|
534
|
+
reqFileMaps,
|
|
354
535
|
specs,
|
|
355
|
-
crossRefPatterns
|
|
536
|
+
crossRefPatterns,
|
|
537
|
+
specFile.content
|
|
356
538
|
);
|
|
357
539
|
fileResults.push({ filePath: specFile.filePath, changed });
|
|
358
540
|
}
|
|
@@ -362,25 +544,54 @@ async function fixMatrices(specs, crossRefPatterns) {
|
|
|
362
544
|
fileResults
|
|
363
545
|
};
|
|
364
546
|
}
|
|
365
|
-
function
|
|
366
|
-
const
|
|
547
|
+
function buildReqFileMaps(specFiles) {
|
|
548
|
+
const idToReqFile = /* @__PURE__ */ new Map();
|
|
549
|
+
const codeToReqFilesSet = /* @__PURE__ */ new Map();
|
|
367
550
|
for (const sf of specFiles) {
|
|
368
|
-
|
|
369
|
-
|
|
551
|
+
const fileName = basename3(sf.filePath);
|
|
552
|
+
if (!/\bREQ-/.test(fileName)) continue;
|
|
553
|
+
for (const reqId of sf.requirementIds) {
|
|
554
|
+
idToReqFile.set(reqId, fileName);
|
|
555
|
+
}
|
|
556
|
+
for (const acId of sf.acIds) {
|
|
557
|
+
idToReqFile.set(acId, fileName);
|
|
558
|
+
}
|
|
559
|
+
if (sf.code) {
|
|
560
|
+
const existing = codeToReqFilesSet.get(sf.code) ?? /* @__PURE__ */ new Set();
|
|
561
|
+
existing.add(fileName);
|
|
562
|
+
codeToReqFilesSet.set(sf.code, existing);
|
|
370
563
|
}
|
|
371
564
|
}
|
|
372
|
-
|
|
565
|
+
const codeToReqFiles = /* @__PURE__ */ new Map();
|
|
566
|
+
for (const [code, files] of codeToReqFilesSet) {
|
|
567
|
+
codeToReqFiles.set(code, [...files].sort());
|
|
568
|
+
}
|
|
569
|
+
return { idToReqFile, codeToReqFiles };
|
|
373
570
|
}
|
|
374
|
-
function
|
|
375
|
-
const
|
|
376
|
-
|
|
571
|
+
function resolveReqFile(id, maps) {
|
|
572
|
+
const direct = maps.idToReqFile.get(id);
|
|
573
|
+
if (direct) return direct;
|
|
574
|
+
const acMatch = /^(.+)_AC-\d+$/.exec(id);
|
|
575
|
+
if (acMatch?.[1]) {
|
|
576
|
+
return maps.idToReqFile.get(acMatch[1]);
|
|
577
|
+
}
|
|
578
|
+
const propMatch = /^([A-Z][A-Z0-9]*)_P-\d+$/.exec(id);
|
|
579
|
+
if (propMatch?.[1]) {
|
|
580
|
+
const files = maps.codeToReqFiles.get(propMatch[1]);
|
|
581
|
+
return files?.[0];
|
|
582
|
+
}
|
|
583
|
+
return void 0;
|
|
377
584
|
}
|
|
378
|
-
async function fixDesignMatrix(filePath,
|
|
585
|
+
async function fixDesignMatrix(filePath, reqFileMaps, crossRefPatterns, cachedContent) {
|
|
379
586
|
let content;
|
|
380
|
-
|
|
381
|
-
content =
|
|
382
|
-
}
|
|
383
|
-
|
|
587
|
+
if (cachedContent != null) {
|
|
588
|
+
content = cachedContent;
|
|
589
|
+
} else {
|
|
590
|
+
try {
|
|
591
|
+
content = await readFile3(filePath, "utf-8");
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
384
595
|
}
|
|
385
596
|
const { components, properties } = parseDesignFileData(content, crossRefPatterns);
|
|
386
597
|
const acToComponents = /* @__PURE__ */ new Map();
|
|
@@ -400,11 +611,11 @@ async function fixDesignMatrix(filePath, codeToReqFile, crossRefPatterns) {
|
|
|
400
611
|
}
|
|
401
612
|
}
|
|
402
613
|
const allAcIds = [...acToComponents.keys()];
|
|
403
|
-
const grouped = groupByReqFile(allAcIds,
|
|
614
|
+
const grouped = groupByReqFile(allAcIds, reqFileMaps);
|
|
404
615
|
const newSection = generateDesignSection(grouped, acToComponents, acToProperties);
|
|
405
616
|
const newContent = replaceTraceabilitySection(content, newSection);
|
|
406
617
|
if (newContent === content) return false;
|
|
407
|
-
await
|
|
618
|
+
await writeFile2(filePath, newContent, "utf-8");
|
|
408
619
|
return true;
|
|
409
620
|
}
|
|
410
621
|
function parseDesignFileData(content, crossRefPatterns) {
|
|
@@ -475,12 +686,16 @@ function generateDesignSection(grouped, acToComponents, acToProperties) {
|
|
|
475
686
|
}
|
|
476
687
|
return lines.join("\n");
|
|
477
688
|
}
|
|
478
|
-
async function fixTaskMatrix(filePath,
|
|
689
|
+
async function fixTaskMatrix(filePath, reqFileMaps, specs, crossRefPatterns, cachedContent) {
|
|
479
690
|
let content;
|
|
480
|
-
|
|
481
|
-
content =
|
|
482
|
-
}
|
|
483
|
-
|
|
691
|
+
if (cachedContent != null) {
|
|
692
|
+
content = cachedContent;
|
|
693
|
+
} else {
|
|
694
|
+
try {
|
|
695
|
+
content = await readFile3(filePath, "utf-8");
|
|
696
|
+
} catch {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
484
699
|
}
|
|
485
700
|
const { tasks, sourceDesigns } = parseTaskFileData(content, crossRefPatterns);
|
|
486
701
|
const acToTasks = /* @__PURE__ */ new Map();
|
|
@@ -503,17 +718,17 @@ async function fixTaskMatrix(filePath, codeToReqFile, specs, crossRefPatterns) {
|
|
|
503
718
|
for (const designName of sourceDesigns) {
|
|
504
719
|
const designCode = extractCodeFromFileName(designName);
|
|
505
720
|
for (const sf of specs.specFiles) {
|
|
506
|
-
if (sf.code === designCode && /\bDESIGN-/.test(
|
|
721
|
+
if (sf.code === designCode && /\bDESIGN-/.test(basename3(sf.filePath))) {
|
|
507
722
|
for (const propId of sf.propertyIds) sourcePropertyIds.add(propId);
|
|
508
723
|
}
|
|
509
724
|
}
|
|
510
725
|
}
|
|
511
726
|
const allIdsForMatrix = [.../* @__PURE__ */ new Set([...acToTasks.keys(), ...idToTestTasks.keys()])];
|
|
512
|
-
const grouped = groupByReqFile(allIdsForMatrix,
|
|
727
|
+
const grouped = groupByReqFile(allIdsForMatrix, reqFileMaps);
|
|
513
728
|
const newSection = generateTaskSection(grouped, acToTasks, idToTestTasks, sourcePropertyIds);
|
|
514
729
|
const newContent = replaceTraceabilitySection(content, newSection);
|
|
515
730
|
if (newContent === content) return false;
|
|
516
|
-
await
|
|
731
|
+
await writeFile2(filePath, newContent, "utf-8");
|
|
517
732
|
return true;
|
|
518
733
|
}
|
|
519
734
|
function parseTaskFileData(content, crossRefPatterns) {
|
|
@@ -604,11 +819,10 @@ function generateTaskSection(grouped, acToTasks, idToTestTasks, propertyIds) {
|
|
|
604
819
|
}
|
|
605
820
|
return lines.join("\n").trimEnd();
|
|
606
821
|
}
|
|
607
|
-
function groupByReqFile(ids,
|
|
822
|
+
function groupByReqFile(ids, maps) {
|
|
608
823
|
const groups = /* @__PURE__ */ new Map();
|
|
609
824
|
for (const id of ids) {
|
|
610
|
-
const
|
|
611
|
-
const reqFile = codeToReqFile.get(code);
|
|
825
|
+
const reqFile = resolveReqFile(id, maps);
|
|
612
826
|
if (!reqFile) continue;
|
|
613
827
|
const existing = groups.get(reqFile) ?? [];
|
|
614
828
|
existing.push(id);
|
|
@@ -644,7 +858,7 @@ function extractIdsFromText(text) {
|
|
|
644
858
|
return ids;
|
|
645
859
|
}
|
|
646
860
|
function extractCodeFromFileName(fileName) {
|
|
647
|
-
const match = /^(?:REQ|DESIGN|FEAT|
|
|
861
|
+
const match = /^(?:REQ|DESIGN|FEAT|EXAMPLE|API|TASK)-([A-Z][A-Z0-9]*)-/.exec(fileName);
|
|
648
862
|
return match?.[1] ?? "";
|
|
649
863
|
}
|
|
650
864
|
function compareIds(a, b) {
|
|
@@ -726,7 +940,7 @@ function printRuleContext(f) {
|
|
|
726
940
|
}
|
|
727
941
|
|
|
728
942
|
// src/core/check/rule-loader.ts
|
|
729
|
-
import { readFile as
|
|
943
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
730
944
|
import { join } from "path";
|
|
731
945
|
import { parse as parseYaml } from "yaml";
|
|
732
946
|
async function loadRules(schemaDir) {
|
|
@@ -747,7 +961,7 @@ function matchesTargetGlob(filePath, targetGlob) {
|
|
|
747
961
|
async function loadRuleFile(filePath) {
|
|
748
962
|
let content;
|
|
749
963
|
try {
|
|
750
|
-
content = await
|
|
964
|
+
content = await readFile4(filePath, "utf-8");
|
|
751
965
|
} catch {
|
|
752
966
|
return null;
|
|
753
967
|
}
|
|
@@ -939,7 +1153,7 @@ var RuleValidationError = class extends Error {
|
|
|
939
1153
|
};
|
|
940
1154
|
|
|
941
1155
|
// src/core/check/schema-checker.ts
|
|
942
|
-
import { readFile as
|
|
1156
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
943
1157
|
import remarkGfm from "remark-gfm";
|
|
944
1158
|
import remarkParse from "remark-parse";
|
|
945
1159
|
import { unified } from "unified";
|
|
@@ -981,14 +1195,18 @@ function formatProhibitedRule(pattern) {
|
|
|
981
1195
|
async function checkSchemasAsync(specFiles, ruleSets) {
|
|
982
1196
|
const findings = [];
|
|
983
1197
|
const parser = unified().use(remarkParse).use(remarkGfm);
|
|
984
|
-
for (const
|
|
985
|
-
const matchingRules = ruleSets.filter((rs) => matchesTargetGlob(
|
|
1198
|
+
for (const spec2 of specFiles) {
|
|
1199
|
+
const matchingRules = ruleSets.filter((rs) => matchesTargetGlob(spec2.filePath, rs.targetGlob));
|
|
986
1200
|
if (matchingRules.length === 0) continue;
|
|
987
1201
|
let content;
|
|
988
|
-
|
|
989
|
-
content =
|
|
990
|
-
}
|
|
991
|
-
|
|
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
|
+
}
|
|
992
1210
|
}
|
|
993
1211
|
const tree = parser.parse(content);
|
|
994
1212
|
const sectionTree = buildSectionTree(tree);
|
|
@@ -1002,7 +1220,7 @@ async function checkSchemasAsync(specFiles, ruleSets) {
|
|
|
1002
1220
|
severity: "warning",
|
|
1003
1221
|
code: "schema-line-limit",
|
|
1004
1222
|
message: `File has ${lineCount} lines, exceeds limit of ${ruleSet.ruleFile["line-limit"]}`,
|
|
1005
|
-
filePath:
|
|
1223
|
+
filePath: spec2.filePath,
|
|
1006
1224
|
ruleSource,
|
|
1007
1225
|
rule: formatLineLimitRule(ruleSet.ruleFile["line-limit"])
|
|
1008
1226
|
});
|
|
@@ -1012,7 +1230,7 @@ async function checkSchemasAsync(specFiles, ruleSets) {
|
|
|
1012
1230
|
...checkRulesAgainstSections(
|
|
1013
1231
|
allSections,
|
|
1014
1232
|
ruleSet.ruleFile.sections,
|
|
1015
|
-
|
|
1233
|
+
spec2.filePath,
|
|
1016
1234
|
ruleSource
|
|
1017
1235
|
)
|
|
1018
1236
|
);
|
|
@@ -1021,7 +1239,7 @@ async function checkSchemasAsync(specFiles, ruleSets) {
|
|
|
1021
1239
|
...checkProhibited(
|
|
1022
1240
|
content,
|
|
1023
1241
|
ruleSet.ruleFile["sections-prohibited"],
|
|
1024
|
-
|
|
1242
|
+
spec2.filePath,
|
|
1025
1243
|
ruleSource
|
|
1026
1244
|
)
|
|
1027
1245
|
);
|
|
@@ -1430,131 +1648,6 @@ function collectAllCodeBlocks(section) {
|
|
|
1430
1648
|
return blocks;
|
|
1431
1649
|
}
|
|
1432
1650
|
|
|
1433
|
-
// src/core/check/spec-parser.ts
|
|
1434
|
-
import { readFile as readFile5 } from "fs/promises";
|
|
1435
|
-
import { basename as basename2 } from "path";
|
|
1436
|
-
async function parseSpecs(config) {
|
|
1437
|
-
const files = await collectSpecFiles(config.specGlobs, config.specIgnore);
|
|
1438
|
-
const specFiles = [];
|
|
1439
|
-
const requirementIds = /* @__PURE__ */ new Set();
|
|
1440
|
-
const acIds = /* @__PURE__ */ new Set();
|
|
1441
|
-
const propertyIds = /* @__PURE__ */ new Set();
|
|
1442
|
-
const componentNames = /* @__PURE__ */ new Set();
|
|
1443
|
-
const idLocations = /* @__PURE__ */ new Map();
|
|
1444
|
-
for (const filePath of files) {
|
|
1445
|
-
const specFile = await parseSpecFile(filePath, config.crossRefPatterns);
|
|
1446
|
-
if (specFile) {
|
|
1447
|
-
specFiles.push(specFile);
|
|
1448
|
-
for (const id of specFile.requirementIds) requirementIds.add(id);
|
|
1449
|
-
for (const id of specFile.acIds) acIds.add(id);
|
|
1450
|
-
for (const id of specFile.propertyIds) propertyIds.add(id);
|
|
1451
|
-
for (const name of specFile.componentNames) componentNames.add(name);
|
|
1452
|
-
for (const [id, loc] of specFile.idLocations ?? []) {
|
|
1453
|
-
idLocations.set(id, loc);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
const allIds = /* @__PURE__ */ new Set([...requirementIds, ...acIds, ...propertyIds, ...componentNames]);
|
|
1458
|
-
return { requirementIds, acIds, propertyIds, componentNames, allIds, specFiles, idLocations };
|
|
1459
|
-
}
|
|
1460
|
-
async function parseSpecFile(filePath, crossRefPatterns) {
|
|
1461
|
-
let content;
|
|
1462
|
-
try {
|
|
1463
|
-
content = await readFile5(filePath, "utf-8");
|
|
1464
|
-
} catch {
|
|
1465
|
-
return null;
|
|
1466
|
-
}
|
|
1467
|
-
const code = extractCodePrefix(filePath);
|
|
1468
|
-
const lines = content.split("\n");
|
|
1469
|
-
const requirementIds = [];
|
|
1470
|
-
const acIds = [];
|
|
1471
|
-
const propertyIds = [];
|
|
1472
|
-
const componentNames = [];
|
|
1473
|
-
const crossRefs = [];
|
|
1474
|
-
const idLocations = /* @__PURE__ */ new Map();
|
|
1475
|
-
const componentImplements = /* @__PURE__ */ new Map();
|
|
1476
|
-
const reqIdRegex = /^###\s+([A-Z][A-Z0-9]*-\d+(?:\.\d+)?)\s*:/;
|
|
1477
|
-
const acIdRegex = /^-\s+(?:\[[ x]\]\s+)?([A-Z][A-Z0-9]*-\d+(?:\.\d+)?_AC-\d+)\s/;
|
|
1478
|
-
const propIdRegex = /^-\s+([A-Z][A-Z0-9]*_P-\d+)\s/;
|
|
1479
|
-
const componentRegex = /^###\s+([A-Z][A-Z0-9]*-[A-Za-z][A-Za-z0-9]*(?:[A-Z][a-z0-9]*)*)\s*$/;
|
|
1480
|
-
let currentComponent = null;
|
|
1481
|
-
for (const [i, line] of lines.entries()) {
|
|
1482
|
-
const lineNum = i + 1;
|
|
1483
|
-
const reqMatch = reqIdRegex.exec(line);
|
|
1484
|
-
if (reqMatch?.[1]) {
|
|
1485
|
-
requirementIds.push(reqMatch[1]);
|
|
1486
|
-
idLocations.set(reqMatch[1], { filePath, line: lineNum });
|
|
1487
|
-
}
|
|
1488
|
-
const acMatch = acIdRegex.exec(line);
|
|
1489
|
-
if (acMatch?.[1]) {
|
|
1490
|
-
acIds.push(acMatch[1]);
|
|
1491
|
-
idLocations.set(acMatch[1], { filePath, line: lineNum });
|
|
1492
|
-
}
|
|
1493
|
-
const propMatch = propIdRegex.exec(line);
|
|
1494
|
-
if (propMatch?.[1]) {
|
|
1495
|
-
propertyIds.push(propMatch[1]);
|
|
1496
|
-
idLocations.set(propMatch[1], { filePath, line: lineNum });
|
|
1497
|
-
}
|
|
1498
|
-
const compMatch = componentRegex.exec(line);
|
|
1499
|
-
if (compMatch?.[1]) {
|
|
1500
|
-
if (!reqIdRegex.test(line)) {
|
|
1501
|
-
componentNames.push(compMatch[1]);
|
|
1502
|
-
idLocations.set(compMatch[1], { filePath, line: lineNum });
|
|
1503
|
-
currentComponent = compMatch[1];
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
if (/^#{1,2}\s/.test(line) && !compMatch) {
|
|
1507
|
-
currentComponent = null;
|
|
1508
|
-
}
|
|
1509
|
-
for (const pattern of crossRefPatterns) {
|
|
1510
|
-
const patIdx = line.indexOf(pattern);
|
|
1511
|
-
if (patIdx !== -1) {
|
|
1512
|
-
const afterPattern = line.slice(patIdx + pattern.length);
|
|
1513
|
-
const ids = extractIdsFromText2(afterPattern);
|
|
1514
|
-
if (ids.length > 0) {
|
|
1515
|
-
const type = pattern.toLowerCase().includes("implements") ? "implements" : "validates";
|
|
1516
|
-
crossRefs.push({ type, ids, filePath, line: i + 1 });
|
|
1517
|
-
if (type === "implements" && currentComponent) {
|
|
1518
|
-
const existing = componentImplements.get(currentComponent) ?? [];
|
|
1519
|
-
existing.push(...ids);
|
|
1520
|
-
componentImplements.set(currentComponent, existing);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
return {
|
|
1527
|
-
filePath,
|
|
1528
|
-
code,
|
|
1529
|
-
requirementIds,
|
|
1530
|
-
acIds,
|
|
1531
|
-
propertyIds,
|
|
1532
|
-
componentNames,
|
|
1533
|
-
crossRefs,
|
|
1534
|
-
idLocations,
|
|
1535
|
-
componentImplements
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
function extractCodePrefix(filePath) {
|
|
1539
|
-
const name = basename2(filePath, ".md");
|
|
1540
|
-
const match = /^(?:REQ|DESIGN|FEAT|EXAMPLES|API)-([A-Z][A-Z0-9]*)-/.exec(name);
|
|
1541
|
-
if (match?.[1]) return match[1];
|
|
1542
|
-
return "";
|
|
1543
|
-
}
|
|
1544
|
-
function extractIdsFromText2(text) {
|
|
1545
|
-
const idRegex = /[A-Z][A-Z0-9]*-\d+(?:\.\d+)?(?:_AC-\d+)?|[A-Z][A-Z0-9]*_P-\d+/g;
|
|
1546
|
-
const ids = [];
|
|
1547
|
-
let match = idRegex.exec(text);
|
|
1548
|
-
while (match !== null) {
|
|
1549
|
-
ids.push(match[0]);
|
|
1550
|
-
match = idRegex.exec(text);
|
|
1551
|
-
}
|
|
1552
|
-
return ids;
|
|
1553
|
-
}
|
|
1554
|
-
async function collectSpecFiles(specGlobs, ignore) {
|
|
1555
|
-
return collectFiles(specGlobs, ignore);
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
1651
|
// src/core/check/spec-spec-checker.ts
|
|
1559
1652
|
function checkSpecAgainstSpec(specs, markers, config) {
|
|
1560
1653
|
const findings = [];
|
|
@@ -1639,44 +1732,6 @@ function checkSpecAgainstSpec(specs, markers, config) {
|
|
|
1639
1732
|
return { findings };
|
|
1640
1733
|
}
|
|
1641
1734
|
|
|
1642
|
-
// src/core/check/types.ts
|
|
1643
|
-
var DEFAULT_CHECK_CONFIG = {
|
|
1644
|
-
specGlobs: [
|
|
1645
|
-
".awa/specs/ARCHITECTURE.md",
|
|
1646
|
-
".awa/specs/FEAT-*.md",
|
|
1647
|
-
".awa/specs/REQ-*.md",
|
|
1648
|
-
".awa/specs/DESIGN-*.md",
|
|
1649
|
-
".awa/specs/EXAMPLES-*.md",
|
|
1650
|
-
".awa/specs/API-*.tsp",
|
|
1651
|
-
".awa/tasks/TASK-*.md",
|
|
1652
|
-
".awa/plans/PLAN-*.md",
|
|
1653
|
-
".awa/align/ALIGN-*.md"
|
|
1654
|
-
],
|
|
1655
|
-
codeGlobs: [
|
|
1656
|
-
"**/*.{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}"
|
|
1657
|
-
],
|
|
1658
|
-
specIgnore: [],
|
|
1659
|
-
codeIgnore: [
|
|
1660
|
-
"node_modules/**",
|
|
1661
|
-
"dist/**",
|
|
1662
|
-
"vendor/**",
|
|
1663
|
-
"target/**",
|
|
1664
|
-
"build/**",
|
|
1665
|
-
"out/**",
|
|
1666
|
-
".awa/**"
|
|
1667
|
-
],
|
|
1668
|
-
ignoreMarkers: [],
|
|
1669
|
-
markers: ["@awa-impl", "@awa-test", "@awa-component"],
|
|
1670
|
-
idPattern: "([A-Z][A-Z0-9]*-\\d+(?:\\.\\d+)?(?:_AC-\\d+)?|[A-Z][A-Z0-9]*_P-\\d+)",
|
|
1671
|
-
crossRefPatterns: ["IMPLEMENTS:", "VALIDATES:"],
|
|
1672
|
-
format: "text",
|
|
1673
|
-
schemaDir: ".awa/.agent/schemas",
|
|
1674
|
-
schemaEnabled: true,
|
|
1675
|
-
allowWarnings: false,
|
|
1676
|
-
specOnly: false,
|
|
1677
|
-
fix: true
|
|
1678
|
-
};
|
|
1679
|
-
|
|
1680
1735
|
// src/commands/check.ts
|
|
1681
1736
|
async function checkCommand(cliOptions) {
|
|
1682
1737
|
try {
|
|
@@ -1693,6 +1748,21 @@ async function checkCommand(cliOptions) {
|
|
|
1693
1748
|
]);
|
|
1694
1749
|
const codeSpecResult = config.specOnly ? { findings: [] } : checkCodeAgainstSpec(markers, specs, config);
|
|
1695
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
|
+
}
|
|
1696
1766
|
const schemaResult = config.schemaEnabled && ruleSets.length > 0 ? await checkSchemasAsync(specs.specFiles, ruleSets) : { findings: [] };
|
|
1697
1767
|
const combinedFindings = [
|
|
1698
1768
|
...markers.findings,
|
|
@@ -1711,12 +1781,6 @@ async function checkCommand(cliOptions) {
|
|
|
1711
1781
|
} else {
|
|
1712
1782
|
report(allFindings, config.format);
|
|
1713
1783
|
}
|
|
1714
|
-
if (config.fix) {
|
|
1715
|
-
const fixResult = await fixMatrices(specs, config.crossRefPatterns);
|
|
1716
|
-
if (fixResult.filesFixed > 0) {
|
|
1717
|
-
logger.info(`Fixed traceability matrices in ${fixResult.filesFixed} file(s)`);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
1784
|
const hasErrors = allFindings.some((f) => f.severity === "error");
|
|
1721
1785
|
return hasErrors ? 1 : 0;
|
|
1722
1786
|
} catch (error) {
|
|
@@ -1778,6 +1842,98 @@ function toStringArray(value) {
|
|
|
1778
1842
|
return null;
|
|
1779
1843
|
}
|
|
1780
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
|
+
|
|
1781
1937
|
// src/commands/diff.ts
|
|
1782
1938
|
import { intro, outro } from "@clack/prompts";
|
|
1783
1939
|
|
|
@@ -1886,7 +2042,7 @@ import { join as join3, relative } from "path";
|
|
|
1886
2042
|
// src/core/resolver.ts
|
|
1887
2043
|
import { MultiSelectPrompt } from "@clack/core";
|
|
1888
2044
|
import { isCancel, multiselect } from "@clack/prompts";
|
|
1889
|
-
import
|
|
2045
|
+
import chalk3 from "chalk";
|
|
1890
2046
|
var _unicode = process.platform !== "win32";
|
|
1891
2047
|
var _s = (c, fb) => _unicode ? c : fb;
|
|
1892
2048
|
var _CHECKED = _s("\u25FC", "[+]");
|
|
@@ -1896,20 +2052,20 @@ var _BAR = _s("\u2502", "|");
|
|
|
1896
2052
|
var _BAR_END = _s("\u2514", "-");
|
|
1897
2053
|
function _renderDeleteItem(opt, state) {
|
|
1898
2054
|
const label = opt.label ?? opt.value;
|
|
1899
|
-
const hint = opt.hint ? ` ${
|
|
2055
|
+
const hint = opt.hint ? ` ${chalk3.dim(`(${opt.hint})`)}` : "";
|
|
1900
2056
|
switch (state) {
|
|
1901
2057
|
case "active":
|
|
1902
|
-
return `${
|
|
2058
|
+
return `${chalk3.cyan(_UNCHECKED_A)} ${label}${hint}`;
|
|
1903
2059
|
case "selected":
|
|
1904
|
-
return `${
|
|
2060
|
+
return `${chalk3.red(_CHECKED)} ${chalk3.dim(label)}${hint}`;
|
|
1905
2061
|
case "active-selected":
|
|
1906
|
-
return `${
|
|
2062
|
+
return `${chalk3.red(_CHECKED)} ${label}${hint}`;
|
|
1907
2063
|
case "cancelled":
|
|
1908
|
-
return
|
|
2064
|
+
return chalk3.strikethrough(chalk3.dim(label));
|
|
1909
2065
|
case "submitted":
|
|
1910
|
-
return
|
|
2066
|
+
return chalk3.dim(label);
|
|
1911
2067
|
default:
|
|
1912
|
-
return `${
|
|
2068
|
+
return `${chalk3.dim(_UNCHECKED)} ${chalk3.dim(label)}`;
|
|
1913
2069
|
}
|
|
1914
2070
|
}
|
|
1915
2071
|
async function deleteMultiselect(opts) {
|
|
@@ -1920,8 +2076,8 @@ async function deleteMultiselect(opts) {
|
|
|
1920
2076
|
required,
|
|
1921
2077
|
render() {
|
|
1922
2078
|
const self = this;
|
|
1923
|
-
const header = `${
|
|
1924
|
-
${
|
|
2079
|
+
const header = `${chalk3.gray(_BAR)}
|
|
2080
|
+
${chalk3.cyan(_BAR)} ${message}
|
|
1925
2081
|
`;
|
|
1926
2082
|
const getState = (opt, idx) => {
|
|
1927
2083
|
const active = idx === self.cursor;
|
|
@@ -1933,16 +2089,16 @@ ${chalk2.cyan(_BAR)} ${message}
|
|
|
1933
2089
|
};
|
|
1934
2090
|
switch (self.state) {
|
|
1935
2091
|
case "submit":
|
|
1936
|
-
return `${header}${
|
|
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"));
|
|
1937
2093
|
case "cancel": {
|
|
1938
|
-
const cancelled = self.options.filter((o) => self.value.includes(o.value)).map((o) => _renderDeleteItem(o, "cancelled")).join(
|
|
1939
|
-
return `${header}${
|
|
1940
|
-
${
|
|
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")}`;
|
|
1941
2097
|
}
|
|
1942
2098
|
default:
|
|
1943
|
-
return `${header}${
|
|
1944
|
-
${
|
|
1945
|
-
${
|
|
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)}
|
|
1946
2102
|
`;
|
|
1947
2103
|
}
|
|
1948
2104
|
}
|
|
@@ -2038,11 +2194,9 @@ var EMPTY_FILE_MARKER = "<!-- AWA:EMPTY_FILE -->";
|
|
|
2038
2194
|
var TemplateEngine = class {
|
|
2039
2195
|
eta = null;
|
|
2040
2196
|
templateDir = null;
|
|
2041
|
-
compiledCache = /* @__PURE__ */ new Map();
|
|
2042
2197
|
// @awa-impl: TPL-8_AC-1, TPL-8_AC-2, TPL-8_AC-3, TPL-8_AC-4
|
|
2043
2198
|
configure(templateDir) {
|
|
2044
2199
|
this.templateDir = templateDir;
|
|
2045
|
-
this.compiledCache.clear();
|
|
2046
2200
|
this.eta = new Eta({
|
|
2047
2201
|
views: templateDir,
|
|
2048
2202
|
cache: true,
|
|
@@ -2863,7 +3017,7 @@ async function diffCommand(cliOptions) {
|
|
|
2863
3017
|
import { intro as intro2, outro as outro2 } from "@clack/prompts";
|
|
2864
3018
|
|
|
2865
3019
|
// src/core/features/reporter.ts
|
|
2866
|
-
import
|
|
3020
|
+
import chalk4 from "chalk";
|
|
2867
3021
|
var FeaturesReporter = class {
|
|
2868
3022
|
// @awa-impl: DISC-6_AC-1, DISC-7_AC-1
|
|
2869
3023
|
/** Render the features report to stdout. */
|
|
@@ -2898,25 +3052,25 @@ var FeaturesReporter = class {
|
|
|
2898
3052
|
reportTable(scanResult, presets) {
|
|
2899
3053
|
const { features, filesScanned } = scanResult;
|
|
2900
3054
|
if (features.length === 0) {
|
|
2901
|
-
console.log(
|
|
2902
|
-
console.log(
|
|
3055
|
+
console.log(chalk4.yellow("No feature flags found."));
|
|
3056
|
+
console.log(chalk4.dim(`(${filesScanned} files scanned)`));
|
|
2903
3057
|
return;
|
|
2904
3058
|
}
|
|
2905
|
-
console.log(
|
|
3059
|
+
console.log(chalk4.bold(`Feature flags (${features.length} found):
|
|
2906
3060
|
`));
|
|
2907
3061
|
for (const feature of features) {
|
|
2908
|
-
console.log(` ${
|
|
3062
|
+
console.log(` ${chalk4.cyan(feature.name)}`);
|
|
2909
3063
|
for (const file of feature.files) {
|
|
2910
|
-
console.log(` ${
|
|
3064
|
+
console.log(` ${chalk4.dim(file)}`);
|
|
2911
3065
|
}
|
|
2912
3066
|
}
|
|
2913
3067
|
console.log("");
|
|
2914
|
-
console.log(
|
|
3068
|
+
console.log(chalk4.dim(`${filesScanned} files scanned`));
|
|
2915
3069
|
if (presets && Object.keys(presets).length > 0) {
|
|
2916
3070
|
console.log("");
|
|
2917
|
-
console.log(
|
|
3071
|
+
console.log(chalk4.bold("Presets (from .awa.toml):\n"));
|
|
2918
3072
|
for (const [name, flags] of Object.entries(presets)) {
|
|
2919
|
-
console.log(` ${
|
|
3073
|
+
console.log(` ${chalk4.green(name)}: ${flags.join(", ")}`);
|
|
2920
3074
|
}
|
|
2921
3075
|
}
|
|
2922
3076
|
}
|
|
@@ -3149,25 +3303,665 @@ async function generateCommand(cliOptions) {
|
|
|
3149
3303
|
}
|
|
3150
3304
|
}
|
|
3151
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
|
+
|
|
3152
3946
|
// src/commands/test.ts
|
|
3153
3947
|
import { intro as intro4, outro as outro4 } from "@clack/prompts";
|
|
3154
3948
|
|
|
3155
3949
|
// src/core/template-test/fixture-loader.ts
|
|
3156
3950
|
import { readdir as readdir2 } from "fs/promises";
|
|
3157
|
-
import { basename as
|
|
3951
|
+
import { basename as basename7, extname as extname2, join as join10 } from "path";
|
|
3158
3952
|
import { parse } from "smol-toml";
|
|
3159
3953
|
async function discoverFixtures(templatePath) {
|
|
3160
|
-
const testsDir =
|
|
3954
|
+
const testsDir = join10(templatePath, "_tests");
|
|
3161
3955
|
let entries;
|
|
3162
3956
|
try {
|
|
3163
3957
|
const dirEntries = await readdir2(testsDir, { withFileTypes: true });
|
|
3164
|
-
entries = dirEntries.filter((e) => e.isFile() &&
|
|
3958
|
+
entries = dirEntries.filter((e) => e.isFile() && extname2(e.name) === ".toml").map((e) => e.name).sort();
|
|
3165
3959
|
} catch {
|
|
3166
3960
|
return [];
|
|
3167
3961
|
}
|
|
3168
3962
|
const fixtures = [];
|
|
3169
3963
|
for (const filename of entries) {
|
|
3170
|
-
const filePath =
|
|
3964
|
+
const filePath = join10(testsDir, filename);
|
|
3171
3965
|
const fixture = await parseFixture(filePath);
|
|
3172
3966
|
fixtures.push(fixture);
|
|
3173
3967
|
}
|
|
@@ -3176,7 +3970,7 @@ async function discoverFixtures(templatePath) {
|
|
|
3176
3970
|
async function parseFixture(filePath) {
|
|
3177
3971
|
const content = await readTextFile(filePath);
|
|
3178
3972
|
const parsed = parse(content);
|
|
3179
|
-
const name =
|
|
3973
|
+
const name = basename7(filePath, extname2(filePath));
|
|
3180
3974
|
const features = toStringArray2(parsed.features) ?? [];
|
|
3181
3975
|
const preset = toStringArray2(parsed.preset) ?? [];
|
|
3182
3976
|
const removeFeatures = toStringArray2(parsed["remove-features"]) ?? [];
|
|
@@ -3198,7 +3992,7 @@ function toStringArray2(value) {
|
|
|
3198
3992
|
}
|
|
3199
3993
|
|
|
3200
3994
|
// src/core/template-test/reporter.ts
|
|
3201
|
-
import
|
|
3995
|
+
import chalk5 from "chalk";
|
|
3202
3996
|
function report2(result, options) {
|
|
3203
3997
|
if (options?.json) {
|
|
3204
3998
|
reportJson2(result);
|
|
@@ -3209,11 +4003,11 @@ function report2(result, options) {
|
|
|
3209
4003
|
reportFixture(fixture);
|
|
3210
4004
|
}
|
|
3211
4005
|
console.log("");
|
|
3212
|
-
console.log(
|
|
4006
|
+
console.log(chalk5.bold("Test Summary:"));
|
|
3213
4007
|
console.log(` Total: ${result.total}`);
|
|
3214
|
-
console.log(
|
|
4008
|
+
console.log(chalk5.green(` Passed: ${result.passed}`));
|
|
3215
4009
|
if (result.failed > 0) {
|
|
3216
|
-
console.log(
|
|
4010
|
+
console.log(chalk5.red(` Failed: ${result.failed}`));
|
|
3217
4011
|
}
|
|
3218
4012
|
console.log("");
|
|
3219
4013
|
}
|
|
@@ -3239,27 +4033,27 @@ function reportJson2(result) {
|
|
|
3239
4033
|
console.log(JSON.stringify(output, null, 2));
|
|
3240
4034
|
}
|
|
3241
4035
|
function reportFixture(fixture) {
|
|
3242
|
-
const icon = fixture.passed ?
|
|
4036
|
+
const icon = fixture.passed ? chalk5.green("\u2714") : chalk5.red("\u2716");
|
|
3243
4037
|
console.log(`${icon} ${fixture.name}`);
|
|
3244
4038
|
if (fixture.error) {
|
|
3245
|
-
console.log(
|
|
4039
|
+
console.log(chalk5.red(` Error: ${fixture.error}`));
|
|
3246
4040
|
return;
|
|
3247
4041
|
}
|
|
3248
4042
|
const missingFiles = fixture.fileResults.filter((r) => !r.found);
|
|
3249
4043
|
for (const missing of missingFiles) {
|
|
3250
|
-
console.log(
|
|
4044
|
+
console.log(chalk5.red(` Missing file: ${missing.path}`));
|
|
3251
4045
|
}
|
|
3252
4046
|
const snapshotFailures = fixture.snapshotResults.filter((r) => r.status !== "match");
|
|
3253
4047
|
for (const failure of snapshotFailures) {
|
|
3254
4048
|
switch (failure.status) {
|
|
3255
4049
|
case "mismatch":
|
|
3256
|
-
console.log(
|
|
4050
|
+
console.log(chalk5.yellow(` Snapshot mismatch: ${failure.path}`));
|
|
3257
4051
|
break;
|
|
3258
4052
|
case "missing-snapshot":
|
|
3259
|
-
console.log(
|
|
4053
|
+
console.log(chalk5.yellow(` Missing snapshot: ${failure.path}`));
|
|
3260
4054
|
break;
|
|
3261
4055
|
case "extra-snapshot":
|
|
3262
|
-
console.log(
|
|
4056
|
+
console.log(chalk5.yellow(` Extra snapshot (not in output): ${failure.path}`));
|
|
3263
4057
|
break;
|
|
3264
4058
|
}
|
|
3265
4059
|
}
|
|
@@ -3268,16 +4062,16 @@ function reportFixture(fixture) {
|
|
|
3268
4062
|
// src/core/template-test/runner.ts
|
|
3269
4063
|
import { mkdir as mkdir2, rm as rm3 } from "fs/promises";
|
|
3270
4064
|
import { tmpdir as tmpdir3 } from "os";
|
|
3271
|
-
import { join as
|
|
4065
|
+
import { join as join12 } from "path";
|
|
3272
4066
|
|
|
3273
4067
|
// src/core/template-test/snapshot.ts
|
|
3274
4068
|
import { mkdir, readdir as readdir3, rm as rm2 } from "fs/promises";
|
|
3275
|
-
import { join as
|
|
4069
|
+
import { join as join11, relative as relative4 } from "path";
|
|
3276
4070
|
async function walkRelative(dir, base) {
|
|
3277
4071
|
const results = [];
|
|
3278
4072
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
3279
4073
|
for (const entry of entries) {
|
|
3280
|
-
const fullPath =
|
|
4074
|
+
const fullPath = join11(dir, entry.name);
|
|
3281
4075
|
if (entry.isDirectory()) {
|
|
3282
4076
|
const sub = await walkRelative(fullPath, base);
|
|
3283
4077
|
results.push(...sub);
|
|
@@ -3294,8 +4088,8 @@ async function compareSnapshots(renderedDir, snapshotDir) {
|
|
|
3294
4088
|
const snapshotSet = new Set(snapshotFiles);
|
|
3295
4089
|
const renderedSet = new Set(renderedFiles);
|
|
3296
4090
|
for (const file of renderedFiles) {
|
|
3297
|
-
const renderedPath =
|
|
3298
|
-
const snapshotPath =
|
|
4091
|
+
const renderedPath = join11(renderedDir, file);
|
|
4092
|
+
const snapshotPath = join11(snapshotDir, file);
|
|
3299
4093
|
if (!snapshotSet.has(file)) {
|
|
3300
4094
|
results.push({ path: file, status: "missing-snapshot" });
|
|
3301
4095
|
continue;
|
|
@@ -3321,8 +4115,8 @@ async function updateSnapshots(renderedDir, snapshotDir) {
|
|
|
3321
4115
|
await mkdir(snapshotDir, { recursive: true });
|
|
3322
4116
|
const files = await walkRelative(renderedDir, renderedDir);
|
|
3323
4117
|
for (const file of files) {
|
|
3324
|
-
const srcPath =
|
|
3325
|
-
const destPath =
|
|
4118
|
+
const srcPath = join11(renderedDir, file);
|
|
4119
|
+
const destPath = join11(snapshotDir, file);
|
|
3326
4120
|
const content = await readTextFile(srcPath);
|
|
3327
4121
|
await writeTextFile(destPath, content);
|
|
3328
4122
|
}
|
|
@@ -3330,7 +4124,7 @@ async function updateSnapshots(renderedDir, snapshotDir) {
|
|
|
3330
4124
|
|
|
3331
4125
|
// src/core/template-test/runner.ts
|
|
3332
4126
|
async function runFixture(fixture, templatePath, options, presetDefinitions = {}) {
|
|
3333
|
-
const tempDir =
|
|
4127
|
+
const tempDir = join12(tmpdir3(), `awa-test-${fixture.name}-${Date.now()}`);
|
|
3334
4128
|
try {
|
|
3335
4129
|
await mkdir2(tempDir, { recursive: true });
|
|
3336
4130
|
const features = featureResolver.resolve({
|
|
@@ -3349,12 +4143,12 @@ async function runFixture(fixture, templatePath, options, presetDefinitions = {}
|
|
|
3349
4143
|
});
|
|
3350
4144
|
const fileResults = [];
|
|
3351
4145
|
for (const expectedFile of fixture.expectedFiles) {
|
|
3352
|
-
const fullPath =
|
|
4146
|
+
const fullPath = join12(tempDir, expectedFile);
|
|
3353
4147
|
const found = await pathExists(fullPath);
|
|
3354
4148
|
fileResults.push({ path: expectedFile, found });
|
|
3355
4149
|
}
|
|
3356
4150
|
const missingFiles = fileResults.filter((r) => !r.found);
|
|
3357
|
-
const snapshotDir =
|
|
4151
|
+
const snapshotDir = join12(templatePath, "_tests", fixture.name);
|
|
3358
4152
|
let snapshotResults = [];
|
|
3359
4153
|
if (options.updateSnapshots) {
|
|
3360
4154
|
await updateSnapshots(tempDir, snapshotDir);
|
|
@@ -3471,7 +4265,7 @@ async function testCommand(options) {
|
|
|
3471
4265
|
}
|
|
3472
4266
|
|
|
3473
4267
|
// src/core/trace/content-assembler.ts
|
|
3474
|
-
import { readFile as
|
|
4268
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3475
4269
|
var DEFAULT_BEFORE_CONTEXT = 5;
|
|
3476
4270
|
var DEFAULT_AFTER_CONTEXT = 20;
|
|
3477
4271
|
async function assembleContent(result, taskPath, contextOptions) {
|
|
@@ -3479,8 +4273,9 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3479
4273
|
const afterCtx = contextOptions?.afterContext ?? DEFAULT_AFTER_CONTEXT;
|
|
3480
4274
|
const sections = [];
|
|
3481
4275
|
const seen = /* @__PURE__ */ new Set();
|
|
4276
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
3482
4277
|
if (taskPath) {
|
|
3483
|
-
const content = await
|
|
4278
|
+
const content = await cachedReadFile(fileCache, taskPath);
|
|
3484
4279
|
if (content) {
|
|
3485
4280
|
const lineCount = content.split("\n").length;
|
|
3486
4281
|
sections.push({
|
|
@@ -3499,6 +4294,7 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3499
4294
|
if (!seen.has(key)) {
|
|
3500
4295
|
seen.add(key);
|
|
3501
4296
|
const section = await extractSpecSection(
|
|
4297
|
+
fileCache,
|
|
3502
4298
|
chain.requirement.location.filePath,
|
|
3503
4299
|
chain.requirement.id,
|
|
3504
4300
|
chain.requirement.location.line,
|
|
@@ -3514,6 +4310,7 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3514
4310
|
seen.add(key);
|
|
3515
4311
|
if (!chain.requirement || ac.location.filePath !== chain.requirement.location.filePath) {
|
|
3516
4312
|
const section = await extractSpecSection(
|
|
4313
|
+
fileCache,
|
|
3517
4314
|
ac.location.filePath,
|
|
3518
4315
|
ac.id,
|
|
3519
4316
|
ac.location.line,
|
|
@@ -3529,6 +4326,7 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3529
4326
|
if (!seen.has(key)) {
|
|
3530
4327
|
seen.add(key);
|
|
3531
4328
|
const section = await extractSpecSection(
|
|
4329
|
+
fileCache,
|
|
3532
4330
|
comp.location.filePath,
|
|
3533
4331
|
comp.id,
|
|
3534
4332
|
comp.location.line,
|
|
@@ -3543,6 +4341,7 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3543
4341
|
if (!seen.has(key)) {
|
|
3544
4342
|
seen.add(key);
|
|
3545
4343
|
const section = await extractCodeSection(
|
|
4344
|
+
fileCache,
|
|
3546
4345
|
impl.location.filePath,
|
|
3547
4346
|
impl.location.line,
|
|
3548
4347
|
"implementation",
|
|
@@ -3558,6 +4357,7 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3558
4357
|
if (!seen.has(key)) {
|
|
3559
4358
|
seen.add(key);
|
|
3560
4359
|
const section = await extractCodeSection(
|
|
4360
|
+
fileCache,
|
|
3561
4361
|
t.location.filePath,
|
|
3562
4362
|
t.location.line,
|
|
3563
4363
|
"test",
|
|
@@ -3573,6 +4373,7 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3573
4373
|
if (!seen.has(key)) {
|
|
3574
4374
|
seen.add(key);
|
|
3575
4375
|
const section = await extractSpecSection(
|
|
4376
|
+
fileCache,
|
|
3576
4377
|
prop.location.filePath,
|
|
3577
4378
|
prop.id,
|
|
3578
4379
|
prop.location.line,
|
|
@@ -3586,8 +4387,8 @@ async function assembleContent(result, taskPath, contextOptions) {
|
|
|
3586
4387
|
sections.sort((a, b) => a.priority - b.priority);
|
|
3587
4388
|
return sections;
|
|
3588
4389
|
}
|
|
3589
|
-
async function extractSpecSection(filePath, _id, line, type, priority) {
|
|
3590
|
-
const content = await
|
|
4390
|
+
async function extractSpecSection(fileCache, filePath, _id, line, type, priority) {
|
|
4391
|
+
const content = await cachedReadFile(fileCache, filePath);
|
|
3591
4392
|
if (!content) return null;
|
|
3592
4393
|
const lines = content.split("\n");
|
|
3593
4394
|
let startIdx = line - 1;
|
|
@@ -3611,8 +4412,8 @@ async function extractSpecSection(filePath, _id, line, type, priority) {
|
|
|
3611
4412
|
priority
|
|
3612
4413
|
};
|
|
3613
4414
|
}
|
|
3614
|
-
async function extractCodeSection(filePath, line, type, priority, beforeContext = DEFAULT_BEFORE_CONTEXT, afterContext = DEFAULT_AFTER_CONTEXT) {
|
|
3615
|
-
const content = await
|
|
4415
|
+
async function extractCodeSection(fileCache, filePath, line, type, priority, beforeContext = DEFAULT_BEFORE_CONTEXT, afterContext = DEFAULT_AFTER_CONTEXT) {
|
|
4416
|
+
const content = await cachedReadFile(fileCache, filePath);
|
|
3616
4417
|
if (!content) return null;
|
|
3617
4418
|
const lines = content.split("\n");
|
|
3618
4419
|
const lineIdx = line - 1;
|
|
@@ -3668,12 +4469,16 @@ function findEnclosingBlock(lines, lineIdx, beforeContext = DEFAULT_BEFORE_CONTE
|
|
|
3668
4469
|
}
|
|
3669
4470
|
return { start, end };
|
|
3670
4471
|
}
|
|
3671
|
-
async function
|
|
4472
|
+
async function cachedReadFile(cache, filePath) {
|
|
4473
|
+
if (cache.has(filePath)) return cache.get(filePath) ?? null;
|
|
4474
|
+
let content;
|
|
3672
4475
|
try {
|
|
3673
|
-
|
|
4476
|
+
content = await readFile9(filePath, "utf-8");
|
|
3674
4477
|
} catch {
|
|
3675
|
-
|
|
4478
|
+
content = null;
|
|
3676
4479
|
}
|
|
4480
|
+
cache.set(filePath, content);
|
|
4481
|
+
return content;
|
|
3677
4482
|
}
|
|
3678
4483
|
|
|
3679
4484
|
// src/core/trace/content-formatter.ts
|
|
@@ -3836,7 +4641,7 @@ function formatList(result) {
|
|
|
3836
4641
|
}
|
|
3837
4642
|
return [...paths].join("\n");
|
|
3838
4643
|
}
|
|
3839
|
-
function
|
|
4644
|
+
function formatJson4(result) {
|
|
3840
4645
|
const output = {
|
|
3841
4646
|
chains: result.chains.map((chain) => ({
|
|
3842
4647
|
queryId: chain.queryId,
|
|
@@ -3902,10 +4707,6 @@ function buildTraceIndex(specs, markers) {
|
|
|
3902
4707
|
...specs.propertyIds,
|
|
3903
4708
|
...specs.componentNames
|
|
3904
4709
|
]);
|
|
3905
|
-
const idLocations = /* @__PURE__ */ new Map();
|
|
3906
|
-
for (const [id, loc] of specs.idLocations) {
|
|
3907
|
-
idLocations.set(id, loc);
|
|
3908
|
-
}
|
|
3909
4710
|
return {
|
|
3910
4711
|
reqToACs,
|
|
3911
4712
|
acToDesignComponents,
|
|
@@ -3916,7 +4717,7 @@ function buildTraceIndex(specs, markers) {
|
|
|
3916
4717
|
acToReq,
|
|
3917
4718
|
componentToACs,
|
|
3918
4719
|
propertyToACs,
|
|
3919
|
-
idLocations,
|
|
4720
|
+
idLocations: specs.idLocations,
|
|
3920
4721
|
allIds
|
|
3921
4722
|
};
|
|
3922
4723
|
}
|
|
@@ -4022,7 +4823,7 @@ function pushToMap(map, key, value) {
|
|
|
4022
4823
|
}
|
|
4023
4824
|
|
|
4024
4825
|
// src/core/trace/input-resolver.ts
|
|
4025
|
-
import { readFile as
|
|
4826
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4026
4827
|
function resolveIds(ids, index) {
|
|
4027
4828
|
const resolved = [];
|
|
4028
4829
|
const warnings = [];
|
|
@@ -4038,7 +4839,7 @@ function resolveIds(ids, index) {
|
|
|
4038
4839
|
async function resolveTaskFile(taskPath, index) {
|
|
4039
4840
|
let content;
|
|
4040
4841
|
try {
|
|
4041
|
-
content = await
|
|
4842
|
+
content = await readFile10(taskPath, "utf-8");
|
|
4042
4843
|
} catch {
|
|
4043
4844
|
return { ids: [], warnings: [`Task file not found: ${taskPath}`] };
|
|
4044
4845
|
}
|
|
@@ -4089,7 +4890,7 @@ async function resolveTaskFile(taskPath, index) {
|
|
|
4089
4890
|
async function resolveSourceFile(filePath, index) {
|
|
4090
4891
|
let content;
|
|
4091
4892
|
try {
|
|
4092
|
-
content = await
|
|
4893
|
+
content = await readFile10(filePath, "utf-8");
|
|
4093
4894
|
} catch {
|
|
4094
4895
|
return { ids: [], warnings: [`Source file not found: ${filePath}`] };
|
|
4095
4896
|
}
|
|
@@ -4369,44 +5170,6 @@ function deduplicateNodes(nodes) {
|
|
|
4369
5170
|
return result;
|
|
4370
5171
|
}
|
|
4371
5172
|
|
|
4372
|
-
// src/core/trace/scanner.ts
|
|
4373
|
-
function buildScanConfig(fileConfig, overrides) {
|
|
4374
|
-
const section = fileConfig?.check;
|
|
4375
|
-
return {
|
|
4376
|
-
specGlobs: toStringArray3(section?.["spec-globs"]) ?? [...DEFAULT_CHECK_CONFIG.specGlobs],
|
|
4377
|
-
codeGlobs: toStringArray3(section?.["code-globs"]) ?? [...DEFAULT_CHECK_CONFIG.codeGlobs],
|
|
4378
|
-
specIgnore: toStringArray3(section?.["spec-ignore"]) ?? [...DEFAULT_CHECK_CONFIG.specIgnore],
|
|
4379
|
-
codeIgnore: toStringArray3(section?.["code-ignore"]) ?? [...DEFAULT_CHECK_CONFIG.codeIgnore],
|
|
4380
|
-
ignoreMarkers: toStringArray3(section?.["ignore-markers"]) ?? [
|
|
4381
|
-
...DEFAULT_CHECK_CONFIG.ignoreMarkers
|
|
4382
|
-
],
|
|
4383
|
-
markers: toStringArray3(section?.markers) ?? [...DEFAULT_CHECK_CONFIG.markers],
|
|
4384
|
-
idPattern: typeof section?.["id-pattern"] === "string" ? section["id-pattern"] : DEFAULT_CHECK_CONFIG.idPattern,
|
|
4385
|
-
crossRefPatterns: toStringArray3(section?.["cross-ref-patterns"]) ?? [
|
|
4386
|
-
...DEFAULT_CHECK_CONFIG.crossRefPatterns
|
|
4387
|
-
],
|
|
4388
|
-
format: DEFAULT_CHECK_CONFIG.format,
|
|
4389
|
-
schemaDir: DEFAULT_CHECK_CONFIG.schemaDir,
|
|
4390
|
-
schemaEnabled: false,
|
|
4391
|
-
allowWarnings: true,
|
|
4392
|
-
specOnly: false,
|
|
4393
|
-
fix: true,
|
|
4394
|
-
...overrides
|
|
4395
|
-
};
|
|
4396
|
-
}
|
|
4397
|
-
async function scan(configPath) {
|
|
4398
|
-
const fileConfig = await configLoader.load(configPath ?? null);
|
|
4399
|
-
const config = buildScanConfig(fileConfig);
|
|
4400
|
-
const [markers, specs] = await Promise.all([scanMarkers(config), parseSpecs(config)]);
|
|
4401
|
-
return { markers, specs, config };
|
|
4402
|
-
}
|
|
4403
|
-
function toStringArray3(value) {
|
|
4404
|
-
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
4405
|
-
return value;
|
|
4406
|
-
}
|
|
4407
|
-
return null;
|
|
4408
|
-
}
|
|
4409
|
-
|
|
4410
5173
|
// src/core/trace/token-estimator.ts
|
|
4411
5174
|
function estimateTokens(text) {
|
|
4412
5175
|
return Math.ceil(text.length / 4);
|
|
@@ -4524,7 +5287,7 @@ async function traceCommand(options) {
|
|
|
4524
5287
|
} else if (options.list) {
|
|
4525
5288
|
output = formatList(result);
|
|
4526
5289
|
} else if (options.json) {
|
|
4527
|
-
output =
|
|
5290
|
+
output = formatJson4(result);
|
|
4528
5291
|
} else {
|
|
4529
5292
|
output = formatTree(result);
|
|
4530
5293
|
}
|
|
@@ -4546,7 +5309,7 @@ async function traceCommand(options) {
|
|
|
4546
5309
|
}
|
|
4547
5310
|
|
|
4548
5311
|
// src/utils/update-check.ts
|
|
4549
|
-
import
|
|
5312
|
+
import chalk6 from "chalk";
|
|
4550
5313
|
function compareSemver(a, b) {
|
|
4551
5314
|
const partsA = a.split(".").map((s) => Number.parseInt(s, 10));
|
|
4552
5315
|
const partsB = b.split(".").map((s) => Number.parseInt(s, 10));
|
|
@@ -4587,28 +5350,28 @@ function printUpdateWarning(log, result) {
|
|
|
4587
5350
|
console.log("");
|
|
4588
5351
|
if (result.isMajorBump) {
|
|
4589
5352
|
log.warn(
|
|
4590
|
-
|
|
5353
|
+
chalk6.yellow(
|
|
4591
5354
|
`New major version available: ${result.current} \u2192 ${result.latest} (breaking changes)`
|
|
4592
5355
|
)
|
|
4593
5356
|
);
|
|
4594
|
-
log.warn(
|
|
5357
|
+
log.warn(chalk6.dim(" See https://github.com/ncoderz/awa/releases for details"));
|
|
4595
5358
|
} else {
|
|
4596
|
-
log.warn(
|
|
5359
|
+
log.warn(chalk6.yellow(`Update available: ${result.current} \u2192 ${result.latest}`));
|
|
4597
5360
|
}
|
|
4598
|
-
log.warn(
|
|
5361
|
+
log.warn(chalk6.dim(" Run `npm install -g @ncoderz/awa` to update"));
|
|
4599
5362
|
console.log("");
|
|
4600
5363
|
}
|
|
4601
5364
|
|
|
4602
5365
|
// src/utils/update-check-cache.ts
|
|
4603
|
-
import { mkdir as mkdir3, readFile as
|
|
5366
|
+
import { mkdir as mkdir3, readFile as readFile11, writeFile as writeFile4 } from "fs/promises";
|
|
4604
5367
|
import { homedir } from "os";
|
|
4605
|
-
import { dirname, join as
|
|
4606
|
-
var CACHE_DIR =
|
|
4607
|
-
var CACHE_FILE =
|
|
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");
|
|
4608
5371
|
var DEFAULT_INTERVAL_MS = 864e5;
|
|
4609
5372
|
async function shouldCheck(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
4610
5373
|
try {
|
|
4611
|
-
const raw = await
|
|
5374
|
+
const raw = await readFile11(CACHE_FILE, "utf-8");
|
|
4612
5375
|
const data = JSON.parse(raw);
|
|
4613
5376
|
if (typeof data.timestamp !== "number" || typeof data.latestVersion !== "string") {
|
|
4614
5377
|
return true;
|
|
@@ -4620,12 +5383,12 @@ async function shouldCheck(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
|
4620
5383
|
}
|
|
4621
5384
|
async function writeCache(latestVersion) {
|
|
4622
5385
|
try {
|
|
4623
|
-
await mkdir3(
|
|
5386
|
+
await mkdir3(dirname3(CACHE_FILE), { recursive: true });
|
|
4624
5387
|
const data = {
|
|
4625
5388
|
timestamp: Date.now(),
|
|
4626
5389
|
latestVersion
|
|
4627
5390
|
};
|
|
4628
|
-
await
|
|
5391
|
+
await writeFile4(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
4629
5392
|
} catch {
|
|
4630
5393
|
}
|
|
4631
5394
|
}
|
|
@@ -4752,29 +5515,85 @@ template.command("test").description("Run template test fixtures to verify expec
|
|
|
4752
5515
|
process.exit(exitCode);
|
|
4753
5516
|
});
|
|
4754
5517
|
program.addCommand(template);
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
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 = {
|
|
4763
5587
|
json: options.json,
|
|
4764
5588
|
summary: options.summary,
|
|
4765
|
-
maxTokens: options.maxTokens !== void 0 ? Number(options.maxTokens) : void 0,
|
|
4766
|
-
depth: options.depth !== void 0 ? Number(options.depth) : void 0,
|
|
4767
|
-
scope: options.scope,
|
|
4768
|
-
direction: options.direction,
|
|
4769
|
-
noCode: options.code === false,
|
|
4770
|
-
noTests: options.tests === false,
|
|
4771
|
-
beforeContext: options.C !== void 0 ? Number(options.C) : options.B !== void 0 ? Number(options.B) : void 0,
|
|
4772
|
-
afterContext: options.C !== void 0 ? Number(options.C) : options.A !== void 0 ? Number(options.A) : void 0,
|
|
4773
5589
|
config: options.config
|
|
4774
5590
|
};
|
|
4775
|
-
const exitCode = await
|
|
5591
|
+
const exitCode = await codesCommand(codesOptions);
|
|
4776
5592
|
process.exit(exitCode);
|
|
4777
5593
|
});
|
|
5594
|
+
program.addCommand(spec);
|
|
5595
|
+
configureTraceCommand(program.command("trace"));
|
|
5596
|
+
configureRenumberCommand(program.command("renumber", { hidden: true }));
|
|
4778
5597
|
var updateCheckPromise = null;
|
|
4779
5598
|
var isJsonOrSummary = process.argv.includes("--json") || process.argv.includes("--summary");
|
|
4780
5599
|
var isTTY = process.stdout.isTTY === true;
|
|
@@ -4782,7 +5601,7 @@ var isDisabledByEnv = !!process.env.NO_UPDATE_NOTIFIER;
|
|
|
4782
5601
|
if (!isJsonOrSummary && isTTY && !isDisabledByEnv) {
|
|
4783
5602
|
updateCheckPromise = (async () => {
|
|
4784
5603
|
try {
|
|
4785
|
-
const { configLoader: configLoader2 } = await import("./config-
|
|
5604
|
+
const { configLoader: configLoader2 } = await import("./config-EJIXC7D7.js");
|
|
4786
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;
|
|
4787
5606
|
const fileConfig = await configLoader2.load(configPath ?? null);
|
|
4788
5607
|
const updateCheckConfig = fileConfig?.["update-check"];
|
|
@@ -4811,5 +5630,5 @@ program.hook("postAction", async () => {
|
|
|
4811
5630
|
} catch {
|
|
4812
5631
|
}
|
|
4813
5632
|
});
|
|
4814
|
-
program.parseAsync();
|
|
5633
|
+
void program.parseAsync();
|
|
4815
5634
|
//# sourceMappingURL=index.js.map
|