@aisdlc/core 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1463 -981
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -6
- package/dist/index.d.ts +62 -6
- package/dist/index.js +1460 -979
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
1
|
+
import * as fs7 from 'fs';
|
|
2
|
+
import * as path4 from 'path';
|
|
3
|
+
import yaml3 from 'js-yaml';
|
|
4
4
|
import { SyntaxKind, Project, Node } from 'ts-morph';
|
|
5
5
|
import * as crypto from 'crypto';
|
|
6
6
|
import * as child_process from 'child_process';
|
|
@@ -170,7 +170,7 @@ function buildNodeMetrics(node, filePath) {
|
|
|
170
170
|
return { functionName, filePath, loc, cyclomatic, cognitive, maintainability: mi, codeSmells };
|
|
171
171
|
}
|
|
172
172
|
function createTempSourceFile(project, content, filePath) {
|
|
173
|
-
const ext =
|
|
173
|
+
const ext = path4.extname(filePath).toLowerCase() || ".ts";
|
|
174
174
|
const fileName = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}${ext}`;
|
|
175
175
|
return project.createSourceFile(fileName, content, { overwrite: true });
|
|
176
176
|
}
|
|
@@ -207,7 +207,7 @@ function extractFunctionsTypeScriptAst(content, filePath) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
function extractFunctionsPolyglot(content, filePath) {
|
|
210
|
-
const ext =
|
|
210
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
211
211
|
return ext === ".py" ? extractPythonFunctions(content, filePath) : extractBraceFunctions(content, filePath);
|
|
212
212
|
}
|
|
213
213
|
function isDocstringToggle(trimmed) {
|
|
@@ -284,7 +284,7 @@ function processBraceLine(line, state, blocks, lineIndex, ext) {
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
function extractBraceFunctions(content, filePath) {
|
|
287
|
-
const ext =
|
|
287
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
288
288
|
const lines = content.split("\n");
|
|
289
289
|
const blocks = [];
|
|
290
290
|
const state = { currentFn: null, braceCount: 0, fnLines: [], inBlockComment: false };
|
|
@@ -429,7 +429,7 @@ function countCommentsAndLoc(rawLines, isPy) {
|
|
|
429
429
|
return { loc, commentCount, cleanLines };
|
|
430
430
|
}
|
|
431
431
|
function calculatePolyglotMetrics(body, name, filePath) {
|
|
432
|
-
const ext =
|
|
432
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
433
433
|
const rawLines = body.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
434
434
|
const { loc, commentCount, cleanLines } = countCommentsAndLoc(rawLines, ext === ".py");
|
|
435
435
|
const cyclomatic = computePolyglotCyclomatic(body, ext);
|
|
@@ -479,6 +479,652 @@ function computePolyglotCognitive(lines, isPy) {
|
|
|
479
479
|
}
|
|
480
480
|
return cognitive;
|
|
481
481
|
}
|
|
482
|
+
function walkMdFiles(dir, fileList = [], excludeDirs = [
|
|
483
|
+
"node_modules",
|
|
484
|
+
".git",
|
|
485
|
+
"dist",
|
|
486
|
+
"bin",
|
|
487
|
+
"obj",
|
|
488
|
+
"build",
|
|
489
|
+
"out",
|
|
490
|
+
"target",
|
|
491
|
+
"coverage",
|
|
492
|
+
".changeset",
|
|
493
|
+
"scratch",
|
|
494
|
+
"test-scaffold",
|
|
495
|
+
"fixtures",
|
|
496
|
+
"templates",
|
|
497
|
+
"examples",
|
|
498
|
+
".agent",
|
|
499
|
+
".agents",
|
|
500
|
+
".claude",
|
|
501
|
+
".cursor",
|
|
502
|
+
".github",
|
|
503
|
+
".vscode",
|
|
504
|
+
".vs",
|
|
505
|
+
".idea"
|
|
506
|
+
]) {
|
|
507
|
+
try {
|
|
508
|
+
if (!fs7.existsSync(dir)) return fileList;
|
|
509
|
+
const files = fs7.readdirSync(dir);
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
const fullPath = path4.join(dir, file);
|
|
512
|
+
try {
|
|
513
|
+
if (fs7.statSync(fullPath).isDirectory()) {
|
|
514
|
+
if (!excludeDirs.includes(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
515
|
+
walkMdFiles(fullPath, fileList, excludeDirs);
|
|
516
|
+
}
|
|
517
|
+
} else if (file.endsWith(".md")) {
|
|
518
|
+
fileList.push(fullPath);
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
return fileList;
|
|
526
|
+
}
|
|
527
|
+
function parseMarkdownFrontmatter(content) {
|
|
528
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
529
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
530
|
+
const yamlLines = match[1].split("\n");
|
|
531
|
+
const frontmatter = {};
|
|
532
|
+
for (const line of yamlLines) {
|
|
533
|
+
const parts = line.split(":");
|
|
534
|
+
if (parts.length >= 2) {
|
|
535
|
+
const key = parts[0].trim();
|
|
536
|
+
let val = parts.slice(1).join(":").trim();
|
|
537
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
538
|
+
val = val.substring(1, val.length - 1);
|
|
539
|
+
}
|
|
540
|
+
frontmatter[key] = val;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const body = content.substring(match[0].length);
|
|
544
|
+
return { frontmatter, body };
|
|
545
|
+
}
|
|
546
|
+
function extractGherkinBlock(body) {
|
|
547
|
+
const gherkinRegex = /```gherkin\r?\n([\s\S]*?)\r?\n```/g;
|
|
548
|
+
const blocks = [];
|
|
549
|
+
let match;
|
|
550
|
+
while ((match = gherkinRegex.exec(body)) !== null) {
|
|
551
|
+
blocks.push(match[1].trim());
|
|
552
|
+
}
|
|
553
|
+
return blocks;
|
|
554
|
+
}
|
|
555
|
+
function resolveTargetFiles(rootDir, targetPath) {
|
|
556
|
+
if (targetPath) {
|
|
557
|
+
const fullTarget = path4.isAbsolute(targetPath) ? targetPath : path4.join(rootDir, targetPath);
|
|
558
|
+
if (!fs7.existsSync(fullTarget)) return [];
|
|
559
|
+
return fs7.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
560
|
+
}
|
|
561
|
+
const searchDirs = [
|
|
562
|
+
path4.join(rootDir, "product"),
|
|
563
|
+
path4.join(rootDir, "security"),
|
|
564
|
+
path4.join(rootDir, "specs"),
|
|
565
|
+
path4.join(rootDir, "examples", "product"),
|
|
566
|
+
path4.join(rootDir, "examples", "security"),
|
|
567
|
+
path4.join(rootDir, "examples", "specs")
|
|
568
|
+
];
|
|
569
|
+
return searchDirs.filter((d) => fs7.existsSync(d)).flatMap((d) => walkMdFiles(d));
|
|
570
|
+
}
|
|
571
|
+
function resolveFeatureFilePath(rootDir, filePath, id, customTarget) {
|
|
572
|
+
if (customTarget) {
|
|
573
|
+
return path4.isAbsolute(customTarget) ? customTarget : path4.join(rootDir, customTarget);
|
|
574
|
+
}
|
|
575
|
+
const baseDir = path4.join(rootDir, "tests", "features");
|
|
576
|
+
const candidateSecurity = path4.join(baseDir, "security", `${id.toLowerCase()}.feature`);
|
|
577
|
+
const candidateRoot = path4.join(baseDir, `${id.toLowerCase()}.feature`);
|
|
578
|
+
if (fs7.existsSync(candidateSecurity)) return candidateSecurity;
|
|
579
|
+
if (fs7.existsSync(candidateRoot)) return candidateRoot;
|
|
580
|
+
const subDir = filePath.includes("security") ? "security" : "";
|
|
581
|
+
return path4.join(baseDir, subDir, `${id.toLowerCase()}.feature`);
|
|
582
|
+
}
|
|
583
|
+
function buildFeatureFileContent(rootDir, filePath, id, version, gherkinBlocks) {
|
|
584
|
+
const banner = [
|
|
585
|
+
`# ==============================================================================`,
|
|
586
|
+
`# AUTO-GENERADO POR AI-SDLC (Cucumber Integration)`,
|
|
587
|
+
`# Origen: ${path4.relative(rootDir, filePath).replace(/\\/g, "/")}`,
|
|
588
|
+
`# ID Requerimiento: ${id}`,
|
|
589
|
+
`# Versi\xF3n: ${version || "1.0.0"}`,
|
|
590
|
+
`# NO EDITAR MANUALMENTE: Cualquier cambio debe realizarse en el Markdown origen.`,
|
|
591
|
+
`# ==============================================================================`,
|
|
592
|
+
"",
|
|
593
|
+
""
|
|
594
|
+
].join("\n");
|
|
595
|
+
return banner + gherkinBlocks.join("\n\n") + "\n";
|
|
596
|
+
}
|
|
597
|
+
function countScenarios(gherkinBlocks) {
|
|
598
|
+
let count = 0;
|
|
599
|
+
for (const block of gherkinBlocks) {
|
|
600
|
+
const matches = block.match(/\b(Scenario|Escenario|Scenario Outline|Esquema del escenario):/g);
|
|
601
|
+
if (matches) count += matches.length;
|
|
602
|
+
}
|
|
603
|
+
return count;
|
|
604
|
+
}
|
|
605
|
+
function extractGherkinFeatures(options = {}) {
|
|
606
|
+
const rootDir = options.rootDir || process.cwd();
|
|
607
|
+
const features = [];
|
|
608
|
+
let totalScenarios = 0;
|
|
609
|
+
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
610
|
+
for (const filePath of targetFiles) {
|
|
611
|
+
try {
|
|
612
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
613
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
614
|
+
const gherkinBlocks = extractGherkinBlock(body);
|
|
615
|
+
if (gherkinBlocks.length === 0) continue;
|
|
616
|
+
const id = frontmatter.id || path4.basename(filePath, path4.extname(filePath));
|
|
617
|
+
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
618
|
+
fs7.mkdirSync(path4.dirname(targetPath), { recursive: true });
|
|
619
|
+
const fullContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
620
|
+
fs7.writeFileSync(targetPath, fullContent, "utf-8");
|
|
621
|
+
const scenarioCount = countScenarios(gherkinBlocks);
|
|
622
|
+
totalScenarios += scenarioCount;
|
|
623
|
+
features.push({
|
|
624
|
+
sourceFile: path4.relative(rootDir, filePath).replace(/\\/g, "/"),
|
|
625
|
+
outputFile: path4.relative(rootDir, targetPath).replace(/\\/g, "/"),
|
|
626
|
+
featureName: id,
|
|
627
|
+
scenarioCount
|
|
628
|
+
});
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return { success: true, features, totalScenarios };
|
|
633
|
+
}
|
|
634
|
+
function checkGherkinInSync(options = {}) {
|
|
635
|
+
const rootDir = options.rootDir || process.cwd();
|
|
636
|
+
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
637
|
+
const outOfSyncFiles = [];
|
|
638
|
+
const missingFiles = [];
|
|
639
|
+
let totalFeatures = 0;
|
|
640
|
+
for (const filePath of targetFiles) {
|
|
641
|
+
try {
|
|
642
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
643
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
644
|
+
const gherkinBlocks = extractGherkinBlock(body);
|
|
645
|
+
if (gherkinBlocks.length === 0) continue;
|
|
646
|
+
totalFeatures++;
|
|
647
|
+
const id = frontmatter.id || path4.basename(filePath, path4.extname(filePath));
|
|
648
|
+
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
649
|
+
const relTarget = path4.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
650
|
+
if (!fs7.existsSync(targetPath)) {
|
|
651
|
+
missingFiles.push(relTarget);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const expectedContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
655
|
+
const actualContent = fs7.readFileSync(targetPath, "utf-8");
|
|
656
|
+
if (actualContent.replace(/\r\n/g, "\n") !== expectedContent.replace(/\r\n/g, "\n")) {
|
|
657
|
+
outOfSyncFiles.push(relTarget);
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
inSync: missingFiles.length === 0 && outOfSyncFiles.length === 0,
|
|
664
|
+
outOfSyncFiles,
|
|
665
|
+
missingFiles,
|
|
666
|
+
totalFeatures
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/verifiers/testing-coverage.ts
|
|
671
|
+
function parseTaskVerification(v) {
|
|
672
|
+
const verification = {};
|
|
673
|
+
if (v && typeof v === "object") {
|
|
674
|
+
const rec = v;
|
|
675
|
+
if (typeof rec["method"] === "string") verification.method = rec["method"];
|
|
676
|
+
if (typeof rec["command-or-criteria"] === "string") verification.criteria = rec["command-or-criteria"];
|
|
677
|
+
}
|
|
678
|
+
return verification;
|
|
679
|
+
}
|
|
680
|
+
function parseTaskItem(t) {
|
|
681
|
+
return {
|
|
682
|
+
id: typeof t["id"] === "string" ? t["id"] : String(t["id"] ?? ""),
|
|
683
|
+
title: typeof t["title"] === "string" ? t["title"] : void 0,
|
|
684
|
+
verification: parseTaskVerification(t["verification"])
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function parseVerifiedByTests(verifiedBy) {
|
|
688
|
+
if (Array.isArray(verifiedBy)) {
|
|
689
|
+
return verifiedBy.map(String);
|
|
690
|
+
}
|
|
691
|
+
if (typeof verifiedBy === "string") {
|
|
692
|
+
return [verifiedBy];
|
|
693
|
+
}
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
function parseGenericDoc(content) {
|
|
697
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
698
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
699
|
+
let parsed;
|
|
700
|
+
try {
|
|
701
|
+
parsed = yaml3.load(match[1]);
|
|
702
|
+
} catch {
|
|
703
|
+
return { frontmatter: {}, body: content };
|
|
704
|
+
}
|
|
705
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
706
|
+
return { frontmatter: {}, body: content };
|
|
707
|
+
}
|
|
708
|
+
const raw = parsed;
|
|
709
|
+
const rawTasks = Array.isArray(raw["tasks"]) ? raw["tasks"] : [];
|
|
710
|
+
const tasks = rawTasks.map(parseTaskItem);
|
|
711
|
+
const frontmatter = {
|
|
712
|
+
...raw,
|
|
713
|
+
tasks,
|
|
714
|
+
"verified-by-tests": parseVerifiedByTests(raw["verified-by-tests"])
|
|
715
|
+
};
|
|
716
|
+
return { frontmatter, body: content.substring(match[0].length) };
|
|
717
|
+
}
|
|
718
|
+
function buildRequirementsTable(reqAudit) {
|
|
719
|
+
const lines = [
|
|
720
|
+
`## 1. Requirements Audit (Product, Quality, and Security)`,
|
|
721
|
+
``,
|
|
722
|
+
`| Requirement ID | Title | Source File | Declared Method | Linked Test Files | Status |`,
|
|
723
|
+
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
724
|
+
];
|
|
725
|
+
for (const r of reqAudit) {
|
|
726
|
+
const icon = r.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
727
|
+
lines.push(
|
|
728
|
+
`| **\`${r.id}\`** | ${r.title} | \`${r.file}\` | \`${r.method}\` | \`${r.testRefs}\` | ${icon} |`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
return lines;
|
|
732
|
+
}
|
|
733
|
+
function buildTasksTable(taskAudit) {
|
|
734
|
+
const lines = [
|
|
735
|
+
`## 2. Tasks Audit (SDD Delivery Engineering)`,
|
|
736
|
+
``,
|
|
737
|
+
`| Task ID | Title | tasks.md File | Method | Verification Command / Criterion | Status |`,
|
|
738
|
+
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
739
|
+
];
|
|
740
|
+
for (const t of taskAudit) {
|
|
741
|
+
const icon = t.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
742
|
+
lines.push(
|
|
743
|
+
`| **\`${t.id}\`** | ${t.title} | \`${t.file}\` | \`${t.method}\` | \`${t.commandOrCriteria}\` | ${icon} |`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
return lines;
|
|
747
|
+
}
|
|
748
|
+
function generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors) {
|
|
749
|
+
const totalErrors = reqErrors + taskErrors;
|
|
750
|
+
const verdict = totalErrors === 0 ? "100% VERIFIED WITH TESTS (PASSED)" : `BLOCKED: Detected ${totalErrors} items without tests (${reqErrors} Requirements, ${taskErrors} Tasks)`;
|
|
751
|
+
const reportLines = [
|
|
752
|
+
`# \u{1F9EA} Deterministic Test Coverage Audit for Requirements and Tasks`,
|
|
753
|
+
``,
|
|
754
|
+
`> **Audit Date:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
755
|
+
`> **Overall Verdict:** ${verdict}`,
|
|
756
|
+
``,
|
|
757
|
+
`---`,
|
|
758
|
+
``,
|
|
759
|
+
...buildRequirementsTable(reqAudit),
|
|
760
|
+
``,
|
|
761
|
+
`---`,
|
|
762
|
+
``,
|
|
763
|
+
...buildTasksTable(taskAudit)
|
|
764
|
+
];
|
|
765
|
+
return reportLines.join("\n");
|
|
766
|
+
}
|
|
767
|
+
var EXCLUDED_TEST_DIRS = /* @__PURE__ */ new Set([
|
|
768
|
+
"node_modules",
|
|
769
|
+
".git",
|
|
770
|
+
"dist",
|
|
771
|
+
"bin",
|
|
772
|
+
"obj",
|
|
773
|
+
"build",
|
|
774
|
+
"out",
|
|
775
|
+
"target",
|
|
776
|
+
"coverage",
|
|
777
|
+
".changeset",
|
|
778
|
+
"scratch",
|
|
779
|
+
"fixtures",
|
|
780
|
+
"templates",
|
|
781
|
+
"examples",
|
|
782
|
+
".vs",
|
|
783
|
+
".idea"
|
|
784
|
+
]);
|
|
785
|
+
var TEST_EXTENSIONS = [".feature", ".spec.ts", ".spec.js", ".test.ts", ".test.js", "_test.py", "_test.go"];
|
|
786
|
+
function isTestFile(fileName) {
|
|
787
|
+
return TEST_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
788
|
+
}
|
|
789
|
+
function recordRequirementTest(reqToTestsMap, reqId, relTest) {
|
|
790
|
+
const existing = reqToTestsMap.get(reqId) || [];
|
|
791
|
+
if (!existing.includes(relTest)) {
|
|
792
|
+
existing.push(relTest);
|
|
793
|
+
reqToTestsMap.set(reqId, existing);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function parseFeatureTags(content, relTest, reqToTestsMap) {
|
|
797
|
+
const lines = content.split("\n");
|
|
798
|
+
for (const line of lines) {
|
|
799
|
+
const trimmed = line.trim();
|
|
800
|
+
if (!trimmed.startsWith("@")) continue;
|
|
801
|
+
const tags = trimmed.split(/\s+/).filter((t) => t.startsWith("@"));
|
|
802
|
+
for (const t of tags) {
|
|
803
|
+
recordRequirementTest(reqToTestsMap, t.substring(1), relTest);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
var REQ_ID_REGEX = /\b(FR|QR|CON|SEC-REQ)-[A-Z0-9]+(-[A-Z0-9]+)*\b/g;
|
|
808
|
+
function parseCodeTestIds(content, relTest, reqToTestsMap) {
|
|
809
|
+
const idMatches = content.match(REQ_ID_REGEX);
|
|
810
|
+
if (!idMatches) return;
|
|
811
|
+
for (const reqId of idMatches) {
|
|
812
|
+
recordRequirementTest(reqToTestsMap, reqId, relTest);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function processTestFile(fullPath, rootDir, reqToTestsMap) {
|
|
816
|
+
try {
|
|
817
|
+
const content = fs7.readFileSync(fullPath, "utf-8");
|
|
818
|
+
const relTest = path4.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
819
|
+
if (fullPath.endsWith(".feature")) {
|
|
820
|
+
parseFeatureTags(content, relTest, reqToTestsMap);
|
|
821
|
+
}
|
|
822
|
+
parseCodeTestIds(content, relTest, reqToTestsMap);
|
|
823
|
+
} catch {
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function collectTestFiles(dir, files = []) {
|
|
827
|
+
if (!fs7.existsSync(dir)) return files;
|
|
828
|
+
try {
|
|
829
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
830
|
+
for (const e of entries) {
|
|
831
|
+
if (e.isDirectory()) {
|
|
832
|
+
if (!EXCLUDED_TEST_DIRS.has(e.name)) {
|
|
833
|
+
collectTestFiles(path4.join(dir, e.name), files);
|
|
834
|
+
}
|
|
835
|
+
} else if (isTestFile(e.name)) {
|
|
836
|
+
files.push(path4.join(dir, e.name));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
841
|
+
return files;
|
|
842
|
+
}
|
|
843
|
+
function scanAllTestArtifacts(rootDir) {
|
|
844
|
+
const reqToTestsMap = /* @__PURE__ */ new Map();
|
|
845
|
+
const testFiles = collectTestFiles(rootDir);
|
|
846
|
+
for (const file of testFiles) {
|
|
847
|
+
processTestFile(file, rootDir, reqToTestsMap);
|
|
848
|
+
}
|
|
849
|
+
return reqToTestsMap;
|
|
850
|
+
}
|
|
851
|
+
var NOMINAL_KEYWORDS = /\b(nominal|happy|valid|exitoso|normal|standard|flujo principal)\b/i;
|
|
852
|
+
var BOUNDARY_KEYWORDS = /\b(boundary|limit|edge|min|max|l[ií]mite|borde|extremo|rango)\b/i;
|
|
853
|
+
var INVALID_KEYWORDS = /\b(invalid|out[- ]of[- ]range|error|null|empty|reject|fail|unauthorized|exception|inv[aá]lido|fuera de rango|rechaz|excepci[oó]n|fallo)\b/i;
|
|
854
|
+
function extractScenariosForReq(gherkinText, reqId) {
|
|
855
|
+
const lines = gherkinText.split(/\r?\n/);
|
|
856
|
+
const featureApplies = gherkinText.includes(`@${reqId}`);
|
|
857
|
+
if (!featureApplies) return [];
|
|
858
|
+
const chunks = [];
|
|
859
|
+
let pendingTags = "";
|
|
860
|
+
for (const rawLine of lines) {
|
|
861
|
+
const line = rawLine.trim();
|
|
862
|
+
if (line.startsWith("@")) {
|
|
863
|
+
pendingTags += " " + line;
|
|
864
|
+
} else if (/^(Scenario|Escenario|Scenario Outline|Esquema del escenario):/i.test(line)) {
|
|
865
|
+
const isOutline = /^(Scenario Outline|Esquema del escenario):/i.test(line);
|
|
866
|
+
chunks.push({ tags: pendingTags.toLowerCase(), header: line, isOutline });
|
|
867
|
+
pendingTags = "";
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return chunks;
|
|
871
|
+
}
|
|
872
|
+
var INVALID_TAG_RE = /@(invalid|error|out-of-range)\b/i;
|
|
873
|
+
function isInvalidScenario(chunk, combined) {
|
|
874
|
+
return INVALID_TAG_RE.test(chunk.tags) || INVALID_KEYWORDS.test(combined);
|
|
875
|
+
}
|
|
876
|
+
function isBoundaryScenario(chunk, combined, isInvalid) {
|
|
877
|
+
if (chunk.tags.includes("@boundary") || BOUNDARY_KEYWORDS.test(combined)) return true;
|
|
878
|
+
return chunk.isOutline && !isInvalid;
|
|
879
|
+
}
|
|
880
|
+
function classifyScenarioChunk(chunk, cov) {
|
|
881
|
+
const combined = `${chunk.tags} ${chunk.header}`;
|
|
882
|
+
const isInvalid = isInvalidScenario(chunk, combined);
|
|
883
|
+
const isBoundary = isBoundaryScenario(chunk, combined, isInvalid);
|
|
884
|
+
const isNominal = chunk.tags.includes("@nominal") || NOMINAL_KEYWORDS.test(combined) || !isInvalid && !isBoundary;
|
|
885
|
+
if (isNominal) cov.nominal = true;
|
|
886
|
+
if (isBoundary) cov.boundary = true;
|
|
887
|
+
if (isInvalid) cov.invalid = true;
|
|
888
|
+
}
|
|
889
|
+
function analyzeBddVariantsForRequirement(reqId, gherkinSources) {
|
|
890
|
+
const cov = { nominal: false, boundary: false, invalid: false, totalScenarios: 0 };
|
|
891
|
+
for (const src of gherkinSources) {
|
|
892
|
+
const chunks = extractScenariosForReq(src, reqId);
|
|
893
|
+
cov.totalScenarios += chunks.length;
|
|
894
|
+
for (const chunk of chunks) {
|
|
895
|
+
classifyScenarioChunk(chunk, cov);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return cov;
|
|
899
|
+
}
|
|
900
|
+
function computeMissingVariants(cov) {
|
|
901
|
+
const missing = [];
|
|
902
|
+
if (!cov.nominal) missing.push("nominal");
|
|
903
|
+
if (!cov.boundary) missing.push("boundary");
|
|
904
|
+
if (!cov.invalid) missing.push("invalid");
|
|
905
|
+
return missing;
|
|
906
|
+
}
|
|
907
|
+
var REQ_PREFIXES = ["FR-", "QR-", "CON-", "SEC-REQ-"];
|
|
908
|
+
function isRequirementArtifact(frontmatter, id) {
|
|
909
|
+
if (frontmatter.type === "requirement" || frontmatter.type === "security-requirement") {
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
return REQ_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
913
|
+
}
|
|
914
|
+
function resolveRequirementMethod(declaredMethod, matchingTests) {
|
|
915
|
+
if (typeof declaredMethod === "string" && declaredMethod) {
|
|
916
|
+
return declaredMethod;
|
|
917
|
+
}
|
|
918
|
+
return matchingTests.some((t) => t.endsWith(".feature")) ? "BDD_CUCUMBER" : "TEST_EXECUTION";
|
|
919
|
+
}
|
|
920
|
+
function shouldEnforceVariantsForReq(id, frontmatter, options) {
|
|
921
|
+
if (!id.startsWith("FR-")) return false;
|
|
922
|
+
return Boolean(options.enforceBddVariants || frontmatter["enforce-bdd-variants"] === true);
|
|
923
|
+
}
|
|
924
|
+
function evaluateReqVerification(id, isTemplate, hasTests, missingVariants, frontmatter, options) {
|
|
925
|
+
if (isTemplate) return true;
|
|
926
|
+
const enforceVariants = shouldEnforceVariantsForReq(id, frontmatter, options);
|
|
927
|
+
const variantsOk = !enforceVariants || missingVariants.length === 0;
|
|
928
|
+
return hasTests && variantsOk;
|
|
929
|
+
}
|
|
930
|
+
function formatTestRefs(matchingTests, relPath, totalScenarios) {
|
|
931
|
+
if (matchingTests.length > 0) return matchingTests.join(", ");
|
|
932
|
+
return totalScenarios > 0 ? `${relPath} (gherkin)` : "NINGUNA DETECTADA";
|
|
933
|
+
}
|
|
934
|
+
function buildRequirementAuditItem(frontmatter, id, relPath, isTemplate, matchingTests, bddVariants, missingVariants, isVerified) {
|
|
935
|
+
const status = isVerified ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA";
|
|
936
|
+
return {
|
|
937
|
+
id: id || "SIN-ID",
|
|
938
|
+
title: frontmatter.title || "Sin t\xEDtulo",
|
|
939
|
+
file: relPath,
|
|
940
|
+
isTemplate,
|
|
941
|
+
method: resolveRequirementMethod(frontmatter["verifiable-by"], matchingTests),
|
|
942
|
+
testRefs: formatTestRefs(matchingTests, relPath, bddVariants.totalScenarios),
|
|
943
|
+
existingCount: matchingTests.length || bddVariants.totalScenarios,
|
|
944
|
+
bddVariants,
|
|
945
|
+
missingVariants,
|
|
946
|
+
status
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap, gherkinSources, options) {
|
|
950
|
+
const matchingTests = reqToTestsMap.get(id) || [];
|
|
951
|
+
const bddVariants = analyzeBddVariantsForRequirement(id, gherkinSources);
|
|
952
|
+
const missingVariants = id.startsWith("FR-") ? computeMissingVariants(bddVariants) : [];
|
|
953
|
+
const hasTests = matchingTests.length > 0 || bddVariants.totalScenarios > 0;
|
|
954
|
+
const isVerified = evaluateReqVerification(id, isTemplate, hasTests, missingVariants, frontmatter, options);
|
|
955
|
+
const item = buildRequirementAuditItem(
|
|
956
|
+
frontmatter,
|
|
957
|
+
id,
|
|
958
|
+
relPath,
|
|
959
|
+
isTemplate,
|
|
960
|
+
matchingTests,
|
|
961
|
+
bddVariants,
|
|
962
|
+
missingVariants,
|
|
963
|
+
isVerified
|
|
964
|
+
);
|
|
965
|
+
return { item, isError: !isVerified };
|
|
966
|
+
}
|
|
967
|
+
function parseSingleFlowEntry(entry) {
|
|
968
|
+
if (typeof entry === "string") return entry.trim();
|
|
969
|
+
if (entry && typeof entry === "object") {
|
|
970
|
+
const rec = entry;
|
|
971
|
+
return String(rec.id || rec.name || "").trim();
|
|
972
|
+
}
|
|
973
|
+
return "";
|
|
974
|
+
}
|
|
975
|
+
function extractDeclaredUseCaseFlows(frontmatter) {
|
|
976
|
+
const flows = [];
|
|
977
|
+
const rawLists = [frontmatter["alternative-flows"], frontmatter["exception-flows"]];
|
|
978
|
+
for (const list of rawLists) {
|
|
979
|
+
if (!Array.isArray(list)) continue;
|
|
980
|
+
for (const entry of list) {
|
|
981
|
+
const flowId = parseSingleFlowEntry(entry);
|
|
982
|
+
if (flowId) flows.push(flowId);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return flows;
|
|
986
|
+
}
|
|
987
|
+
function auditUseCaseFlowItem(id, relPath, declaredFlows, combinedTestsText) {
|
|
988
|
+
const coveredFlows = [];
|
|
989
|
+
const missingFlows = [];
|
|
990
|
+
const lowerCorpus = combinedTestsText.toLowerCase();
|
|
991
|
+
for (const flow of declaredFlows) {
|
|
992
|
+
if (lowerCorpus.includes(flow.toLowerCase())) {
|
|
993
|
+
coveredFlows.push(flow);
|
|
994
|
+
} else {
|
|
995
|
+
missingFlows.push(flow);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
id,
|
|
1000
|
+
file: relPath,
|
|
1001
|
+
declaredFlows,
|
|
1002
|
+
coveredFlows,
|
|
1003
|
+
missingFlows,
|
|
1004
|
+
status: missingFlows.length === 0 ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function isTaskProofValid(criteria, isTemplate) {
|
|
1008
|
+
if (isTemplate) return true;
|
|
1009
|
+
return criteria.length > 4 && !criteria.includes("TODO") && !criteria.includes("TBD");
|
|
1010
|
+
}
|
|
1011
|
+
function auditSingleTask(t, relPath, isTemplate) {
|
|
1012
|
+
const crit = t.verification?.criteria || "";
|
|
1013
|
+
const method = t.verification?.method || "TEST_EXECUTION";
|
|
1014
|
+
const hasProof = isTaskProofValid(crit, isTemplate);
|
|
1015
|
+
return {
|
|
1016
|
+
item: {
|
|
1017
|
+
id: t.id,
|
|
1018
|
+
title: t.title || "Sin t\xEDtulo",
|
|
1019
|
+
file: relPath,
|
|
1020
|
+
method,
|
|
1021
|
+
commandOrCriteria: crit || "AUSENTE",
|
|
1022
|
+
status: hasProof ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
1023
|
+
},
|
|
1024
|
+
hasProof
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function auditTasks(tasks, relPath, isTemplate) {
|
|
1028
|
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
1029
|
+
return { items: [], errorCount: 0 };
|
|
1030
|
+
}
|
|
1031
|
+
const items = [];
|
|
1032
|
+
let errorCount = 0;
|
|
1033
|
+
for (const t of tasks) {
|
|
1034
|
+
const { item, hasProof } = auditSingleTask(t, relPath, isTemplate);
|
|
1035
|
+
if (!hasProof) errorCount++;
|
|
1036
|
+
items.push(item);
|
|
1037
|
+
}
|
|
1038
|
+
return { items, errorCount };
|
|
1039
|
+
}
|
|
1040
|
+
function collectTestCorpus(rootDir, allMdFiles) {
|
|
1041
|
+
const gherkinSources = [];
|
|
1042
|
+
const testContents = [];
|
|
1043
|
+
for (const tf of collectTestFiles(rootDir)) {
|
|
1044
|
+
try {
|
|
1045
|
+
const content = fs7.readFileSync(tf, "utf-8");
|
|
1046
|
+
testContents.push(content);
|
|
1047
|
+
if (tf.endsWith(".feature")) gherkinSources.push(content);
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
for (const md of allMdFiles) {
|
|
1052
|
+
try {
|
|
1053
|
+
const blocks = extractGherkinBlock(fs7.readFileSync(md, "utf-8"));
|
|
1054
|
+
gherkinSources.push(...blocks);
|
|
1055
|
+
testContents.push(...blocks);
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return { gherkinSources, combinedTests: testContents.join("\n") };
|
|
1060
|
+
}
|
|
1061
|
+
function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, taskErrors, ucErrors) {
|
|
1062
|
+
return {
|
|
1063
|
+
success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0,
|
|
1064
|
+
totalRequirements: reqAudit.length,
|
|
1065
|
+
passedRequirements: reqAudit.length - reqErrors,
|
|
1066
|
+
failedRequirements: reqErrors,
|
|
1067
|
+
totalTasks: taskAudit.length,
|
|
1068
|
+
passedTasks: taskAudit.length - taskErrors,
|
|
1069
|
+
failedTasks: taskErrors,
|
|
1070
|
+
requirements: reqAudit,
|
|
1071
|
+
tasks: taskAudit,
|
|
1072
|
+
useCaseFlows,
|
|
1073
|
+
failedUseCaseFlows: ucErrors,
|
|
1074
|
+
reportMarkdown: generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors)
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function recordUseCaseAuditIfPresent(frontmatter, id, relPath, isTemplate, combinedTests, state) {
|
|
1078
|
+
if (isTemplate || !(frontmatter.type === "use-case" || id.startsWith("UC-"))) return;
|
|
1079
|
+
const declaredFlows = extractDeclaredUseCaseFlows(frontmatter);
|
|
1080
|
+
if (declaredFlows.length === 0) return;
|
|
1081
|
+
const flowAudit = auditUseCaseFlowItem(id, relPath, declaredFlows, combinedTests);
|
|
1082
|
+
state.useCaseFlows.push(flowAudit);
|
|
1083
|
+
if (flowAudit.status === "FALLO_SIN_PRUEBA") state.ucErrors++;
|
|
1084
|
+
}
|
|
1085
|
+
function processMdFileForCoverage(file, rootDir, reqToTestsMap, corpus, options, state) {
|
|
1086
|
+
const { frontmatter } = parseGenericDoc(fs7.readFileSync(file, "utf-8"));
|
|
1087
|
+
const relPath = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
1088
|
+
const isTemplate = relPath.includes("templates/");
|
|
1089
|
+
const id = frontmatter.id || "";
|
|
1090
|
+
if (isRequirementArtifact(frontmatter, id)) {
|
|
1091
|
+
const audited = auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap, corpus.gherkinSources, options);
|
|
1092
|
+
state.reqAudit.push(audited.item);
|
|
1093
|
+
if (audited.isError) state.reqErrors++;
|
|
1094
|
+
}
|
|
1095
|
+
recordUseCaseAuditIfPresent(frontmatter, id, relPath, isTemplate, corpus.combinedTests, state);
|
|
1096
|
+
const auditedTasks = auditTasks(frontmatter.tasks, relPath, isTemplate);
|
|
1097
|
+
state.taskAudit.push(...auditedTasks.items);
|
|
1098
|
+
state.taskErrors += auditedTasks.errorCount;
|
|
1099
|
+
}
|
|
1100
|
+
function verifyTestingCoverage(options = {}) {
|
|
1101
|
+
const rootDir = options.rootDir || process.cwd();
|
|
1102
|
+
const allMdFiles = walkMdFiles(rootDir);
|
|
1103
|
+
const reqToTestsMap = scanAllTestArtifacts(rootDir);
|
|
1104
|
+
const corpus = collectTestCorpus(rootDir, allMdFiles);
|
|
1105
|
+
const state = {
|
|
1106
|
+
reqAudit: [],
|
|
1107
|
+
taskAudit: [],
|
|
1108
|
+
useCaseFlows: [],
|
|
1109
|
+
reqErrors: 0,
|
|
1110
|
+
taskErrors: 0,
|
|
1111
|
+
ucErrors: 0
|
|
1112
|
+
};
|
|
1113
|
+
for (const file of allMdFiles) {
|
|
1114
|
+
try {
|
|
1115
|
+
processMdFileForCoverage(file, rootDir, reqToTestsMap, corpus, options, state);
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return assembleCoverageResult(
|
|
1120
|
+
state.reqAudit,
|
|
1121
|
+
state.taskAudit,
|
|
1122
|
+
state.useCaseFlows,
|
|
1123
|
+
state.reqErrors,
|
|
1124
|
+
state.taskErrors,
|
|
1125
|
+
state.ucErrors
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
482
1128
|
|
|
483
1129
|
// src/verifiers/quality-gate.ts
|
|
484
1130
|
var DEFAULT_EXTENSIONS = [
|
|
@@ -593,7 +1239,7 @@ function applyOptionOverrides(policy, overrides) {
|
|
|
593
1239
|
function parseQualityPolicy(content, overrides) {
|
|
594
1240
|
const policy = createDefaultQualityPolicy();
|
|
595
1241
|
try {
|
|
596
|
-
const doc =
|
|
1242
|
+
const doc = yaml3.load(content);
|
|
597
1243
|
if (doc && typeof doc === "object") {
|
|
598
1244
|
const record = doc;
|
|
599
1245
|
applyDocScope(policy, record);
|
|
@@ -609,7 +1255,7 @@ function parseQualityPolicy(content, overrides) {
|
|
|
609
1255
|
return policy;
|
|
610
1256
|
}
|
|
611
1257
|
function extractFunctions(content, filePath) {
|
|
612
|
-
const ext =
|
|
1258
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
613
1259
|
return TS_JS_EXTENSIONS.includes(ext) ? extractFunctionsTypeScriptAst(content, filePath) : extractFunctionsPolyglot(content, filePath);
|
|
614
1260
|
}
|
|
615
1261
|
function extractFunctionsBraceLanguages(content, filePath) {
|
|
@@ -640,55 +1286,55 @@ function isExcludedFile(file, normalized, excludePatterns) {
|
|
|
640
1286
|
);
|
|
641
1287
|
}
|
|
642
1288
|
function walkDir(dir, extensions = DEFAULT_EXTENSIONS, excludePatterns = DEFAULT_EXCLUDES) {
|
|
643
|
-
if (!
|
|
1289
|
+
if (!fs7.existsSync(dir)) return [];
|
|
644
1290
|
const results = [];
|
|
645
|
-
for (const file of
|
|
646
|
-
const fullPath =
|
|
1291
|
+
for (const file of fs7.readdirSync(dir)) {
|
|
1292
|
+
const fullPath = path4.join(dir, file);
|
|
647
1293
|
const normalized = fullPath.replace(/\\/g, "/");
|
|
648
1294
|
if (isExcludedFile(file, normalized, excludePatterns)) continue;
|
|
649
|
-
const stat =
|
|
1295
|
+
const stat = fs7.statSync(fullPath);
|
|
650
1296
|
if (stat.isDirectory()) {
|
|
651
1297
|
results.push(...walkDir(fullPath, extensions, excludePatterns));
|
|
652
|
-
} else if (extensions.includes(
|
|
1298
|
+
} else if (extensions.includes(path4.extname(file).toLowerCase())) {
|
|
653
1299
|
results.push(fullPath);
|
|
654
1300
|
}
|
|
655
1301
|
}
|
|
656
1302
|
return results;
|
|
657
1303
|
}
|
|
658
1304
|
function resolvePolicy(rootDir, options) {
|
|
659
|
-
const policyFile = options.policyPath ||
|
|
1305
|
+
const policyFile = options.policyPath || path4.join(rootDir, "quality-policy.yaml");
|
|
660
1306
|
if (options.policy) {
|
|
661
1307
|
const policy = { ...options.policy };
|
|
662
1308
|
applyOptionOverrides(policy, options.thresholds);
|
|
663
1309
|
return policy;
|
|
664
1310
|
}
|
|
665
|
-
if (
|
|
666
|
-
return parseQualityPolicy(
|
|
1311
|
+
if (fs7.existsSync(policyFile)) {
|
|
1312
|
+
return parseQualityPolicy(fs7.readFileSync(policyFile, "utf-8"), options.thresholds);
|
|
667
1313
|
}
|
|
668
1314
|
return parseQualityPolicy("", options.thresholds);
|
|
669
1315
|
}
|
|
670
1316
|
function isPathExcluded(targetPath, rootDir, excludePatterns) {
|
|
671
|
-
const rel =
|
|
672
|
-
const base =
|
|
1317
|
+
const rel = path4.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
1318
|
+
const base = path4.basename(targetPath);
|
|
673
1319
|
return excludePatterns.some(
|
|
674
1320
|
(pat) => base === pat || rel === pat || rel.startsWith(pat + "/") || rel.includes("/" + pat + "/") || rel.endsWith("/" + pat) || rel.includes(pat)
|
|
675
1321
|
);
|
|
676
1322
|
}
|
|
677
1323
|
function scanContainerSubdir(containerDir, subdirName, targetDir, rootDir, policy, files) {
|
|
678
1324
|
if (policy.exclude_patterns.includes(subdirName)) return;
|
|
679
|
-
const subTarget =
|
|
680
|
-
if (!
|
|
1325
|
+
const subTarget = path4.join(containerDir, subdirName, targetDir);
|
|
1326
|
+
if (!fs7.existsSync(subTarget) || !fs7.statSync(subTarget).isDirectory()) return;
|
|
681
1327
|
if (isPathExcluded(subTarget, rootDir, policy.exclude_patterns)) return;
|
|
682
1328
|
for (const f of walkDir(subTarget, policy.supported_extensions, policy.exclude_patterns)) {
|
|
683
1329
|
files.add(f);
|
|
684
1330
|
}
|
|
685
1331
|
}
|
|
686
1332
|
function scanMonorepoContainer(rootDir, container, targetDir, policy, files) {
|
|
687
|
-
const containerDir =
|
|
688
|
-
if (!
|
|
1333
|
+
const containerDir = path4.join(rootDir, container);
|
|
1334
|
+
if (!fs7.existsSync(containerDir) || !fs7.statSync(containerDir).isDirectory()) return;
|
|
689
1335
|
if (isPathExcluded(containerDir, rootDir, policy.exclude_patterns)) return;
|
|
690
1336
|
try {
|
|
691
|
-
for (const subdir of
|
|
1337
|
+
for (const subdir of fs7.readdirSync(containerDir, { withFileTypes: true })) {
|
|
692
1338
|
if (subdir.isDirectory()) {
|
|
693
1339
|
scanContainerSubdir(containerDir, subdir.name, targetDir, rootDir, policy, files);
|
|
694
1340
|
}
|
|
@@ -697,8 +1343,8 @@ function scanMonorepoContainer(rootDir, container, targetDir, policy, files) {
|
|
|
697
1343
|
}
|
|
698
1344
|
}
|
|
699
1345
|
function scanDirectTarget(rootDir, targetDir, policy, files) {
|
|
700
|
-
const fullDir =
|
|
701
|
-
if (
|
|
1346
|
+
const fullDir = path4.join(rootDir, targetDir);
|
|
1347
|
+
if (fs7.existsSync(fullDir) && !isPathExcluded(fullDir, rootDir, policy.exclude_patterns)) {
|
|
702
1348
|
for (const f of walkDir(fullDir, policy.supported_extensions, policy.exclude_patterns)) {
|
|
703
1349
|
files.add(f);
|
|
704
1350
|
}
|
|
@@ -735,23 +1381,86 @@ function evaluateViolations(fn, policy) {
|
|
|
735
1381
|
return violations;
|
|
736
1382
|
}
|
|
737
1383
|
function evaluateFileQuality(file, rootDir, policy) {
|
|
738
|
-
const content =
|
|
1384
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
739
1385
|
const fns = extractFunctions(content, file);
|
|
740
1386
|
return fns.map((fn) => {
|
|
741
1387
|
const violations = evaluateViolations(fn, policy);
|
|
742
1388
|
return {
|
|
743
1389
|
...fn,
|
|
744
|
-
relPath:
|
|
1390
|
+
relPath: path4.relative(rootDir, file).replace(/\\/g, "/"),
|
|
745
1391
|
status: violations.length === 0 ? "PASS" : "FAIL",
|
|
746
1392
|
violations
|
|
747
1393
|
};
|
|
748
1394
|
});
|
|
749
1395
|
}
|
|
750
|
-
|
|
1396
|
+
var UNIT_TEST_SUFFIXES = [".spec.ts", ".test.ts", ".spec.js", ".test.js", "_test.py", "_test.go"];
|
|
1397
|
+
function collectUnitTestCorpus(rootDir) {
|
|
1398
|
+
const allCodeFiles = walkDir(rootDir, DEFAULT_EXTENSIONS, DEFAULT_EXCLUDES);
|
|
1399
|
+
const chunks = [];
|
|
1400
|
+
for (const tf of allCodeFiles) {
|
|
1401
|
+
if (!UNIT_TEST_SUFFIXES.some((s) => tf.endsWith(s))) continue;
|
|
1402
|
+
try {
|
|
1403
|
+
chunks.push(fs7.readFileSync(tf, "utf-8"));
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return chunks.join("\n");
|
|
1408
|
+
}
|
|
1409
|
+
function auditUnitTestsPerFunction(results, rootDir) {
|
|
1410
|
+
const corpus = collectUnitTestCorpus(rootDir);
|
|
1411
|
+
const untested = [];
|
|
1412
|
+
for (const r of results) {
|
|
1413
|
+
const fnName = r.functionName.includes(".") ? r.functionName.split(".").pop() : r.functionName;
|
|
1414
|
+
if (!fnName || fnName === "anonymous" || fnName === "constructor") continue;
|
|
1415
|
+
if (!corpus.includes(fnName)) {
|
|
1416
|
+
untested.push({ file: r.relPath, functionName: r.functionName });
|
|
1417
|
+
r.violations.push(`Missing unit test for function '${r.functionName}'`);
|
|
1418
|
+
r.status = "FAIL";
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return untested;
|
|
1422
|
+
}
|
|
1423
|
+
function readSummaryCoverageJson(rootDir) {
|
|
1424
|
+
for (const rel of ["coverage/coverage-summary.json", "reports/coverage-summary.json"]) {
|
|
1425
|
+
const full = path4.join(rootDir, rel);
|
|
1426
|
+
if (!fs7.existsSync(full)) continue;
|
|
1427
|
+
try {
|
|
1428
|
+
const raw = JSON.parse(fs7.readFileSync(full, "utf-8"));
|
|
1429
|
+
const total = raw.total;
|
|
1430
|
+
if (!total) continue;
|
|
1431
|
+
return {
|
|
1432
|
+
linesPct: Number(total.lines?.pct ?? 0),
|
|
1433
|
+
branchesPct: Number(total.branches?.pct ?? 0),
|
|
1434
|
+
functionsPct: Number(total.functions?.pct ?? 0),
|
|
1435
|
+
source: rel
|
|
1436
|
+
};
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
function checkCoverageThreshold(label, actual, min, out) {
|
|
1443
|
+
if (actual < min) {
|
|
1444
|
+
out.push(`${label} coverage ${actual}% is below minimum threshold of ${min}%`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function evaluateCoverageThresholds(rootDir, policy, requireReport) {
|
|
1448
|
+
const cov = readSummaryCoverageJson(rootDir);
|
|
1449
|
+
if (!cov) {
|
|
1450
|
+
return requireReport ? ["Missing code coverage summary report (coverage/coverage-summary.json)"] : [];
|
|
1451
|
+
}
|
|
1452
|
+
const violations = [];
|
|
1453
|
+
checkCoverageThreshold("Line", cov.linesPct, policy.min_line_coverage, violations);
|
|
1454
|
+
checkCoverageThreshold("Branch", cov.branchesPct, policy.min_branch_coverage, violations);
|
|
1455
|
+
checkCoverageThreshold("Function", cov.functionsPct, policy.min_function_coverage, violations);
|
|
1456
|
+
return violations;
|
|
1457
|
+
}
|
|
1458
|
+
function buildQualityGateResult(results, totalFiles, policy, coverageViolations, untestedFunctions) {
|
|
751
1459
|
const failCount = results.filter((r) => r.status === "FAIL").length;
|
|
752
1460
|
const passCount = results.length - failCount;
|
|
753
|
-
const violationsCount = results.reduce((acc, r) => acc + r.violations.length, 0);
|
|
754
|
-
const
|
|
1461
|
+
const violationsCount = results.reduce((acc, r) => acc + r.violations.length, 0) + coverageViolations.length;
|
|
1462
|
+
const hasFailures = failCount > 0 || coverageViolations.length > 0;
|
|
1463
|
+
const success = policy.enforce_mode === "STRICT" ? !hasFailures : true;
|
|
755
1464
|
return {
|
|
756
1465
|
success,
|
|
757
1466
|
totalFiles,
|
|
@@ -760,7 +1469,9 @@ function buildQualityGateResult(results, totalFiles, policy) {
|
|
|
760
1469
|
failCount,
|
|
761
1470
|
violationsCount,
|
|
762
1471
|
results,
|
|
763
|
-
policy
|
|
1472
|
+
policy,
|
|
1473
|
+
coverageViolations,
|
|
1474
|
+
untestedFunctions
|
|
764
1475
|
};
|
|
765
1476
|
}
|
|
766
1477
|
function verifyQualityGate(options = {}) {
|
|
@@ -774,199 +1485,10 @@ function verifyQualityGate(options = {}) {
|
|
|
774
1485
|
} catch {
|
|
775
1486
|
}
|
|
776
1487
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
"node_modules",
|
|
781
|
-
".git",
|
|
782
|
-
"dist",
|
|
783
|
-
"bin",
|
|
784
|
-
"obj",
|
|
785
|
-
"build",
|
|
786
|
-
"out",
|
|
787
|
-
"target",
|
|
788
|
-
"coverage",
|
|
789
|
-
".changeset",
|
|
790
|
-
"scratch",
|
|
791
|
-
"test-scaffold",
|
|
792
|
-
"fixtures",
|
|
793
|
-
"templates",
|
|
794
|
-
"examples",
|
|
795
|
-
".agent",
|
|
796
|
-
".agents",
|
|
797
|
-
".claude",
|
|
798
|
-
".cursor",
|
|
799
|
-
".github",
|
|
800
|
-
".vscode",
|
|
801
|
-
".vs",
|
|
802
|
-
".idea"
|
|
803
|
-
]) {
|
|
804
|
-
try {
|
|
805
|
-
if (!fs6.existsSync(dir)) return fileList;
|
|
806
|
-
const files = fs6.readdirSync(dir);
|
|
807
|
-
for (const file of files) {
|
|
808
|
-
const fullPath = path5.join(dir, file);
|
|
809
|
-
try {
|
|
810
|
-
if (fs6.statSync(fullPath).isDirectory()) {
|
|
811
|
-
if (!excludeDirs.includes(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
812
|
-
walkMdFiles(fullPath, fileList, excludeDirs);
|
|
813
|
-
}
|
|
814
|
-
} else if (file.endsWith(".md")) {
|
|
815
|
-
fileList.push(fullPath);
|
|
816
|
-
}
|
|
817
|
-
} catch {
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
} catch {
|
|
821
|
-
}
|
|
822
|
-
return fileList;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// src/verifiers/gherkin.ts
|
|
826
|
-
function parseMarkdownFrontmatter(content) {
|
|
827
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
828
|
-
if (!match) return { frontmatter: {}, body: content };
|
|
829
|
-
const yamlLines = match[1].split("\n");
|
|
830
|
-
const frontmatter = {};
|
|
831
|
-
for (const line of yamlLines) {
|
|
832
|
-
const parts = line.split(":");
|
|
833
|
-
if (parts.length >= 2) {
|
|
834
|
-
const key = parts[0].trim();
|
|
835
|
-
let val = parts.slice(1).join(":").trim();
|
|
836
|
-
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
837
|
-
val = val.substring(1, val.length - 1);
|
|
838
|
-
}
|
|
839
|
-
frontmatter[key] = val;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
const body = content.substring(match[0].length);
|
|
843
|
-
return { frontmatter, body };
|
|
844
|
-
}
|
|
845
|
-
function extractGherkinBlock(body) {
|
|
846
|
-
const gherkinRegex = /```gherkin\r?\n([\s\S]*?)\r?\n```/g;
|
|
847
|
-
const blocks = [];
|
|
848
|
-
let match;
|
|
849
|
-
while ((match = gherkinRegex.exec(body)) !== null) {
|
|
850
|
-
blocks.push(match[1].trim());
|
|
851
|
-
}
|
|
852
|
-
return blocks;
|
|
853
|
-
}
|
|
854
|
-
function resolveTargetFiles(rootDir, targetPath) {
|
|
855
|
-
if (targetPath) {
|
|
856
|
-
const fullTarget = path5.isAbsolute(targetPath) ? targetPath : path5.join(rootDir, targetPath);
|
|
857
|
-
if (!fs6.existsSync(fullTarget)) return [];
|
|
858
|
-
return fs6.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
859
|
-
}
|
|
860
|
-
const searchDirs = [
|
|
861
|
-
path5.join(rootDir, "product"),
|
|
862
|
-
path5.join(rootDir, "security"),
|
|
863
|
-
path5.join(rootDir, "specs"),
|
|
864
|
-
path5.join(rootDir, "examples", "product"),
|
|
865
|
-
path5.join(rootDir, "examples", "security"),
|
|
866
|
-
path5.join(rootDir, "examples", "specs")
|
|
867
|
-
];
|
|
868
|
-
return searchDirs.filter((d) => fs6.existsSync(d)).flatMap((d) => walkMdFiles(d));
|
|
869
|
-
}
|
|
870
|
-
function resolveFeatureFilePath(rootDir, filePath, id, customTarget) {
|
|
871
|
-
if (customTarget) {
|
|
872
|
-
return path5.isAbsolute(customTarget) ? customTarget : path5.join(rootDir, customTarget);
|
|
873
|
-
}
|
|
874
|
-
const baseDir = path5.join(rootDir, "tests", "features");
|
|
875
|
-
const candidateSecurity = path5.join(baseDir, "security", `${id.toLowerCase()}.feature`);
|
|
876
|
-
const candidateRoot = path5.join(baseDir, `${id.toLowerCase()}.feature`);
|
|
877
|
-
if (fs6.existsSync(candidateSecurity)) return candidateSecurity;
|
|
878
|
-
if (fs6.existsSync(candidateRoot)) return candidateRoot;
|
|
879
|
-
const subDir = filePath.includes("security") ? "security" : "";
|
|
880
|
-
return path5.join(baseDir, subDir, `${id.toLowerCase()}.feature`);
|
|
881
|
-
}
|
|
882
|
-
function buildFeatureFileContent(rootDir, filePath, id, version, gherkinBlocks) {
|
|
883
|
-
const banner = [
|
|
884
|
-
`# ==============================================================================`,
|
|
885
|
-
`# AUTO-GENERADO POR AI-SDLC (Cucumber Integration)`,
|
|
886
|
-
`# Origen: ${path5.relative(rootDir, filePath).replace(/\\/g, "/")}`,
|
|
887
|
-
`# ID Requerimiento: ${id}`,
|
|
888
|
-
`# Versi\xF3n: ${version || "1.0.0"}`,
|
|
889
|
-
`# NO EDITAR MANUALMENTE: Cualquier cambio debe realizarse en el Markdown origen.`,
|
|
890
|
-
`# ==============================================================================`,
|
|
891
|
-
"",
|
|
892
|
-
""
|
|
893
|
-
].join("\n");
|
|
894
|
-
return banner + gherkinBlocks.join("\n\n") + "\n";
|
|
895
|
-
}
|
|
896
|
-
function countScenarios(gherkinBlocks) {
|
|
897
|
-
let count = 0;
|
|
898
|
-
for (const block of gherkinBlocks) {
|
|
899
|
-
const matches = block.match(/\b(Scenario|Escenario|Scenario Outline|Esquema del escenario):/g);
|
|
900
|
-
if (matches) count += matches.length;
|
|
901
|
-
}
|
|
902
|
-
return count;
|
|
903
|
-
}
|
|
904
|
-
function extractGherkinFeatures(options = {}) {
|
|
905
|
-
const rootDir = options.rootDir || process.cwd();
|
|
906
|
-
const features = [];
|
|
907
|
-
let totalScenarios = 0;
|
|
908
|
-
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
909
|
-
for (const filePath of targetFiles) {
|
|
910
|
-
try {
|
|
911
|
-
const content = fs6.readFileSync(filePath, "utf-8");
|
|
912
|
-
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
913
|
-
const gherkinBlocks = extractGherkinBlock(body);
|
|
914
|
-
if (gherkinBlocks.length === 0) continue;
|
|
915
|
-
const id = frontmatter.id || path5.basename(filePath, path5.extname(filePath));
|
|
916
|
-
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
917
|
-
fs6.mkdirSync(path5.dirname(targetPath), { recursive: true });
|
|
918
|
-
const fullContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
919
|
-
fs6.writeFileSync(targetPath, fullContent, "utf-8");
|
|
920
|
-
const scenarioCount = countScenarios(gherkinBlocks);
|
|
921
|
-
totalScenarios += scenarioCount;
|
|
922
|
-
features.push({
|
|
923
|
-
sourceFile: path5.relative(rootDir, filePath).replace(/\\/g, "/"),
|
|
924
|
-
outputFile: path5.relative(rootDir, targetPath).replace(/\\/g, "/"),
|
|
925
|
-
featureName: id,
|
|
926
|
-
scenarioCount
|
|
927
|
-
});
|
|
928
|
-
} catch {
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
return { success: true, features, totalScenarios };
|
|
932
|
-
}
|
|
933
|
-
function checkGherkinInSync(options = {}) {
|
|
934
|
-
const rootDir = options.rootDir || process.cwd();
|
|
935
|
-
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
936
|
-
const outOfSyncFiles = [];
|
|
937
|
-
const missingFiles = [];
|
|
938
|
-
let totalFeatures = 0;
|
|
939
|
-
for (const filePath of targetFiles) {
|
|
940
|
-
try {
|
|
941
|
-
const content = fs6.readFileSync(filePath, "utf-8");
|
|
942
|
-
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
943
|
-
const gherkinBlocks = extractGherkinBlock(body);
|
|
944
|
-
if (gherkinBlocks.length === 0) continue;
|
|
945
|
-
totalFeatures++;
|
|
946
|
-
const id = frontmatter.id || path5.basename(filePath, path5.extname(filePath));
|
|
947
|
-
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
948
|
-
const relTarget = path5.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
949
|
-
if (!fs6.existsSync(targetPath)) {
|
|
950
|
-
missingFiles.push(relTarget);
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
const expectedContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
954
|
-
const actualContent = fs6.readFileSync(targetPath, "utf-8");
|
|
955
|
-
if (actualContent.replace(/\r\n/g, "\n") !== expectedContent.replace(/\r\n/g, "\n")) {
|
|
956
|
-
outOfSyncFiles.push(relTarget);
|
|
957
|
-
}
|
|
958
|
-
} catch {
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return {
|
|
962
|
-
inSync: missingFiles.length === 0 && outOfSyncFiles.length === 0,
|
|
963
|
-
outOfSyncFiles,
|
|
964
|
-
missingFiles,
|
|
965
|
-
totalFeatures
|
|
966
|
-
};
|
|
1488
|
+
const untestedFunctions = options.requireFunctionUnitTests ? auditUnitTestsPerFunction(results, rootDir) : [];
|
|
1489
|
+
const coverageViolations = evaluateCoverageThresholds(rootDir, policy, options.requireCoverageReport);
|
|
1490
|
+
return buildQualityGateResult(results, allFiles.length, policy, coverageViolations, untestedFunctions);
|
|
967
1491
|
}
|
|
968
|
-
|
|
969
|
-
// src/adapters/sdd/openspec.ts
|
|
970
1492
|
function normalizeHandoffPayload(handoff, changeId) {
|
|
971
1493
|
return {
|
|
972
1494
|
...handoff,
|
|
@@ -976,12 +1498,12 @@ function normalizeHandoffPayload(handoff, changeId) {
|
|
|
976
1498
|
};
|
|
977
1499
|
}
|
|
978
1500
|
function parseChangeProfile(specFile) {
|
|
979
|
-
if (!
|
|
1501
|
+
if (!fs7.existsSync(specFile)) return "standard";
|
|
980
1502
|
try {
|
|
981
|
-
const rawSpec =
|
|
1503
|
+
const rawSpec = fs7.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
982
1504
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
983
1505
|
if (match) {
|
|
984
|
-
const parsedFm =
|
|
1506
|
+
const parsedFm = yaml3.load(match[1]);
|
|
985
1507
|
if (parsedFm?.profile && ["patch", "standard", "critical"].includes(String(parsedFm.profile))) {
|
|
986
1508
|
return parsedFm.profile;
|
|
987
1509
|
}
|
|
@@ -1000,15 +1522,15 @@ function checkSpecGherkinFormat(parsedFm, rawSpec, changeWorkspaceDir, errors) {
|
|
|
1000
1522
|
}
|
|
1001
1523
|
}
|
|
1002
1524
|
function validateSpecFileContent(specFile, changeWorkspaceDir, errors) {
|
|
1003
|
-
if (!
|
|
1525
|
+
if (!fs7.existsSync(specFile)) {
|
|
1004
1526
|
errors.push(`Falta el archivo obligatorio 'spec.md' en ${changeWorkspaceDir}.`);
|
|
1005
1527
|
return;
|
|
1006
1528
|
}
|
|
1007
1529
|
try {
|
|
1008
|
-
const rawSpec =
|
|
1530
|
+
const rawSpec = fs7.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1009
1531
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1010
1532
|
if (match) {
|
|
1011
|
-
const parsedFm =
|
|
1533
|
+
const parsedFm = yaml3.load(match[1]);
|
|
1012
1534
|
if (parsedFm) checkSpecGherkinFormat(parsedFm, rawSpec, changeWorkspaceDir, errors);
|
|
1013
1535
|
}
|
|
1014
1536
|
} catch {
|
|
@@ -1024,19 +1546,19 @@ function validateHandoffSidecar(handoff, changeWorkspaceDir, errors) {
|
|
|
1024
1546
|
if (!handoff.subgraph || !Array.isArray(handoff.subgraph.requirements)) {
|
|
1025
1547
|
errors.push(`El sidecar '${handoff.id}' no define un subgrafo de requerimientos v\xE1lido.`);
|
|
1026
1548
|
}
|
|
1027
|
-
return
|
|
1549
|
+
return path4.join(changeWorkspaceDir, "handoff.yaml");
|
|
1028
1550
|
}
|
|
1029
1551
|
function validateOpenSpecStructure(profile, changeWorkspaceDir, adapter, errors, warnings) {
|
|
1030
|
-
const proposalFile =
|
|
1031
|
-
const tasksFile =
|
|
1552
|
+
const proposalFile = path4.join(changeWorkspaceDir, "proposal.md");
|
|
1553
|
+
const tasksFile = path4.join(changeWorkspaceDir, "tasks.md");
|
|
1032
1554
|
if (profile === "patch") {
|
|
1033
1555
|
const handoff2 = adapter.loadSidecar(changeWorkspaceDir);
|
|
1034
|
-
return { handoff: handoff2, sidecarFile: handoff2 ?
|
|
1556
|
+
return { handoff: handoff2, sidecarFile: handoff2 ? path4.join(changeWorkspaceDir, "handoff.yaml") : void 0 };
|
|
1035
1557
|
}
|
|
1036
|
-
if (!
|
|
1558
|
+
if (!fs7.existsSync(proposalFile)) {
|
|
1037
1559
|
warnings.push(`Falta 'proposal.md' en el espacio de trabajo OpenSpec.`);
|
|
1038
1560
|
}
|
|
1039
|
-
if (!
|
|
1561
|
+
if (!fs7.existsSync(tasksFile)) {
|
|
1040
1562
|
errors.push(`Falta el archivo obligatorio 'tasks.md' en ${changeWorkspaceDir}.`);
|
|
1041
1563
|
}
|
|
1042
1564
|
const handoff = adapter.loadSidecar(changeWorkspaceDir);
|
|
@@ -1044,20 +1566,20 @@ function validateOpenSpecStructure(profile, changeWorkspaceDir, adapter, errors,
|
|
|
1044
1566
|
return { handoff, sidecarFile };
|
|
1045
1567
|
}
|
|
1046
1568
|
function buildOpenSpecWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecarFile) {
|
|
1047
|
-
const changeId =
|
|
1048
|
-
const proposalFile =
|
|
1049
|
-
const specFile =
|
|
1050
|
-
const designFile =
|
|
1051
|
-
const tasksFile =
|
|
1569
|
+
const changeId = path4.basename(changeWorkspaceDir);
|
|
1570
|
+
const proposalFile = path4.join(changeWorkspaceDir, "proposal.md");
|
|
1571
|
+
const specFile = path4.join(changeWorkspaceDir, "spec.md");
|
|
1572
|
+
const designFile = path4.join(changeWorkspaceDir, "design.md");
|
|
1573
|
+
const tasksFile = path4.join(changeWorkspaceDir, "tasks.md");
|
|
1052
1574
|
return {
|
|
1053
1575
|
framework: "openspec",
|
|
1054
1576
|
changeId,
|
|
1055
1577
|
changeDir: changeWorkspaceDir,
|
|
1056
1578
|
profile,
|
|
1057
|
-
proposalFile:
|
|
1058
|
-
specFile:
|
|
1059
|
-
designFile:
|
|
1060
|
-
tasksFile:
|
|
1579
|
+
proposalFile: fs7.existsSync(proposalFile) ? proposalFile : void 0,
|
|
1580
|
+
specFile: fs7.existsSync(specFile) ? specFile : void 0,
|
|
1581
|
+
designFile: fs7.existsSync(designFile) ? designFile : void 0,
|
|
1582
|
+
tasksFile: fs7.existsSync(tasksFile) ? tasksFile : void 0,
|
|
1061
1583
|
sidecarFile,
|
|
1062
1584
|
handoff: handoff || void 0
|
|
1063
1585
|
};
|
|
@@ -1065,28 +1587,28 @@ function buildOpenSpecWorkspaceInfo(changeWorkspaceDir, profile, handoff, sideca
|
|
|
1065
1587
|
var OpenSpecAdapter = class {
|
|
1066
1588
|
framework = "openspec";
|
|
1067
1589
|
getChangeWorkspaceDir(rootDir, changeId) {
|
|
1068
|
-
const openspecDir =
|
|
1069
|
-
if (
|
|
1070
|
-
const activeChangeDir =
|
|
1071
|
-
if (
|
|
1072
|
-
const completedChangeDir =
|
|
1073
|
-
if (
|
|
1590
|
+
const openspecDir = path4.join(rootDir, "openspec", "changes", changeId);
|
|
1591
|
+
if (fs7.existsSync(openspecDir)) return openspecDir;
|
|
1592
|
+
const activeChangeDir = path4.join(rootDir, "specs", "changes", "active", changeId);
|
|
1593
|
+
if (fs7.existsSync(activeChangeDir)) return activeChangeDir;
|
|
1594
|
+
const completedChangeDir = path4.join(rootDir, "specs", "changes", "completed", changeId);
|
|
1595
|
+
if (fs7.existsSync(completedChangeDir)) return completedChangeDir;
|
|
1074
1596
|
return activeChangeDir;
|
|
1075
1597
|
}
|
|
1076
1598
|
depositSidecar(options) {
|
|
1077
1599
|
const changeDir = this.getChangeWorkspaceDir(options.rootDir, options.changeId);
|
|
1078
|
-
if (!
|
|
1079
|
-
|
|
1600
|
+
if (!fs7.existsSync(changeDir)) {
|
|
1601
|
+
fs7.mkdirSync(changeDir, { recursive: true });
|
|
1080
1602
|
}
|
|
1081
1603
|
const handoff = normalizeHandoffPayload(options.handoff, options.changeId);
|
|
1082
1604
|
const filename = options.format === "json" ? "handoff.json" : "handoff.yaml";
|
|
1083
|
-
const filePath =
|
|
1084
|
-
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) :
|
|
1085
|
-
|
|
1605
|
+
const filePath = path4.join(changeDir, filename);
|
|
1606
|
+
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) : yaml3.dump(handoff, { indent: 2, lineWidth: -1 });
|
|
1607
|
+
fs7.writeFileSync(filePath, content, "utf-8");
|
|
1086
1608
|
return filePath;
|
|
1087
1609
|
}
|
|
1088
1610
|
loadSidecar(changeWorkspaceDir) {
|
|
1089
|
-
if (!
|
|
1611
|
+
if (!fs7.existsSync(changeWorkspaceDir)) return null;
|
|
1090
1612
|
const candidateFiles = [
|
|
1091
1613
|
"handoff.yaml",
|
|
1092
1614
|
"handoff.yml",
|
|
@@ -1097,11 +1619,11 @@ var OpenSpecAdapter = class {
|
|
|
1097
1619
|
"product-handoff.json"
|
|
1098
1620
|
];
|
|
1099
1621
|
for (const file of candidateFiles) {
|
|
1100
|
-
const fullPath =
|
|
1101
|
-
if (
|
|
1622
|
+
const fullPath = path4.join(changeWorkspaceDir, file);
|
|
1623
|
+
if (fs7.existsSync(fullPath)) {
|
|
1102
1624
|
try {
|
|
1103
|
-
const raw =
|
|
1104
|
-
const parsed = file.endsWith(".json") ? JSON.parse(raw) :
|
|
1625
|
+
const raw = fs7.readFileSync(fullPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1626
|
+
const parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml3.load(raw);
|
|
1105
1627
|
if (parsed && typeof parsed === "object" && parsed.id && String(parsed.id).startsWith("HOF-")) {
|
|
1106
1628
|
return parsed;
|
|
1107
1629
|
}
|
|
@@ -1112,7 +1634,7 @@ var OpenSpecAdapter = class {
|
|
|
1112
1634
|
return null;
|
|
1113
1635
|
}
|
|
1114
1636
|
validateWorkspace(changeWorkspaceDir) {
|
|
1115
|
-
if (!
|
|
1637
|
+
if (!fs7.existsSync(changeWorkspaceDir)) {
|
|
1116
1638
|
return {
|
|
1117
1639
|
valid: false,
|
|
1118
1640
|
errors: [`El directorio del cambio OpenSpec no existe: ${changeWorkspaceDir}`],
|
|
@@ -1121,7 +1643,7 @@ var OpenSpecAdapter = class {
|
|
|
1121
1643
|
}
|
|
1122
1644
|
const errors = [];
|
|
1123
1645
|
const warnings = [];
|
|
1124
|
-
const specFile =
|
|
1646
|
+
const specFile = path4.join(changeWorkspaceDir, "spec.md");
|
|
1125
1647
|
const profile = parseChangeProfile(specFile);
|
|
1126
1648
|
validateSpecFileContent(specFile, changeWorkspaceDir, errors);
|
|
1127
1649
|
const { handoff, sidecarFile } = validateOpenSpecStructure(profile, changeWorkspaceDir, this, errors, warnings);
|
|
@@ -1135,12 +1657,12 @@ var OpenSpecAdapter = class {
|
|
|
1135
1657
|
}
|
|
1136
1658
|
};
|
|
1137
1659
|
function parseChangeProfile2(specFile) {
|
|
1138
|
-
if (!
|
|
1660
|
+
if (!fs7.existsSync(specFile)) return "standard";
|
|
1139
1661
|
try {
|
|
1140
|
-
const rawSpec =
|
|
1662
|
+
const rawSpec = fs7.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1141
1663
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1142
1664
|
if (match) {
|
|
1143
|
-
const parsedFm =
|
|
1665
|
+
const parsedFm = yaml3.load(match[1]);
|
|
1144
1666
|
if (parsedFm?.profile && ["patch", "standard", "critical"].includes(String(parsedFm.profile))) {
|
|
1145
1667
|
return parsedFm.profile;
|
|
1146
1668
|
}
|
|
@@ -1159,15 +1681,15 @@ function checkSpecGherkinFormat2(parsedFm, rawSpec, changeWorkspaceDir, errors)
|
|
|
1159
1681
|
}
|
|
1160
1682
|
}
|
|
1161
1683
|
function validateSpecFileContent2(specFile, changeWorkspaceDir, errors) {
|
|
1162
|
-
if (!
|
|
1684
|
+
if (!fs7.existsSync(specFile)) {
|
|
1163
1685
|
errors.push(`Falta el archivo de especificaci\xF3n obligatorio 'spec.md' en ${changeWorkspaceDir}.`);
|
|
1164
1686
|
return;
|
|
1165
1687
|
}
|
|
1166
1688
|
try {
|
|
1167
|
-
const rawSpec =
|
|
1689
|
+
const rawSpec = fs7.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1168
1690
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1169
1691
|
if (match) {
|
|
1170
|
-
const parsedFm =
|
|
1692
|
+
const parsedFm = yaml3.load(match[1]);
|
|
1171
1693
|
if (parsedFm) checkSpecGherkinFormat2(parsedFm, rawSpec, changeWorkspaceDir, errors);
|
|
1172
1694
|
}
|
|
1173
1695
|
} catch {
|
|
@@ -1183,19 +1705,19 @@ function validateHandoffSidecar2(handoff, changeWorkspaceDir, errors) {
|
|
|
1183
1705
|
if (!handoff.subgraph || !Array.isArray(handoff.subgraph.requirements)) {
|
|
1184
1706
|
errors.push(`El sidecar '${handoff.id}' no define un subgrafo de requerimientos v\xE1lido.`);
|
|
1185
1707
|
}
|
|
1186
|
-
return
|
|
1708
|
+
return path4.join(changeWorkspaceDir, "handoff.yaml");
|
|
1187
1709
|
}
|
|
1188
1710
|
function validateSpecKitStructure(profile, changeWorkspaceDir, adapter, errors, warnings) {
|
|
1189
|
-
const planFile =
|
|
1190
|
-
const tasksFile =
|
|
1711
|
+
const planFile = path4.join(changeWorkspaceDir, "plan.md");
|
|
1712
|
+
const tasksFile = path4.join(changeWorkspaceDir, "tasks.md");
|
|
1191
1713
|
if (profile === "patch") {
|
|
1192
1714
|
const handoff2 = adapter.loadSidecar(changeWorkspaceDir);
|
|
1193
|
-
return { handoff: handoff2, sidecarFile: handoff2 ?
|
|
1715
|
+
return { handoff: handoff2, sidecarFile: handoff2 ? path4.join(changeWorkspaceDir, "handoff.yaml") : void 0 };
|
|
1194
1716
|
}
|
|
1195
|
-
if (!
|
|
1717
|
+
if (!fs7.existsSync(tasksFile)) {
|
|
1196
1718
|
errors.push(`Falta el archivo de tareas obligatorio 'tasks.md' en ${changeWorkspaceDir}.`);
|
|
1197
1719
|
}
|
|
1198
|
-
if (!
|
|
1720
|
+
if (!fs7.existsSync(planFile)) {
|
|
1199
1721
|
warnings.push(`Se recomienda incluir 'plan.md' en el espacio de trabajo Spec Kit.`);
|
|
1200
1722
|
}
|
|
1201
1723
|
const handoff = adapter.loadSidecar(changeWorkspaceDir);
|
|
@@ -1203,18 +1725,18 @@ function validateSpecKitStructure(profile, changeWorkspaceDir, adapter, errors,
|
|
|
1203
1725
|
return { handoff, sidecarFile };
|
|
1204
1726
|
}
|
|
1205
1727
|
function buildSpecKitWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecarFile) {
|
|
1206
|
-
const changeId =
|
|
1207
|
-
const specFile =
|
|
1208
|
-
const planFile =
|
|
1209
|
-
const tasksFile =
|
|
1728
|
+
const changeId = path4.basename(changeWorkspaceDir);
|
|
1729
|
+
const specFile = path4.join(changeWorkspaceDir, "spec.md");
|
|
1730
|
+
const planFile = path4.join(changeWorkspaceDir, "plan.md");
|
|
1731
|
+
const tasksFile = path4.join(changeWorkspaceDir, "tasks.md");
|
|
1210
1732
|
return {
|
|
1211
1733
|
framework: "speckit",
|
|
1212
1734
|
changeId,
|
|
1213
1735
|
changeDir: changeWorkspaceDir,
|
|
1214
1736
|
profile,
|
|
1215
|
-
specFile:
|
|
1216
|
-
designFile:
|
|
1217
|
-
tasksFile:
|
|
1737
|
+
specFile: fs7.existsSync(specFile) ? specFile : void 0,
|
|
1738
|
+
designFile: fs7.existsSync(planFile) ? planFile : void 0,
|
|
1739
|
+
tasksFile: fs7.existsSync(tasksFile) ? tasksFile : void 0,
|
|
1218
1740
|
sidecarFile,
|
|
1219
1741
|
handoff: handoff || void 0
|
|
1220
1742
|
};
|
|
@@ -1222,18 +1744,18 @@ function buildSpecKitWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecar
|
|
|
1222
1744
|
var SpecKitAdapter = class {
|
|
1223
1745
|
framework = "speckit";
|
|
1224
1746
|
getChangeWorkspaceDir(rootDir, changeId) {
|
|
1225
|
-
const standardDir =
|
|
1226
|
-
if (
|
|
1227
|
-
const activeChangeDir =
|
|
1228
|
-
if (
|
|
1229
|
-
const completedChangeDir =
|
|
1230
|
-
if (
|
|
1747
|
+
const standardDir = path4.join(rootDir, "specs", changeId);
|
|
1748
|
+
if (fs7.existsSync(standardDir)) return standardDir;
|
|
1749
|
+
const activeChangeDir = path4.join(rootDir, "specs", "changes", "active", changeId);
|
|
1750
|
+
if (fs7.existsSync(activeChangeDir)) return activeChangeDir;
|
|
1751
|
+
const completedChangeDir = path4.join(rootDir, "specs", "changes", "completed", changeId);
|
|
1752
|
+
if (fs7.existsSync(completedChangeDir)) return completedChangeDir;
|
|
1231
1753
|
return standardDir;
|
|
1232
1754
|
}
|
|
1233
1755
|
depositSidecar(options) {
|
|
1234
1756
|
const changeDir = this.getChangeWorkspaceDir(options.rootDir, options.changeId);
|
|
1235
|
-
if (!
|
|
1236
|
-
|
|
1757
|
+
if (!fs7.existsSync(changeDir)) {
|
|
1758
|
+
fs7.mkdirSync(changeDir, { recursive: true });
|
|
1237
1759
|
}
|
|
1238
1760
|
const handoff = { ...options.handoff };
|
|
1239
1761
|
if (!handoff.id.startsWith("HOF-")) {
|
|
@@ -1246,20 +1768,20 @@ var SpecKitAdapter = class {
|
|
|
1246
1768
|
handoff.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1247
1769
|
}
|
|
1248
1770
|
const filename = options.format === "json" ? "handoff.json" : "handoff.yaml";
|
|
1249
|
-
const filePath =
|
|
1250
|
-
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) :
|
|
1251
|
-
|
|
1252
|
-
const specifyMemoryDir =
|
|
1253
|
-
if (
|
|
1254
|
-
if (!
|
|
1255
|
-
|
|
1771
|
+
const filePath = path4.join(changeDir, filename);
|
|
1772
|
+
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) : yaml3.dump(handoff, { indent: 2, lineWidth: -1 });
|
|
1773
|
+
fs7.writeFileSync(filePath, content, "utf-8");
|
|
1774
|
+
const specifyMemoryDir = path4.join(options.rootDir, ".specify", "memory");
|
|
1775
|
+
if (fs7.existsSync(path4.join(options.rootDir, ".specify"))) {
|
|
1776
|
+
if (!fs7.existsSync(specifyMemoryDir)) {
|
|
1777
|
+
fs7.mkdirSync(specifyMemoryDir, { recursive: true });
|
|
1256
1778
|
}
|
|
1257
|
-
|
|
1779
|
+
fs7.writeFileSync(path4.join(specifyMemoryDir, `${options.changeId}.handoff.yaml`), content, "utf-8");
|
|
1258
1780
|
}
|
|
1259
1781
|
return filePath;
|
|
1260
1782
|
}
|
|
1261
1783
|
loadSidecar(changeWorkspaceDir) {
|
|
1262
|
-
if (!
|
|
1784
|
+
if (!fs7.existsSync(changeWorkspaceDir)) return null;
|
|
1263
1785
|
const candidateFiles = [
|
|
1264
1786
|
"handoff.yaml",
|
|
1265
1787
|
"handoff.yml",
|
|
@@ -1269,11 +1791,11 @@ var SpecKitAdapter = class {
|
|
|
1269
1791
|
"handoff.json"
|
|
1270
1792
|
];
|
|
1271
1793
|
for (const file of candidateFiles) {
|
|
1272
|
-
const fullPath =
|
|
1273
|
-
if (
|
|
1794
|
+
const fullPath = path4.join(changeWorkspaceDir, file);
|
|
1795
|
+
if (fs7.existsSync(fullPath)) {
|
|
1274
1796
|
try {
|
|
1275
|
-
const raw =
|
|
1276
|
-
const parsed = file.endsWith(".json") ? JSON.parse(raw) :
|
|
1797
|
+
const raw = fs7.readFileSync(fullPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1798
|
+
const parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml3.load(raw);
|
|
1277
1799
|
if (parsed && typeof parsed === "object" && parsed.id && String(parsed.id).startsWith("HOF-")) {
|
|
1278
1800
|
return parsed;
|
|
1279
1801
|
}
|
|
@@ -1284,7 +1806,7 @@ var SpecKitAdapter = class {
|
|
|
1284
1806
|
return null;
|
|
1285
1807
|
}
|
|
1286
1808
|
validateWorkspace(changeWorkspaceDir) {
|
|
1287
|
-
if (!
|
|
1809
|
+
if (!fs7.existsSync(changeWorkspaceDir)) {
|
|
1288
1810
|
return {
|
|
1289
1811
|
valid: false,
|
|
1290
1812
|
errors: [`El directorio del cambio Spec Kit no existe: ${changeWorkspaceDir}`],
|
|
@@ -1293,7 +1815,7 @@ var SpecKitAdapter = class {
|
|
|
1293
1815
|
}
|
|
1294
1816
|
const errors = [];
|
|
1295
1817
|
const warnings = [];
|
|
1296
|
-
const specFile =
|
|
1818
|
+
const specFile = path4.join(changeWorkspaceDir, "spec.md");
|
|
1297
1819
|
const profile = parseChangeProfile2(specFile);
|
|
1298
1820
|
validateSpecFileContent2(specFile, changeWorkspaceDir, errors);
|
|
1299
1821
|
const { handoff, sidecarFile } = validateSpecKitStructure(profile, changeWorkspaceDir, this, errors, warnings);
|
|
@@ -1436,27 +1958,27 @@ ${frontmatter}---
|
|
|
1436
1958
|
${body}`;
|
|
1437
1959
|
}
|
|
1438
1960
|
function materializeCanonicalArtifact(rootDir, item, changeId, author, date) {
|
|
1439
|
-
const targetPath =
|
|
1440
|
-
if (
|
|
1441
|
-
|
|
1961
|
+
const targetPath = path4.join(rootDir, item.targetDir, `${item.id}.md`);
|
|
1962
|
+
if (fs7.existsSync(targetPath)) return targetPath;
|
|
1963
|
+
fs7.mkdirSync(path4.dirname(targetPath), { recursive: true });
|
|
1442
1964
|
const initialContent = buildCanonicalInitialContent(item, changeId, author, date);
|
|
1443
|
-
|
|
1965
|
+
fs7.writeFileSync(targetPath, initialContent, "utf-8");
|
|
1444
1966
|
return targetPath;
|
|
1445
1967
|
}
|
|
1446
1968
|
function findChangeWorkspaceDir(rootDir, changeId) {
|
|
1447
|
-
const activeDir =
|
|
1448
|
-
if (
|
|
1449
|
-
const completedDir =
|
|
1450
|
-
if (
|
|
1451
|
-
const openspecActive =
|
|
1452
|
-
if (
|
|
1969
|
+
const activeDir = path4.join(rootDir, "specs", "changes", "active", changeId);
|
|
1970
|
+
if (fs7.existsSync(activeDir)) return activeDir;
|
|
1971
|
+
const completedDir = path4.join(rootDir, "specs", "changes", "completed", changeId);
|
|
1972
|
+
if (fs7.existsSync(completedDir)) return completedDir;
|
|
1973
|
+
const openspecActive = path4.join(rootDir, "openspec", "changes", changeId);
|
|
1974
|
+
if (fs7.existsSync(openspecActive)) return openspecActive;
|
|
1453
1975
|
return null;
|
|
1454
1976
|
}
|
|
1455
1977
|
function checkTasksExecution(changeDir, changeId) {
|
|
1456
|
-
const tasksFile =
|
|
1978
|
+
const tasksFile = path4.join(changeDir, "tasks.md");
|
|
1457
1979
|
const allTasks = [];
|
|
1458
|
-
if (
|
|
1459
|
-
allTasks.push(...parseTasksFromContent(
|
|
1980
|
+
if (fs7.existsSync(tasksFile)) {
|
|
1981
|
+
allTasks.push(...parseTasksFromContent(fs7.readFileSync(tasksFile, "utf-8")));
|
|
1460
1982
|
}
|
|
1461
1983
|
const completedTasks = allTasks.filter((t) => t.status === "COMPLETED").map((t) => t.id);
|
|
1462
1984
|
const pendingTasks = allTasks.filter((t) => t.status !== "COMPLETED").map((t) => t.id);
|
|
@@ -1473,9 +1995,9 @@ function checkTasksExecution(changeDir, changeId) {
|
|
|
1473
1995
|
function resolveChangeHandoff(changeDir, changeId) {
|
|
1474
1996
|
const sidecar = loadProductHandoffSidecar(changeDir);
|
|
1475
1997
|
if (sidecar) return sidecar;
|
|
1476
|
-
const proposalFile =
|
|
1477
|
-
if (!
|
|
1478
|
-
const propContent =
|
|
1998
|
+
const proposalFile = path4.join(changeDir, "proposal.md");
|
|
1999
|
+
if (!fs7.existsSync(proposalFile)) return null;
|
|
2000
|
+
const propContent = fs7.readFileSync(proposalFile, "utf-8");
|
|
1479
2001
|
const { frontmatter } = parseFrontmatter(propContent);
|
|
1480
2002
|
const citations = Array.isArray(frontmatter.citations) ? frontmatter.citations : [];
|
|
1481
2003
|
const reqs = citations.map((c) => c.id || "").filter((id) => id.startsWith("FR-") || id.startsWith("QR-") || id.startsWith("CON-") || id.startsWith("SEC-REQ-"));
|
|
@@ -1489,9 +2011,9 @@ function resolveChangeHandoff(changeDir, changeId) {
|
|
|
1489
2011
|
}
|
|
1490
2012
|
function materializeNewCatalogArtifacts(changeDir, rootDir, changeId, author, date, errors) {
|
|
1491
2013
|
const created = [];
|
|
1492
|
-
const specFile =
|
|
1493
|
-
if (!
|
|
1494
|
-
const specContent =
|
|
2014
|
+
const specFile = path4.join(changeDir, "spec.md");
|
|
2015
|
+
if (!fs7.existsSync(specFile)) return created;
|
|
2016
|
+
const specContent = fs7.readFileSync(specFile, "utf-8");
|
|
1495
2017
|
const { newItems } = parseSpecCatalogScope(specContent);
|
|
1496
2018
|
for (const item of newItems) {
|
|
1497
2019
|
try {
|
|
@@ -1508,10 +2030,10 @@ function loadCanonicalArtifacts(rootDir) {
|
|
|
1508
2030
|
const allMdFiles = walkMdFiles(rootDir);
|
|
1509
2031
|
const canonicalArtifacts = /* @__PURE__ */ new Map();
|
|
1510
2032
|
for (const file of allMdFiles) {
|
|
1511
|
-
const relFile =
|
|
2033
|
+
const relFile = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
1512
2034
|
if (relFile.includes("specs/changes/") || relFile.includes("templates/")) continue;
|
|
1513
2035
|
try {
|
|
1514
|
-
const content =
|
|
2036
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
1515
2037
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
1516
2038
|
if (frontmatter.id) canonicalArtifacts.set(String(frontmatter.id), { file, frontmatter, body });
|
|
1517
2039
|
} catch {
|
|
@@ -1521,10 +2043,10 @@ function loadCanonicalArtifacts(rootDir) {
|
|
|
1521
2043
|
}
|
|
1522
2044
|
function writeUpdatedArtifact(file, frontmatter, body) {
|
|
1523
2045
|
const yamlHeader = `---
|
|
1524
|
-
${
|
|
2046
|
+
${yaml3.dump(frontmatter, { indent: 2, lineWidth: -1 }).trim()}
|
|
1525
2047
|
---
|
|
1526
2048
|
`;
|
|
1527
|
-
|
|
2049
|
+
fs7.writeFileSync(file, yamlHeader + body, "utf-8");
|
|
1528
2050
|
}
|
|
1529
2051
|
function updateSingleCanonicalRequirement(canonical, completedTasks, changeId, date, author) {
|
|
1530
2052
|
const fm = canonical.frontmatter;
|
|
@@ -1636,28 +2158,28 @@ function updateArchitectureComponents(citedComponents, canonicalArtifacts, targe
|
|
|
1636
2158
|
return updated;
|
|
1637
2159
|
}
|
|
1638
2160
|
function moveToCompletedFolder(changeDir, changeId, rootDir) {
|
|
1639
|
-
const completedParent =
|
|
1640
|
-
if (!
|
|
1641
|
-
const destDir =
|
|
1642
|
-
if (
|
|
1643
|
-
|
|
1644
|
-
const destProposal =
|
|
1645
|
-
if (
|
|
1646
|
-
const { frontmatter, body } = parseFrontmatter(
|
|
2161
|
+
const completedParent = path4.join(rootDir, "specs", "changes", "completed");
|
|
2162
|
+
if (!fs7.existsSync(completedParent)) fs7.mkdirSync(completedParent, { recursive: true });
|
|
2163
|
+
const destDir = path4.join(completedParent, changeId);
|
|
2164
|
+
if (fs7.existsSync(destDir)) fs7.rmSync(destDir, { recursive: true, force: true });
|
|
2165
|
+
fs7.renameSync(changeDir, destDir);
|
|
2166
|
+
const destProposal = path4.join(destDir, "proposal.md");
|
|
2167
|
+
if (fs7.existsSync(destProposal)) {
|
|
2168
|
+
const { frontmatter, body } = parseFrontmatter(fs7.readFileSync(destProposal, "utf-8"));
|
|
1647
2169
|
frontmatter.status = "applied";
|
|
1648
2170
|
writeUpdatedArtifact(destProposal, frontmatter, body);
|
|
1649
2171
|
}
|
|
1650
2172
|
return destDir;
|
|
1651
2173
|
}
|
|
1652
2174
|
function archiveChangeWorkspace(changeDir, changeId, rootDir, autoArchive, completedAll) {
|
|
1653
|
-
const isActiveFolder = changeDir.includes("specs" +
|
|
2175
|
+
const isActiveFolder = changeDir.includes("specs" + path4.sep + "changes" + path4.sep + "active");
|
|
1654
2176
|
if (completedAll && autoArchive && isActiveFolder) {
|
|
1655
2177
|
const destDir = moveToCompletedFolder(changeDir, changeId, rootDir);
|
|
1656
2178
|
return { archived: true, archivedPath: destDir };
|
|
1657
2179
|
}
|
|
1658
|
-
const proposalFile =
|
|
1659
|
-
if (
|
|
1660
|
-
const { frontmatter, body } = parseFrontmatter(
|
|
2180
|
+
const proposalFile = path4.join(changeDir, "proposal.md");
|
|
2181
|
+
if (fs7.existsSync(proposalFile)) {
|
|
2182
|
+
const { frontmatter, body } = parseFrontmatter(fs7.readFileSync(proposalFile, "utf-8"));
|
|
1661
2183
|
if (completedAll) frontmatter.status = "completed";
|
|
1662
2184
|
writeUpdatedArtifact(proposalFile, frontmatter, body);
|
|
1663
2185
|
}
|
|
@@ -1751,8 +2273,8 @@ function resolveAuditStatus(integratedCount, targetCount, gapsCount) {
|
|
|
1751
2273
|
function auditChangeEntry(changeDir, changeName, canonicalMap) {
|
|
1752
2274
|
const handoff = loadProductHandoffSidecar(changeDir);
|
|
1753
2275
|
if (!handoff) return null;
|
|
1754
|
-
const tasksFile =
|
|
1755
|
-
const tasks =
|
|
2276
|
+
const tasksFile = path4.join(changeDir, "tasks.md");
|
|
2277
|
+
const tasks = fs7.existsSync(tasksFile) ? parseTasksFromContent(fs7.readFileSync(tasksFile, "utf-8")) : [];
|
|
1756
2278
|
const completed = tasks.filter((t) => t.status === "COMPLETED");
|
|
1757
2279
|
const targetReqs = handoff.subgraph?.requirements || [];
|
|
1758
2280
|
const { integrated, missing, gaps } = auditChangeRequirements(targetReqs, canonicalMap);
|
|
@@ -1768,11 +2290,11 @@ function auditChangeEntry(changeDir, changeName, canonicalMap) {
|
|
|
1768
2290
|
};
|
|
1769
2291
|
}
|
|
1770
2292
|
function collectParentDirAudits(parent, canonicalMap, audits, errors) {
|
|
1771
|
-
if (!
|
|
1772
|
-
const entries =
|
|
2293
|
+
if (!fs7.existsSync(parent)) return;
|
|
2294
|
+
const entries = fs7.readdirSync(parent, { withFileTypes: true });
|
|
1773
2295
|
for (const e of entries) {
|
|
1774
2296
|
if (!e.isDirectory()) continue;
|
|
1775
|
-
const audit = auditChangeEntry(
|
|
2297
|
+
const audit = auditChangeEntry(path4.join(parent, e.name), e.name, canonicalMap);
|
|
1776
2298
|
if (audit) {
|
|
1777
2299
|
if (audit.gaps.length > 0) errors.push(`[${e.name}] Desalineaci\xF3n: ${audit.gaps.join("; ")}`);
|
|
1778
2300
|
audits.push(audit);
|
|
@@ -1784,8 +2306,8 @@ function verifySddIntegration(options = {}) {
|
|
|
1784
2306
|
const audits = [];
|
|
1785
2307
|
const errors = [];
|
|
1786
2308
|
const searchDirs = [
|
|
1787
|
-
|
|
1788
|
-
|
|
2309
|
+
path4.join(rootDir, "specs", "changes", "completed"),
|
|
2310
|
+
path4.join(rootDir, "specs", "changes", "active")
|
|
1789
2311
|
];
|
|
1790
2312
|
const canonicalMap = loadCanonicalArtifacts(rootDir);
|
|
1791
2313
|
for (const parent of searchDirs) {
|
|
@@ -1833,10 +2355,10 @@ function resolveMatchFromMetadata(activeFolders, options) {
|
|
|
1833
2355
|
return {};
|
|
1834
2356
|
}
|
|
1835
2357
|
function validateActiveDirectory(activeParent) {
|
|
1836
|
-
if (!
|
|
2358
|
+
if (!fs7.existsSync(activeParent)) {
|
|
1837
2359
|
return { ok: false, folders: [], error: `No existe el directorio de cambios activos en '${activeParent}'.` };
|
|
1838
2360
|
}
|
|
1839
|
-
const entries =
|
|
2361
|
+
const entries = fs7.readdirSync(activeParent, { withFileTypes: true });
|
|
1840
2362
|
const activeFolders = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1841
2363
|
if (activeFolders.length === 0) {
|
|
1842
2364
|
return { ok: false, folders: [], error: "No se encontraron carpetas de cambios activos en specs/changes/active/." };
|
|
@@ -1868,7 +2390,7 @@ function evaluateTasksProgress(changeId, tasks) {
|
|
|
1868
2390
|
}
|
|
1869
2391
|
function detectActiveChangeForIntegration(options = {}) {
|
|
1870
2392
|
const rootDir = options.rootDir || process.cwd();
|
|
1871
|
-
const activeParent =
|
|
2393
|
+
const activeParent = path4.join(rootDir, "specs", "changes", "active");
|
|
1872
2394
|
const dirCheck = validateActiveDirectory(activeParent);
|
|
1873
2395
|
if (!dirCheck.ok) {
|
|
1874
2396
|
return {
|
|
@@ -1883,9 +2405,9 @@ function detectActiveChangeForIntegration(options = {}) {
|
|
|
1883
2405
|
if (!match.changeId) {
|
|
1884
2406
|
return buildUnmatchedActiveResult(dirCheck.folders, options);
|
|
1885
2407
|
}
|
|
1886
|
-
const changeDir =
|
|
1887
|
-
const tasksFile =
|
|
1888
|
-
const tasks =
|
|
2408
|
+
const changeDir = path4.join(activeParent, match.changeId);
|
|
2409
|
+
const tasksFile = path4.join(changeDir, "tasks.md");
|
|
2410
|
+
const tasks = fs7.existsSync(tasksFile) ? parseTasksFromContent(fs7.readFileSync(tasksFile, "utf-8")) : [];
|
|
1889
2411
|
const progress = evaluateTasksProgress(match.changeId, tasks);
|
|
1890
2412
|
const reasons = match.reason ? [match.reason] : [];
|
|
1891
2413
|
if (progress.error) reasons.push(progress.error);
|
|
@@ -2016,9 +2538,9 @@ function isPdacDeliveryDoc(relFile) {
|
|
|
2016
2538
|
}
|
|
2017
2539
|
function processPdacFile(file, rootDir, baselineMap, allCitations) {
|
|
2018
2540
|
try {
|
|
2019
|
-
const content =
|
|
2541
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
2020
2542
|
const { id, type, title } = parseFrontmatterId(content);
|
|
2021
|
-
const relFile =
|
|
2543
|
+
const relFile = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
2022
2544
|
if (id) {
|
|
2023
2545
|
const citations = extractCitations(content, relFile);
|
|
2024
2546
|
baselineMap.set(id, {
|
|
@@ -2146,22 +2668,22 @@ function updateCitationsInContent(content, baselineMap, sourceFile, details) {
|
|
|
2146
2668
|
}
|
|
2147
2669
|
function collectCandidateFilesForSync(rootDir, targetPath) {
|
|
2148
2670
|
if (targetPath) {
|
|
2149
|
-
const fullTarget =
|
|
2150
|
-
if (!
|
|
2151
|
-
return
|
|
2671
|
+
const fullTarget = path4.isAbsolute(targetPath) ? targetPath : path4.join(rootDir, targetPath);
|
|
2672
|
+
if (!fs7.existsSync(fullTarget)) return [];
|
|
2673
|
+
return fs7.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
2152
2674
|
}
|
|
2153
2675
|
return walkMdFiles(rootDir).filter((f) => {
|
|
2154
|
-
const relFile =
|
|
2676
|
+
const relFile = path4.relative(rootDir, f).replace(/\\/g, "/");
|
|
2155
2677
|
return isPdacDeliveryDoc(relFile);
|
|
2156
2678
|
});
|
|
2157
2679
|
}
|
|
2158
2680
|
function syncSingleFile(file, rootDir, baselineMap, details) {
|
|
2159
2681
|
try {
|
|
2160
|
-
const content =
|
|
2161
|
-
const relFile =
|
|
2682
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
2683
|
+
const relFile = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
2162
2684
|
const { updatedContent, changed } = updateCitationsInContent(content, baselineMap, relFile, details);
|
|
2163
2685
|
if (changed) {
|
|
2164
|
-
|
|
2686
|
+
fs7.writeFileSync(file, updatedContent, "utf-8");
|
|
2165
2687
|
return relFile;
|
|
2166
2688
|
}
|
|
2167
2689
|
} catch {
|
|
@@ -2281,7 +2803,7 @@ function extractFrontmatter(content) {
|
|
|
2281
2803
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2282
2804
|
if (!match) return null;
|
|
2283
2805
|
try {
|
|
2284
|
-
const parsed =
|
|
2806
|
+
const parsed = yaml3.load(match[1]);
|
|
2285
2807
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2286
2808
|
return parsed;
|
|
2287
2809
|
}
|
|
@@ -2317,10 +2839,10 @@ var EXCLUDED_SCHEMA_DIRS = /* @__PURE__ */ new Set([
|
|
|
2317
2839
|
]);
|
|
2318
2840
|
function walkHandoffSidecars(dir, fileList = []) {
|
|
2319
2841
|
try {
|
|
2320
|
-
if (!
|
|
2321
|
-
for (const file of
|
|
2322
|
-
const fullPath =
|
|
2323
|
-
if (
|
|
2842
|
+
if (!fs7.existsSync(dir)) return fileList;
|
|
2843
|
+
for (const file of fs7.readdirSync(dir)) {
|
|
2844
|
+
const fullPath = path4.join(dir, file);
|
|
2845
|
+
if (fs7.statSync(fullPath).isDirectory()) {
|
|
2324
2846
|
if (!EXCLUDED_SCHEMA_DIRS.has(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
2325
2847
|
walkHandoffSidecars(fullPath, fileList);
|
|
2326
2848
|
}
|
|
@@ -2335,7 +2857,7 @@ function walkHandoffSidecars(dir, fileList = []) {
|
|
|
2335
2857
|
function extractArtifactData(filePath, content) {
|
|
2336
2858
|
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
|
2337
2859
|
try {
|
|
2338
|
-
const parsed =
|
|
2860
|
+
const parsed = yaml3.load(content);
|
|
2339
2861
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2340
2862
|
} catch {
|
|
2341
2863
|
return null;
|
|
@@ -2484,10 +3006,10 @@ function validateAgainstSchema(value, schema, propPath = "") {
|
|
|
2484
3006
|
return validateTypeSpecificRules(value, schema, propPath, currentProp);
|
|
2485
3007
|
}
|
|
2486
3008
|
function findSchemaFile(relSchemaPath, rootDir) {
|
|
2487
|
-
const fullPath =
|
|
2488
|
-
if (
|
|
2489
|
-
const fallbackPath =
|
|
2490
|
-
if (
|
|
3009
|
+
const fullPath = path4.join(rootDir, relSchemaPath);
|
|
3010
|
+
if (fs7.existsSync(fullPath)) return fullPath;
|
|
3011
|
+
const fallbackPath = path4.join(process.cwd(), relSchemaPath);
|
|
3012
|
+
if (fs7.existsSync(fallbackPath)) return fallbackPath;
|
|
2491
3013
|
return null;
|
|
2492
3014
|
}
|
|
2493
3015
|
function resolveSchemaForArtifact(frontmatter, rootDir) {
|
|
@@ -2497,7 +3019,7 @@ function resolveSchemaForArtifact(frontmatter, rootDir) {
|
|
|
2497
3019
|
const fullPath = findSchemaFile(relSchemaPath, rootDir);
|
|
2498
3020
|
if (!fullPath) return null;
|
|
2499
3021
|
try {
|
|
2500
|
-
const raw =
|
|
3022
|
+
const raw = fs7.readFileSync(fullPath, "utf-8");
|
|
2501
3023
|
return { schemaPath: relSchemaPath, schema: JSON.parse(raw) };
|
|
2502
3024
|
} catch {
|
|
2503
3025
|
return null;
|
|
@@ -2505,15 +3027,15 @@ function resolveSchemaForArtifact(frontmatter, rootDir) {
|
|
|
2505
3027
|
}
|
|
2506
3028
|
function loadSchemaFromPathOrType(schemaOrType, rootDir) {
|
|
2507
3029
|
if (schemaOrType.endsWith(".json")) {
|
|
2508
|
-
const fullPath =
|
|
2509
|
-
if (fullPath &&
|
|
2510
|
-
return { schema: JSON.parse(
|
|
3030
|
+
const fullPath = path4.isAbsolute(schemaOrType) ? schemaOrType : findSchemaFile(schemaOrType, rootDir);
|
|
3031
|
+
if (fullPath && fs7.existsSync(fullPath)) {
|
|
3032
|
+
return { schema: JSON.parse(fs7.readFileSync(fullPath, "utf-8")), schemaId: schemaOrType };
|
|
2511
3033
|
}
|
|
2512
3034
|
} else if (TYPE_TO_SCHEMA_MAP[schemaOrType]) {
|
|
2513
3035
|
const relPath = TYPE_TO_SCHEMA_MAP[schemaOrType];
|
|
2514
3036
|
const fullPath = findSchemaFile(relPath, rootDir);
|
|
2515
|
-
if (fullPath &&
|
|
2516
|
-
return { schema: JSON.parse(
|
|
3037
|
+
if (fullPath && fs7.existsSync(fullPath)) {
|
|
3038
|
+
return { schema: JSON.parse(fs7.readFileSync(fullPath, "utf-8")), schemaId: relPath };
|
|
2517
3039
|
}
|
|
2518
3040
|
}
|
|
2519
3041
|
return { schema: null };
|
|
@@ -2551,9 +3073,9 @@ function collectCandidateSchemaFiles(options, rootDir) {
|
|
|
2551
3073
|
if (!options.targetPath) {
|
|
2552
3074
|
return [...walkMdFiles(rootDir), ...walkHandoffSidecars(rootDir)];
|
|
2553
3075
|
}
|
|
2554
|
-
const target =
|
|
2555
|
-
if (!
|
|
2556
|
-
if (
|
|
3076
|
+
const target = path4.isAbsolute(options.targetPath) ? options.targetPath : path4.join(rootDir, options.targetPath);
|
|
3077
|
+
if (!fs7.existsSync(target)) return [];
|
|
3078
|
+
if (fs7.statSync(target).isDirectory()) {
|
|
2557
3079
|
return [...walkMdFiles(target), ...walkHandoffSidecars(target)];
|
|
2558
3080
|
}
|
|
2559
3081
|
if (target.endsWith(".md") || target.endsWith(".yaml") || target.endsWith(".yml")) {
|
|
@@ -2604,7 +3126,7 @@ function buildEvaluationFailure(relPath, err) {
|
|
|
2604
3126
|
}
|
|
2605
3127
|
function evaluateArtifactFile(file, relPath, rootDir) {
|
|
2606
3128
|
try {
|
|
2607
|
-
const content =
|
|
3129
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
2608
3130
|
const frontmatter = extractArtifactData(file, content);
|
|
2609
3131
|
if (!frontmatter || !frontmatter.id) return null;
|
|
2610
3132
|
const schemaInfo = resolveSchemaForArtifact(frontmatter, rootDir);
|
|
@@ -2639,7 +3161,7 @@ function verifyArtifactsSchemas(options = {}) {
|
|
|
2639
3161
|
let validCount = 0;
|
|
2640
3162
|
let totalEvaluated = 0;
|
|
2641
3163
|
for (const file of filesToScan) {
|
|
2642
|
-
const relPath =
|
|
3164
|
+
const relPath = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
2643
3165
|
if (isIgnoredArtifactPath(relPath, Boolean(options.targetPath))) continue;
|
|
2644
3166
|
const evaluation = evaluateArtifactFile(file, relPath, rootDir);
|
|
2645
3167
|
if (!evaluation) continue;
|
|
@@ -2658,10 +3180,10 @@ function slugify(text) {
|
|
|
2658
3180
|
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2659
3181
|
}
|
|
2660
3182
|
function extractDirMaxNum(dir) {
|
|
2661
|
-
if (!
|
|
3183
|
+
if (!fs7.existsSync(dir)) return 0;
|
|
2662
3184
|
let max = 0;
|
|
2663
3185
|
try {
|
|
2664
|
-
const entries =
|
|
3186
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2665
3187
|
for (const e of entries) {
|
|
2666
3188
|
if (!e.isDirectory()) continue;
|
|
2667
3189
|
const match = e.name.match(/^(?:chg|CHG|patch|PATCH)-(\d+)/i);
|
|
@@ -2678,10 +3200,10 @@ function extractDirMaxNum(dir) {
|
|
|
2678
3200
|
}
|
|
2679
3201
|
function getNextCorrelativeNumber(rootDir) {
|
|
2680
3202
|
const candidateDirs = [
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
3203
|
+
path4.join(rootDir, "specs", "changes", "active"),
|
|
3204
|
+
path4.join(rootDir, "specs", "changes", "completed"),
|
|
3205
|
+
path4.join(rootDir, "specs"),
|
|
3206
|
+
path4.join(rootDir, "openspec", "changes")
|
|
2685
3207
|
];
|
|
2686
3208
|
let maxNum = 0;
|
|
2687
3209
|
for (const dir of candidateDirs) {
|
|
@@ -2711,13 +3233,13 @@ function resolveChangeIdentifiers(rootDir, name, profile, options) {
|
|
|
2711
3233
|
};
|
|
2712
3234
|
}
|
|
2713
3235
|
function ensureEmptyChangeDir(changeDir) {
|
|
2714
|
-
if (
|
|
2715
|
-
const existing =
|
|
3236
|
+
if (fs7.existsSync(changeDir)) {
|
|
3237
|
+
const existing = fs7.readdirSync(changeDir);
|
|
2716
3238
|
if (existing.length > 0) {
|
|
2717
3239
|
return `El directorio del cambio '${changeDir}' ya existe y no est\xE1 vac\xEDo.`;
|
|
2718
3240
|
}
|
|
2719
3241
|
} else {
|
|
2720
|
-
|
|
3242
|
+
fs7.mkdirSync(changeDir, { recursive: true });
|
|
2721
3243
|
}
|
|
2722
3244
|
return null;
|
|
2723
3245
|
}
|
|
@@ -2726,7 +3248,7 @@ function scanCanonicalArtifacts(rootDir) {
|
|
|
2726
3248
|
const artifactMap = /* @__PURE__ */ new Map();
|
|
2727
3249
|
for (const file of allMdFiles) {
|
|
2728
3250
|
try {
|
|
2729
|
-
const content =
|
|
3251
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
2730
3252
|
const { frontmatter } = parseFrontmatter(content);
|
|
2731
3253
|
if (frontmatter?.id) {
|
|
2732
3254
|
const id = String(frontmatter.id);
|
|
@@ -2754,7 +3276,7 @@ function parseInputFromIds(rawFrom) {
|
|
|
2754
3276
|
}
|
|
2755
3277
|
function renderCitationsYamlBlock(citedArtifacts) {
|
|
2756
3278
|
if (citedArtifacts.length === 0) return "";
|
|
2757
|
-
return
|
|
3279
|
+
return yaml3.dump(
|
|
2758
3280
|
citedArtifacts.map((c) => ({ id: c.id, digest: c.digest, comment: c.comment })),
|
|
2759
3281
|
{ indent: 2, lineWidth: -1 }
|
|
2760
3282
|
).trim().split("\n").map((line) => ` ${line}`).join("\n");
|
|
@@ -2789,16 +3311,39 @@ El sistema DEBE implementar y soportar la capacidad de {{NAME}}.
|
|
|
2789
3311
|
## 2. Criterios de Aceptaci\xF3n en Formato Gherkin (Cucumber)
|
|
2790
3312
|
|
|
2791
3313
|
\`\`\`gherkin
|
|
2792
|
-
@{{REQ_ID}} @automated
|
|
3314
|
+
@{{REQ_ID}} @automated @regression
|
|
2793
3315
|
Feature: {{NAME}}
|
|
2794
3316
|
As a user or client system
|
|
2795
3317
|
I want to execute the capability for {{NAME}}
|
|
2796
3318
|
So that the expected outcome and value are achieved
|
|
2797
3319
|
|
|
3320
|
+
@nominal
|
|
2798
3321
|
Scenario: Nominal successful flow
|
|
2799
3322
|
Given the system is in an operational state
|
|
2800
3323
|
When the operation for "{{NAME}}" is executed
|
|
2801
3324
|
Then the operation completes successfully
|
|
3325
|
+
|
|
3326
|
+
@boundary
|
|
3327
|
+
Scenario Outline: Boundary limits validation
|
|
3328
|
+
Given the system receives boundary input "<boundary_input>"
|
|
3329
|
+
When the operation for "{{NAME}}" is evaluated at range limits
|
|
3330
|
+
Then the system responds with expected boundary status "<expected_status>"
|
|
3331
|
+
|
|
3332
|
+
Examples:
|
|
3333
|
+
| boundary_input | expected_status |
|
|
3334
|
+
| min_bound | 200 |
|
|
3335
|
+
| max_bound | 200 |
|
|
3336
|
+
|
|
3337
|
+
@invalid
|
|
3338
|
+
Scenario Outline: Out-of-range and invalid payload rejection
|
|
3339
|
+
Given the system receives invalid or out-of-range input "<invalid_input>"
|
|
3340
|
+
When the operation for "{{NAME}}" is invoked
|
|
3341
|
+
Then the system rejects the request with error "<error_code>"
|
|
3342
|
+
|
|
3343
|
+
Examples:
|
|
3344
|
+
| invalid_input | error_code |
|
|
3345
|
+
| empty_payload | ERR_INVALID_ARGS |
|
|
3346
|
+
| overflow_val | ERR_OUT_OF_RANGE |
|
|
2802
3347
|
\`\`\`
|
|
2803
3348
|
|
|
2804
3349
|
---
|
|
@@ -2817,12 +3362,33 @@ Definici\xF3n de artefacto citado en {{CANONICAL_ID}}.
|
|
|
2817
3362
|
## 2. Criterios de Aceptaci\xF3n en Formato Gherkin (Cucumber)
|
|
2818
3363
|
|
|
2819
3364
|
\`\`\`gherkin
|
|
2820
|
-
@{{ID}} @automated
|
|
3365
|
+
@{{ID}} @automated @regression
|
|
2821
3366
|
Feature: {{NAME}}
|
|
3367
|
+
@nominal
|
|
2822
3368
|
Scenario: Expected behavior verification
|
|
2823
3369
|
Given the system is initialized for "{{NAME}}"
|
|
2824
3370
|
When the request associated with {{ID}} is processed
|
|
2825
3371
|
Then the response satisfies acceptance criteria
|
|
3372
|
+
|
|
3373
|
+
@boundary
|
|
3374
|
+
Scenario Outline: Boundary limits verification
|
|
3375
|
+
Given boundary input "<input>" for {{ID}}
|
|
3376
|
+
When the request is processed
|
|
3377
|
+
Then the response status is "<status>"
|
|
3378
|
+
|
|
3379
|
+
Examples:
|
|
3380
|
+
| input | status |
|
|
3381
|
+
| min | 200 |
|
|
3382
|
+
|
|
3383
|
+
@invalid
|
|
3384
|
+
Scenario Outline: Invalid input rejection
|
|
3385
|
+
Given invalid input "<input>" for {{ID}}
|
|
3386
|
+
When the request is processed
|
|
3387
|
+
Then the request is rejected with "<error>"
|
|
3388
|
+
|
|
3389
|
+
Examples:
|
|
3390
|
+
| input | error |
|
|
3391
|
+
| null | ERR_INVALID_ARGS |
|
|
2826
3392
|
\`\`\`
|
|
2827
3393
|
`;
|
|
2828
3394
|
var SPEC_BODY = `# Delivery Specification: {{CANONICAL_ID}}
|
|
@@ -2836,10 +3402,33 @@ Feature: {{NAME}}
|
|
|
2836
3402
|
I want to process the operation for {{NAME}}
|
|
2837
3403
|
So that delivery criteria are satisfied
|
|
2838
3404
|
|
|
3405
|
+
@nominal
|
|
2839
3406
|
Scenario: Nominal successful flow
|
|
2840
3407
|
Given the system is in a nominal operational state
|
|
2841
3408
|
When the operation for "{{NAME}}" is processed
|
|
2842
3409
|
Then the operation completes successfully satisfying acceptance criteria
|
|
3410
|
+
|
|
3411
|
+
@boundary
|
|
3412
|
+
Scenario Outline: Boundary conditions verification
|
|
3413
|
+
Given the system is configured with boundary value "<value>"
|
|
3414
|
+
When the operation for "{{NAME}}" is processed
|
|
3415
|
+
Then the boundary result matches "<expected>"
|
|
3416
|
+
|
|
3417
|
+
Examples:
|
|
3418
|
+
| value | expected |
|
|
3419
|
+
| min | accepted |
|
|
3420
|
+
| max | accepted |
|
|
3421
|
+
|
|
3422
|
+
@invalid
|
|
3423
|
+
Scenario Outline: Out-of-range and invalid input handling
|
|
3424
|
+
Given an invalid or malformed payload "<payload>"
|
|
3425
|
+
When the operation for "{{NAME}}" is attempted
|
|
3426
|
+
Then the system rejects the operation with "<error>"
|
|
3427
|
+
|
|
3428
|
+
Examples:
|
|
3429
|
+
| payload | error |
|
|
3430
|
+
| null | ERR_INVALID_DATA |
|
|
3431
|
+
| overflow | ERR_OUT_OF_RANGE |
|
|
2843
3432
|
\`\`\`
|
|
2844
3433
|
|
|
2845
3434
|
---
|
|
@@ -2944,6 +3533,7 @@ derives-from:
|
|
|
2944
3533
|
- {{UC_ID}}
|
|
2945
3534
|
verifiable-by: cucumber-bdd
|
|
2946
3535
|
acceptance-format: gherkin
|
|
3536
|
+
enforce-bdd-variants: true
|
|
2947
3537
|
cucumber-tags:
|
|
2948
3538
|
- "@{{REQ_ID}}"
|
|
2949
3539
|
- "@automated"
|
|
@@ -2961,6 +3551,7 @@ schema-version: "1.0"
|
|
|
2961
3551
|
category: functional
|
|
2962
3552
|
verifiable-by: cucumber-bdd
|
|
2963
3553
|
acceptance-format: gherkin
|
|
3554
|
+
enforce-bdd-variants: true
|
|
2964
3555
|
cucumber-tags:
|
|
2965
3556
|
- "@{{ID}}"
|
|
2966
3557
|
- "@automated"
|
|
@@ -3036,7 +3627,7 @@ function buildPatchSuccess(slug, canonicalId, specPath, citedArtifacts, errors)
|
|
|
3036
3627
|
changeId: slug,
|
|
3037
3628
|
canonicalId,
|
|
3038
3629
|
profile: "patch",
|
|
3039
|
-
changeDir:
|
|
3630
|
+
changeDir: path4.dirname(specPath),
|
|
3040
3631
|
createdFiles: [specPath],
|
|
3041
3632
|
citedArtifacts,
|
|
3042
3633
|
errors
|
|
@@ -3047,10 +3638,10 @@ function scaffoldPatchChange(rootDir, changeDir, slug, canonicalId, name, inputF
|
|
|
3047
3638
|
const citationsSection = citedArtifacts.length > 0 ? `citations:
|
|
3048
3639
|
${renderCitationsYamlBlock(citedArtifacts)}
|
|
3049
3640
|
` : "";
|
|
3050
|
-
const specPath =
|
|
3641
|
+
const specPath = path4.join(changeDir, "spec.md");
|
|
3051
3642
|
const specFmObj = buildPatchFmObj(canonicalId);
|
|
3052
3643
|
const specContent = buildPatchSpecContent(canonicalId, name, citationsSection);
|
|
3053
|
-
|
|
3644
|
+
fs7.writeFileSync(specPath, specContent, "utf-8");
|
|
3054
3645
|
const errors = validatePatchArtifacts(rootDir, changeDir, specContent, specFmObj, adapter);
|
|
3055
3646
|
return buildPatchSuccess(slug, canonicalId, specPath, citedArtifacts, errors);
|
|
3056
3647
|
}
|
|
@@ -3064,26 +3655,26 @@ function createGreenfieldRequirement(rootDir, name, padNum, canonicalId, author,
|
|
|
3064
3655
|
const nameSlugUpper = slugify(name).toUpperCase();
|
|
3065
3656
|
const reqId = `FR-${padNum}-${nameSlugUpper}-001`;
|
|
3066
3657
|
const ucId = `UC-${padNum}-${nameSlugUpper}`;
|
|
3067
|
-
const productDir =
|
|
3068
|
-
if (!
|
|
3069
|
-
const reqFilePath =
|
|
3070
|
-
if (
|
|
3658
|
+
const productDir = path4.join(rootDir, "specs", "product");
|
|
3659
|
+
if (!fs7.existsSync(productDir)) fs7.mkdirSync(productDir, { recursive: true });
|
|
3660
|
+
const reqFilePath = path4.join(productDir, `${reqId}.md`);
|
|
3661
|
+
if (fs7.existsSync(reqFilePath)) return null;
|
|
3071
3662
|
const reqContent = buildGreenfieldContent(reqId, ucId, name, canonicalId, author, dateStr);
|
|
3072
|
-
|
|
3663
|
+
fs7.writeFileSync(reqFilePath, reqContent, "utf-8");
|
|
3073
3664
|
createdFiles.push(reqFilePath);
|
|
3074
3665
|
const digest = computeCanonicalSha256(reqContent);
|
|
3075
3666
|
return { reqId, ucId, reqFilePath, digest };
|
|
3076
3667
|
}
|
|
3077
3668
|
function createMissingDraftRequirement(rootDir, name, id, canonicalId, createdFiles) {
|
|
3078
|
-
const productDir =
|
|
3079
|
-
if (!
|
|
3080
|
-
const reqFilePath =
|
|
3669
|
+
const productDir = path4.join(rootDir, "specs", "product");
|
|
3670
|
+
if (!fs7.existsSync(productDir)) fs7.mkdirSync(productDir, { recursive: true });
|
|
3671
|
+
const reqFilePath = path4.join(productDir, `${id}.md`);
|
|
3081
3672
|
const reqType = id.startsWith("UC-") ? "use-case" : id.startsWith("SEC-REQ-") ? "security-requirement" : "requirement";
|
|
3082
3673
|
const header = MISSING_DRAFT_HEADER.replaceAll("{{ID}}", id).replaceAll("{{TYPE}}", reqType).replaceAll("{{NAME}}", name);
|
|
3083
3674
|
const body = MISSING_DRAFT_BODY.replaceAll("{{ID}}", id).replaceAll("{{NAME}}", name).replaceAll("{{CANONICAL_ID}}", canonicalId);
|
|
3084
3675
|
const reqContent = `${header}
|
|
3085
3676
|
${body}`;
|
|
3086
|
-
|
|
3677
|
+
fs7.writeFileSync(reqFilePath, reqContent, "utf-8");
|
|
3087
3678
|
createdFiles.push(reqFilePath);
|
|
3088
3679
|
const digest = computeCanonicalSha256(reqContent);
|
|
3089
3680
|
return { reqFilePath, digest };
|
|
@@ -3171,7 +3762,7 @@ function collectChangeCitations(rootDir, name, padNum, canonicalId, author, date
|
|
|
3171
3762
|
return col;
|
|
3172
3763
|
}
|
|
3173
3764
|
function renderProposalFile(changeDir, canonicalId, name, author, citationsYamlBlock) {
|
|
3174
|
-
const proposalPath =
|
|
3765
|
+
const proposalPath = path4.join(changeDir, "proposal.md");
|
|
3175
3766
|
const citationsSection = citationsYamlBlock ? `citations:
|
|
3176
3767
|
${citationsYamlBlock}
|
|
3177
3768
|
` : "";
|
|
@@ -3191,11 +3782,11 @@ ${name}
|
|
|
3191
3782
|
## 2. Dependencias Externas Evaluadas
|
|
3192
3783
|
- Lista de dependencias nuevas a incorporar y verificaci\xF3n de licencia seg\xFAn \`license-policy.yaml\`.
|
|
3193
3784
|
`;
|
|
3194
|
-
|
|
3785
|
+
fs7.writeFileSync(proposalPath, proposalContent, "utf-8");
|
|
3195
3786
|
return proposalPath;
|
|
3196
3787
|
}
|
|
3197
3788
|
function renderSpecFile(changeDir, canonicalId, name, profile) {
|
|
3198
|
-
const specPath =
|
|
3789
|
+
const specPath = path4.join(changeDir, "spec.md");
|
|
3199
3790
|
const specFmObj = {
|
|
3200
3791
|
id: `SPEC-${canonicalId}`,
|
|
3201
3792
|
type: "delivery-spec",
|
|
@@ -3216,12 +3807,12 @@ cucumber-tags:
|
|
|
3216
3807
|
---
|
|
3217
3808
|
`;
|
|
3218
3809
|
const body = SPEC_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name);
|
|
3219
|
-
|
|
3810
|
+
fs7.writeFileSync(specPath, `${header}
|
|
3220
3811
|
${body}`, "utf-8");
|
|
3221
3812
|
return { specPath, specFmObj };
|
|
3222
3813
|
}
|
|
3223
3814
|
function renderDesignFile(changeDir, canonicalId, name, author, dateStr, citationsYamlBlock) {
|
|
3224
|
-
const designPath =
|
|
3815
|
+
const designPath = path4.join(changeDir, "design.md");
|
|
3225
3816
|
const citationsSection = citationsYamlBlock ? `citations:
|
|
3226
3817
|
${citationsYamlBlock}
|
|
3227
3818
|
` : "";
|
|
@@ -3236,14 +3827,20 @@ handoff: "HOF-${canonicalId}"
|
|
|
3236
3827
|
architecture-component: "CMP-CORE"
|
|
3237
3828
|
enclave: "SEC-ENC-DMZ"
|
|
3238
3829
|
status: draft
|
|
3830
|
+
contracts: []
|
|
3239
3831
|
${citationsSection}---
|
|
3240
3832
|
`;
|
|
3241
3833
|
const body = DESIGN_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name).replaceAll("{{AUTHOR}}", author).replaceAll("{{DATE}}", dateStr);
|
|
3242
|
-
|
|
3834
|
+
fs7.writeFileSync(designPath, `${header}
|
|
3243
3835
|
${body}`, "utf-8");
|
|
3244
3836
|
return designPath;
|
|
3245
3837
|
}
|
|
3246
|
-
function buildTasksFrontmatter(canonicalId, name) {
|
|
3838
|
+
function buildTasksFrontmatter(canonicalId, name, reqIds) {
|
|
3839
|
+
const tasks = DEFAULT_TASKS_ARRAY.map((t) => ({
|
|
3840
|
+
...t,
|
|
3841
|
+
"implements-components": ["CMP-CORE"],
|
|
3842
|
+
...reqIds.length > 0 ? { "satisfies-requirements": reqIds } : {}
|
|
3843
|
+
}));
|
|
3247
3844
|
return {
|
|
3248
3845
|
id: `TSK-PLAN-${canonicalId}`,
|
|
3249
3846
|
type: "task-plan",
|
|
@@ -3253,20 +3850,20 @@ function buildTasksFrontmatter(canonicalId, name) {
|
|
|
3253
3850
|
"schema-version": "1.0",
|
|
3254
3851
|
status: "draft",
|
|
3255
3852
|
"governance-summary": GOVERNANCE_SUMMARY_DEFAULT,
|
|
3256
|
-
tasks
|
|
3853
|
+
tasks,
|
|
3257
3854
|
supersedes: null,
|
|
3258
3855
|
"superseded-by": null
|
|
3259
3856
|
};
|
|
3260
3857
|
}
|
|
3261
|
-
function renderTasksFile(changeDir, canonicalId, name, author, dateStr) {
|
|
3262
|
-
const tasksPath =
|
|
3263
|
-
const tasksFm = buildTasksFrontmatter(canonicalId, name);
|
|
3858
|
+
function renderTasksFile(changeDir, canonicalId, name, author, dateStr, reqIds) {
|
|
3859
|
+
const tasksPath = path4.join(changeDir, "tasks.md");
|
|
3860
|
+
const tasksFm = buildTasksFrontmatter(canonicalId, name, reqIds);
|
|
3264
3861
|
const header = `---
|
|
3265
|
-
${
|
|
3862
|
+
${yaml3.dump(tasksFm, { indent: 2, lineWidth: -1 }).trim()}
|
|
3266
3863
|
---
|
|
3267
3864
|
`;
|
|
3268
3865
|
const body = TASKS_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name).replaceAll("{{AUTHOR}}", author).replaceAll("{{DATE}}", dateStr);
|
|
3269
|
-
|
|
3866
|
+
fs7.writeFileSync(tasksPath, `${header}
|
|
3270
3867
|
${body}`, "utf-8");
|
|
3271
3868
|
return { tasksPath, tasksFm };
|
|
3272
3869
|
}
|
|
@@ -3318,12 +3915,14 @@ function buildScaffoldFailure(profile, error, slug = "", canonicalId = "", chang
|
|
|
3318
3915
|
errors: [error]
|
|
3319
3916
|
};
|
|
3320
3917
|
}
|
|
3321
|
-
function renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile,
|
|
3918
|
+
function renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile, citations, createdFiles) {
|
|
3919
|
+
const citationsBlock = renderCitationsYamlBlock(citations.citedArtifacts);
|
|
3920
|
+
const reqIds = [...citations.subRequirements, ...citations.subSecReqs];
|
|
3322
3921
|
createdFiles.push(renderProposalFile(changeDir, canonicalId, name, author, citationsBlock));
|
|
3323
3922
|
const { specPath, specFmObj } = renderSpecFile(changeDir, canonicalId, name, profile);
|
|
3324
3923
|
createdFiles.push(specPath);
|
|
3325
3924
|
createdFiles.push(renderDesignFile(changeDir, canonicalId, name, author, dateStr, citationsBlock));
|
|
3326
|
-
const { tasksPath, tasksFm } = renderTasksFile(changeDir, canonicalId, name, author, dateStr);
|
|
3925
|
+
const { tasksPath, tasksFm } = renderTasksFile(changeDir, canonicalId, name, author, dateStr, reqIds);
|
|
3327
3926
|
createdFiles.push(tasksPath);
|
|
3328
3927
|
return { specFmObj, tasksFm };
|
|
3329
3928
|
}
|
|
@@ -3343,8 +3942,7 @@ function buildScaffoldSuccess(slug, canonicalId, profile, changeDir, createdFile
|
|
|
3343
3942
|
function executeStandardScaffolding(rootDir, changeDir, slug, canonicalId, name, padNum, author, dateStr, profile, framework, inputFromIds, artifactMap, adapter) {
|
|
3344
3943
|
const createdFiles = [];
|
|
3345
3944
|
const citations = collectChangeCitations(rootDir, name, padNum, canonicalId, author, dateStr, inputFromIds, artifactMap, createdFiles);
|
|
3346
|
-
const
|
|
3347
|
-
const { specFmObj, tasksFm } = renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile, citationsBlock, createdFiles);
|
|
3945
|
+
const { specFmObj, tasksFm } = renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile, citations, createdFiles);
|
|
3348
3946
|
const { depositedPath, handoff } = renderHandoffSidecar(rootDir, slug, canonicalId, name, framework, citations);
|
|
3349
3947
|
createdFiles.push(depositedPath);
|
|
3350
3948
|
const errors = validateScaffoldedArtifacts(rootDir, changeDir, adapter, specFmObj, tasksFm, handoff);
|
|
@@ -3441,7 +4039,7 @@ function resolveCiEnvironment(env = process.env) {
|
|
|
3441
4039
|
};
|
|
3442
4040
|
}
|
|
3443
4041
|
function updateExistingEnvFile(filePath, envString, uppercaseKey) {
|
|
3444
|
-
const content =
|
|
4042
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
3445
4043
|
const lines = content.split("\n");
|
|
3446
4044
|
let found = false;
|
|
3447
4045
|
const newLines = lines.map((line) => {
|
|
@@ -3452,15 +4050,15 @@ function updateExistingEnvFile(filePath, envString, uppercaseKey) {
|
|
|
3452
4050
|
return line;
|
|
3453
4051
|
});
|
|
3454
4052
|
if (!found) newLines.push(envString);
|
|
3455
|
-
|
|
4053
|
+
fs7.writeFileSync(filePath, newLines.filter((l) => l.trim() !== "").join("\n") + "\n", "utf-8");
|
|
3456
4054
|
}
|
|
3457
4055
|
function writeEnvOutputFile(envOutputFile, key, value) {
|
|
3458
4056
|
const uppercaseKey = key.toUpperCase();
|
|
3459
4057
|
const envString = `${uppercaseKey}=${value}`;
|
|
3460
|
-
if (
|
|
4058
|
+
if (fs7.existsSync(envOutputFile)) {
|
|
3461
4059
|
updateExistingEnvFile(envOutputFile, envString, uppercaseKey);
|
|
3462
4060
|
} else {
|
|
3463
|
-
|
|
4061
|
+
fs7.writeFileSync(envOutputFile, envString + "\n", "utf-8");
|
|
3464
4062
|
}
|
|
3465
4063
|
}
|
|
3466
4064
|
function resolveEnvOutputFilePath(env) {
|
|
@@ -3470,8 +4068,8 @@ function resolveEnvOutputFilePath(env) {
|
|
|
3470
4068
|
return void 0;
|
|
3471
4069
|
}
|
|
3472
4070
|
function emitCiOutput(key, value, env = process.env) {
|
|
3473
|
-
if (env.GITHUB_OUTPUT &&
|
|
3474
|
-
|
|
4071
|
+
if (env.GITHUB_OUTPUT && fs7.existsSync(env.GITHUB_OUTPUT)) {
|
|
4072
|
+
fs7.appendFileSync(env.GITHUB_OUTPUT, `${key}=${value}
|
|
3475
4073
|
`, "utf-8");
|
|
3476
4074
|
}
|
|
3477
4075
|
if (env.TF_BUILD === "true") {
|
|
@@ -3513,11 +4111,11 @@ function isIgnoredEntry(name) {
|
|
|
3513
4111
|
return name === "changes" || name === "active" || name === "node_modules";
|
|
3514
4112
|
}
|
|
3515
4113
|
function scanParentDirectoryHandoffs(parentDir, handoffsMap) {
|
|
3516
|
-
if (!
|
|
3517
|
-
const entries =
|
|
4114
|
+
if (!fs7.existsSync(parentDir)) return;
|
|
4115
|
+
const entries = fs7.readdirSync(parentDir, { withFileTypes: true });
|
|
3518
4116
|
for (const entry of entries) {
|
|
3519
4117
|
if (!entry.isDirectory() || isIgnoredEntry(entry.name)) continue;
|
|
3520
|
-
const changeDir =
|
|
4118
|
+
const changeDir = path4.join(parentDir, entry.name);
|
|
3521
4119
|
if (handooffsHasDir(handoffsMap, changeDir)) continue;
|
|
3522
4120
|
const handoff = loadProductHandoffSidecar(changeDir);
|
|
3523
4121
|
if (handoff?.id) {
|
|
@@ -3528,10 +4126,10 @@ function scanParentDirectoryHandoffs(parentDir, handoffsMap) {
|
|
|
3528
4126
|
function scanAllProductHandoffs(rootDir) {
|
|
3529
4127
|
const handoffsMap = /* @__PURE__ */ new Map();
|
|
3530
4128
|
const candidateParentDirs = [
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
4129
|
+
path4.join(rootDir, "specs", "changes", "active"),
|
|
4130
|
+
path4.join(rootDir, "specs", "changes", "completed"),
|
|
4131
|
+
path4.join(rootDir, "specs"),
|
|
4132
|
+
path4.join(rootDir, "openspec", "changes")
|
|
3535
4133
|
];
|
|
3536
4134
|
for (const parentDir of candidateParentDirs) {
|
|
3537
4135
|
scanParentDirectoryHandoffs(parentDir, handoffsMap);
|
|
@@ -3539,9 +4137,9 @@ function scanAllProductHandoffs(rootDir) {
|
|
|
3539
4137
|
return handoffsMap;
|
|
3540
4138
|
}
|
|
3541
4139
|
function handooffsHasDir(map, dir) {
|
|
3542
|
-
const norm =
|
|
4140
|
+
const norm = path4.resolve(dir);
|
|
3543
4141
|
for (const key of map.keys()) {
|
|
3544
|
-
if (
|
|
4142
|
+
if (path4.resolve(key) === norm) return true;
|
|
3545
4143
|
}
|
|
3546
4144
|
return false;
|
|
3547
4145
|
}
|
|
@@ -3551,7 +4149,7 @@ function parseFrontmatter(content) {
|
|
|
3551
4149
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
3552
4150
|
if (!match) return { frontmatter: {}, body: content };
|
|
3553
4151
|
try {
|
|
3554
|
-
const parsed =
|
|
4152
|
+
const parsed = yaml3.load(match[1]);
|
|
3555
4153
|
const frontmatter = parsed && typeof parsed === "object" ? parsed : {};
|
|
3556
4154
|
return { frontmatter, body: content.substring(match[0].length) };
|
|
3557
4155
|
} catch {
|
|
@@ -3638,9 +4236,9 @@ function isCodeTestFile(name) {
|
|
|
3638
4236
|
}
|
|
3639
4237
|
function indexFeatureFile(fullPath, rootDir, map) {
|
|
3640
4238
|
try {
|
|
3641
|
-
const content =
|
|
4239
|
+
const content = fs7.readFileSync(fullPath, "utf-8");
|
|
3642
4240
|
const tags = extractFeatureTags(content);
|
|
3643
|
-
const relFeature =
|
|
4241
|
+
const relFeature = path4.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
3644
4242
|
for (const tag of tags) {
|
|
3645
4243
|
const existing = map.get(tag) || [];
|
|
3646
4244
|
if (!existing.includes(relFeature)) existing.push(relFeature);
|
|
@@ -3650,11 +4248,11 @@ function indexFeatureFile(fullPath, rootDir, map) {
|
|
|
3650
4248
|
}
|
|
3651
4249
|
}
|
|
3652
4250
|
function scanFeatureFiles(dir, rootDir, map) {
|
|
3653
|
-
if (!
|
|
4251
|
+
if (!fs7.existsSync(dir)) return;
|
|
3654
4252
|
try {
|
|
3655
|
-
const entries =
|
|
4253
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
3656
4254
|
for (const e of entries) {
|
|
3657
|
-
const fullPath =
|
|
4255
|
+
const fullPath = path4.join(dir, e.name);
|
|
3658
4256
|
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
3659
4257
|
scanFeatureFiles(fullPath, rootDir, map);
|
|
3660
4258
|
} else if (e.name.endsWith(".feature")) {
|
|
@@ -3666,8 +4264,8 @@ function scanFeatureFiles(dir, rootDir, map) {
|
|
|
3666
4264
|
}
|
|
3667
4265
|
function indexCodeTestFile(fullPath, rootDir, reqIds, map) {
|
|
3668
4266
|
try {
|
|
3669
|
-
const content =
|
|
3670
|
-
const relTest =
|
|
4267
|
+
const content = fs7.readFileSync(fullPath, "utf-8");
|
|
4268
|
+
const relTest = path4.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
3671
4269
|
for (const artId of reqIds) {
|
|
3672
4270
|
if (content.includes(artId)) {
|
|
3673
4271
|
const existing = map.get(artId) || [];
|
|
@@ -3679,11 +4277,11 @@ function indexCodeTestFile(fullPath, rootDir, reqIds, map) {
|
|
|
3679
4277
|
}
|
|
3680
4278
|
}
|
|
3681
4279
|
function scanCodeTestFiles(dir, rootDir, reqIds, map) {
|
|
3682
|
-
if (!
|
|
4280
|
+
if (!fs7.existsSync(dir)) return;
|
|
3683
4281
|
try {
|
|
3684
|
-
const entries =
|
|
4282
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
3685
4283
|
for (const e of entries) {
|
|
3686
|
-
const fullPath =
|
|
4284
|
+
const fullPath = path4.join(dir, e.name);
|
|
3687
4285
|
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
3688
4286
|
scanCodeTestFiles(fullPath, rootDir, reqIds, map);
|
|
3689
4287
|
} else if (isCodeTestFile(e.name)) {
|
|
@@ -3717,9 +4315,9 @@ function isArchitectureViewFile(relFile, frontmatter) {
|
|
|
3717
4315
|
}
|
|
3718
4316
|
function processSingleMdFile(file, rootDir, artifactMap, componentsList, architectureViews) {
|
|
3719
4317
|
try {
|
|
3720
|
-
const content =
|
|
4318
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
3721
4319
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
3722
|
-
const relFile =
|
|
4320
|
+
const relFile = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
3723
4321
|
if (frontmatter.id) {
|
|
3724
4322
|
artifactMap.set(frontmatter.id, {
|
|
3725
4323
|
file: relFile,
|
|
@@ -3880,7 +4478,7 @@ function collectArchitectureViewTraces(reqId, architectureViews) {
|
|
|
3880
4478
|
const traces = [];
|
|
3881
4479
|
for (const view of architectureViews) {
|
|
3882
4480
|
if (view.content.includes(reqId)) {
|
|
3883
|
-
const viewIdentifier = isEntityWithId(view) && view.id ? view.id :
|
|
4481
|
+
const viewIdentifier = isEntityWithId(view) && view.id ? view.id : path4.basename(view.file);
|
|
3884
4482
|
traces.push(viewIdentifier);
|
|
3885
4483
|
}
|
|
3886
4484
|
}
|
|
@@ -3962,33 +4560,115 @@ function evaluateAllRequirements(requirements, ctx) {
|
|
|
3962
4560
|
}
|
|
3963
4561
|
return { matrixRows, orphans, errorCount };
|
|
3964
4562
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
4563
|
+
var SOURCE_CODE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".java", ".cs", ".rs"];
|
|
4564
|
+
function isProductionSourceFile(name) {
|
|
4565
|
+
if (isCodeTestFile(name) || name.endsWith(".d.ts")) return false;
|
|
4566
|
+
return SOURCE_CODE_EXTS.some((ext) => name.endsWith(ext));
|
|
4567
|
+
}
|
|
4568
|
+
function collectCodeAndTestCorpora(dir, prodChunks = [], testChunks = []) {
|
|
4569
|
+
if (!fs7.existsSync(dir)) return { prodCorpus: "", testCorpus: "" };
|
|
4570
|
+
try {
|
|
4571
|
+
for (const e of fs7.readdirSync(dir, { withFileTypes: true })) {
|
|
4572
|
+
const fullPath = path4.join(dir, e.name);
|
|
4573
|
+
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
4574
|
+
collectCodeAndTestCorpora(fullPath, prodChunks, testChunks);
|
|
4575
|
+
} else if (isCodeTestFile(e.name) || e.name.endsWith(".feature")) {
|
|
4576
|
+
testChunks.push(fs7.readFileSync(fullPath, "utf-8"));
|
|
4577
|
+
} else if (isProductionSourceFile(e.name)) {
|
|
4578
|
+
prodChunks.push(fs7.readFileSync(fullPath, "utf-8"));
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
} catch {
|
|
4582
|
+
}
|
|
4583
|
+
return { prodCorpus: prodChunks.join("\n"), testCorpus: testChunks.join("\n") };
|
|
4584
|
+
}
|
|
4585
|
+
function parseStringContractEntry(designId, defaultComp, raw) {
|
|
4586
|
+
const trimmed = raw.trim();
|
|
4587
|
+
if (!trimmed) return null;
|
|
4588
|
+
const parts = trimmed.split(".");
|
|
4589
|
+
const method = parts.pop();
|
|
4590
|
+
const interfaceName = parts.length > 0 ? parts.join(".") : void 0;
|
|
4591
|
+
return { designId, component: defaultComp, interfaceName, method };
|
|
4592
|
+
}
|
|
4593
|
+
function parseObjectContractEntries(designId, defaultComp, item) {
|
|
4594
|
+
if (!Array.isArray(item.methods)) return [];
|
|
4595
|
+
const component = item.component || defaultComp;
|
|
4596
|
+
return item.methods.filter((m) => typeof m === "string" && Boolean(m.trim())).map((m) => ({ designId, component, interfaceName: item.interface, method: m.trim() }));
|
|
4597
|
+
}
|
|
4598
|
+
function expandContractDeclarations(designId, defaultComp, rawContracts) {
|
|
4599
|
+
const expanded = [];
|
|
4600
|
+
for (const item of rawContracts) {
|
|
4601
|
+
if (typeof item === "string") {
|
|
4602
|
+
const parsed = parseStringContractEntry(designId, defaultComp, item);
|
|
4603
|
+
if (parsed) expanded.push(parsed);
|
|
4604
|
+
} else if (item && typeof item === "object") {
|
|
4605
|
+
expanded.push(...parseObjectContractEntries(designId, defaultComp, item));
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
return expanded;
|
|
4609
|
+
}
|
|
4610
|
+
function evaluateSingleContractMethod(entry, prodCorpus, testCorpus) {
|
|
4611
|
+
const symbol = entry.method.includes(".") ? entry.method.split(".").pop() : entry.method;
|
|
4612
|
+
const implementedInCode = prodCorpus.includes(symbol);
|
|
4613
|
+
const testedInSuite = testCorpus.includes(symbol);
|
|
4614
|
+
const status = !implementedInCode ? "MISSING_IMPLEMENTATION" : !testedInSuite ? "MISSING_UNIT_TEST" : "COMPLIANT";
|
|
4615
|
+
return {
|
|
4616
|
+
designId: entry.designId,
|
|
4617
|
+
component: entry.component,
|
|
4618
|
+
interfaceName: entry.interfaceName,
|
|
4619
|
+
method: entry.method,
|
|
4620
|
+
implementedInCode,
|
|
4621
|
+
testedInSuite,
|
|
4622
|
+
status
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
function auditDesignContracts(rootDir, artifactMap) {
|
|
4626
|
+
const pending = [];
|
|
4627
|
+
for (const [id, entry] of artifactMap.entries()) {
|
|
4628
|
+
if (entry.file.includes("templates/") || entry.file.includes(".template.")) continue;
|
|
4629
|
+
const rawContracts = entry.frontmatter.contracts;
|
|
4630
|
+
if (!Array.isArray(rawContracts) || rawContracts.length === 0) continue;
|
|
4631
|
+
const defaultComp = typeof entry.frontmatter["architecture-component"] === "string" ? entry.frontmatter["architecture-component"] : void 0;
|
|
4632
|
+
pending.push(...expandContractDeclarations(id, defaultComp, rawContracts));
|
|
4633
|
+
}
|
|
4634
|
+
if (pending.length === 0) {
|
|
4635
|
+
return { contractAudits: [], missingContractsCount: 0 };
|
|
4636
|
+
}
|
|
4637
|
+
const { prodCorpus, testCorpus } = collectCodeAndTestCorpora(rootDir);
|
|
4638
|
+
const contractAudits = pending.map((p) => evaluateSingleContractMethod(p, prodCorpus, testCorpus));
|
|
4639
|
+
const missingContractsCount = contractAudits.filter((c) => c.status !== "COMPLIANT").length;
|
|
4640
|
+
return { contractAudits, missingContractsCount };
|
|
4641
|
+
}
|
|
4642
|
+
function buildTraceabilityContext(rootDir, allMdFiles) {
|
|
3968
4643
|
const { artifactMap, componentsList, architectureViews } = indexAllArtifacts(allMdFiles, rootDir);
|
|
3969
4644
|
const handoffList = buildHandoffList(rootDir, artifactMap);
|
|
3970
4645
|
const reqToHandoffMap = indexRequirementsToHandoff(handoffList);
|
|
3971
4646
|
const { featureTagMap, codeTestMap } = buildTestMaps(rootDir, artifactMap);
|
|
3972
4647
|
const requirements = collectRequirementsToVerify(artifactMap);
|
|
3973
|
-
|
|
3974
|
-
reqToHandoffMap,
|
|
3975
|
-
|
|
3976
|
-
componentsList,
|
|
3977
|
-
architectureViews,
|
|
3978
|
-
featureTagMap,
|
|
3979
|
-
codeTestMap
|
|
4648
|
+
return {
|
|
4649
|
+
ctx: { reqToHandoffMap, artifactMap, componentsList, architectureViews, featureTagMap, codeTestMap },
|
|
4650
|
+
requirements
|
|
3980
4651
|
};
|
|
4652
|
+
}
|
|
4653
|
+
function verifyTraceability(options = {}) {
|
|
4654
|
+
const rootDir = options.rootDir || process.cwd();
|
|
4655
|
+
const allMdFiles = walkMdFiles(rootDir);
|
|
4656
|
+
const { ctx, requirements } = buildTraceabilityContext(rootDir, allMdFiles);
|
|
3981
4657
|
const { matrixRows, orphans, errorCount } = evaluateAllRequirements(requirements, ctx);
|
|
4658
|
+
const { contractAudits, missingContractsCount } = auditDesignContracts(rootDir, ctx.artifactMap);
|
|
4659
|
+
const totalIssues = errorCount + missingContractsCount;
|
|
3982
4660
|
return {
|
|
3983
|
-
success:
|
|
4661
|
+
success: totalIssues === 0,
|
|
3984
4662
|
rows: matrixRows,
|
|
3985
4663
|
orphans,
|
|
3986
4664
|
orphanCount: orphans.length,
|
|
3987
4665
|
totalRequirements: requirements.length,
|
|
3988
|
-
|
|
4666
|
+
contractAudits,
|
|
4667
|
+
missingContractsCount,
|
|
4668
|
+
reportMarkdown: generateTraceabilityReportMarkdown(matrixRows, totalIssues)
|
|
3989
4669
|
};
|
|
3990
4670
|
}
|
|
3991
|
-
function
|
|
4671
|
+
function parseTaskVerification2(rawVerification) {
|
|
3992
4672
|
const verification = {};
|
|
3993
4673
|
if (rawVerification && typeof rawVerification === "object") {
|
|
3994
4674
|
const v = rawVerification;
|
|
@@ -3997,6 +4677,12 @@ function parseTaskVerification(rawVerification) {
|
|
|
3997
4677
|
}
|
|
3998
4678
|
return verification;
|
|
3999
4679
|
}
|
|
4680
|
+
function parseOptionalStringList(val) {
|
|
4681
|
+
if (Array.isArray(val)) {
|
|
4682
|
+
return val.map(String).filter(Boolean);
|
|
4683
|
+
}
|
|
4684
|
+
return void 0;
|
|
4685
|
+
}
|
|
4000
4686
|
function mapRawTaskItem(t) {
|
|
4001
4687
|
return {
|
|
4002
4688
|
id: typeof t["id"] === "string" ? t["id"] : String(t["id"] ?? ""),
|
|
@@ -4007,7 +4693,10 @@ function mapRawTaskItem(t) {
|
|
|
4007
4693
|
assignedTo: typeof t["assigned-to"] === "string" ? t["assigned-to"] : void 0,
|
|
4008
4694
|
status: typeof t["status"] === "string" ? t["status"] : void 0,
|
|
4009
4695
|
blockingReason: typeof t["blocking-reason"] === "string" ? t["blocking-reason"] : void 0,
|
|
4010
|
-
|
|
4696
|
+
implementsComponents: parseOptionalStringList(t["implements-components"]),
|
|
4697
|
+
satisfiesRequirements: parseOptionalStringList(t["satisfies-requirements"]),
|
|
4698
|
+
implementsContracts: parseOptionalStringList(t["implements-contracts"]),
|
|
4699
|
+
verification: parseTaskVerification2(t["verification"])
|
|
4011
4700
|
};
|
|
4012
4701
|
}
|
|
4013
4702
|
function parseTasksDoc(content) {
|
|
@@ -4015,7 +4704,7 @@ function parseTasksDoc(content) {
|
|
|
4015
4704
|
if (!match) return { frontmatter: { tasks: [] }, body: content };
|
|
4016
4705
|
let parsed;
|
|
4017
4706
|
try {
|
|
4018
|
-
parsed =
|
|
4707
|
+
parsed = yaml3.load(match[1]);
|
|
4019
4708
|
} catch {
|
|
4020
4709
|
return { frontmatter: { tasks: [] }, body: content };
|
|
4021
4710
|
}
|
|
@@ -4040,12 +4729,12 @@ function walkTaskFiles(dir, fileList = [], excludeDirs = [
|
|
|
4040
4729
|
"examples"
|
|
4041
4730
|
]) {
|
|
4042
4731
|
try {
|
|
4043
|
-
if (!
|
|
4044
|
-
const files =
|
|
4732
|
+
if (!fs7.existsSync(dir)) return fileList;
|
|
4733
|
+
const files = fs7.readdirSync(dir);
|
|
4045
4734
|
for (const file of files) {
|
|
4046
|
-
const fullPath =
|
|
4735
|
+
const fullPath = path4.join(dir, file);
|
|
4047
4736
|
try {
|
|
4048
|
-
if (
|
|
4737
|
+
if (fs7.statSync(fullPath).isDirectory()) {
|
|
4049
4738
|
if (!excludeDirs.includes(file)) {
|
|
4050
4739
|
walkTaskFiles(fullPath, fileList, excludeDirs);
|
|
4051
4740
|
}
|
|
@@ -4158,26 +4847,86 @@ function updateGovernanceCounts(t, modeCounts, riskCounts) {
|
|
|
4158
4847
|
}
|
|
4159
4848
|
}
|
|
4160
4849
|
function parseTasksFromFile(file) {
|
|
4161
|
-
if (!
|
|
4850
|
+
if (!fs7.existsSync(file)) return [];
|
|
4162
4851
|
try {
|
|
4163
|
-
const content =
|
|
4852
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
4164
4853
|
const { frontmatter } = parseTasksDoc(content);
|
|
4165
4854
|
return frontmatter.tasks || [];
|
|
4166
4855
|
} catch {
|
|
4167
4856
|
return [];
|
|
4168
4857
|
}
|
|
4169
4858
|
}
|
|
4170
|
-
function
|
|
4859
|
+
function extractDesignExpectedScope(changeDir) {
|
|
4860
|
+
const designFile = path4.join(changeDir, "design.md");
|
|
4861
|
+
if (!fs7.existsSync(designFile)) return [];
|
|
4862
|
+
const expected = [];
|
|
4863
|
+
try {
|
|
4864
|
+
const { frontmatter } = parseTasksDoc(fs7.readFileSync(designFile, "utf-8"));
|
|
4865
|
+
if (typeof frontmatter["architecture-component"] === "string") {
|
|
4866
|
+
expected.push(frontmatter["architecture-component"]);
|
|
4867
|
+
}
|
|
4868
|
+
if (Array.isArray(frontmatter["architecture-components"])) {
|
|
4869
|
+
expected.push(...frontmatter["architecture-components"].map(String));
|
|
4870
|
+
}
|
|
4871
|
+
} catch {
|
|
4872
|
+
}
|
|
4873
|
+
return expected;
|
|
4874
|
+
}
|
|
4875
|
+
function extractHandoffExpectedScope(changeDir, maxUc, violations) {
|
|
4876
|
+
const handoff = loadProductHandoffSidecar(changeDir);
|
|
4877
|
+
if (!handoff?.subgraph) return [];
|
|
4878
|
+
const ucList = handoff.subgraph.useCases || [];
|
|
4879
|
+
if (maxUc !== void 0 && ucList.length > maxUc && violations) {
|
|
4880
|
+
violations.push(
|
|
4881
|
+
`[${handoff.changeId}] Greenfield monolith detected: change bundles ${ucList.length} use cases (max ${maxUc}). Decompose into incremental SDD changes per Use Case.`
|
|
4882
|
+
);
|
|
4883
|
+
}
|
|
4884
|
+
const reqs = handoff.subgraph.requirements || [];
|
|
4885
|
+
const secReqs = handoff.subgraph.securityRequirements || [];
|
|
4886
|
+
return [...reqs, ...secReqs];
|
|
4887
|
+
}
|
|
4888
|
+
function collectTaskCoveredScope(tasks) {
|
|
4889
|
+
const covered = /* @__PURE__ */ new Set();
|
|
4890
|
+
for (const t of tasks) {
|
|
4891
|
+
for (const c of t.implementsComponents || []) covered.add(c);
|
|
4892
|
+
for (const r of t.satisfiesRequirements || []) covered.add(r);
|
|
4893
|
+
for (const m of t.implementsContracts || []) covered.add(m);
|
|
4894
|
+
}
|
|
4895
|
+
return covered;
|
|
4896
|
+
}
|
|
4897
|
+
function hasExplicitScopeBindings(tasks) {
|
|
4898
|
+
return tasks.some(
|
|
4899
|
+
(t) => t.implementsComponents && t.implementsComponents.length > 0 || t.satisfiesRequirements && t.satisfiesRequirements.length > 0 || t.implementsContracts && t.implementsContracts.length > 0
|
|
4900
|
+
);
|
|
4901
|
+
}
|
|
4902
|
+
function auditDesignScopeForFile(file, tasks, options, unassignedList, violationsList) {
|
|
4903
|
+
const changeDir = path4.dirname(file);
|
|
4904
|
+
const expectedItems = [
|
|
4905
|
+
...extractDesignExpectedScope(changeDir),
|
|
4906
|
+
...extractHandoffExpectedScope(changeDir, options.maxUseCasesPerChange, violationsList)
|
|
4907
|
+
];
|
|
4908
|
+
const shouldEnforce = Boolean(options.enforceDesignCoverage || hasExplicitScopeBindings(tasks));
|
|
4909
|
+
if (!shouldEnforce || expectedItems.length === 0) return;
|
|
4910
|
+
const covered = collectTaskCoveredScope(tasks);
|
|
4911
|
+
for (const item of new Set(expectedItems)) {
|
|
4912
|
+
if (!covered.has(item)) {
|
|
4913
|
+
unassignedList.push(item);
|
|
4914
|
+
violationsList.push(`[${path4.basename(changeDir)}] Unassigned design/handoff scope in tasks.md: missing task coverage for ${item}`);
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
function processTasksFile(file, rootDir, options, acc) {
|
|
4171
4919
|
const tasks = parseTasksFromFile(file);
|
|
4172
|
-
const relPath =
|
|
4920
|
+
const relPath = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
4173
4921
|
let unverifiedDelta = 0;
|
|
4174
4922
|
for (const t of tasks) {
|
|
4175
4923
|
const { summary, violation, unverified } = evaluateTaskItem(t, relPath);
|
|
4176
|
-
if (violation) violationsList.push(violation);
|
|
4924
|
+
if (violation) acc.violationsList.push(violation);
|
|
4177
4925
|
if (unverified) unverifiedDelta++;
|
|
4178
|
-
updateGovernanceCounts(t, modeCounts, riskCounts);
|
|
4179
|
-
taskSummaries.push(summary);
|
|
4926
|
+
updateGovernanceCounts(t, acc.modeCounts, acc.riskCounts);
|
|
4927
|
+
acc.taskSummaries.push(summary);
|
|
4180
4928
|
}
|
|
4929
|
+
auditDesignScopeForFile(file, tasks, options, acc.unassignedDesignScope, acc.violationsList);
|
|
4181
4930
|
return { tasksCount: tasks.length, unverifiedDelta };
|
|
4182
4931
|
}
|
|
4183
4932
|
function createInitialModeCounts() {
|
|
@@ -4196,30 +4945,32 @@ function createInitialRiskCounts() {
|
|
|
4196
4945
|
CRITICAL: 0
|
|
4197
4946
|
};
|
|
4198
4947
|
}
|
|
4199
|
-
function assembleGovernanceResult(
|
|
4948
|
+
function assembleGovernanceResult(acc, reportMarkdown) {
|
|
4200
4949
|
return {
|
|
4201
|
-
success: violationsList.length === 0,
|
|
4202
|
-
totalTasks,
|
|
4203
|
-
verifiedCount: totalTasks - unverifiedCount,
|
|
4204
|
-
unverifiedCount,
|
|
4205
|
-
riskCounts,
|
|
4206
|
-
modeCounts,
|
|
4207
|
-
tasks: taskSummaries,
|
|
4208
|
-
violations: violationsList,
|
|
4950
|
+
success: acc.violationsList.length === 0,
|
|
4951
|
+
totalTasks: acc.totalTasks,
|
|
4952
|
+
verifiedCount: acc.totalTasks - acc.unverifiedCount,
|
|
4953
|
+
unverifiedCount: acc.unverifiedCount,
|
|
4954
|
+
riskCounts: acc.riskCounts,
|
|
4955
|
+
modeCounts: acc.modeCounts,
|
|
4956
|
+
tasks: acc.taskSummaries,
|
|
4957
|
+
violations: acc.violationsList,
|
|
4958
|
+
unassignedDesignScope: acc.unassignedDesignScope,
|
|
4209
4959
|
reportMarkdown
|
|
4210
4960
|
};
|
|
4211
4961
|
}
|
|
4212
|
-
function collectAllTasksData(taskFiles, rootDir) {
|
|
4962
|
+
function collectAllTasksData(taskFiles, rootDir, options) {
|
|
4213
4963
|
const acc = {
|
|
4214
4964
|
totalTasks: 0,
|
|
4215
4965
|
unverifiedCount: 0,
|
|
4216
4966
|
taskSummaries: [],
|
|
4217
4967
|
violationsList: [],
|
|
4968
|
+
unassignedDesignScope: [],
|
|
4218
4969
|
modeCounts: createInitialModeCounts(),
|
|
4219
4970
|
riskCounts: createInitialRiskCounts()
|
|
4220
4971
|
};
|
|
4221
4972
|
for (const file of taskFiles) {
|
|
4222
|
-
const delta = processTasksFile(file, rootDir,
|
|
4973
|
+
const delta = processTasksFile(file, rootDir, options, acc);
|
|
4223
4974
|
acc.totalTasks += delta.tasksCount;
|
|
4224
4975
|
acc.unverifiedCount += delta.unverifiedDelta;
|
|
4225
4976
|
}
|
|
@@ -4228,308 +4979,14 @@ function collectAllTasksData(taskFiles, rootDir) {
|
|
|
4228
4979
|
function verifyTasksGovernance(options = {}) {
|
|
4229
4980
|
const rootDir = options.rootDir || process.cwd();
|
|
4230
4981
|
const searchDir = options.specsDir || rootDir;
|
|
4231
|
-
const data = collectAllTasksData(walkTaskFiles(searchDir), rootDir);
|
|
4982
|
+
const data = collectAllTasksData(walkTaskFiles(searchDir), rootDir, options);
|
|
4232
4983
|
const reportMarkdown = generateGovernanceReportMarkdown(
|
|
4233
4984
|
data.taskSummaries,
|
|
4234
4985
|
data.violationsList.length,
|
|
4235
4986
|
data.modeCounts,
|
|
4236
4987
|
data.totalTasks
|
|
4237
4988
|
);
|
|
4238
|
-
return assembleGovernanceResult(
|
|
4239
|
-
data.totalTasks,
|
|
4240
|
-
data.unverifiedCount,
|
|
4241
|
-
data.riskCounts,
|
|
4242
|
-
data.modeCounts,
|
|
4243
|
-
data.taskSummaries,
|
|
4244
|
-
data.violationsList,
|
|
4245
|
-
reportMarkdown
|
|
4246
|
-
);
|
|
4247
|
-
}
|
|
4248
|
-
function parseTaskVerification2(v) {
|
|
4249
|
-
const verification = {};
|
|
4250
|
-
if (v && typeof v === "object") {
|
|
4251
|
-
const rec = v;
|
|
4252
|
-
if (typeof rec["method"] === "string") verification.method = rec["method"];
|
|
4253
|
-
if (typeof rec["command-or-criteria"] === "string") verification.criteria = rec["command-or-criteria"];
|
|
4254
|
-
}
|
|
4255
|
-
return verification;
|
|
4256
|
-
}
|
|
4257
|
-
function parseTaskItem(t) {
|
|
4258
|
-
return {
|
|
4259
|
-
id: typeof t["id"] === "string" ? t["id"] : String(t["id"] ?? ""),
|
|
4260
|
-
title: typeof t["title"] === "string" ? t["title"] : void 0,
|
|
4261
|
-
verification: parseTaskVerification2(t["verification"])
|
|
4262
|
-
};
|
|
4263
|
-
}
|
|
4264
|
-
function parseVerifiedByTests(verifiedBy) {
|
|
4265
|
-
if (Array.isArray(verifiedBy)) {
|
|
4266
|
-
return verifiedBy.map(String);
|
|
4267
|
-
}
|
|
4268
|
-
if (typeof verifiedBy === "string") {
|
|
4269
|
-
return [verifiedBy];
|
|
4270
|
-
}
|
|
4271
|
-
return void 0;
|
|
4272
|
-
}
|
|
4273
|
-
function parseGenericDoc(content) {
|
|
4274
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4275
|
-
if (!match) return { frontmatter: {}, body: content };
|
|
4276
|
-
let parsed;
|
|
4277
|
-
try {
|
|
4278
|
-
parsed = yaml2.load(match[1]);
|
|
4279
|
-
} catch {
|
|
4280
|
-
return { frontmatter: {}, body: content };
|
|
4281
|
-
}
|
|
4282
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4283
|
-
return { frontmatter: {}, body: content };
|
|
4284
|
-
}
|
|
4285
|
-
const raw = parsed;
|
|
4286
|
-
const rawTasks = Array.isArray(raw["tasks"]) ? raw["tasks"] : [];
|
|
4287
|
-
const tasks = rawTasks.map(parseTaskItem);
|
|
4288
|
-
const frontmatter = {
|
|
4289
|
-
...raw,
|
|
4290
|
-
tasks,
|
|
4291
|
-
"verified-by-tests": parseVerifiedByTests(raw["verified-by-tests"])
|
|
4292
|
-
};
|
|
4293
|
-
return { frontmatter, body: content.substring(match[0].length) };
|
|
4294
|
-
}
|
|
4295
|
-
function buildRequirementsTable(reqAudit) {
|
|
4296
|
-
const lines = [
|
|
4297
|
-
`## 1. Requirements Audit (Product, Quality, and Security)`,
|
|
4298
|
-
``,
|
|
4299
|
-
`| Requirement ID | Title | Source File | Declared Method | Linked Test Files | Status |`,
|
|
4300
|
-
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
4301
|
-
];
|
|
4302
|
-
for (const r of reqAudit) {
|
|
4303
|
-
const icon = r.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
4304
|
-
lines.push(
|
|
4305
|
-
`| **\`${r.id}\`** | ${r.title} | \`${r.file}\` | \`${r.method}\` | \`${r.testRefs}\` | ${icon} |`
|
|
4306
|
-
);
|
|
4307
|
-
}
|
|
4308
|
-
return lines;
|
|
4309
|
-
}
|
|
4310
|
-
function buildTasksTable(taskAudit) {
|
|
4311
|
-
const lines = [
|
|
4312
|
-
`## 2. Tasks Audit (SDD Delivery Engineering)`,
|
|
4313
|
-
``,
|
|
4314
|
-
`| Task ID | Title | tasks.md File | Method | Verification Command / Criterion | Status |`,
|
|
4315
|
-
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
4316
|
-
];
|
|
4317
|
-
for (const t of taskAudit) {
|
|
4318
|
-
const icon = t.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
4319
|
-
lines.push(
|
|
4320
|
-
`| **\`${t.id}\`** | ${t.title} | \`${t.file}\` | \`${t.method}\` | \`${t.commandOrCriteria}\` | ${icon} |`
|
|
4321
|
-
);
|
|
4322
|
-
}
|
|
4323
|
-
return lines;
|
|
4324
|
-
}
|
|
4325
|
-
function generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors) {
|
|
4326
|
-
const totalErrors = reqErrors + taskErrors;
|
|
4327
|
-
const verdict = totalErrors === 0 ? "100% VERIFIED WITH TESTS (PASSED)" : `BLOCKED: Detected ${totalErrors} items without tests (${reqErrors} Requirements, ${taskErrors} Tasks)`;
|
|
4328
|
-
const reportLines = [
|
|
4329
|
-
`# \u{1F9EA} Deterministic Test Coverage Audit for Requirements and Tasks`,
|
|
4330
|
-
``,
|
|
4331
|
-
`> **Audit Date:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
4332
|
-
`> **Overall Verdict:** ${verdict}`,
|
|
4333
|
-
``,
|
|
4334
|
-
`---`,
|
|
4335
|
-
``,
|
|
4336
|
-
...buildRequirementsTable(reqAudit),
|
|
4337
|
-
``,
|
|
4338
|
-
`---`,
|
|
4339
|
-
``,
|
|
4340
|
-
...buildTasksTable(taskAudit)
|
|
4341
|
-
];
|
|
4342
|
-
return reportLines.join("\n");
|
|
4343
|
-
}
|
|
4344
|
-
var EXCLUDED_TEST_DIRS = /* @__PURE__ */ new Set([
|
|
4345
|
-
"node_modules",
|
|
4346
|
-
".git",
|
|
4347
|
-
"dist",
|
|
4348
|
-
"bin",
|
|
4349
|
-
"obj",
|
|
4350
|
-
"build",
|
|
4351
|
-
"out",
|
|
4352
|
-
"target",
|
|
4353
|
-
"coverage",
|
|
4354
|
-
".changeset",
|
|
4355
|
-
"scratch",
|
|
4356
|
-
"fixtures",
|
|
4357
|
-
"templates",
|
|
4358
|
-
"examples",
|
|
4359
|
-
".vs",
|
|
4360
|
-
".idea"
|
|
4361
|
-
]);
|
|
4362
|
-
var TEST_EXTENSIONS = [".feature", ".spec.ts", ".spec.js", ".test.ts", ".test.js", "_test.py", "_test.go"];
|
|
4363
|
-
function isTestFile(fileName) {
|
|
4364
|
-
return TEST_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
4365
|
-
}
|
|
4366
|
-
function recordRequirementTest(reqToTestsMap, reqId, relTest) {
|
|
4367
|
-
const existing = reqToTestsMap.get(reqId) || [];
|
|
4368
|
-
if (!existing.includes(relTest)) {
|
|
4369
|
-
existing.push(relTest);
|
|
4370
|
-
reqToTestsMap.set(reqId, existing);
|
|
4371
|
-
}
|
|
4372
|
-
}
|
|
4373
|
-
function parseFeatureTags(content, relTest, reqToTestsMap) {
|
|
4374
|
-
const lines = content.split("\n");
|
|
4375
|
-
for (const line of lines) {
|
|
4376
|
-
const trimmed = line.trim();
|
|
4377
|
-
if (!trimmed.startsWith("@")) continue;
|
|
4378
|
-
const tags = trimmed.split(/\s+/).filter((t) => t.startsWith("@"));
|
|
4379
|
-
for (const t of tags) {
|
|
4380
|
-
recordRequirementTest(reqToTestsMap, t.substring(1), relTest);
|
|
4381
|
-
}
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
|
-
var REQ_ID_REGEX = /\b(FR|QR|CON|SEC-REQ)-[A-Z0-9]+(-[A-Z0-9]+)*\b/g;
|
|
4385
|
-
function parseCodeTestIds(content, relTest, reqToTestsMap) {
|
|
4386
|
-
const idMatches = content.match(REQ_ID_REGEX);
|
|
4387
|
-
if (!idMatches) return;
|
|
4388
|
-
for (const reqId of idMatches) {
|
|
4389
|
-
recordRequirementTest(reqToTestsMap, reqId, relTest);
|
|
4390
|
-
}
|
|
4391
|
-
}
|
|
4392
|
-
function processTestFile(fullPath, rootDir, reqToTestsMap) {
|
|
4393
|
-
try {
|
|
4394
|
-
const content = fs6.readFileSync(fullPath, "utf-8");
|
|
4395
|
-
const relTest = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
4396
|
-
if (fullPath.endsWith(".feature")) {
|
|
4397
|
-
parseFeatureTags(content, relTest, reqToTestsMap);
|
|
4398
|
-
}
|
|
4399
|
-
parseCodeTestIds(content, relTest, reqToTestsMap);
|
|
4400
|
-
} catch {
|
|
4401
|
-
}
|
|
4402
|
-
}
|
|
4403
|
-
function collectTestFiles(dir, files = []) {
|
|
4404
|
-
if (!fs6.existsSync(dir)) return files;
|
|
4405
|
-
try {
|
|
4406
|
-
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
4407
|
-
for (const e of entries) {
|
|
4408
|
-
if (e.isDirectory()) {
|
|
4409
|
-
if (!EXCLUDED_TEST_DIRS.has(e.name)) {
|
|
4410
|
-
collectTestFiles(path5.join(dir, e.name), files);
|
|
4411
|
-
}
|
|
4412
|
-
} else if (isTestFile(e.name)) {
|
|
4413
|
-
files.push(path5.join(dir, e.name));
|
|
4414
|
-
}
|
|
4415
|
-
}
|
|
4416
|
-
} catch {
|
|
4417
|
-
}
|
|
4418
|
-
return files;
|
|
4419
|
-
}
|
|
4420
|
-
function scanAllTestArtifacts(rootDir) {
|
|
4421
|
-
const reqToTestsMap = /* @__PURE__ */ new Map();
|
|
4422
|
-
const testFiles = collectTestFiles(rootDir);
|
|
4423
|
-
for (const file of testFiles) {
|
|
4424
|
-
processTestFile(file, rootDir, reqToTestsMap);
|
|
4425
|
-
}
|
|
4426
|
-
return reqToTestsMap;
|
|
4427
|
-
}
|
|
4428
|
-
var REQ_PREFIXES = ["FR-", "QR-", "CON-", "SEC-REQ-"];
|
|
4429
|
-
function isRequirementArtifact(frontmatter, id) {
|
|
4430
|
-
if (frontmatter.type === "requirement" || frontmatter.type === "security-requirement") {
|
|
4431
|
-
return true;
|
|
4432
|
-
}
|
|
4433
|
-
return REQ_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
4434
|
-
}
|
|
4435
|
-
function resolveRequirementMethod(declaredMethod, matchingTests) {
|
|
4436
|
-
if (typeof declaredMethod === "string" && declaredMethod) {
|
|
4437
|
-
return declaredMethod;
|
|
4438
|
-
}
|
|
4439
|
-
return matchingTests.some((t) => t.endsWith(".feature")) ? "BDD_CUCUMBER" : "TEST_EXECUTION";
|
|
4440
|
-
}
|
|
4441
|
-
function auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap) {
|
|
4442
|
-
const matchingTests = reqToTestsMap.get(id) || [];
|
|
4443
|
-
const isVerified = isTemplate || matchingTests.length > 0;
|
|
4444
|
-
const method = resolveRequirementMethod(frontmatter["verifiable-by"], matchingTests);
|
|
4445
|
-
return {
|
|
4446
|
-
item: {
|
|
4447
|
-
id: id || "SIN-ID",
|
|
4448
|
-
title: frontmatter.title || "Sin t\xEDtulo",
|
|
4449
|
-
file: relPath,
|
|
4450
|
-
isTemplate,
|
|
4451
|
-
method,
|
|
4452
|
-
testRefs: matchingTests.join(", ") || "NINGUNA DETECTADA",
|
|
4453
|
-
existingCount: matchingTests.length,
|
|
4454
|
-
status: isVerified ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
4455
|
-
},
|
|
4456
|
-
isError: !isVerified
|
|
4457
|
-
};
|
|
4458
|
-
}
|
|
4459
|
-
function isTaskProofValid(criteria, isTemplate) {
|
|
4460
|
-
if (isTemplate) return true;
|
|
4461
|
-
return criteria.length > 4 && !criteria.includes("TODO") && !criteria.includes("TBD");
|
|
4462
|
-
}
|
|
4463
|
-
function auditSingleTask(t, relPath, isTemplate) {
|
|
4464
|
-
const crit = t.verification?.criteria || "";
|
|
4465
|
-
const method = t.verification?.method || "TEST_EXECUTION";
|
|
4466
|
-
const hasProof = isTaskProofValid(crit, isTemplate);
|
|
4467
|
-
return {
|
|
4468
|
-
item: {
|
|
4469
|
-
id: t.id,
|
|
4470
|
-
title: t.title || "Sin t\xEDtulo",
|
|
4471
|
-
file: relPath,
|
|
4472
|
-
method,
|
|
4473
|
-
commandOrCriteria: crit || "AUSENTE",
|
|
4474
|
-
status: hasProof ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
4475
|
-
},
|
|
4476
|
-
hasProof
|
|
4477
|
-
};
|
|
4478
|
-
}
|
|
4479
|
-
function auditTasks(tasks, relPath, isTemplate) {
|
|
4480
|
-
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
4481
|
-
return { items: [], errorCount: 0 };
|
|
4482
|
-
}
|
|
4483
|
-
const items = [];
|
|
4484
|
-
let errorCount = 0;
|
|
4485
|
-
for (const t of tasks) {
|
|
4486
|
-
const { item, hasProof } = auditSingleTask(t, relPath, isTemplate);
|
|
4487
|
-
if (!hasProof) errorCount++;
|
|
4488
|
-
items.push(item);
|
|
4489
|
-
}
|
|
4490
|
-
return { items, errorCount };
|
|
4491
|
-
}
|
|
4492
|
-
function assembleCoverageResult(reqAudit, taskAudit, reqErrors, taskErrors) {
|
|
4493
|
-
return {
|
|
4494
|
-
success: reqErrors === 0 && taskErrors === 0,
|
|
4495
|
-
totalRequirements: reqAudit.length,
|
|
4496
|
-
passedRequirements: reqAudit.length - reqErrors,
|
|
4497
|
-
failedRequirements: reqErrors,
|
|
4498
|
-
totalTasks: taskAudit.length,
|
|
4499
|
-
passedTasks: taskAudit.length - taskErrors,
|
|
4500
|
-
failedTasks: taskErrors,
|
|
4501
|
-
requirements: reqAudit,
|
|
4502
|
-
tasks: taskAudit,
|
|
4503
|
-
reportMarkdown: generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors)
|
|
4504
|
-
};
|
|
4505
|
-
}
|
|
4506
|
-
function verifyTestingCoverage(options = {}) {
|
|
4507
|
-
const rootDir = options.rootDir || process.cwd();
|
|
4508
|
-
const allMdFiles = walkMdFiles(rootDir);
|
|
4509
|
-
const reqToTestsMap = scanAllTestArtifacts(rootDir);
|
|
4510
|
-
const reqAudit = [];
|
|
4511
|
-
const taskAudit = [];
|
|
4512
|
-
let reqErrors = 0;
|
|
4513
|
-
let taskErrors = 0;
|
|
4514
|
-
for (const file of allMdFiles) {
|
|
4515
|
-
try {
|
|
4516
|
-
const content = fs6.readFileSync(file, "utf-8");
|
|
4517
|
-
const { frontmatter } = parseGenericDoc(content);
|
|
4518
|
-
const relPath = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
4519
|
-
const isTemplate = relPath.includes("templates/");
|
|
4520
|
-
const id = frontmatter.id || "";
|
|
4521
|
-
if (isRequirementArtifact(frontmatter, id)) {
|
|
4522
|
-
const auditedReq = auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap);
|
|
4523
|
-
reqAudit.push(auditedReq.item);
|
|
4524
|
-
if (auditedReq.isError) reqErrors++;
|
|
4525
|
-
}
|
|
4526
|
-
const auditedTasks = auditTasks(frontmatter.tasks, relPath, isTemplate);
|
|
4527
|
-
taskAudit.push(...auditedTasks.items);
|
|
4528
|
-
taskErrors += auditedTasks.errorCount;
|
|
4529
|
-
} catch {
|
|
4530
|
-
}
|
|
4531
|
-
}
|
|
4532
|
-
return assembleCoverageResult(reqAudit, taskAudit, reqErrors, taskErrors);
|
|
4989
|
+
return assembleGovernanceResult(data, reportMarkdown);
|
|
4533
4990
|
}
|
|
4534
4991
|
function isToolAvailable(toolName) {
|
|
4535
4992
|
try {
|
|
@@ -4575,8 +5032,8 @@ function findLicenseFile(pkgDir) {
|
|
|
4575
5032
|
"license.txt"
|
|
4576
5033
|
];
|
|
4577
5034
|
for (const c of candidates) {
|
|
4578
|
-
const full =
|
|
4579
|
-
if (
|
|
5035
|
+
const full = path4.join(pkgDir, c);
|
|
5036
|
+
if (fs7.existsSync(full)) {
|
|
4580
5037
|
return full;
|
|
4581
5038
|
}
|
|
4582
5039
|
}
|
|
@@ -4593,9 +5050,9 @@ var LICENSE_FILE_PATTERNS = [
|
|
|
4593
5050
|
[(t) => t.includes("Microsoft Reciprocal License") || t.includes("MS-RL"), "MS-RL"]
|
|
4594
5051
|
];
|
|
4595
5052
|
function resolveLicenseFromFile(licenseFilePath) {
|
|
4596
|
-
if (!licenseFilePath || !
|
|
5053
|
+
if (!licenseFilePath || !fs7.existsSync(licenseFilePath)) return void 0;
|
|
4597
5054
|
try {
|
|
4598
|
-
const text =
|
|
5055
|
+
const text = fs7.readFileSync(licenseFilePath, "utf-8");
|
|
4599
5056
|
for (const [matches, spdx] of LICENSE_FILE_PATTERNS) {
|
|
4600
5057
|
if (matches(text)) return spdx;
|
|
4601
5058
|
}
|
|
@@ -4604,16 +5061,16 @@ function resolveLicenseFromFile(licenseFilePath) {
|
|
|
4604
5061
|
return void 0;
|
|
4605
5062
|
}
|
|
4606
5063
|
function readPackageJson(filePath) {
|
|
4607
|
-
if (!
|
|
5064
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
4608
5065
|
try {
|
|
4609
|
-
return JSON.parse(
|
|
5066
|
+
return JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
4610
5067
|
} catch {
|
|
4611
5068
|
return null;
|
|
4612
5069
|
}
|
|
4613
5070
|
}
|
|
4614
5071
|
function extractDirectDepNames(rootDir, productionOnly) {
|
|
4615
5072
|
const names = /* @__PURE__ */ new Set();
|
|
4616
|
-
const pkgPath =
|
|
5073
|
+
const pkgPath = path4.join(rootDir, "package.json");
|
|
4617
5074
|
const pkg = readPackageJson(pkgPath);
|
|
4618
5075
|
if (!pkg) return names;
|
|
4619
5076
|
if (pkg.dependencies && typeof pkg.dependencies === "object") {
|
|
@@ -4633,7 +5090,7 @@ function resolveSpdxForPackage(pkg, licFile) {
|
|
|
4633
5090
|
return spdx;
|
|
4634
5091
|
}
|
|
4635
5092
|
function processPackageDir(pkgDir, directNames, seen) {
|
|
4636
|
-
const pkg = readPackageJson(
|
|
5093
|
+
const pkg = readPackageJson(path4.join(pkgDir, "package.json"));
|
|
4637
5094
|
if (!pkg?.name || !pkg.version) return;
|
|
4638
5095
|
const key = `${pkg.name}@${pkg.version}`;
|
|
4639
5096
|
if (seen.has(key)) return;
|
|
@@ -4654,32 +5111,32 @@ function processPackageDir(pkgDir, directNames, seen) {
|
|
|
4654
5111
|
});
|
|
4655
5112
|
}
|
|
4656
5113
|
function scanScopeSubdirectory(scopeDir, directNames, seen) {
|
|
4657
|
-
const scopedEntries =
|
|
5114
|
+
const scopedEntries = fs7.readdirSync(scopeDir, { withFileTypes: true });
|
|
4658
5115
|
for (const scoped of scopedEntries) {
|
|
4659
5116
|
if (scoped.isDirectory()) {
|
|
4660
|
-
processPackageDir(
|
|
5117
|
+
processPackageDir(path4.join(scopeDir, scoped.name), directNames, seen);
|
|
4661
5118
|
}
|
|
4662
5119
|
}
|
|
4663
5120
|
}
|
|
4664
5121
|
function scanSubdirectories(baseDir, directNames, seen) {
|
|
4665
|
-
if (!
|
|
4666
|
-
const entries =
|
|
5122
|
+
if (!fs7.existsSync(baseDir)) return;
|
|
5123
|
+
const entries = fs7.readdirSync(baseDir, { withFileTypes: true });
|
|
4667
5124
|
for (const entry of entries) {
|
|
4668
5125
|
if (!entry.isDirectory()) continue;
|
|
4669
5126
|
if (entry.name.startsWith("@")) {
|
|
4670
|
-
scanScopeSubdirectory(
|
|
5127
|
+
scanScopeSubdirectory(path4.join(baseDir, entry.name), directNames, seen);
|
|
4671
5128
|
} else if (entry.name !== ".bin" && entry.name !== ".pnpm") {
|
|
4672
|
-
processPackageDir(
|
|
5129
|
+
processPackageDir(path4.join(baseDir, entry.name), directNames, seen);
|
|
4673
5130
|
}
|
|
4674
5131
|
}
|
|
4675
5132
|
}
|
|
4676
5133
|
function scanPnpmStore(pnpmDir, directNames, seen) {
|
|
4677
|
-
if (!
|
|
4678
|
-
const entries =
|
|
5134
|
+
if (!fs7.existsSync(pnpmDir)) return;
|
|
5135
|
+
const entries = fs7.readdirSync(pnpmDir, { withFileTypes: true });
|
|
4679
5136
|
for (const entry of entries) {
|
|
4680
5137
|
if (!entry.isDirectory()) continue;
|
|
4681
|
-
const candidateModules =
|
|
4682
|
-
if (
|
|
5138
|
+
const candidateModules = path4.join(pnpmDir, entry.name, "node_modules");
|
|
5139
|
+
if (fs7.existsSync(candidateModules)) {
|
|
4683
5140
|
scanSubdirectories(candidateModules, directNames, seen);
|
|
4684
5141
|
}
|
|
4685
5142
|
}
|
|
@@ -4687,11 +5144,11 @@ function scanPnpmStore(pnpmDir, directNames, seen) {
|
|
|
4687
5144
|
function scanWithNativeFs(rootDir, options = {}) {
|
|
4688
5145
|
const directNames = extractDirectDepNames(rootDir, options.productionOnly);
|
|
4689
5146
|
const seen = /* @__PURE__ */ new Map();
|
|
4690
|
-
const nmDir =
|
|
4691
|
-
if (
|
|
5147
|
+
const nmDir = path4.join(rootDir, "node_modules");
|
|
5148
|
+
if (fs7.existsSync(nmDir)) {
|
|
4692
5149
|
scanSubdirectories(nmDir, directNames, seen);
|
|
4693
5150
|
if (options.depth !== "direct") {
|
|
4694
|
-
scanPnpmStore(
|
|
5151
|
+
scanPnpmStore(path4.join(nmDir, ".pnpm"), directNames, seen);
|
|
4695
5152
|
}
|
|
4696
5153
|
}
|
|
4697
5154
|
const allDeps = Array.from(seen.values());
|
|
@@ -4815,10 +5272,10 @@ function generateCycloneDxSbom(dependencies, options = {}) {
|
|
|
4815
5272
|
let name = options.projectName || "ai-sdlc-project";
|
|
4816
5273
|
let version = options.projectVersion || "1.0.0";
|
|
4817
5274
|
if (options.rootDir) {
|
|
4818
|
-
const pkgPath =
|
|
4819
|
-
if (
|
|
5275
|
+
const pkgPath = path4.join(options.rootDir, "package.json");
|
|
5276
|
+
if (fs7.existsSync(pkgPath)) {
|
|
4820
5277
|
try {
|
|
4821
|
-
const pkg = JSON.parse(
|
|
5278
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
4822
5279
|
if (pkg.name) name = pkg.name;
|
|
4823
5280
|
if (pkg.version) version = pkg.version;
|
|
4824
5281
|
} catch {
|
|
@@ -4852,15 +5309,15 @@ function generateCycloneDxSbom(dependencies, options = {}) {
|
|
|
4852
5309
|
function tryAtomicWrite(filePath, content) {
|
|
4853
5310
|
const tempPath = `${filePath}.${process.pid}.${Math.random().toString(36).substring(2)}.tmp`;
|
|
4854
5311
|
try {
|
|
4855
|
-
|
|
5312
|
+
fs7.writeFileSync(tempPath, content, "utf-8");
|
|
4856
5313
|
try {
|
|
4857
|
-
if (
|
|
4858
|
-
|
|
5314
|
+
if (fs7.existsSync(filePath)) {
|
|
5315
|
+
fs7.rmSync(filePath, { force: true });
|
|
4859
5316
|
}
|
|
4860
|
-
|
|
5317
|
+
fs7.renameSync(tempPath, filePath);
|
|
4861
5318
|
} catch {
|
|
4862
|
-
|
|
4863
|
-
|
|
5319
|
+
fs7.copyFileSync(tempPath, filePath);
|
|
5320
|
+
fs7.unlinkSync(tempPath);
|
|
4864
5321
|
}
|
|
4865
5322
|
return true;
|
|
4866
5323
|
} catch {
|
|
@@ -4868,15 +5325,15 @@ function tryAtomicWrite(filePath, content) {
|
|
|
4868
5325
|
}
|
|
4869
5326
|
}
|
|
4870
5327
|
function safeWriteFileSync(filePath, content) {
|
|
4871
|
-
const dir =
|
|
4872
|
-
if (!
|
|
4873
|
-
|
|
5328
|
+
const dir = path4.dirname(filePath);
|
|
5329
|
+
if (!fs7.existsSync(dir)) {
|
|
5330
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
4874
5331
|
}
|
|
4875
5332
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
4876
5333
|
if (tryAtomicWrite(filePath, content)) return;
|
|
4877
5334
|
}
|
|
4878
5335
|
try {
|
|
4879
|
-
|
|
5336
|
+
fs7.writeFileSync(filePath, content, "utf-8");
|
|
4880
5337
|
} catch {
|
|
4881
5338
|
}
|
|
4882
5339
|
}
|
|
@@ -4902,9 +5359,9 @@ function buildThirdPartyHeader(projectName, count) {
|
|
|
4902
5359
|
];
|
|
4903
5360
|
}
|
|
4904
5361
|
function readLicenseTextPreview(licenseFile) {
|
|
4905
|
-
if (!licenseFile || !
|
|
5362
|
+
if (!licenseFile || !fs7.existsSync(licenseFile)) return null;
|
|
4906
5363
|
try {
|
|
4907
|
-
const text =
|
|
5364
|
+
const text = fs7.readFileSync(licenseFile, "utf-8").trim();
|
|
4908
5365
|
return text.slice(0, 1e3) + (text.length > 1e3 ? "\n... (texto truncado)" : "");
|
|
4909
5366
|
} catch {
|
|
4910
5367
|
return null;
|
|
@@ -5016,7 +5473,7 @@ function parseExceptions(exceptions) {
|
|
|
5016
5473
|
}
|
|
5017
5474
|
function parseLicensePolicy(content) {
|
|
5018
5475
|
try {
|
|
5019
|
-
const doc =
|
|
5476
|
+
const doc = yaml3.load(content);
|
|
5020
5477
|
const { permitted, restricted, blocked } = classifyCategories(doc?.categories);
|
|
5021
5478
|
const exception_packages = parseExceptions(doc?.exceptions);
|
|
5022
5479
|
return {
|
|
@@ -5152,9 +5609,9 @@ function generateLicenseReportMarkdown(dependencies, violations, policy, sbomPat
|
|
|
5152
5609
|
return lines.filter(Boolean).join("\n");
|
|
5153
5610
|
}
|
|
5154
5611
|
function parseManifestItems(manifestFile) {
|
|
5155
|
-
if (!
|
|
5612
|
+
if (!fs7.existsSync(manifestFile)) return [];
|
|
5156
5613
|
try {
|
|
5157
|
-
const doc =
|
|
5614
|
+
const doc = yaml3.load(fs7.readFileSync(manifestFile, "utf-8"));
|
|
5158
5615
|
return Array.isArray(doc?.evaluated_dependencies) ? doc.evaluated_dependencies : [];
|
|
5159
5616
|
} catch {
|
|
5160
5617
|
return [];
|
|
@@ -5186,10 +5643,10 @@ function verifyStaticManifest(manifestFile, policy) {
|
|
|
5186
5643
|
function resolveManifestPath(rootDir, customPath) {
|
|
5187
5644
|
if (customPath) return customPath;
|
|
5188
5645
|
const candidates = [
|
|
5189
|
-
|
|
5190
|
-
|
|
5646
|
+
path4.join(rootDir, "compliance", "license-manifest.yaml"),
|
|
5647
|
+
path4.join(rootDir, "license-manifest.yaml")
|
|
5191
5648
|
];
|
|
5192
|
-
return candidates.find((p) =>
|
|
5649
|
+
return candidates.find((p) => fs7.existsSync(p)) || candidates[0];
|
|
5193
5650
|
}
|
|
5194
5651
|
function handleStaticManifestVerification(options, rootDir, policy) {
|
|
5195
5652
|
const manifestFile = resolveManifestPath(rootDir, options.manifestPath);
|
|
@@ -5210,7 +5667,7 @@ function shouldGenerateSbom(options, rawPolicy) {
|
|
|
5210
5667
|
}
|
|
5211
5668
|
function maybeGenerateSbom(options, rootDir, rawPolicy, dependencies) {
|
|
5212
5669
|
if (!shouldGenerateSbom(options, rawPolicy)) return void 0;
|
|
5213
|
-
const sbomPath = options.sbomPath ||
|
|
5670
|
+
const sbomPath = options.sbomPath || path4.join(rootDir, "reports", "sbom.cdx.json");
|
|
5214
5671
|
const sbom = generateCycloneDxSbom(dependencies, { rootDir });
|
|
5215
5672
|
writeCycloneDxSbom(sbom, sbomPath);
|
|
5216
5673
|
return sbomPath;
|
|
@@ -5219,7 +5676,7 @@ function maybeGenerateNotices(options, rootDir, rawPolicy, dependencies) {
|
|
|
5219
5676
|
const shouldGen = options.generateNotices === true || Boolean(options.noticesPath || rawPolicy?.ci_gates?.output_notices_file);
|
|
5220
5677
|
if (!shouldGen) return void 0;
|
|
5221
5678
|
const noticesFileName = rawPolicy?.ci_gates?.output_notices_file || "THIRD_PARTY_NOTICES.md";
|
|
5222
|
-
const noticesPath = options.noticesPath ||
|
|
5679
|
+
const noticesPath = options.noticesPath || path4.join(rootDir, noticesFileName);
|
|
5223
5680
|
const noticesContent = generateThirdPartyNotices(dependencies);
|
|
5224
5681
|
writeThirdPartyNotices(noticesContent, noticesPath);
|
|
5225
5682
|
return noticesPath;
|
|
@@ -5231,8 +5688,8 @@ function handleDynamicScaArtifacts(options, rootDir, rawPolicy, dependencies) {
|
|
|
5231
5688
|
};
|
|
5232
5689
|
}
|
|
5233
5690
|
function loadLicensePolicy(rootDir, customPath) {
|
|
5234
|
-
const policyFile = customPath ||
|
|
5235
|
-
return
|
|
5691
|
+
const policyFile = customPath || path4.join(rootDir, "license-policy.yaml");
|
|
5692
|
+
return fs7.existsSync(policyFile) ? parseLicensePolicy(fs7.readFileSync(policyFile, "utf-8")) : parseLicensePolicy("");
|
|
5236
5693
|
}
|
|
5237
5694
|
function evaluateDependencyList(dependencies, policy) {
|
|
5238
5695
|
const violations = [];
|
|
@@ -5302,14 +5759,14 @@ var PROTECTED_BYPASS_PATTERNS = [
|
|
|
5302
5759
|
/migrations?[\\/]/i
|
|
5303
5760
|
];
|
|
5304
5761
|
function detectProfileFromChange(changeDir) {
|
|
5305
|
-
if (!
|
|
5306
|
-
const specFile =
|
|
5307
|
-
if (
|
|
5762
|
+
if (!fs7.existsSync(changeDir)) return "standard";
|
|
5763
|
+
const specFile = path4.join(changeDir, "spec.md");
|
|
5764
|
+
if (fs7.existsSync(specFile)) {
|
|
5308
5765
|
try {
|
|
5309
|
-
const rawSpec =
|
|
5766
|
+
const rawSpec = fs7.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
5310
5767
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5311
5768
|
if (match) {
|
|
5312
|
-
const parsedFm =
|
|
5769
|
+
const parsedFm = yaml3.load(match[1]);
|
|
5313
5770
|
if (parsedFm && parsedFm.profile && ["patch", "standard", "critical"].includes(String(parsedFm.profile))) {
|
|
5314
5771
|
return parsedFm.profile;
|
|
5315
5772
|
}
|
|
@@ -5324,17 +5781,17 @@ function isPathProtectedFromPatch(filePath) {
|
|
|
5324
5781
|
return PROTECTED_BYPASS_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
5325
5782
|
}
|
|
5326
5783
|
function resolveActiveChangeDir(rootDir) {
|
|
5327
|
-
const activeRoot =
|
|
5328
|
-
if (!
|
|
5784
|
+
const activeRoot = path4.join(rootDir, "specs", "changes", "active");
|
|
5785
|
+
if (!fs7.existsSync(activeRoot)) return void 0;
|
|
5329
5786
|
try {
|
|
5330
|
-
const dirs =
|
|
5787
|
+
const dirs = fs7.readdirSync(activeRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path4.join(activeRoot, d.name));
|
|
5331
5788
|
return dirs.find((d) => detectProfileFromChange(d) === "patch") || dirs[0];
|
|
5332
5789
|
} catch {
|
|
5333
5790
|
return void 0;
|
|
5334
5791
|
}
|
|
5335
5792
|
}
|
|
5336
5793
|
function detectGitDiffFiles(rootDir) {
|
|
5337
|
-
if (!
|
|
5794
|
+
if (!fs7.existsSync(path4.join(rootDir, ".git"))) return [];
|
|
5338
5795
|
try {
|
|
5339
5796
|
const tracked = execFileSync("git", ["diff", "--name-only", "HEAD"], {
|
|
5340
5797
|
cwd: rootDir,
|
|
@@ -5357,19 +5814,19 @@ function resolveChangeDirectory(rootDir, options) {
|
|
|
5357
5814
|
if (options.changeDir) return options.changeDir;
|
|
5358
5815
|
if (options.changeId) {
|
|
5359
5816
|
const candidates = [
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5817
|
+
path4.join(rootDir, "specs", options.changeId),
|
|
5818
|
+
path4.join(rootDir, "specs", "changes", "active", options.changeId),
|
|
5819
|
+
path4.join(rootDir, "examples", "specs", options.changeId),
|
|
5820
|
+
path4.join(rootDir, "specs", "changes", "completed", options.changeId)
|
|
5364
5821
|
];
|
|
5365
|
-
return candidates.find((c) =>
|
|
5822
|
+
return candidates.find((c) => fs7.existsSync(c));
|
|
5366
5823
|
}
|
|
5367
5824
|
return resolveActiveChangeDir(rootDir);
|
|
5368
5825
|
}
|
|
5369
5826
|
function collectPatchViolations(rootDir, modifiedFiles) {
|
|
5370
5827
|
const violations = [];
|
|
5371
5828
|
for (const rawFile of modifiedFiles) {
|
|
5372
|
-
const relPath =
|
|
5829
|
+
const relPath = path4.isAbsolute(rawFile) ? path4.relative(rootDir, rawFile) : rawFile;
|
|
5373
5830
|
if (isPathProtectedFromPatch(relPath)) {
|
|
5374
5831
|
violations.push(
|
|
5375
5832
|
`[ANTI-PATCH-BYPASS] File '${relPath}' is a protected path (security, schemas, architectural decisions, or master policies). Changes with 'patch' profile are strictly prohibited from modifying these paths. Must be reclassified to 'standard' or 'critical'.`
|
|
@@ -5435,7 +5892,7 @@ function parseMdFrontmatter(raw) {
|
|
|
5435
5892
|
return { fm: {}, body: cleaned.trim() };
|
|
5436
5893
|
}
|
|
5437
5894
|
try {
|
|
5438
|
-
const parsed =
|
|
5895
|
+
const parsed = yaml3.load(match[1]);
|
|
5439
5896
|
const fm = parsed && typeof parsed === "object" ? parsed : {};
|
|
5440
5897
|
return { fm, body: cleaned.substring(match[0].length).trim() };
|
|
5441
5898
|
} catch {
|
|
@@ -5475,13 +5932,13 @@ function extractManualMetadata(fm, id, manualType, relPath, body) {
|
|
|
5475
5932
|
}
|
|
5476
5933
|
function buildManualItem(rootDir, filePath) {
|
|
5477
5934
|
try {
|
|
5478
|
-
const raw =
|
|
5935
|
+
const raw = fs7.readFileSync(filePath, "utf-8");
|
|
5479
5936
|
const { fm, body } = parseMdFrontmatter(raw);
|
|
5480
5937
|
const id = typeof fm.id === "string" ? fm.id.trim() : "";
|
|
5481
5938
|
const rawType = typeof fm.type === "string" ? fm.type.trim() : "";
|
|
5482
5939
|
const manualType = resolveManualType(id, rawType);
|
|
5483
5940
|
if (!id || !manualType) return null;
|
|
5484
|
-
const relPath =
|
|
5941
|
+
const relPath = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
5485
5942
|
return extractManualMetadata(fm, id, manualType, relPath, body);
|
|
5486
5943
|
} catch {
|
|
5487
5944
|
return null;
|
|
@@ -5492,8 +5949,8 @@ function scanManualsInWorkspace(rootDir) {
|
|
|
5492
5949
|
const seenIds = /* @__PURE__ */ new Set();
|
|
5493
5950
|
const items = [];
|
|
5494
5951
|
for (const relDir of manualDirs) {
|
|
5495
|
-
const fullDir =
|
|
5496
|
-
if (!
|
|
5952
|
+
const fullDir = path4.join(rootDir, relDir);
|
|
5953
|
+
if (!fs7.existsSync(fullDir)) continue;
|
|
5497
5954
|
for (const file of walkMdFiles(fullDir)) {
|
|
5498
5955
|
const item = buildManualItem(rootDir, file);
|
|
5499
5956
|
if (item && !seenIds.has(item.id)) {
|
|
@@ -5516,10 +5973,10 @@ function classifyBaselineId(id, type, buckets) {
|
|
|
5516
5973
|
}
|
|
5517
5974
|
}
|
|
5518
5975
|
function processBaselineFile(file, rootDir, buckets) {
|
|
5519
|
-
const rel =
|
|
5976
|
+
const rel = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
5520
5977
|
if (rel.includes("/changes/") || rel.startsWith("changes/")) return;
|
|
5521
5978
|
try {
|
|
5522
|
-
const { fm } = parseMdFrontmatter(
|
|
5979
|
+
const { fm } = parseMdFrontmatter(fs7.readFileSync(file, "utf-8"));
|
|
5523
5980
|
const id = typeof fm.id === "string" ? fm.id.trim() : "";
|
|
5524
5981
|
const status = typeof fm.status === "string" ? fm.status.trim().toLowerCase() : "active";
|
|
5525
5982
|
if (!id || status === "retired" || status === "deprecated") return;
|
|
@@ -5540,8 +5997,8 @@ function collectBaselineTargets(rootDir) {
|
|
|
5540
5997
|
enclaves: /* @__PURE__ */ new Set()
|
|
5541
5998
|
};
|
|
5542
5999
|
for (const dir of ["product", "architecture", "security", "specs"]) {
|
|
5543
|
-
const fullDir =
|
|
5544
|
-
if (
|
|
6000
|
+
const fullDir = path4.join(rootDir, dir);
|
|
6001
|
+
if (fs7.existsSync(fullDir)) {
|
|
5545
6002
|
scanDirectoryForBaseline(rootDir, fullDir, buckets);
|
|
5546
6003
|
}
|
|
5547
6004
|
}
|
|
@@ -5598,10 +6055,10 @@ function detectCurrentGitBranch(rootDir) {
|
|
|
5598
6055
|
}
|
|
5599
6056
|
}
|
|
5600
6057
|
function resolveActiveChangeProfile(rootDir) {
|
|
5601
|
-
const activeRoot =
|
|
5602
|
-
if (!
|
|
6058
|
+
const activeRoot = path4.join(rootDir, "specs", "changes", "active");
|
|
6059
|
+
if (!fs7.existsSync(activeRoot)) return "standard";
|
|
5603
6060
|
try {
|
|
5604
|
-
const dirs =
|
|
6061
|
+
const dirs = fs7.readdirSync(activeRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path4.join(activeRoot, d.name));
|
|
5605
6062
|
const profiles = dirs.map((d) => detectProfileFromChange(d));
|
|
5606
6063
|
if (profiles.includes("critical")) return "critical";
|
|
5607
6064
|
if (profiles.includes("patch") && profiles.length === 1) return "patch";
|
|
@@ -5785,7 +6242,7 @@ function extractParsedFrontmatter(content) {
|
|
|
5785
6242
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5786
6243
|
if (!match) return null;
|
|
5787
6244
|
try {
|
|
5788
|
-
const parsed =
|
|
6245
|
+
const parsed = yaml3.load(match[1]);
|
|
5789
6246
|
if (parsed && typeof parsed === "object") {
|
|
5790
6247
|
const fm = parsed;
|
|
5791
6248
|
if (fm.id && isRequirementArtifact2(fm)) {
|
|
@@ -5814,11 +6271,11 @@ function buildRequirementObject(content, fm, body, filePath, relFile) {
|
|
|
5814
6271
|
}
|
|
5815
6272
|
function parseRequirementFile(filePath, rootDir) {
|
|
5816
6273
|
try {
|
|
5817
|
-
const content =
|
|
6274
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
5818
6275
|
const extracted = extractParsedFrontmatter(content);
|
|
5819
6276
|
if (!extracted) return null;
|
|
5820
6277
|
const body = content.substring(extracted.matchLen);
|
|
5821
|
-
const relFile =
|
|
6278
|
+
const relFile = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
5822
6279
|
return buildRequirementObject(content, extracted.fm, body, filePath, relFile);
|
|
5823
6280
|
} catch {
|
|
5824
6281
|
return null;
|
|
@@ -5830,7 +6287,7 @@ function isIgnoredScanPath(relFile) {
|
|
|
5830
6287
|
function collectRequirements(rootDir) {
|
|
5831
6288
|
const reqs = [];
|
|
5832
6289
|
for (const file of walkMdFiles(rootDir)) {
|
|
5833
|
-
const relFile =
|
|
6290
|
+
const relFile = path4.relative(rootDir, file).replace(/\\/g, "/");
|
|
5834
6291
|
if (isIgnoredScanPath(relFile)) continue;
|
|
5835
6292
|
const req = parseRequirementFile(file, rootDir);
|
|
5836
6293
|
if (req) reqs.push(req);
|
|
@@ -6189,10 +6646,10 @@ function isPathExcluded2(relPath, patterns) {
|
|
|
6189
6646
|
function walkProjectFiles(dir, rootDir, excludedPatterns = DEFAULT_EXCLUDED_PATTERNS) {
|
|
6190
6647
|
const results = [];
|
|
6191
6648
|
try {
|
|
6192
|
-
const entries =
|
|
6649
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
6193
6650
|
for (const entry of entries) {
|
|
6194
|
-
const fullPath =
|
|
6195
|
-
const relPath =
|
|
6651
|
+
const fullPath = path4.join(dir, entry.name);
|
|
6652
|
+
const relPath = path4.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
6196
6653
|
if (isPathExcluded2(relPath, excludedPatterns)) continue;
|
|
6197
6654
|
if (entry.isDirectory()) {
|
|
6198
6655
|
results.push(...walkProjectFiles(fullPath, rootDir, excludedPatterns));
|
|
@@ -6221,8 +6678,8 @@ function pickFirstString(...vals) {
|
|
|
6221
6678
|
}
|
|
6222
6679
|
function parseGitleaksItem(item, rootDir) {
|
|
6223
6680
|
const rawFile = pickFirstString(item.File, item.file);
|
|
6224
|
-
const filePath =
|
|
6225
|
-
const relPath =
|
|
6681
|
+
const filePath = path4.resolve(rootDir, rawFile);
|
|
6682
|
+
const relPath = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6226
6683
|
const ruleId = pickFirstString(item.RuleID, item.ruleID) || "GITLEAKS-MATCH";
|
|
6227
6684
|
const rawSecret = pickFirstString(item.Secret, item.secret, item.Match);
|
|
6228
6685
|
const line = item.StartLine ?? item.line ?? 1;
|
|
@@ -6295,7 +6752,7 @@ function scanLineForSecrets(line, lineNumber, filePath, relPath, entropyThreshol
|
|
|
6295
6752
|
function scanContentForSecrets(content, filePath, rootDir, entropyThreshold = 4.3) {
|
|
6296
6753
|
const violations = [];
|
|
6297
6754
|
const lines = content.split(/\r?\n/);
|
|
6298
|
-
const relPath =
|
|
6755
|
+
const relPath = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6299
6756
|
for (let i = 0; i < lines.length; i++) {
|
|
6300
6757
|
violations.push(...scanLineForSecrets(lines[i], i + 1, filePath, relPath, entropyThreshold));
|
|
6301
6758
|
}
|
|
@@ -6315,7 +6772,7 @@ function scanDiffItems(diffLines, rootDir, excludePatterns, entropyThreshold) {
|
|
|
6315
6772
|
const findings = [];
|
|
6316
6773
|
for (const [relFile, items] of fileGroups) {
|
|
6317
6774
|
if (isPathExcluded2(relFile, excludePatterns)) continue;
|
|
6318
|
-
const absPath =
|
|
6775
|
+
const absPath = path4.resolve(rootDir, relFile);
|
|
6319
6776
|
for (const item of items) {
|
|
6320
6777
|
const lineViolations = scanLineForSecrets(item.content, item.lineNumber, absPath, relFile, entropyThreshold);
|
|
6321
6778
|
for (const v of lineViolations) {
|
|
@@ -6330,7 +6787,7 @@ function scanAllProjectFiles(rootDir, excludePatterns, entropyThreshold) {
|
|
|
6330
6787
|
const findings = [];
|
|
6331
6788
|
for (const file of allFiles) {
|
|
6332
6789
|
try {
|
|
6333
|
-
const content =
|
|
6790
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
6334
6791
|
findings.push(...scanContentForSecrets(content, file, rootDir, entropyThreshold));
|
|
6335
6792
|
} catch {
|
|
6336
6793
|
}
|
|
@@ -6554,8 +7011,8 @@ function runSemgrepCli(rootDir) {
|
|
|
6554
7011
|
const parsed = JSON.parse(output);
|
|
6555
7012
|
if (Array.isArray(parsed.results)) {
|
|
6556
7013
|
for (const item of parsed.results) {
|
|
6557
|
-
const filePath =
|
|
6558
|
-
const relPath =
|
|
7014
|
+
const filePath = path4.resolve(rootDir, item.path);
|
|
7015
|
+
const relPath = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6559
7016
|
violations.push({
|
|
6560
7017
|
filePath,
|
|
6561
7018
|
relPath,
|
|
@@ -6582,15 +7039,15 @@ function isSastExcluded(relPath, excludes) {
|
|
|
6582
7039
|
function walkSourceFiles(dir, rootDir, excludes = DEFAULT_SAST_EXCLUDES) {
|
|
6583
7040
|
const files = [];
|
|
6584
7041
|
try {
|
|
6585
|
-
const entries =
|
|
7042
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
6586
7043
|
for (const entry of entries) {
|
|
6587
|
-
const fullPath =
|
|
6588
|
-
const relPath =
|
|
7044
|
+
const fullPath = path4.join(dir, entry.name);
|
|
7045
|
+
const relPath = path4.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
6589
7046
|
if (isSastExcluded(relPath, excludes)) continue;
|
|
6590
7047
|
if (entry.isDirectory()) {
|
|
6591
7048
|
files.push(...walkSourceFiles(fullPath, rootDir, excludes));
|
|
6592
7049
|
} else if (entry.isFile()) {
|
|
6593
|
-
const ext =
|
|
7050
|
+
const ext = path4.extname(entry.name).toLowerCase();
|
|
6594
7051
|
if (GENERAL_SOURCE_EXTENSIONS.includes(ext)) {
|
|
6595
7052
|
files.push(fullPath);
|
|
6596
7053
|
}
|
|
@@ -6611,11 +7068,11 @@ function checkRuleMatch(rule, line, ext, minSeverity) {
|
|
|
6611
7068
|
}
|
|
6612
7069
|
function scanFileForSast(filePath, rootDir, minSeverity = "HIGH") {
|
|
6613
7070
|
const violations = [];
|
|
6614
|
-
const ext =
|
|
7071
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
6615
7072
|
try {
|
|
6616
|
-
const content =
|
|
7073
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
6617
7074
|
const lines = content.split(/\r?\n/);
|
|
6618
|
-
const relPath =
|
|
7075
|
+
const relPath = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6619
7076
|
for (let i = 0; i < lines.length; i++) {
|
|
6620
7077
|
const line = lines[i];
|
|
6621
7078
|
if (isLineExemptFromSast(line)) continue;
|
|
@@ -6641,8 +7098,8 @@ function scanFileForSast(filePath, rootDir, minSeverity = "HIGH") {
|
|
|
6641
7098
|
function collectSastFiles(rootDir, targetDirs) {
|
|
6642
7099
|
const files = [];
|
|
6643
7100
|
for (const tDir of targetDirs) {
|
|
6644
|
-
const fullDir =
|
|
6645
|
-
if (
|
|
7101
|
+
const fullDir = path4.join(rootDir, tDir);
|
|
7102
|
+
if (fs7.existsSync(fullDir)) {
|
|
6646
7103
|
files.push(...walkSourceFiles(fullDir, rootDir));
|
|
6647
7104
|
}
|
|
6648
7105
|
}
|
|
@@ -6797,7 +7254,7 @@ function calculateRating(mi, cc) {
|
|
|
6797
7254
|
return "F";
|
|
6798
7255
|
}
|
|
6799
7256
|
function detectLanguage(filePath) {
|
|
6800
|
-
const ext =
|
|
7257
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
6801
7258
|
switch (ext) {
|
|
6802
7259
|
case ".ts":
|
|
6803
7260
|
return "TypeScript";
|
|
@@ -6822,7 +7279,7 @@ function detectLanguage(filePath) {
|
|
|
6822
7279
|
}
|
|
6823
7280
|
function parseJsonSummaryFile(filePath, relSource) {
|
|
6824
7281
|
try {
|
|
6825
|
-
const raw = JSON.parse(
|
|
7282
|
+
const raw = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
6826
7283
|
const total = raw.total;
|
|
6827
7284
|
if (!total || typeof total !== "object") return null;
|
|
6828
7285
|
const linesPct = Number(total.lines?.pct);
|
|
@@ -6845,7 +7302,7 @@ function updateLcovCounts(line, counts) {
|
|
|
6845
7302
|
}
|
|
6846
7303
|
function parseLcovFile(filePath, relSource) {
|
|
6847
7304
|
try {
|
|
6848
|
-
const lines =
|
|
7305
|
+
const lines = fs7.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
6849
7306
|
const counts = { LF: 0, LH: 0, BRF: 0, BRH: 0, FNF: 0, FNH: 0 };
|
|
6850
7307
|
for (const line of lines) {
|
|
6851
7308
|
updateLcovCounts(line, counts);
|
|
@@ -6865,16 +7322,16 @@ function parseLcovFile(filePath, relSource) {
|
|
|
6865
7322
|
function readCodeCoverageSummary(rootDir = process.cwd()) {
|
|
6866
7323
|
const jsonCandidates = ["coverage/coverage-summary.json", "reports/coverage-summary.json"];
|
|
6867
7324
|
for (const rel of jsonCandidates) {
|
|
6868
|
-
const full =
|
|
6869
|
-
if (
|
|
7325
|
+
const full = path4.join(rootDir, rel);
|
|
7326
|
+
if (fs7.existsSync(full)) {
|
|
6870
7327
|
const parsed = parseJsonSummaryFile(full, rel);
|
|
6871
7328
|
if (parsed) return parsed;
|
|
6872
7329
|
}
|
|
6873
7330
|
}
|
|
6874
7331
|
const lcovCandidates = ["coverage/lcov.info", "lcov.info", "reports/lcov.info"];
|
|
6875
7332
|
for (const rel of lcovCandidates) {
|
|
6876
|
-
const full =
|
|
6877
|
-
if (
|
|
7333
|
+
const full = path4.join(rootDir, rel);
|
|
7334
|
+
if (fs7.existsSync(full)) {
|
|
6878
7335
|
const parsed = parseLcovFile(full, rel);
|
|
6879
7336
|
if (parsed) return parsed;
|
|
6880
7337
|
}
|
|
@@ -7010,9 +7467,9 @@ function renderQualityReportMarkdown(gateResult, coverage, accumulated, averages
|
|
|
7010
7467
|
return [...header, "", "---", "", ...table, "", "---", "", ...polyglot, ...violationsSection, "", EVALUATION_STANDARDS_MD].join("\n");
|
|
7011
7468
|
}
|
|
7012
7469
|
function writeQualityReportFile(rootDir, markdown) {
|
|
7013
|
-
const reportsDir =
|
|
7014
|
-
|
|
7015
|
-
|
|
7470
|
+
const reportsDir = path4.join(rootDir, "reports");
|
|
7471
|
+
fs7.mkdirSync(reportsDir, { recursive: true });
|
|
7472
|
+
fs7.writeFileSync(path4.join(reportsDir, "QUALITY_REPORT.md"), markdown, "utf-8");
|
|
7016
7473
|
}
|
|
7017
7474
|
function generateQualityReport(options = {}) {
|
|
7018
7475
|
const rootDir = options.rootDir || process.cwd();
|
|
@@ -7154,12 +7611,12 @@ Flujo de Integraci\xF3n (Merges):`,
|
|
|
7154
7611
|
};
|
|
7155
7612
|
}
|
|
7156
7613
|
function resolveVersionFromProposal(changeDir) {
|
|
7157
|
-
const proposalFile =
|
|
7158
|
-
if (!
|
|
7159
|
-
const propContent =
|
|
7614
|
+
const proposalFile = path4.join(changeDir, "proposal.md");
|
|
7615
|
+
if (!fs7.existsSync(proposalFile)) return "1.0.0";
|
|
7616
|
+
const propContent = fs7.readFileSync(proposalFile, "utf-8");
|
|
7160
7617
|
const propFm = propContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7161
7618
|
if (!propFm) return "1.0.0";
|
|
7162
|
-
const parsedProp =
|
|
7619
|
+
const parsedProp = yaml3.load(propFm[1]);
|
|
7163
7620
|
return parsedProp?.version ? String(parsedProp.version) : "1.0.0";
|
|
7164
7621
|
}
|
|
7165
7622
|
function resolveChangeVersion(fm, changeDir) {
|
|
@@ -7173,12 +7630,12 @@ function isTaskMatchingId(currentId, cleanTaskId) {
|
|
|
7173
7630
|
return currentLower === targetLower || currentLower.startsWith(`${targetLower}-`) || targetLower.startsWith(`${currentLower}-`);
|
|
7174
7631
|
}
|
|
7175
7632
|
function parseTasksFrontmatter(tasksFile) {
|
|
7176
|
-
if (!
|
|
7633
|
+
if (!fs7.existsSync(tasksFile)) return null;
|
|
7177
7634
|
try {
|
|
7178
|
-
const content =
|
|
7635
|
+
const content = fs7.readFileSync(tasksFile, "utf-8");
|
|
7179
7636
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7180
7637
|
if (!fmMatch) return null;
|
|
7181
|
-
const fm =
|
|
7638
|
+
const fm = yaml3.load(fmMatch[1]);
|
|
7182
7639
|
return fm && Array.isArray(fm.tasks) ? fm : null;
|
|
7183
7640
|
} catch {
|
|
7184
7641
|
return null;
|
|
@@ -7197,7 +7654,7 @@ function buildTaskMatch(currentId, currentTitle, rawChangeId, changeDir, version
|
|
|
7197
7654
|
};
|
|
7198
7655
|
}
|
|
7199
7656
|
function inspectChangeTasks(changeDir, entryName, cleanTaskId, availableTasks) {
|
|
7200
|
-
const fm = parseTasksFrontmatter(
|
|
7657
|
+
const fm = parseTasksFrontmatter(path4.join(changeDir, "tasks.md"));
|
|
7201
7658
|
if (!fm || !fm.tasks) return void 0;
|
|
7202
7659
|
const rawChangeId = resolveRawChangeId(fm, entryName);
|
|
7203
7660
|
const versionStr = resolveChangeVersion(fm, changeDir);
|
|
@@ -7218,16 +7675,16 @@ function locateTaskInActiveChanges(taskId, rootDir = process.cwd()) {
|
|
|
7218
7675
|
if (!cleanTaskId) {
|
|
7219
7676
|
return { found: false, error: "Debe especificar un identificador de tarea v\xE1lido (ej. TSK-001)." };
|
|
7220
7677
|
}
|
|
7221
|
-
const activeChangesDir =
|
|
7222
|
-
if (!
|
|
7678
|
+
const activeChangesDir = path4.join(rootDir, "specs", "changes", "active");
|
|
7679
|
+
if (!fs7.existsSync(activeChangesDir)) {
|
|
7223
7680
|
return { found: false, error: `No se encontr\xF3 el directorio de cambios activos en '${activeChangesDir}'.` };
|
|
7224
7681
|
}
|
|
7225
7682
|
const availableTasks = [];
|
|
7226
|
-
const candidateDirs =
|
|
7683
|
+
const candidateDirs = fs7.readdirSync(activeChangesDir, { withFileTypes: true });
|
|
7227
7684
|
let matchedTask;
|
|
7228
7685
|
for (const entry of candidateDirs) {
|
|
7229
7686
|
if (!entry.isDirectory()) continue;
|
|
7230
|
-
const changeDir =
|
|
7687
|
+
const changeDir = path4.join(activeChangesDir, entry.name);
|
|
7231
7688
|
const m = inspectChangeTasks(changeDir, entry.name, cleanTaskId, availableTasks);
|
|
7232
7689
|
if (m && !matchedTask) {
|
|
7233
7690
|
matchedTask = m;
|
|
@@ -7291,7 +7748,7 @@ function resolveBranchHierarchy(match, rootDir = process.cwd()) {
|
|
|
7291
7748
|
const branches = getGitBranchList(rootDir);
|
|
7292
7749
|
const cleanVersion = (match.version || "1.0.0").replace(/^v/, "");
|
|
7293
7750
|
const releaseBranch = detectExistingReleaseBranch(branches, cleanVersion);
|
|
7294
|
-
const dirName =
|
|
7751
|
+
const dirName = path4.basename(match.changeDir);
|
|
7295
7752
|
const changeSlug = dirName.replace(/^(chg|CHG)-/i, (m) => m.toUpperCase());
|
|
7296
7753
|
const featureBranch = detectExistingFeatureBranch(branches, dirName, match.changeId, changeSlug);
|
|
7297
7754
|
const parentIdMatch = featureBranch.replace(/^(feat|feature)\//i, "").match(/^[a-zA-Z0-9]+-[a-zA-Z0-9]+/);
|
|
@@ -7598,13 +8055,13 @@ function getGitConfigValue(key, cwd) {
|
|
|
7598
8055
|
}
|
|
7599
8056
|
function readSessionTelemetry(cwd) {
|
|
7600
8057
|
const possiblePaths = [
|
|
7601
|
-
|
|
7602
|
-
|
|
8058
|
+
path4.join(cwd, ".ai-sdlc", "session-telemetry.json"),
|
|
8059
|
+
path4.join(cwd, ".ai-sdlc", "agent-session.json")
|
|
7603
8060
|
];
|
|
7604
8061
|
for (const p of possiblePaths) {
|
|
7605
|
-
if (
|
|
8062
|
+
if (fs7.existsSync(p)) {
|
|
7606
8063
|
try {
|
|
7607
|
-
const raw =
|
|
8064
|
+
const raw = fs7.readFileSync(p, "utf-8");
|
|
7608
8065
|
return JSON.parse(raw);
|
|
7609
8066
|
} catch {
|
|
7610
8067
|
}
|
|
@@ -7824,20 +8281,20 @@ if [ -f "$TELEMETRY_FILE" ]; then
|
|
|
7824
8281
|
fi
|
|
7825
8282
|
`;
|
|
7826
8283
|
function installGitHooks(rootDir = process.cwd()) {
|
|
7827
|
-
const gitDir =
|
|
7828
|
-
if (!
|
|
8284
|
+
const gitDir = path4.join(rootDir, ".git");
|
|
8285
|
+
if (!fs7.existsSync(gitDir)) {
|
|
7829
8286
|
return {
|
|
7830
8287
|
success: false,
|
|
7831
8288
|
error: `No se encontr\xF3 el directorio Git en '${gitDir}'.`
|
|
7832
8289
|
};
|
|
7833
8290
|
}
|
|
7834
|
-
const hooksDir =
|
|
7835
|
-
if (!
|
|
7836
|
-
|
|
8291
|
+
const hooksDir = path4.join(gitDir, "hooks");
|
|
8292
|
+
if (!fs7.existsSync(hooksDir)) {
|
|
8293
|
+
fs7.mkdirSync(hooksDir, { recursive: true });
|
|
7837
8294
|
}
|
|
7838
|
-
const hookPath =
|
|
8295
|
+
const hookPath = path4.join(hooksDir, "prepare-commit-msg");
|
|
7839
8296
|
try {
|
|
7840
|
-
|
|
8297
|
+
fs7.writeFileSync(hookPath, PREPARE_COMMIT_MSG_SCRIPT, { encoding: "utf-8", mode: 493 });
|
|
7841
8298
|
return {
|
|
7842
8299
|
success: true,
|
|
7843
8300
|
hookPath
|
|
@@ -8149,16 +8606,16 @@ function formatReleaseKpiMarkdown(report) {
|
|
|
8149
8606
|
].join("\n");
|
|
8150
8607
|
}
|
|
8151
8608
|
function writeReleaseKpiReport(report, options = {}) {
|
|
8152
|
-
const root = options.outputDir || (options.rootDir ?
|
|
8153
|
-
if (!
|
|
8154
|
-
|
|
8609
|
+
const root = options.outputDir || (options.rootDir ? path4.join(options.rootDir, "reports", "releases") : path4.join(process.cwd(), "reports", "releases"));
|
|
8610
|
+
if (!fs7.existsSync(root)) {
|
|
8611
|
+
fs7.mkdirSync(root, { recursive: true });
|
|
8155
8612
|
}
|
|
8156
8613
|
const cleanBranchName = report.releaseBranch.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
8157
|
-
const mdPath =
|
|
8158
|
-
const jsonPath =
|
|
8614
|
+
const mdPath = path4.join(root, `RELEASE_KPIS_${cleanBranchName}.md`);
|
|
8615
|
+
const jsonPath = path4.join(root, `release_${cleanBranchName}.kpis.json`);
|
|
8159
8616
|
const mdContent = formatReleaseKpiMarkdown(report);
|
|
8160
|
-
|
|
8161
|
-
|
|
8617
|
+
fs7.writeFileSync(mdPath, mdContent, "utf-8");
|
|
8618
|
+
fs7.writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
8162
8619
|
return { markdownPath: mdPath, jsonPath };
|
|
8163
8620
|
}
|
|
8164
8621
|
var cachedScript = null;
|
|
@@ -8167,19 +8624,19 @@ function resolveCytoscapePath() {
|
|
|
8167
8624
|
try {
|
|
8168
8625
|
const req = createRequire(import.meta.url);
|
|
8169
8626
|
const entryPath = req.resolve("cytoscape");
|
|
8170
|
-
const dir =
|
|
8171
|
-
const minPath =
|
|
8172
|
-
if (
|
|
8627
|
+
const dir = path4.dirname(entryPath);
|
|
8628
|
+
const minPath = path4.join(dir, "cytoscape.min.js");
|
|
8629
|
+
if (fs7.existsSync(minPath)) {
|
|
8173
8630
|
return minPath;
|
|
8174
8631
|
}
|
|
8175
8632
|
} catch {
|
|
8176
8633
|
}
|
|
8177
8634
|
const searchDirs = [
|
|
8178
|
-
|
|
8179
|
-
|
|
8635
|
+
path4.join(process.cwd(), "node_modules", "cytoscape", "dist", "cytoscape.min.js"),
|
|
8636
|
+
path4.join(process.cwd(), "packages", "core", "node_modules", "cytoscape", "dist", "cytoscape.min.js")
|
|
8180
8637
|
];
|
|
8181
8638
|
for (const candidate of searchDirs) {
|
|
8182
|
-
if (
|
|
8639
|
+
if (fs7.existsSync(candidate)) {
|
|
8183
8640
|
return candidate;
|
|
8184
8641
|
}
|
|
8185
8642
|
}
|
|
@@ -8192,7 +8649,7 @@ function getCytoscapeScript() {
|
|
|
8192
8649
|
const scriptPath = resolveCytoscapePath();
|
|
8193
8650
|
if (scriptPath) {
|
|
8194
8651
|
try {
|
|
8195
|
-
cachedScript =
|
|
8652
|
+
cachedScript = fs7.readFileSync(scriptPath, "utf-8");
|
|
8196
8653
|
return cachedScript;
|
|
8197
8654
|
} catch {
|
|
8198
8655
|
}
|
|
@@ -8276,13 +8733,13 @@ function extractFrontmatter2(fileContent) {
|
|
|
8276
8733
|
const match = fileContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
8277
8734
|
if (!match) return null;
|
|
8278
8735
|
try {
|
|
8279
|
-
return
|
|
8736
|
+
return yaml3.load(match[1]) || null;
|
|
8280
8737
|
} catch {
|
|
8281
8738
|
return null;
|
|
8282
8739
|
}
|
|
8283
8740
|
}
|
|
8284
8741
|
function toScannedArtifact(filePath, relPath) {
|
|
8285
|
-
const fileContent =
|
|
8742
|
+
const fileContent = fs7.readFileSync(filePath, "utf-8");
|
|
8286
8743
|
const fm = extractFrontmatter2(fileContent);
|
|
8287
8744
|
const id = typeof fm?.id === "string" ? fm.id.trim() : "";
|
|
8288
8745
|
if (!id) return null;
|
|
@@ -8294,12 +8751,12 @@ function toScannedArtifact(filePath, relPath) {
|
|
|
8294
8751
|
return { id, type: classified.type, title, layer: classified.layer, filePath: relPath, content, frontmatter: fm };
|
|
8295
8752
|
}
|
|
8296
8753
|
function parseAndCatalogArtifactFile(rootDir, filePath, scannedMap, archViewByBasename) {
|
|
8297
|
-
const relPath =
|
|
8754
|
+
const relPath = path4.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
8298
8755
|
if (relPath.includes("/changes/") || relPath.startsWith("changes/")) return;
|
|
8299
8756
|
const scannedArt = toScannedArtifact(filePath, relPath);
|
|
8300
8757
|
if (!scannedArt) return;
|
|
8301
8758
|
if (scannedArt.layer === "architecture" && scannedArt.type === "architecture") {
|
|
8302
|
-
archViewByBasename.set(
|
|
8759
|
+
archViewByBasename.set(path4.basename(relPath), scannedArt);
|
|
8303
8760
|
} else {
|
|
8304
8761
|
scannedMap.set(scannedArt.id, scannedArt);
|
|
8305
8762
|
}
|
|
@@ -8309,8 +8766,8 @@ function scanWorkspaceArtifacts(rootDir) {
|
|
|
8309
8766
|
const archViewByBasename = /* @__PURE__ */ new Map();
|
|
8310
8767
|
const artifactDirs = ["product", "architecture", "specs", "security"];
|
|
8311
8768
|
for (const dirName of artifactDirs) {
|
|
8312
|
-
const fullDirPath =
|
|
8313
|
-
if (
|
|
8769
|
+
const fullDirPath = path4.join(rootDir, dirName);
|
|
8770
|
+
if (fs7.existsSync(fullDirPath)) {
|
|
8314
8771
|
for (const f of walkMdFiles(fullDirPath)) {
|
|
8315
8772
|
parseAndCatalogArtifactFile(rootDir, f, scannedArtifactsMap, archViewByBasename);
|
|
8316
8773
|
}
|
|
@@ -8368,9 +8825,9 @@ function addTraceArchEdges(r, status, archViewByBasename, nodeMap, addEdge) {
|
|
|
8368
8825
|
}
|
|
8369
8826
|
}
|
|
8370
8827
|
function loadTestContentPreview(filePath) {
|
|
8371
|
-
if (!
|
|
8828
|
+
if (!fs7.existsSync(filePath)) return void 0;
|
|
8372
8829
|
try {
|
|
8373
|
-
const testContent =
|
|
8830
|
+
const testContent = fs7.readFileSync(filePath, "utf-8");
|
|
8374
8831
|
const lang = filePath.endsWith(".feature") ? "gherkin" : "typescript";
|
|
8375
8832
|
const preview = testContent.substring(0, 1500) + (testContent.length > 1500 ? "\n... (truncated)" : "");
|
|
8376
8833
|
return "```" + lang + "\n" + preview + "\n```";
|
|
@@ -8380,10 +8837,10 @@ function loadTestContentPreview(filePath) {
|
|
|
8380
8837
|
}
|
|
8381
8838
|
function ensureTestNode(t, testId, rootDir, nodeMap) {
|
|
8382
8839
|
if (nodeMap.has(testId)) return;
|
|
8383
|
-
const fullTestPath =
|
|
8840
|
+
const fullTestPath = path4.isAbsolute(t) ? t : path4.join(rootDir, t);
|
|
8384
8841
|
nodeMap.set(testId, {
|
|
8385
8842
|
id: testId,
|
|
8386
|
-
label:
|
|
8843
|
+
label: path4.basename(t),
|
|
8387
8844
|
title: t,
|
|
8388
8845
|
type: t.endsWith(".feature") ? "bdd-feature" : "code-test",
|
|
8389
8846
|
layer: "test",
|
|
@@ -10418,9 +10875,9 @@ var IGNORED_SCAN_DIRS = /* @__PURE__ */ new Set([
|
|
|
10418
10875
|
".agent"
|
|
10419
10876
|
]);
|
|
10420
10877
|
function readKpiFilesInDir(dir) {
|
|
10421
|
-
if (!
|
|
10878
|
+
if (!fs7.existsSync(dir)) return [];
|
|
10422
10879
|
try {
|
|
10423
|
-
return
|
|
10880
|
+
return fs7.readdirSync(dir).filter((f) => f.endsWith(".kpis.json")).map((f) => path4.join(dir, f));
|
|
10424
10881
|
} catch {
|
|
10425
10882
|
return [];
|
|
10426
10883
|
}
|
|
@@ -10428,11 +10885,11 @@ function readKpiFilesInDir(dir) {
|
|
|
10428
10885
|
function scanSubdirKpiFiles(dir, depth, acc) {
|
|
10429
10886
|
if (depth > 4) return;
|
|
10430
10887
|
try {
|
|
10431
|
-
const entries =
|
|
10888
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
10432
10889
|
for (const entry of entries) {
|
|
10433
10890
|
if (!entry.isDirectory() || IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
10434
|
-
const sub =
|
|
10435
|
-
if (entry.name === "releases" &&
|
|
10891
|
+
const sub = path4.join(dir, entry.name);
|
|
10892
|
+
if (entry.name === "releases" && path4.basename(dir) === "reports") {
|
|
10436
10893
|
acc.push(...readKpiFilesInDir(sub));
|
|
10437
10894
|
} else {
|
|
10438
10895
|
scanSubdirKpiFiles(sub, depth + 1, acc);
|
|
@@ -10442,7 +10899,7 @@ function scanSubdirKpiFiles(dir, depth, acc) {
|
|
|
10442
10899
|
}
|
|
10443
10900
|
}
|
|
10444
10901
|
function findKpiJsonFiles(rootDir) {
|
|
10445
|
-
const primaryFiles = readKpiFilesInDir(
|
|
10902
|
+
const primaryFiles = readKpiFilesInDir(path4.join(rootDir, "reports", "releases"));
|
|
10446
10903
|
if (primaryFiles.length > 0) return primaryFiles;
|
|
10447
10904
|
const found = [];
|
|
10448
10905
|
scanSubdirKpiFiles(rootDir, 0, found);
|
|
@@ -10453,7 +10910,7 @@ function collectHistoricalKpis(rootDir) {
|
|
|
10453
10910
|
const kpiFiles = findKpiJsonFiles(rootDir);
|
|
10454
10911
|
for (const fullPath of kpiFiles) {
|
|
10455
10912
|
try {
|
|
10456
|
-
const content =
|
|
10913
|
+
const content = fs7.readFileSync(fullPath, "utf-8");
|
|
10457
10914
|
const report = JSON.parse(content);
|
|
10458
10915
|
if (!items.some((it) => it.id === report.releaseBranch && it.timestamp === report.generatedAt)) {
|
|
10459
10916
|
items.push({
|
|
@@ -10650,15 +11107,15 @@ function computeGateAverages(gateResults) {
|
|
|
10650
11107
|
}
|
|
10651
11108
|
function generateDashboardReport(options = {}) {
|
|
10652
11109
|
const rootDir = options.rootDir || process.cwd();
|
|
10653
|
-
const outputPath = options.outputPath ||
|
|
11110
|
+
const outputPath = options.outputPath || path4.join(rootDir, "reports", "dashboard.html");
|
|
10654
11111
|
const graphData = buildGraphElements(rootDir);
|
|
10655
11112
|
const metricsData = collectDashboardMetrics(rootDir);
|
|
10656
11113
|
const html = renderDashboardHtml(graphData, metricsData, options.title);
|
|
10657
|
-
const outDir =
|
|
10658
|
-
if (!
|
|
10659
|
-
|
|
11114
|
+
const outDir = path4.dirname(outputPath);
|
|
11115
|
+
if (!fs7.existsSync(outDir)) {
|
|
11116
|
+
fs7.mkdirSync(outDir, { recursive: true });
|
|
10660
11117
|
}
|
|
10661
|
-
|
|
11118
|
+
fs7.writeFileSync(outputPath, html, "utf-8");
|
|
10662
11119
|
const { bddCoverageRatio, traceabilityRatio } = computeSummaryRatios(metricsData);
|
|
10663
11120
|
const { avgMi, avgCc } = computeGateAverages(metricsData.gate.results);
|
|
10664
11121
|
return {
|
|
@@ -10754,18 +11211,25 @@ var SPECIALIZED_AGENT_TEMPLATES = [
|
|
|
10754
11211
|
{
|
|
10755
11212
|
id: "agent-qa-engineer",
|
|
10756
11213
|
title: "QA Engineer and SDET",
|
|
10757
|
-
description: "Translate approved requirements (FR-*, SEC-REQ-*, QR-*) into comprehensive FAILING (red) BDD/Gherkin and unit test suites before production code is written.",
|
|
11214
|
+
description: "Translate approved requirements (FR-*, SEC-REQ-*, QR-*) into comprehensive FAILING (red) BDD/Gherkin, End-to-End (E2E), UI, and unit test suites before production code is written.",
|
|
10758
11215
|
globs: "tests/**, specs/changes/**/spec.md",
|
|
10759
11216
|
body: `## Role & Mission
|
|
10760
11217
|
- **Role**: QA Engineer and SDET (Software Development Engineer in Test) Agent (\`agent-qa-engineer\`).
|
|
10761
|
-
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin test suites before \`agent-developer\` writes a single line of production code.
|
|
11218
|
+
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin, End-to-End (E2E), UI, and unit test suites before \`agent-developer\` writes a single line of production code.
|
|
10762
11219
|
|
|
10763
11220
|
## Operational Directives & Guardrails
|
|
10764
11221
|
- Read exclusively requirements cited in the \`handoff.yaml\` sidecar (\`HOF-*\`) and their referenced artifacts.
|
|
10765
|
-
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
11222
|
+
- **Agreed Project Technology**: Always use the testing frameworks, E2E/UI runners, and technology stack agreed upon for the project (as defined in \`ADR-*\`, \`design.md\`, \`package.json\`, or project configuration) if present, without requiring the user to specify it in the prompt.
|
|
11223
|
+
- **Mandatory End-to-End (E2E) & User Interface (UI) Testing**: Never forget or omit End-to-End (E2E) and User Interface (UI) tests. Automatically include E2E and UI test suites alongside BDD/integration tests whenever requirements involve user-facing flows or interfaces, so the user never has to remind you in the prompt.
|
|
11224
|
+
- **Per-Interactive-Element Exhaustive Testing**: Create specific tests for **every** UI element with which the user can interact (buttons, inputs, selects, forms, modals, links, toggles, tabs, etc.), explicitly evaluating for each element:
|
|
11225
|
+
1. **Happy Path**: nominal user interaction with valid inputs producing the expected UI state/response.
|
|
11226
|
+
2. **Sad Path**: error handling, invalid interactions, validation rejections, disabled/blocked states, and failure recovery.
|
|
11227
|
+
3. **Boundary Values**: minimum/maximum lengths, numeric extremes, empty/whitespace inputs, edge selections, and rapid/repeated interactions.
|
|
11228
|
+
- For each \`FR-*\` requirement, generate the three mandatory test categories tagged with \`@nominal\`, \`@boundary\`, and \`@invalid\`:
|
|
11229
|
+
1. **Nominal Case** (\`@nominal\` \`Scenario\`): happy path with valid inputs within expected boundaries.
|
|
11230
|
+
2. **Boundary Cases** (\`@boundary\` \`Scenario Outline\` + \`Examples\` table): values at the extremes of the accepted range.
|
|
11231
|
+
3. **Out-of-Range / Invalid Cases** (\`@invalid\` \`Scenario Outline\` + \`Examples\` table): null, empty, wrong types, values exceeding bounds.
|
|
11232
|
+
- **Use Case Variant Coverage**: Inspect every \`UC-*\` in \`handoff.yaml\` and generate BDD scenarios covering 100% of its \`alternative-flows\` and \`exception-flows\`.
|
|
10769
11233
|
- Apply conditional categories when applicable: Security (\`@security @mitigation\`), Performance (\`@performance\`), Idempotency (\`@idempotence\`), Postconditions, and Interface Contracts.
|
|
10770
11234
|
- **Zero-Mock Integration Rule**: When an \`FR-*\` spans UI and Backend, unit tests mocking \`fetch\`/HTTP in isolation are insufficient; include integration/contract or E2E test scenarios verifying real request/response wiring and UI state transitions (\`Loading\`, \`Empty\`, \`Error\`, \`Nominal\`).
|
|
10771
11235
|
- Tag all scenarios with \`@<FR-ID> @automated @regression\`.
|
|
@@ -10782,9 +11246,21 @@ var SPECIALIZED_AGENT_TEMPLATES = [
|
|
|
10782
11246
|
- **Role**: High-Precision Software Developer / Coder Agent (\`agent-developer\`).
|
|
10783
11247
|
- **Mission**: Implement atomic tasks from SDD specifications (\`tasks.md\`) delivering clean, strictly typed, and tested code.
|
|
10784
11248
|
|
|
11249
|
+
## Mandatory 4-Step Implementation Order
|
|
11250
|
+
Execute every implementation task following this strict sequential order:
|
|
11251
|
+
1. **Clean Architecture Definition / Redefinition**: Define or refine the module/layer architecture adhering to Clean Architecture patterns (Domain/Entities, Application/Use Cases, Interface Adapters, Frameworks/Drivers), strictly enforcing the Dependency Rule (dependencies point inward toward the domain).
|
|
11252
|
+
2. **Inter-Module Interfaces First**: Define explicit interfaces/ports (contracts, DTOs, types) for all interactions across module boundaries before writing concrete logic, avoiding direct coupling between modules.
|
|
11253
|
+
3. **Component Implementation**: Implement the concrete components, use cases, and adapters that implement and consume the defined interfaces.
|
|
11254
|
+
4. **Exhaustive Per-Function Unit Testing**: Write unit tests for every function in \`tests/unit/\`, always verifying:
|
|
11255
|
+
- **Happy Path**: nominal execution with valid inputs.
|
|
11256
|
+
- **Sad Path**: controlled error paths, rejections, and business exceptions.
|
|
11257
|
+
- **Boundary Values**: minimum, maximum, exact limits, and off-by-one edges.
|
|
11258
|
+
- **Unexpected, Null & Empty Values**: \`null\`, \`undefined\`, empty strings/arrays/objects, and malformed inputs.
|
|
11259
|
+
|
|
10785
11260
|
## Operational Directives & Guardrails
|
|
10786
11261
|
- Respect the autonomy mode assigned to each task in \`tasks.md\` (\`AUTONOMOUS\`, \`HUMAN_REVIEW_PLAN\`, \`AMBIGUOUS\`, \`HIGH_RISK_MANUAL\`).
|
|
10787
|
-
-
|
|
11262
|
+
- **100% Design & Handoff Scope Coverage in Tasks**: Ensure every task in \`tasks.md\` declares \`implements-components\` (\`CMP-*\`), \`satisfies-requirements\` (\`FR-*\`, \`SEC-REQ-*\`), and \`implements-contracts\` so 100% of \`design.md\` and \`handoff.yaml\` is mapped and implemented.
|
|
11263
|
+
- Apply Test-Driven Development (TDD): make failing tests delivered by \`agent-qa-engineer\` pass green without altering tests to accommodate buggy code, and write direct unit tests in \`tests/unit/\` for every exported function and contract method declared in \`design.md\` or implemented in \`src/\`.
|
|
10788
11264
|
- Cite implemented \`FR-*\` or \`SEC-REQ-*\` IDs in test header comments to maintain 360\xB0 reverse traceability.
|
|
10789
11265
|
- **Zero Fake-Wiring Guardrail**: Strictly forbidden from marking a task \`COMPLETED\` with hardcoded \`mockData\` arrays in production views, \`setTimeout\` simulating network calls, or empty UI event handlers (\`onClick={() => {}}\`, \`onSubmit={() => {}}\`). Frontend components must wire directly to the shared contract and real Backend endpoints.
|
|
10790
11266
|
- **Mandatory 4 UI States Matrix**: Every data-driven UI component or screen must explicitly implement all 4 usability states: \`Loading\` (spinner/skeleton + disabled submit), \`Empty\` (clear message + CTA), \`Error\` (visible error recovery banner, never silent failure), and \`Nominal/Success\` (accessible semantic DOM + keyboard navigation).
|
|
@@ -10978,11 +11454,14 @@ This file defines the operational rules and specialized role mappings for Google
|
|
|
10978
11454
|
- Emit a return handoff to \`agent-threat-modeler\` in the technical security feedback loop when infrastructure or persistence decisions introduce new attack vectors.
|
|
10979
11455
|
|
|
10980
11456
|
### 4. \`agent-qa-engineer\` (QA Engineer and SDET)
|
|
10981
|
-
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin test suites, before \`agent-developer\` writes production code.
|
|
11457
|
+
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin, End-to-End (E2E), UI, and unit test suites, before \`agent-developer\` writes production code.
|
|
10982
11458
|
- **Input**: Approved requirements from the \`handoff.yaml\` sidecar (\`HOF-*\`).
|
|
10983
|
-
- **Output**: Failing Gherkin
|
|
11459
|
+
- **Output**: Failing Gherkin, E2E, and UI test suites covering: nominal case, boundary cases, out-of-range cases, per-interactive-element tests (Happy Path, Sad Path, Boundary Values), and conditional categories (security, performance, idempotency, postconditions, interface contract).
|
|
10984
11460
|
- **Guardrails**:
|
|
10985
11461
|
- FORBIDDEN to include production code or anticipate implementations.
|
|
11462
|
+
- **Agreed Project Technology**: Always use the testing frameworks, E2E/UI runners, and technology stack agreed upon for the project (if any), without requiring the user to specify it in the prompt.
|
|
11463
|
+
- **Mandatory E2E & UI Testing**: Never forget or omit End-to-End (E2E) and User Interface (UI) tests.
|
|
11464
|
+
- **Per-Interactive-Element Coverage**: Create specific tests for every UI element the user can interact with, covering Happy Path, Sad Path, and Boundary Values for each one.
|
|
10986
11465
|
- Every \`FR-*\` must have at least nominal + boundary + out-of-range scenarios; missing these blocks handoff to \`agent-developer\`.
|
|
10987
11466
|
- Mandatory tags: \`@<FR-ID> @automated @regression\`.
|
|
10988
11467
|
- Run \`pnpm run verify:testing\` before emitting handoff.
|
|
@@ -10990,6 +11469,7 @@ This file defines the operational rules and specialized role mappings for Google
|
|
|
10990
11469
|
### 5. \`agent-developer\` (Software Developer)
|
|
10991
11470
|
- **Mission**: Implement atomic tasks from SDD specifications (\`tasks.md\`) with clean, strictly typed code and thorough tests.
|
|
10992
11471
|
- **Directives**:
|
|
11472
|
+
- **Mandatory 4-Step Implementation Order**: (1) Define/refine Clean Architecture layers & boundaries, (2) Define interfaces/ports for all inter-module interactions to avoid direct module coupling, (3) Implement components consuming/fulfilling those interfaces, (4) Write unit tests for every function covering Happy Path, Sad Path, Boundary Values, and Unexpected/Null/Empty values.
|
|
10993
11473
|
- Respect the autonomy mode assigned in \`tasks.md\` (\`AUTONOMOUS\`, \`HUMAN_REVIEW_PLAN\`, \`AMBIGUOUS\`, \`HIGH_RISK_MANUAL\`).
|
|
10994
11474
|
- Make tests delivered by \`agent-qa-engineer\` pass green without altering them to accommodate code.
|
|
10995
11475
|
- Respect \`quality-policy.yaml\` thresholds: CC $\\le 10$, Cognitive $\\le 15$, MI $\\ge 50$, LOC $\\le 40$.
|
|
@@ -11088,7 +11568,7 @@ Before finalizing changes or proposing commits:
|
|
|
11088
11568
|
| \`agent-product-analyst\` | Phase 0 NAF v4 Enterprise & Capability Vision (\`ARCH-INTRO-*\`, 3\u20135 question interview) + ProductShape artifacts (\`ACT-*\`, \`UC-*\`, \`FR-*\`, \`QR-*\`, \`BR-*\`); status starts \`draft\`. |
|
|
11089
11569
|
| \`agent-threat-modeler\` | STRIDE/OWASP threat triad (\`ACT-THREAT-*\`, \`ABUSE-*\`, \`SEC-REQ-*\`). |
|
|
11090
11570
|
| \`agent-system-architect\` | Modular arc42 + NAF v4 architecture (\`CMP-*\`, Mermaid diagrams, \`ADR-*\`). |
|
|
11091
|
-
| \`agent-qa-engineer\` | FAILING BDD/Gherkin suites before \`agent-developer\` writes code. |
|
|
11571
|
+
| \`agent-qa-engineer\` | FAILING BDD/Gherkin, E2E, and UI suites (using agreed project tech; Happy Path, Sad Path & Boundary Values per interactive UI element) before \`agent-developer\` writes code. |
|
|
11092
11572
|
| \`agent-developer\` | Atomic tasks from \`tasks.md\`; respects autonomy mode; runs \`pnpm run check:fix\` + \`pnpm run verify:all\`. |
|
|
11093
11573
|
| \`agent-security-auditor\` | Adversarial PR security audit, SAST, secrets, and CVSS v3.1 evaluation. |
|
|
11094
11574
|
| \`agent-compliance-checker\` | SPDX license compliance audit against \`license-policy.yaml\`. |
|
|
@@ -11155,7 +11635,8 @@ Canonical Reference: [\`process/09_agent_protocols.md\`](process/09_agent_protoc
|
|
|
11155
11635
|
- **Function Length**: $\\le 40$ lines (warning if $> 30$).
|
|
11156
11636
|
- If a function exceeds these limits, decompose logic into cohesive helper functions before completing the task.
|
|
11157
11637
|
|
|
11158
|
-
## 2. TDD Methodology
|
|
11638
|
+
## 2. TDD Methodology, Clean Architecture & Unit Testing
|
|
11639
|
+
- **Mandatory 4-Step Implementation Order**: (1) Define/refine Clean Architecture layers & boundaries, (2) Define interfaces/ports for all inter-module interactions to avoid direct module coupling, (3) Implement components consuming/fulfilling those interfaces, (4) Write unit tests for every function covering Happy Path, Sad Path, Boundary Values, and Unexpected/Null/Empty values.
|
|
11159
11640
|
- Apply Test-Driven Development: generate unit tests before or alongside component logic.
|
|
11160
11641
|
- Tag tests linked to requirements with header comments citing the ID (\`FR-*\` or \`SEC-REQ-*\`) to enable reverse traceability.
|
|
11161
11642
|
- Suppressing types (\`any\`), linter rules (\`eslint-disable\`), or unit tests to force pipeline passage is prohibited.
|
|
@@ -11228,12 +11709,12 @@ npx tsx packages/cli/src/index.ts sdd integrate --change <chg-id>
|
|
|
11228
11709
|
- **Guardrails**: Declare \`satisfies-requirements\` on every \`CMP-*\` and trigger the technical security feedback loop toward \`agent-threat-modeler\` when infrastructure decisions introduce new attack vectors.
|
|
11229
11710
|
|
|
11230
11711
|
### 4. \`agent-qa-engineer\` (QA Engineer and SDET)
|
|
11231
|
-
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin test suites, before \`agent-developer\` writes production code.
|
|
11232
|
-
- **Guardrails**: FORBIDDEN to include production code. Mandatory tags: \`@<FR-ID> @automated @regression\`.
|
|
11712
|
+
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin, End-to-End (E2E), UI, and unit test suites, before \`agent-developer\` writes production code.
|
|
11713
|
+
- **Guardrails**: Always use the agreed project technology/testing stack (if any). Never omit End-to-End (E2E) and UI tests. Create specific tests for every interactive UI element covering Happy Path, Sad Path, and Boundary Values. FORBIDDEN to include production code. Mandatory tags: \`@<FR-ID> @automated @regression\`.
|
|
11233
11714
|
|
|
11234
11715
|
### 5. \`agent-developer\` (Software Developer)
|
|
11235
11716
|
- **Mission**: Implement atomic tasks from SDD specifications (\`tasks.md\`) with clean, strictly typed code and thorough tests.
|
|
11236
|
-
- **Directives**: Respect the autonomy mode. Make tests delivered by \`agent-qa-engineer\` pass green. Run \`pnpm run check:fix\` and \`pnpm run verify:all\`.
|
|
11717
|
+
- **Directives**: Execute the mandatory 4-step order: (1) Clean Architecture definition/redefinition, (2) Inter-module interfaces first, (3) Component implementation, (4) Per-function unit tests (Happy Path, Sad Path, Boundary Values, Unexpected/Null/Empty values). Respect the autonomy mode. Make tests delivered by \`agent-qa-engineer\` pass green. Run \`pnpm run check:fix\` and \`pnpm run verify:all\`.
|
|
11237
11718
|
|
|
11238
11719
|
### 6. \`agent-security-auditor\` (Adversarial Code Auditor)
|
|
11239
11720
|
- **Mission**: Examine Pull Requests for business logic vulnerabilities, injection vectors, secret leaks, and authorization flaws as part of the Pre-Merge Audit Triad.
|
|
@@ -11378,7 +11859,7 @@ Non-Negotiable Quality Thresholds (\`quality-policy.yaml\`):
|
|
|
11378
11859
|
| \`agent-product-analyst\` | Phase 0 NAF v4 Enterprise & Capability Vision (\`ARCH-INTRO-*\`, 3\u20135 question interview) + ProductShape artifacts (\`ACT-*\`, \`UC-*\`, \`FR-*\`, \`QR-*\`, \`BR-*\`); \`status\` starts \`draft\`. |
|
|
11379
11860
|
| \`agent-threat-modeler\` | STRIDE/OWASP threat triad (\`ACT-THREAT-*\`, \`ABUSE-*\`, \`SEC-REQ-*\`). |
|
|
11380
11861
|
| \`agent-system-architect\` | Modular arc42 + NAF v4 architecture (\`CMP-*\`, Mermaid diagrams, \`ADR-*\`). |
|
|
11381
|
-
| \`agent-qa-engineer\` | FAILING BDD/Gherkin suites before \`agent-developer\` writes production code. |
|
|
11862
|
+
| \`agent-qa-engineer\` | FAILING BDD/Gherkin, E2E, and UI suites (using agreed project tech; Happy Path, Sad Path & Boundary Values per interactive UI element) before \`agent-developer\` writes production code. |
|
|
11382
11863
|
| \`agent-developer\` | Atomic tasks from \`tasks.md\`; autonomy mode must be respected; runs \`pnpm run check:fix\` + \`pnpm run verify:all\`. |
|
|
11383
11864
|
| \`agent-security-auditor\` | Adversarial PR security audit, SAST, secrets, and CVSS v3.1 evaluation. |
|
|
11384
11865
|
| \`agent-compliance-checker\` | SPDX license compliance audit against \`license-policy.yaml\`. |
|
|
@@ -12804,14 +13285,14 @@ function collectAgentFiles(targets) {
|
|
|
12804
13285
|
function writeProjectFiles(destDir, files, dryRun) {
|
|
12805
13286
|
const filesCreated = [];
|
|
12806
13287
|
for (const f of files) {
|
|
12807
|
-
const fullFilePath =
|
|
12808
|
-
const parentDir =
|
|
13288
|
+
const fullFilePath = path4.join(destDir, f.relPath);
|
|
13289
|
+
const parentDir = path4.dirname(fullFilePath);
|
|
12809
13290
|
if (!dryRun) {
|
|
12810
|
-
if (!
|
|
12811
|
-
|
|
13291
|
+
if (!fs7.existsSync(parentDir)) {
|
|
13292
|
+
fs7.mkdirSync(parentDir, { recursive: true });
|
|
12812
13293
|
}
|
|
12813
|
-
if (!
|
|
12814
|
-
|
|
13294
|
+
if (!fs7.existsSync(fullFilePath)) {
|
|
13295
|
+
fs7.writeFileSync(fullFilePath, f.content, "utf-8");
|
|
12815
13296
|
filesCreated.push(f.relPath);
|
|
12816
13297
|
}
|
|
12817
13298
|
} else {
|
|
@@ -12884,7 +13365,7 @@ function getCiWorkflowFiles(ciProvider) {
|
|
|
12884
13365
|
function createDirectoriesOnDisk(destDir, directories, dryRun) {
|
|
12885
13366
|
const created = [];
|
|
12886
13367
|
for (const d of directories) {
|
|
12887
|
-
if (!dryRun)
|
|
13368
|
+
if (!dryRun) fs7.mkdirSync(path4.join(destDir, d), { recursive: true });
|
|
12888
13369
|
created.push(d);
|
|
12889
13370
|
}
|
|
12890
13371
|
return created;
|
|
@@ -12902,13 +13383,13 @@ function collectStarterFiles(ciProvider, agentFiles = [], archGranularity = "min
|
|
|
12902
13383
|
...getCiWorkflowFiles(ciProvider),
|
|
12903
13384
|
...agentFiles,
|
|
12904
13385
|
...getArchitectureTemplates(archGranularity).map((t) => ({
|
|
12905
|
-
relPath:
|
|
13386
|
+
relPath: path4.join("templates", "architecture", t.filename).replace(/\\/g, "/"),
|
|
12906
13387
|
content: t.content
|
|
12907
13388
|
}))
|
|
12908
13389
|
];
|
|
12909
13390
|
}
|
|
12910
13391
|
function tryInstallGitHook(destDir, dryRun) {
|
|
12911
|
-
if (!dryRun &&
|
|
13392
|
+
if (!dryRun && fs7.existsSync(path4.join(destDir, ".git"))) {
|
|
12912
13393
|
return installGitHooks(destDir).success;
|
|
12913
13394
|
}
|
|
12914
13395
|
return false;
|
|
@@ -12950,7 +13431,7 @@ function buildInitSuccessResult(destDir, directoriesCreated, filesCreated, ciPro
|
|
|
12950
13431
|
}
|
|
12951
13432
|
function initProject(options = {}) {
|
|
12952
13433
|
const baseDir = options.rootDir || process.cwd();
|
|
12953
|
-
const destDir =
|
|
13434
|
+
const destDir = path4.resolve(baseDir, options.targetDir || ".");
|
|
12954
13435
|
const ciRes = validateCiProvider(options.ci);
|
|
12955
13436
|
if (!ciRes.valid) return buildInitError(destDir, ciRes.error);
|
|
12956
13437
|
const archRes = validateArchitectureGranularity(options.architecture);
|
|
@@ -12972,6 +13453,6 @@ function initProject(options = {}) {
|
|
|
12972
13453
|
);
|
|
12973
13454
|
}
|
|
12974
13455
|
|
|
12975
|
-
export { ALL_ARCHITECTURE_TEMPLATES, BRANCH_PATTERNS, DEFAULT_EXCLUDED_PATTERNS, DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS, DEFAULT_SAST_EXCLUDES, GENERAL_SOURCE_EXTENSIONS, KPI_SUMMARY_END_TAG, KPI_SUMMARY_START_TAG, OpenSpecAdapter, PREPARE_COMMIT_MSG_SCRIPT, PROMPT_INJECTION_SIGNATURES, PROTECTED_BYPASS_PATTERNS, SAST_RULES, SECRET_RULES, SPECIALIZED_AGENT_TEMPLATES, STARTER_ANTIGRAVITY_MCP, STARTER_ANTIGRAVITY_RULES, STARTER_ARCH_ADR, STARTER_ARCH_ARCHITECTURE_CONSTRAINTS, STARTER_ARCH_COMPONENT, STARTER_ARCH_CONTEXT_AND_SCOPE, STARTER_ARCH_CROSS_CUTTING_CONCEPTS, STARTER_ARCH_DEPLOYMENT_VIEW, STARTER_ARCH_GLOSSARY, STARTER_ARCH_INTRODUCTION_AND_GOALS, STARTER_ARCH_LEVEL_1_WHITEBOX, STARTER_ARCH_QUALITY_REQUIREMENTS, STARTER_ARCH_RISKS_AND_TECHNICAL_DEBT, STARTER_ARCH_RUNTIME_VIEW, STARTER_ARCH_SOLUTION_STRATEGY, STARTER_AZURE_PIPELINES, STARTER_BITBUCKET_PIPELINES, STARTER_CLAUDE_RULES, STARTER_COPILOT_RULES, STARTER_CURSOR_CORE_RULES, STARTER_CURSOR_MCP, STARTER_CURSOR_PRODUCT_RULES, STARTER_CURSOR_QUALITY_RULES, STARTER_GITHUB_CI, STARTER_GITHUB_SDD_INTEGRATE, STARTER_GITLAB_CI, STARTER_LICENSE_POLICY, STARTER_QUALITY_POLICY, STARTER_REQUIREMENT_TEMPLATE, STARTER_TASK_TEMPLATE, STARTER_VSCODE_MCP, STARTER_WORKFLOW_HANDOFF_TEMPLATE, SpecKitAdapter, TYPE_TO_SCHEMA_MAP, VALID_AGENT_TARGETS, VALID_ARCHITECTURE_GRANULARITIES, VALID_AUTONOMY_MODES, VALID_CHANGE_PROFILES, VALID_CI_PROVIDERS, aggregateCommitKpiRecords, aggregatePrKpis, aggregateReleaseKpis, appendRevisionHistoryRow, buildCycloneDxComponent, buildFeatureFileContent, buildGraphElements, calculateJaccardSimilarity, calculateMetrics, calculateRating, calculateShannonEntropy, checkGherkinInSync, checkoutTaskBranch, classifyBranch, collectAgentFiles, collectDashboardMetrics, collectHistoricalKpis, computeCanonicalSha256, computeSha256, createDefaultQualityPolicy, depositProductHandoffSidecar, detectActiveChangeForIntegration, detectAuthorIdentity, detectLanguage, detectProfileFromChange, detectPromptInjection, emitCiOutput, evaluateCompoundLicense, evaluateSingleDependency, extractCitations, extractCommitKpisFromRange, extractDirectDepNames, extractFeatureTags, extractFrontmatter, extractFunctions, extractFunctionsBraceLanguages, extractFunctionsPolyglot, extractFunctionsPython, extractFunctionsTypeScriptAst, extractGherkinBlock, extractGherkinFeatures, extractNormativeText, findChangeWorkspaceDir, findLicenseFile, formatCommitTrailers, formatPrKpiMarkdown, formatReleaseKpiMarkdown, generateBranchHierarchyPlan, generateCycloneDxSbom, generateDashboardReport, generateDuplicatesReportMarkdown, generateGovernanceReportMarkdown, generateLicenseReportMarkdown, generatePdacReportMarkdown, generateQualityReport, generateSastReportMarkdown, generateSecretsReportMarkdown, generateTestingCoverageReportMarkdown, generateThirdPartyNotices, generateTraceabilityReportMarkdown, getArchitectureTemplates, getCytoscapeScript, getGitDiffLines, getNextCorrelativeNumber, getSddAdapter, inferArtifactTypeFromId, inferMissingTrailers, initProject, injectKpisIntoPrBody, installGitHooks, integrateSddChange, isGitleaksInstalled, isPathProtectedFromPatch, isPlaceholderOrIgnored, isRequirementArtifact2 as isRequirementArtifact, isSemgrepInstalled, isToolAvailable, loadProductHandoffSidecar, locateTaskInActiveChanges, maskSecret, normalizeSpdxLicense, parseCommitTrailers, parseFrontmatter, parseFrontmatterId, parseGenericDoc, parseLicensePolicy, parseMarkdownFrontmatter, parseQualityPolicy, parseRequirementFile, parseSpecCatalogScope, parseTasksDoc, parseTasksFromContent, processPackageDir, readCodeCoverageSummary, readPackageJson, renderAntigravityAgentFile, renderAutonomyModeSection, renderClaudeAgentFile, renderCopilotAgentFile, renderCursorAgentFile, renderDashboardHtml, renderManualsTab, resolveAgentTargets, resolveBranchHierarchy, resolveCiEnvironment, resolveCytoscapePath, resolveFeatureFilePath, resolveGitRef, resolveLicenseFromFile, resolveSchemaForArtifact, resolveTargetFiles, runGitleaksCli, runSemgrepCli, scaffoldSddChange, scanAllProductHandoffs, scanAllTestArtifacts, scanContentForSecrets, scanFileForSast, scanInstalledLicenses, scanManualsInWorkspace, scanPnpmStore, scanSubdirectories, scanWithNativeFs, scanWithSyft, scanWithTrivy, slugify, syncPdacDigests, tokenizeTitle, updateCitationsInContent, validateAgainstSchema, validateArtifactSchema, verifyArtifactDuplicates, verifyArtifactsSchemas, verifyLicenses, verifyManuals, verifyPdacGraph, verifyProgressiveFriction, verifyQualityGate, verifySast, verifySddIntegration, verifySecrets, verifyTasksGovernance, verifyTestingCoverage, verifyTraceability, walkDir, walkMdFiles, walkProjectFiles, walkSourceFiles, walkTaskFiles, writeCycloneDxSbom, writeReleaseKpiReport, writeThirdPartyNotices };
|
|
13456
|
+
export { ALL_ARCHITECTURE_TEMPLATES, BRANCH_PATTERNS, DEFAULT_EXCLUDED_PATTERNS, DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS, DEFAULT_SAST_EXCLUDES, GENERAL_SOURCE_EXTENSIONS, KPI_SUMMARY_END_TAG, KPI_SUMMARY_START_TAG, OpenSpecAdapter, PREPARE_COMMIT_MSG_SCRIPT, PROMPT_INJECTION_SIGNATURES, PROTECTED_BYPASS_PATTERNS, SAST_RULES, SECRET_RULES, SPECIALIZED_AGENT_TEMPLATES, STARTER_ANTIGRAVITY_MCP, STARTER_ANTIGRAVITY_RULES, STARTER_ARCH_ADR, STARTER_ARCH_ARCHITECTURE_CONSTRAINTS, STARTER_ARCH_COMPONENT, STARTER_ARCH_CONTEXT_AND_SCOPE, STARTER_ARCH_CROSS_CUTTING_CONCEPTS, STARTER_ARCH_DEPLOYMENT_VIEW, STARTER_ARCH_GLOSSARY, STARTER_ARCH_INTRODUCTION_AND_GOALS, STARTER_ARCH_LEVEL_1_WHITEBOX, STARTER_ARCH_QUALITY_REQUIREMENTS, STARTER_ARCH_RISKS_AND_TECHNICAL_DEBT, STARTER_ARCH_RUNTIME_VIEW, STARTER_ARCH_SOLUTION_STRATEGY, STARTER_AZURE_PIPELINES, STARTER_BITBUCKET_PIPELINES, STARTER_CLAUDE_RULES, STARTER_COPILOT_RULES, STARTER_CURSOR_CORE_RULES, STARTER_CURSOR_MCP, STARTER_CURSOR_PRODUCT_RULES, STARTER_CURSOR_QUALITY_RULES, STARTER_GITHUB_CI, STARTER_GITHUB_SDD_INTEGRATE, STARTER_GITLAB_CI, STARTER_LICENSE_POLICY, STARTER_QUALITY_POLICY, STARTER_REQUIREMENT_TEMPLATE, STARTER_TASK_TEMPLATE, STARTER_VSCODE_MCP, STARTER_WORKFLOW_HANDOFF_TEMPLATE, SpecKitAdapter, TYPE_TO_SCHEMA_MAP, VALID_AGENT_TARGETS, VALID_ARCHITECTURE_GRANULARITIES, VALID_AUTONOMY_MODES, VALID_CHANGE_PROFILES, VALID_CI_PROVIDERS, aggregateCommitKpiRecords, aggregatePrKpis, aggregateReleaseKpis, analyzeBddVariantsForRequirement, appendRevisionHistoryRow, buildCycloneDxComponent, buildFeatureFileContent, buildGraphElements, calculateJaccardSimilarity, calculateMetrics, calculateRating, calculateShannonEntropy, checkGherkinInSync, checkoutTaskBranch, classifyBranch, collectAgentFiles, collectDashboardMetrics, collectHistoricalKpis, computeCanonicalSha256, computeSha256, createDefaultQualityPolicy, depositProductHandoffSidecar, detectActiveChangeForIntegration, detectAuthorIdentity, detectLanguage, detectProfileFromChange, detectPromptInjection, emitCiOutput, evaluateCompoundLicense, evaluateSingleDependency, extractCitations, extractCommitKpisFromRange, extractDirectDepNames, extractFeatureTags, extractFrontmatter, extractFunctions, extractFunctionsBraceLanguages, extractFunctionsPolyglot, extractFunctionsPython, extractFunctionsTypeScriptAst, extractGherkinBlock, extractGherkinFeatures, extractNormativeText, findChangeWorkspaceDir, findLicenseFile, formatCommitTrailers, formatPrKpiMarkdown, formatReleaseKpiMarkdown, generateBranchHierarchyPlan, generateCycloneDxSbom, generateDashboardReport, generateDuplicatesReportMarkdown, generateGovernanceReportMarkdown, generateLicenseReportMarkdown, generatePdacReportMarkdown, generateQualityReport, generateSastReportMarkdown, generateSecretsReportMarkdown, generateTestingCoverageReportMarkdown, generateThirdPartyNotices, generateTraceabilityReportMarkdown, getArchitectureTemplates, getCytoscapeScript, getGitDiffLines, getNextCorrelativeNumber, getSddAdapter, inferArtifactTypeFromId, inferMissingTrailers, initProject, injectKpisIntoPrBody, installGitHooks, integrateSddChange, isGitleaksInstalled, isPathProtectedFromPatch, isPlaceholderOrIgnored, isRequirementArtifact2 as isRequirementArtifact, isSemgrepInstalled, isToolAvailable, loadProductHandoffSidecar, locateTaskInActiveChanges, maskSecret, normalizeSpdxLicense, parseCommitTrailers, parseFrontmatter, parseFrontmatterId, parseGenericDoc, parseLicensePolicy, parseMarkdownFrontmatter, parseQualityPolicy, parseRequirementFile, parseSpecCatalogScope, parseTasksDoc, parseTasksFromContent, processPackageDir, readCodeCoverageSummary, readPackageJson, renderAntigravityAgentFile, renderAutonomyModeSection, renderClaudeAgentFile, renderCopilotAgentFile, renderCursorAgentFile, renderDashboardHtml, renderManualsTab, resolveAgentTargets, resolveBranchHierarchy, resolveCiEnvironment, resolveCytoscapePath, resolveFeatureFilePath, resolveGitRef, resolveLicenseFromFile, resolveSchemaForArtifact, resolveTargetFiles, runGitleaksCli, runSemgrepCli, scaffoldSddChange, scanAllProductHandoffs, scanAllTestArtifacts, scanContentForSecrets, scanFileForSast, scanInstalledLicenses, scanManualsInWorkspace, scanPnpmStore, scanSubdirectories, scanWithNativeFs, scanWithSyft, scanWithTrivy, slugify, syncPdacDigests, tokenizeTitle, updateCitationsInContent, validateAgainstSchema, validateArtifactSchema, verifyArtifactDuplicates, verifyArtifactsSchemas, verifyLicenses, verifyManuals, verifyPdacGraph, verifyProgressiveFriction, verifyQualityGate, verifySast, verifySddIntegration, verifySecrets, verifyTasksGovernance, verifyTestingCoverage, verifyTraceability, walkDir, walkMdFiles, walkProjectFiles, walkSourceFiles, walkTaskFiles, writeCycloneDxSbom, writeReleaseKpiReport, writeThirdPartyNotices };
|
|
12976
13457
|
//# sourceMappingURL=index.js.map
|
|
12977
13458
|
//# sourceMappingURL=index.js.map
|