@ncoderz/awa 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ALEGCDAZ.js → chunk-OQZTQ5ZI.js} +1 -1
- package/dist/chunk-OQZTQ5ZI.js.map +1 -0
- package/dist/{config-LWUO5EXL.js → config-WL3SLSP6.js} +2 -2
- package/dist/index.js +619 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/awa/.awa/.agent/schemas/ARCHITECTURE.schema.yaml +0 -10
- package/templates/awa/.awa/.agent/schemas/DESIGN.schema.yaml +0 -10
- package/templates/awa/.awa/.agent/schemas/EXAMPLES.schema.yaml +0 -9
- package/templates/awa/.awa/.agent/schemas/FEAT.schema.yaml +0 -8
- package/templates/awa/.awa/.agent/schemas/PLAN.schema.yaml +5 -10
- package/templates/awa/.awa/.agent/schemas/REQ.schema.yaml +0 -9
- package/templates/awa/.awa/.agent/schemas/TASK.schema.yaml +14 -10
- package/templates/awa/_partials/awa.check.md +1 -1
- package/templates/awa/_partials/awa.code.md +9 -3
- package/templates/awa/_partials/awa.refactor.md +1 -0
- package/templates/awa/_partials/awa.tasks.md +7 -1
- package/templates/awa/_partials/awa.upgrade.md +0 -1
- package/templates/awa/_partials/awa.usage.md +17 -9
- package/templates/awa/_partials/awa.vibe.md +3 -0
- package/dist/chunk-ALEGCDAZ.js.map +0 -1
- /package/dist/{config-LWUO5EXL.js.map → config-WL3SLSP6.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -16,15 +16,15 @@ import {
|
|
|
16
16
|
rmDir,
|
|
17
17
|
walkDirectory,
|
|
18
18
|
writeTextFile
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-OQZTQ5ZI.js";
|
|
20
20
|
|
|
21
21
|
// src/cli/index.ts
|
|
22
|
-
import { Command } from "commander";
|
|
22
|
+
import { Command, Option } from "commander";
|
|
23
23
|
|
|
24
24
|
// src/_generated/package_info.ts
|
|
25
25
|
var PACKAGE_INFO = {
|
|
26
26
|
"name": "@ncoderz/awa",
|
|
27
|
-
"version": "1.
|
|
27
|
+
"version": "1.7.1",
|
|
28
28
|
"author": "Richard Sewell <richard.sewell@ncoderz.com>",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"description": "awa is an Agent Workflow for AIs. It is also a CLI tool to powerfully manage agent workflow files using templates."
|
|
@@ -121,6 +121,83 @@ function checkCodeAgainstSpec(markers, specs, config) {
|
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
for (const propId of specs.propertyIds) {
|
|
125
|
+
if (!testedIds.has(propId)) {
|
|
126
|
+
const loc = specs.idLocations.get(propId);
|
|
127
|
+
const specFile = loc ? void 0 : specs.specFiles.find((sf) => sf.propertyIds.includes(propId));
|
|
128
|
+
findings.push({
|
|
129
|
+
severity: "warning",
|
|
130
|
+
code: "uncovered-property",
|
|
131
|
+
message: `Property '${propId}' has no @awa-test reference`,
|
|
132
|
+
filePath: loc?.filePath ?? specFile?.filePath,
|
|
133
|
+
line: loc?.line,
|
|
134
|
+
id: propId
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const componentFiles = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const marker of markers.markers) {
|
|
140
|
+
if (marker.type === "component") {
|
|
141
|
+
if (!componentFiles.has(marker.id)) {
|
|
142
|
+
componentFiles.set(marker.id, /* @__PURE__ */ new Set());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const fileToComponents = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const marker of markers.markers) {
|
|
148
|
+
if (marker.type === "component") {
|
|
149
|
+
const existing = fileToComponents.get(marker.filePath) ?? [];
|
|
150
|
+
existing.push(marker.id);
|
|
151
|
+
fileToComponents.set(marker.filePath, existing);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const marker of markers.markers) {
|
|
155
|
+
if (marker.type === "impl") {
|
|
156
|
+
const components = fileToComponents.get(marker.filePath);
|
|
157
|
+
if (components) {
|
|
158
|
+
for (const comp of components) {
|
|
159
|
+
componentFiles.get(comp)?.add(marker.id);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const designImplements = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const specFile of specs.specFiles) {
|
|
166
|
+
if (specFile.componentImplements) {
|
|
167
|
+
for (const [comp, ids] of specFile.componentImplements) {
|
|
168
|
+
const existing = designImplements.get(comp) ?? /* @__PURE__ */ new Set();
|
|
169
|
+
for (const id of ids) existing.add(id);
|
|
170
|
+
designImplements.set(comp, existing);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const [compName, codeImplIds] of componentFiles) {
|
|
175
|
+
const designImplIds = designImplements.get(compName);
|
|
176
|
+
if (!designImplIds) continue;
|
|
177
|
+
for (const implId of codeImplIds) {
|
|
178
|
+
if (!designImplIds.has(implId)) {
|
|
179
|
+
findings.push({
|
|
180
|
+
severity: "warning",
|
|
181
|
+
code: "impl-not-in-implements",
|
|
182
|
+
message: `@awa-impl '${implId}' in component '${compName}' is not listed in its IMPLEMENTS`,
|
|
183
|
+
id: implId
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const implId of designImplIds) {
|
|
188
|
+
if (!codeImplIds.has(implId)) {
|
|
189
|
+
const loc = specs.idLocations.get(compName);
|
|
190
|
+
findings.push({
|
|
191
|
+
severity: "warning",
|
|
192
|
+
code: "implements-not-in-impl",
|
|
193
|
+
message: `IMPLEMENTS '${implId}' in component '${compName}' has no @awa-impl in code`,
|
|
194
|
+
filePath: loc?.filePath,
|
|
195
|
+
line: loc?.line,
|
|
196
|
+
id: implId
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
124
201
|
return { findings };
|
|
125
202
|
}
|
|
126
203
|
|
|
@@ -259,6 +336,321 @@ async function collectCodeFiles(codeGlobs, ignore) {
|
|
|
259
336
|
return collectFiles(codeGlobs, ignore);
|
|
260
337
|
}
|
|
261
338
|
|
|
339
|
+
// src/core/check/matrix-fixer.ts
|
|
340
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
341
|
+
import { basename } from "path";
|
|
342
|
+
async function fixMatrices(specs, crossRefPatterns) {
|
|
343
|
+
const codeToReqFile = buildCodeToReqFileMap(specs.specFiles);
|
|
344
|
+
const fileResults = [];
|
|
345
|
+
for (const specFile of specs.specFiles) {
|
|
346
|
+
const fileName = basename(specFile.filePath);
|
|
347
|
+
if (fileName.startsWith("DESIGN-")) {
|
|
348
|
+
const changed = await fixDesignMatrix(specFile.filePath, codeToReqFile, crossRefPatterns);
|
|
349
|
+
fileResults.push({ filePath: specFile.filePath, changed });
|
|
350
|
+
} else if (fileName.startsWith("TASK-")) {
|
|
351
|
+
const changed = await fixTaskMatrix(
|
|
352
|
+
specFile.filePath,
|
|
353
|
+
codeToReqFile,
|
|
354
|
+
specs,
|
|
355
|
+
crossRefPatterns
|
|
356
|
+
);
|
|
357
|
+
fileResults.push({ filePath: specFile.filePath, changed });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
filesFixed: fileResults.filter((r) => r.changed).length,
|
|
362
|
+
fileResults
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function buildCodeToReqFileMap(specFiles) {
|
|
366
|
+
const map = /* @__PURE__ */ new Map();
|
|
367
|
+
for (const sf of specFiles) {
|
|
368
|
+
if (/\bREQ-/.test(basename(sf.filePath)) && sf.code) {
|
|
369
|
+
map.set(sf.code, basename(sf.filePath));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return map;
|
|
373
|
+
}
|
|
374
|
+
function getCodePrefix(id) {
|
|
375
|
+
const match = /^([A-Z][A-Z0-9]*)[-_]/.exec(id);
|
|
376
|
+
return match?.[1] ?? "";
|
|
377
|
+
}
|
|
378
|
+
async function fixDesignMatrix(filePath, codeToReqFile, crossRefPatterns) {
|
|
379
|
+
let content;
|
|
380
|
+
try {
|
|
381
|
+
content = await readFile2(filePath, "utf-8");
|
|
382
|
+
} catch {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const { components, properties } = parseDesignFileData(content, crossRefPatterns);
|
|
386
|
+
const acToComponents = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const comp of components) {
|
|
388
|
+
for (const acId of comp.implements) {
|
|
389
|
+
const existing = acToComponents.get(acId) ?? [];
|
|
390
|
+
existing.push(comp.name);
|
|
391
|
+
acToComponents.set(acId, existing);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const acToProperties = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const prop of properties) {
|
|
396
|
+
for (const acId of prop.validates) {
|
|
397
|
+
const existing = acToProperties.get(acId) ?? [];
|
|
398
|
+
existing.push(prop.id);
|
|
399
|
+
acToProperties.set(acId, existing);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const allAcIds = [...acToComponents.keys()];
|
|
403
|
+
const grouped = groupByReqFile(allAcIds, codeToReqFile);
|
|
404
|
+
const newSection = generateDesignSection(grouped, acToComponents, acToProperties);
|
|
405
|
+
const newContent = replaceTraceabilitySection(content, newSection);
|
|
406
|
+
if (newContent === content) return false;
|
|
407
|
+
await writeFile(filePath, newContent, "utf-8");
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
function parseDesignFileData(content, crossRefPatterns) {
|
|
411
|
+
const lines = content.split("\n");
|
|
412
|
+
const components = [];
|
|
413
|
+
const properties = [];
|
|
414
|
+
const componentRegex = /^###\s+([A-Z][A-Z0-9]*-[A-Za-z][A-Za-z0-9]*(?:[A-Z][a-z0-9]*)*)\s*$/;
|
|
415
|
+
const reqIdRegex = /^###\s+([A-Z][A-Z0-9]*-\d+(?:\.\d+)?)\s*:/;
|
|
416
|
+
const propIdRegex = /^-\s+([A-Z][A-Z0-9]*_P-\d+)\s/;
|
|
417
|
+
let currentComponent = null;
|
|
418
|
+
let lastPropertyId = null;
|
|
419
|
+
for (const line of lines) {
|
|
420
|
+
const compMatch = componentRegex.exec(line);
|
|
421
|
+
if (compMatch?.[1] && !reqIdRegex.test(line)) {
|
|
422
|
+
currentComponent = compMatch[1];
|
|
423
|
+
lastPropertyId = null;
|
|
424
|
+
components.push({ name: currentComponent, implements: [] });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (/^#{1,2}\s/.test(line) && !compMatch) {
|
|
428
|
+
currentComponent = null;
|
|
429
|
+
lastPropertyId = null;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const propMatch = propIdRegex.exec(line);
|
|
433
|
+
if (propMatch?.[1]) {
|
|
434
|
+
lastPropertyId = propMatch[1];
|
|
435
|
+
properties.push({ id: lastPropertyId, validates: [] });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
for (const pattern of crossRefPatterns) {
|
|
439
|
+
const patIdx = line.indexOf(pattern);
|
|
440
|
+
if (patIdx !== -1) {
|
|
441
|
+
const afterPattern = line.slice(patIdx + pattern.length);
|
|
442
|
+
const ids = extractIdsFromText(afterPattern);
|
|
443
|
+
if (ids.length > 0) {
|
|
444
|
+
const isImplements = pattern.toLowerCase().includes("implements");
|
|
445
|
+
if (isImplements && currentComponent) {
|
|
446
|
+
const comp = components.find((c) => c.name === currentComponent);
|
|
447
|
+
if (comp) comp.implements.push(...ids);
|
|
448
|
+
} else if (!isImplements && lastPropertyId) {
|
|
449
|
+
const prop = properties.find((p) => p.id === lastPropertyId);
|
|
450
|
+
if (prop) prop.validates.push(...ids);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { components, properties };
|
|
457
|
+
}
|
|
458
|
+
function generateDesignSection(grouped, acToComponents, acToProperties) {
|
|
459
|
+
const lines = [];
|
|
460
|
+
const reqFiles = [...grouped.keys()].sort();
|
|
461
|
+
for (const reqFile of reqFiles) {
|
|
462
|
+
lines.push(`### ${reqFile}`);
|
|
463
|
+
lines.push("");
|
|
464
|
+
const acIds = grouped.get(reqFile) ?? [];
|
|
465
|
+
acIds.sort(compareIds);
|
|
466
|
+
for (const acId of acIds) {
|
|
467
|
+
const components = acToComponents.get(acId) ?? [];
|
|
468
|
+
const props = acToProperties.get(acId) ?? [];
|
|
469
|
+
for (const comp of components) {
|
|
470
|
+
const propStr = props.length > 0 ? ` (${props.join(", ")})` : "";
|
|
471
|
+
lines.push(`- ${acId} \u2192 ${comp}${propStr}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
lines.push("");
|
|
475
|
+
}
|
|
476
|
+
return lines.join("\n");
|
|
477
|
+
}
|
|
478
|
+
async function fixTaskMatrix(filePath, codeToReqFile, specs, crossRefPatterns) {
|
|
479
|
+
let content;
|
|
480
|
+
try {
|
|
481
|
+
content = await readFile2(filePath, "utf-8");
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
const { tasks, sourceDesigns } = parseTaskFileData(content, crossRefPatterns);
|
|
486
|
+
const acToTasks = /* @__PURE__ */ new Map();
|
|
487
|
+
for (const task of tasks) {
|
|
488
|
+
for (const acId of task.implements) {
|
|
489
|
+
const existing = acToTasks.get(acId) ?? [];
|
|
490
|
+
existing.push(task.id);
|
|
491
|
+
acToTasks.set(acId, existing);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const idToTestTasks = /* @__PURE__ */ new Map();
|
|
495
|
+
for (const task of tasks) {
|
|
496
|
+
for (const testId of task.tests) {
|
|
497
|
+
const existing = idToTestTasks.get(testId) ?? [];
|
|
498
|
+
existing.push(task.id);
|
|
499
|
+
idToTestTasks.set(testId, existing);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const sourcePropertyIds = /* @__PURE__ */ new Set();
|
|
503
|
+
for (const designName of sourceDesigns) {
|
|
504
|
+
const designCode = extractCodeFromFileName(designName);
|
|
505
|
+
for (const sf of specs.specFiles) {
|
|
506
|
+
if (sf.code === designCode && /\bDESIGN-/.test(basename(sf.filePath))) {
|
|
507
|
+
for (const propId of sf.propertyIds) sourcePropertyIds.add(propId);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const allIdsForMatrix = [.../* @__PURE__ */ new Set([...acToTasks.keys(), ...idToTestTasks.keys()])];
|
|
512
|
+
const grouped = groupByReqFile(allIdsForMatrix, codeToReqFile);
|
|
513
|
+
const newSection = generateTaskSection(grouped, acToTasks, idToTestTasks, sourcePropertyIds);
|
|
514
|
+
const newContent = replaceTraceabilitySection(content, newSection);
|
|
515
|
+
if (newContent === content) return false;
|
|
516
|
+
await writeFile(filePath, newContent, "utf-8");
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
function parseTaskFileData(content, crossRefPatterns) {
|
|
520
|
+
const lines = content.split("\n");
|
|
521
|
+
const tasks = [];
|
|
522
|
+
const sourceReqs = [];
|
|
523
|
+
const sourceDesigns = [];
|
|
524
|
+
const sourceRegex = /^SOURCE:\s*(.+)/;
|
|
525
|
+
const taskIdRegex = /^-\s+\[[ x]\]\s+(T-[A-Z][A-Z0-9]*-\d+)/;
|
|
526
|
+
let currentTaskId = null;
|
|
527
|
+
for (const line of lines) {
|
|
528
|
+
const sourceMatch = sourceRegex.exec(line);
|
|
529
|
+
if (sourceMatch?.[1]) {
|
|
530
|
+
const parts = sourceMatch[1].split(",").map((s) => s.trim());
|
|
531
|
+
for (const part of parts) {
|
|
532
|
+
if (part.startsWith("REQ-")) sourceReqs.push(part);
|
|
533
|
+
else if (part.startsWith("DESIGN-")) sourceDesigns.push(part);
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const taskMatch = taskIdRegex.exec(line);
|
|
538
|
+
if (taskMatch?.[1]) {
|
|
539
|
+
currentTaskId = taskMatch[1];
|
|
540
|
+
tasks.push({ id: currentTaskId, implements: [], tests: [] });
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (/^##\s/.test(line)) {
|
|
544
|
+
currentTaskId = null;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (/^---/.test(line)) {
|
|
548
|
+
currentTaskId = null;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (currentTaskId) {
|
|
552
|
+
const task = tasks.find((t) => t.id === currentTaskId);
|
|
553
|
+
if (task) {
|
|
554
|
+
for (const pattern of crossRefPatterns) {
|
|
555
|
+
const patIdx = line.indexOf(pattern);
|
|
556
|
+
if (patIdx !== -1) {
|
|
557
|
+
const afterPattern = line.slice(patIdx + pattern.length);
|
|
558
|
+
const ids = extractIdsFromText(afterPattern);
|
|
559
|
+
if (ids.length > 0) {
|
|
560
|
+
const isImplements = pattern.toLowerCase().includes("implements");
|
|
561
|
+
if (isImplements) {
|
|
562
|
+
task.implements.push(...ids);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const testsIdx = line.indexOf("TESTS:");
|
|
568
|
+
if (testsIdx !== -1) {
|
|
569
|
+
const afterTests = line.slice(testsIdx + "TESTS:".length);
|
|
570
|
+
const ids = extractIdsFromText(afterTests);
|
|
571
|
+
if (ids.length > 0) {
|
|
572
|
+
task.tests.push(...ids);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return { tasks, sourceReqs, sourceDesigns };
|
|
579
|
+
}
|
|
580
|
+
function generateTaskSection(grouped, acToTasks, idToTestTasks, propertyIds) {
|
|
581
|
+
const lines = [];
|
|
582
|
+
const reqFiles = [...grouped.keys()].sort();
|
|
583
|
+
for (const reqFile of reqFiles) {
|
|
584
|
+
lines.push(`### ${reqFile}`);
|
|
585
|
+
lines.push("");
|
|
586
|
+
const ids = grouped.get(reqFile) ?? [];
|
|
587
|
+
const acIds = ids.filter((id) => !propertyIds.has(id));
|
|
588
|
+
const propIds = ids.filter((id) => propertyIds.has(id));
|
|
589
|
+
acIds.sort(compareIds);
|
|
590
|
+
propIds.sort(compareIds);
|
|
591
|
+
for (const acId of acIds) {
|
|
592
|
+
const tasks = acToTasks.get(acId) ?? [];
|
|
593
|
+
const testTasks = idToTestTasks.get(acId) ?? [];
|
|
594
|
+
const taskStr = tasks.length > 0 ? tasks.join(", ") : "(none)";
|
|
595
|
+
const testStr = testTasks.length > 0 ? ` (${testTasks.join(", ")})` : "";
|
|
596
|
+
lines.push(`- ${acId} \u2192 ${taskStr}${testStr}`);
|
|
597
|
+
}
|
|
598
|
+
for (const propId of propIds) {
|
|
599
|
+
const testTasks = idToTestTasks.get(propId) ?? [];
|
|
600
|
+
const testStr = testTasks.length > 0 ? testTasks.join(", ") : "(none)";
|
|
601
|
+
lines.push(`- ${propId} \u2192 ${testStr}`);
|
|
602
|
+
}
|
|
603
|
+
lines.push("");
|
|
604
|
+
}
|
|
605
|
+
return lines.join("\n").trimEnd();
|
|
606
|
+
}
|
|
607
|
+
function groupByReqFile(ids, codeToReqFile) {
|
|
608
|
+
const groups = /* @__PURE__ */ new Map();
|
|
609
|
+
for (const id of ids) {
|
|
610
|
+
const code = getCodePrefix(id);
|
|
611
|
+
const reqFile = codeToReqFile.get(code);
|
|
612
|
+
if (!reqFile) continue;
|
|
613
|
+
const existing = groups.get(reqFile) ?? [];
|
|
614
|
+
existing.push(id);
|
|
615
|
+
groups.set(reqFile, existing);
|
|
616
|
+
}
|
|
617
|
+
return groups;
|
|
618
|
+
}
|
|
619
|
+
function replaceTraceabilitySection(content, newSection) {
|
|
620
|
+
const lines = content.split("\n");
|
|
621
|
+
const sectionStart = lines.findIndex((l) => /^##\s+Requirements Traceability\s*$/.test(l));
|
|
622
|
+
if (sectionStart === -1) return content;
|
|
623
|
+
let sectionEnd = lines.length;
|
|
624
|
+
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
625
|
+
const line = lines[i];
|
|
626
|
+
if (line !== void 0 && /^##\s/.test(line)) {
|
|
627
|
+
sectionEnd = i;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const before = lines.slice(0, sectionStart + 1);
|
|
632
|
+
const after = lines.slice(sectionEnd);
|
|
633
|
+
const result = [...before, "", newSection.trimEnd(), "", ...after];
|
|
634
|
+
return result.join("\n");
|
|
635
|
+
}
|
|
636
|
+
function extractIdsFromText(text) {
|
|
637
|
+
const idRegex = /[A-Z][A-Z0-9]*-\d+(?:\.\d+)?(?:_AC-\d+)?|[A-Z][A-Z0-9]*_P-\d+/g;
|
|
638
|
+
const ids = [];
|
|
639
|
+
let match = idRegex.exec(text);
|
|
640
|
+
while (match !== null) {
|
|
641
|
+
ids.push(match[0]);
|
|
642
|
+
match = idRegex.exec(text);
|
|
643
|
+
}
|
|
644
|
+
return ids;
|
|
645
|
+
}
|
|
646
|
+
function extractCodeFromFileName(fileName) {
|
|
647
|
+
const match = /^(?:REQ|DESIGN|FEAT|EXAMPLES|API|TASK)-([A-Z][A-Z0-9]*)-/.exec(fileName);
|
|
648
|
+
return match?.[1] ?? "";
|
|
649
|
+
}
|
|
650
|
+
function compareIds(a, b) {
|
|
651
|
+
return a.localeCompare(b, void 0, { numeric: true });
|
|
652
|
+
}
|
|
653
|
+
|
|
262
654
|
// src/core/check/reporter.ts
|
|
263
655
|
import chalk from "chalk";
|
|
264
656
|
function report(findings, format) {
|
|
@@ -334,7 +726,7 @@ function printRuleContext(f) {
|
|
|
334
726
|
}
|
|
335
727
|
|
|
336
728
|
// src/core/check/rule-loader.ts
|
|
337
|
-
import { readFile as
|
|
729
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
338
730
|
import { join } from "path";
|
|
339
731
|
import { parse as parseYaml } from "yaml";
|
|
340
732
|
async function loadRules(schemaDir) {
|
|
@@ -355,7 +747,7 @@ function matchesTargetGlob(filePath, targetGlob) {
|
|
|
355
747
|
async function loadRuleFile(filePath) {
|
|
356
748
|
let content;
|
|
357
749
|
try {
|
|
358
|
-
content = await
|
|
750
|
+
content = await readFile3(filePath, "utf-8");
|
|
359
751
|
} catch {
|
|
360
752
|
return null;
|
|
361
753
|
}
|
|
@@ -547,7 +939,7 @@ var RuleValidationError = class extends Error {
|
|
|
547
939
|
};
|
|
548
940
|
|
|
549
941
|
// src/core/check/schema-checker.ts
|
|
550
|
-
import { readFile as
|
|
942
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
551
943
|
import remarkGfm from "remark-gfm";
|
|
552
944
|
import remarkParse from "remark-parse";
|
|
553
945
|
import { unified } from "unified";
|
|
@@ -594,7 +986,7 @@ async function checkSchemasAsync(specFiles, ruleSets) {
|
|
|
594
986
|
if (matchingRules.length === 0) continue;
|
|
595
987
|
let content;
|
|
596
988
|
try {
|
|
597
|
-
content = await
|
|
989
|
+
content = await readFile4(spec.filePath, "utf-8");
|
|
598
990
|
} catch {
|
|
599
991
|
continue;
|
|
600
992
|
}
|
|
@@ -1039,8 +1431,8 @@ function collectAllCodeBlocks(section) {
|
|
|
1039
1431
|
}
|
|
1040
1432
|
|
|
1041
1433
|
// src/core/check/spec-parser.ts
|
|
1042
|
-
import { readFile as
|
|
1043
|
-
import { basename } from "path";
|
|
1434
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1435
|
+
import { basename as basename2 } from "path";
|
|
1044
1436
|
async function parseSpecs(config) {
|
|
1045
1437
|
const files = await collectSpecFiles(config.specGlobs, config.specIgnore);
|
|
1046
1438
|
const specFiles = [];
|
|
@@ -1068,7 +1460,7 @@ async function parseSpecs(config) {
|
|
|
1068
1460
|
async function parseSpecFile(filePath, crossRefPatterns) {
|
|
1069
1461
|
let content;
|
|
1070
1462
|
try {
|
|
1071
|
-
content = await
|
|
1463
|
+
content = await readFile5(filePath, "utf-8");
|
|
1072
1464
|
} catch {
|
|
1073
1465
|
return null;
|
|
1074
1466
|
}
|
|
@@ -1080,10 +1472,12 @@ async function parseSpecFile(filePath, crossRefPatterns) {
|
|
|
1080
1472
|
const componentNames = [];
|
|
1081
1473
|
const crossRefs = [];
|
|
1082
1474
|
const idLocations = /* @__PURE__ */ new Map();
|
|
1475
|
+
const componentImplements = /* @__PURE__ */ new Map();
|
|
1083
1476
|
const reqIdRegex = /^###\s+([A-Z][A-Z0-9]*-\d+(?:\.\d+)?)\s*:/;
|
|
1084
1477
|
const acIdRegex = /^-\s+(?:\[[ x]\]\s+)?([A-Z][A-Z0-9]*-\d+(?:\.\d+)?_AC-\d+)\s/;
|
|
1085
1478
|
const propIdRegex = /^-\s+([A-Z][A-Z0-9]*_P-\d+)\s/;
|
|
1086
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;
|
|
1087
1481
|
for (const [i, line] of lines.entries()) {
|
|
1088
1482
|
const lineNum = i + 1;
|
|
1089
1483
|
const reqMatch = reqIdRegex.exec(line);
|
|
@@ -1106,16 +1500,25 @@ async function parseSpecFile(filePath, crossRefPatterns) {
|
|
|
1106
1500
|
if (!reqIdRegex.test(line)) {
|
|
1107
1501
|
componentNames.push(compMatch[1]);
|
|
1108
1502
|
idLocations.set(compMatch[1], { filePath, line: lineNum });
|
|
1503
|
+
currentComponent = compMatch[1];
|
|
1109
1504
|
}
|
|
1110
1505
|
}
|
|
1506
|
+
if (/^#{1,2}\s/.test(line) && !compMatch) {
|
|
1507
|
+
currentComponent = null;
|
|
1508
|
+
}
|
|
1111
1509
|
for (const pattern of crossRefPatterns) {
|
|
1112
1510
|
const patIdx = line.indexOf(pattern);
|
|
1113
1511
|
if (patIdx !== -1) {
|
|
1114
1512
|
const afterPattern = line.slice(patIdx + pattern.length);
|
|
1115
|
-
const ids =
|
|
1513
|
+
const ids = extractIdsFromText2(afterPattern);
|
|
1116
1514
|
if (ids.length > 0) {
|
|
1117
1515
|
const type = pattern.toLowerCase().includes("implements") ? "implements" : "validates";
|
|
1118
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
|
+
}
|
|
1119
1522
|
}
|
|
1120
1523
|
}
|
|
1121
1524
|
}
|
|
@@ -1128,16 +1531,17 @@ async function parseSpecFile(filePath, crossRefPatterns) {
|
|
|
1128
1531
|
propertyIds,
|
|
1129
1532
|
componentNames,
|
|
1130
1533
|
crossRefs,
|
|
1131
|
-
idLocations
|
|
1534
|
+
idLocations,
|
|
1535
|
+
componentImplements
|
|
1132
1536
|
};
|
|
1133
1537
|
}
|
|
1134
1538
|
function extractCodePrefix(filePath) {
|
|
1135
|
-
const name =
|
|
1539
|
+
const name = basename2(filePath, ".md");
|
|
1136
1540
|
const match = /^(?:REQ|DESIGN|FEAT|EXAMPLES|API)-([A-Z][A-Z0-9]*)-/.exec(name);
|
|
1137
1541
|
if (match?.[1]) return match[1];
|
|
1138
1542
|
return "";
|
|
1139
1543
|
}
|
|
1140
|
-
function
|
|
1544
|
+
function extractIdsFromText2(text) {
|
|
1141
1545
|
const idRegex = /[A-Z][A-Z0-9]*-\d+(?:\.\d+)?(?:_AC-\d+)?|[A-Z][A-Z0-9]*_P-\d+/g;
|
|
1142
1546
|
const ids = [];
|
|
1143
1547
|
let match = idRegex.exec(text);
|
|
@@ -1170,6 +1574,37 @@ function checkSpecAgainstSpec(specs, markers, config) {
|
|
|
1170
1574
|
}
|
|
1171
1575
|
}
|
|
1172
1576
|
}
|
|
1577
|
+
const implementedAcIds = /* @__PURE__ */ new Set();
|
|
1578
|
+
for (const specFile of specs.specFiles) {
|
|
1579
|
+
for (const crossRef of specFile.crossRefs) {
|
|
1580
|
+
if (crossRef.type === "implements") {
|
|
1581
|
+
for (const id of crossRef.ids) {
|
|
1582
|
+
implementedAcIds.add(id);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const reqAcIds = /* @__PURE__ */ new Set();
|
|
1588
|
+
for (const specFile of specs.specFiles) {
|
|
1589
|
+
if (/\bREQ-/.test(specFile.filePath)) {
|
|
1590
|
+
for (const acId of specFile.acIds) {
|
|
1591
|
+
reqAcIds.add(acId);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
for (const acId of reqAcIds) {
|
|
1596
|
+
if (!implementedAcIds.has(acId)) {
|
|
1597
|
+
const loc = specs.idLocations.get(acId);
|
|
1598
|
+
findings.push({
|
|
1599
|
+
severity: "error",
|
|
1600
|
+
code: "unlinked-ac",
|
|
1601
|
+
message: `Acceptance criterion '${acId}' is not claimed by any DESIGN IMPLEMENTS`,
|
|
1602
|
+
filePath: loc?.filePath,
|
|
1603
|
+
line: loc?.line,
|
|
1604
|
+
id: acId
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1173
1608
|
if (!config.specOnly) {
|
|
1174
1609
|
const referencedCodes = /* @__PURE__ */ new Set();
|
|
1175
1610
|
for (const marker of markers.markers) {
|
|
@@ -1238,7 +1673,8 @@ var DEFAULT_CHECK_CONFIG = {
|
|
|
1238
1673
|
schemaDir: ".awa/.agent/schemas",
|
|
1239
1674
|
schemaEnabled: true,
|
|
1240
1675
|
allowWarnings: false,
|
|
1241
|
-
specOnly: false
|
|
1676
|
+
specOnly: false,
|
|
1677
|
+
fix: true
|
|
1242
1678
|
};
|
|
1243
1679
|
|
|
1244
1680
|
// src/commands/check.ts
|
|
@@ -1267,7 +1703,20 @@ async function checkCommand(cliOptions) {
|
|
|
1267
1703
|
const allFindings = config.allowWarnings ? combinedFindings : combinedFindings.map(
|
|
1268
1704
|
(f) => f.severity === "warning" ? { ...f, severity: "error" } : f
|
|
1269
1705
|
);
|
|
1270
|
-
|
|
1706
|
+
const isSummary = cliOptions.summary === true;
|
|
1707
|
+
if (isSummary) {
|
|
1708
|
+
const errors = allFindings.filter((f) => f.severity === "error").length;
|
|
1709
|
+
const warnings = allFindings.filter((f) => f.severity === "warning").length;
|
|
1710
|
+
console.log(`errors: ${errors}, warnings: ${warnings}`);
|
|
1711
|
+
} else {
|
|
1712
|
+
report(allFindings, config.format);
|
|
1713
|
+
}
|
|
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
|
+
}
|
|
1271
1720
|
const hasErrors = allFindings.some((f) => f.severity === "error");
|
|
1272
1721
|
return hasErrors ? 1 : 0;
|
|
1273
1722
|
} catch (error) {
|
|
@@ -1299,11 +1748,12 @@ function buildCheckConfig(fileConfig, cliOptions) {
|
|
|
1299
1748
|
const ignoreMarkers = toStringArray(section?.["ignore-markers"]) ?? [
|
|
1300
1749
|
...DEFAULT_CHECK_CONFIG.ignoreMarkers
|
|
1301
1750
|
];
|
|
1302
|
-
const format = cliOptions.format === "json" ? "json" : section?.format === "json" ? "json" : DEFAULT_CHECK_CONFIG.format;
|
|
1751
|
+
const format = cliOptions.json === true ? "json" : cliOptions.format === "json" ? "json" : section?.format === "json" ? "json" : DEFAULT_CHECK_CONFIG.format;
|
|
1303
1752
|
const schemaDir = typeof section?.["schema-dir"] === "string" ? section["schema-dir"] : DEFAULT_CHECK_CONFIG.schemaDir;
|
|
1304
1753
|
const schemaEnabled = typeof section?.["schema-enabled"] === "boolean" ? section["schema-enabled"] : DEFAULT_CHECK_CONFIG.schemaEnabled;
|
|
1305
1754
|
const allowWarnings = cliOptions.allowWarnings === true ? true : typeof section?.["allow-warnings"] === "boolean" ? section["allow-warnings"] : DEFAULT_CHECK_CONFIG.allowWarnings;
|
|
1306
1755
|
const specOnly = cliOptions.specOnly === true ? true : typeof section?.["spec-only"] === "boolean" ? section["spec-only"] : DEFAULT_CHECK_CONFIG.specOnly;
|
|
1756
|
+
const fix = cliOptions.fix === false ? false : DEFAULT_CHECK_CONFIG.fix;
|
|
1307
1757
|
return {
|
|
1308
1758
|
specGlobs,
|
|
1309
1759
|
codeGlobs,
|
|
@@ -1317,7 +1767,8 @@ function buildCheckConfig(fileConfig, cliOptions) {
|
|
|
1317
1767
|
schemaDir,
|
|
1318
1768
|
schemaEnabled,
|
|
1319
1769
|
allowWarnings,
|
|
1320
|
-
specOnly
|
|
1770
|
+
specOnly,
|
|
1771
|
+
fix
|
|
1321
1772
|
};
|
|
1322
1773
|
}
|
|
1323
1774
|
function toStringArray(value) {
|
|
@@ -2341,8 +2792,8 @@ async function prepareDiff(options) {
|
|
|
2341
2792
|
async function diffCommand(cliOptions) {
|
|
2342
2793
|
try {
|
|
2343
2794
|
const fileConfig = await configLoader.load(cliOptions.config ?? null);
|
|
2344
|
-
if (cliOptions.
|
|
2345
|
-
const mode = cliOptions.
|
|
2795
|
+
if (cliOptions.allTargets || cliOptions.target) {
|
|
2796
|
+
const mode = cliOptions.allTargets ? "all" : "single";
|
|
2346
2797
|
const targets = batchRunner.resolveTargets(cliOptions, fileConfig, mode, cliOptions.target);
|
|
2347
2798
|
let hasDifferences = false;
|
|
2348
2799
|
for (const { targetName, options: options2 } of targets) {
|
|
@@ -2473,7 +2924,7 @@ var FeaturesReporter = class {
|
|
|
2473
2924
|
var featuresReporter = new FeaturesReporter();
|
|
2474
2925
|
|
|
2475
2926
|
// src/core/features/scanner.ts
|
|
2476
|
-
import { readdir, readFile as
|
|
2927
|
+
import { readdir, readFile as readFile6 } from "fs/promises";
|
|
2477
2928
|
import { join as join7, relative as relative3 } from "path";
|
|
2478
2929
|
var FEATURE_PATTERN = /it\.features\.(?:includes|indexOf)\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
2479
2930
|
async function* walkAllFiles(dir) {
|
|
@@ -2507,7 +2958,7 @@ var FeatureScanner = class {
|
|
|
2507
2958
|
for await (const filePath of walkAllFiles(templatePath)) {
|
|
2508
2959
|
filesScanned++;
|
|
2509
2960
|
try {
|
|
2510
|
-
const content = await
|
|
2961
|
+
const content = await readFile6(filePath, "utf-8");
|
|
2511
2962
|
const flags = this.extractFlags(content);
|
|
2512
2963
|
const relPath = relative3(templatePath, filePath);
|
|
2513
2964
|
for (const flag of flags) {
|
|
@@ -2532,22 +2983,37 @@ var featureScanner = new FeatureScanner();
|
|
|
2532
2983
|
|
|
2533
2984
|
// src/commands/features.ts
|
|
2534
2985
|
async function featuresCommand(cliOptions) {
|
|
2986
|
+
let mergedDir = null;
|
|
2535
2987
|
try {
|
|
2536
|
-
|
|
2988
|
+
const silent = cliOptions.json || cliOptions.summary;
|
|
2989
|
+
if (!silent) {
|
|
2537
2990
|
intro2("awa CLI - Feature Discovery");
|
|
2538
2991
|
}
|
|
2539
2992
|
const fileConfig = await configLoader.load(cliOptions.config ?? null);
|
|
2540
2993
|
const templateSource = cliOptions.template ?? fileConfig?.template ?? null;
|
|
2541
2994
|
const refresh = cliOptions.refresh ?? fileConfig?.refresh ?? false;
|
|
2542
2995
|
const template2 = await templateResolver.resolve(templateSource, refresh);
|
|
2543
|
-
const
|
|
2996
|
+
const overlays = cliOptions.overlay ?? fileConfig?.overlay ?? [];
|
|
2997
|
+
let templatePath = template2.localPath;
|
|
2998
|
+
if (overlays.length > 0) {
|
|
2999
|
+
const overlayDirs = await resolveOverlays(overlays, refresh);
|
|
3000
|
+
mergedDir = await buildMergedDir(template2.localPath, overlayDirs);
|
|
3001
|
+
templatePath = mergedDir;
|
|
3002
|
+
}
|
|
3003
|
+
const scanResult = await featureScanner.scan(templatePath);
|
|
2544
3004
|
const presets = fileConfig?.presets;
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
}
|
|
2550
|
-
|
|
3005
|
+
if (cliOptions.summary) {
|
|
3006
|
+
console.log(
|
|
3007
|
+
`features: ${scanResult.features.length}, files-scanned: ${scanResult.filesScanned}`
|
|
3008
|
+
);
|
|
3009
|
+
} else {
|
|
3010
|
+
featuresReporter.report({
|
|
3011
|
+
scanResult,
|
|
3012
|
+
json: cliOptions.json ?? false,
|
|
3013
|
+
presets
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
if (!silent) {
|
|
2551
3017
|
outro2("Feature discovery complete!");
|
|
2552
3018
|
}
|
|
2553
3019
|
return 0;
|
|
@@ -2558,6 +3024,10 @@ async function featuresCommand(cliOptions) {
|
|
|
2558
3024
|
logger.error(String(error));
|
|
2559
3025
|
}
|
|
2560
3026
|
return 1;
|
|
3027
|
+
} finally {
|
|
3028
|
+
if (mergedDir) {
|
|
3029
|
+
await rmDir(mergedDir);
|
|
3030
|
+
}
|
|
2561
3031
|
}
|
|
2562
3032
|
}
|
|
2563
3033
|
|
|
@@ -2649,8 +3119,8 @@ async function generateCommand(cliOptions) {
|
|
|
2649
3119
|
if (!cliOptions.config && fileConfig === null) {
|
|
2650
3120
|
logger.info("Tip: create .awa.toml to save your options for next time.");
|
|
2651
3121
|
}
|
|
2652
|
-
if (cliOptions.
|
|
2653
|
-
const mode = cliOptions.
|
|
3122
|
+
if (cliOptions.allTargets || cliOptions.target) {
|
|
3123
|
+
const mode = cliOptions.allTargets ? "all" : "single";
|
|
2654
3124
|
const targets = batchRunner.resolveTargets(cliOptions, fileConfig, mode, cliOptions.target);
|
|
2655
3125
|
for (const { targetName, options: options2 } of targets) {
|
|
2656
3126
|
batchRunner.logForTarget(targetName, "Starting generation...");
|
|
@@ -2684,7 +3154,7 @@ import { intro as intro4, outro as outro4 } from "@clack/prompts";
|
|
|
2684
3154
|
|
|
2685
3155
|
// src/core/template-test/fixture-loader.ts
|
|
2686
3156
|
import { readdir as readdir2 } from "fs/promises";
|
|
2687
|
-
import { basename as
|
|
3157
|
+
import { basename as basename3, extname, join as join8 } from "path";
|
|
2688
3158
|
import { parse } from "smol-toml";
|
|
2689
3159
|
async function discoverFixtures(templatePath) {
|
|
2690
3160
|
const testsDir = join8(templatePath, "_tests");
|
|
@@ -2706,7 +3176,7 @@ async function discoverFixtures(templatePath) {
|
|
|
2706
3176
|
async function parseFixture(filePath) {
|
|
2707
3177
|
const content = await readTextFile(filePath);
|
|
2708
3178
|
const parsed = parse(content);
|
|
2709
|
-
const name =
|
|
3179
|
+
const name = basename3(filePath, extname(filePath));
|
|
2710
3180
|
const features = toStringArray2(parsed.features) ?? [];
|
|
2711
3181
|
const preset = toStringArray2(parsed.preset) ?? [];
|
|
2712
3182
|
const removeFeatures = toStringArray2(parsed["remove-features"]) ?? [];
|
|
@@ -2729,7 +3199,11 @@ function toStringArray2(value) {
|
|
|
2729
3199
|
|
|
2730
3200
|
// src/core/template-test/reporter.ts
|
|
2731
3201
|
import chalk4 from "chalk";
|
|
2732
|
-
function report2(result) {
|
|
3202
|
+
function report2(result, options) {
|
|
3203
|
+
if (options?.json) {
|
|
3204
|
+
reportJson2(result);
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
2733
3207
|
console.log("");
|
|
2734
3208
|
for (const fixture of result.results) {
|
|
2735
3209
|
reportFixture(fixture);
|
|
@@ -2743,6 +3217,27 @@ function report2(result) {
|
|
|
2743
3217
|
}
|
|
2744
3218
|
console.log("");
|
|
2745
3219
|
}
|
|
3220
|
+
function reportJson2(result) {
|
|
3221
|
+
const output = {
|
|
3222
|
+
total: result.total,
|
|
3223
|
+
passed: result.passed,
|
|
3224
|
+
failed: result.failed,
|
|
3225
|
+
results: result.results.map((r) => ({
|
|
3226
|
+
name: r.name,
|
|
3227
|
+
passed: r.passed,
|
|
3228
|
+
...r.error ? { error: r.error } : {},
|
|
3229
|
+
fileResults: r.fileResults.map((f) => ({
|
|
3230
|
+
path: f.path,
|
|
3231
|
+
found: f.found
|
|
3232
|
+
})),
|
|
3233
|
+
snapshotResults: r.snapshotResults.map((s) => ({
|
|
3234
|
+
path: s.path,
|
|
3235
|
+
status: s.status
|
|
3236
|
+
}))
|
|
3237
|
+
}))
|
|
3238
|
+
};
|
|
3239
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3240
|
+
}
|
|
2746
3241
|
function reportFixture(fixture) {
|
|
2747
3242
|
const icon = fixture.passed ? chalk4.green("\u2714") : chalk4.red("\u2716");
|
|
2748
3243
|
console.log(`${icon} ${fixture.name}`);
|
|
@@ -2905,31 +3400,61 @@ async function runAll(fixtures, templatePath, options, presetDefinitions = {}) {
|
|
|
2905
3400
|
|
|
2906
3401
|
// src/commands/test.ts
|
|
2907
3402
|
async function testCommand(options) {
|
|
3403
|
+
let mergedDir = null;
|
|
2908
3404
|
try {
|
|
2909
|
-
|
|
3405
|
+
const isJson = options.json === true;
|
|
3406
|
+
const isSummary = options.summary === true;
|
|
3407
|
+
const silent = isJson || isSummary;
|
|
3408
|
+
if (!silent) {
|
|
3409
|
+
intro4("awa CLI - Template Test");
|
|
3410
|
+
}
|
|
2910
3411
|
const fileConfig = await configLoader.load(options.config ?? null);
|
|
2911
3412
|
const templateSource = options.template ?? fileConfig?.template ?? null;
|
|
2912
|
-
const
|
|
2913
|
-
const
|
|
3413
|
+
const refresh = options.refresh ?? false;
|
|
3414
|
+
const template2 = await templateResolver.resolve(templateSource, refresh);
|
|
3415
|
+
const overlays = options.overlay ?? fileConfig?.overlay ?? [];
|
|
3416
|
+
let templatePath = template2.localPath;
|
|
3417
|
+
if (overlays.length > 0) {
|
|
3418
|
+
const overlayDirs = await resolveOverlays(overlays, refresh);
|
|
3419
|
+
mergedDir = await buildMergedDir(template2.localPath, overlayDirs);
|
|
3420
|
+
templatePath = mergedDir;
|
|
3421
|
+
}
|
|
3422
|
+
const fixtures = await discoverFixtures(templatePath);
|
|
2914
3423
|
if (fixtures.length === 0) {
|
|
2915
|
-
|
|
2916
|
-
|
|
3424
|
+
if (isSummary) {
|
|
3425
|
+
console.log("passed: 0, failed: 0, total: 0");
|
|
3426
|
+
} else if (isJson) {
|
|
3427
|
+
console.log(JSON.stringify({ total: 0, passed: 0, failed: 0, results: [] }, null, 2));
|
|
3428
|
+
} else {
|
|
3429
|
+
logger.warn("No test fixtures found in _tests/ directory");
|
|
3430
|
+
outro4("No tests to run.");
|
|
3431
|
+
}
|
|
2917
3432
|
return 0;
|
|
2918
3433
|
}
|
|
2919
|
-
|
|
3434
|
+
if (!silent) {
|
|
3435
|
+
logger.info(`Found ${fixtures.length} fixture(s)`);
|
|
3436
|
+
}
|
|
2920
3437
|
const presetDefinitions = fileConfig?.presets ?? {};
|
|
2921
3438
|
const result = await runAll(
|
|
2922
3439
|
fixtures,
|
|
2923
|
-
|
|
3440
|
+
templatePath,
|
|
2924
3441
|
{ updateSnapshots: options.updateSnapshots },
|
|
2925
3442
|
presetDefinitions
|
|
2926
3443
|
);
|
|
2927
|
-
|
|
3444
|
+
if (isSummary) {
|
|
3445
|
+
console.log(`passed: ${result.passed}, failed: ${result.failed}, total: ${result.total}`);
|
|
3446
|
+
} else {
|
|
3447
|
+
report2(result, { json: isJson });
|
|
3448
|
+
}
|
|
2928
3449
|
if (result.failed > 0) {
|
|
2929
|
-
|
|
3450
|
+
if (!silent) {
|
|
3451
|
+
outro4(`${result.failed} fixture(s) failed.`);
|
|
3452
|
+
}
|
|
2930
3453
|
return 1;
|
|
2931
3454
|
}
|
|
2932
|
-
|
|
3455
|
+
if (!silent) {
|
|
3456
|
+
outro4("All tests passed!");
|
|
3457
|
+
}
|
|
2933
3458
|
return 0;
|
|
2934
3459
|
} catch (error) {
|
|
2935
3460
|
if (error instanceof Error) {
|
|
@@ -2938,11 +3463,15 @@ async function testCommand(options) {
|
|
|
2938
3463
|
logger.error(String(error));
|
|
2939
3464
|
}
|
|
2940
3465
|
return 2;
|
|
3466
|
+
} finally {
|
|
3467
|
+
if (mergedDir) {
|
|
3468
|
+
await rmDir(mergedDir);
|
|
3469
|
+
}
|
|
2941
3470
|
}
|
|
2942
3471
|
}
|
|
2943
3472
|
|
|
2944
3473
|
// src/core/trace/content-assembler.ts
|
|
2945
|
-
import { readFile as
|
|
3474
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2946
3475
|
var DEFAULT_BEFORE_CONTEXT = 5;
|
|
2947
3476
|
var DEFAULT_AFTER_CONTEXT = 20;
|
|
2948
3477
|
async function assembleContent(result, taskPath, contextOptions) {
|
|
@@ -3141,7 +3670,7 @@ function findEnclosingBlock(lines, lineIdx, beforeContext = DEFAULT_BEFORE_CONTE
|
|
|
3141
3670
|
}
|
|
3142
3671
|
async function safeReadFile(filePath) {
|
|
3143
3672
|
try {
|
|
3144
|
-
return await
|
|
3673
|
+
return await readFile7(filePath, "utf-8");
|
|
3145
3674
|
} catch {
|
|
3146
3675
|
return null;
|
|
3147
3676
|
}
|
|
@@ -3493,7 +4022,7 @@ function pushToMap(map, key, value) {
|
|
|
3493
4022
|
}
|
|
3494
4023
|
|
|
3495
4024
|
// src/core/trace/input-resolver.ts
|
|
3496
|
-
import { readFile as
|
|
4025
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3497
4026
|
function resolveIds(ids, index) {
|
|
3498
4027
|
const resolved = [];
|
|
3499
4028
|
const warnings = [];
|
|
@@ -3509,7 +4038,7 @@ function resolveIds(ids, index) {
|
|
|
3509
4038
|
async function resolveTaskFile(taskPath, index) {
|
|
3510
4039
|
let content;
|
|
3511
4040
|
try {
|
|
3512
|
-
content = await
|
|
4041
|
+
content = await readFile8(taskPath, "utf-8");
|
|
3513
4042
|
} catch {
|
|
3514
4043
|
return { ids: [], warnings: [`Task file not found: ${taskPath}`] };
|
|
3515
4044
|
}
|
|
@@ -3560,7 +4089,7 @@ async function resolveTaskFile(taskPath, index) {
|
|
|
3560
4089
|
async function resolveSourceFile(filePath, index) {
|
|
3561
4090
|
let content;
|
|
3562
4091
|
try {
|
|
3563
|
-
content = await
|
|
4092
|
+
content = await readFile8(filePath, "utf-8");
|
|
3564
4093
|
} catch {
|
|
3565
4094
|
return { ids: [], warnings: [`Source file not found: ${filePath}`] };
|
|
3566
4095
|
}
|
|
@@ -3861,6 +4390,7 @@ function buildScanConfig(fileConfig, overrides) {
|
|
|
3861
4390
|
schemaEnabled: false,
|
|
3862
4391
|
allowWarnings: true,
|
|
3863
4392
|
specOnly: false,
|
|
4393
|
+
fix: true,
|
|
3864
4394
|
...overrides
|
|
3865
4395
|
};
|
|
3866
4396
|
}
|
|
@@ -3964,6 +4494,13 @@ async function traceCommand(options) {
|
|
|
3964
4494
|
}
|
|
3965
4495
|
return 1;
|
|
3966
4496
|
}
|
|
4497
|
+
if (options.summary) {
|
|
4498
|
+
const chainCount = result.chains.length;
|
|
4499
|
+
const notFoundCount = result.notFound.length;
|
|
4500
|
+
process.stdout.write(`chains: ${chainCount}, not-found: ${notFoundCount}
|
|
4501
|
+
`);
|
|
4502
|
+
return result.notFound.length > 0 && result.chains.length === 0 ? 1 : 0;
|
|
4503
|
+
}
|
|
3967
4504
|
let output;
|
|
3968
4505
|
const isContentMode = options.content || options.maxTokens !== void 0;
|
|
3969
4506
|
const queryLabel = ids.join(", ");
|
|
@@ -4063,7 +4600,7 @@ function printUpdateWarning(log, result) {
|
|
|
4063
4600
|
}
|
|
4064
4601
|
|
|
4065
4602
|
// src/utils/update-check-cache.ts
|
|
4066
|
-
import { mkdir as mkdir3, readFile as
|
|
4603
|
+
import { mkdir as mkdir3, readFile as readFile9, writeFile as writeFile2 } from "fs/promises";
|
|
4067
4604
|
import { homedir } from "os";
|
|
4068
4605
|
import { dirname, join as join11 } from "path";
|
|
4069
4606
|
var CACHE_DIR = join11(homedir(), ".cache", "awa");
|
|
@@ -4071,7 +4608,7 @@ var CACHE_FILE = join11(CACHE_DIR, "update-check.json");
|
|
|
4071
4608
|
var DEFAULT_INTERVAL_MS = 864e5;
|
|
4072
4609
|
async function shouldCheck(intervalMs = DEFAULT_INTERVAL_MS) {
|
|
4073
4610
|
try {
|
|
4074
|
-
const raw = await
|
|
4611
|
+
const raw = await readFile9(CACHE_FILE, "utf-8");
|
|
4075
4612
|
const data = JSON.parse(raw);
|
|
4076
4613
|
if (typeof data.timestamp !== "number" || typeof data.latestVersion !== "string") {
|
|
4077
4614
|
return true;
|
|
@@ -4088,7 +4625,7 @@ async function writeCache(latestVersion) {
|
|
|
4088
4625
|
timestamp: Date.now(),
|
|
4089
4626
|
latestVersion
|
|
4090
4627
|
};
|
|
4091
|
-
await
|
|
4628
|
+
await writeFile2(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
4092
4629
|
} catch {
|
|
4093
4630
|
}
|
|
4094
4631
|
}
|
|
@@ -4108,7 +4645,7 @@ function configureGenerateCommand(cmd) {
|
|
|
4108
4645
|
"--delete",
|
|
4109
4646
|
"Enable deletion of files listed in the delete list (default: warn only)",
|
|
4110
4647
|
false
|
|
4111
|
-
).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--all", "Process all named targets from config", false).option("--target <name>", "Process a specific named target from config").option(
|
|
4648
|
+
).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--all-targets", "Process all named targets from config", false).option("--target <name>", "Process a specific named target from config").option(
|
|
4112
4649
|
"--overlay <path...>",
|
|
4113
4650
|
"Overlay directory paths applied over base template (repeatable)"
|
|
4114
4651
|
).option("--json", "Output results as JSON (implies --dry-run)", false).option("--summary", "Output compact one-line summary", false).action(async (output, options) => {
|
|
@@ -4123,7 +4660,8 @@ function configureGenerateCommand(cmd) {
|
|
|
4123
4660
|
delete: options.delete,
|
|
4124
4661
|
config: options.config,
|
|
4125
4662
|
refresh: options.refresh,
|
|
4126
|
-
all: options.
|
|
4663
|
+
all: options.allTargets,
|
|
4664
|
+
allTargets: options.allTargets,
|
|
4127
4665
|
target: options.target,
|
|
4128
4666
|
overlay: options.overlay || [],
|
|
4129
4667
|
json: options.json,
|
|
@@ -4137,7 +4675,7 @@ configureGenerateCommand(program.command("init"));
|
|
|
4137
4675
|
template.command("diff").description("Compare template output with existing target directory").argument("[target]", "Target directory to compare against (optional if specified in config)").option("-t, --template <source>", "Template source (local path or Git repository)").option("-f, --features <flag...>", "Feature flags (can be specified multiple times)").option("--preset <name...>", "Preset names to enable (can be specified multiple times)").option(
|
|
4138
4676
|
"--remove-features <flag...>",
|
|
4139
4677
|
"Feature flags to remove (can be specified multiple times)"
|
|
4140
|
-
).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--list-unknown", "Include target-only files in diff results", false).option("--all", "Process all named targets from config", false).option("--target <name>", "Process a specific named target from config").option("-w, --watch", "Watch template directory for changes and re-run diff", false).option("--overlay <path...>", "Overlay directory paths applied over base template (repeatable)").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).action(async (target, options) => {
|
|
4678
|
+
).option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--list-unknown", "Include target-only files in diff results", false).option("--all-targets", "Process all named targets from config", false).option("--target <name>", "Process a specific named target from config").option("-w, --watch", "Watch template directory for changes and re-run diff", false).option("--overlay <path...>", "Overlay directory paths applied over base template (repeatable)").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).action(async (target, options) => {
|
|
4141
4679
|
const cliOptions = {
|
|
4142
4680
|
output: target,
|
|
4143
4681
|
// Use target as output for consistency
|
|
@@ -4148,7 +4686,8 @@ template.command("diff").description("Compare template output with existing targ
|
|
|
4148
4686
|
config: options.config,
|
|
4149
4687
|
refresh: options.refresh,
|
|
4150
4688
|
listUnknown: options.listUnknown,
|
|
4151
|
-
all: options.
|
|
4689
|
+
all: options.allTargets,
|
|
4690
|
+
allTargets: options.allTargets,
|
|
4152
4691
|
target: options.target,
|
|
4153
4692
|
watch: options.watch,
|
|
4154
4693
|
overlay: options.overlay || [],
|
|
@@ -4160,7 +4699,9 @@ template.command("diff").description("Compare template output with existing targ
|
|
|
4160
4699
|
});
|
|
4161
4700
|
program.command("check").description(
|
|
4162
4701
|
"Validate spec files against schemas and check traceability between code markers and specs"
|
|
4163
|
-
).option("-c, --config <path>", "Path to configuration file").option("--spec-ignore <pattern...>", "Glob patterns to exclude from spec file scanning").option("--code-ignore <pattern...>", "Glob patterns to exclude from code file scanning").option("--
|
|
4702
|
+
).option("-c, --config <path>", "Path to configuration file").option("--spec-ignore <pattern...>", "Glob patterns to exclude from spec file scanning").option("--code-ignore <pattern...>", "Glob patterns to exclude from code file scanning").option("--json", "Output results as JSON", false).addOption(
|
|
4703
|
+
new Option("--format <format>", "Output format (text or json)").default("text").hideHelp()
|
|
4704
|
+
).option("--summary", "Output compact one-line summary", false).option(
|
|
4164
4705
|
"--allow-warnings",
|
|
4165
4706
|
"Allow warnings without failing (default: warnings are errors)",
|
|
4166
4707
|
false
|
|
@@ -4168,38 +4709,50 @@ program.command("check").description(
|
|
|
4168
4709
|
"--spec-only",
|
|
4169
4710
|
"Run only spec-level checks (schema and cross-refs); skip code-to-spec traceability",
|
|
4170
4711
|
false
|
|
4712
|
+
).option(
|
|
4713
|
+
"--no-fix",
|
|
4714
|
+
"Skip regeneration of Requirements Traceability sections in DESIGN and TASK files"
|
|
4171
4715
|
).action(async (options) => {
|
|
4172
4716
|
const cliOptions = {
|
|
4173
4717
|
config: options.config,
|
|
4174
4718
|
specIgnore: options.specIgnore,
|
|
4175
4719
|
codeIgnore: options.codeIgnore,
|
|
4176
4720
|
format: options.format,
|
|
4721
|
+
json: options.json,
|
|
4722
|
+
summary: options.summary,
|
|
4177
4723
|
allowWarnings: options.allowWarnings,
|
|
4178
|
-
specOnly: options.specOnly
|
|
4724
|
+
specOnly: options.specOnly,
|
|
4725
|
+
fix: options.fix
|
|
4179
4726
|
};
|
|
4180
4727
|
const exitCode = await checkCommand(cliOptions);
|
|
4181
4728
|
process.exit(exitCode);
|
|
4182
4729
|
});
|
|
4183
|
-
template.command("features").description("Discover feature flags available in a template").option("-t, --template <source>", "Template source (local path or Git repository)").option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--json", "Output results as JSON", false).action(async (options) => {
|
|
4730
|
+
template.command("features").description("Discover feature flags available in a template").option("-t, --template <source>", "Template source (local path or Git repository)").option("-c, --config <path>", "Path to configuration file").option("--refresh", "Force refresh of cached Git templates", false).option("--overlay <path...>", "Overlay directory paths applied over base template (repeatable)").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).action(async (options) => {
|
|
4184
4731
|
const exitCode = await featuresCommand({
|
|
4185
4732
|
template: options.template,
|
|
4186
4733
|
config: options.config,
|
|
4187
4734
|
refresh: options.refresh,
|
|
4188
|
-
json: options.json
|
|
4735
|
+
json: options.json,
|
|
4736
|
+
summary: options.summary,
|
|
4737
|
+
overlay: options.overlay || []
|
|
4189
4738
|
});
|
|
4190
4739
|
process.exit(exitCode);
|
|
4191
4740
|
});
|
|
4192
|
-
template.command("test").description("Run template test fixtures to verify expected output").option("-t, --template <source>", "Template source (local path or Git repository)").option("-c, --config <path>", "Path to configuration file").option("--update-snapshots", "Update stored snapshots with current rendered output", false).action(async (options) => {
|
|
4741
|
+
template.command("test").description("Run template test fixtures to verify expected output").option("-t, --template <source>", "Template source (local path or Git repository)").option("-c, --config <path>", "Path to configuration file").option("--update-snapshots", "Update stored snapshots with current rendered output", false).option("--refresh", "Force refresh of cached Git templates", false).option("--overlay <path...>", "Overlay directory paths applied over base template (repeatable)").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).action(async (options) => {
|
|
4193
4742
|
const testOptions = {
|
|
4194
4743
|
template: options.template,
|
|
4195
4744
|
config: options.config,
|
|
4196
|
-
updateSnapshots: options.updateSnapshots
|
|
4745
|
+
updateSnapshots: options.updateSnapshots,
|
|
4746
|
+
refresh: options.refresh,
|
|
4747
|
+
json: options.json,
|
|
4748
|
+
summary: options.summary,
|
|
4749
|
+
overlay: options.overlay || []
|
|
4197
4750
|
};
|
|
4198
4751
|
const exitCode = await testCommand(testOptions);
|
|
4199
4752
|
process.exit(exitCode);
|
|
4200
4753
|
});
|
|
4201
4754
|
program.addCommand(template);
|
|
4202
|
-
program.command("trace").description("Explore traceability chains and assemble context from specs, code, and tests").argument("[ids...]", "Traceability ID(s) to trace").option("--all", "Trace all known IDs in the project", false).option("--task <path>", "Resolve IDs from a task file").option("--file <path>", "Resolve IDs from a source file's markers").option("--content", "Output actual file sections instead of locations", false).option("--list", "Output file paths only (no content or tree)", false).option("--max-tokens <n>", "Cap content output size (implies --content)").option("--depth <n>", "Maximum traversal depth").option("--scope <code>", "Limit results to a feature code").option("--direction <dir>", "Traversal direction: both, forward, reverse", "both").option("--no-code", "Exclude source code (spec-only context)").option("--no-tests", "Exclude test files").option("--json", "Output as JSON", 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) => {
|
|
4755
|
+
program.command("trace").description("Explore traceability chains and assemble context from specs, code, and tests").argument("[ids...]", "Traceability ID(s) to trace").option("--all", "Trace all known IDs in the project", false).option("--task <path>", "Resolve IDs from a task file").option("--file <path>", "Resolve IDs from a source file's markers").option("--content", "Output actual file sections instead of locations", false).option("--list", "Output file paths only (no content or tree)", false).option("--max-tokens <n>", "Cap content output size (implies --content)").option("--depth <n>", "Maximum traversal depth").option("--scope <code>", "Limit results to a feature code").option("--direction <dir>", "Traversal direction: both, forward, reverse", "both").option("--no-code", "Exclude source code (spec-only context)").option("--no-tests", "Exclude test files").option("--json", "Output results as JSON", false).option("--summary", "Output compact one-line summary", false).option("-A <n>", "Lines of context after a code marker (--content only; default: 20)").option("-B <n>", "Lines of context before a code marker (--content only; default: 5)").option("-C <n>", "Lines of context before and after (--content only; overrides -A and -B)").option("-c, --config <path>", "Path to configuration file").action(async (ids, options) => {
|
|
4203
4756
|
const traceOptions = {
|
|
4204
4757
|
ids,
|
|
4205
4758
|
all: options.all,
|
|
@@ -4208,6 +4761,7 @@ program.command("trace").description("Explore traceability chains and assemble c
|
|
|
4208
4761
|
content: options.content,
|
|
4209
4762
|
list: options.list,
|
|
4210
4763
|
json: options.json,
|
|
4764
|
+
summary: options.summary,
|
|
4211
4765
|
maxTokens: options.maxTokens !== void 0 ? Number(options.maxTokens) : void 0,
|
|
4212
4766
|
depth: options.depth !== void 0 ? Number(options.depth) : void 0,
|
|
4213
4767
|
scope: options.scope,
|
|
@@ -4228,7 +4782,7 @@ var isDisabledByEnv = !!process.env.NO_UPDATE_NOTIFIER;
|
|
|
4228
4782
|
if (!isJsonOrSummary && isTTY && !isDisabledByEnv) {
|
|
4229
4783
|
updateCheckPromise = (async () => {
|
|
4230
4784
|
try {
|
|
4231
|
-
const { configLoader: configLoader2 } = await import("./config-
|
|
4785
|
+
const { configLoader: configLoader2 } = await import("./config-WL3SLSP6.js");
|
|
4232
4786
|
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;
|
|
4233
4787
|
const fileConfig = await configLoader2.load(configPath ?? null);
|
|
4234
4788
|
const updateCheckConfig = fileConfig?.["update-check"];
|