@aisdlc/core 1.4.1 → 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.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
3
|
+
var fs7 = require('fs');
|
|
4
|
+
var path4 = require('path');
|
|
5
|
+
var yaml3 = require('js-yaml');
|
|
6
6
|
var tsMorph = require('ts-morph');
|
|
7
7
|
var crypto = require('crypto');
|
|
8
8
|
var child_process = require('child_process');
|
|
@@ -29,9 +29,9 @@ function _interopNamespace(e) {
|
|
|
29
29
|
return Object.freeze(n);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
var
|
|
33
|
-
var
|
|
34
|
-
var
|
|
32
|
+
var fs7__namespace = /*#__PURE__*/_interopNamespace(fs7);
|
|
33
|
+
var path4__namespace = /*#__PURE__*/_interopNamespace(path4);
|
|
34
|
+
var yaml3__default = /*#__PURE__*/_interopDefault(yaml3);
|
|
35
35
|
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
36
36
|
var child_process__namespace = /*#__PURE__*/_interopNamespace(child_process);
|
|
37
37
|
|
|
@@ -198,7 +198,7 @@ function buildNodeMetrics(node, filePath) {
|
|
|
198
198
|
return { functionName, filePath, loc, cyclomatic, cognitive, maintainability: mi, codeSmells };
|
|
199
199
|
}
|
|
200
200
|
function createTempSourceFile(project, content, filePath) {
|
|
201
|
-
const ext =
|
|
201
|
+
const ext = path4__namespace.extname(filePath).toLowerCase() || ".ts";
|
|
202
202
|
const fileName = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}${ext}`;
|
|
203
203
|
return project.createSourceFile(fileName, content, { overwrite: true });
|
|
204
204
|
}
|
|
@@ -235,7 +235,7 @@ function extractFunctionsTypeScriptAst(content, filePath) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
function extractFunctionsPolyglot(content, filePath) {
|
|
238
|
-
const ext =
|
|
238
|
+
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
239
239
|
return ext === ".py" ? extractPythonFunctions(content, filePath) : extractBraceFunctions(content, filePath);
|
|
240
240
|
}
|
|
241
241
|
function isDocstringToggle(trimmed) {
|
|
@@ -312,7 +312,7 @@ function processBraceLine(line, state, blocks, lineIndex, ext) {
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
function extractBraceFunctions(content, filePath) {
|
|
315
|
-
const ext =
|
|
315
|
+
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
316
316
|
const lines = content.split("\n");
|
|
317
317
|
const blocks = [];
|
|
318
318
|
const state = { currentFn: null, braceCount: 0, fnLines: [], inBlockComment: false };
|
|
@@ -457,7 +457,7 @@ function countCommentsAndLoc(rawLines, isPy) {
|
|
|
457
457
|
return { loc, commentCount, cleanLines };
|
|
458
458
|
}
|
|
459
459
|
function calculatePolyglotMetrics(body, name, filePath) {
|
|
460
|
-
const ext =
|
|
460
|
+
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
461
461
|
const rawLines = body.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
462
462
|
const { loc, commentCount, cleanLines } = countCommentsAndLoc(rawLines, ext === ".py");
|
|
463
463
|
const cyclomatic = computePolyglotCyclomatic(body, ext);
|
|
@@ -507,6 +507,652 @@ function computePolyglotCognitive(lines, isPy) {
|
|
|
507
507
|
}
|
|
508
508
|
return cognitive;
|
|
509
509
|
}
|
|
510
|
+
function walkMdFiles(dir, fileList = [], excludeDirs = [
|
|
511
|
+
"node_modules",
|
|
512
|
+
".git",
|
|
513
|
+
"dist",
|
|
514
|
+
"bin",
|
|
515
|
+
"obj",
|
|
516
|
+
"build",
|
|
517
|
+
"out",
|
|
518
|
+
"target",
|
|
519
|
+
"coverage",
|
|
520
|
+
".changeset",
|
|
521
|
+
"scratch",
|
|
522
|
+
"test-scaffold",
|
|
523
|
+
"fixtures",
|
|
524
|
+
"templates",
|
|
525
|
+
"examples",
|
|
526
|
+
".agent",
|
|
527
|
+
".agents",
|
|
528
|
+
".claude",
|
|
529
|
+
".cursor",
|
|
530
|
+
".github",
|
|
531
|
+
".vscode",
|
|
532
|
+
".vs",
|
|
533
|
+
".idea"
|
|
534
|
+
]) {
|
|
535
|
+
try {
|
|
536
|
+
if (!fs7__namespace.existsSync(dir)) return fileList;
|
|
537
|
+
const files = fs7__namespace.readdirSync(dir);
|
|
538
|
+
for (const file of files) {
|
|
539
|
+
const fullPath = path4__namespace.join(dir, file);
|
|
540
|
+
try {
|
|
541
|
+
if (fs7__namespace.statSync(fullPath).isDirectory()) {
|
|
542
|
+
if (!excludeDirs.includes(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
543
|
+
walkMdFiles(fullPath, fileList, excludeDirs);
|
|
544
|
+
}
|
|
545
|
+
} else if (file.endsWith(".md")) {
|
|
546
|
+
fileList.push(fullPath);
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
return fileList;
|
|
554
|
+
}
|
|
555
|
+
function parseMarkdownFrontmatter(content) {
|
|
556
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
557
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
558
|
+
const yamlLines = match[1].split("\n");
|
|
559
|
+
const frontmatter = {};
|
|
560
|
+
for (const line of yamlLines) {
|
|
561
|
+
const parts = line.split(":");
|
|
562
|
+
if (parts.length >= 2) {
|
|
563
|
+
const key = parts[0].trim();
|
|
564
|
+
let val = parts.slice(1).join(":").trim();
|
|
565
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
566
|
+
val = val.substring(1, val.length - 1);
|
|
567
|
+
}
|
|
568
|
+
frontmatter[key] = val;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const body = content.substring(match[0].length);
|
|
572
|
+
return { frontmatter, body };
|
|
573
|
+
}
|
|
574
|
+
function extractGherkinBlock(body) {
|
|
575
|
+
const gherkinRegex = /```gherkin\r?\n([\s\S]*?)\r?\n```/g;
|
|
576
|
+
const blocks = [];
|
|
577
|
+
let match;
|
|
578
|
+
while ((match = gherkinRegex.exec(body)) !== null) {
|
|
579
|
+
blocks.push(match[1].trim());
|
|
580
|
+
}
|
|
581
|
+
return blocks;
|
|
582
|
+
}
|
|
583
|
+
function resolveTargetFiles(rootDir, targetPath) {
|
|
584
|
+
if (targetPath) {
|
|
585
|
+
const fullTarget = path4__namespace.isAbsolute(targetPath) ? targetPath : path4__namespace.join(rootDir, targetPath);
|
|
586
|
+
if (!fs7__namespace.existsSync(fullTarget)) return [];
|
|
587
|
+
return fs7__namespace.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
588
|
+
}
|
|
589
|
+
const searchDirs = [
|
|
590
|
+
path4__namespace.join(rootDir, "product"),
|
|
591
|
+
path4__namespace.join(rootDir, "security"),
|
|
592
|
+
path4__namespace.join(rootDir, "specs"),
|
|
593
|
+
path4__namespace.join(rootDir, "examples", "product"),
|
|
594
|
+
path4__namespace.join(rootDir, "examples", "security"),
|
|
595
|
+
path4__namespace.join(rootDir, "examples", "specs")
|
|
596
|
+
];
|
|
597
|
+
return searchDirs.filter((d) => fs7__namespace.existsSync(d)).flatMap((d) => walkMdFiles(d));
|
|
598
|
+
}
|
|
599
|
+
function resolveFeatureFilePath(rootDir, filePath, id, customTarget) {
|
|
600
|
+
if (customTarget) {
|
|
601
|
+
return path4__namespace.isAbsolute(customTarget) ? customTarget : path4__namespace.join(rootDir, customTarget);
|
|
602
|
+
}
|
|
603
|
+
const baseDir = path4__namespace.join(rootDir, "tests", "features");
|
|
604
|
+
const candidateSecurity = path4__namespace.join(baseDir, "security", `${id.toLowerCase()}.feature`);
|
|
605
|
+
const candidateRoot = path4__namespace.join(baseDir, `${id.toLowerCase()}.feature`);
|
|
606
|
+
if (fs7__namespace.existsSync(candidateSecurity)) return candidateSecurity;
|
|
607
|
+
if (fs7__namespace.existsSync(candidateRoot)) return candidateRoot;
|
|
608
|
+
const subDir = filePath.includes("security") ? "security" : "";
|
|
609
|
+
return path4__namespace.join(baseDir, subDir, `${id.toLowerCase()}.feature`);
|
|
610
|
+
}
|
|
611
|
+
function buildFeatureFileContent(rootDir, filePath, id, version, gherkinBlocks) {
|
|
612
|
+
const banner = [
|
|
613
|
+
`# ==============================================================================`,
|
|
614
|
+
`# AUTO-GENERADO POR AI-SDLC (Cucumber Integration)`,
|
|
615
|
+
`# Origen: ${path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/")}`,
|
|
616
|
+
`# ID Requerimiento: ${id}`,
|
|
617
|
+
`# Versi\xF3n: ${version || "1.0.0"}`,
|
|
618
|
+
`# NO EDITAR MANUALMENTE: Cualquier cambio debe realizarse en el Markdown origen.`,
|
|
619
|
+
`# ==============================================================================`,
|
|
620
|
+
"",
|
|
621
|
+
""
|
|
622
|
+
].join("\n");
|
|
623
|
+
return banner + gherkinBlocks.join("\n\n") + "\n";
|
|
624
|
+
}
|
|
625
|
+
function countScenarios(gherkinBlocks) {
|
|
626
|
+
let count = 0;
|
|
627
|
+
for (const block of gherkinBlocks) {
|
|
628
|
+
const matches = block.match(/\b(Scenario|Escenario|Scenario Outline|Esquema del escenario):/g);
|
|
629
|
+
if (matches) count += matches.length;
|
|
630
|
+
}
|
|
631
|
+
return count;
|
|
632
|
+
}
|
|
633
|
+
function extractGherkinFeatures(options = {}) {
|
|
634
|
+
const rootDir = options.rootDir || process.cwd();
|
|
635
|
+
const features = [];
|
|
636
|
+
let totalScenarios = 0;
|
|
637
|
+
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
638
|
+
for (const filePath of targetFiles) {
|
|
639
|
+
try {
|
|
640
|
+
const content = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
641
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
642
|
+
const gherkinBlocks = extractGherkinBlock(body);
|
|
643
|
+
if (gherkinBlocks.length === 0) continue;
|
|
644
|
+
const id = frontmatter.id || path4__namespace.basename(filePath, path4__namespace.extname(filePath));
|
|
645
|
+
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
646
|
+
fs7__namespace.mkdirSync(path4__namespace.dirname(targetPath), { recursive: true });
|
|
647
|
+
const fullContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
648
|
+
fs7__namespace.writeFileSync(targetPath, fullContent, "utf-8");
|
|
649
|
+
const scenarioCount = countScenarios(gherkinBlocks);
|
|
650
|
+
totalScenarios += scenarioCount;
|
|
651
|
+
features.push({
|
|
652
|
+
sourceFile: path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/"),
|
|
653
|
+
outputFile: path4__namespace.relative(rootDir, targetPath).replace(/\\/g, "/"),
|
|
654
|
+
featureName: id,
|
|
655
|
+
scenarioCount
|
|
656
|
+
});
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return { success: true, features, totalScenarios };
|
|
661
|
+
}
|
|
662
|
+
function checkGherkinInSync(options = {}) {
|
|
663
|
+
const rootDir = options.rootDir || process.cwd();
|
|
664
|
+
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
665
|
+
const outOfSyncFiles = [];
|
|
666
|
+
const missingFiles = [];
|
|
667
|
+
let totalFeatures = 0;
|
|
668
|
+
for (const filePath of targetFiles) {
|
|
669
|
+
try {
|
|
670
|
+
const content = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
671
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
672
|
+
const gherkinBlocks = extractGherkinBlock(body);
|
|
673
|
+
if (gherkinBlocks.length === 0) continue;
|
|
674
|
+
totalFeatures++;
|
|
675
|
+
const id = frontmatter.id || path4__namespace.basename(filePath, path4__namespace.extname(filePath));
|
|
676
|
+
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
677
|
+
const relTarget = path4__namespace.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
678
|
+
if (!fs7__namespace.existsSync(targetPath)) {
|
|
679
|
+
missingFiles.push(relTarget);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const expectedContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
683
|
+
const actualContent = fs7__namespace.readFileSync(targetPath, "utf-8");
|
|
684
|
+
if (actualContent.replace(/\r\n/g, "\n") !== expectedContent.replace(/\r\n/g, "\n")) {
|
|
685
|
+
outOfSyncFiles.push(relTarget);
|
|
686
|
+
}
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
inSync: missingFiles.length === 0 && outOfSyncFiles.length === 0,
|
|
692
|
+
outOfSyncFiles,
|
|
693
|
+
missingFiles,
|
|
694
|
+
totalFeatures
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/verifiers/testing-coverage.ts
|
|
699
|
+
function parseTaskVerification(v) {
|
|
700
|
+
const verification = {};
|
|
701
|
+
if (v && typeof v === "object") {
|
|
702
|
+
const rec = v;
|
|
703
|
+
if (typeof rec["method"] === "string") verification.method = rec["method"];
|
|
704
|
+
if (typeof rec["command-or-criteria"] === "string") verification.criteria = rec["command-or-criteria"];
|
|
705
|
+
}
|
|
706
|
+
return verification;
|
|
707
|
+
}
|
|
708
|
+
function parseTaskItem(t) {
|
|
709
|
+
return {
|
|
710
|
+
id: typeof t["id"] === "string" ? t["id"] : String(t["id"] ?? ""),
|
|
711
|
+
title: typeof t["title"] === "string" ? t["title"] : void 0,
|
|
712
|
+
verification: parseTaskVerification(t["verification"])
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function parseVerifiedByTests(verifiedBy) {
|
|
716
|
+
if (Array.isArray(verifiedBy)) {
|
|
717
|
+
return verifiedBy.map(String);
|
|
718
|
+
}
|
|
719
|
+
if (typeof verifiedBy === "string") {
|
|
720
|
+
return [verifiedBy];
|
|
721
|
+
}
|
|
722
|
+
return void 0;
|
|
723
|
+
}
|
|
724
|
+
function parseGenericDoc(content) {
|
|
725
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
726
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
727
|
+
let parsed;
|
|
728
|
+
try {
|
|
729
|
+
parsed = yaml3__default.default.load(match[1]);
|
|
730
|
+
} catch {
|
|
731
|
+
return { frontmatter: {}, body: content };
|
|
732
|
+
}
|
|
733
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
734
|
+
return { frontmatter: {}, body: content };
|
|
735
|
+
}
|
|
736
|
+
const raw = parsed;
|
|
737
|
+
const rawTasks = Array.isArray(raw["tasks"]) ? raw["tasks"] : [];
|
|
738
|
+
const tasks = rawTasks.map(parseTaskItem);
|
|
739
|
+
const frontmatter = {
|
|
740
|
+
...raw,
|
|
741
|
+
tasks,
|
|
742
|
+
"verified-by-tests": parseVerifiedByTests(raw["verified-by-tests"])
|
|
743
|
+
};
|
|
744
|
+
return { frontmatter, body: content.substring(match[0].length) };
|
|
745
|
+
}
|
|
746
|
+
function buildRequirementsTable(reqAudit) {
|
|
747
|
+
const lines = [
|
|
748
|
+
`## 1. Requirements Audit (Product, Quality, and Security)`,
|
|
749
|
+
``,
|
|
750
|
+
`| Requirement ID | Title | Source File | Declared Method | Linked Test Files | Status |`,
|
|
751
|
+
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
752
|
+
];
|
|
753
|
+
for (const r of reqAudit) {
|
|
754
|
+
const icon = r.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
755
|
+
lines.push(
|
|
756
|
+
`| **\`${r.id}\`** | ${r.title} | \`${r.file}\` | \`${r.method}\` | \`${r.testRefs}\` | ${icon} |`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return lines;
|
|
760
|
+
}
|
|
761
|
+
function buildTasksTable(taskAudit) {
|
|
762
|
+
const lines = [
|
|
763
|
+
`## 2. Tasks Audit (SDD Delivery Engineering)`,
|
|
764
|
+
``,
|
|
765
|
+
`| Task ID | Title | tasks.md File | Method | Verification Command / Criterion | Status |`,
|
|
766
|
+
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
767
|
+
];
|
|
768
|
+
for (const t of taskAudit) {
|
|
769
|
+
const icon = t.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
770
|
+
lines.push(
|
|
771
|
+
`| **\`${t.id}\`** | ${t.title} | \`${t.file}\` | \`${t.method}\` | \`${t.commandOrCriteria}\` | ${icon} |`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
return lines;
|
|
775
|
+
}
|
|
776
|
+
function generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors) {
|
|
777
|
+
const totalErrors = reqErrors + taskErrors;
|
|
778
|
+
const verdict = totalErrors === 0 ? "100% VERIFIED WITH TESTS (PASSED)" : `BLOCKED: Detected ${totalErrors} items without tests (${reqErrors} Requirements, ${taskErrors} Tasks)`;
|
|
779
|
+
const reportLines = [
|
|
780
|
+
`# \u{1F9EA} Deterministic Test Coverage Audit for Requirements and Tasks`,
|
|
781
|
+
``,
|
|
782
|
+
`> **Audit Date:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
783
|
+
`> **Overall Verdict:** ${verdict}`,
|
|
784
|
+
``,
|
|
785
|
+
`---`,
|
|
786
|
+
``,
|
|
787
|
+
...buildRequirementsTable(reqAudit),
|
|
788
|
+
``,
|
|
789
|
+
`---`,
|
|
790
|
+
``,
|
|
791
|
+
...buildTasksTable(taskAudit)
|
|
792
|
+
];
|
|
793
|
+
return reportLines.join("\n");
|
|
794
|
+
}
|
|
795
|
+
var EXCLUDED_TEST_DIRS = /* @__PURE__ */ new Set([
|
|
796
|
+
"node_modules",
|
|
797
|
+
".git",
|
|
798
|
+
"dist",
|
|
799
|
+
"bin",
|
|
800
|
+
"obj",
|
|
801
|
+
"build",
|
|
802
|
+
"out",
|
|
803
|
+
"target",
|
|
804
|
+
"coverage",
|
|
805
|
+
".changeset",
|
|
806
|
+
"scratch",
|
|
807
|
+
"fixtures",
|
|
808
|
+
"templates",
|
|
809
|
+
"examples",
|
|
810
|
+
".vs",
|
|
811
|
+
".idea"
|
|
812
|
+
]);
|
|
813
|
+
var TEST_EXTENSIONS = [".feature", ".spec.ts", ".spec.js", ".test.ts", ".test.js", "_test.py", "_test.go"];
|
|
814
|
+
function isTestFile(fileName) {
|
|
815
|
+
return TEST_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
816
|
+
}
|
|
817
|
+
function recordRequirementTest(reqToTestsMap, reqId, relTest) {
|
|
818
|
+
const existing = reqToTestsMap.get(reqId) || [];
|
|
819
|
+
if (!existing.includes(relTest)) {
|
|
820
|
+
existing.push(relTest);
|
|
821
|
+
reqToTestsMap.set(reqId, existing);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function parseFeatureTags(content, relTest, reqToTestsMap) {
|
|
825
|
+
const lines = content.split("\n");
|
|
826
|
+
for (const line of lines) {
|
|
827
|
+
const trimmed = line.trim();
|
|
828
|
+
if (!trimmed.startsWith("@")) continue;
|
|
829
|
+
const tags = trimmed.split(/\s+/).filter((t) => t.startsWith("@"));
|
|
830
|
+
for (const t of tags) {
|
|
831
|
+
recordRequirementTest(reqToTestsMap, t.substring(1), relTest);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
var REQ_ID_REGEX = /\b(FR|QR|CON|SEC-REQ)-[A-Z0-9]+(-[A-Z0-9]+)*\b/g;
|
|
836
|
+
function parseCodeTestIds(content, relTest, reqToTestsMap) {
|
|
837
|
+
const idMatches = content.match(REQ_ID_REGEX);
|
|
838
|
+
if (!idMatches) return;
|
|
839
|
+
for (const reqId of idMatches) {
|
|
840
|
+
recordRequirementTest(reqToTestsMap, reqId, relTest);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function processTestFile(fullPath, rootDir, reqToTestsMap) {
|
|
844
|
+
try {
|
|
845
|
+
const content = fs7__namespace.readFileSync(fullPath, "utf-8");
|
|
846
|
+
const relTest = path4__namespace.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
847
|
+
if (fullPath.endsWith(".feature")) {
|
|
848
|
+
parseFeatureTags(content, relTest, reqToTestsMap);
|
|
849
|
+
}
|
|
850
|
+
parseCodeTestIds(content, relTest, reqToTestsMap);
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function collectTestFiles(dir, files = []) {
|
|
855
|
+
if (!fs7__namespace.existsSync(dir)) return files;
|
|
856
|
+
try {
|
|
857
|
+
const entries = fs7__namespace.readdirSync(dir, { withFileTypes: true });
|
|
858
|
+
for (const e of entries) {
|
|
859
|
+
if (e.isDirectory()) {
|
|
860
|
+
if (!EXCLUDED_TEST_DIRS.has(e.name)) {
|
|
861
|
+
collectTestFiles(path4__namespace.join(dir, e.name), files);
|
|
862
|
+
}
|
|
863
|
+
} else if (isTestFile(e.name)) {
|
|
864
|
+
files.push(path4__namespace.join(dir, e.name));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
return files;
|
|
870
|
+
}
|
|
871
|
+
function scanAllTestArtifacts(rootDir) {
|
|
872
|
+
const reqToTestsMap = /* @__PURE__ */ new Map();
|
|
873
|
+
const testFiles = collectTestFiles(rootDir);
|
|
874
|
+
for (const file of testFiles) {
|
|
875
|
+
processTestFile(file, rootDir, reqToTestsMap);
|
|
876
|
+
}
|
|
877
|
+
return reqToTestsMap;
|
|
878
|
+
}
|
|
879
|
+
var NOMINAL_KEYWORDS = /\b(nominal|happy|valid|exitoso|normal|standard|flujo principal)\b/i;
|
|
880
|
+
var BOUNDARY_KEYWORDS = /\b(boundary|limit|edge|min|max|l[ií]mite|borde|extremo|rango)\b/i;
|
|
881
|
+
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;
|
|
882
|
+
function extractScenariosForReq(gherkinText, reqId) {
|
|
883
|
+
const lines = gherkinText.split(/\r?\n/);
|
|
884
|
+
const featureApplies = gherkinText.includes(`@${reqId}`);
|
|
885
|
+
if (!featureApplies) return [];
|
|
886
|
+
const chunks = [];
|
|
887
|
+
let pendingTags = "";
|
|
888
|
+
for (const rawLine of lines) {
|
|
889
|
+
const line = rawLine.trim();
|
|
890
|
+
if (line.startsWith("@")) {
|
|
891
|
+
pendingTags += " " + line;
|
|
892
|
+
} else if (/^(Scenario|Escenario|Scenario Outline|Esquema del escenario):/i.test(line)) {
|
|
893
|
+
const isOutline = /^(Scenario Outline|Esquema del escenario):/i.test(line);
|
|
894
|
+
chunks.push({ tags: pendingTags.toLowerCase(), header: line, isOutline });
|
|
895
|
+
pendingTags = "";
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return chunks;
|
|
899
|
+
}
|
|
900
|
+
var INVALID_TAG_RE = /@(invalid|error|out-of-range)\b/i;
|
|
901
|
+
function isInvalidScenario(chunk, combined) {
|
|
902
|
+
return INVALID_TAG_RE.test(chunk.tags) || INVALID_KEYWORDS.test(combined);
|
|
903
|
+
}
|
|
904
|
+
function isBoundaryScenario(chunk, combined, isInvalid) {
|
|
905
|
+
if (chunk.tags.includes("@boundary") || BOUNDARY_KEYWORDS.test(combined)) return true;
|
|
906
|
+
return chunk.isOutline && !isInvalid;
|
|
907
|
+
}
|
|
908
|
+
function classifyScenarioChunk(chunk, cov) {
|
|
909
|
+
const combined = `${chunk.tags} ${chunk.header}`;
|
|
910
|
+
const isInvalid = isInvalidScenario(chunk, combined);
|
|
911
|
+
const isBoundary = isBoundaryScenario(chunk, combined, isInvalid);
|
|
912
|
+
const isNominal = chunk.tags.includes("@nominal") || NOMINAL_KEYWORDS.test(combined) || !isInvalid && !isBoundary;
|
|
913
|
+
if (isNominal) cov.nominal = true;
|
|
914
|
+
if (isBoundary) cov.boundary = true;
|
|
915
|
+
if (isInvalid) cov.invalid = true;
|
|
916
|
+
}
|
|
917
|
+
function analyzeBddVariantsForRequirement(reqId, gherkinSources) {
|
|
918
|
+
const cov = { nominal: false, boundary: false, invalid: false, totalScenarios: 0 };
|
|
919
|
+
for (const src of gherkinSources) {
|
|
920
|
+
const chunks = extractScenariosForReq(src, reqId);
|
|
921
|
+
cov.totalScenarios += chunks.length;
|
|
922
|
+
for (const chunk of chunks) {
|
|
923
|
+
classifyScenarioChunk(chunk, cov);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return cov;
|
|
927
|
+
}
|
|
928
|
+
function computeMissingVariants(cov) {
|
|
929
|
+
const missing = [];
|
|
930
|
+
if (!cov.nominal) missing.push("nominal");
|
|
931
|
+
if (!cov.boundary) missing.push("boundary");
|
|
932
|
+
if (!cov.invalid) missing.push("invalid");
|
|
933
|
+
return missing;
|
|
934
|
+
}
|
|
935
|
+
var REQ_PREFIXES = ["FR-", "QR-", "CON-", "SEC-REQ-"];
|
|
936
|
+
function isRequirementArtifact(frontmatter, id) {
|
|
937
|
+
if (frontmatter.type === "requirement" || frontmatter.type === "security-requirement") {
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
return REQ_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
941
|
+
}
|
|
942
|
+
function resolveRequirementMethod(declaredMethod, matchingTests) {
|
|
943
|
+
if (typeof declaredMethod === "string" && declaredMethod) {
|
|
944
|
+
return declaredMethod;
|
|
945
|
+
}
|
|
946
|
+
return matchingTests.some((t) => t.endsWith(".feature")) ? "BDD_CUCUMBER" : "TEST_EXECUTION";
|
|
947
|
+
}
|
|
948
|
+
function shouldEnforceVariantsForReq(id, frontmatter, options) {
|
|
949
|
+
if (!id.startsWith("FR-")) return false;
|
|
950
|
+
return Boolean(options.enforceBddVariants || frontmatter["enforce-bdd-variants"] === true);
|
|
951
|
+
}
|
|
952
|
+
function evaluateReqVerification(id, isTemplate, hasTests, missingVariants, frontmatter, options) {
|
|
953
|
+
if (isTemplate) return true;
|
|
954
|
+
const enforceVariants = shouldEnforceVariantsForReq(id, frontmatter, options);
|
|
955
|
+
const variantsOk = !enforceVariants || missingVariants.length === 0;
|
|
956
|
+
return hasTests && variantsOk;
|
|
957
|
+
}
|
|
958
|
+
function formatTestRefs(matchingTests, relPath, totalScenarios) {
|
|
959
|
+
if (matchingTests.length > 0) return matchingTests.join(", ");
|
|
960
|
+
return totalScenarios > 0 ? `${relPath} (gherkin)` : "NINGUNA DETECTADA";
|
|
961
|
+
}
|
|
962
|
+
function buildRequirementAuditItem(frontmatter, id, relPath, isTemplate, matchingTests, bddVariants, missingVariants, isVerified) {
|
|
963
|
+
const status = isVerified ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA";
|
|
964
|
+
return {
|
|
965
|
+
id: id || "SIN-ID",
|
|
966
|
+
title: frontmatter.title || "Sin t\xEDtulo",
|
|
967
|
+
file: relPath,
|
|
968
|
+
isTemplate,
|
|
969
|
+
method: resolveRequirementMethod(frontmatter["verifiable-by"], matchingTests),
|
|
970
|
+
testRefs: formatTestRefs(matchingTests, relPath, bddVariants.totalScenarios),
|
|
971
|
+
existingCount: matchingTests.length || bddVariants.totalScenarios,
|
|
972
|
+
bddVariants,
|
|
973
|
+
missingVariants,
|
|
974
|
+
status
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap, gherkinSources, options) {
|
|
978
|
+
const matchingTests = reqToTestsMap.get(id) || [];
|
|
979
|
+
const bddVariants = analyzeBddVariantsForRequirement(id, gherkinSources);
|
|
980
|
+
const missingVariants = id.startsWith("FR-") ? computeMissingVariants(bddVariants) : [];
|
|
981
|
+
const hasTests = matchingTests.length > 0 || bddVariants.totalScenarios > 0;
|
|
982
|
+
const isVerified = evaluateReqVerification(id, isTemplate, hasTests, missingVariants, frontmatter, options);
|
|
983
|
+
const item = buildRequirementAuditItem(
|
|
984
|
+
frontmatter,
|
|
985
|
+
id,
|
|
986
|
+
relPath,
|
|
987
|
+
isTemplate,
|
|
988
|
+
matchingTests,
|
|
989
|
+
bddVariants,
|
|
990
|
+
missingVariants,
|
|
991
|
+
isVerified
|
|
992
|
+
);
|
|
993
|
+
return { item, isError: !isVerified };
|
|
994
|
+
}
|
|
995
|
+
function parseSingleFlowEntry(entry) {
|
|
996
|
+
if (typeof entry === "string") return entry.trim();
|
|
997
|
+
if (entry && typeof entry === "object") {
|
|
998
|
+
const rec = entry;
|
|
999
|
+
return String(rec.id || rec.name || "").trim();
|
|
1000
|
+
}
|
|
1001
|
+
return "";
|
|
1002
|
+
}
|
|
1003
|
+
function extractDeclaredUseCaseFlows(frontmatter) {
|
|
1004
|
+
const flows = [];
|
|
1005
|
+
const rawLists = [frontmatter["alternative-flows"], frontmatter["exception-flows"]];
|
|
1006
|
+
for (const list of rawLists) {
|
|
1007
|
+
if (!Array.isArray(list)) continue;
|
|
1008
|
+
for (const entry of list) {
|
|
1009
|
+
const flowId = parseSingleFlowEntry(entry);
|
|
1010
|
+
if (flowId) flows.push(flowId);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return flows;
|
|
1014
|
+
}
|
|
1015
|
+
function auditUseCaseFlowItem(id, relPath, declaredFlows, combinedTestsText) {
|
|
1016
|
+
const coveredFlows = [];
|
|
1017
|
+
const missingFlows = [];
|
|
1018
|
+
const lowerCorpus = combinedTestsText.toLowerCase();
|
|
1019
|
+
for (const flow of declaredFlows) {
|
|
1020
|
+
if (lowerCorpus.includes(flow.toLowerCase())) {
|
|
1021
|
+
coveredFlows.push(flow);
|
|
1022
|
+
} else {
|
|
1023
|
+
missingFlows.push(flow);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
id,
|
|
1028
|
+
file: relPath,
|
|
1029
|
+
declaredFlows,
|
|
1030
|
+
coveredFlows,
|
|
1031
|
+
missingFlows,
|
|
1032
|
+
status: missingFlows.length === 0 ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function isTaskProofValid(criteria, isTemplate) {
|
|
1036
|
+
if (isTemplate) return true;
|
|
1037
|
+
return criteria.length > 4 && !criteria.includes("TODO") && !criteria.includes("TBD");
|
|
1038
|
+
}
|
|
1039
|
+
function auditSingleTask(t, relPath, isTemplate) {
|
|
1040
|
+
const crit = t.verification?.criteria || "";
|
|
1041
|
+
const method = t.verification?.method || "TEST_EXECUTION";
|
|
1042
|
+
const hasProof = isTaskProofValid(crit, isTemplate);
|
|
1043
|
+
return {
|
|
1044
|
+
item: {
|
|
1045
|
+
id: t.id,
|
|
1046
|
+
title: t.title || "Sin t\xEDtulo",
|
|
1047
|
+
file: relPath,
|
|
1048
|
+
method,
|
|
1049
|
+
commandOrCriteria: crit || "AUSENTE",
|
|
1050
|
+
status: hasProof ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
1051
|
+
},
|
|
1052
|
+
hasProof
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function auditTasks(tasks, relPath, isTemplate) {
|
|
1056
|
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
1057
|
+
return { items: [], errorCount: 0 };
|
|
1058
|
+
}
|
|
1059
|
+
const items = [];
|
|
1060
|
+
let errorCount = 0;
|
|
1061
|
+
for (const t of tasks) {
|
|
1062
|
+
const { item, hasProof } = auditSingleTask(t, relPath, isTemplate);
|
|
1063
|
+
if (!hasProof) errorCount++;
|
|
1064
|
+
items.push(item);
|
|
1065
|
+
}
|
|
1066
|
+
return { items, errorCount };
|
|
1067
|
+
}
|
|
1068
|
+
function collectTestCorpus(rootDir, allMdFiles) {
|
|
1069
|
+
const gherkinSources = [];
|
|
1070
|
+
const testContents = [];
|
|
1071
|
+
for (const tf of collectTestFiles(rootDir)) {
|
|
1072
|
+
try {
|
|
1073
|
+
const content = fs7__namespace.readFileSync(tf, "utf-8");
|
|
1074
|
+
testContents.push(content);
|
|
1075
|
+
if (tf.endsWith(".feature")) gherkinSources.push(content);
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
for (const md of allMdFiles) {
|
|
1080
|
+
try {
|
|
1081
|
+
const blocks = extractGherkinBlock(fs7__namespace.readFileSync(md, "utf-8"));
|
|
1082
|
+
gherkinSources.push(...blocks);
|
|
1083
|
+
testContents.push(...blocks);
|
|
1084
|
+
} catch {
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return { gherkinSources, combinedTests: testContents.join("\n") };
|
|
1088
|
+
}
|
|
1089
|
+
function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, taskErrors, ucErrors) {
|
|
1090
|
+
return {
|
|
1091
|
+
success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0,
|
|
1092
|
+
totalRequirements: reqAudit.length,
|
|
1093
|
+
passedRequirements: reqAudit.length - reqErrors,
|
|
1094
|
+
failedRequirements: reqErrors,
|
|
1095
|
+
totalTasks: taskAudit.length,
|
|
1096
|
+
passedTasks: taskAudit.length - taskErrors,
|
|
1097
|
+
failedTasks: taskErrors,
|
|
1098
|
+
requirements: reqAudit,
|
|
1099
|
+
tasks: taskAudit,
|
|
1100
|
+
useCaseFlows,
|
|
1101
|
+
failedUseCaseFlows: ucErrors,
|
|
1102
|
+
reportMarkdown: generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors)
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function recordUseCaseAuditIfPresent(frontmatter, id, relPath, isTemplate, combinedTests, state) {
|
|
1106
|
+
if (isTemplate || !(frontmatter.type === "use-case" || id.startsWith("UC-"))) return;
|
|
1107
|
+
const declaredFlows = extractDeclaredUseCaseFlows(frontmatter);
|
|
1108
|
+
if (declaredFlows.length === 0) return;
|
|
1109
|
+
const flowAudit = auditUseCaseFlowItem(id, relPath, declaredFlows, combinedTests);
|
|
1110
|
+
state.useCaseFlows.push(flowAudit);
|
|
1111
|
+
if (flowAudit.status === "FALLO_SIN_PRUEBA") state.ucErrors++;
|
|
1112
|
+
}
|
|
1113
|
+
function processMdFileForCoverage(file, rootDir, reqToTestsMap, corpus, options, state) {
|
|
1114
|
+
const { frontmatter } = parseGenericDoc(fs7__namespace.readFileSync(file, "utf-8"));
|
|
1115
|
+
const relPath = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
1116
|
+
const isTemplate = relPath.includes("templates/");
|
|
1117
|
+
const id = frontmatter.id || "";
|
|
1118
|
+
if (isRequirementArtifact(frontmatter, id)) {
|
|
1119
|
+
const audited = auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap, corpus.gherkinSources, options);
|
|
1120
|
+
state.reqAudit.push(audited.item);
|
|
1121
|
+
if (audited.isError) state.reqErrors++;
|
|
1122
|
+
}
|
|
1123
|
+
recordUseCaseAuditIfPresent(frontmatter, id, relPath, isTemplate, corpus.combinedTests, state);
|
|
1124
|
+
const auditedTasks = auditTasks(frontmatter.tasks, relPath, isTemplate);
|
|
1125
|
+
state.taskAudit.push(...auditedTasks.items);
|
|
1126
|
+
state.taskErrors += auditedTasks.errorCount;
|
|
1127
|
+
}
|
|
1128
|
+
function verifyTestingCoverage(options = {}) {
|
|
1129
|
+
const rootDir = options.rootDir || process.cwd();
|
|
1130
|
+
const allMdFiles = walkMdFiles(rootDir);
|
|
1131
|
+
const reqToTestsMap = scanAllTestArtifacts(rootDir);
|
|
1132
|
+
const corpus = collectTestCorpus(rootDir, allMdFiles);
|
|
1133
|
+
const state = {
|
|
1134
|
+
reqAudit: [],
|
|
1135
|
+
taskAudit: [],
|
|
1136
|
+
useCaseFlows: [],
|
|
1137
|
+
reqErrors: 0,
|
|
1138
|
+
taskErrors: 0,
|
|
1139
|
+
ucErrors: 0
|
|
1140
|
+
};
|
|
1141
|
+
for (const file of allMdFiles) {
|
|
1142
|
+
try {
|
|
1143
|
+
processMdFileForCoverage(file, rootDir, reqToTestsMap, corpus, options, state);
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return assembleCoverageResult(
|
|
1148
|
+
state.reqAudit,
|
|
1149
|
+
state.taskAudit,
|
|
1150
|
+
state.useCaseFlows,
|
|
1151
|
+
state.reqErrors,
|
|
1152
|
+
state.taskErrors,
|
|
1153
|
+
state.ucErrors
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
510
1156
|
|
|
511
1157
|
// src/verifiers/quality-gate.ts
|
|
512
1158
|
var DEFAULT_EXTENSIONS = [
|
|
@@ -621,7 +1267,7 @@ function applyOptionOverrides(policy, overrides) {
|
|
|
621
1267
|
function parseQualityPolicy(content, overrides) {
|
|
622
1268
|
const policy = createDefaultQualityPolicy();
|
|
623
1269
|
try {
|
|
624
|
-
const doc =
|
|
1270
|
+
const doc = yaml3__default.default.load(content);
|
|
625
1271
|
if (doc && typeof doc === "object") {
|
|
626
1272
|
const record = doc;
|
|
627
1273
|
applyDocScope(policy, record);
|
|
@@ -637,7 +1283,7 @@ function parseQualityPolicy(content, overrides) {
|
|
|
637
1283
|
return policy;
|
|
638
1284
|
}
|
|
639
1285
|
function extractFunctions(content, filePath) {
|
|
640
|
-
const ext =
|
|
1286
|
+
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
641
1287
|
return TS_JS_EXTENSIONS.includes(ext) ? extractFunctionsTypeScriptAst(content, filePath) : extractFunctionsPolyglot(content, filePath);
|
|
642
1288
|
}
|
|
643
1289
|
function extractFunctionsBraceLanguages(content, filePath) {
|
|
@@ -668,55 +1314,55 @@ function isExcludedFile(file, normalized, excludePatterns) {
|
|
|
668
1314
|
);
|
|
669
1315
|
}
|
|
670
1316
|
function walkDir(dir, extensions = DEFAULT_EXTENSIONS, excludePatterns = DEFAULT_EXCLUDES) {
|
|
671
|
-
if (!
|
|
1317
|
+
if (!fs7__namespace.existsSync(dir)) return [];
|
|
672
1318
|
const results = [];
|
|
673
|
-
for (const file of
|
|
674
|
-
const fullPath =
|
|
1319
|
+
for (const file of fs7__namespace.readdirSync(dir)) {
|
|
1320
|
+
const fullPath = path4__namespace.join(dir, file);
|
|
675
1321
|
const normalized = fullPath.replace(/\\/g, "/");
|
|
676
1322
|
if (isExcludedFile(file, normalized, excludePatterns)) continue;
|
|
677
|
-
const stat =
|
|
1323
|
+
const stat = fs7__namespace.statSync(fullPath);
|
|
678
1324
|
if (stat.isDirectory()) {
|
|
679
1325
|
results.push(...walkDir(fullPath, extensions, excludePatterns));
|
|
680
|
-
} else if (extensions.includes(
|
|
1326
|
+
} else if (extensions.includes(path4__namespace.extname(file).toLowerCase())) {
|
|
681
1327
|
results.push(fullPath);
|
|
682
1328
|
}
|
|
683
1329
|
}
|
|
684
1330
|
return results;
|
|
685
1331
|
}
|
|
686
1332
|
function resolvePolicy(rootDir, options) {
|
|
687
|
-
const policyFile = options.policyPath ||
|
|
1333
|
+
const policyFile = options.policyPath || path4__namespace.join(rootDir, "quality-policy.yaml");
|
|
688
1334
|
if (options.policy) {
|
|
689
1335
|
const policy = { ...options.policy };
|
|
690
1336
|
applyOptionOverrides(policy, options.thresholds);
|
|
691
1337
|
return policy;
|
|
692
1338
|
}
|
|
693
|
-
if (
|
|
694
|
-
return parseQualityPolicy(
|
|
1339
|
+
if (fs7__namespace.existsSync(policyFile)) {
|
|
1340
|
+
return parseQualityPolicy(fs7__namespace.readFileSync(policyFile, "utf-8"), options.thresholds);
|
|
695
1341
|
}
|
|
696
1342
|
return parseQualityPolicy("", options.thresholds);
|
|
697
1343
|
}
|
|
698
1344
|
function isPathExcluded(targetPath, rootDir, excludePatterns) {
|
|
699
|
-
const rel =
|
|
700
|
-
const base =
|
|
1345
|
+
const rel = path4__namespace.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
1346
|
+
const base = path4__namespace.basename(targetPath);
|
|
701
1347
|
return excludePatterns.some(
|
|
702
1348
|
(pat) => base === pat || rel === pat || rel.startsWith(pat + "/") || rel.includes("/" + pat + "/") || rel.endsWith("/" + pat) || rel.includes(pat)
|
|
703
1349
|
);
|
|
704
1350
|
}
|
|
705
1351
|
function scanContainerSubdir(containerDir, subdirName, targetDir, rootDir, policy, files) {
|
|
706
1352
|
if (policy.exclude_patterns.includes(subdirName)) return;
|
|
707
|
-
const subTarget =
|
|
708
|
-
if (!
|
|
1353
|
+
const subTarget = path4__namespace.join(containerDir, subdirName, targetDir);
|
|
1354
|
+
if (!fs7__namespace.existsSync(subTarget) || !fs7__namespace.statSync(subTarget).isDirectory()) return;
|
|
709
1355
|
if (isPathExcluded(subTarget, rootDir, policy.exclude_patterns)) return;
|
|
710
1356
|
for (const f of walkDir(subTarget, policy.supported_extensions, policy.exclude_patterns)) {
|
|
711
1357
|
files.add(f);
|
|
712
1358
|
}
|
|
713
1359
|
}
|
|
714
1360
|
function scanMonorepoContainer(rootDir, container, targetDir, policy, files) {
|
|
715
|
-
const containerDir =
|
|
716
|
-
if (!
|
|
1361
|
+
const containerDir = path4__namespace.join(rootDir, container);
|
|
1362
|
+
if (!fs7__namespace.existsSync(containerDir) || !fs7__namespace.statSync(containerDir).isDirectory()) return;
|
|
717
1363
|
if (isPathExcluded(containerDir, rootDir, policy.exclude_patterns)) return;
|
|
718
1364
|
try {
|
|
719
|
-
for (const subdir of
|
|
1365
|
+
for (const subdir of fs7__namespace.readdirSync(containerDir, { withFileTypes: true })) {
|
|
720
1366
|
if (subdir.isDirectory()) {
|
|
721
1367
|
scanContainerSubdir(containerDir, subdir.name, targetDir, rootDir, policy, files);
|
|
722
1368
|
}
|
|
@@ -725,8 +1371,8 @@ function scanMonorepoContainer(rootDir, container, targetDir, policy, files) {
|
|
|
725
1371
|
}
|
|
726
1372
|
}
|
|
727
1373
|
function scanDirectTarget(rootDir, targetDir, policy, files) {
|
|
728
|
-
const fullDir =
|
|
729
|
-
if (
|
|
1374
|
+
const fullDir = path4__namespace.join(rootDir, targetDir);
|
|
1375
|
+
if (fs7__namespace.existsSync(fullDir) && !isPathExcluded(fullDir, rootDir, policy.exclude_patterns)) {
|
|
730
1376
|
for (const f of walkDir(fullDir, policy.supported_extensions, policy.exclude_patterns)) {
|
|
731
1377
|
files.add(f);
|
|
732
1378
|
}
|
|
@@ -763,23 +1409,86 @@ function evaluateViolations(fn, policy) {
|
|
|
763
1409
|
return violations;
|
|
764
1410
|
}
|
|
765
1411
|
function evaluateFileQuality(file, rootDir, policy) {
|
|
766
|
-
const content =
|
|
1412
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
767
1413
|
const fns = extractFunctions(content, file);
|
|
768
1414
|
return fns.map((fn) => {
|
|
769
1415
|
const violations = evaluateViolations(fn, policy);
|
|
770
1416
|
return {
|
|
771
1417
|
...fn,
|
|
772
|
-
relPath:
|
|
1418
|
+
relPath: path4__namespace.relative(rootDir, file).replace(/\\/g, "/"),
|
|
773
1419
|
status: violations.length === 0 ? "PASS" : "FAIL",
|
|
774
1420
|
violations
|
|
775
1421
|
};
|
|
776
1422
|
});
|
|
777
1423
|
}
|
|
778
|
-
|
|
1424
|
+
var UNIT_TEST_SUFFIXES = [".spec.ts", ".test.ts", ".spec.js", ".test.js", "_test.py", "_test.go"];
|
|
1425
|
+
function collectUnitTestCorpus(rootDir) {
|
|
1426
|
+
const allCodeFiles = walkDir(rootDir, DEFAULT_EXTENSIONS, DEFAULT_EXCLUDES);
|
|
1427
|
+
const chunks = [];
|
|
1428
|
+
for (const tf of allCodeFiles) {
|
|
1429
|
+
if (!UNIT_TEST_SUFFIXES.some((s) => tf.endsWith(s))) continue;
|
|
1430
|
+
try {
|
|
1431
|
+
chunks.push(fs7__namespace.readFileSync(tf, "utf-8"));
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return chunks.join("\n");
|
|
1436
|
+
}
|
|
1437
|
+
function auditUnitTestsPerFunction(results, rootDir) {
|
|
1438
|
+
const corpus = collectUnitTestCorpus(rootDir);
|
|
1439
|
+
const untested = [];
|
|
1440
|
+
for (const r of results) {
|
|
1441
|
+
const fnName = r.functionName.includes(".") ? r.functionName.split(".").pop() : r.functionName;
|
|
1442
|
+
if (!fnName || fnName === "anonymous" || fnName === "constructor") continue;
|
|
1443
|
+
if (!corpus.includes(fnName)) {
|
|
1444
|
+
untested.push({ file: r.relPath, functionName: r.functionName });
|
|
1445
|
+
r.violations.push(`Missing unit test for function '${r.functionName}'`);
|
|
1446
|
+
r.status = "FAIL";
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return untested;
|
|
1450
|
+
}
|
|
1451
|
+
function readSummaryCoverageJson(rootDir) {
|
|
1452
|
+
for (const rel of ["coverage/coverage-summary.json", "reports/coverage-summary.json"]) {
|
|
1453
|
+
const full = path4__namespace.join(rootDir, rel);
|
|
1454
|
+
if (!fs7__namespace.existsSync(full)) continue;
|
|
1455
|
+
try {
|
|
1456
|
+
const raw = JSON.parse(fs7__namespace.readFileSync(full, "utf-8"));
|
|
1457
|
+
const total = raw.total;
|
|
1458
|
+
if (!total) continue;
|
|
1459
|
+
return {
|
|
1460
|
+
linesPct: Number(total.lines?.pct ?? 0),
|
|
1461
|
+
branchesPct: Number(total.branches?.pct ?? 0),
|
|
1462
|
+
functionsPct: Number(total.functions?.pct ?? 0),
|
|
1463
|
+
source: rel
|
|
1464
|
+
};
|
|
1465
|
+
} catch {
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
function checkCoverageThreshold(label, actual, min, out) {
|
|
1471
|
+
if (actual < min) {
|
|
1472
|
+
out.push(`${label} coverage ${actual}% is below minimum threshold of ${min}%`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function evaluateCoverageThresholds(rootDir, policy, requireReport) {
|
|
1476
|
+
const cov = readSummaryCoverageJson(rootDir);
|
|
1477
|
+
if (!cov) {
|
|
1478
|
+
return requireReport ? ["Missing code coverage summary report (coverage/coverage-summary.json)"] : [];
|
|
1479
|
+
}
|
|
1480
|
+
const violations = [];
|
|
1481
|
+
checkCoverageThreshold("Line", cov.linesPct, policy.min_line_coverage, violations);
|
|
1482
|
+
checkCoverageThreshold("Branch", cov.branchesPct, policy.min_branch_coverage, violations);
|
|
1483
|
+
checkCoverageThreshold("Function", cov.functionsPct, policy.min_function_coverage, violations);
|
|
1484
|
+
return violations;
|
|
1485
|
+
}
|
|
1486
|
+
function buildQualityGateResult(results, totalFiles, policy, coverageViolations, untestedFunctions) {
|
|
779
1487
|
const failCount = results.filter((r) => r.status === "FAIL").length;
|
|
780
1488
|
const passCount = results.length - failCount;
|
|
781
|
-
const violationsCount = results.reduce((acc, r) => acc + r.violations.length, 0);
|
|
782
|
-
const
|
|
1489
|
+
const violationsCount = results.reduce((acc, r) => acc + r.violations.length, 0) + coverageViolations.length;
|
|
1490
|
+
const hasFailures = failCount > 0 || coverageViolations.length > 0;
|
|
1491
|
+
const success = policy.enforce_mode === "STRICT" ? !hasFailures : true;
|
|
783
1492
|
return {
|
|
784
1493
|
success,
|
|
785
1494
|
totalFiles,
|
|
@@ -788,7 +1497,9 @@ function buildQualityGateResult(results, totalFiles, policy) {
|
|
|
788
1497
|
failCount,
|
|
789
1498
|
violationsCount,
|
|
790
1499
|
results,
|
|
791
|
-
policy
|
|
1500
|
+
policy,
|
|
1501
|
+
coverageViolations,
|
|
1502
|
+
untestedFunctions
|
|
792
1503
|
};
|
|
793
1504
|
}
|
|
794
1505
|
function verifyQualityGate(options = {}) {
|
|
@@ -802,199 +1513,10 @@ function verifyQualityGate(options = {}) {
|
|
|
802
1513
|
} catch {
|
|
803
1514
|
}
|
|
804
1515
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
"node_modules",
|
|
809
|
-
".git",
|
|
810
|
-
"dist",
|
|
811
|
-
"bin",
|
|
812
|
-
"obj",
|
|
813
|
-
"build",
|
|
814
|
-
"out",
|
|
815
|
-
"target",
|
|
816
|
-
"coverage",
|
|
817
|
-
".changeset",
|
|
818
|
-
"scratch",
|
|
819
|
-
"test-scaffold",
|
|
820
|
-
"fixtures",
|
|
821
|
-
"templates",
|
|
822
|
-
"examples",
|
|
823
|
-
".agent",
|
|
824
|
-
".agents",
|
|
825
|
-
".claude",
|
|
826
|
-
".cursor",
|
|
827
|
-
".github",
|
|
828
|
-
".vscode",
|
|
829
|
-
".vs",
|
|
830
|
-
".idea"
|
|
831
|
-
]) {
|
|
832
|
-
try {
|
|
833
|
-
if (!fs6__namespace.existsSync(dir)) return fileList;
|
|
834
|
-
const files = fs6__namespace.readdirSync(dir);
|
|
835
|
-
for (const file of files) {
|
|
836
|
-
const fullPath = path5__namespace.join(dir, file);
|
|
837
|
-
try {
|
|
838
|
-
if (fs6__namespace.statSync(fullPath).isDirectory()) {
|
|
839
|
-
if (!excludeDirs.includes(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
840
|
-
walkMdFiles(fullPath, fileList, excludeDirs);
|
|
841
|
-
}
|
|
842
|
-
} else if (file.endsWith(".md")) {
|
|
843
|
-
fileList.push(fullPath);
|
|
844
|
-
}
|
|
845
|
-
} catch {
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
} catch {
|
|
849
|
-
}
|
|
850
|
-
return fileList;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// src/verifiers/gherkin.ts
|
|
854
|
-
function parseMarkdownFrontmatter(content) {
|
|
855
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
856
|
-
if (!match) return { frontmatter: {}, body: content };
|
|
857
|
-
const yamlLines = match[1].split("\n");
|
|
858
|
-
const frontmatter = {};
|
|
859
|
-
for (const line of yamlLines) {
|
|
860
|
-
const parts = line.split(":");
|
|
861
|
-
if (parts.length >= 2) {
|
|
862
|
-
const key = parts[0].trim();
|
|
863
|
-
let val = parts.slice(1).join(":").trim();
|
|
864
|
-
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
865
|
-
val = val.substring(1, val.length - 1);
|
|
866
|
-
}
|
|
867
|
-
frontmatter[key] = val;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
const body = content.substring(match[0].length);
|
|
871
|
-
return { frontmatter, body };
|
|
872
|
-
}
|
|
873
|
-
function extractGherkinBlock(body) {
|
|
874
|
-
const gherkinRegex = /```gherkin\r?\n([\s\S]*?)\r?\n```/g;
|
|
875
|
-
const blocks = [];
|
|
876
|
-
let match;
|
|
877
|
-
while ((match = gherkinRegex.exec(body)) !== null) {
|
|
878
|
-
blocks.push(match[1].trim());
|
|
879
|
-
}
|
|
880
|
-
return blocks;
|
|
881
|
-
}
|
|
882
|
-
function resolveTargetFiles(rootDir, targetPath) {
|
|
883
|
-
if (targetPath) {
|
|
884
|
-
const fullTarget = path5__namespace.isAbsolute(targetPath) ? targetPath : path5__namespace.join(rootDir, targetPath);
|
|
885
|
-
if (!fs6__namespace.existsSync(fullTarget)) return [];
|
|
886
|
-
return fs6__namespace.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
887
|
-
}
|
|
888
|
-
const searchDirs = [
|
|
889
|
-
path5__namespace.join(rootDir, "product"),
|
|
890
|
-
path5__namespace.join(rootDir, "security"),
|
|
891
|
-
path5__namespace.join(rootDir, "specs"),
|
|
892
|
-
path5__namespace.join(rootDir, "examples", "product"),
|
|
893
|
-
path5__namespace.join(rootDir, "examples", "security"),
|
|
894
|
-
path5__namespace.join(rootDir, "examples", "specs")
|
|
895
|
-
];
|
|
896
|
-
return searchDirs.filter((d) => fs6__namespace.existsSync(d)).flatMap((d) => walkMdFiles(d));
|
|
897
|
-
}
|
|
898
|
-
function resolveFeatureFilePath(rootDir, filePath, id, customTarget) {
|
|
899
|
-
if (customTarget) {
|
|
900
|
-
return path5__namespace.isAbsolute(customTarget) ? customTarget : path5__namespace.join(rootDir, customTarget);
|
|
901
|
-
}
|
|
902
|
-
const baseDir = path5__namespace.join(rootDir, "tests", "features");
|
|
903
|
-
const candidateSecurity = path5__namespace.join(baseDir, "security", `${id.toLowerCase()}.feature`);
|
|
904
|
-
const candidateRoot = path5__namespace.join(baseDir, `${id.toLowerCase()}.feature`);
|
|
905
|
-
if (fs6__namespace.existsSync(candidateSecurity)) return candidateSecurity;
|
|
906
|
-
if (fs6__namespace.existsSync(candidateRoot)) return candidateRoot;
|
|
907
|
-
const subDir = filePath.includes("security") ? "security" : "";
|
|
908
|
-
return path5__namespace.join(baseDir, subDir, `${id.toLowerCase()}.feature`);
|
|
909
|
-
}
|
|
910
|
-
function buildFeatureFileContent(rootDir, filePath, id, version, gherkinBlocks) {
|
|
911
|
-
const banner = [
|
|
912
|
-
`# ==============================================================================`,
|
|
913
|
-
`# AUTO-GENERADO POR AI-SDLC (Cucumber Integration)`,
|
|
914
|
-
`# Origen: ${path5__namespace.relative(rootDir, filePath).replace(/\\/g, "/")}`,
|
|
915
|
-
`# ID Requerimiento: ${id}`,
|
|
916
|
-
`# Versi\xF3n: ${version || "1.0.0"}`,
|
|
917
|
-
`# NO EDITAR MANUALMENTE: Cualquier cambio debe realizarse en el Markdown origen.`,
|
|
918
|
-
`# ==============================================================================`,
|
|
919
|
-
"",
|
|
920
|
-
""
|
|
921
|
-
].join("\n");
|
|
922
|
-
return banner + gherkinBlocks.join("\n\n") + "\n";
|
|
923
|
-
}
|
|
924
|
-
function countScenarios(gherkinBlocks) {
|
|
925
|
-
let count = 0;
|
|
926
|
-
for (const block of gherkinBlocks) {
|
|
927
|
-
const matches = block.match(/\b(Scenario|Escenario|Scenario Outline|Esquema del escenario):/g);
|
|
928
|
-
if (matches) count += matches.length;
|
|
929
|
-
}
|
|
930
|
-
return count;
|
|
931
|
-
}
|
|
932
|
-
function extractGherkinFeatures(options = {}) {
|
|
933
|
-
const rootDir = options.rootDir || process.cwd();
|
|
934
|
-
const features = [];
|
|
935
|
-
let totalScenarios = 0;
|
|
936
|
-
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
937
|
-
for (const filePath of targetFiles) {
|
|
938
|
-
try {
|
|
939
|
-
const content = fs6__namespace.readFileSync(filePath, "utf-8");
|
|
940
|
-
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
941
|
-
const gherkinBlocks = extractGherkinBlock(body);
|
|
942
|
-
if (gherkinBlocks.length === 0) continue;
|
|
943
|
-
const id = frontmatter.id || path5__namespace.basename(filePath, path5__namespace.extname(filePath));
|
|
944
|
-
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
945
|
-
fs6__namespace.mkdirSync(path5__namespace.dirname(targetPath), { recursive: true });
|
|
946
|
-
const fullContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
947
|
-
fs6__namespace.writeFileSync(targetPath, fullContent, "utf-8");
|
|
948
|
-
const scenarioCount = countScenarios(gherkinBlocks);
|
|
949
|
-
totalScenarios += scenarioCount;
|
|
950
|
-
features.push({
|
|
951
|
-
sourceFile: path5__namespace.relative(rootDir, filePath).replace(/\\/g, "/"),
|
|
952
|
-
outputFile: path5__namespace.relative(rootDir, targetPath).replace(/\\/g, "/"),
|
|
953
|
-
featureName: id,
|
|
954
|
-
scenarioCount
|
|
955
|
-
});
|
|
956
|
-
} catch {
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
return { success: true, features, totalScenarios };
|
|
960
|
-
}
|
|
961
|
-
function checkGherkinInSync(options = {}) {
|
|
962
|
-
const rootDir = options.rootDir || process.cwd();
|
|
963
|
-
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
964
|
-
const outOfSyncFiles = [];
|
|
965
|
-
const missingFiles = [];
|
|
966
|
-
let totalFeatures = 0;
|
|
967
|
-
for (const filePath of targetFiles) {
|
|
968
|
-
try {
|
|
969
|
-
const content = fs6__namespace.readFileSync(filePath, "utf-8");
|
|
970
|
-
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
971
|
-
const gherkinBlocks = extractGherkinBlock(body);
|
|
972
|
-
if (gherkinBlocks.length === 0) continue;
|
|
973
|
-
totalFeatures++;
|
|
974
|
-
const id = frontmatter.id || path5__namespace.basename(filePath, path5__namespace.extname(filePath));
|
|
975
|
-
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
976
|
-
const relTarget = path5__namespace.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
977
|
-
if (!fs6__namespace.existsSync(targetPath)) {
|
|
978
|
-
missingFiles.push(relTarget);
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
const expectedContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
982
|
-
const actualContent = fs6__namespace.readFileSync(targetPath, "utf-8");
|
|
983
|
-
if (actualContent.replace(/\r\n/g, "\n") !== expectedContent.replace(/\r\n/g, "\n")) {
|
|
984
|
-
outOfSyncFiles.push(relTarget);
|
|
985
|
-
}
|
|
986
|
-
} catch {
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
return {
|
|
990
|
-
inSync: missingFiles.length === 0 && outOfSyncFiles.length === 0,
|
|
991
|
-
outOfSyncFiles,
|
|
992
|
-
missingFiles,
|
|
993
|
-
totalFeatures
|
|
994
|
-
};
|
|
1516
|
+
const untestedFunctions = options.requireFunctionUnitTests ? auditUnitTestsPerFunction(results, rootDir) : [];
|
|
1517
|
+
const coverageViolations = evaluateCoverageThresholds(rootDir, policy, options.requireCoverageReport);
|
|
1518
|
+
return buildQualityGateResult(results, allFiles.length, policy, coverageViolations, untestedFunctions);
|
|
995
1519
|
}
|
|
996
|
-
|
|
997
|
-
// src/adapters/sdd/openspec.ts
|
|
998
1520
|
function normalizeHandoffPayload(handoff, changeId) {
|
|
999
1521
|
return {
|
|
1000
1522
|
...handoff,
|
|
@@ -1004,12 +1526,12 @@ function normalizeHandoffPayload(handoff, changeId) {
|
|
|
1004
1526
|
};
|
|
1005
1527
|
}
|
|
1006
1528
|
function parseChangeProfile(specFile) {
|
|
1007
|
-
if (!
|
|
1529
|
+
if (!fs7__namespace.existsSync(specFile)) return "standard";
|
|
1008
1530
|
try {
|
|
1009
|
-
const rawSpec =
|
|
1531
|
+
const rawSpec = fs7__namespace.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1010
1532
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1011
1533
|
if (match) {
|
|
1012
|
-
const parsedFm =
|
|
1534
|
+
const parsedFm = yaml3__default.default.load(match[1]);
|
|
1013
1535
|
if (parsedFm?.profile && ["patch", "standard", "critical"].includes(String(parsedFm.profile))) {
|
|
1014
1536
|
return parsedFm.profile;
|
|
1015
1537
|
}
|
|
@@ -1028,15 +1550,15 @@ function checkSpecGherkinFormat(parsedFm, rawSpec, changeWorkspaceDir, errors) {
|
|
|
1028
1550
|
}
|
|
1029
1551
|
}
|
|
1030
1552
|
function validateSpecFileContent(specFile, changeWorkspaceDir, errors) {
|
|
1031
|
-
if (!
|
|
1553
|
+
if (!fs7__namespace.existsSync(specFile)) {
|
|
1032
1554
|
errors.push(`Falta el archivo obligatorio 'spec.md' en ${changeWorkspaceDir}.`);
|
|
1033
1555
|
return;
|
|
1034
1556
|
}
|
|
1035
1557
|
try {
|
|
1036
|
-
const rawSpec =
|
|
1558
|
+
const rawSpec = fs7__namespace.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1037
1559
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1038
1560
|
if (match) {
|
|
1039
|
-
const parsedFm =
|
|
1561
|
+
const parsedFm = yaml3__default.default.load(match[1]);
|
|
1040
1562
|
if (parsedFm) checkSpecGherkinFormat(parsedFm, rawSpec, changeWorkspaceDir, errors);
|
|
1041
1563
|
}
|
|
1042
1564
|
} catch {
|
|
@@ -1052,19 +1574,19 @@ function validateHandoffSidecar(handoff, changeWorkspaceDir, errors) {
|
|
|
1052
1574
|
if (!handoff.subgraph || !Array.isArray(handoff.subgraph.requirements)) {
|
|
1053
1575
|
errors.push(`El sidecar '${handoff.id}' no define un subgrafo de requerimientos v\xE1lido.`);
|
|
1054
1576
|
}
|
|
1055
|
-
return
|
|
1577
|
+
return path4__namespace.join(changeWorkspaceDir, "handoff.yaml");
|
|
1056
1578
|
}
|
|
1057
1579
|
function validateOpenSpecStructure(profile, changeWorkspaceDir, adapter, errors, warnings) {
|
|
1058
|
-
const proposalFile =
|
|
1059
|
-
const tasksFile =
|
|
1580
|
+
const proposalFile = path4__namespace.join(changeWorkspaceDir, "proposal.md");
|
|
1581
|
+
const tasksFile = path4__namespace.join(changeWorkspaceDir, "tasks.md");
|
|
1060
1582
|
if (profile === "patch") {
|
|
1061
1583
|
const handoff2 = adapter.loadSidecar(changeWorkspaceDir);
|
|
1062
|
-
return { handoff: handoff2, sidecarFile: handoff2 ?
|
|
1584
|
+
return { handoff: handoff2, sidecarFile: handoff2 ? path4__namespace.join(changeWorkspaceDir, "handoff.yaml") : void 0 };
|
|
1063
1585
|
}
|
|
1064
|
-
if (!
|
|
1586
|
+
if (!fs7__namespace.existsSync(proposalFile)) {
|
|
1065
1587
|
warnings.push(`Falta 'proposal.md' en el espacio de trabajo OpenSpec.`);
|
|
1066
1588
|
}
|
|
1067
|
-
if (!
|
|
1589
|
+
if (!fs7__namespace.existsSync(tasksFile)) {
|
|
1068
1590
|
errors.push(`Falta el archivo obligatorio 'tasks.md' en ${changeWorkspaceDir}.`);
|
|
1069
1591
|
}
|
|
1070
1592
|
const handoff = adapter.loadSidecar(changeWorkspaceDir);
|
|
@@ -1072,20 +1594,20 @@ function validateOpenSpecStructure(profile, changeWorkspaceDir, adapter, errors,
|
|
|
1072
1594
|
return { handoff, sidecarFile };
|
|
1073
1595
|
}
|
|
1074
1596
|
function buildOpenSpecWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecarFile) {
|
|
1075
|
-
const changeId =
|
|
1076
|
-
const proposalFile =
|
|
1077
|
-
const specFile =
|
|
1078
|
-
const designFile =
|
|
1079
|
-
const tasksFile =
|
|
1597
|
+
const changeId = path4__namespace.basename(changeWorkspaceDir);
|
|
1598
|
+
const proposalFile = path4__namespace.join(changeWorkspaceDir, "proposal.md");
|
|
1599
|
+
const specFile = path4__namespace.join(changeWorkspaceDir, "spec.md");
|
|
1600
|
+
const designFile = path4__namespace.join(changeWorkspaceDir, "design.md");
|
|
1601
|
+
const tasksFile = path4__namespace.join(changeWorkspaceDir, "tasks.md");
|
|
1080
1602
|
return {
|
|
1081
1603
|
framework: "openspec",
|
|
1082
1604
|
changeId,
|
|
1083
1605
|
changeDir: changeWorkspaceDir,
|
|
1084
1606
|
profile,
|
|
1085
|
-
proposalFile:
|
|
1086
|
-
specFile:
|
|
1087
|
-
designFile:
|
|
1088
|
-
tasksFile:
|
|
1607
|
+
proposalFile: fs7__namespace.existsSync(proposalFile) ? proposalFile : void 0,
|
|
1608
|
+
specFile: fs7__namespace.existsSync(specFile) ? specFile : void 0,
|
|
1609
|
+
designFile: fs7__namespace.existsSync(designFile) ? designFile : void 0,
|
|
1610
|
+
tasksFile: fs7__namespace.existsSync(tasksFile) ? tasksFile : void 0,
|
|
1089
1611
|
sidecarFile,
|
|
1090
1612
|
handoff: handoff || void 0
|
|
1091
1613
|
};
|
|
@@ -1093,28 +1615,28 @@ function buildOpenSpecWorkspaceInfo(changeWorkspaceDir, profile, handoff, sideca
|
|
|
1093
1615
|
var OpenSpecAdapter = class {
|
|
1094
1616
|
framework = "openspec";
|
|
1095
1617
|
getChangeWorkspaceDir(rootDir, changeId) {
|
|
1096
|
-
const openspecDir =
|
|
1097
|
-
if (
|
|
1098
|
-
const activeChangeDir =
|
|
1099
|
-
if (
|
|
1100
|
-
const completedChangeDir =
|
|
1101
|
-
if (
|
|
1618
|
+
const openspecDir = path4__namespace.join(rootDir, "openspec", "changes", changeId);
|
|
1619
|
+
if (fs7__namespace.existsSync(openspecDir)) return openspecDir;
|
|
1620
|
+
const activeChangeDir = path4__namespace.join(rootDir, "specs", "changes", "active", changeId);
|
|
1621
|
+
if (fs7__namespace.existsSync(activeChangeDir)) return activeChangeDir;
|
|
1622
|
+
const completedChangeDir = path4__namespace.join(rootDir, "specs", "changes", "completed", changeId);
|
|
1623
|
+
if (fs7__namespace.existsSync(completedChangeDir)) return completedChangeDir;
|
|
1102
1624
|
return activeChangeDir;
|
|
1103
1625
|
}
|
|
1104
1626
|
depositSidecar(options) {
|
|
1105
1627
|
const changeDir = this.getChangeWorkspaceDir(options.rootDir, options.changeId);
|
|
1106
|
-
if (!
|
|
1107
|
-
|
|
1628
|
+
if (!fs7__namespace.existsSync(changeDir)) {
|
|
1629
|
+
fs7__namespace.mkdirSync(changeDir, { recursive: true });
|
|
1108
1630
|
}
|
|
1109
1631
|
const handoff = normalizeHandoffPayload(options.handoff, options.changeId);
|
|
1110
1632
|
const filename = options.format === "json" ? "handoff.json" : "handoff.yaml";
|
|
1111
|
-
const filePath =
|
|
1112
|
-
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) :
|
|
1113
|
-
|
|
1633
|
+
const filePath = path4__namespace.join(changeDir, filename);
|
|
1634
|
+
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) : yaml3__default.default.dump(handoff, { indent: 2, lineWidth: -1 });
|
|
1635
|
+
fs7__namespace.writeFileSync(filePath, content, "utf-8");
|
|
1114
1636
|
return filePath;
|
|
1115
1637
|
}
|
|
1116
1638
|
loadSidecar(changeWorkspaceDir) {
|
|
1117
|
-
if (!
|
|
1639
|
+
if (!fs7__namespace.existsSync(changeWorkspaceDir)) return null;
|
|
1118
1640
|
const candidateFiles = [
|
|
1119
1641
|
"handoff.yaml",
|
|
1120
1642
|
"handoff.yml",
|
|
@@ -1125,11 +1647,11 @@ var OpenSpecAdapter = class {
|
|
|
1125
1647
|
"product-handoff.json"
|
|
1126
1648
|
];
|
|
1127
1649
|
for (const file of candidateFiles) {
|
|
1128
|
-
const fullPath =
|
|
1129
|
-
if (
|
|
1650
|
+
const fullPath = path4__namespace.join(changeWorkspaceDir, file);
|
|
1651
|
+
if (fs7__namespace.existsSync(fullPath)) {
|
|
1130
1652
|
try {
|
|
1131
|
-
const raw =
|
|
1132
|
-
const parsed = file.endsWith(".json") ? JSON.parse(raw) :
|
|
1653
|
+
const raw = fs7__namespace.readFileSync(fullPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1654
|
+
const parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml3__default.default.load(raw);
|
|
1133
1655
|
if (parsed && typeof parsed === "object" && parsed.id && String(parsed.id).startsWith("HOF-")) {
|
|
1134
1656
|
return parsed;
|
|
1135
1657
|
}
|
|
@@ -1140,7 +1662,7 @@ var OpenSpecAdapter = class {
|
|
|
1140
1662
|
return null;
|
|
1141
1663
|
}
|
|
1142
1664
|
validateWorkspace(changeWorkspaceDir) {
|
|
1143
|
-
if (!
|
|
1665
|
+
if (!fs7__namespace.existsSync(changeWorkspaceDir)) {
|
|
1144
1666
|
return {
|
|
1145
1667
|
valid: false,
|
|
1146
1668
|
errors: [`El directorio del cambio OpenSpec no existe: ${changeWorkspaceDir}`],
|
|
@@ -1149,7 +1671,7 @@ var OpenSpecAdapter = class {
|
|
|
1149
1671
|
}
|
|
1150
1672
|
const errors = [];
|
|
1151
1673
|
const warnings = [];
|
|
1152
|
-
const specFile =
|
|
1674
|
+
const specFile = path4__namespace.join(changeWorkspaceDir, "spec.md");
|
|
1153
1675
|
const profile = parseChangeProfile(specFile);
|
|
1154
1676
|
validateSpecFileContent(specFile, changeWorkspaceDir, errors);
|
|
1155
1677
|
const { handoff, sidecarFile } = validateOpenSpecStructure(profile, changeWorkspaceDir, this, errors, warnings);
|
|
@@ -1163,12 +1685,12 @@ var OpenSpecAdapter = class {
|
|
|
1163
1685
|
}
|
|
1164
1686
|
};
|
|
1165
1687
|
function parseChangeProfile2(specFile) {
|
|
1166
|
-
if (!
|
|
1688
|
+
if (!fs7__namespace.existsSync(specFile)) return "standard";
|
|
1167
1689
|
try {
|
|
1168
|
-
const rawSpec =
|
|
1690
|
+
const rawSpec = fs7__namespace.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1169
1691
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1170
1692
|
if (match) {
|
|
1171
|
-
const parsedFm =
|
|
1693
|
+
const parsedFm = yaml3__default.default.load(match[1]);
|
|
1172
1694
|
if (parsedFm?.profile && ["patch", "standard", "critical"].includes(String(parsedFm.profile))) {
|
|
1173
1695
|
return parsedFm.profile;
|
|
1174
1696
|
}
|
|
@@ -1187,15 +1709,15 @@ function checkSpecGherkinFormat2(parsedFm, rawSpec, changeWorkspaceDir, errors)
|
|
|
1187
1709
|
}
|
|
1188
1710
|
}
|
|
1189
1711
|
function validateSpecFileContent2(specFile, changeWorkspaceDir, errors) {
|
|
1190
|
-
if (!
|
|
1712
|
+
if (!fs7__namespace.existsSync(specFile)) {
|
|
1191
1713
|
errors.push(`Falta el archivo de especificaci\xF3n obligatorio 'spec.md' en ${changeWorkspaceDir}.`);
|
|
1192
1714
|
return;
|
|
1193
1715
|
}
|
|
1194
1716
|
try {
|
|
1195
|
-
const rawSpec =
|
|
1717
|
+
const rawSpec = fs7__namespace.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1196
1718
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1197
1719
|
if (match) {
|
|
1198
|
-
const parsedFm =
|
|
1720
|
+
const parsedFm = yaml3__default.default.load(match[1]);
|
|
1199
1721
|
if (parsedFm) checkSpecGherkinFormat2(parsedFm, rawSpec, changeWorkspaceDir, errors);
|
|
1200
1722
|
}
|
|
1201
1723
|
} catch {
|
|
@@ -1211,19 +1733,19 @@ function validateHandoffSidecar2(handoff, changeWorkspaceDir, errors) {
|
|
|
1211
1733
|
if (!handoff.subgraph || !Array.isArray(handoff.subgraph.requirements)) {
|
|
1212
1734
|
errors.push(`El sidecar '${handoff.id}' no define un subgrafo de requerimientos v\xE1lido.`);
|
|
1213
1735
|
}
|
|
1214
|
-
return
|
|
1736
|
+
return path4__namespace.join(changeWorkspaceDir, "handoff.yaml");
|
|
1215
1737
|
}
|
|
1216
1738
|
function validateSpecKitStructure(profile, changeWorkspaceDir, adapter, errors, warnings) {
|
|
1217
|
-
const planFile =
|
|
1218
|
-
const tasksFile =
|
|
1739
|
+
const planFile = path4__namespace.join(changeWorkspaceDir, "plan.md");
|
|
1740
|
+
const tasksFile = path4__namespace.join(changeWorkspaceDir, "tasks.md");
|
|
1219
1741
|
if (profile === "patch") {
|
|
1220
1742
|
const handoff2 = adapter.loadSidecar(changeWorkspaceDir);
|
|
1221
|
-
return { handoff: handoff2, sidecarFile: handoff2 ?
|
|
1743
|
+
return { handoff: handoff2, sidecarFile: handoff2 ? path4__namespace.join(changeWorkspaceDir, "handoff.yaml") : void 0 };
|
|
1222
1744
|
}
|
|
1223
|
-
if (!
|
|
1745
|
+
if (!fs7__namespace.existsSync(tasksFile)) {
|
|
1224
1746
|
errors.push(`Falta el archivo de tareas obligatorio 'tasks.md' en ${changeWorkspaceDir}.`);
|
|
1225
1747
|
}
|
|
1226
|
-
if (!
|
|
1748
|
+
if (!fs7__namespace.existsSync(planFile)) {
|
|
1227
1749
|
warnings.push(`Se recomienda incluir 'plan.md' en el espacio de trabajo Spec Kit.`);
|
|
1228
1750
|
}
|
|
1229
1751
|
const handoff = adapter.loadSidecar(changeWorkspaceDir);
|
|
@@ -1231,18 +1753,18 @@ function validateSpecKitStructure(profile, changeWorkspaceDir, adapter, errors,
|
|
|
1231
1753
|
return { handoff, sidecarFile };
|
|
1232
1754
|
}
|
|
1233
1755
|
function buildSpecKitWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecarFile) {
|
|
1234
|
-
const changeId =
|
|
1235
|
-
const specFile =
|
|
1236
|
-
const planFile =
|
|
1237
|
-
const tasksFile =
|
|
1756
|
+
const changeId = path4__namespace.basename(changeWorkspaceDir);
|
|
1757
|
+
const specFile = path4__namespace.join(changeWorkspaceDir, "spec.md");
|
|
1758
|
+
const planFile = path4__namespace.join(changeWorkspaceDir, "plan.md");
|
|
1759
|
+
const tasksFile = path4__namespace.join(changeWorkspaceDir, "tasks.md");
|
|
1238
1760
|
return {
|
|
1239
1761
|
framework: "speckit",
|
|
1240
1762
|
changeId,
|
|
1241
1763
|
changeDir: changeWorkspaceDir,
|
|
1242
1764
|
profile,
|
|
1243
|
-
specFile:
|
|
1244
|
-
designFile:
|
|
1245
|
-
tasksFile:
|
|
1765
|
+
specFile: fs7__namespace.existsSync(specFile) ? specFile : void 0,
|
|
1766
|
+
designFile: fs7__namespace.existsSync(planFile) ? planFile : void 0,
|
|
1767
|
+
tasksFile: fs7__namespace.existsSync(tasksFile) ? tasksFile : void 0,
|
|
1246
1768
|
sidecarFile,
|
|
1247
1769
|
handoff: handoff || void 0
|
|
1248
1770
|
};
|
|
@@ -1250,18 +1772,18 @@ function buildSpecKitWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecar
|
|
|
1250
1772
|
var SpecKitAdapter = class {
|
|
1251
1773
|
framework = "speckit";
|
|
1252
1774
|
getChangeWorkspaceDir(rootDir, changeId) {
|
|
1253
|
-
const standardDir =
|
|
1254
|
-
if (
|
|
1255
|
-
const activeChangeDir =
|
|
1256
|
-
if (
|
|
1257
|
-
const completedChangeDir =
|
|
1258
|
-
if (
|
|
1775
|
+
const standardDir = path4__namespace.join(rootDir, "specs", changeId);
|
|
1776
|
+
if (fs7__namespace.existsSync(standardDir)) return standardDir;
|
|
1777
|
+
const activeChangeDir = path4__namespace.join(rootDir, "specs", "changes", "active", changeId);
|
|
1778
|
+
if (fs7__namespace.existsSync(activeChangeDir)) return activeChangeDir;
|
|
1779
|
+
const completedChangeDir = path4__namespace.join(rootDir, "specs", "changes", "completed", changeId);
|
|
1780
|
+
if (fs7__namespace.existsSync(completedChangeDir)) return completedChangeDir;
|
|
1259
1781
|
return standardDir;
|
|
1260
1782
|
}
|
|
1261
1783
|
depositSidecar(options) {
|
|
1262
1784
|
const changeDir = this.getChangeWorkspaceDir(options.rootDir, options.changeId);
|
|
1263
|
-
if (!
|
|
1264
|
-
|
|
1785
|
+
if (!fs7__namespace.existsSync(changeDir)) {
|
|
1786
|
+
fs7__namespace.mkdirSync(changeDir, { recursive: true });
|
|
1265
1787
|
}
|
|
1266
1788
|
const handoff = { ...options.handoff };
|
|
1267
1789
|
if (!handoff.id.startsWith("HOF-")) {
|
|
@@ -1274,20 +1796,20 @@ var SpecKitAdapter = class {
|
|
|
1274
1796
|
handoff.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1275
1797
|
}
|
|
1276
1798
|
const filename = options.format === "json" ? "handoff.json" : "handoff.yaml";
|
|
1277
|
-
const filePath =
|
|
1278
|
-
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) :
|
|
1279
|
-
|
|
1280
|
-
const specifyMemoryDir =
|
|
1281
|
-
if (
|
|
1282
|
-
if (!
|
|
1283
|
-
|
|
1799
|
+
const filePath = path4__namespace.join(changeDir, filename);
|
|
1800
|
+
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) : yaml3__default.default.dump(handoff, { indent: 2, lineWidth: -1 });
|
|
1801
|
+
fs7__namespace.writeFileSync(filePath, content, "utf-8");
|
|
1802
|
+
const specifyMemoryDir = path4__namespace.join(options.rootDir, ".specify", "memory");
|
|
1803
|
+
if (fs7__namespace.existsSync(path4__namespace.join(options.rootDir, ".specify"))) {
|
|
1804
|
+
if (!fs7__namespace.existsSync(specifyMemoryDir)) {
|
|
1805
|
+
fs7__namespace.mkdirSync(specifyMemoryDir, { recursive: true });
|
|
1284
1806
|
}
|
|
1285
|
-
|
|
1807
|
+
fs7__namespace.writeFileSync(path4__namespace.join(specifyMemoryDir, `${options.changeId}.handoff.yaml`), content, "utf-8");
|
|
1286
1808
|
}
|
|
1287
1809
|
return filePath;
|
|
1288
1810
|
}
|
|
1289
1811
|
loadSidecar(changeWorkspaceDir) {
|
|
1290
|
-
if (!
|
|
1812
|
+
if (!fs7__namespace.existsSync(changeWorkspaceDir)) return null;
|
|
1291
1813
|
const candidateFiles = [
|
|
1292
1814
|
"handoff.yaml",
|
|
1293
1815
|
"handoff.yml",
|
|
@@ -1297,11 +1819,11 @@ var SpecKitAdapter = class {
|
|
|
1297
1819
|
"handoff.json"
|
|
1298
1820
|
];
|
|
1299
1821
|
for (const file of candidateFiles) {
|
|
1300
|
-
const fullPath =
|
|
1301
|
-
if (
|
|
1822
|
+
const fullPath = path4__namespace.join(changeWorkspaceDir, file);
|
|
1823
|
+
if (fs7__namespace.existsSync(fullPath)) {
|
|
1302
1824
|
try {
|
|
1303
|
-
const raw =
|
|
1304
|
-
const parsed = file.endsWith(".json") ? JSON.parse(raw) :
|
|
1825
|
+
const raw = fs7__namespace.readFileSync(fullPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1826
|
+
const parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml3__default.default.load(raw);
|
|
1305
1827
|
if (parsed && typeof parsed === "object" && parsed.id && String(parsed.id).startsWith("HOF-")) {
|
|
1306
1828
|
return parsed;
|
|
1307
1829
|
}
|
|
@@ -1312,7 +1834,7 @@ var SpecKitAdapter = class {
|
|
|
1312
1834
|
return null;
|
|
1313
1835
|
}
|
|
1314
1836
|
validateWorkspace(changeWorkspaceDir) {
|
|
1315
|
-
if (!
|
|
1837
|
+
if (!fs7__namespace.existsSync(changeWorkspaceDir)) {
|
|
1316
1838
|
return {
|
|
1317
1839
|
valid: false,
|
|
1318
1840
|
errors: [`El directorio del cambio Spec Kit no existe: ${changeWorkspaceDir}`],
|
|
@@ -1321,7 +1843,7 @@ var SpecKitAdapter = class {
|
|
|
1321
1843
|
}
|
|
1322
1844
|
const errors = [];
|
|
1323
1845
|
const warnings = [];
|
|
1324
|
-
const specFile =
|
|
1846
|
+
const specFile = path4__namespace.join(changeWorkspaceDir, "spec.md");
|
|
1325
1847
|
const profile = parseChangeProfile2(specFile);
|
|
1326
1848
|
validateSpecFileContent2(specFile, changeWorkspaceDir, errors);
|
|
1327
1849
|
const { handoff, sidecarFile } = validateSpecKitStructure(profile, changeWorkspaceDir, this, errors, warnings);
|
|
@@ -1464,27 +1986,27 @@ ${frontmatter}---
|
|
|
1464
1986
|
${body}`;
|
|
1465
1987
|
}
|
|
1466
1988
|
function materializeCanonicalArtifact(rootDir, item, changeId, author, date) {
|
|
1467
|
-
const targetPath =
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1989
|
+
const targetPath = path4__namespace.join(rootDir, item.targetDir, `${item.id}.md`);
|
|
1990
|
+
if (fs7__namespace.existsSync(targetPath)) return targetPath;
|
|
1991
|
+
fs7__namespace.mkdirSync(path4__namespace.dirname(targetPath), { recursive: true });
|
|
1470
1992
|
const initialContent = buildCanonicalInitialContent(item, changeId, author, date);
|
|
1471
|
-
|
|
1993
|
+
fs7__namespace.writeFileSync(targetPath, initialContent, "utf-8");
|
|
1472
1994
|
return targetPath;
|
|
1473
1995
|
}
|
|
1474
1996
|
function findChangeWorkspaceDir(rootDir, changeId) {
|
|
1475
|
-
const activeDir =
|
|
1476
|
-
if (
|
|
1477
|
-
const completedDir =
|
|
1478
|
-
if (
|
|
1479
|
-
const openspecActive =
|
|
1480
|
-
if (
|
|
1997
|
+
const activeDir = path4__namespace.join(rootDir, "specs", "changes", "active", changeId);
|
|
1998
|
+
if (fs7__namespace.existsSync(activeDir)) return activeDir;
|
|
1999
|
+
const completedDir = path4__namespace.join(rootDir, "specs", "changes", "completed", changeId);
|
|
2000
|
+
if (fs7__namespace.existsSync(completedDir)) return completedDir;
|
|
2001
|
+
const openspecActive = path4__namespace.join(rootDir, "openspec", "changes", changeId);
|
|
2002
|
+
if (fs7__namespace.existsSync(openspecActive)) return openspecActive;
|
|
1481
2003
|
return null;
|
|
1482
2004
|
}
|
|
1483
2005
|
function checkTasksExecution(changeDir, changeId) {
|
|
1484
|
-
const tasksFile =
|
|
2006
|
+
const tasksFile = path4__namespace.join(changeDir, "tasks.md");
|
|
1485
2007
|
const allTasks = [];
|
|
1486
|
-
if (
|
|
1487
|
-
allTasks.push(...parseTasksFromContent(
|
|
2008
|
+
if (fs7__namespace.existsSync(tasksFile)) {
|
|
2009
|
+
allTasks.push(...parseTasksFromContent(fs7__namespace.readFileSync(tasksFile, "utf-8")));
|
|
1488
2010
|
}
|
|
1489
2011
|
const completedTasks = allTasks.filter((t) => t.status === "COMPLETED").map((t) => t.id);
|
|
1490
2012
|
const pendingTasks = allTasks.filter((t) => t.status !== "COMPLETED").map((t) => t.id);
|
|
@@ -1501,9 +2023,9 @@ function checkTasksExecution(changeDir, changeId) {
|
|
|
1501
2023
|
function resolveChangeHandoff(changeDir, changeId) {
|
|
1502
2024
|
const sidecar = loadProductHandoffSidecar(changeDir);
|
|
1503
2025
|
if (sidecar) return sidecar;
|
|
1504
|
-
const proposalFile =
|
|
1505
|
-
if (!
|
|
1506
|
-
const propContent =
|
|
2026
|
+
const proposalFile = path4__namespace.join(changeDir, "proposal.md");
|
|
2027
|
+
if (!fs7__namespace.existsSync(proposalFile)) return null;
|
|
2028
|
+
const propContent = fs7__namespace.readFileSync(proposalFile, "utf-8");
|
|
1507
2029
|
const { frontmatter } = parseFrontmatter(propContent);
|
|
1508
2030
|
const citations = Array.isArray(frontmatter.citations) ? frontmatter.citations : [];
|
|
1509
2031
|
const reqs = citations.map((c) => c.id || "").filter((id) => id.startsWith("FR-") || id.startsWith("QR-") || id.startsWith("CON-") || id.startsWith("SEC-REQ-"));
|
|
@@ -1517,9 +2039,9 @@ function resolveChangeHandoff(changeDir, changeId) {
|
|
|
1517
2039
|
}
|
|
1518
2040
|
function materializeNewCatalogArtifacts(changeDir, rootDir, changeId, author, date, errors) {
|
|
1519
2041
|
const created = [];
|
|
1520
|
-
const specFile =
|
|
1521
|
-
if (!
|
|
1522
|
-
const specContent =
|
|
2042
|
+
const specFile = path4__namespace.join(changeDir, "spec.md");
|
|
2043
|
+
if (!fs7__namespace.existsSync(specFile)) return created;
|
|
2044
|
+
const specContent = fs7__namespace.readFileSync(specFile, "utf-8");
|
|
1523
2045
|
const { newItems } = parseSpecCatalogScope(specContent);
|
|
1524
2046
|
for (const item of newItems) {
|
|
1525
2047
|
try {
|
|
@@ -1536,10 +2058,10 @@ function loadCanonicalArtifacts(rootDir) {
|
|
|
1536
2058
|
const allMdFiles = walkMdFiles(rootDir);
|
|
1537
2059
|
const canonicalArtifacts = /* @__PURE__ */ new Map();
|
|
1538
2060
|
for (const file of allMdFiles) {
|
|
1539
|
-
const relFile =
|
|
2061
|
+
const relFile = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
1540
2062
|
if (relFile.includes("specs/changes/") || relFile.includes("templates/")) continue;
|
|
1541
2063
|
try {
|
|
1542
|
-
const content =
|
|
2064
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
1543
2065
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
1544
2066
|
if (frontmatter.id) canonicalArtifacts.set(String(frontmatter.id), { file, frontmatter, body });
|
|
1545
2067
|
} catch {
|
|
@@ -1549,10 +2071,10 @@ function loadCanonicalArtifacts(rootDir) {
|
|
|
1549
2071
|
}
|
|
1550
2072
|
function writeUpdatedArtifact(file, frontmatter, body) {
|
|
1551
2073
|
const yamlHeader = `---
|
|
1552
|
-
${
|
|
2074
|
+
${yaml3__default.default.dump(frontmatter, { indent: 2, lineWidth: -1 }).trim()}
|
|
1553
2075
|
---
|
|
1554
2076
|
`;
|
|
1555
|
-
|
|
2077
|
+
fs7__namespace.writeFileSync(file, yamlHeader + body, "utf-8");
|
|
1556
2078
|
}
|
|
1557
2079
|
function updateSingleCanonicalRequirement(canonical, completedTasks, changeId, date, author) {
|
|
1558
2080
|
const fm = canonical.frontmatter;
|
|
@@ -1664,28 +2186,28 @@ function updateArchitectureComponents(citedComponents, canonicalArtifacts, targe
|
|
|
1664
2186
|
return updated;
|
|
1665
2187
|
}
|
|
1666
2188
|
function moveToCompletedFolder(changeDir, changeId, rootDir) {
|
|
1667
|
-
const completedParent =
|
|
1668
|
-
if (!
|
|
1669
|
-
const destDir =
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1672
|
-
const destProposal =
|
|
1673
|
-
if (
|
|
1674
|
-
const { frontmatter, body } = parseFrontmatter(
|
|
2189
|
+
const completedParent = path4__namespace.join(rootDir, "specs", "changes", "completed");
|
|
2190
|
+
if (!fs7__namespace.existsSync(completedParent)) fs7__namespace.mkdirSync(completedParent, { recursive: true });
|
|
2191
|
+
const destDir = path4__namespace.join(completedParent, changeId);
|
|
2192
|
+
if (fs7__namespace.existsSync(destDir)) fs7__namespace.rmSync(destDir, { recursive: true, force: true });
|
|
2193
|
+
fs7__namespace.renameSync(changeDir, destDir);
|
|
2194
|
+
const destProposal = path4__namespace.join(destDir, "proposal.md");
|
|
2195
|
+
if (fs7__namespace.existsSync(destProposal)) {
|
|
2196
|
+
const { frontmatter, body } = parseFrontmatter(fs7__namespace.readFileSync(destProposal, "utf-8"));
|
|
1675
2197
|
frontmatter.status = "applied";
|
|
1676
2198
|
writeUpdatedArtifact(destProposal, frontmatter, body);
|
|
1677
2199
|
}
|
|
1678
2200
|
return destDir;
|
|
1679
2201
|
}
|
|
1680
2202
|
function archiveChangeWorkspace(changeDir, changeId, rootDir, autoArchive, completedAll) {
|
|
1681
|
-
const isActiveFolder = changeDir.includes("specs" +
|
|
2203
|
+
const isActiveFolder = changeDir.includes("specs" + path4__namespace.sep + "changes" + path4__namespace.sep + "active");
|
|
1682
2204
|
if (completedAll && autoArchive && isActiveFolder) {
|
|
1683
2205
|
const destDir = moveToCompletedFolder(changeDir, changeId, rootDir);
|
|
1684
2206
|
return { archived: true, archivedPath: destDir };
|
|
1685
2207
|
}
|
|
1686
|
-
const proposalFile =
|
|
1687
|
-
if (
|
|
1688
|
-
const { frontmatter, body } = parseFrontmatter(
|
|
2208
|
+
const proposalFile = path4__namespace.join(changeDir, "proposal.md");
|
|
2209
|
+
if (fs7__namespace.existsSync(proposalFile)) {
|
|
2210
|
+
const { frontmatter, body } = parseFrontmatter(fs7__namespace.readFileSync(proposalFile, "utf-8"));
|
|
1689
2211
|
if (completedAll) frontmatter.status = "completed";
|
|
1690
2212
|
writeUpdatedArtifact(proposalFile, frontmatter, body);
|
|
1691
2213
|
}
|
|
@@ -1779,8 +2301,8 @@ function resolveAuditStatus(integratedCount, targetCount, gapsCount) {
|
|
|
1779
2301
|
function auditChangeEntry(changeDir, changeName, canonicalMap) {
|
|
1780
2302
|
const handoff = loadProductHandoffSidecar(changeDir);
|
|
1781
2303
|
if (!handoff) return null;
|
|
1782
|
-
const tasksFile =
|
|
1783
|
-
const tasks =
|
|
2304
|
+
const tasksFile = path4__namespace.join(changeDir, "tasks.md");
|
|
2305
|
+
const tasks = fs7__namespace.existsSync(tasksFile) ? parseTasksFromContent(fs7__namespace.readFileSync(tasksFile, "utf-8")) : [];
|
|
1784
2306
|
const completed = tasks.filter((t) => t.status === "COMPLETED");
|
|
1785
2307
|
const targetReqs = handoff.subgraph?.requirements || [];
|
|
1786
2308
|
const { integrated, missing, gaps } = auditChangeRequirements(targetReqs, canonicalMap);
|
|
@@ -1796,11 +2318,11 @@ function auditChangeEntry(changeDir, changeName, canonicalMap) {
|
|
|
1796
2318
|
};
|
|
1797
2319
|
}
|
|
1798
2320
|
function collectParentDirAudits(parent, canonicalMap, audits, errors) {
|
|
1799
|
-
if (!
|
|
1800
|
-
const entries =
|
|
2321
|
+
if (!fs7__namespace.existsSync(parent)) return;
|
|
2322
|
+
const entries = fs7__namespace.readdirSync(parent, { withFileTypes: true });
|
|
1801
2323
|
for (const e of entries) {
|
|
1802
2324
|
if (!e.isDirectory()) continue;
|
|
1803
|
-
const audit = auditChangeEntry(
|
|
2325
|
+
const audit = auditChangeEntry(path4__namespace.join(parent, e.name), e.name, canonicalMap);
|
|
1804
2326
|
if (audit) {
|
|
1805
2327
|
if (audit.gaps.length > 0) errors.push(`[${e.name}] Desalineaci\xF3n: ${audit.gaps.join("; ")}`);
|
|
1806
2328
|
audits.push(audit);
|
|
@@ -1812,8 +2334,8 @@ function verifySddIntegration(options = {}) {
|
|
|
1812
2334
|
const audits = [];
|
|
1813
2335
|
const errors = [];
|
|
1814
2336
|
const searchDirs = [
|
|
1815
|
-
|
|
1816
|
-
|
|
2337
|
+
path4__namespace.join(rootDir, "specs", "changes", "completed"),
|
|
2338
|
+
path4__namespace.join(rootDir, "specs", "changes", "active")
|
|
1817
2339
|
];
|
|
1818
2340
|
const canonicalMap = loadCanonicalArtifacts(rootDir);
|
|
1819
2341
|
for (const parent of searchDirs) {
|
|
@@ -1861,10 +2383,10 @@ function resolveMatchFromMetadata(activeFolders, options) {
|
|
|
1861
2383
|
return {};
|
|
1862
2384
|
}
|
|
1863
2385
|
function validateActiveDirectory(activeParent) {
|
|
1864
|
-
if (!
|
|
2386
|
+
if (!fs7__namespace.existsSync(activeParent)) {
|
|
1865
2387
|
return { ok: false, folders: [], error: `No existe el directorio de cambios activos en '${activeParent}'.` };
|
|
1866
2388
|
}
|
|
1867
|
-
const entries =
|
|
2389
|
+
const entries = fs7__namespace.readdirSync(activeParent, { withFileTypes: true });
|
|
1868
2390
|
const activeFolders = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1869
2391
|
if (activeFolders.length === 0) {
|
|
1870
2392
|
return { ok: false, folders: [], error: "No se encontraron carpetas de cambios activos en specs/changes/active/." };
|
|
@@ -1896,7 +2418,7 @@ function evaluateTasksProgress(changeId, tasks) {
|
|
|
1896
2418
|
}
|
|
1897
2419
|
function detectActiveChangeForIntegration(options = {}) {
|
|
1898
2420
|
const rootDir = options.rootDir || process.cwd();
|
|
1899
|
-
const activeParent =
|
|
2421
|
+
const activeParent = path4__namespace.join(rootDir, "specs", "changes", "active");
|
|
1900
2422
|
const dirCheck = validateActiveDirectory(activeParent);
|
|
1901
2423
|
if (!dirCheck.ok) {
|
|
1902
2424
|
return {
|
|
@@ -1911,9 +2433,9 @@ function detectActiveChangeForIntegration(options = {}) {
|
|
|
1911
2433
|
if (!match.changeId) {
|
|
1912
2434
|
return buildUnmatchedActiveResult(dirCheck.folders, options);
|
|
1913
2435
|
}
|
|
1914
|
-
const changeDir =
|
|
1915
|
-
const tasksFile =
|
|
1916
|
-
const tasks =
|
|
2436
|
+
const changeDir = path4__namespace.join(activeParent, match.changeId);
|
|
2437
|
+
const tasksFile = path4__namespace.join(changeDir, "tasks.md");
|
|
2438
|
+
const tasks = fs7__namespace.existsSync(tasksFile) ? parseTasksFromContent(fs7__namespace.readFileSync(tasksFile, "utf-8")) : [];
|
|
1917
2439
|
const progress = evaluateTasksProgress(match.changeId, tasks);
|
|
1918
2440
|
const reasons = match.reason ? [match.reason] : [];
|
|
1919
2441
|
if (progress.error) reasons.push(progress.error);
|
|
@@ -2044,9 +2566,9 @@ function isPdacDeliveryDoc(relFile) {
|
|
|
2044
2566
|
}
|
|
2045
2567
|
function processPdacFile(file, rootDir, baselineMap, allCitations) {
|
|
2046
2568
|
try {
|
|
2047
|
-
const content =
|
|
2569
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
2048
2570
|
const { id, type, title } = parseFrontmatterId(content);
|
|
2049
|
-
const relFile =
|
|
2571
|
+
const relFile = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
2050
2572
|
if (id) {
|
|
2051
2573
|
const citations = extractCitations(content, relFile);
|
|
2052
2574
|
baselineMap.set(id, {
|
|
@@ -2174,22 +2696,22 @@ function updateCitationsInContent(content, baselineMap, sourceFile, details) {
|
|
|
2174
2696
|
}
|
|
2175
2697
|
function collectCandidateFilesForSync(rootDir, targetPath) {
|
|
2176
2698
|
if (targetPath) {
|
|
2177
|
-
const fullTarget =
|
|
2178
|
-
if (!
|
|
2179
|
-
return
|
|
2699
|
+
const fullTarget = path4__namespace.isAbsolute(targetPath) ? targetPath : path4__namespace.join(rootDir, targetPath);
|
|
2700
|
+
if (!fs7__namespace.existsSync(fullTarget)) return [];
|
|
2701
|
+
return fs7__namespace.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
2180
2702
|
}
|
|
2181
2703
|
return walkMdFiles(rootDir).filter((f) => {
|
|
2182
|
-
const relFile =
|
|
2704
|
+
const relFile = path4__namespace.relative(rootDir, f).replace(/\\/g, "/");
|
|
2183
2705
|
return isPdacDeliveryDoc(relFile);
|
|
2184
2706
|
});
|
|
2185
2707
|
}
|
|
2186
2708
|
function syncSingleFile(file, rootDir, baselineMap, details) {
|
|
2187
2709
|
try {
|
|
2188
|
-
const content =
|
|
2189
|
-
const relFile =
|
|
2710
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
2711
|
+
const relFile = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
2190
2712
|
const { updatedContent, changed } = updateCitationsInContent(content, baselineMap, relFile, details);
|
|
2191
2713
|
if (changed) {
|
|
2192
|
-
|
|
2714
|
+
fs7__namespace.writeFileSync(file, updatedContent, "utf-8");
|
|
2193
2715
|
return relFile;
|
|
2194
2716
|
}
|
|
2195
2717
|
} catch {
|
|
@@ -2309,7 +2831,7 @@ function extractFrontmatter(content) {
|
|
|
2309
2831
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2310
2832
|
if (!match) return null;
|
|
2311
2833
|
try {
|
|
2312
|
-
const parsed =
|
|
2834
|
+
const parsed = yaml3__default.default.load(match[1]);
|
|
2313
2835
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2314
2836
|
return parsed;
|
|
2315
2837
|
}
|
|
@@ -2345,10 +2867,10 @@ var EXCLUDED_SCHEMA_DIRS = /* @__PURE__ */ new Set([
|
|
|
2345
2867
|
]);
|
|
2346
2868
|
function walkHandoffSidecars(dir, fileList = []) {
|
|
2347
2869
|
try {
|
|
2348
|
-
if (!
|
|
2349
|
-
for (const file of
|
|
2350
|
-
const fullPath =
|
|
2351
|
-
if (
|
|
2870
|
+
if (!fs7__namespace.existsSync(dir)) return fileList;
|
|
2871
|
+
for (const file of fs7__namespace.readdirSync(dir)) {
|
|
2872
|
+
const fullPath = path4__namespace.join(dir, file);
|
|
2873
|
+
if (fs7__namespace.statSync(fullPath).isDirectory()) {
|
|
2352
2874
|
if (!EXCLUDED_SCHEMA_DIRS.has(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
2353
2875
|
walkHandoffSidecars(fullPath, fileList);
|
|
2354
2876
|
}
|
|
@@ -2363,7 +2885,7 @@ function walkHandoffSidecars(dir, fileList = []) {
|
|
|
2363
2885
|
function extractArtifactData(filePath, content) {
|
|
2364
2886
|
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
|
2365
2887
|
try {
|
|
2366
|
-
const parsed =
|
|
2888
|
+
const parsed = yaml3__default.default.load(content);
|
|
2367
2889
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2368
2890
|
} catch {
|
|
2369
2891
|
return null;
|
|
@@ -2512,10 +3034,10 @@ function validateAgainstSchema(value, schema, propPath = "") {
|
|
|
2512
3034
|
return validateTypeSpecificRules(value, schema, propPath, currentProp);
|
|
2513
3035
|
}
|
|
2514
3036
|
function findSchemaFile(relSchemaPath, rootDir) {
|
|
2515
|
-
const fullPath =
|
|
2516
|
-
if (
|
|
2517
|
-
const fallbackPath =
|
|
2518
|
-
if (
|
|
3037
|
+
const fullPath = path4__namespace.join(rootDir, relSchemaPath);
|
|
3038
|
+
if (fs7__namespace.existsSync(fullPath)) return fullPath;
|
|
3039
|
+
const fallbackPath = path4__namespace.join(process.cwd(), relSchemaPath);
|
|
3040
|
+
if (fs7__namespace.existsSync(fallbackPath)) return fallbackPath;
|
|
2519
3041
|
return null;
|
|
2520
3042
|
}
|
|
2521
3043
|
function resolveSchemaForArtifact(frontmatter, rootDir) {
|
|
@@ -2525,7 +3047,7 @@ function resolveSchemaForArtifact(frontmatter, rootDir) {
|
|
|
2525
3047
|
const fullPath = findSchemaFile(relSchemaPath, rootDir);
|
|
2526
3048
|
if (!fullPath) return null;
|
|
2527
3049
|
try {
|
|
2528
|
-
const raw =
|
|
3050
|
+
const raw = fs7__namespace.readFileSync(fullPath, "utf-8");
|
|
2529
3051
|
return { schemaPath: relSchemaPath, schema: JSON.parse(raw) };
|
|
2530
3052
|
} catch {
|
|
2531
3053
|
return null;
|
|
@@ -2533,15 +3055,15 @@ function resolveSchemaForArtifact(frontmatter, rootDir) {
|
|
|
2533
3055
|
}
|
|
2534
3056
|
function loadSchemaFromPathOrType(schemaOrType, rootDir) {
|
|
2535
3057
|
if (schemaOrType.endsWith(".json")) {
|
|
2536
|
-
const fullPath =
|
|
2537
|
-
if (fullPath &&
|
|
2538
|
-
return { schema: JSON.parse(
|
|
3058
|
+
const fullPath = path4__namespace.isAbsolute(schemaOrType) ? schemaOrType : findSchemaFile(schemaOrType, rootDir);
|
|
3059
|
+
if (fullPath && fs7__namespace.existsSync(fullPath)) {
|
|
3060
|
+
return { schema: JSON.parse(fs7__namespace.readFileSync(fullPath, "utf-8")), schemaId: schemaOrType };
|
|
2539
3061
|
}
|
|
2540
3062
|
} else if (TYPE_TO_SCHEMA_MAP[schemaOrType]) {
|
|
2541
3063
|
const relPath = TYPE_TO_SCHEMA_MAP[schemaOrType];
|
|
2542
3064
|
const fullPath = findSchemaFile(relPath, rootDir);
|
|
2543
|
-
if (fullPath &&
|
|
2544
|
-
return { schema: JSON.parse(
|
|
3065
|
+
if (fullPath && fs7__namespace.existsSync(fullPath)) {
|
|
3066
|
+
return { schema: JSON.parse(fs7__namespace.readFileSync(fullPath, "utf-8")), schemaId: relPath };
|
|
2545
3067
|
}
|
|
2546
3068
|
}
|
|
2547
3069
|
return { schema: null };
|
|
@@ -2579,9 +3101,9 @@ function collectCandidateSchemaFiles(options, rootDir) {
|
|
|
2579
3101
|
if (!options.targetPath) {
|
|
2580
3102
|
return [...walkMdFiles(rootDir), ...walkHandoffSidecars(rootDir)];
|
|
2581
3103
|
}
|
|
2582
|
-
const target =
|
|
2583
|
-
if (!
|
|
2584
|
-
if (
|
|
3104
|
+
const target = path4__namespace.isAbsolute(options.targetPath) ? options.targetPath : path4__namespace.join(rootDir, options.targetPath);
|
|
3105
|
+
if (!fs7__namespace.existsSync(target)) return [];
|
|
3106
|
+
if (fs7__namespace.statSync(target).isDirectory()) {
|
|
2585
3107
|
return [...walkMdFiles(target), ...walkHandoffSidecars(target)];
|
|
2586
3108
|
}
|
|
2587
3109
|
if (target.endsWith(".md") || target.endsWith(".yaml") || target.endsWith(".yml")) {
|
|
@@ -2632,7 +3154,7 @@ function buildEvaluationFailure(relPath, err) {
|
|
|
2632
3154
|
}
|
|
2633
3155
|
function evaluateArtifactFile(file, relPath, rootDir) {
|
|
2634
3156
|
try {
|
|
2635
|
-
const content =
|
|
3157
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
2636
3158
|
const frontmatter = extractArtifactData(file, content);
|
|
2637
3159
|
if (!frontmatter || !frontmatter.id) return null;
|
|
2638
3160
|
const schemaInfo = resolveSchemaForArtifact(frontmatter, rootDir);
|
|
@@ -2667,7 +3189,7 @@ function verifyArtifactsSchemas(options = {}) {
|
|
|
2667
3189
|
let validCount = 0;
|
|
2668
3190
|
let totalEvaluated = 0;
|
|
2669
3191
|
for (const file of filesToScan) {
|
|
2670
|
-
const relPath =
|
|
3192
|
+
const relPath = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
2671
3193
|
if (isIgnoredArtifactPath(relPath, Boolean(options.targetPath))) continue;
|
|
2672
3194
|
const evaluation = evaluateArtifactFile(file, relPath, rootDir);
|
|
2673
3195
|
if (!evaluation) continue;
|
|
@@ -2686,10 +3208,10 @@ function slugify(text) {
|
|
|
2686
3208
|
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2687
3209
|
}
|
|
2688
3210
|
function extractDirMaxNum(dir) {
|
|
2689
|
-
if (!
|
|
3211
|
+
if (!fs7__namespace.existsSync(dir)) return 0;
|
|
2690
3212
|
let max = 0;
|
|
2691
3213
|
try {
|
|
2692
|
-
const entries =
|
|
3214
|
+
const entries = fs7__namespace.readdirSync(dir, { withFileTypes: true });
|
|
2693
3215
|
for (const e of entries) {
|
|
2694
3216
|
if (!e.isDirectory()) continue;
|
|
2695
3217
|
const match = e.name.match(/^(?:chg|CHG|patch|PATCH)-(\d+)/i);
|
|
@@ -2706,10 +3228,10 @@ function extractDirMaxNum(dir) {
|
|
|
2706
3228
|
}
|
|
2707
3229
|
function getNextCorrelativeNumber(rootDir) {
|
|
2708
3230
|
const candidateDirs = [
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
3231
|
+
path4__namespace.join(rootDir, "specs", "changes", "active"),
|
|
3232
|
+
path4__namespace.join(rootDir, "specs", "changes", "completed"),
|
|
3233
|
+
path4__namespace.join(rootDir, "specs"),
|
|
3234
|
+
path4__namespace.join(rootDir, "openspec", "changes")
|
|
2713
3235
|
];
|
|
2714
3236
|
let maxNum = 0;
|
|
2715
3237
|
for (const dir of candidateDirs) {
|
|
@@ -2739,13 +3261,13 @@ function resolveChangeIdentifiers(rootDir, name, profile, options) {
|
|
|
2739
3261
|
};
|
|
2740
3262
|
}
|
|
2741
3263
|
function ensureEmptyChangeDir(changeDir) {
|
|
2742
|
-
if (
|
|
2743
|
-
const existing =
|
|
3264
|
+
if (fs7__namespace.existsSync(changeDir)) {
|
|
3265
|
+
const existing = fs7__namespace.readdirSync(changeDir);
|
|
2744
3266
|
if (existing.length > 0) {
|
|
2745
3267
|
return `El directorio del cambio '${changeDir}' ya existe y no est\xE1 vac\xEDo.`;
|
|
2746
3268
|
}
|
|
2747
3269
|
} else {
|
|
2748
|
-
|
|
3270
|
+
fs7__namespace.mkdirSync(changeDir, { recursive: true });
|
|
2749
3271
|
}
|
|
2750
3272
|
return null;
|
|
2751
3273
|
}
|
|
@@ -2754,7 +3276,7 @@ function scanCanonicalArtifacts(rootDir) {
|
|
|
2754
3276
|
const artifactMap = /* @__PURE__ */ new Map();
|
|
2755
3277
|
for (const file of allMdFiles) {
|
|
2756
3278
|
try {
|
|
2757
|
-
const content =
|
|
3279
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
2758
3280
|
const { frontmatter } = parseFrontmatter(content);
|
|
2759
3281
|
if (frontmatter?.id) {
|
|
2760
3282
|
const id = String(frontmatter.id);
|
|
@@ -2782,7 +3304,7 @@ function parseInputFromIds(rawFrom) {
|
|
|
2782
3304
|
}
|
|
2783
3305
|
function renderCitationsYamlBlock(citedArtifacts) {
|
|
2784
3306
|
if (citedArtifacts.length === 0) return "";
|
|
2785
|
-
return
|
|
3307
|
+
return yaml3__default.default.dump(
|
|
2786
3308
|
citedArtifacts.map((c) => ({ id: c.id, digest: c.digest, comment: c.comment })),
|
|
2787
3309
|
{ indent: 2, lineWidth: -1 }
|
|
2788
3310
|
).trim().split("\n").map((line) => ` ${line}`).join("\n");
|
|
@@ -2817,16 +3339,39 @@ El sistema DEBE implementar y soportar la capacidad de {{NAME}}.
|
|
|
2817
3339
|
## 2. Criterios de Aceptaci\xF3n en Formato Gherkin (Cucumber)
|
|
2818
3340
|
|
|
2819
3341
|
\`\`\`gherkin
|
|
2820
|
-
@{{REQ_ID}} @automated
|
|
3342
|
+
@{{REQ_ID}} @automated @regression
|
|
2821
3343
|
Feature: {{NAME}}
|
|
2822
3344
|
As a user or client system
|
|
2823
3345
|
I want to execute the capability for {{NAME}}
|
|
2824
3346
|
So that the expected outcome and value are achieved
|
|
2825
3347
|
|
|
3348
|
+
@nominal
|
|
2826
3349
|
Scenario: Nominal successful flow
|
|
2827
3350
|
Given the system is in an operational state
|
|
2828
3351
|
When the operation for "{{NAME}}" is executed
|
|
2829
3352
|
Then the operation completes successfully
|
|
3353
|
+
|
|
3354
|
+
@boundary
|
|
3355
|
+
Scenario Outline: Boundary limits validation
|
|
3356
|
+
Given the system receives boundary input "<boundary_input>"
|
|
3357
|
+
When the operation for "{{NAME}}" is evaluated at range limits
|
|
3358
|
+
Then the system responds with expected boundary status "<expected_status>"
|
|
3359
|
+
|
|
3360
|
+
Examples:
|
|
3361
|
+
| boundary_input | expected_status |
|
|
3362
|
+
| min_bound | 200 |
|
|
3363
|
+
| max_bound | 200 |
|
|
3364
|
+
|
|
3365
|
+
@invalid
|
|
3366
|
+
Scenario Outline: Out-of-range and invalid payload rejection
|
|
3367
|
+
Given the system receives invalid or out-of-range input "<invalid_input>"
|
|
3368
|
+
When the operation for "{{NAME}}" is invoked
|
|
3369
|
+
Then the system rejects the request with error "<error_code>"
|
|
3370
|
+
|
|
3371
|
+
Examples:
|
|
3372
|
+
| invalid_input | error_code |
|
|
3373
|
+
| empty_payload | ERR_INVALID_ARGS |
|
|
3374
|
+
| overflow_val | ERR_OUT_OF_RANGE |
|
|
2830
3375
|
\`\`\`
|
|
2831
3376
|
|
|
2832
3377
|
---
|
|
@@ -2845,12 +3390,33 @@ Definici\xF3n de artefacto citado en {{CANONICAL_ID}}.
|
|
|
2845
3390
|
## 2. Criterios de Aceptaci\xF3n en Formato Gherkin (Cucumber)
|
|
2846
3391
|
|
|
2847
3392
|
\`\`\`gherkin
|
|
2848
|
-
@{{ID}} @automated
|
|
3393
|
+
@{{ID}} @automated @regression
|
|
2849
3394
|
Feature: {{NAME}}
|
|
3395
|
+
@nominal
|
|
2850
3396
|
Scenario: Expected behavior verification
|
|
2851
3397
|
Given the system is initialized for "{{NAME}}"
|
|
2852
3398
|
When the request associated with {{ID}} is processed
|
|
2853
3399
|
Then the response satisfies acceptance criteria
|
|
3400
|
+
|
|
3401
|
+
@boundary
|
|
3402
|
+
Scenario Outline: Boundary limits verification
|
|
3403
|
+
Given boundary input "<input>" for {{ID}}
|
|
3404
|
+
When the request is processed
|
|
3405
|
+
Then the response status is "<status>"
|
|
3406
|
+
|
|
3407
|
+
Examples:
|
|
3408
|
+
| input | status |
|
|
3409
|
+
| min | 200 |
|
|
3410
|
+
|
|
3411
|
+
@invalid
|
|
3412
|
+
Scenario Outline: Invalid input rejection
|
|
3413
|
+
Given invalid input "<input>" for {{ID}}
|
|
3414
|
+
When the request is processed
|
|
3415
|
+
Then the request is rejected with "<error>"
|
|
3416
|
+
|
|
3417
|
+
Examples:
|
|
3418
|
+
| input | error |
|
|
3419
|
+
| null | ERR_INVALID_ARGS |
|
|
2854
3420
|
\`\`\`
|
|
2855
3421
|
`;
|
|
2856
3422
|
var SPEC_BODY = `# Delivery Specification: {{CANONICAL_ID}}
|
|
@@ -2864,10 +3430,33 @@ Feature: {{NAME}}
|
|
|
2864
3430
|
I want to process the operation for {{NAME}}
|
|
2865
3431
|
So that delivery criteria are satisfied
|
|
2866
3432
|
|
|
3433
|
+
@nominal
|
|
2867
3434
|
Scenario: Nominal successful flow
|
|
2868
3435
|
Given the system is in a nominal operational state
|
|
2869
3436
|
When the operation for "{{NAME}}" is processed
|
|
2870
3437
|
Then the operation completes successfully satisfying acceptance criteria
|
|
3438
|
+
|
|
3439
|
+
@boundary
|
|
3440
|
+
Scenario Outline: Boundary conditions verification
|
|
3441
|
+
Given the system is configured with boundary value "<value>"
|
|
3442
|
+
When the operation for "{{NAME}}" is processed
|
|
3443
|
+
Then the boundary result matches "<expected>"
|
|
3444
|
+
|
|
3445
|
+
Examples:
|
|
3446
|
+
| value | expected |
|
|
3447
|
+
| min | accepted |
|
|
3448
|
+
| max | accepted |
|
|
3449
|
+
|
|
3450
|
+
@invalid
|
|
3451
|
+
Scenario Outline: Out-of-range and invalid input handling
|
|
3452
|
+
Given an invalid or malformed payload "<payload>"
|
|
3453
|
+
When the operation for "{{NAME}}" is attempted
|
|
3454
|
+
Then the system rejects the operation with "<error>"
|
|
3455
|
+
|
|
3456
|
+
Examples:
|
|
3457
|
+
| payload | error |
|
|
3458
|
+
| null | ERR_INVALID_DATA |
|
|
3459
|
+
| overflow | ERR_OUT_OF_RANGE |
|
|
2871
3460
|
\`\`\`
|
|
2872
3461
|
|
|
2873
3462
|
---
|
|
@@ -2972,6 +3561,7 @@ derives-from:
|
|
|
2972
3561
|
- {{UC_ID}}
|
|
2973
3562
|
verifiable-by: cucumber-bdd
|
|
2974
3563
|
acceptance-format: gherkin
|
|
3564
|
+
enforce-bdd-variants: true
|
|
2975
3565
|
cucumber-tags:
|
|
2976
3566
|
- "@{{REQ_ID}}"
|
|
2977
3567
|
- "@automated"
|
|
@@ -2989,6 +3579,7 @@ schema-version: "1.0"
|
|
|
2989
3579
|
category: functional
|
|
2990
3580
|
verifiable-by: cucumber-bdd
|
|
2991
3581
|
acceptance-format: gherkin
|
|
3582
|
+
enforce-bdd-variants: true
|
|
2992
3583
|
cucumber-tags:
|
|
2993
3584
|
- "@{{ID}}"
|
|
2994
3585
|
- "@automated"
|
|
@@ -3064,7 +3655,7 @@ function buildPatchSuccess(slug, canonicalId, specPath, citedArtifacts, errors)
|
|
|
3064
3655
|
changeId: slug,
|
|
3065
3656
|
canonicalId,
|
|
3066
3657
|
profile: "patch",
|
|
3067
|
-
changeDir:
|
|
3658
|
+
changeDir: path4__namespace.dirname(specPath),
|
|
3068
3659
|
createdFiles: [specPath],
|
|
3069
3660
|
citedArtifacts,
|
|
3070
3661
|
errors
|
|
@@ -3075,10 +3666,10 @@ function scaffoldPatchChange(rootDir, changeDir, slug, canonicalId, name, inputF
|
|
|
3075
3666
|
const citationsSection = citedArtifacts.length > 0 ? `citations:
|
|
3076
3667
|
${renderCitationsYamlBlock(citedArtifacts)}
|
|
3077
3668
|
` : "";
|
|
3078
|
-
const specPath =
|
|
3669
|
+
const specPath = path4__namespace.join(changeDir, "spec.md");
|
|
3079
3670
|
const specFmObj = buildPatchFmObj(canonicalId);
|
|
3080
3671
|
const specContent = buildPatchSpecContent(canonicalId, name, citationsSection);
|
|
3081
|
-
|
|
3672
|
+
fs7__namespace.writeFileSync(specPath, specContent, "utf-8");
|
|
3082
3673
|
const errors = validatePatchArtifacts(rootDir, changeDir, specContent, specFmObj, adapter);
|
|
3083
3674
|
return buildPatchSuccess(slug, canonicalId, specPath, citedArtifacts, errors);
|
|
3084
3675
|
}
|
|
@@ -3092,26 +3683,26 @@ function createGreenfieldRequirement(rootDir, name, padNum, canonicalId, author,
|
|
|
3092
3683
|
const nameSlugUpper = slugify(name).toUpperCase();
|
|
3093
3684
|
const reqId = `FR-${padNum}-${nameSlugUpper}-001`;
|
|
3094
3685
|
const ucId = `UC-${padNum}-${nameSlugUpper}`;
|
|
3095
|
-
const productDir =
|
|
3096
|
-
if (!
|
|
3097
|
-
const reqFilePath =
|
|
3098
|
-
if (
|
|
3686
|
+
const productDir = path4__namespace.join(rootDir, "specs", "product");
|
|
3687
|
+
if (!fs7__namespace.existsSync(productDir)) fs7__namespace.mkdirSync(productDir, { recursive: true });
|
|
3688
|
+
const reqFilePath = path4__namespace.join(productDir, `${reqId}.md`);
|
|
3689
|
+
if (fs7__namespace.existsSync(reqFilePath)) return null;
|
|
3099
3690
|
const reqContent = buildGreenfieldContent(reqId, ucId, name, canonicalId, author, dateStr);
|
|
3100
|
-
|
|
3691
|
+
fs7__namespace.writeFileSync(reqFilePath, reqContent, "utf-8");
|
|
3101
3692
|
createdFiles.push(reqFilePath);
|
|
3102
3693
|
const digest = computeCanonicalSha256(reqContent);
|
|
3103
3694
|
return { reqId, ucId, reqFilePath, digest };
|
|
3104
3695
|
}
|
|
3105
3696
|
function createMissingDraftRequirement(rootDir, name, id, canonicalId, createdFiles) {
|
|
3106
|
-
const productDir =
|
|
3107
|
-
if (!
|
|
3108
|
-
const reqFilePath =
|
|
3697
|
+
const productDir = path4__namespace.join(rootDir, "specs", "product");
|
|
3698
|
+
if (!fs7__namespace.existsSync(productDir)) fs7__namespace.mkdirSync(productDir, { recursive: true });
|
|
3699
|
+
const reqFilePath = path4__namespace.join(productDir, `${id}.md`);
|
|
3109
3700
|
const reqType = id.startsWith("UC-") ? "use-case" : id.startsWith("SEC-REQ-") ? "security-requirement" : "requirement";
|
|
3110
3701
|
const header = MISSING_DRAFT_HEADER.replaceAll("{{ID}}", id).replaceAll("{{TYPE}}", reqType).replaceAll("{{NAME}}", name);
|
|
3111
3702
|
const body = MISSING_DRAFT_BODY.replaceAll("{{ID}}", id).replaceAll("{{NAME}}", name).replaceAll("{{CANONICAL_ID}}", canonicalId);
|
|
3112
3703
|
const reqContent = `${header}
|
|
3113
3704
|
${body}`;
|
|
3114
|
-
|
|
3705
|
+
fs7__namespace.writeFileSync(reqFilePath, reqContent, "utf-8");
|
|
3115
3706
|
createdFiles.push(reqFilePath);
|
|
3116
3707
|
const digest = computeCanonicalSha256(reqContent);
|
|
3117
3708
|
return { reqFilePath, digest };
|
|
@@ -3199,7 +3790,7 @@ function collectChangeCitations(rootDir, name, padNum, canonicalId, author, date
|
|
|
3199
3790
|
return col;
|
|
3200
3791
|
}
|
|
3201
3792
|
function renderProposalFile(changeDir, canonicalId, name, author, citationsYamlBlock) {
|
|
3202
|
-
const proposalPath =
|
|
3793
|
+
const proposalPath = path4__namespace.join(changeDir, "proposal.md");
|
|
3203
3794
|
const citationsSection = citationsYamlBlock ? `citations:
|
|
3204
3795
|
${citationsYamlBlock}
|
|
3205
3796
|
` : "";
|
|
@@ -3219,11 +3810,11 @@ ${name}
|
|
|
3219
3810
|
## 2. Dependencias Externas Evaluadas
|
|
3220
3811
|
- Lista de dependencias nuevas a incorporar y verificaci\xF3n de licencia seg\xFAn \`license-policy.yaml\`.
|
|
3221
3812
|
`;
|
|
3222
|
-
|
|
3813
|
+
fs7__namespace.writeFileSync(proposalPath, proposalContent, "utf-8");
|
|
3223
3814
|
return proposalPath;
|
|
3224
3815
|
}
|
|
3225
3816
|
function renderSpecFile(changeDir, canonicalId, name, profile) {
|
|
3226
|
-
const specPath =
|
|
3817
|
+
const specPath = path4__namespace.join(changeDir, "spec.md");
|
|
3227
3818
|
const specFmObj = {
|
|
3228
3819
|
id: `SPEC-${canonicalId}`,
|
|
3229
3820
|
type: "delivery-spec",
|
|
@@ -3244,12 +3835,12 @@ cucumber-tags:
|
|
|
3244
3835
|
---
|
|
3245
3836
|
`;
|
|
3246
3837
|
const body = SPEC_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name);
|
|
3247
|
-
|
|
3838
|
+
fs7__namespace.writeFileSync(specPath, `${header}
|
|
3248
3839
|
${body}`, "utf-8");
|
|
3249
3840
|
return { specPath, specFmObj };
|
|
3250
3841
|
}
|
|
3251
3842
|
function renderDesignFile(changeDir, canonicalId, name, author, dateStr, citationsYamlBlock) {
|
|
3252
|
-
const designPath =
|
|
3843
|
+
const designPath = path4__namespace.join(changeDir, "design.md");
|
|
3253
3844
|
const citationsSection = citationsYamlBlock ? `citations:
|
|
3254
3845
|
${citationsYamlBlock}
|
|
3255
3846
|
` : "";
|
|
@@ -3264,14 +3855,20 @@ handoff: "HOF-${canonicalId}"
|
|
|
3264
3855
|
architecture-component: "CMP-CORE"
|
|
3265
3856
|
enclave: "SEC-ENC-DMZ"
|
|
3266
3857
|
status: draft
|
|
3858
|
+
contracts: []
|
|
3267
3859
|
${citationsSection}---
|
|
3268
3860
|
`;
|
|
3269
3861
|
const body = DESIGN_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name).replaceAll("{{AUTHOR}}", author).replaceAll("{{DATE}}", dateStr);
|
|
3270
|
-
|
|
3862
|
+
fs7__namespace.writeFileSync(designPath, `${header}
|
|
3271
3863
|
${body}`, "utf-8");
|
|
3272
3864
|
return designPath;
|
|
3273
3865
|
}
|
|
3274
|
-
function buildTasksFrontmatter(canonicalId, name) {
|
|
3866
|
+
function buildTasksFrontmatter(canonicalId, name, reqIds) {
|
|
3867
|
+
const tasks = DEFAULT_TASKS_ARRAY.map((t) => ({
|
|
3868
|
+
...t,
|
|
3869
|
+
"implements-components": ["CMP-CORE"],
|
|
3870
|
+
...reqIds.length > 0 ? { "satisfies-requirements": reqIds } : {}
|
|
3871
|
+
}));
|
|
3275
3872
|
return {
|
|
3276
3873
|
id: `TSK-PLAN-${canonicalId}`,
|
|
3277
3874
|
type: "task-plan",
|
|
@@ -3281,20 +3878,20 @@ function buildTasksFrontmatter(canonicalId, name) {
|
|
|
3281
3878
|
"schema-version": "1.0",
|
|
3282
3879
|
status: "draft",
|
|
3283
3880
|
"governance-summary": GOVERNANCE_SUMMARY_DEFAULT,
|
|
3284
|
-
tasks
|
|
3881
|
+
tasks,
|
|
3285
3882
|
supersedes: null,
|
|
3286
3883
|
"superseded-by": null
|
|
3287
3884
|
};
|
|
3288
3885
|
}
|
|
3289
|
-
function renderTasksFile(changeDir, canonicalId, name, author, dateStr) {
|
|
3290
|
-
const tasksPath =
|
|
3291
|
-
const tasksFm = buildTasksFrontmatter(canonicalId, name);
|
|
3886
|
+
function renderTasksFile(changeDir, canonicalId, name, author, dateStr, reqIds) {
|
|
3887
|
+
const tasksPath = path4__namespace.join(changeDir, "tasks.md");
|
|
3888
|
+
const tasksFm = buildTasksFrontmatter(canonicalId, name, reqIds);
|
|
3292
3889
|
const header = `---
|
|
3293
|
-
${
|
|
3890
|
+
${yaml3__default.default.dump(tasksFm, { indent: 2, lineWidth: -1 }).trim()}
|
|
3294
3891
|
---
|
|
3295
3892
|
`;
|
|
3296
3893
|
const body = TASKS_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name).replaceAll("{{AUTHOR}}", author).replaceAll("{{DATE}}", dateStr);
|
|
3297
|
-
|
|
3894
|
+
fs7__namespace.writeFileSync(tasksPath, `${header}
|
|
3298
3895
|
${body}`, "utf-8");
|
|
3299
3896
|
return { tasksPath, tasksFm };
|
|
3300
3897
|
}
|
|
@@ -3346,12 +3943,14 @@ function buildScaffoldFailure(profile, error, slug = "", canonicalId = "", chang
|
|
|
3346
3943
|
errors: [error]
|
|
3347
3944
|
};
|
|
3348
3945
|
}
|
|
3349
|
-
function renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile,
|
|
3946
|
+
function renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile, citations, createdFiles) {
|
|
3947
|
+
const citationsBlock = renderCitationsYamlBlock(citations.citedArtifacts);
|
|
3948
|
+
const reqIds = [...citations.subRequirements, ...citations.subSecReqs];
|
|
3350
3949
|
createdFiles.push(renderProposalFile(changeDir, canonicalId, name, author, citationsBlock));
|
|
3351
3950
|
const { specPath, specFmObj } = renderSpecFile(changeDir, canonicalId, name, profile);
|
|
3352
3951
|
createdFiles.push(specPath);
|
|
3353
3952
|
createdFiles.push(renderDesignFile(changeDir, canonicalId, name, author, dateStr, citationsBlock));
|
|
3354
|
-
const { tasksPath, tasksFm } = renderTasksFile(changeDir, canonicalId, name, author, dateStr);
|
|
3953
|
+
const { tasksPath, tasksFm } = renderTasksFile(changeDir, canonicalId, name, author, dateStr, reqIds);
|
|
3355
3954
|
createdFiles.push(tasksPath);
|
|
3356
3955
|
return { specFmObj, tasksFm };
|
|
3357
3956
|
}
|
|
@@ -3371,8 +3970,7 @@ function buildScaffoldSuccess(slug, canonicalId, profile, changeDir, createdFile
|
|
|
3371
3970
|
function executeStandardScaffolding(rootDir, changeDir, slug, canonicalId, name, padNum, author, dateStr, profile, framework, inputFromIds, artifactMap, adapter) {
|
|
3372
3971
|
const createdFiles = [];
|
|
3373
3972
|
const citations = collectChangeCitations(rootDir, name, padNum, canonicalId, author, dateStr, inputFromIds, artifactMap, createdFiles);
|
|
3374
|
-
const
|
|
3375
|
-
const { specFmObj, tasksFm } = renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile, citationsBlock, createdFiles);
|
|
3973
|
+
const { specFmObj, tasksFm } = renderAndCollectArtifactFiles(changeDir, canonicalId, name, author, dateStr, profile, citations, createdFiles);
|
|
3376
3974
|
const { depositedPath, handoff } = renderHandoffSidecar(rootDir, slug, canonicalId, name, framework, citations);
|
|
3377
3975
|
createdFiles.push(depositedPath);
|
|
3378
3976
|
const errors = validateScaffoldedArtifacts(rootDir, changeDir, adapter, specFmObj, tasksFm, handoff);
|
|
@@ -3469,7 +4067,7 @@ function resolveCiEnvironment(env = process.env) {
|
|
|
3469
4067
|
};
|
|
3470
4068
|
}
|
|
3471
4069
|
function updateExistingEnvFile(filePath, envString, uppercaseKey) {
|
|
3472
|
-
const content =
|
|
4070
|
+
const content = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
3473
4071
|
const lines = content.split("\n");
|
|
3474
4072
|
let found = false;
|
|
3475
4073
|
const newLines = lines.map((line) => {
|
|
@@ -3480,15 +4078,15 @@ function updateExistingEnvFile(filePath, envString, uppercaseKey) {
|
|
|
3480
4078
|
return line;
|
|
3481
4079
|
});
|
|
3482
4080
|
if (!found) newLines.push(envString);
|
|
3483
|
-
|
|
4081
|
+
fs7__namespace.writeFileSync(filePath, newLines.filter((l) => l.trim() !== "").join("\n") + "\n", "utf-8");
|
|
3484
4082
|
}
|
|
3485
4083
|
function writeEnvOutputFile(envOutputFile, key, value) {
|
|
3486
4084
|
const uppercaseKey = key.toUpperCase();
|
|
3487
4085
|
const envString = `${uppercaseKey}=${value}`;
|
|
3488
|
-
if (
|
|
4086
|
+
if (fs7__namespace.existsSync(envOutputFile)) {
|
|
3489
4087
|
updateExistingEnvFile(envOutputFile, envString, uppercaseKey);
|
|
3490
4088
|
} else {
|
|
3491
|
-
|
|
4089
|
+
fs7__namespace.writeFileSync(envOutputFile, envString + "\n", "utf-8");
|
|
3492
4090
|
}
|
|
3493
4091
|
}
|
|
3494
4092
|
function resolveEnvOutputFilePath(env) {
|
|
@@ -3498,8 +4096,8 @@ function resolveEnvOutputFilePath(env) {
|
|
|
3498
4096
|
return void 0;
|
|
3499
4097
|
}
|
|
3500
4098
|
function emitCiOutput(key, value, env = process.env) {
|
|
3501
|
-
if (env.GITHUB_OUTPUT &&
|
|
3502
|
-
|
|
4099
|
+
if (env.GITHUB_OUTPUT && fs7__namespace.existsSync(env.GITHUB_OUTPUT)) {
|
|
4100
|
+
fs7__namespace.appendFileSync(env.GITHUB_OUTPUT, `${key}=${value}
|
|
3503
4101
|
`, "utf-8");
|
|
3504
4102
|
}
|
|
3505
4103
|
if (env.TF_BUILD === "true") {
|
|
@@ -3541,11 +4139,11 @@ function isIgnoredEntry(name) {
|
|
|
3541
4139
|
return name === "changes" || name === "active" || name === "node_modules";
|
|
3542
4140
|
}
|
|
3543
4141
|
function scanParentDirectoryHandoffs(parentDir, handoffsMap) {
|
|
3544
|
-
if (!
|
|
3545
|
-
const entries =
|
|
4142
|
+
if (!fs7__namespace.existsSync(parentDir)) return;
|
|
4143
|
+
const entries = fs7__namespace.readdirSync(parentDir, { withFileTypes: true });
|
|
3546
4144
|
for (const entry of entries) {
|
|
3547
4145
|
if (!entry.isDirectory() || isIgnoredEntry(entry.name)) continue;
|
|
3548
|
-
const changeDir =
|
|
4146
|
+
const changeDir = path4__namespace.join(parentDir, entry.name);
|
|
3549
4147
|
if (handooffsHasDir(handoffsMap, changeDir)) continue;
|
|
3550
4148
|
const handoff = loadProductHandoffSidecar(changeDir);
|
|
3551
4149
|
if (handoff?.id) {
|
|
@@ -3556,10 +4154,10 @@ function scanParentDirectoryHandoffs(parentDir, handoffsMap) {
|
|
|
3556
4154
|
function scanAllProductHandoffs(rootDir) {
|
|
3557
4155
|
const handoffsMap = /* @__PURE__ */ new Map();
|
|
3558
4156
|
const candidateParentDirs = [
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
4157
|
+
path4__namespace.join(rootDir, "specs", "changes", "active"),
|
|
4158
|
+
path4__namespace.join(rootDir, "specs", "changes", "completed"),
|
|
4159
|
+
path4__namespace.join(rootDir, "specs"),
|
|
4160
|
+
path4__namespace.join(rootDir, "openspec", "changes")
|
|
3563
4161
|
];
|
|
3564
4162
|
for (const parentDir of candidateParentDirs) {
|
|
3565
4163
|
scanParentDirectoryHandoffs(parentDir, handoffsMap);
|
|
@@ -3567,9 +4165,9 @@ function scanAllProductHandoffs(rootDir) {
|
|
|
3567
4165
|
return handoffsMap;
|
|
3568
4166
|
}
|
|
3569
4167
|
function handooffsHasDir(map, dir) {
|
|
3570
|
-
const norm =
|
|
4168
|
+
const norm = path4__namespace.resolve(dir);
|
|
3571
4169
|
for (const key of map.keys()) {
|
|
3572
|
-
if (
|
|
4170
|
+
if (path4__namespace.resolve(key) === norm) return true;
|
|
3573
4171
|
}
|
|
3574
4172
|
return false;
|
|
3575
4173
|
}
|
|
@@ -3579,7 +4177,7 @@ function parseFrontmatter(content) {
|
|
|
3579
4177
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
3580
4178
|
if (!match) return { frontmatter: {}, body: content };
|
|
3581
4179
|
try {
|
|
3582
|
-
const parsed =
|
|
4180
|
+
const parsed = yaml3__default.default.load(match[1]);
|
|
3583
4181
|
const frontmatter = parsed && typeof parsed === "object" ? parsed : {};
|
|
3584
4182
|
return { frontmatter, body: content.substring(match[0].length) };
|
|
3585
4183
|
} catch {
|
|
@@ -3666,9 +4264,9 @@ function isCodeTestFile(name) {
|
|
|
3666
4264
|
}
|
|
3667
4265
|
function indexFeatureFile(fullPath, rootDir, map) {
|
|
3668
4266
|
try {
|
|
3669
|
-
const content =
|
|
4267
|
+
const content = fs7__namespace.readFileSync(fullPath, "utf-8");
|
|
3670
4268
|
const tags = extractFeatureTags(content);
|
|
3671
|
-
const relFeature =
|
|
4269
|
+
const relFeature = path4__namespace.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
3672
4270
|
for (const tag of tags) {
|
|
3673
4271
|
const existing = map.get(tag) || [];
|
|
3674
4272
|
if (!existing.includes(relFeature)) existing.push(relFeature);
|
|
@@ -3678,11 +4276,11 @@ function indexFeatureFile(fullPath, rootDir, map) {
|
|
|
3678
4276
|
}
|
|
3679
4277
|
}
|
|
3680
4278
|
function scanFeatureFiles(dir, rootDir, map) {
|
|
3681
|
-
if (!
|
|
4279
|
+
if (!fs7__namespace.existsSync(dir)) return;
|
|
3682
4280
|
try {
|
|
3683
|
-
const entries =
|
|
4281
|
+
const entries = fs7__namespace.readdirSync(dir, { withFileTypes: true });
|
|
3684
4282
|
for (const e of entries) {
|
|
3685
|
-
const fullPath =
|
|
4283
|
+
const fullPath = path4__namespace.join(dir, e.name);
|
|
3686
4284
|
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
3687
4285
|
scanFeatureFiles(fullPath, rootDir, map);
|
|
3688
4286
|
} else if (e.name.endsWith(".feature")) {
|
|
@@ -3694,8 +4292,8 @@ function scanFeatureFiles(dir, rootDir, map) {
|
|
|
3694
4292
|
}
|
|
3695
4293
|
function indexCodeTestFile(fullPath, rootDir, reqIds, map) {
|
|
3696
4294
|
try {
|
|
3697
|
-
const content =
|
|
3698
|
-
const relTest =
|
|
4295
|
+
const content = fs7__namespace.readFileSync(fullPath, "utf-8");
|
|
4296
|
+
const relTest = path4__namespace.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
3699
4297
|
for (const artId of reqIds) {
|
|
3700
4298
|
if (content.includes(artId)) {
|
|
3701
4299
|
const existing = map.get(artId) || [];
|
|
@@ -3707,11 +4305,11 @@ function indexCodeTestFile(fullPath, rootDir, reqIds, map) {
|
|
|
3707
4305
|
}
|
|
3708
4306
|
}
|
|
3709
4307
|
function scanCodeTestFiles(dir, rootDir, reqIds, map) {
|
|
3710
|
-
if (!
|
|
4308
|
+
if (!fs7__namespace.existsSync(dir)) return;
|
|
3711
4309
|
try {
|
|
3712
|
-
const entries =
|
|
4310
|
+
const entries = fs7__namespace.readdirSync(dir, { withFileTypes: true });
|
|
3713
4311
|
for (const e of entries) {
|
|
3714
|
-
const fullPath =
|
|
4312
|
+
const fullPath = path4__namespace.join(dir, e.name);
|
|
3715
4313
|
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
3716
4314
|
scanCodeTestFiles(fullPath, rootDir, reqIds, map);
|
|
3717
4315
|
} else if (isCodeTestFile(e.name)) {
|
|
@@ -3745,9 +4343,9 @@ function isArchitectureViewFile(relFile, frontmatter) {
|
|
|
3745
4343
|
}
|
|
3746
4344
|
function processSingleMdFile(file, rootDir, artifactMap, componentsList, architectureViews) {
|
|
3747
4345
|
try {
|
|
3748
|
-
const content =
|
|
4346
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
3749
4347
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
3750
|
-
const relFile =
|
|
4348
|
+
const relFile = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
3751
4349
|
if (frontmatter.id) {
|
|
3752
4350
|
artifactMap.set(frontmatter.id, {
|
|
3753
4351
|
file: relFile,
|
|
@@ -3908,7 +4506,7 @@ function collectArchitectureViewTraces(reqId, architectureViews) {
|
|
|
3908
4506
|
const traces = [];
|
|
3909
4507
|
for (const view of architectureViews) {
|
|
3910
4508
|
if (view.content.includes(reqId)) {
|
|
3911
|
-
const viewIdentifier = isEntityWithId(view) && view.id ? view.id :
|
|
4509
|
+
const viewIdentifier = isEntityWithId(view) && view.id ? view.id : path4__namespace.basename(view.file);
|
|
3912
4510
|
traces.push(viewIdentifier);
|
|
3913
4511
|
}
|
|
3914
4512
|
}
|
|
@@ -3990,33 +4588,115 @@ function evaluateAllRequirements(requirements, ctx) {
|
|
|
3990
4588
|
}
|
|
3991
4589
|
return { matrixRows, orphans, errorCount };
|
|
3992
4590
|
}
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
4591
|
+
var SOURCE_CODE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".java", ".cs", ".rs"];
|
|
4592
|
+
function isProductionSourceFile(name) {
|
|
4593
|
+
if (isCodeTestFile(name) || name.endsWith(".d.ts")) return false;
|
|
4594
|
+
return SOURCE_CODE_EXTS.some((ext) => name.endsWith(ext));
|
|
4595
|
+
}
|
|
4596
|
+
function collectCodeAndTestCorpora(dir, prodChunks = [], testChunks = []) {
|
|
4597
|
+
if (!fs7__namespace.existsSync(dir)) return { prodCorpus: "", testCorpus: "" };
|
|
4598
|
+
try {
|
|
4599
|
+
for (const e of fs7__namespace.readdirSync(dir, { withFileTypes: true })) {
|
|
4600
|
+
const fullPath = path4__namespace.join(dir, e.name);
|
|
4601
|
+
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
4602
|
+
collectCodeAndTestCorpora(fullPath, prodChunks, testChunks);
|
|
4603
|
+
} else if (isCodeTestFile(e.name) || e.name.endsWith(".feature")) {
|
|
4604
|
+
testChunks.push(fs7__namespace.readFileSync(fullPath, "utf-8"));
|
|
4605
|
+
} else if (isProductionSourceFile(e.name)) {
|
|
4606
|
+
prodChunks.push(fs7__namespace.readFileSync(fullPath, "utf-8"));
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
} catch {
|
|
4610
|
+
}
|
|
4611
|
+
return { prodCorpus: prodChunks.join("\n"), testCorpus: testChunks.join("\n") };
|
|
4612
|
+
}
|
|
4613
|
+
function parseStringContractEntry(designId, defaultComp, raw) {
|
|
4614
|
+
const trimmed = raw.trim();
|
|
4615
|
+
if (!trimmed) return null;
|
|
4616
|
+
const parts = trimmed.split(".");
|
|
4617
|
+
const method = parts.pop();
|
|
4618
|
+
const interfaceName = parts.length > 0 ? parts.join(".") : void 0;
|
|
4619
|
+
return { designId, component: defaultComp, interfaceName, method };
|
|
4620
|
+
}
|
|
4621
|
+
function parseObjectContractEntries(designId, defaultComp, item) {
|
|
4622
|
+
if (!Array.isArray(item.methods)) return [];
|
|
4623
|
+
const component = item.component || defaultComp;
|
|
4624
|
+
return item.methods.filter((m) => typeof m === "string" && Boolean(m.trim())).map((m) => ({ designId, component, interfaceName: item.interface, method: m.trim() }));
|
|
4625
|
+
}
|
|
4626
|
+
function expandContractDeclarations(designId, defaultComp, rawContracts) {
|
|
4627
|
+
const expanded = [];
|
|
4628
|
+
for (const item of rawContracts) {
|
|
4629
|
+
if (typeof item === "string") {
|
|
4630
|
+
const parsed = parseStringContractEntry(designId, defaultComp, item);
|
|
4631
|
+
if (parsed) expanded.push(parsed);
|
|
4632
|
+
} else if (item && typeof item === "object") {
|
|
4633
|
+
expanded.push(...parseObjectContractEntries(designId, defaultComp, item));
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
return expanded;
|
|
4637
|
+
}
|
|
4638
|
+
function evaluateSingleContractMethod(entry, prodCorpus, testCorpus) {
|
|
4639
|
+
const symbol = entry.method.includes(".") ? entry.method.split(".").pop() : entry.method;
|
|
4640
|
+
const implementedInCode = prodCorpus.includes(symbol);
|
|
4641
|
+
const testedInSuite = testCorpus.includes(symbol);
|
|
4642
|
+
const status = !implementedInCode ? "MISSING_IMPLEMENTATION" : !testedInSuite ? "MISSING_UNIT_TEST" : "COMPLIANT";
|
|
4643
|
+
return {
|
|
4644
|
+
designId: entry.designId,
|
|
4645
|
+
component: entry.component,
|
|
4646
|
+
interfaceName: entry.interfaceName,
|
|
4647
|
+
method: entry.method,
|
|
4648
|
+
implementedInCode,
|
|
4649
|
+
testedInSuite,
|
|
4650
|
+
status
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
function auditDesignContracts(rootDir, artifactMap) {
|
|
4654
|
+
const pending = [];
|
|
4655
|
+
for (const [id, entry] of artifactMap.entries()) {
|
|
4656
|
+
if (entry.file.includes("templates/") || entry.file.includes(".template.")) continue;
|
|
4657
|
+
const rawContracts = entry.frontmatter.contracts;
|
|
4658
|
+
if (!Array.isArray(rawContracts) || rawContracts.length === 0) continue;
|
|
4659
|
+
const defaultComp = typeof entry.frontmatter["architecture-component"] === "string" ? entry.frontmatter["architecture-component"] : void 0;
|
|
4660
|
+
pending.push(...expandContractDeclarations(id, defaultComp, rawContracts));
|
|
4661
|
+
}
|
|
4662
|
+
if (pending.length === 0) {
|
|
4663
|
+
return { contractAudits: [], missingContractsCount: 0 };
|
|
4664
|
+
}
|
|
4665
|
+
const { prodCorpus, testCorpus } = collectCodeAndTestCorpora(rootDir);
|
|
4666
|
+
const contractAudits = pending.map((p) => evaluateSingleContractMethod(p, prodCorpus, testCorpus));
|
|
4667
|
+
const missingContractsCount = contractAudits.filter((c) => c.status !== "COMPLIANT").length;
|
|
4668
|
+
return { contractAudits, missingContractsCount };
|
|
4669
|
+
}
|
|
4670
|
+
function buildTraceabilityContext(rootDir, allMdFiles) {
|
|
3996
4671
|
const { artifactMap, componentsList, architectureViews } = indexAllArtifacts(allMdFiles, rootDir);
|
|
3997
4672
|
const handoffList = buildHandoffList(rootDir, artifactMap);
|
|
3998
4673
|
const reqToHandoffMap = indexRequirementsToHandoff(handoffList);
|
|
3999
4674
|
const { featureTagMap, codeTestMap } = buildTestMaps(rootDir, artifactMap);
|
|
4000
4675
|
const requirements = collectRequirementsToVerify(artifactMap);
|
|
4001
|
-
|
|
4002
|
-
reqToHandoffMap,
|
|
4003
|
-
|
|
4004
|
-
componentsList,
|
|
4005
|
-
architectureViews,
|
|
4006
|
-
featureTagMap,
|
|
4007
|
-
codeTestMap
|
|
4676
|
+
return {
|
|
4677
|
+
ctx: { reqToHandoffMap, artifactMap, componentsList, architectureViews, featureTagMap, codeTestMap },
|
|
4678
|
+
requirements
|
|
4008
4679
|
};
|
|
4680
|
+
}
|
|
4681
|
+
function verifyTraceability(options = {}) {
|
|
4682
|
+
const rootDir = options.rootDir || process.cwd();
|
|
4683
|
+
const allMdFiles = walkMdFiles(rootDir);
|
|
4684
|
+
const { ctx, requirements } = buildTraceabilityContext(rootDir, allMdFiles);
|
|
4009
4685
|
const { matrixRows, orphans, errorCount } = evaluateAllRequirements(requirements, ctx);
|
|
4686
|
+
const { contractAudits, missingContractsCount } = auditDesignContracts(rootDir, ctx.artifactMap);
|
|
4687
|
+
const totalIssues = errorCount + missingContractsCount;
|
|
4010
4688
|
return {
|
|
4011
|
-
success:
|
|
4689
|
+
success: totalIssues === 0,
|
|
4012
4690
|
rows: matrixRows,
|
|
4013
4691
|
orphans,
|
|
4014
4692
|
orphanCount: orphans.length,
|
|
4015
4693
|
totalRequirements: requirements.length,
|
|
4016
|
-
|
|
4694
|
+
contractAudits,
|
|
4695
|
+
missingContractsCount,
|
|
4696
|
+
reportMarkdown: generateTraceabilityReportMarkdown(matrixRows, totalIssues)
|
|
4017
4697
|
};
|
|
4018
4698
|
}
|
|
4019
|
-
function
|
|
4699
|
+
function parseTaskVerification2(rawVerification) {
|
|
4020
4700
|
const verification = {};
|
|
4021
4701
|
if (rawVerification && typeof rawVerification === "object") {
|
|
4022
4702
|
const v = rawVerification;
|
|
@@ -4025,6 +4705,12 @@ function parseTaskVerification(rawVerification) {
|
|
|
4025
4705
|
}
|
|
4026
4706
|
return verification;
|
|
4027
4707
|
}
|
|
4708
|
+
function parseOptionalStringList(val) {
|
|
4709
|
+
if (Array.isArray(val)) {
|
|
4710
|
+
return val.map(String).filter(Boolean);
|
|
4711
|
+
}
|
|
4712
|
+
return void 0;
|
|
4713
|
+
}
|
|
4028
4714
|
function mapRawTaskItem(t) {
|
|
4029
4715
|
return {
|
|
4030
4716
|
id: typeof t["id"] === "string" ? t["id"] : String(t["id"] ?? ""),
|
|
@@ -4035,7 +4721,10 @@ function mapRawTaskItem(t) {
|
|
|
4035
4721
|
assignedTo: typeof t["assigned-to"] === "string" ? t["assigned-to"] : void 0,
|
|
4036
4722
|
status: typeof t["status"] === "string" ? t["status"] : void 0,
|
|
4037
4723
|
blockingReason: typeof t["blocking-reason"] === "string" ? t["blocking-reason"] : void 0,
|
|
4038
|
-
|
|
4724
|
+
implementsComponents: parseOptionalStringList(t["implements-components"]),
|
|
4725
|
+
satisfiesRequirements: parseOptionalStringList(t["satisfies-requirements"]),
|
|
4726
|
+
implementsContracts: parseOptionalStringList(t["implements-contracts"]),
|
|
4727
|
+
verification: parseTaskVerification2(t["verification"])
|
|
4039
4728
|
};
|
|
4040
4729
|
}
|
|
4041
4730
|
function parseTasksDoc(content) {
|
|
@@ -4043,7 +4732,7 @@ function parseTasksDoc(content) {
|
|
|
4043
4732
|
if (!match) return { frontmatter: { tasks: [] }, body: content };
|
|
4044
4733
|
let parsed;
|
|
4045
4734
|
try {
|
|
4046
|
-
parsed =
|
|
4735
|
+
parsed = yaml3__default.default.load(match[1]);
|
|
4047
4736
|
} catch {
|
|
4048
4737
|
return { frontmatter: { tasks: [] }, body: content };
|
|
4049
4738
|
}
|
|
@@ -4068,12 +4757,12 @@ function walkTaskFiles(dir, fileList = [], excludeDirs = [
|
|
|
4068
4757
|
"examples"
|
|
4069
4758
|
]) {
|
|
4070
4759
|
try {
|
|
4071
|
-
if (!
|
|
4072
|
-
const files =
|
|
4760
|
+
if (!fs7__namespace.existsSync(dir)) return fileList;
|
|
4761
|
+
const files = fs7__namespace.readdirSync(dir);
|
|
4073
4762
|
for (const file of files) {
|
|
4074
|
-
const fullPath =
|
|
4763
|
+
const fullPath = path4__namespace.join(dir, file);
|
|
4075
4764
|
try {
|
|
4076
|
-
if (
|
|
4765
|
+
if (fs7__namespace.statSync(fullPath).isDirectory()) {
|
|
4077
4766
|
if (!excludeDirs.includes(file)) {
|
|
4078
4767
|
walkTaskFiles(fullPath, fileList, excludeDirs);
|
|
4079
4768
|
}
|
|
@@ -4186,26 +4875,86 @@ function updateGovernanceCounts(t, modeCounts, riskCounts) {
|
|
|
4186
4875
|
}
|
|
4187
4876
|
}
|
|
4188
4877
|
function parseTasksFromFile(file) {
|
|
4189
|
-
if (!
|
|
4878
|
+
if (!fs7__namespace.existsSync(file)) return [];
|
|
4190
4879
|
try {
|
|
4191
|
-
const content =
|
|
4880
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
4192
4881
|
const { frontmatter } = parseTasksDoc(content);
|
|
4193
4882
|
return frontmatter.tasks || [];
|
|
4194
4883
|
} catch {
|
|
4195
4884
|
return [];
|
|
4196
4885
|
}
|
|
4197
4886
|
}
|
|
4198
|
-
function
|
|
4887
|
+
function extractDesignExpectedScope(changeDir) {
|
|
4888
|
+
const designFile = path4__namespace.join(changeDir, "design.md");
|
|
4889
|
+
if (!fs7__namespace.existsSync(designFile)) return [];
|
|
4890
|
+
const expected = [];
|
|
4891
|
+
try {
|
|
4892
|
+
const { frontmatter } = parseTasksDoc(fs7__namespace.readFileSync(designFile, "utf-8"));
|
|
4893
|
+
if (typeof frontmatter["architecture-component"] === "string") {
|
|
4894
|
+
expected.push(frontmatter["architecture-component"]);
|
|
4895
|
+
}
|
|
4896
|
+
if (Array.isArray(frontmatter["architecture-components"])) {
|
|
4897
|
+
expected.push(...frontmatter["architecture-components"].map(String));
|
|
4898
|
+
}
|
|
4899
|
+
} catch {
|
|
4900
|
+
}
|
|
4901
|
+
return expected;
|
|
4902
|
+
}
|
|
4903
|
+
function extractHandoffExpectedScope(changeDir, maxUc, violations) {
|
|
4904
|
+
const handoff = loadProductHandoffSidecar(changeDir);
|
|
4905
|
+
if (!handoff?.subgraph) return [];
|
|
4906
|
+
const ucList = handoff.subgraph.useCases || [];
|
|
4907
|
+
if (maxUc !== void 0 && ucList.length > maxUc && violations) {
|
|
4908
|
+
violations.push(
|
|
4909
|
+
`[${handoff.changeId}] Greenfield monolith detected: change bundles ${ucList.length} use cases (max ${maxUc}). Decompose into incremental SDD changes per Use Case.`
|
|
4910
|
+
);
|
|
4911
|
+
}
|
|
4912
|
+
const reqs = handoff.subgraph.requirements || [];
|
|
4913
|
+
const secReqs = handoff.subgraph.securityRequirements || [];
|
|
4914
|
+
return [...reqs, ...secReqs];
|
|
4915
|
+
}
|
|
4916
|
+
function collectTaskCoveredScope(tasks) {
|
|
4917
|
+
const covered = /* @__PURE__ */ new Set();
|
|
4918
|
+
for (const t of tasks) {
|
|
4919
|
+
for (const c of t.implementsComponents || []) covered.add(c);
|
|
4920
|
+
for (const r of t.satisfiesRequirements || []) covered.add(r);
|
|
4921
|
+
for (const m of t.implementsContracts || []) covered.add(m);
|
|
4922
|
+
}
|
|
4923
|
+
return covered;
|
|
4924
|
+
}
|
|
4925
|
+
function hasExplicitScopeBindings(tasks) {
|
|
4926
|
+
return tasks.some(
|
|
4927
|
+
(t) => t.implementsComponents && t.implementsComponents.length > 0 || t.satisfiesRequirements && t.satisfiesRequirements.length > 0 || t.implementsContracts && t.implementsContracts.length > 0
|
|
4928
|
+
);
|
|
4929
|
+
}
|
|
4930
|
+
function auditDesignScopeForFile(file, tasks, options, unassignedList, violationsList) {
|
|
4931
|
+
const changeDir = path4__namespace.dirname(file);
|
|
4932
|
+
const expectedItems = [
|
|
4933
|
+
...extractDesignExpectedScope(changeDir),
|
|
4934
|
+
...extractHandoffExpectedScope(changeDir, options.maxUseCasesPerChange, violationsList)
|
|
4935
|
+
];
|
|
4936
|
+
const shouldEnforce = Boolean(options.enforceDesignCoverage || hasExplicitScopeBindings(tasks));
|
|
4937
|
+
if (!shouldEnforce || expectedItems.length === 0) return;
|
|
4938
|
+
const covered = collectTaskCoveredScope(tasks);
|
|
4939
|
+
for (const item of new Set(expectedItems)) {
|
|
4940
|
+
if (!covered.has(item)) {
|
|
4941
|
+
unassignedList.push(item);
|
|
4942
|
+
violationsList.push(`[${path4__namespace.basename(changeDir)}] Unassigned design/handoff scope in tasks.md: missing task coverage for ${item}`);
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
function processTasksFile(file, rootDir, options, acc) {
|
|
4199
4947
|
const tasks = parseTasksFromFile(file);
|
|
4200
|
-
const relPath =
|
|
4948
|
+
const relPath = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
4201
4949
|
let unverifiedDelta = 0;
|
|
4202
4950
|
for (const t of tasks) {
|
|
4203
4951
|
const { summary, violation, unverified } = evaluateTaskItem(t, relPath);
|
|
4204
|
-
if (violation) violationsList.push(violation);
|
|
4952
|
+
if (violation) acc.violationsList.push(violation);
|
|
4205
4953
|
if (unverified) unverifiedDelta++;
|
|
4206
|
-
updateGovernanceCounts(t, modeCounts, riskCounts);
|
|
4207
|
-
taskSummaries.push(summary);
|
|
4954
|
+
updateGovernanceCounts(t, acc.modeCounts, acc.riskCounts);
|
|
4955
|
+
acc.taskSummaries.push(summary);
|
|
4208
4956
|
}
|
|
4957
|
+
auditDesignScopeForFile(file, tasks, options, acc.unassignedDesignScope, acc.violationsList);
|
|
4209
4958
|
return { tasksCount: tasks.length, unverifiedDelta };
|
|
4210
4959
|
}
|
|
4211
4960
|
function createInitialModeCounts() {
|
|
@@ -4224,30 +4973,32 @@ function createInitialRiskCounts() {
|
|
|
4224
4973
|
CRITICAL: 0
|
|
4225
4974
|
};
|
|
4226
4975
|
}
|
|
4227
|
-
function assembleGovernanceResult(
|
|
4976
|
+
function assembleGovernanceResult(acc, reportMarkdown) {
|
|
4228
4977
|
return {
|
|
4229
|
-
success: violationsList.length === 0,
|
|
4230
|
-
totalTasks,
|
|
4231
|
-
verifiedCount: totalTasks - unverifiedCount,
|
|
4232
|
-
unverifiedCount,
|
|
4233
|
-
riskCounts,
|
|
4234
|
-
modeCounts,
|
|
4235
|
-
tasks: taskSummaries,
|
|
4236
|
-
violations: violationsList,
|
|
4978
|
+
success: acc.violationsList.length === 0,
|
|
4979
|
+
totalTasks: acc.totalTasks,
|
|
4980
|
+
verifiedCount: acc.totalTasks - acc.unverifiedCount,
|
|
4981
|
+
unverifiedCount: acc.unverifiedCount,
|
|
4982
|
+
riskCounts: acc.riskCounts,
|
|
4983
|
+
modeCounts: acc.modeCounts,
|
|
4984
|
+
tasks: acc.taskSummaries,
|
|
4985
|
+
violations: acc.violationsList,
|
|
4986
|
+
unassignedDesignScope: acc.unassignedDesignScope,
|
|
4237
4987
|
reportMarkdown
|
|
4238
4988
|
};
|
|
4239
4989
|
}
|
|
4240
|
-
function collectAllTasksData(taskFiles, rootDir) {
|
|
4990
|
+
function collectAllTasksData(taskFiles, rootDir, options) {
|
|
4241
4991
|
const acc = {
|
|
4242
4992
|
totalTasks: 0,
|
|
4243
4993
|
unverifiedCount: 0,
|
|
4244
4994
|
taskSummaries: [],
|
|
4245
4995
|
violationsList: [],
|
|
4996
|
+
unassignedDesignScope: [],
|
|
4246
4997
|
modeCounts: createInitialModeCounts(),
|
|
4247
4998
|
riskCounts: createInitialRiskCounts()
|
|
4248
4999
|
};
|
|
4249
5000
|
for (const file of taskFiles) {
|
|
4250
|
-
const delta = processTasksFile(file, rootDir,
|
|
5001
|
+
const delta = processTasksFile(file, rootDir, options, acc);
|
|
4251
5002
|
acc.totalTasks += delta.tasksCount;
|
|
4252
5003
|
acc.unverifiedCount += delta.unverifiedDelta;
|
|
4253
5004
|
}
|
|
@@ -4256,308 +5007,14 @@ function collectAllTasksData(taskFiles, rootDir) {
|
|
|
4256
5007
|
function verifyTasksGovernance(options = {}) {
|
|
4257
5008
|
const rootDir = options.rootDir || process.cwd();
|
|
4258
5009
|
const searchDir = options.specsDir || rootDir;
|
|
4259
|
-
const data = collectAllTasksData(walkTaskFiles(searchDir), rootDir);
|
|
5010
|
+
const data = collectAllTasksData(walkTaskFiles(searchDir), rootDir, options);
|
|
4260
5011
|
const reportMarkdown = generateGovernanceReportMarkdown(
|
|
4261
5012
|
data.taskSummaries,
|
|
4262
5013
|
data.violationsList.length,
|
|
4263
5014
|
data.modeCounts,
|
|
4264
5015
|
data.totalTasks
|
|
4265
5016
|
);
|
|
4266
|
-
return assembleGovernanceResult(
|
|
4267
|
-
data.totalTasks,
|
|
4268
|
-
data.unverifiedCount,
|
|
4269
|
-
data.riskCounts,
|
|
4270
|
-
data.modeCounts,
|
|
4271
|
-
data.taskSummaries,
|
|
4272
|
-
data.violationsList,
|
|
4273
|
-
reportMarkdown
|
|
4274
|
-
);
|
|
4275
|
-
}
|
|
4276
|
-
function parseTaskVerification2(v) {
|
|
4277
|
-
const verification = {};
|
|
4278
|
-
if (v && typeof v === "object") {
|
|
4279
|
-
const rec = v;
|
|
4280
|
-
if (typeof rec["method"] === "string") verification.method = rec["method"];
|
|
4281
|
-
if (typeof rec["command-or-criteria"] === "string") verification.criteria = rec["command-or-criteria"];
|
|
4282
|
-
}
|
|
4283
|
-
return verification;
|
|
4284
|
-
}
|
|
4285
|
-
function parseTaskItem(t) {
|
|
4286
|
-
return {
|
|
4287
|
-
id: typeof t["id"] === "string" ? t["id"] : String(t["id"] ?? ""),
|
|
4288
|
-
title: typeof t["title"] === "string" ? t["title"] : void 0,
|
|
4289
|
-
verification: parseTaskVerification2(t["verification"])
|
|
4290
|
-
};
|
|
4291
|
-
}
|
|
4292
|
-
function parseVerifiedByTests(verifiedBy) {
|
|
4293
|
-
if (Array.isArray(verifiedBy)) {
|
|
4294
|
-
return verifiedBy.map(String);
|
|
4295
|
-
}
|
|
4296
|
-
if (typeof verifiedBy === "string") {
|
|
4297
|
-
return [verifiedBy];
|
|
4298
|
-
}
|
|
4299
|
-
return void 0;
|
|
4300
|
-
}
|
|
4301
|
-
function parseGenericDoc(content) {
|
|
4302
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4303
|
-
if (!match) return { frontmatter: {}, body: content };
|
|
4304
|
-
let parsed;
|
|
4305
|
-
try {
|
|
4306
|
-
parsed = yaml2__default.default.load(match[1]);
|
|
4307
|
-
} catch {
|
|
4308
|
-
return { frontmatter: {}, body: content };
|
|
4309
|
-
}
|
|
4310
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4311
|
-
return { frontmatter: {}, body: content };
|
|
4312
|
-
}
|
|
4313
|
-
const raw = parsed;
|
|
4314
|
-
const rawTasks = Array.isArray(raw["tasks"]) ? raw["tasks"] : [];
|
|
4315
|
-
const tasks = rawTasks.map(parseTaskItem);
|
|
4316
|
-
const frontmatter = {
|
|
4317
|
-
...raw,
|
|
4318
|
-
tasks,
|
|
4319
|
-
"verified-by-tests": parseVerifiedByTests(raw["verified-by-tests"])
|
|
4320
|
-
};
|
|
4321
|
-
return { frontmatter, body: content.substring(match[0].length) };
|
|
4322
|
-
}
|
|
4323
|
-
function buildRequirementsTable(reqAudit) {
|
|
4324
|
-
const lines = [
|
|
4325
|
-
`## 1. Requirements Audit (Product, Quality, and Security)`,
|
|
4326
|
-
``,
|
|
4327
|
-
`| Requirement ID | Title | Source File | Declared Method | Linked Test Files | Status |`,
|
|
4328
|
-
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
4329
|
-
];
|
|
4330
|
-
for (const r of reqAudit) {
|
|
4331
|
-
const icon = r.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
4332
|
-
lines.push(
|
|
4333
|
-
`| **\`${r.id}\`** | ${r.title} | \`${r.file}\` | \`${r.method}\` | \`${r.testRefs}\` | ${icon} |`
|
|
4334
|
-
);
|
|
4335
|
-
}
|
|
4336
|
-
return lines;
|
|
4337
|
-
}
|
|
4338
|
-
function buildTasksTable(taskAudit) {
|
|
4339
|
-
const lines = [
|
|
4340
|
-
`## 2. Tasks Audit (SDD Delivery Engineering)`,
|
|
4341
|
-
``,
|
|
4342
|
-
`| Task ID | Title | tasks.md File | Method | Verification Command / Criterion | Status |`,
|
|
4343
|
-
`| :--- | :--- | :--- | :---: | :--- | :---: |`
|
|
4344
|
-
];
|
|
4345
|
-
for (const t of taskAudit) {
|
|
4346
|
-
const icon = t.status === "VERIFICADO_CON_PRUEBA" ? "\u2705 VERIFIED" : "\u274C NO TESTS";
|
|
4347
|
-
lines.push(
|
|
4348
|
-
`| **\`${t.id}\`** | ${t.title} | \`${t.file}\` | \`${t.method}\` | \`${t.commandOrCriteria}\` | ${icon} |`
|
|
4349
|
-
);
|
|
4350
|
-
}
|
|
4351
|
-
return lines;
|
|
4352
|
-
}
|
|
4353
|
-
function generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors) {
|
|
4354
|
-
const totalErrors = reqErrors + taskErrors;
|
|
4355
|
-
const verdict = totalErrors === 0 ? "100% VERIFIED WITH TESTS (PASSED)" : `BLOCKED: Detected ${totalErrors} items without tests (${reqErrors} Requirements, ${taskErrors} Tasks)`;
|
|
4356
|
-
const reportLines = [
|
|
4357
|
-
`# \u{1F9EA} Deterministic Test Coverage Audit for Requirements and Tasks`,
|
|
4358
|
-
``,
|
|
4359
|
-
`> **Audit Date:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
4360
|
-
`> **Overall Verdict:** ${verdict}`,
|
|
4361
|
-
``,
|
|
4362
|
-
`---`,
|
|
4363
|
-
``,
|
|
4364
|
-
...buildRequirementsTable(reqAudit),
|
|
4365
|
-
``,
|
|
4366
|
-
`---`,
|
|
4367
|
-
``,
|
|
4368
|
-
...buildTasksTable(taskAudit)
|
|
4369
|
-
];
|
|
4370
|
-
return reportLines.join("\n");
|
|
4371
|
-
}
|
|
4372
|
-
var EXCLUDED_TEST_DIRS = /* @__PURE__ */ new Set([
|
|
4373
|
-
"node_modules",
|
|
4374
|
-
".git",
|
|
4375
|
-
"dist",
|
|
4376
|
-
"bin",
|
|
4377
|
-
"obj",
|
|
4378
|
-
"build",
|
|
4379
|
-
"out",
|
|
4380
|
-
"target",
|
|
4381
|
-
"coverage",
|
|
4382
|
-
".changeset",
|
|
4383
|
-
"scratch",
|
|
4384
|
-
"fixtures",
|
|
4385
|
-
"templates",
|
|
4386
|
-
"examples",
|
|
4387
|
-
".vs",
|
|
4388
|
-
".idea"
|
|
4389
|
-
]);
|
|
4390
|
-
var TEST_EXTENSIONS = [".feature", ".spec.ts", ".spec.js", ".test.ts", ".test.js", "_test.py", "_test.go"];
|
|
4391
|
-
function isTestFile(fileName) {
|
|
4392
|
-
return TEST_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
4393
|
-
}
|
|
4394
|
-
function recordRequirementTest(reqToTestsMap, reqId, relTest) {
|
|
4395
|
-
const existing = reqToTestsMap.get(reqId) || [];
|
|
4396
|
-
if (!existing.includes(relTest)) {
|
|
4397
|
-
existing.push(relTest);
|
|
4398
|
-
reqToTestsMap.set(reqId, existing);
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
function parseFeatureTags(content, relTest, reqToTestsMap) {
|
|
4402
|
-
const lines = content.split("\n");
|
|
4403
|
-
for (const line of lines) {
|
|
4404
|
-
const trimmed = line.trim();
|
|
4405
|
-
if (!trimmed.startsWith("@")) continue;
|
|
4406
|
-
const tags = trimmed.split(/\s+/).filter((t) => t.startsWith("@"));
|
|
4407
|
-
for (const t of tags) {
|
|
4408
|
-
recordRequirementTest(reqToTestsMap, t.substring(1), relTest);
|
|
4409
|
-
}
|
|
4410
|
-
}
|
|
4411
|
-
}
|
|
4412
|
-
var REQ_ID_REGEX = /\b(FR|QR|CON|SEC-REQ)-[A-Z0-9]+(-[A-Z0-9]+)*\b/g;
|
|
4413
|
-
function parseCodeTestIds(content, relTest, reqToTestsMap) {
|
|
4414
|
-
const idMatches = content.match(REQ_ID_REGEX);
|
|
4415
|
-
if (!idMatches) return;
|
|
4416
|
-
for (const reqId of idMatches) {
|
|
4417
|
-
recordRequirementTest(reqToTestsMap, reqId, relTest);
|
|
4418
|
-
}
|
|
4419
|
-
}
|
|
4420
|
-
function processTestFile(fullPath, rootDir, reqToTestsMap) {
|
|
4421
|
-
try {
|
|
4422
|
-
const content = fs6__namespace.readFileSync(fullPath, "utf-8");
|
|
4423
|
-
const relTest = path5__namespace.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
4424
|
-
if (fullPath.endsWith(".feature")) {
|
|
4425
|
-
parseFeatureTags(content, relTest, reqToTestsMap);
|
|
4426
|
-
}
|
|
4427
|
-
parseCodeTestIds(content, relTest, reqToTestsMap);
|
|
4428
|
-
} catch {
|
|
4429
|
-
}
|
|
4430
|
-
}
|
|
4431
|
-
function collectTestFiles(dir, files = []) {
|
|
4432
|
-
if (!fs6__namespace.existsSync(dir)) return files;
|
|
4433
|
-
try {
|
|
4434
|
-
const entries = fs6__namespace.readdirSync(dir, { withFileTypes: true });
|
|
4435
|
-
for (const e of entries) {
|
|
4436
|
-
if (e.isDirectory()) {
|
|
4437
|
-
if (!EXCLUDED_TEST_DIRS.has(e.name)) {
|
|
4438
|
-
collectTestFiles(path5__namespace.join(dir, e.name), files);
|
|
4439
|
-
}
|
|
4440
|
-
} else if (isTestFile(e.name)) {
|
|
4441
|
-
files.push(path5__namespace.join(dir, e.name));
|
|
4442
|
-
}
|
|
4443
|
-
}
|
|
4444
|
-
} catch {
|
|
4445
|
-
}
|
|
4446
|
-
return files;
|
|
4447
|
-
}
|
|
4448
|
-
function scanAllTestArtifacts(rootDir) {
|
|
4449
|
-
const reqToTestsMap = /* @__PURE__ */ new Map();
|
|
4450
|
-
const testFiles = collectTestFiles(rootDir);
|
|
4451
|
-
for (const file of testFiles) {
|
|
4452
|
-
processTestFile(file, rootDir, reqToTestsMap);
|
|
4453
|
-
}
|
|
4454
|
-
return reqToTestsMap;
|
|
4455
|
-
}
|
|
4456
|
-
var REQ_PREFIXES = ["FR-", "QR-", "CON-", "SEC-REQ-"];
|
|
4457
|
-
function isRequirementArtifact(frontmatter, id) {
|
|
4458
|
-
if (frontmatter.type === "requirement" || frontmatter.type === "security-requirement") {
|
|
4459
|
-
return true;
|
|
4460
|
-
}
|
|
4461
|
-
return REQ_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
4462
|
-
}
|
|
4463
|
-
function resolveRequirementMethod(declaredMethod, matchingTests) {
|
|
4464
|
-
if (typeof declaredMethod === "string" && declaredMethod) {
|
|
4465
|
-
return declaredMethod;
|
|
4466
|
-
}
|
|
4467
|
-
return matchingTests.some((t) => t.endsWith(".feature")) ? "BDD_CUCUMBER" : "TEST_EXECUTION";
|
|
4468
|
-
}
|
|
4469
|
-
function auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap) {
|
|
4470
|
-
const matchingTests = reqToTestsMap.get(id) || [];
|
|
4471
|
-
const isVerified = isTemplate || matchingTests.length > 0;
|
|
4472
|
-
const method = resolveRequirementMethod(frontmatter["verifiable-by"], matchingTests);
|
|
4473
|
-
return {
|
|
4474
|
-
item: {
|
|
4475
|
-
id: id || "SIN-ID",
|
|
4476
|
-
title: frontmatter.title || "Sin t\xEDtulo",
|
|
4477
|
-
file: relPath,
|
|
4478
|
-
isTemplate,
|
|
4479
|
-
method,
|
|
4480
|
-
testRefs: matchingTests.join(", ") || "NINGUNA DETECTADA",
|
|
4481
|
-
existingCount: matchingTests.length,
|
|
4482
|
-
status: isVerified ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
4483
|
-
},
|
|
4484
|
-
isError: !isVerified
|
|
4485
|
-
};
|
|
4486
|
-
}
|
|
4487
|
-
function isTaskProofValid(criteria, isTemplate) {
|
|
4488
|
-
if (isTemplate) return true;
|
|
4489
|
-
return criteria.length > 4 && !criteria.includes("TODO") && !criteria.includes("TBD");
|
|
4490
|
-
}
|
|
4491
|
-
function auditSingleTask(t, relPath, isTemplate) {
|
|
4492
|
-
const crit = t.verification?.criteria || "";
|
|
4493
|
-
const method = t.verification?.method || "TEST_EXECUTION";
|
|
4494
|
-
const hasProof = isTaskProofValid(crit, isTemplate);
|
|
4495
|
-
return {
|
|
4496
|
-
item: {
|
|
4497
|
-
id: t.id,
|
|
4498
|
-
title: t.title || "Sin t\xEDtulo",
|
|
4499
|
-
file: relPath,
|
|
4500
|
-
method,
|
|
4501
|
-
commandOrCriteria: crit || "AUSENTE",
|
|
4502
|
-
status: hasProof ? "VERIFICADO_CON_PRUEBA" : "FALLO_SIN_PRUEBA"
|
|
4503
|
-
},
|
|
4504
|
-
hasProof
|
|
4505
|
-
};
|
|
4506
|
-
}
|
|
4507
|
-
function auditTasks(tasks, relPath, isTemplate) {
|
|
4508
|
-
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
4509
|
-
return { items: [], errorCount: 0 };
|
|
4510
|
-
}
|
|
4511
|
-
const items = [];
|
|
4512
|
-
let errorCount = 0;
|
|
4513
|
-
for (const t of tasks) {
|
|
4514
|
-
const { item, hasProof } = auditSingleTask(t, relPath, isTemplate);
|
|
4515
|
-
if (!hasProof) errorCount++;
|
|
4516
|
-
items.push(item);
|
|
4517
|
-
}
|
|
4518
|
-
return { items, errorCount };
|
|
4519
|
-
}
|
|
4520
|
-
function assembleCoverageResult(reqAudit, taskAudit, reqErrors, taskErrors) {
|
|
4521
|
-
return {
|
|
4522
|
-
success: reqErrors === 0 && taskErrors === 0,
|
|
4523
|
-
totalRequirements: reqAudit.length,
|
|
4524
|
-
passedRequirements: reqAudit.length - reqErrors,
|
|
4525
|
-
failedRequirements: reqErrors,
|
|
4526
|
-
totalTasks: taskAudit.length,
|
|
4527
|
-
passedTasks: taskAudit.length - taskErrors,
|
|
4528
|
-
failedTasks: taskErrors,
|
|
4529
|
-
requirements: reqAudit,
|
|
4530
|
-
tasks: taskAudit,
|
|
4531
|
-
reportMarkdown: generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors)
|
|
4532
|
-
};
|
|
4533
|
-
}
|
|
4534
|
-
function verifyTestingCoverage(options = {}) {
|
|
4535
|
-
const rootDir = options.rootDir || process.cwd();
|
|
4536
|
-
const allMdFiles = walkMdFiles(rootDir);
|
|
4537
|
-
const reqToTestsMap = scanAllTestArtifacts(rootDir);
|
|
4538
|
-
const reqAudit = [];
|
|
4539
|
-
const taskAudit = [];
|
|
4540
|
-
let reqErrors = 0;
|
|
4541
|
-
let taskErrors = 0;
|
|
4542
|
-
for (const file of allMdFiles) {
|
|
4543
|
-
try {
|
|
4544
|
-
const content = fs6__namespace.readFileSync(file, "utf-8");
|
|
4545
|
-
const { frontmatter } = parseGenericDoc(content);
|
|
4546
|
-
const relPath = path5__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
4547
|
-
const isTemplate = relPath.includes("templates/");
|
|
4548
|
-
const id = frontmatter.id || "";
|
|
4549
|
-
if (isRequirementArtifact(frontmatter, id)) {
|
|
4550
|
-
const auditedReq = auditRequirement(frontmatter, id, relPath, isTemplate, reqToTestsMap);
|
|
4551
|
-
reqAudit.push(auditedReq.item);
|
|
4552
|
-
if (auditedReq.isError) reqErrors++;
|
|
4553
|
-
}
|
|
4554
|
-
const auditedTasks = auditTasks(frontmatter.tasks, relPath, isTemplate);
|
|
4555
|
-
taskAudit.push(...auditedTasks.items);
|
|
4556
|
-
taskErrors += auditedTasks.errorCount;
|
|
4557
|
-
} catch {
|
|
4558
|
-
}
|
|
4559
|
-
}
|
|
4560
|
-
return assembleCoverageResult(reqAudit, taskAudit, reqErrors, taskErrors);
|
|
5017
|
+
return assembleGovernanceResult(data, reportMarkdown);
|
|
4561
5018
|
}
|
|
4562
5019
|
function isToolAvailable(toolName) {
|
|
4563
5020
|
try {
|
|
@@ -4603,8 +5060,8 @@ function findLicenseFile(pkgDir) {
|
|
|
4603
5060
|
"license.txt"
|
|
4604
5061
|
];
|
|
4605
5062
|
for (const c of candidates) {
|
|
4606
|
-
const full =
|
|
4607
|
-
if (
|
|
5063
|
+
const full = path4__namespace.join(pkgDir, c);
|
|
5064
|
+
if (fs7__namespace.existsSync(full)) {
|
|
4608
5065
|
return full;
|
|
4609
5066
|
}
|
|
4610
5067
|
}
|
|
@@ -4621,9 +5078,9 @@ var LICENSE_FILE_PATTERNS = [
|
|
|
4621
5078
|
[(t) => t.includes("Microsoft Reciprocal License") || t.includes("MS-RL"), "MS-RL"]
|
|
4622
5079
|
];
|
|
4623
5080
|
function resolveLicenseFromFile(licenseFilePath) {
|
|
4624
|
-
if (!licenseFilePath || !
|
|
5081
|
+
if (!licenseFilePath || !fs7__namespace.existsSync(licenseFilePath)) return void 0;
|
|
4625
5082
|
try {
|
|
4626
|
-
const text =
|
|
5083
|
+
const text = fs7__namespace.readFileSync(licenseFilePath, "utf-8");
|
|
4627
5084
|
for (const [matches, spdx] of LICENSE_FILE_PATTERNS) {
|
|
4628
5085
|
if (matches(text)) return spdx;
|
|
4629
5086
|
}
|
|
@@ -4632,16 +5089,16 @@ function resolveLicenseFromFile(licenseFilePath) {
|
|
|
4632
5089
|
return void 0;
|
|
4633
5090
|
}
|
|
4634
5091
|
function readPackageJson(filePath) {
|
|
4635
|
-
if (!
|
|
5092
|
+
if (!fs7__namespace.existsSync(filePath)) return null;
|
|
4636
5093
|
try {
|
|
4637
|
-
return JSON.parse(
|
|
5094
|
+
return JSON.parse(fs7__namespace.readFileSync(filePath, "utf-8"));
|
|
4638
5095
|
} catch {
|
|
4639
5096
|
return null;
|
|
4640
5097
|
}
|
|
4641
5098
|
}
|
|
4642
5099
|
function extractDirectDepNames(rootDir, productionOnly) {
|
|
4643
5100
|
const names = /* @__PURE__ */ new Set();
|
|
4644
|
-
const pkgPath =
|
|
5101
|
+
const pkgPath = path4__namespace.join(rootDir, "package.json");
|
|
4645
5102
|
const pkg = readPackageJson(pkgPath);
|
|
4646
5103
|
if (!pkg) return names;
|
|
4647
5104
|
if (pkg.dependencies && typeof pkg.dependencies === "object") {
|
|
@@ -4661,7 +5118,7 @@ function resolveSpdxForPackage(pkg, licFile) {
|
|
|
4661
5118
|
return spdx;
|
|
4662
5119
|
}
|
|
4663
5120
|
function processPackageDir(pkgDir, directNames, seen) {
|
|
4664
|
-
const pkg = readPackageJson(
|
|
5121
|
+
const pkg = readPackageJson(path4__namespace.join(pkgDir, "package.json"));
|
|
4665
5122
|
if (!pkg?.name || !pkg.version) return;
|
|
4666
5123
|
const key = `${pkg.name}@${pkg.version}`;
|
|
4667
5124
|
if (seen.has(key)) return;
|
|
@@ -4682,32 +5139,32 @@ function processPackageDir(pkgDir, directNames, seen) {
|
|
|
4682
5139
|
});
|
|
4683
5140
|
}
|
|
4684
5141
|
function scanScopeSubdirectory(scopeDir, directNames, seen) {
|
|
4685
|
-
const scopedEntries =
|
|
5142
|
+
const scopedEntries = fs7__namespace.readdirSync(scopeDir, { withFileTypes: true });
|
|
4686
5143
|
for (const scoped of scopedEntries) {
|
|
4687
5144
|
if (scoped.isDirectory()) {
|
|
4688
|
-
processPackageDir(
|
|
5145
|
+
processPackageDir(path4__namespace.join(scopeDir, scoped.name), directNames, seen);
|
|
4689
5146
|
}
|
|
4690
5147
|
}
|
|
4691
5148
|
}
|
|
4692
5149
|
function scanSubdirectories(baseDir, directNames, seen) {
|
|
4693
|
-
if (!
|
|
4694
|
-
const entries =
|
|
5150
|
+
if (!fs7__namespace.existsSync(baseDir)) return;
|
|
5151
|
+
const entries = fs7__namespace.readdirSync(baseDir, { withFileTypes: true });
|
|
4695
5152
|
for (const entry of entries) {
|
|
4696
5153
|
if (!entry.isDirectory()) continue;
|
|
4697
5154
|
if (entry.name.startsWith("@")) {
|
|
4698
|
-
scanScopeSubdirectory(
|
|
5155
|
+
scanScopeSubdirectory(path4__namespace.join(baseDir, entry.name), directNames, seen);
|
|
4699
5156
|
} else if (entry.name !== ".bin" && entry.name !== ".pnpm") {
|
|
4700
|
-
processPackageDir(
|
|
5157
|
+
processPackageDir(path4__namespace.join(baseDir, entry.name), directNames, seen);
|
|
4701
5158
|
}
|
|
4702
5159
|
}
|
|
4703
5160
|
}
|
|
4704
5161
|
function scanPnpmStore(pnpmDir, directNames, seen) {
|
|
4705
|
-
if (!
|
|
4706
|
-
const entries =
|
|
5162
|
+
if (!fs7__namespace.existsSync(pnpmDir)) return;
|
|
5163
|
+
const entries = fs7__namespace.readdirSync(pnpmDir, { withFileTypes: true });
|
|
4707
5164
|
for (const entry of entries) {
|
|
4708
5165
|
if (!entry.isDirectory()) continue;
|
|
4709
|
-
const candidateModules =
|
|
4710
|
-
if (
|
|
5166
|
+
const candidateModules = path4__namespace.join(pnpmDir, entry.name, "node_modules");
|
|
5167
|
+
if (fs7__namespace.existsSync(candidateModules)) {
|
|
4711
5168
|
scanSubdirectories(candidateModules, directNames, seen);
|
|
4712
5169
|
}
|
|
4713
5170
|
}
|
|
@@ -4715,11 +5172,11 @@ function scanPnpmStore(pnpmDir, directNames, seen) {
|
|
|
4715
5172
|
function scanWithNativeFs(rootDir, options = {}) {
|
|
4716
5173
|
const directNames = extractDirectDepNames(rootDir, options.productionOnly);
|
|
4717
5174
|
const seen = /* @__PURE__ */ new Map();
|
|
4718
|
-
const nmDir =
|
|
4719
|
-
if (
|
|
5175
|
+
const nmDir = path4__namespace.join(rootDir, "node_modules");
|
|
5176
|
+
if (fs7__namespace.existsSync(nmDir)) {
|
|
4720
5177
|
scanSubdirectories(nmDir, directNames, seen);
|
|
4721
5178
|
if (options.depth !== "direct") {
|
|
4722
|
-
scanPnpmStore(
|
|
5179
|
+
scanPnpmStore(path4__namespace.join(nmDir, ".pnpm"), directNames, seen);
|
|
4723
5180
|
}
|
|
4724
5181
|
}
|
|
4725
5182
|
const allDeps = Array.from(seen.values());
|
|
@@ -4843,10 +5300,10 @@ function generateCycloneDxSbom(dependencies, options = {}) {
|
|
|
4843
5300
|
let name = options.projectName || "ai-sdlc-project";
|
|
4844
5301
|
let version = options.projectVersion || "1.0.0";
|
|
4845
5302
|
if (options.rootDir) {
|
|
4846
|
-
const pkgPath =
|
|
4847
|
-
if (
|
|
5303
|
+
const pkgPath = path4__namespace.join(options.rootDir, "package.json");
|
|
5304
|
+
if (fs7__namespace.existsSync(pkgPath)) {
|
|
4848
5305
|
try {
|
|
4849
|
-
const pkg = JSON.parse(
|
|
5306
|
+
const pkg = JSON.parse(fs7__namespace.readFileSync(pkgPath, "utf-8"));
|
|
4850
5307
|
if (pkg.name) name = pkg.name;
|
|
4851
5308
|
if (pkg.version) version = pkg.version;
|
|
4852
5309
|
} catch {
|
|
@@ -4880,15 +5337,15 @@ function generateCycloneDxSbom(dependencies, options = {}) {
|
|
|
4880
5337
|
function tryAtomicWrite(filePath, content) {
|
|
4881
5338
|
const tempPath = `${filePath}.${process.pid}.${Math.random().toString(36).substring(2)}.tmp`;
|
|
4882
5339
|
try {
|
|
4883
|
-
|
|
5340
|
+
fs7__namespace.writeFileSync(tempPath, content, "utf-8");
|
|
4884
5341
|
try {
|
|
4885
|
-
if (
|
|
4886
|
-
|
|
5342
|
+
if (fs7__namespace.existsSync(filePath)) {
|
|
5343
|
+
fs7__namespace.rmSync(filePath, { force: true });
|
|
4887
5344
|
}
|
|
4888
|
-
|
|
5345
|
+
fs7__namespace.renameSync(tempPath, filePath);
|
|
4889
5346
|
} catch {
|
|
4890
|
-
|
|
4891
|
-
|
|
5347
|
+
fs7__namespace.copyFileSync(tempPath, filePath);
|
|
5348
|
+
fs7__namespace.unlinkSync(tempPath);
|
|
4892
5349
|
}
|
|
4893
5350
|
return true;
|
|
4894
5351
|
} catch {
|
|
@@ -4896,15 +5353,15 @@ function tryAtomicWrite(filePath, content) {
|
|
|
4896
5353
|
}
|
|
4897
5354
|
}
|
|
4898
5355
|
function safeWriteFileSync(filePath, content) {
|
|
4899
|
-
const dir =
|
|
4900
|
-
if (!
|
|
4901
|
-
|
|
5356
|
+
const dir = path4__namespace.dirname(filePath);
|
|
5357
|
+
if (!fs7__namespace.existsSync(dir)) {
|
|
5358
|
+
fs7__namespace.mkdirSync(dir, { recursive: true });
|
|
4902
5359
|
}
|
|
4903
5360
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
4904
5361
|
if (tryAtomicWrite(filePath, content)) return;
|
|
4905
5362
|
}
|
|
4906
5363
|
try {
|
|
4907
|
-
|
|
5364
|
+
fs7__namespace.writeFileSync(filePath, content, "utf-8");
|
|
4908
5365
|
} catch {
|
|
4909
5366
|
}
|
|
4910
5367
|
}
|
|
@@ -4930,9 +5387,9 @@ function buildThirdPartyHeader(projectName, count) {
|
|
|
4930
5387
|
];
|
|
4931
5388
|
}
|
|
4932
5389
|
function readLicenseTextPreview(licenseFile) {
|
|
4933
|
-
if (!licenseFile || !
|
|
5390
|
+
if (!licenseFile || !fs7__namespace.existsSync(licenseFile)) return null;
|
|
4934
5391
|
try {
|
|
4935
|
-
const text =
|
|
5392
|
+
const text = fs7__namespace.readFileSync(licenseFile, "utf-8").trim();
|
|
4936
5393
|
return text.slice(0, 1e3) + (text.length > 1e3 ? "\n... (texto truncado)" : "");
|
|
4937
5394
|
} catch {
|
|
4938
5395
|
return null;
|
|
@@ -5044,7 +5501,7 @@ function parseExceptions(exceptions) {
|
|
|
5044
5501
|
}
|
|
5045
5502
|
function parseLicensePolicy(content) {
|
|
5046
5503
|
try {
|
|
5047
|
-
const doc =
|
|
5504
|
+
const doc = yaml3__default.default.load(content);
|
|
5048
5505
|
const { permitted, restricted, blocked } = classifyCategories(doc?.categories);
|
|
5049
5506
|
const exception_packages = parseExceptions(doc?.exceptions);
|
|
5050
5507
|
return {
|
|
@@ -5180,9 +5637,9 @@ function generateLicenseReportMarkdown(dependencies, violations, policy, sbomPat
|
|
|
5180
5637
|
return lines.filter(Boolean).join("\n");
|
|
5181
5638
|
}
|
|
5182
5639
|
function parseManifestItems(manifestFile) {
|
|
5183
|
-
if (!
|
|
5640
|
+
if (!fs7__namespace.existsSync(manifestFile)) return [];
|
|
5184
5641
|
try {
|
|
5185
|
-
const doc =
|
|
5642
|
+
const doc = yaml3__default.default.load(fs7__namespace.readFileSync(manifestFile, "utf-8"));
|
|
5186
5643
|
return Array.isArray(doc?.evaluated_dependencies) ? doc.evaluated_dependencies : [];
|
|
5187
5644
|
} catch {
|
|
5188
5645
|
return [];
|
|
@@ -5214,10 +5671,10 @@ function verifyStaticManifest(manifestFile, policy) {
|
|
|
5214
5671
|
function resolveManifestPath(rootDir, customPath) {
|
|
5215
5672
|
if (customPath) return customPath;
|
|
5216
5673
|
const candidates = [
|
|
5217
|
-
|
|
5218
|
-
|
|
5674
|
+
path4__namespace.join(rootDir, "compliance", "license-manifest.yaml"),
|
|
5675
|
+
path4__namespace.join(rootDir, "license-manifest.yaml")
|
|
5219
5676
|
];
|
|
5220
|
-
return candidates.find((p) =>
|
|
5677
|
+
return candidates.find((p) => fs7__namespace.existsSync(p)) || candidates[0];
|
|
5221
5678
|
}
|
|
5222
5679
|
function handleStaticManifestVerification(options, rootDir, policy) {
|
|
5223
5680
|
const manifestFile = resolveManifestPath(rootDir, options.manifestPath);
|
|
@@ -5238,7 +5695,7 @@ function shouldGenerateSbom(options, rawPolicy) {
|
|
|
5238
5695
|
}
|
|
5239
5696
|
function maybeGenerateSbom(options, rootDir, rawPolicy, dependencies) {
|
|
5240
5697
|
if (!shouldGenerateSbom(options, rawPolicy)) return void 0;
|
|
5241
|
-
const sbomPath = options.sbomPath ||
|
|
5698
|
+
const sbomPath = options.sbomPath || path4__namespace.join(rootDir, "reports", "sbom.cdx.json");
|
|
5242
5699
|
const sbom = generateCycloneDxSbom(dependencies, { rootDir });
|
|
5243
5700
|
writeCycloneDxSbom(sbom, sbomPath);
|
|
5244
5701
|
return sbomPath;
|
|
@@ -5247,7 +5704,7 @@ function maybeGenerateNotices(options, rootDir, rawPolicy, dependencies) {
|
|
|
5247
5704
|
const shouldGen = options.generateNotices === true || Boolean(options.noticesPath || rawPolicy?.ci_gates?.output_notices_file);
|
|
5248
5705
|
if (!shouldGen) return void 0;
|
|
5249
5706
|
const noticesFileName = rawPolicy?.ci_gates?.output_notices_file || "THIRD_PARTY_NOTICES.md";
|
|
5250
|
-
const noticesPath = options.noticesPath ||
|
|
5707
|
+
const noticesPath = options.noticesPath || path4__namespace.join(rootDir, noticesFileName);
|
|
5251
5708
|
const noticesContent = generateThirdPartyNotices(dependencies);
|
|
5252
5709
|
writeThirdPartyNotices(noticesContent, noticesPath);
|
|
5253
5710
|
return noticesPath;
|
|
@@ -5259,8 +5716,8 @@ function handleDynamicScaArtifacts(options, rootDir, rawPolicy, dependencies) {
|
|
|
5259
5716
|
};
|
|
5260
5717
|
}
|
|
5261
5718
|
function loadLicensePolicy(rootDir, customPath) {
|
|
5262
|
-
const policyFile = customPath ||
|
|
5263
|
-
return
|
|
5719
|
+
const policyFile = customPath || path4__namespace.join(rootDir, "license-policy.yaml");
|
|
5720
|
+
return fs7__namespace.existsSync(policyFile) ? parseLicensePolicy(fs7__namespace.readFileSync(policyFile, "utf-8")) : parseLicensePolicy("");
|
|
5264
5721
|
}
|
|
5265
5722
|
function evaluateDependencyList(dependencies, policy) {
|
|
5266
5723
|
const violations = [];
|
|
@@ -5330,14 +5787,14 @@ var PROTECTED_BYPASS_PATTERNS = [
|
|
|
5330
5787
|
/migrations?[\\/]/i
|
|
5331
5788
|
];
|
|
5332
5789
|
function detectProfileFromChange(changeDir) {
|
|
5333
|
-
if (!
|
|
5334
|
-
const specFile =
|
|
5335
|
-
if (
|
|
5790
|
+
if (!fs7__namespace.existsSync(changeDir)) return "standard";
|
|
5791
|
+
const specFile = path4__namespace.join(changeDir, "spec.md");
|
|
5792
|
+
if (fs7__namespace.existsSync(specFile)) {
|
|
5336
5793
|
try {
|
|
5337
|
-
const rawSpec =
|
|
5794
|
+
const rawSpec = fs7__namespace.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
5338
5795
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5339
5796
|
if (match) {
|
|
5340
|
-
const parsedFm =
|
|
5797
|
+
const parsedFm = yaml3__default.default.load(match[1]);
|
|
5341
5798
|
if (parsedFm && parsedFm.profile && ["patch", "standard", "critical"].includes(String(parsedFm.profile))) {
|
|
5342
5799
|
return parsedFm.profile;
|
|
5343
5800
|
}
|
|
@@ -5352,17 +5809,17 @@ function isPathProtectedFromPatch(filePath) {
|
|
|
5352
5809
|
return PROTECTED_BYPASS_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
5353
5810
|
}
|
|
5354
5811
|
function resolveActiveChangeDir(rootDir) {
|
|
5355
|
-
const activeRoot =
|
|
5356
|
-
if (!
|
|
5812
|
+
const activeRoot = path4__namespace.join(rootDir, "specs", "changes", "active");
|
|
5813
|
+
if (!fs7__namespace.existsSync(activeRoot)) return void 0;
|
|
5357
5814
|
try {
|
|
5358
|
-
const dirs =
|
|
5815
|
+
const dirs = fs7__namespace.readdirSync(activeRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path4__namespace.join(activeRoot, d.name));
|
|
5359
5816
|
return dirs.find((d) => detectProfileFromChange(d) === "patch") || dirs[0];
|
|
5360
5817
|
} catch {
|
|
5361
5818
|
return void 0;
|
|
5362
5819
|
}
|
|
5363
5820
|
}
|
|
5364
5821
|
function detectGitDiffFiles(rootDir) {
|
|
5365
|
-
if (!
|
|
5822
|
+
if (!fs7__namespace.existsSync(path4__namespace.join(rootDir, ".git"))) return [];
|
|
5366
5823
|
try {
|
|
5367
5824
|
const tracked = child_process.execFileSync("git", ["diff", "--name-only", "HEAD"], {
|
|
5368
5825
|
cwd: rootDir,
|
|
@@ -5385,19 +5842,19 @@ function resolveChangeDirectory(rootDir, options) {
|
|
|
5385
5842
|
if (options.changeDir) return options.changeDir;
|
|
5386
5843
|
if (options.changeId) {
|
|
5387
5844
|
const candidates = [
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5845
|
+
path4__namespace.join(rootDir, "specs", options.changeId),
|
|
5846
|
+
path4__namespace.join(rootDir, "specs", "changes", "active", options.changeId),
|
|
5847
|
+
path4__namespace.join(rootDir, "examples", "specs", options.changeId),
|
|
5848
|
+
path4__namespace.join(rootDir, "specs", "changes", "completed", options.changeId)
|
|
5392
5849
|
];
|
|
5393
|
-
return candidates.find((c) =>
|
|
5850
|
+
return candidates.find((c) => fs7__namespace.existsSync(c));
|
|
5394
5851
|
}
|
|
5395
5852
|
return resolveActiveChangeDir(rootDir);
|
|
5396
5853
|
}
|
|
5397
5854
|
function collectPatchViolations(rootDir, modifiedFiles) {
|
|
5398
5855
|
const violations = [];
|
|
5399
5856
|
for (const rawFile of modifiedFiles) {
|
|
5400
|
-
const relPath =
|
|
5857
|
+
const relPath = path4__namespace.isAbsolute(rawFile) ? path4__namespace.relative(rootDir, rawFile) : rawFile;
|
|
5401
5858
|
if (isPathProtectedFromPatch(relPath)) {
|
|
5402
5859
|
violations.push(
|
|
5403
5860
|
`[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'.`
|
|
@@ -5463,7 +5920,7 @@ function parseMdFrontmatter(raw) {
|
|
|
5463
5920
|
return { fm: {}, body: cleaned.trim() };
|
|
5464
5921
|
}
|
|
5465
5922
|
try {
|
|
5466
|
-
const parsed =
|
|
5923
|
+
const parsed = yaml3__default.default.load(match[1]);
|
|
5467
5924
|
const fm = parsed && typeof parsed === "object" ? parsed : {};
|
|
5468
5925
|
return { fm, body: cleaned.substring(match[0].length).trim() };
|
|
5469
5926
|
} catch {
|
|
@@ -5503,13 +5960,13 @@ function extractManualMetadata(fm, id, manualType, relPath, body) {
|
|
|
5503
5960
|
}
|
|
5504
5961
|
function buildManualItem(rootDir, filePath) {
|
|
5505
5962
|
try {
|
|
5506
|
-
const raw =
|
|
5963
|
+
const raw = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
5507
5964
|
const { fm, body } = parseMdFrontmatter(raw);
|
|
5508
5965
|
const id = typeof fm.id === "string" ? fm.id.trim() : "";
|
|
5509
5966
|
const rawType = typeof fm.type === "string" ? fm.type.trim() : "";
|
|
5510
5967
|
const manualType = resolveManualType(id, rawType);
|
|
5511
5968
|
if (!id || !manualType) return null;
|
|
5512
|
-
const relPath =
|
|
5969
|
+
const relPath = path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
5513
5970
|
return extractManualMetadata(fm, id, manualType, relPath, body);
|
|
5514
5971
|
} catch {
|
|
5515
5972
|
return null;
|
|
@@ -5520,8 +5977,8 @@ function scanManualsInWorkspace(rootDir) {
|
|
|
5520
5977
|
const seenIds = /* @__PURE__ */ new Set();
|
|
5521
5978
|
const items = [];
|
|
5522
5979
|
for (const relDir of manualDirs) {
|
|
5523
|
-
const fullDir =
|
|
5524
|
-
if (!
|
|
5980
|
+
const fullDir = path4__namespace.join(rootDir, relDir);
|
|
5981
|
+
if (!fs7__namespace.existsSync(fullDir)) continue;
|
|
5525
5982
|
for (const file of walkMdFiles(fullDir)) {
|
|
5526
5983
|
const item = buildManualItem(rootDir, file);
|
|
5527
5984
|
if (item && !seenIds.has(item.id)) {
|
|
@@ -5544,10 +6001,10 @@ function classifyBaselineId(id, type, buckets) {
|
|
|
5544
6001
|
}
|
|
5545
6002
|
}
|
|
5546
6003
|
function processBaselineFile(file, rootDir, buckets) {
|
|
5547
|
-
const rel =
|
|
6004
|
+
const rel = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
5548
6005
|
if (rel.includes("/changes/") || rel.startsWith("changes/")) return;
|
|
5549
6006
|
try {
|
|
5550
|
-
const { fm } = parseMdFrontmatter(
|
|
6007
|
+
const { fm } = parseMdFrontmatter(fs7__namespace.readFileSync(file, "utf-8"));
|
|
5551
6008
|
const id = typeof fm.id === "string" ? fm.id.trim() : "";
|
|
5552
6009
|
const status = typeof fm.status === "string" ? fm.status.trim().toLowerCase() : "active";
|
|
5553
6010
|
if (!id || status === "retired" || status === "deprecated") return;
|
|
@@ -5568,8 +6025,8 @@ function collectBaselineTargets(rootDir) {
|
|
|
5568
6025
|
enclaves: /* @__PURE__ */ new Set()
|
|
5569
6026
|
};
|
|
5570
6027
|
for (const dir of ["product", "architecture", "security", "specs"]) {
|
|
5571
|
-
const fullDir =
|
|
5572
|
-
if (
|
|
6028
|
+
const fullDir = path4__namespace.join(rootDir, dir);
|
|
6029
|
+
if (fs7__namespace.existsSync(fullDir)) {
|
|
5573
6030
|
scanDirectoryForBaseline(rootDir, fullDir, buckets);
|
|
5574
6031
|
}
|
|
5575
6032
|
}
|
|
@@ -5626,10 +6083,10 @@ function detectCurrentGitBranch(rootDir) {
|
|
|
5626
6083
|
}
|
|
5627
6084
|
}
|
|
5628
6085
|
function resolveActiveChangeProfile(rootDir) {
|
|
5629
|
-
const activeRoot =
|
|
5630
|
-
if (!
|
|
6086
|
+
const activeRoot = path4__namespace.join(rootDir, "specs", "changes", "active");
|
|
6087
|
+
if (!fs7__namespace.existsSync(activeRoot)) return "standard";
|
|
5631
6088
|
try {
|
|
5632
|
-
const dirs =
|
|
6089
|
+
const dirs = fs7__namespace.readdirSync(activeRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path4__namespace.join(activeRoot, d.name));
|
|
5633
6090
|
const profiles = dirs.map((d) => detectProfileFromChange(d));
|
|
5634
6091
|
if (profiles.includes("critical")) return "critical";
|
|
5635
6092
|
if (profiles.includes("patch") && profiles.length === 1) return "patch";
|
|
@@ -5813,7 +6270,7 @@ function extractParsedFrontmatter(content) {
|
|
|
5813
6270
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5814
6271
|
if (!match) return null;
|
|
5815
6272
|
try {
|
|
5816
|
-
const parsed =
|
|
6273
|
+
const parsed = yaml3__default.default.load(match[1]);
|
|
5817
6274
|
if (parsed && typeof parsed === "object") {
|
|
5818
6275
|
const fm = parsed;
|
|
5819
6276
|
if (fm.id && isRequirementArtifact2(fm)) {
|
|
@@ -5842,11 +6299,11 @@ function buildRequirementObject(content, fm, body, filePath, relFile) {
|
|
|
5842
6299
|
}
|
|
5843
6300
|
function parseRequirementFile(filePath, rootDir) {
|
|
5844
6301
|
try {
|
|
5845
|
-
const content =
|
|
6302
|
+
const content = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
5846
6303
|
const extracted = extractParsedFrontmatter(content);
|
|
5847
6304
|
if (!extracted) return null;
|
|
5848
6305
|
const body = content.substring(extracted.matchLen);
|
|
5849
|
-
const relFile =
|
|
6306
|
+
const relFile = path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
5850
6307
|
return buildRequirementObject(content, extracted.fm, body, filePath, relFile);
|
|
5851
6308
|
} catch {
|
|
5852
6309
|
return null;
|
|
@@ -5858,7 +6315,7 @@ function isIgnoredScanPath(relFile) {
|
|
|
5858
6315
|
function collectRequirements(rootDir) {
|
|
5859
6316
|
const reqs = [];
|
|
5860
6317
|
for (const file of walkMdFiles(rootDir)) {
|
|
5861
|
-
const relFile =
|
|
6318
|
+
const relFile = path4__namespace.relative(rootDir, file).replace(/\\/g, "/");
|
|
5862
6319
|
if (isIgnoredScanPath(relFile)) continue;
|
|
5863
6320
|
const req = parseRequirementFile(file, rootDir);
|
|
5864
6321
|
if (req) reqs.push(req);
|
|
@@ -6217,10 +6674,10 @@ function isPathExcluded2(relPath, patterns) {
|
|
|
6217
6674
|
function walkProjectFiles(dir, rootDir, excludedPatterns = DEFAULT_EXCLUDED_PATTERNS) {
|
|
6218
6675
|
const results = [];
|
|
6219
6676
|
try {
|
|
6220
|
-
const entries =
|
|
6677
|
+
const entries = fs7__namespace.readdirSync(dir, { withFileTypes: true });
|
|
6221
6678
|
for (const entry of entries) {
|
|
6222
|
-
const fullPath =
|
|
6223
|
-
const relPath =
|
|
6679
|
+
const fullPath = path4__namespace.join(dir, entry.name);
|
|
6680
|
+
const relPath = path4__namespace.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
6224
6681
|
if (isPathExcluded2(relPath, excludedPatterns)) continue;
|
|
6225
6682
|
if (entry.isDirectory()) {
|
|
6226
6683
|
results.push(...walkProjectFiles(fullPath, rootDir, excludedPatterns));
|
|
@@ -6249,8 +6706,8 @@ function pickFirstString(...vals) {
|
|
|
6249
6706
|
}
|
|
6250
6707
|
function parseGitleaksItem(item, rootDir) {
|
|
6251
6708
|
const rawFile = pickFirstString(item.File, item.file);
|
|
6252
|
-
const filePath =
|
|
6253
|
-
const relPath =
|
|
6709
|
+
const filePath = path4__namespace.resolve(rootDir, rawFile);
|
|
6710
|
+
const relPath = path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6254
6711
|
const ruleId = pickFirstString(item.RuleID, item.ruleID) || "GITLEAKS-MATCH";
|
|
6255
6712
|
const rawSecret = pickFirstString(item.Secret, item.secret, item.Match);
|
|
6256
6713
|
const line = item.StartLine ?? item.line ?? 1;
|
|
@@ -6323,7 +6780,7 @@ function scanLineForSecrets(line, lineNumber, filePath, relPath, entropyThreshol
|
|
|
6323
6780
|
function scanContentForSecrets(content, filePath, rootDir, entropyThreshold = 4.3) {
|
|
6324
6781
|
const violations = [];
|
|
6325
6782
|
const lines = content.split(/\r?\n/);
|
|
6326
|
-
const relPath =
|
|
6783
|
+
const relPath = path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6327
6784
|
for (let i = 0; i < lines.length; i++) {
|
|
6328
6785
|
violations.push(...scanLineForSecrets(lines[i], i + 1, filePath, relPath, entropyThreshold));
|
|
6329
6786
|
}
|
|
@@ -6343,7 +6800,7 @@ function scanDiffItems(diffLines, rootDir, excludePatterns, entropyThreshold) {
|
|
|
6343
6800
|
const findings = [];
|
|
6344
6801
|
for (const [relFile, items] of fileGroups) {
|
|
6345
6802
|
if (isPathExcluded2(relFile, excludePatterns)) continue;
|
|
6346
|
-
const absPath =
|
|
6803
|
+
const absPath = path4__namespace.resolve(rootDir, relFile);
|
|
6347
6804
|
for (const item of items) {
|
|
6348
6805
|
const lineViolations = scanLineForSecrets(item.content, item.lineNumber, absPath, relFile, entropyThreshold);
|
|
6349
6806
|
for (const v of lineViolations) {
|
|
@@ -6358,7 +6815,7 @@ function scanAllProjectFiles(rootDir, excludePatterns, entropyThreshold) {
|
|
|
6358
6815
|
const findings = [];
|
|
6359
6816
|
for (const file of allFiles) {
|
|
6360
6817
|
try {
|
|
6361
|
-
const content =
|
|
6818
|
+
const content = fs7__namespace.readFileSync(file, "utf-8");
|
|
6362
6819
|
findings.push(...scanContentForSecrets(content, file, rootDir, entropyThreshold));
|
|
6363
6820
|
} catch {
|
|
6364
6821
|
}
|
|
@@ -6582,8 +7039,8 @@ function runSemgrepCli(rootDir) {
|
|
|
6582
7039
|
const parsed = JSON.parse(output);
|
|
6583
7040
|
if (Array.isArray(parsed.results)) {
|
|
6584
7041
|
for (const item of parsed.results) {
|
|
6585
|
-
const filePath =
|
|
6586
|
-
const relPath =
|
|
7042
|
+
const filePath = path4__namespace.resolve(rootDir, item.path);
|
|
7043
|
+
const relPath = path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6587
7044
|
violations.push({
|
|
6588
7045
|
filePath,
|
|
6589
7046
|
relPath,
|
|
@@ -6610,15 +7067,15 @@ function isSastExcluded(relPath, excludes) {
|
|
|
6610
7067
|
function walkSourceFiles(dir, rootDir, excludes = DEFAULT_SAST_EXCLUDES) {
|
|
6611
7068
|
const files = [];
|
|
6612
7069
|
try {
|
|
6613
|
-
const entries =
|
|
7070
|
+
const entries = fs7__namespace.readdirSync(dir, { withFileTypes: true });
|
|
6614
7071
|
for (const entry of entries) {
|
|
6615
|
-
const fullPath =
|
|
6616
|
-
const relPath =
|
|
7072
|
+
const fullPath = path4__namespace.join(dir, entry.name);
|
|
7073
|
+
const relPath = path4__namespace.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
6617
7074
|
if (isSastExcluded(relPath, excludes)) continue;
|
|
6618
7075
|
if (entry.isDirectory()) {
|
|
6619
7076
|
files.push(...walkSourceFiles(fullPath, rootDir, excludes));
|
|
6620
7077
|
} else if (entry.isFile()) {
|
|
6621
|
-
const ext =
|
|
7078
|
+
const ext = path4__namespace.extname(entry.name).toLowerCase();
|
|
6622
7079
|
if (GENERAL_SOURCE_EXTENSIONS.includes(ext)) {
|
|
6623
7080
|
files.push(fullPath);
|
|
6624
7081
|
}
|
|
@@ -6639,11 +7096,11 @@ function checkRuleMatch(rule, line, ext, minSeverity) {
|
|
|
6639
7096
|
}
|
|
6640
7097
|
function scanFileForSast(filePath, rootDir, minSeverity = "HIGH") {
|
|
6641
7098
|
const violations = [];
|
|
6642
|
-
const ext =
|
|
7099
|
+
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
6643
7100
|
try {
|
|
6644
|
-
const content =
|
|
7101
|
+
const content = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
6645
7102
|
const lines = content.split(/\r?\n/);
|
|
6646
|
-
const relPath =
|
|
7103
|
+
const relPath = path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6647
7104
|
for (let i = 0; i < lines.length; i++) {
|
|
6648
7105
|
const line = lines[i];
|
|
6649
7106
|
if (isLineExemptFromSast(line)) continue;
|
|
@@ -6669,8 +7126,8 @@ function scanFileForSast(filePath, rootDir, minSeverity = "HIGH") {
|
|
|
6669
7126
|
function collectSastFiles(rootDir, targetDirs) {
|
|
6670
7127
|
const files = [];
|
|
6671
7128
|
for (const tDir of targetDirs) {
|
|
6672
|
-
const fullDir =
|
|
6673
|
-
if (
|
|
7129
|
+
const fullDir = path4__namespace.join(rootDir, tDir);
|
|
7130
|
+
if (fs7__namespace.existsSync(fullDir)) {
|
|
6674
7131
|
files.push(...walkSourceFiles(fullDir, rootDir));
|
|
6675
7132
|
}
|
|
6676
7133
|
}
|
|
@@ -6825,7 +7282,7 @@ function calculateRating(mi, cc) {
|
|
|
6825
7282
|
return "F";
|
|
6826
7283
|
}
|
|
6827
7284
|
function detectLanguage(filePath) {
|
|
6828
|
-
const ext =
|
|
7285
|
+
const ext = path4__namespace.extname(filePath).toLowerCase();
|
|
6829
7286
|
switch (ext) {
|
|
6830
7287
|
case ".ts":
|
|
6831
7288
|
return "TypeScript";
|
|
@@ -6850,7 +7307,7 @@ function detectLanguage(filePath) {
|
|
|
6850
7307
|
}
|
|
6851
7308
|
function parseJsonSummaryFile(filePath, relSource) {
|
|
6852
7309
|
try {
|
|
6853
|
-
const raw = JSON.parse(
|
|
7310
|
+
const raw = JSON.parse(fs7__namespace.readFileSync(filePath, "utf-8"));
|
|
6854
7311
|
const total = raw.total;
|
|
6855
7312
|
if (!total || typeof total !== "object") return null;
|
|
6856
7313
|
const linesPct = Number(total.lines?.pct);
|
|
@@ -6873,7 +7330,7 @@ function updateLcovCounts(line, counts) {
|
|
|
6873
7330
|
}
|
|
6874
7331
|
function parseLcovFile(filePath, relSource) {
|
|
6875
7332
|
try {
|
|
6876
|
-
const lines =
|
|
7333
|
+
const lines = fs7__namespace.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
6877
7334
|
const counts = { LF: 0, LH: 0, BRF: 0, BRH: 0, FNF: 0, FNH: 0 };
|
|
6878
7335
|
for (const line of lines) {
|
|
6879
7336
|
updateLcovCounts(line, counts);
|
|
@@ -6893,16 +7350,16 @@ function parseLcovFile(filePath, relSource) {
|
|
|
6893
7350
|
function readCodeCoverageSummary(rootDir = process.cwd()) {
|
|
6894
7351
|
const jsonCandidates = ["coverage/coverage-summary.json", "reports/coverage-summary.json"];
|
|
6895
7352
|
for (const rel of jsonCandidates) {
|
|
6896
|
-
const full =
|
|
6897
|
-
if (
|
|
7353
|
+
const full = path4__namespace.join(rootDir, rel);
|
|
7354
|
+
if (fs7__namespace.existsSync(full)) {
|
|
6898
7355
|
const parsed = parseJsonSummaryFile(full, rel);
|
|
6899
7356
|
if (parsed) return parsed;
|
|
6900
7357
|
}
|
|
6901
7358
|
}
|
|
6902
7359
|
const lcovCandidates = ["coverage/lcov.info", "lcov.info", "reports/lcov.info"];
|
|
6903
7360
|
for (const rel of lcovCandidates) {
|
|
6904
|
-
const full =
|
|
6905
|
-
if (
|
|
7361
|
+
const full = path4__namespace.join(rootDir, rel);
|
|
7362
|
+
if (fs7__namespace.existsSync(full)) {
|
|
6906
7363
|
const parsed = parseLcovFile(full, rel);
|
|
6907
7364
|
if (parsed) return parsed;
|
|
6908
7365
|
}
|
|
@@ -7038,9 +7495,9 @@ function renderQualityReportMarkdown(gateResult, coverage, accumulated, averages
|
|
|
7038
7495
|
return [...header, "", "---", "", ...table, "", "---", "", ...polyglot, ...violationsSection, "", EVALUATION_STANDARDS_MD].join("\n");
|
|
7039
7496
|
}
|
|
7040
7497
|
function writeQualityReportFile(rootDir, markdown) {
|
|
7041
|
-
const reportsDir =
|
|
7042
|
-
|
|
7043
|
-
|
|
7498
|
+
const reportsDir = path4__namespace.join(rootDir, "reports");
|
|
7499
|
+
fs7__namespace.mkdirSync(reportsDir, { recursive: true });
|
|
7500
|
+
fs7__namespace.writeFileSync(path4__namespace.join(reportsDir, "QUALITY_REPORT.md"), markdown, "utf-8");
|
|
7044
7501
|
}
|
|
7045
7502
|
function generateQualityReport(options = {}) {
|
|
7046
7503
|
const rootDir = options.rootDir || process.cwd();
|
|
@@ -7182,12 +7639,12 @@ Flujo de Integraci\xF3n (Merges):`,
|
|
|
7182
7639
|
};
|
|
7183
7640
|
}
|
|
7184
7641
|
function resolveVersionFromProposal(changeDir) {
|
|
7185
|
-
const proposalFile =
|
|
7186
|
-
if (!
|
|
7187
|
-
const propContent =
|
|
7642
|
+
const proposalFile = path4__namespace.join(changeDir, "proposal.md");
|
|
7643
|
+
if (!fs7__namespace.existsSync(proposalFile)) return "1.0.0";
|
|
7644
|
+
const propContent = fs7__namespace.readFileSync(proposalFile, "utf-8");
|
|
7188
7645
|
const propFm = propContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7189
7646
|
if (!propFm) return "1.0.0";
|
|
7190
|
-
const parsedProp =
|
|
7647
|
+
const parsedProp = yaml3__default.default.load(propFm[1]);
|
|
7191
7648
|
return parsedProp?.version ? String(parsedProp.version) : "1.0.0";
|
|
7192
7649
|
}
|
|
7193
7650
|
function resolveChangeVersion(fm, changeDir) {
|
|
@@ -7201,12 +7658,12 @@ function isTaskMatchingId(currentId, cleanTaskId) {
|
|
|
7201
7658
|
return currentLower === targetLower || currentLower.startsWith(`${targetLower}-`) || targetLower.startsWith(`${currentLower}-`);
|
|
7202
7659
|
}
|
|
7203
7660
|
function parseTasksFrontmatter(tasksFile) {
|
|
7204
|
-
if (!
|
|
7661
|
+
if (!fs7__namespace.existsSync(tasksFile)) return null;
|
|
7205
7662
|
try {
|
|
7206
|
-
const content =
|
|
7663
|
+
const content = fs7__namespace.readFileSync(tasksFile, "utf-8");
|
|
7207
7664
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7208
7665
|
if (!fmMatch) return null;
|
|
7209
|
-
const fm =
|
|
7666
|
+
const fm = yaml3__default.default.load(fmMatch[1]);
|
|
7210
7667
|
return fm && Array.isArray(fm.tasks) ? fm : null;
|
|
7211
7668
|
} catch {
|
|
7212
7669
|
return null;
|
|
@@ -7225,7 +7682,7 @@ function buildTaskMatch(currentId, currentTitle, rawChangeId, changeDir, version
|
|
|
7225
7682
|
};
|
|
7226
7683
|
}
|
|
7227
7684
|
function inspectChangeTasks(changeDir, entryName, cleanTaskId, availableTasks) {
|
|
7228
|
-
const fm = parseTasksFrontmatter(
|
|
7685
|
+
const fm = parseTasksFrontmatter(path4__namespace.join(changeDir, "tasks.md"));
|
|
7229
7686
|
if (!fm || !fm.tasks) return void 0;
|
|
7230
7687
|
const rawChangeId = resolveRawChangeId(fm, entryName);
|
|
7231
7688
|
const versionStr = resolveChangeVersion(fm, changeDir);
|
|
@@ -7246,16 +7703,16 @@ function locateTaskInActiveChanges(taskId, rootDir = process.cwd()) {
|
|
|
7246
7703
|
if (!cleanTaskId) {
|
|
7247
7704
|
return { found: false, error: "Debe especificar un identificador de tarea v\xE1lido (ej. TSK-001)." };
|
|
7248
7705
|
}
|
|
7249
|
-
const activeChangesDir =
|
|
7250
|
-
if (!
|
|
7706
|
+
const activeChangesDir = path4__namespace.join(rootDir, "specs", "changes", "active");
|
|
7707
|
+
if (!fs7__namespace.existsSync(activeChangesDir)) {
|
|
7251
7708
|
return { found: false, error: `No se encontr\xF3 el directorio de cambios activos en '${activeChangesDir}'.` };
|
|
7252
7709
|
}
|
|
7253
7710
|
const availableTasks = [];
|
|
7254
|
-
const candidateDirs =
|
|
7711
|
+
const candidateDirs = fs7__namespace.readdirSync(activeChangesDir, { withFileTypes: true });
|
|
7255
7712
|
let matchedTask;
|
|
7256
7713
|
for (const entry of candidateDirs) {
|
|
7257
7714
|
if (!entry.isDirectory()) continue;
|
|
7258
|
-
const changeDir =
|
|
7715
|
+
const changeDir = path4__namespace.join(activeChangesDir, entry.name);
|
|
7259
7716
|
const m = inspectChangeTasks(changeDir, entry.name, cleanTaskId, availableTasks);
|
|
7260
7717
|
if (m && !matchedTask) {
|
|
7261
7718
|
matchedTask = m;
|
|
@@ -7319,7 +7776,7 @@ function resolveBranchHierarchy(match, rootDir = process.cwd()) {
|
|
|
7319
7776
|
const branches = getGitBranchList(rootDir);
|
|
7320
7777
|
const cleanVersion = (match.version || "1.0.0").replace(/^v/, "");
|
|
7321
7778
|
const releaseBranch = detectExistingReleaseBranch(branches, cleanVersion);
|
|
7322
|
-
const dirName =
|
|
7779
|
+
const dirName = path4__namespace.basename(match.changeDir);
|
|
7323
7780
|
const changeSlug = dirName.replace(/^(chg|CHG)-/i, (m) => m.toUpperCase());
|
|
7324
7781
|
const featureBranch = detectExistingFeatureBranch(branches, dirName, match.changeId, changeSlug);
|
|
7325
7782
|
const parentIdMatch = featureBranch.replace(/^(feat|feature)\//i, "").match(/^[a-zA-Z0-9]+-[a-zA-Z0-9]+/);
|
|
@@ -7626,13 +8083,13 @@ function getGitConfigValue(key, cwd) {
|
|
|
7626
8083
|
}
|
|
7627
8084
|
function readSessionTelemetry(cwd) {
|
|
7628
8085
|
const possiblePaths = [
|
|
7629
|
-
|
|
7630
|
-
|
|
8086
|
+
path4__namespace.join(cwd, ".ai-sdlc", "session-telemetry.json"),
|
|
8087
|
+
path4__namespace.join(cwd, ".ai-sdlc", "agent-session.json")
|
|
7631
8088
|
];
|
|
7632
8089
|
for (const p of possiblePaths) {
|
|
7633
|
-
if (
|
|
8090
|
+
if (fs7__namespace.existsSync(p)) {
|
|
7634
8091
|
try {
|
|
7635
|
-
const raw =
|
|
8092
|
+
const raw = fs7__namespace.readFileSync(p, "utf-8");
|
|
7636
8093
|
return JSON.parse(raw);
|
|
7637
8094
|
} catch {
|
|
7638
8095
|
}
|
|
@@ -7852,20 +8309,20 @@ if [ -f "$TELEMETRY_FILE" ]; then
|
|
|
7852
8309
|
fi
|
|
7853
8310
|
`;
|
|
7854
8311
|
function installGitHooks(rootDir = process.cwd()) {
|
|
7855
|
-
const gitDir =
|
|
7856
|
-
if (!
|
|
8312
|
+
const gitDir = path4__namespace.join(rootDir, ".git");
|
|
8313
|
+
if (!fs7__namespace.existsSync(gitDir)) {
|
|
7857
8314
|
return {
|
|
7858
8315
|
success: false,
|
|
7859
8316
|
error: `No se encontr\xF3 el directorio Git en '${gitDir}'.`
|
|
7860
8317
|
};
|
|
7861
8318
|
}
|
|
7862
|
-
const hooksDir =
|
|
7863
|
-
if (!
|
|
7864
|
-
|
|
8319
|
+
const hooksDir = path4__namespace.join(gitDir, "hooks");
|
|
8320
|
+
if (!fs7__namespace.existsSync(hooksDir)) {
|
|
8321
|
+
fs7__namespace.mkdirSync(hooksDir, { recursive: true });
|
|
7865
8322
|
}
|
|
7866
|
-
const hookPath =
|
|
8323
|
+
const hookPath = path4__namespace.join(hooksDir, "prepare-commit-msg");
|
|
7867
8324
|
try {
|
|
7868
|
-
|
|
8325
|
+
fs7__namespace.writeFileSync(hookPath, PREPARE_COMMIT_MSG_SCRIPT, { encoding: "utf-8", mode: 493 });
|
|
7869
8326
|
return {
|
|
7870
8327
|
success: true,
|
|
7871
8328
|
hookPath
|
|
@@ -8177,16 +8634,16 @@ function formatReleaseKpiMarkdown(report) {
|
|
|
8177
8634
|
].join("\n");
|
|
8178
8635
|
}
|
|
8179
8636
|
function writeReleaseKpiReport(report, options = {}) {
|
|
8180
|
-
const root = options.outputDir || (options.rootDir ?
|
|
8181
|
-
if (!
|
|
8182
|
-
|
|
8637
|
+
const root = options.outputDir || (options.rootDir ? path4__namespace.join(options.rootDir, "reports", "releases") : path4__namespace.join(process.cwd(), "reports", "releases"));
|
|
8638
|
+
if (!fs7__namespace.existsSync(root)) {
|
|
8639
|
+
fs7__namespace.mkdirSync(root, { recursive: true });
|
|
8183
8640
|
}
|
|
8184
8641
|
const cleanBranchName = report.releaseBranch.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
8185
|
-
const mdPath =
|
|
8186
|
-
const jsonPath =
|
|
8642
|
+
const mdPath = path4__namespace.join(root, `RELEASE_KPIS_${cleanBranchName}.md`);
|
|
8643
|
+
const jsonPath = path4__namespace.join(root, `release_${cleanBranchName}.kpis.json`);
|
|
8187
8644
|
const mdContent = formatReleaseKpiMarkdown(report);
|
|
8188
|
-
|
|
8189
|
-
|
|
8645
|
+
fs7__namespace.writeFileSync(mdPath, mdContent, "utf-8");
|
|
8646
|
+
fs7__namespace.writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
8190
8647
|
return { markdownPath: mdPath, jsonPath };
|
|
8191
8648
|
}
|
|
8192
8649
|
var cachedScript = null;
|
|
@@ -8195,19 +8652,19 @@ function resolveCytoscapePath() {
|
|
|
8195
8652
|
try {
|
|
8196
8653
|
const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
8197
8654
|
const entryPath = req.resolve("cytoscape");
|
|
8198
|
-
const dir =
|
|
8199
|
-
const minPath =
|
|
8200
|
-
if (
|
|
8655
|
+
const dir = path4__namespace.dirname(entryPath);
|
|
8656
|
+
const minPath = path4__namespace.join(dir, "cytoscape.min.js");
|
|
8657
|
+
if (fs7__namespace.existsSync(minPath)) {
|
|
8201
8658
|
return minPath;
|
|
8202
8659
|
}
|
|
8203
8660
|
} catch {
|
|
8204
8661
|
}
|
|
8205
8662
|
const searchDirs = [
|
|
8206
|
-
|
|
8207
|
-
|
|
8663
|
+
path4__namespace.join(process.cwd(), "node_modules", "cytoscape", "dist", "cytoscape.min.js"),
|
|
8664
|
+
path4__namespace.join(process.cwd(), "packages", "core", "node_modules", "cytoscape", "dist", "cytoscape.min.js")
|
|
8208
8665
|
];
|
|
8209
8666
|
for (const candidate of searchDirs) {
|
|
8210
|
-
if (
|
|
8667
|
+
if (fs7__namespace.existsSync(candidate)) {
|
|
8211
8668
|
return candidate;
|
|
8212
8669
|
}
|
|
8213
8670
|
}
|
|
@@ -8220,7 +8677,7 @@ function getCytoscapeScript() {
|
|
|
8220
8677
|
const scriptPath = resolveCytoscapePath();
|
|
8221
8678
|
if (scriptPath) {
|
|
8222
8679
|
try {
|
|
8223
|
-
cachedScript =
|
|
8680
|
+
cachedScript = fs7__namespace.readFileSync(scriptPath, "utf-8");
|
|
8224
8681
|
return cachedScript;
|
|
8225
8682
|
} catch {
|
|
8226
8683
|
}
|
|
@@ -8304,13 +8761,13 @@ function extractFrontmatter2(fileContent) {
|
|
|
8304
8761
|
const match = fileContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
8305
8762
|
if (!match) return null;
|
|
8306
8763
|
try {
|
|
8307
|
-
return
|
|
8764
|
+
return yaml3__default.default.load(match[1]) || null;
|
|
8308
8765
|
} catch {
|
|
8309
8766
|
return null;
|
|
8310
8767
|
}
|
|
8311
8768
|
}
|
|
8312
8769
|
function toScannedArtifact(filePath, relPath) {
|
|
8313
|
-
const fileContent =
|
|
8770
|
+
const fileContent = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
8314
8771
|
const fm = extractFrontmatter2(fileContent);
|
|
8315
8772
|
const id = typeof fm?.id === "string" ? fm.id.trim() : "";
|
|
8316
8773
|
if (!id) return null;
|
|
@@ -8322,12 +8779,12 @@ function toScannedArtifact(filePath, relPath) {
|
|
|
8322
8779
|
return { id, type: classified.type, title, layer: classified.layer, filePath: relPath, content, frontmatter: fm };
|
|
8323
8780
|
}
|
|
8324
8781
|
function parseAndCatalogArtifactFile(rootDir, filePath, scannedMap, archViewByBasename) {
|
|
8325
|
-
const relPath =
|
|
8782
|
+
const relPath = path4__namespace.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
8326
8783
|
if (relPath.includes("/changes/") || relPath.startsWith("changes/")) return;
|
|
8327
8784
|
const scannedArt = toScannedArtifact(filePath, relPath);
|
|
8328
8785
|
if (!scannedArt) return;
|
|
8329
8786
|
if (scannedArt.layer === "architecture" && scannedArt.type === "architecture") {
|
|
8330
|
-
archViewByBasename.set(
|
|
8787
|
+
archViewByBasename.set(path4__namespace.basename(relPath), scannedArt);
|
|
8331
8788
|
} else {
|
|
8332
8789
|
scannedMap.set(scannedArt.id, scannedArt);
|
|
8333
8790
|
}
|
|
@@ -8337,8 +8794,8 @@ function scanWorkspaceArtifacts(rootDir) {
|
|
|
8337
8794
|
const archViewByBasename = /* @__PURE__ */ new Map();
|
|
8338
8795
|
const artifactDirs = ["product", "architecture", "specs", "security"];
|
|
8339
8796
|
for (const dirName of artifactDirs) {
|
|
8340
|
-
const fullDirPath =
|
|
8341
|
-
if (
|
|
8797
|
+
const fullDirPath = path4__namespace.join(rootDir, dirName);
|
|
8798
|
+
if (fs7__namespace.existsSync(fullDirPath)) {
|
|
8342
8799
|
for (const f of walkMdFiles(fullDirPath)) {
|
|
8343
8800
|
parseAndCatalogArtifactFile(rootDir, f, scannedArtifactsMap, archViewByBasename);
|
|
8344
8801
|
}
|
|
@@ -8396,9 +8853,9 @@ function addTraceArchEdges(r, status, archViewByBasename, nodeMap, addEdge) {
|
|
|
8396
8853
|
}
|
|
8397
8854
|
}
|
|
8398
8855
|
function loadTestContentPreview(filePath) {
|
|
8399
|
-
if (!
|
|
8856
|
+
if (!fs7__namespace.existsSync(filePath)) return void 0;
|
|
8400
8857
|
try {
|
|
8401
|
-
const testContent =
|
|
8858
|
+
const testContent = fs7__namespace.readFileSync(filePath, "utf-8");
|
|
8402
8859
|
const lang = filePath.endsWith(".feature") ? "gherkin" : "typescript";
|
|
8403
8860
|
const preview = testContent.substring(0, 1500) + (testContent.length > 1500 ? "\n... (truncated)" : "");
|
|
8404
8861
|
return "```" + lang + "\n" + preview + "\n```";
|
|
@@ -8408,10 +8865,10 @@ function loadTestContentPreview(filePath) {
|
|
|
8408
8865
|
}
|
|
8409
8866
|
function ensureTestNode(t, testId, rootDir, nodeMap) {
|
|
8410
8867
|
if (nodeMap.has(testId)) return;
|
|
8411
|
-
const fullTestPath =
|
|
8868
|
+
const fullTestPath = path4__namespace.isAbsolute(t) ? t : path4__namespace.join(rootDir, t);
|
|
8412
8869
|
nodeMap.set(testId, {
|
|
8413
8870
|
id: testId,
|
|
8414
|
-
label:
|
|
8871
|
+
label: path4__namespace.basename(t),
|
|
8415
8872
|
title: t,
|
|
8416
8873
|
type: t.endsWith(".feature") ? "bdd-feature" : "code-test",
|
|
8417
8874
|
layer: "test",
|
|
@@ -10446,9 +10903,9 @@ var IGNORED_SCAN_DIRS = /* @__PURE__ */ new Set([
|
|
|
10446
10903
|
".agent"
|
|
10447
10904
|
]);
|
|
10448
10905
|
function readKpiFilesInDir(dir) {
|
|
10449
|
-
if (!
|
|
10906
|
+
if (!fs7__namespace.existsSync(dir)) return [];
|
|
10450
10907
|
try {
|
|
10451
|
-
return
|
|
10908
|
+
return fs7__namespace.readdirSync(dir).filter((f) => f.endsWith(".kpis.json")).map((f) => path4__namespace.join(dir, f));
|
|
10452
10909
|
} catch {
|
|
10453
10910
|
return [];
|
|
10454
10911
|
}
|
|
@@ -10456,11 +10913,11 @@ function readKpiFilesInDir(dir) {
|
|
|
10456
10913
|
function scanSubdirKpiFiles(dir, depth, acc) {
|
|
10457
10914
|
if (depth > 4) return;
|
|
10458
10915
|
try {
|
|
10459
|
-
const entries =
|
|
10916
|
+
const entries = fs7__namespace.readdirSync(dir, { withFileTypes: true });
|
|
10460
10917
|
for (const entry of entries) {
|
|
10461
10918
|
if (!entry.isDirectory() || IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
10462
|
-
const sub =
|
|
10463
|
-
if (entry.name === "releases" &&
|
|
10919
|
+
const sub = path4__namespace.join(dir, entry.name);
|
|
10920
|
+
if (entry.name === "releases" && path4__namespace.basename(dir) === "reports") {
|
|
10464
10921
|
acc.push(...readKpiFilesInDir(sub));
|
|
10465
10922
|
} else {
|
|
10466
10923
|
scanSubdirKpiFiles(sub, depth + 1, acc);
|
|
@@ -10470,7 +10927,7 @@ function scanSubdirKpiFiles(dir, depth, acc) {
|
|
|
10470
10927
|
}
|
|
10471
10928
|
}
|
|
10472
10929
|
function findKpiJsonFiles(rootDir) {
|
|
10473
|
-
const primaryFiles = readKpiFilesInDir(
|
|
10930
|
+
const primaryFiles = readKpiFilesInDir(path4__namespace.join(rootDir, "reports", "releases"));
|
|
10474
10931
|
if (primaryFiles.length > 0) return primaryFiles;
|
|
10475
10932
|
const found = [];
|
|
10476
10933
|
scanSubdirKpiFiles(rootDir, 0, found);
|
|
@@ -10481,7 +10938,7 @@ function collectHistoricalKpis(rootDir) {
|
|
|
10481
10938
|
const kpiFiles = findKpiJsonFiles(rootDir);
|
|
10482
10939
|
for (const fullPath of kpiFiles) {
|
|
10483
10940
|
try {
|
|
10484
|
-
const content =
|
|
10941
|
+
const content = fs7__namespace.readFileSync(fullPath, "utf-8");
|
|
10485
10942
|
const report = JSON.parse(content);
|
|
10486
10943
|
if (!items.some((it) => it.id === report.releaseBranch && it.timestamp === report.generatedAt)) {
|
|
10487
10944
|
items.push({
|
|
@@ -10678,15 +11135,15 @@ function computeGateAverages(gateResults) {
|
|
|
10678
11135
|
}
|
|
10679
11136
|
function generateDashboardReport(options = {}) {
|
|
10680
11137
|
const rootDir = options.rootDir || process.cwd();
|
|
10681
|
-
const outputPath = options.outputPath ||
|
|
11138
|
+
const outputPath = options.outputPath || path4__namespace.join(rootDir, "reports", "dashboard.html");
|
|
10682
11139
|
const graphData = buildGraphElements(rootDir);
|
|
10683
11140
|
const metricsData = collectDashboardMetrics(rootDir);
|
|
10684
11141
|
const html = renderDashboardHtml(graphData, metricsData, options.title);
|
|
10685
|
-
const outDir =
|
|
10686
|
-
if (!
|
|
10687
|
-
|
|
11142
|
+
const outDir = path4__namespace.dirname(outputPath);
|
|
11143
|
+
if (!fs7__namespace.existsSync(outDir)) {
|
|
11144
|
+
fs7__namespace.mkdirSync(outDir, { recursive: true });
|
|
10688
11145
|
}
|
|
10689
|
-
|
|
11146
|
+
fs7__namespace.writeFileSync(outputPath, html, "utf-8");
|
|
10690
11147
|
const { bddCoverageRatio, traceabilityRatio } = computeSummaryRatios(metricsData);
|
|
10691
11148
|
const { avgMi, avgCc } = computeGateAverages(metricsData.gate.results);
|
|
10692
11149
|
return {
|
|
@@ -10782,18 +11239,25 @@ var SPECIALIZED_AGENT_TEMPLATES = [
|
|
|
10782
11239
|
{
|
|
10783
11240
|
id: "agent-qa-engineer",
|
|
10784
11241
|
title: "QA Engineer and SDET",
|
|
10785
|
-
description: "Translate approved requirements (FR-*, SEC-REQ-*, QR-*) into comprehensive FAILING (red) BDD/Gherkin and unit test suites before production code is written.",
|
|
11242
|
+
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.",
|
|
10786
11243
|
globs: "tests/**, specs/changes/**/spec.md",
|
|
10787
11244
|
body: `## Role & Mission
|
|
10788
11245
|
- **Role**: QA Engineer and SDET (Software Development Engineer in Test) Agent (\`agent-qa-engineer\`).
|
|
10789
|
-
- **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.
|
|
11246
|
+
- **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.
|
|
10790
11247
|
|
|
10791
11248
|
## Operational Directives & Guardrails
|
|
10792
11249
|
- Read exclusively requirements cited in the \`handoff.yaml\` sidecar (\`HOF-*\`) and their referenced artifacts.
|
|
10793
|
-
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
11250
|
+
- **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.
|
|
11251
|
+
- **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.
|
|
11252
|
+
- **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:
|
|
11253
|
+
1. **Happy Path**: nominal user interaction with valid inputs producing the expected UI state/response.
|
|
11254
|
+
2. **Sad Path**: error handling, invalid interactions, validation rejections, disabled/blocked states, and failure recovery.
|
|
11255
|
+
3. **Boundary Values**: minimum/maximum lengths, numeric extremes, empty/whitespace inputs, edge selections, and rapid/repeated interactions.
|
|
11256
|
+
- For each \`FR-*\` requirement, generate the three mandatory test categories tagged with \`@nominal\`, \`@boundary\`, and \`@invalid\`:
|
|
11257
|
+
1. **Nominal Case** (\`@nominal\` \`Scenario\`): happy path with valid inputs within expected boundaries.
|
|
11258
|
+
2. **Boundary Cases** (\`@boundary\` \`Scenario Outline\` + \`Examples\` table): values at the extremes of the accepted range.
|
|
11259
|
+
3. **Out-of-Range / Invalid Cases** (\`@invalid\` \`Scenario Outline\` + \`Examples\` table): null, empty, wrong types, values exceeding bounds.
|
|
11260
|
+
- **Use Case Variant Coverage**: Inspect every \`UC-*\` in \`handoff.yaml\` and generate BDD scenarios covering 100% of its \`alternative-flows\` and \`exception-flows\`.
|
|
10797
11261
|
- Apply conditional categories when applicable: Security (\`@security @mitigation\`), Performance (\`@performance\`), Idempotency (\`@idempotence\`), Postconditions, and Interface Contracts.
|
|
10798
11262
|
- **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\`).
|
|
10799
11263
|
- Tag all scenarios with \`@<FR-ID> @automated @regression\`.
|
|
@@ -10810,9 +11274,21 @@ var SPECIALIZED_AGENT_TEMPLATES = [
|
|
|
10810
11274
|
- **Role**: High-Precision Software Developer / Coder Agent (\`agent-developer\`).
|
|
10811
11275
|
- **Mission**: Implement atomic tasks from SDD specifications (\`tasks.md\`) delivering clean, strictly typed, and tested code.
|
|
10812
11276
|
|
|
11277
|
+
## Mandatory 4-Step Implementation Order
|
|
11278
|
+
Execute every implementation task following this strict sequential order:
|
|
11279
|
+
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).
|
|
11280
|
+
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.
|
|
11281
|
+
3. **Component Implementation**: Implement the concrete components, use cases, and adapters that implement and consume the defined interfaces.
|
|
11282
|
+
4. **Exhaustive Per-Function Unit Testing**: Write unit tests for every function in \`tests/unit/\`, always verifying:
|
|
11283
|
+
- **Happy Path**: nominal execution with valid inputs.
|
|
11284
|
+
- **Sad Path**: controlled error paths, rejections, and business exceptions.
|
|
11285
|
+
- **Boundary Values**: minimum, maximum, exact limits, and off-by-one edges.
|
|
11286
|
+
- **Unexpected, Null & Empty Values**: \`null\`, \`undefined\`, empty strings/arrays/objects, and malformed inputs.
|
|
11287
|
+
|
|
10813
11288
|
## Operational Directives & Guardrails
|
|
10814
11289
|
- Respect the autonomy mode assigned to each task in \`tasks.md\` (\`AUTONOMOUS\`, \`HUMAN_REVIEW_PLAN\`, \`AMBIGUOUS\`, \`HIGH_RISK_MANUAL\`).
|
|
10815
|
-
-
|
|
11290
|
+
- **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.
|
|
11291
|
+
- 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/\`.
|
|
10816
11292
|
- Cite implemented \`FR-*\` or \`SEC-REQ-*\` IDs in test header comments to maintain 360\xB0 reverse traceability.
|
|
10817
11293
|
- **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.
|
|
10818
11294
|
- **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).
|
|
@@ -11006,11 +11482,14 @@ This file defines the operational rules and specialized role mappings for Google
|
|
|
11006
11482
|
- Emit a return handoff to \`agent-threat-modeler\` in the technical security feedback loop when infrastructure or persistence decisions introduce new attack vectors.
|
|
11007
11483
|
|
|
11008
11484
|
### 4. \`agent-qa-engineer\` (QA Engineer and SDET)
|
|
11009
|
-
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin test suites, before \`agent-developer\` writes production code.
|
|
11485
|
+
- **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.
|
|
11010
11486
|
- **Input**: Approved requirements from the \`handoff.yaml\` sidecar (\`HOF-*\`).
|
|
11011
|
-
- **Output**: Failing Gherkin
|
|
11487
|
+
- **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).
|
|
11012
11488
|
- **Guardrails**:
|
|
11013
11489
|
- FORBIDDEN to include production code or anticipate implementations.
|
|
11490
|
+
- **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.
|
|
11491
|
+
- **Mandatory E2E & UI Testing**: Never forget or omit End-to-End (E2E) and User Interface (UI) tests.
|
|
11492
|
+
- **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.
|
|
11014
11493
|
- Every \`FR-*\` must have at least nominal + boundary + out-of-range scenarios; missing these blocks handoff to \`agent-developer\`.
|
|
11015
11494
|
- Mandatory tags: \`@<FR-ID> @automated @regression\`.
|
|
11016
11495
|
- Run \`pnpm run verify:testing\` before emitting handoff.
|
|
@@ -11018,6 +11497,7 @@ This file defines the operational rules and specialized role mappings for Google
|
|
|
11018
11497
|
### 5. \`agent-developer\` (Software Developer)
|
|
11019
11498
|
- **Mission**: Implement atomic tasks from SDD specifications (\`tasks.md\`) with clean, strictly typed code and thorough tests.
|
|
11020
11499
|
- **Directives**:
|
|
11500
|
+
- **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.
|
|
11021
11501
|
- Respect the autonomy mode assigned in \`tasks.md\` (\`AUTONOMOUS\`, \`HUMAN_REVIEW_PLAN\`, \`AMBIGUOUS\`, \`HIGH_RISK_MANUAL\`).
|
|
11022
11502
|
- Make tests delivered by \`agent-qa-engineer\` pass green without altering them to accommodate code.
|
|
11023
11503
|
- Respect \`quality-policy.yaml\` thresholds: CC $\\le 10$, Cognitive $\\le 15$, MI $\\ge 50$, LOC $\\le 40$.
|
|
@@ -11116,7 +11596,7 @@ Before finalizing changes or proposing commits:
|
|
|
11116
11596
|
| \`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\`. |
|
|
11117
11597
|
| \`agent-threat-modeler\` | STRIDE/OWASP threat triad (\`ACT-THREAT-*\`, \`ABUSE-*\`, \`SEC-REQ-*\`). |
|
|
11118
11598
|
| \`agent-system-architect\` | Modular arc42 + NAF v4 architecture (\`CMP-*\`, Mermaid diagrams, \`ADR-*\`). |
|
|
11119
|
-
| \`agent-qa-engineer\` | FAILING BDD/Gherkin suites before \`agent-developer\` writes code. |
|
|
11599
|
+
| \`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. |
|
|
11120
11600
|
| \`agent-developer\` | Atomic tasks from \`tasks.md\`; respects autonomy mode; runs \`pnpm run check:fix\` + \`pnpm run verify:all\`. |
|
|
11121
11601
|
| \`agent-security-auditor\` | Adversarial PR security audit, SAST, secrets, and CVSS v3.1 evaluation. |
|
|
11122
11602
|
| \`agent-compliance-checker\` | SPDX license compliance audit against \`license-policy.yaml\`. |
|
|
@@ -11183,7 +11663,8 @@ Canonical Reference: [\`process/09_agent_protocols.md\`](process/09_agent_protoc
|
|
|
11183
11663
|
- **Function Length**: $\\le 40$ lines (warning if $> 30$).
|
|
11184
11664
|
- If a function exceeds these limits, decompose logic into cohesive helper functions before completing the task.
|
|
11185
11665
|
|
|
11186
|
-
## 2. TDD Methodology
|
|
11666
|
+
## 2. TDD Methodology, Clean Architecture & Unit Testing
|
|
11667
|
+
- **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.
|
|
11187
11668
|
- Apply Test-Driven Development: generate unit tests before or alongside component logic.
|
|
11188
11669
|
- Tag tests linked to requirements with header comments citing the ID (\`FR-*\` or \`SEC-REQ-*\`) to enable reverse traceability.
|
|
11189
11670
|
- Suppressing types (\`any\`), linter rules (\`eslint-disable\`), or unit tests to force pipeline passage is prohibited.
|
|
@@ -11256,12 +11737,12 @@ npx tsx packages/cli/src/index.ts sdd integrate --change <chg-id>
|
|
|
11256
11737
|
- **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.
|
|
11257
11738
|
|
|
11258
11739
|
### 4. \`agent-qa-engineer\` (QA Engineer and SDET)
|
|
11259
|
-
- **Mission**: Translate approved requirements (\`FR-*\`, \`SEC-REQ-*\`, \`QR-*\`) into comprehensive FAILING (red) BDD/Gherkin test suites, before \`agent-developer\` writes production code.
|
|
11260
|
-
- **Guardrails**: FORBIDDEN to include production code. Mandatory tags: \`@<FR-ID> @automated @regression\`.
|
|
11740
|
+
- **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.
|
|
11741
|
+
- **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\`.
|
|
11261
11742
|
|
|
11262
11743
|
### 5. \`agent-developer\` (Software Developer)
|
|
11263
11744
|
- **Mission**: Implement atomic tasks from SDD specifications (\`tasks.md\`) with clean, strictly typed code and thorough tests.
|
|
11264
|
-
- **Directives**: Respect the autonomy mode. Make tests delivered by \`agent-qa-engineer\` pass green. Run \`pnpm run check:fix\` and \`pnpm run verify:all\`.
|
|
11745
|
+
- **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\`.
|
|
11265
11746
|
|
|
11266
11747
|
### 6. \`agent-security-auditor\` (Adversarial Code Auditor)
|
|
11267
11748
|
- **Mission**: Examine Pull Requests for business logic vulnerabilities, injection vectors, secret leaks, and authorization flaws as part of the Pre-Merge Audit Triad.
|
|
@@ -11406,7 +11887,7 @@ Non-Negotiable Quality Thresholds (\`quality-policy.yaml\`):
|
|
|
11406
11887
|
| \`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\`. |
|
|
11407
11888
|
| \`agent-threat-modeler\` | STRIDE/OWASP threat triad (\`ACT-THREAT-*\`, \`ABUSE-*\`, \`SEC-REQ-*\`). |
|
|
11408
11889
|
| \`agent-system-architect\` | Modular arc42 + NAF v4 architecture (\`CMP-*\`, Mermaid diagrams, \`ADR-*\`). |
|
|
11409
|
-
| \`agent-qa-engineer\` | FAILING BDD/Gherkin suites before \`agent-developer\` writes production code. |
|
|
11890
|
+
| \`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. |
|
|
11410
11891
|
| \`agent-developer\` | Atomic tasks from \`tasks.md\`; autonomy mode must be respected; runs \`pnpm run check:fix\` + \`pnpm run verify:all\`. |
|
|
11411
11892
|
| \`agent-security-auditor\` | Adversarial PR security audit, SAST, secrets, and CVSS v3.1 evaluation. |
|
|
11412
11893
|
| \`agent-compliance-checker\` | SPDX license compliance audit against \`license-policy.yaml\`. |
|
|
@@ -12832,14 +13313,14 @@ function collectAgentFiles(targets) {
|
|
|
12832
13313
|
function writeProjectFiles(destDir, files, dryRun) {
|
|
12833
13314
|
const filesCreated = [];
|
|
12834
13315
|
for (const f of files) {
|
|
12835
|
-
const fullFilePath =
|
|
12836
|
-
const parentDir =
|
|
13316
|
+
const fullFilePath = path4__namespace.join(destDir, f.relPath);
|
|
13317
|
+
const parentDir = path4__namespace.dirname(fullFilePath);
|
|
12837
13318
|
if (!dryRun) {
|
|
12838
|
-
if (!
|
|
12839
|
-
|
|
13319
|
+
if (!fs7__namespace.existsSync(parentDir)) {
|
|
13320
|
+
fs7__namespace.mkdirSync(parentDir, { recursive: true });
|
|
12840
13321
|
}
|
|
12841
|
-
if (!
|
|
12842
|
-
|
|
13322
|
+
if (!fs7__namespace.existsSync(fullFilePath)) {
|
|
13323
|
+
fs7__namespace.writeFileSync(fullFilePath, f.content, "utf-8");
|
|
12843
13324
|
filesCreated.push(f.relPath);
|
|
12844
13325
|
}
|
|
12845
13326
|
} else {
|
|
@@ -12912,7 +13393,7 @@ function getCiWorkflowFiles(ciProvider) {
|
|
|
12912
13393
|
function createDirectoriesOnDisk(destDir, directories, dryRun) {
|
|
12913
13394
|
const created = [];
|
|
12914
13395
|
for (const d of directories) {
|
|
12915
|
-
if (!dryRun)
|
|
13396
|
+
if (!dryRun) fs7__namespace.mkdirSync(path4__namespace.join(destDir, d), { recursive: true });
|
|
12916
13397
|
created.push(d);
|
|
12917
13398
|
}
|
|
12918
13399
|
return created;
|
|
@@ -12930,13 +13411,13 @@ function collectStarterFiles(ciProvider, agentFiles = [], archGranularity = "min
|
|
|
12930
13411
|
...getCiWorkflowFiles(ciProvider),
|
|
12931
13412
|
...agentFiles,
|
|
12932
13413
|
...getArchitectureTemplates(archGranularity).map((t) => ({
|
|
12933
|
-
relPath:
|
|
13414
|
+
relPath: path4__namespace.join("templates", "architecture", t.filename).replace(/\\/g, "/"),
|
|
12934
13415
|
content: t.content
|
|
12935
13416
|
}))
|
|
12936
13417
|
];
|
|
12937
13418
|
}
|
|
12938
13419
|
function tryInstallGitHook(destDir, dryRun) {
|
|
12939
|
-
if (!dryRun &&
|
|
13420
|
+
if (!dryRun && fs7__namespace.existsSync(path4__namespace.join(destDir, ".git"))) {
|
|
12940
13421
|
return installGitHooks(destDir).success;
|
|
12941
13422
|
}
|
|
12942
13423
|
return false;
|
|
@@ -12978,7 +13459,7 @@ function buildInitSuccessResult(destDir, directoriesCreated, filesCreated, ciPro
|
|
|
12978
13459
|
}
|
|
12979
13460
|
function initProject(options = {}) {
|
|
12980
13461
|
const baseDir = options.rootDir || process.cwd();
|
|
12981
|
-
const destDir =
|
|
13462
|
+
const destDir = path4__namespace.resolve(baseDir, options.targetDir || ".");
|
|
12982
13463
|
const ciRes = validateCiProvider(options.ci);
|
|
12983
13464
|
if (!ciRes.valid) return buildInitError(destDir, ciRes.error);
|
|
12984
13465
|
const archRes = validateArchitectureGranularity(options.architecture);
|
|
@@ -13058,6 +13539,7 @@ exports.VALID_CI_PROVIDERS = VALID_CI_PROVIDERS;
|
|
|
13058
13539
|
exports.aggregateCommitKpiRecords = aggregateCommitKpiRecords;
|
|
13059
13540
|
exports.aggregatePrKpis = aggregatePrKpis;
|
|
13060
13541
|
exports.aggregateReleaseKpis = aggregateReleaseKpis;
|
|
13542
|
+
exports.analyzeBddVariantsForRequirement = analyzeBddVariantsForRequirement;
|
|
13061
13543
|
exports.appendRevisionHistoryRow = appendRevisionHistoryRow;
|
|
13062
13544
|
exports.buildCycloneDxComponent = buildCycloneDxComponent;
|
|
13063
13545
|
exports.buildFeatureFileContent = buildFeatureFileContent;
|