@aisdlc/core 2.3.0 → 2.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 +1635 -943
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -39
- package/dist/index.d.ts +165 -39
- package/dist/index.js +1610 -941
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
1
|
+
import * as crypto3 from 'crypto';
|
|
2
|
+
import * as fs8 from 'fs';
|
|
3
|
+
import * as path5 from 'path';
|
|
4
4
|
import yaml3 from 'js-yaml';
|
|
5
|
-
import { SyntaxKind, Project, Node } from 'ts-morph';
|
|
6
5
|
import * as child_process from 'child_process';
|
|
7
|
-
import { execFileSync } from 'child_process';
|
|
6
|
+
import { execFileSync, spawnSync } from 'child_process';
|
|
7
|
+
import { SyntaxKind, Project, Node } from 'ts-morph';
|
|
8
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
9
|
+
import addFormats from 'ajv-formats';
|
|
8
10
|
import { createRequire } from 'module';
|
|
9
11
|
|
|
10
12
|
// src/types/index.ts
|
|
@@ -15,6 +17,168 @@ var VALID_AUTONOMY_MODES = [
|
|
|
15
17
|
"AMBIGUOUS",
|
|
16
18
|
"HIGH_RISK_MANUAL"
|
|
17
19
|
];
|
|
20
|
+
function canonicalSort(a, b) {
|
|
21
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
function normalizePath(p) {
|
|
24
|
+
return p.normalize("NFC").replace(/\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
function walkMdFiles(dir, fileList = [], excludeDirs = [
|
|
27
|
+
"node_modules",
|
|
28
|
+
".git",
|
|
29
|
+
"dist",
|
|
30
|
+
"bin",
|
|
31
|
+
"obj",
|
|
32
|
+
"build",
|
|
33
|
+
"out",
|
|
34
|
+
"target",
|
|
35
|
+
"coverage",
|
|
36
|
+
".changeset",
|
|
37
|
+
"scratch",
|
|
38
|
+
"test-scaffold",
|
|
39
|
+
"fixtures",
|
|
40
|
+
"templates",
|
|
41
|
+
"examples",
|
|
42
|
+
".agent",
|
|
43
|
+
".agents",
|
|
44
|
+
".claude",
|
|
45
|
+
".cursor",
|
|
46
|
+
".github",
|
|
47
|
+
".vscode",
|
|
48
|
+
".vs",
|
|
49
|
+
".idea"
|
|
50
|
+
]) {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs8.existsSync(dir)) return fileList;
|
|
53
|
+
const files = fs8.readdirSync(dir).sort(canonicalSort);
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const fullPath = path5.join(dir, file);
|
|
56
|
+
try {
|
|
57
|
+
if (fs8.statSync(fullPath).isDirectory()) {
|
|
58
|
+
if (!excludeDirs.includes(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
59
|
+
walkMdFiles(fullPath, fileList, excludeDirs);
|
|
60
|
+
}
|
|
61
|
+
} else if (file.endsWith(".md")) {
|
|
62
|
+
fileList.push(fullPath);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
return fileList.sort(canonicalSort);
|
|
70
|
+
}
|
|
71
|
+
function stripVolatileReportMetadata(content, _ext = "") {
|
|
72
|
+
let normalized = content.replace(/\r\n/g, "\n");
|
|
73
|
+
normalized = normalized.replace(/(?:^|\n)>\s*\*\*(?:Generation Date|Generated at|Fecha|Audit Date|Date):\*\*.*$/gim, "");
|
|
74
|
+
normalized = normalized.replace(/Generated:\s*<span>[\s\S]*?<\/span>/gi, "Generated: <span>[STRIPPED]</span>");
|
|
75
|
+
return normalized.trim();
|
|
76
|
+
}
|
|
77
|
+
function areCycloneDxSbomsEqual(existingContent, newContent) {
|
|
78
|
+
try {
|
|
79
|
+
const a = JSON.parse(existingContent);
|
|
80
|
+
const b = JSON.parse(newContent);
|
|
81
|
+
return a.serialNumber === b.serialNumber && JSON.stringify(a.components) === JSON.stringify(b.components) && JSON.stringify(a.vulnerabilities || []) === JSON.stringify(b.vulnerabilities || []);
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function areSarifReportsEqual(existingContent, newContent) {
|
|
87
|
+
try {
|
|
88
|
+
const a = JSON.parse(existingContent);
|
|
89
|
+
const b = JSON.parse(newContent);
|
|
90
|
+
return JSON.stringify(a.runs?.[0]?.results || []) === JSON.stringify(b.runs?.[0]?.results || []);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function isReportContentSemanticallyEqual(existingContent, newContent, filePath) {
|
|
96
|
+
if (existingContent === newContent) return true;
|
|
97
|
+
if (filePath.endsWith(".cdx.json") || filePath.endsWith("sbom.json")) {
|
|
98
|
+
if (areCycloneDxSbomsEqual(existingContent, newContent)) return true;
|
|
99
|
+
}
|
|
100
|
+
if (filePath.endsWith(".sarif")) {
|
|
101
|
+
if (areSarifReportsEqual(existingContent, newContent)) return true;
|
|
102
|
+
}
|
|
103
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
104
|
+
return stripVolatileReportMetadata(existingContent, ext) === stripVolatileReportMetadata(newContent, ext);
|
|
105
|
+
}
|
|
106
|
+
function atomicWriteFile(filePath, content) {
|
|
107
|
+
const tempPath = `${filePath}.${process.pid}.${Math.random().toString(36).substring(2)}.tmp`;
|
|
108
|
+
try {
|
|
109
|
+
fs8.writeFileSync(tempPath, content, "utf-8");
|
|
110
|
+
try {
|
|
111
|
+
if (fs8.existsSync(filePath)) {
|
|
112
|
+
fs8.rmSync(filePath, { force: true });
|
|
113
|
+
}
|
|
114
|
+
fs8.renameSync(tempPath, filePath);
|
|
115
|
+
} catch {
|
|
116
|
+
fs8.copyFileSync(tempPath, filePath);
|
|
117
|
+
fs8.unlinkSync(tempPath);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
fs8.writeFileSync(filePath, content, "utf-8");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function writeReportFileIfChanged(filePath, content) {
|
|
124
|
+
try {
|
|
125
|
+
if (fs8.existsSync(filePath)) {
|
|
126
|
+
const existing = fs8.readFileSync(filePath, "utf-8");
|
|
127
|
+
if (isReportContentSemanticallyEqual(existing, content, filePath)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
const dir = path5.dirname(filePath);
|
|
134
|
+
if (!fs8.existsSync(dir)) {
|
|
135
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
atomicWriteFile(filePath, content);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
var UUID_V5_NAMESPACE_OID = "6ba7b812-9dad-11d1-80b4-00c04fd430c8";
|
|
141
|
+
function resolveDeterministicDate(rootDir) {
|
|
142
|
+
const epoch = process.env.SOURCE_DATE_EPOCH;
|
|
143
|
+
if (epoch) {
|
|
144
|
+
const parsed = parseInt(epoch, 10);
|
|
145
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
146
|
+
return new Date(parsed * 1e3);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const targetDir = rootDir || process.cwd();
|
|
150
|
+
if (fs8.existsSync(path5.join(targetDir, ".git"))) {
|
|
151
|
+
try {
|
|
152
|
+
const gitOut = execFileSync("git", ["log", "-1", "--format=%ct"], {
|
|
153
|
+
cwd: targetDir,
|
|
154
|
+
encoding: "utf-8",
|
|
155
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
156
|
+
}).trim();
|
|
157
|
+
const parsed = parseInt(gitOut, 10);
|
|
158
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
159
|
+
return new Date(parsed * 1e3);
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return /* @__PURE__ */ new Date();
|
|
165
|
+
}
|
|
166
|
+
function generateDeterministicUuidV5(name, namespace = UUID_V5_NAMESPACE_OID) {
|
|
167
|
+
const nsHex = namespace.replace(/-/g, "");
|
|
168
|
+
const nsBytes = Buffer.from(nsHex, "hex");
|
|
169
|
+
const nameBytes = Buffer.from(name, "utf-8");
|
|
170
|
+
const hash = crypto3.createHash("sha1").update(nsBytes).update(nameBytes).digest();
|
|
171
|
+
hash[6] = hash[6] & 15 | 80;
|
|
172
|
+
hash[8] = hash[8] & 63 | 128;
|
|
173
|
+
const hex = hash.toString("hex").substring(0, 32);
|
|
174
|
+
return [
|
|
175
|
+
hex.substring(0, 8),
|
|
176
|
+
hex.substring(8, 12),
|
|
177
|
+
hex.substring(12, 16),
|
|
178
|
+
hex.substring(16, 20),
|
|
179
|
+
hex.substring(20, 32)
|
|
180
|
+
].join("-");
|
|
181
|
+
}
|
|
18
182
|
var CYCLOMATIC_KINDS = /* @__PURE__ */ new Set([
|
|
19
183
|
SyntaxKind.IfStatement,
|
|
20
184
|
SyntaxKind.ConditionalExpression,
|
|
@@ -171,7 +335,7 @@ function buildNodeMetrics(node, filePath) {
|
|
|
171
335
|
return { functionName, filePath, loc, cyclomatic, cognitive, maintainability: mi, codeSmells, line };
|
|
172
336
|
}
|
|
173
337
|
function createTempSourceFile(project, content, filePath) {
|
|
174
|
-
const ext =
|
|
338
|
+
const ext = path5.extname(filePath).toLowerCase() || ".ts";
|
|
175
339
|
const fileName = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}${ext}`;
|
|
176
340
|
return project.createSourceFile(fileName, content, { overwrite: true });
|
|
177
341
|
}
|
|
@@ -208,8 +372,8 @@ function extractFunctionsTypeScriptAst(content, filePath) {
|
|
|
208
372
|
}
|
|
209
373
|
}
|
|
210
374
|
function extractFunctionsPolyglot(content, filePath) {
|
|
211
|
-
const ext =
|
|
212
|
-
return ext === ".py" ?
|
|
375
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
376
|
+
return ext === ".py" ? extractPythonFunctionsPolyglot(content, filePath) : extractBraceFunctions(content, filePath);
|
|
213
377
|
}
|
|
214
378
|
function isDocstringToggle(trimmed) {
|
|
215
379
|
const dCount = (trimmed.match(/"""/g) || []).length;
|
|
@@ -254,7 +418,7 @@ function processPythonLine(raw, state, blocks, lineIndex) {
|
|
|
254
418
|
handlePythonBodyLine(raw, state, blocks, lineIndex);
|
|
255
419
|
}
|
|
256
420
|
}
|
|
257
|
-
function
|
|
421
|
+
function extractPythonFunctionsPolyglot(content, filePath) {
|
|
258
422
|
const lines = content.split("\n");
|
|
259
423
|
const blocks = [];
|
|
260
424
|
const state = { currentFn: null, fnLines: [], baseIndent: 0, inDocstring: false };
|
|
@@ -266,120 +430,182 @@ function extractPythonFunctions(content, filePath) {
|
|
|
266
430
|
}
|
|
267
431
|
return buildMetricsFromBlocks(blocks, content, filePath);
|
|
268
432
|
}
|
|
269
|
-
function
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
433
|
+
function tryStartBraceFunction(line, state, ext) {
|
|
434
|
+
if (state.currentFn || state.inBlockComment || state.rawTerm) return;
|
|
435
|
+
const candidate = matchFunctionHeader(line, ext);
|
|
436
|
+
if (candidate) {
|
|
273
437
|
state.currentFn = candidate;
|
|
274
438
|
state.braceCount = 0;
|
|
275
439
|
state.fnLines = [];
|
|
276
440
|
}
|
|
277
|
-
|
|
278
|
-
|
|
441
|
+
}
|
|
442
|
+
function updateBraceScanState(line, state, ext) {
|
|
443
|
+
const res = scanBracesInLine(line, state.inBlockComment, state.rawTerm, ext);
|
|
279
444
|
state.inBlockComment = res.nowInBlock;
|
|
445
|
+
state.rawTerm = res.nowRawTerm;
|
|
280
446
|
state.braceCount += res.delta;
|
|
281
|
-
|
|
447
|
+
}
|
|
448
|
+
function processBraceLine(line, state, blocks, lineIndex, ext) {
|
|
449
|
+
tryStartBraceFunction(line, state, ext);
|
|
450
|
+
if (state.currentFn) state.fnLines.push(line);
|
|
451
|
+
updateBraceScanState(line, state, ext);
|
|
452
|
+
if (state.currentFn && state.braceCount === 0 && state.fnLines.length > 1) {
|
|
282
453
|
blocks.push({ name: state.currentFn, body: state.fnLines.join("\n"), startLine: lineIndex });
|
|
283
454
|
state.currentFn = null;
|
|
284
455
|
state.fnLines = [];
|
|
285
456
|
}
|
|
286
457
|
}
|
|
287
458
|
function extractBraceFunctions(content, filePath) {
|
|
288
|
-
const ext =
|
|
459
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
289
460
|
const lines = content.split("\n");
|
|
290
461
|
const blocks = [];
|
|
291
|
-
const state = {
|
|
462
|
+
const state = {
|
|
463
|
+
currentFn: null,
|
|
464
|
+
braceCount: 0,
|
|
465
|
+
fnLines: [],
|
|
466
|
+
inBlockComment: false,
|
|
467
|
+
rawTerm: null
|
|
468
|
+
};
|
|
292
469
|
for (let i = 0; i < lines.length; i++) {
|
|
293
470
|
processBraceLine(lines[i], state, blocks, i, ext);
|
|
294
471
|
}
|
|
295
472
|
return buildMetricsFromBlocks(blocks, content, filePath);
|
|
296
473
|
}
|
|
474
|
+
var HEADER_EXCLUSIONS = /* @__PURE__ */ new Set([
|
|
475
|
+
"if",
|
|
476
|
+
"for",
|
|
477
|
+
"foreach",
|
|
478
|
+
"while",
|
|
479
|
+
"switch",
|
|
480
|
+
"catch",
|
|
481
|
+
"select",
|
|
482
|
+
"match",
|
|
483
|
+
"class",
|
|
484
|
+
"struct",
|
|
485
|
+
"interface",
|
|
486
|
+
"record",
|
|
487
|
+
"enum",
|
|
488
|
+
"namespace",
|
|
489
|
+
"using",
|
|
490
|
+
"lock",
|
|
491
|
+
"fixed",
|
|
492
|
+
"synchronized",
|
|
493
|
+
"try",
|
|
494
|
+
"else"
|
|
495
|
+
]);
|
|
297
496
|
function matchCStyleHeader(line) {
|
|
298
497
|
const match = line.match(
|
|
299
|
-
/(?:(?:public|private|protected|static|async|virtual|override|fn)\s+)*(?:function\s+([a-zA-Z0-9_$]+)|(?:[a-zA-Z0-9_<>[\]
|
|
498
|
+
/(?:(?:public|private|protected|internal|static|async|virtual|override|sealed|abstract|final|synchronized|default|fn)\s+)*(?:function\s+([a-zA-Z0-9_$]+)|(?:[a-zA-Z0-9_<>[\]?,.\s]+\s+)+([a-zA-Z0-9_$]+)\s*(?:<[^>]+>)?\s*\([^;{}]*\)\s*(?:where\s+[^{]+|throws\s+[^{]+)?\{|([a-zA-Z0-9_$]+)\s*\([^;{}]*\)\s*(?::\s*[^{]+\s*)?\{)/
|
|
300
499
|
);
|
|
301
500
|
if (!match) return null;
|
|
302
501
|
const candidate = match[1] || match[2] || match[3];
|
|
303
|
-
|
|
304
|
-
return candidate && !exclusions.includes(candidate) ? candidate : null;
|
|
502
|
+
return candidate && !HEADER_EXCLUSIONS.has(candidate) ? candidate : null;
|
|
305
503
|
}
|
|
306
504
|
function matchFunctionHeader(line, ext) {
|
|
307
505
|
const trimmed = line.trim();
|
|
308
|
-
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))
|
|
506
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#")) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
309
509
|
if (ext === ".go") {
|
|
310
|
-
const match = line.match(/func\s+(?:\([^)]*\)\s*)?([a-zA-Z0-9_$]+)\s*\(/);
|
|
510
|
+
const match = line.match(/func\s+(?:\([^)]*\)\s*)?([a-zA-Z0-9_$]+)\s*(?:\[[^\]]*\])?\s*\(/);
|
|
311
511
|
return match ? match[1] : null;
|
|
312
512
|
}
|
|
313
513
|
if (ext === ".rs") {
|
|
314
|
-
const match = line.match(/(?:pub
|
|
514
|
+
const match = line.match(/(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+([a-zA-Z0-9_$]+)\s*(?:<[^>]*>)?\s*\(/);
|
|
315
515
|
return match ? match[1] : null;
|
|
316
516
|
}
|
|
317
517
|
return matchCStyleHeader(line);
|
|
318
518
|
}
|
|
319
|
-
function
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
519
|
+
function detectRustOrGoRawString(sub, ext) {
|
|
520
|
+
if (ext === ".rs") {
|
|
521
|
+
const m = sub.match(/^r(#*)"/);
|
|
522
|
+
return m ? { term: `"${m[1]}`, advance: m[0].length - 1 } : null;
|
|
523
|
+
}
|
|
524
|
+
if (ext === ".go" && sub.startsWith("`")) {
|
|
525
|
+
return { term: "`", advance: 0 };
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
function detectCsJavaOrCppRawString(sub, ext) {
|
|
530
|
+
if (ext === ".cs" || ext === ".java") {
|
|
531
|
+
const m = sub.match(/^\$*("{3,})/);
|
|
532
|
+
if (m) return { term: m[1], advance: m[0].length - 1 };
|
|
533
|
+
const v = ext === ".cs" ? sub.match(/^(@\$|\$@|@)"/) : null;
|
|
534
|
+
return v ? { term: '"', advance: v[0].length - 1 } : null;
|
|
535
|
+
}
|
|
536
|
+
const cpp = sub.match(/^R"([a-zA-Z0-9_]*)\(/);
|
|
537
|
+
return cpp ? { term: `)${cpp[1]}"`, advance: cpp[0].length - 1 } : null;
|
|
538
|
+
}
|
|
539
|
+
function detectRawStringOpen(line, index, ext) {
|
|
540
|
+
const sub = line.slice(index);
|
|
541
|
+
return detectRustOrGoRawString(sub, ext) || detectCsJavaOrCppRawString(sub, ext);
|
|
542
|
+
}
|
|
543
|
+
function consumeRawOrBlock(line, index, state) {
|
|
544
|
+
if (state.inBlock) {
|
|
545
|
+
if (line[index] === "*" && line[index + 1] === "/") {
|
|
546
|
+
state.inBlock = false;
|
|
547
|
+
return { advance: 1, handled: true };
|
|
548
|
+
}
|
|
549
|
+
return { advance: 0, handled: true };
|
|
550
|
+
}
|
|
551
|
+
if (state.rawTerm) {
|
|
552
|
+
if (line.startsWith(state.rawTerm, index)) {
|
|
553
|
+
const len = state.rawTerm.length - 1;
|
|
554
|
+
state.rawTerm = null;
|
|
555
|
+
return { advance: len, handled: true };
|
|
556
|
+
}
|
|
557
|
+
return { advance: 0, handled: true };
|
|
558
|
+
}
|
|
559
|
+
return { advance: 0, handled: false };
|
|
560
|
+
}
|
|
561
|
+
function consumeNormalQuote(line, index, state) {
|
|
562
|
+
if (!state.inString) return { advance: 0, handled: false };
|
|
563
|
+
const ch = line[index];
|
|
564
|
+
if (ch === "\\") return { advance: 1, handled: true };
|
|
565
|
+
if (ch === state.inString) state.inString = null;
|
|
566
|
+
return { advance: 0, handled: true };
|
|
567
|
+
}
|
|
568
|
+
function applyQuoteOrBrace(ch, state) {
|
|
569
|
+
if (ch === '"' || ch === "'") {
|
|
570
|
+
state.inString = ch;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (ch === "{") state.delta++;
|
|
574
|
+
if (ch === "}") state.delta--;
|
|
575
|
+
}
|
|
576
|
+
function stepActiveCodeToken(line, j, ext, state) {
|
|
577
|
+
const pair = line.slice(j, j + 2);
|
|
578
|
+
if (pair === "//") return { advance: 0, stop: true };
|
|
579
|
+
if (pair === "/*") {
|
|
355
580
|
state.inBlock = true;
|
|
356
581
|
return { advance: 1, stop: false };
|
|
357
582
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
583
|
+
const rawOpen = detectRawStringOpen(line, j, ext);
|
|
584
|
+
if (rawOpen) {
|
|
585
|
+
state.rawTerm = rawOpen.term;
|
|
586
|
+
return { advance: rawOpen.advance, stop: false };
|
|
361
587
|
}
|
|
362
|
-
state
|
|
588
|
+
applyQuoteOrBrace(line[j], state);
|
|
363
589
|
return { advance: 0, stop: false };
|
|
364
590
|
}
|
|
365
|
-
function scanBracesInLine(line, wasInBlock, ext) {
|
|
366
|
-
const state = { delta: 0, inBlock: wasInBlock, inString: null };
|
|
591
|
+
function scanBracesInLine(line, wasInBlock, wasRawTerm, ext) {
|
|
592
|
+
const state = { delta: 0, inBlock: wasInBlock, rawTerm: wasRawTerm, inString: null };
|
|
367
593
|
for (let j = 0; j < line.length; j++) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (skipped.handled) {
|
|
372
|
-
state.inBlock = skipped.inBlock;
|
|
373
|
-
state.inString = skipped.inString;
|
|
374
|
-
if (skipped.skip) j++;
|
|
594
|
+
const rawOrBlock = consumeRawOrBlock(line, j, state);
|
|
595
|
+
if (rawOrBlock.handled) {
|
|
596
|
+
j += rawOrBlock.advance;
|
|
375
597
|
continue;
|
|
376
598
|
}
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
599
|
+
const quoteStep = consumeNormalQuote(line, j, state);
|
|
600
|
+
if (quoteStep.handled) {
|
|
601
|
+
j += quoteStep.advance;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const codeStep = stepActiveCodeToken(line, j, ext, state);
|
|
605
|
+
if (codeStep.stop) break;
|
|
606
|
+
j += codeStep.advance;
|
|
381
607
|
}
|
|
382
|
-
return { delta: state.delta, nowInBlock: state.inBlock };
|
|
608
|
+
return { delta: state.delta, nowInBlock: state.inBlock, nowRawTerm: state.rawTerm };
|
|
383
609
|
}
|
|
384
610
|
function buildMetricsFromBlocks(blocks, content, filePath) {
|
|
385
611
|
if (blocks.length === 0) {
|
|
@@ -403,11 +629,20 @@ function buildMetricsFromBlocks(blocks, content, filePath) {
|
|
|
403
629
|
}
|
|
404
630
|
function detectPolyglotSmells(body) {
|
|
405
631
|
const smells = [];
|
|
406
|
-
if (/#\s*noqa|#\s*type:\s*ignore|@SuppressWarnings/.test(body)) {
|
|
632
|
+
if (/#\s*noqa|#\s*type:\s*ignore|@SuppressWarnings|#pragma\s+warning\s+disable/.test(body)) {
|
|
407
633
|
smells.push("Supresi\xF3n no autorizada de linter");
|
|
408
634
|
}
|
|
409
635
|
return smells;
|
|
410
636
|
}
|
|
637
|
+
function stripStringsAndComments(body, ext) {
|
|
638
|
+
let cleaned = body;
|
|
639
|
+
if (ext === ".py") {
|
|
640
|
+
cleaned = cleaned.replace(/"""[\s\S]*?"""|'''[\s\S]*?'''/g, '""').replace(/#.*/g, "");
|
|
641
|
+
} else {
|
|
642
|
+
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "").replace(/r(#+)"[\s\S]*?"\1/g, '""').replace(/R"([a-zA-Z0-9_]*)\([\s\S]*?\)\1"/g, '""').replace(/\$*"{3,}[\s\S]*?"{3,}/g, '""').replace(/`[\s\S]*?`/g, '""');
|
|
643
|
+
}
|
|
644
|
+
return cleaned.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '""');
|
|
645
|
+
}
|
|
411
646
|
function computeHalsteadMI(body, cyclomatic, loc, commentCount) {
|
|
412
647
|
const cleanText = body.replace(/\/\*[\s\S]*?\*\/|\/\/.*|#.*/g, "");
|
|
413
648
|
const tokens = cleanText.split(/[\s,;().{}[\]=+\-*/<>!&|:]+/).filter((t) => t.length > 0);
|
|
@@ -430,11 +665,13 @@ function countCommentsAndLoc(rawLines, isPy) {
|
|
|
430
665
|
return { loc, commentCount, cleanLines };
|
|
431
666
|
}
|
|
432
667
|
function calculatePolyglotMetrics(body, name, filePath, startLine) {
|
|
433
|
-
const ext =
|
|
668
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
434
669
|
const rawLines = body.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
435
|
-
const { loc, commentCount
|
|
436
|
-
const
|
|
437
|
-
const
|
|
670
|
+
const { loc, commentCount } = countCommentsAndLoc(rawLines, ext === ".py");
|
|
671
|
+
const strippedBody = stripStringsAndComments(body, ext);
|
|
672
|
+
const strippedLines = strippedBody.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
673
|
+
const cyclomatic = computePolyglotCyclomatic(strippedBody, ext);
|
|
674
|
+
const cognitive = computePolyglotCognitive(strippedLines, ext === ".py");
|
|
438
675
|
const maintainability = computeHalsteadMI(body, cyclomatic, loc, commentCount);
|
|
439
676
|
return {
|
|
440
677
|
functionName: name,
|
|
@@ -447,84 +684,183 @@ function calculatePolyglotMetrics(body, name, filePath, startLine) {
|
|
|
447
684
|
line: startLine ? startLine + 1 : 1
|
|
448
685
|
};
|
|
449
686
|
}
|
|
450
|
-
function
|
|
451
|
-
let regex;
|
|
687
|
+
function getCyclomaticPattern(ext) {
|
|
452
688
|
if (ext === ".py") {
|
|
453
|
-
|
|
454
|
-
} else if (ext === ".go") {
|
|
455
|
-
regex = /\b(if|for|case|select)\b|&&|\|\|/g;
|
|
456
|
-
} else if (ext === ".rs") {
|
|
457
|
-
regex = /\b(if|for|while|match)\b|&&|\|\||\?/g;
|
|
458
|
-
} else {
|
|
459
|
-
regex = /\b(if|else\s+if|for|while|catch|case)\b|\?|&&|\|\|/g;
|
|
689
|
+
return /\b(if|elif|for|while|except|case)\b|\b(and|or)\b/g;
|
|
460
690
|
}
|
|
461
|
-
|
|
462
|
-
|
|
691
|
+
if (ext === ".go") {
|
|
692
|
+
return /\b(if|for|case|select)\b|&&|\|\|/g;
|
|
693
|
+
}
|
|
694
|
+
if (ext === ".rs") {
|
|
695
|
+
return /\b(if|for|while|match)\b|&&|\|\||\?/g;
|
|
696
|
+
}
|
|
697
|
+
if (ext === ".cs") {
|
|
698
|
+
return /\b(if|else\s+if|for|foreach|while|catch|case|when)\b|\?\?|\?\.(?=[a-zA-Z_])|&&|\|\|/g;
|
|
699
|
+
}
|
|
700
|
+
if (ext === ".java") {
|
|
701
|
+
return /\b(if|else\s+if|for|while|catch|case|when)\b|&&|\|\|/g;
|
|
702
|
+
}
|
|
703
|
+
return /\b(if|else\s+if|for|while|catch|case)\b|\?|&&|\|\|/g;
|
|
704
|
+
}
|
|
705
|
+
function computePolyglotCyclomatic(strippedBody, ext) {
|
|
706
|
+
const regex = getCyclomaticPattern(ext);
|
|
707
|
+
const matches = strippedBody.match(regex);
|
|
463
708
|
return 1 + (matches ? matches.length : 0);
|
|
464
709
|
}
|
|
465
|
-
|
|
710
|
+
var PY_COGNITIVE_RE = /\b(if|elif|for|while|except|match)\b/;
|
|
711
|
+
var BRACE_COGNITIVE_RE = /\b(if|for|foreach|while|catch|match|select|switch)\b/;
|
|
712
|
+
function computePythonCognitive(lines) {
|
|
713
|
+
let cognitive = 0;
|
|
714
|
+
const baseIndent = lines.length > 0 ? lines[0].length - lines[0].trimStart().length : 0;
|
|
715
|
+
for (let i = 1; i < lines.length; i++) {
|
|
716
|
+
const line = lines[i];
|
|
717
|
+
if (PY_COGNITIVE_RE.test(line)) {
|
|
718
|
+
const relIndent = Math.max(0, line.length - line.trimStart().length - baseIndent - 4);
|
|
719
|
+
cognitive += 1 + Math.floor(relIndent / 4);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return cognitive;
|
|
723
|
+
}
|
|
724
|
+
function computeBraceCognitive(lines) {
|
|
466
725
|
let cognitive = 0;
|
|
467
726
|
let nesting = 0;
|
|
468
|
-
for (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
cognitive += 1 + Math.floor(indent / 4);
|
|
473
|
-
}
|
|
474
|
-
} else {
|
|
475
|
-
if (line.includes("{")) nesting++;
|
|
476
|
-
if (line.includes("}")) nesting = Math.max(0, nesting - 1);
|
|
477
|
-
if (/\b(if|for|while|catch|match|select)\b/.test(line)) {
|
|
478
|
-
cognitive += 1 + nesting;
|
|
479
|
-
}
|
|
727
|
+
for (let i = 1; i < lines.length; i++) {
|
|
728
|
+
const line = lines[i];
|
|
729
|
+
if (BRACE_COGNITIVE_RE.test(line)) {
|
|
730
|
+
cognitive += 1 + nesting;
|
|
480
731
|
}
|
|
732
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
733
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
734
|
+
nesting = Math.max(0, nesting + opens - closes);
|
|
481
735
|
}
|
|
482
736
|
return cognitive;
|
|
483
737
|
}
|
|
484
|
-
function
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
738
|
+
function computePolyglotCognitive(lines, isPy) {
|
|
739
|
+
return isPy ? computePythonCognitive(lines) : computeBraceCognitive(lines);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/verifiers/ast/python-ast.ts
|
|
743
|
+
var PYTHON_AST_RUNNER_SCRIPT = `
|
|
744
|
+
import ast, json, sys
|
|
745
|
+
|
|
746
|
+
def analyze(code, filepath):
|
|
747
|
+
try:
|
|
748
|
+
tree = ast.parse(code)
|
|
749
|
+
except Exception as e:
|
|
750
|
+
return {"language": "python", "filePath": filepath, "functions": [], "error": str(e), "unsafeTypesCount": 0, "emptyStubsCount": 0}
|
|
751
|
+
|
|
752
|
+
functions = []
|
|
753
|
+
unsafe_types = 0
|
|
754
|
+
empty_stubs = 0
|
|
755
|
+
for node in ast.walk(tree):
|
|
756
|
+
if isinstance(node, ast.Name) and node.id == "Any":
|
|
757
|
+
unsafe_types += 1
|
|
758
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
759
|
+
start = node.lineno
|
|
760
|
+
end = getattr(node, 'end_lineno', start + len(node.body))
|
|
761
|
+
loc = end - start + 1
|
|
762
|
+
cc = 1
|
|
763
|
+
for child in ast.walk(node):
|
|
764
|
+
if isinstance(child, (ast.If, ast.For, ast.AsyncFor, ast.While, ast.ExceptHandler, ast.With, ast.AsyncWith)):
|
|
765
|
+
cc += 1
|
|
766
|
+
elif isinstance(child, ast.BoolOp):
|
|
767
|
+
cc += len(child.values) - 1
|
|
768
|
+
|
|
769
|
+
real_body = [n for n in node.body if not (isinstance(n, ast.Expr) and isinstance(getattr(n, 'value', None), ast.Constant))]
|
|
770
|
+
is_stub = len(real_body) == 0 or (len(real_body) == 1 and isinstance(real_body[0], ast.Pass))
|
|
771
|
+
if is_stub:
|
|
772
|
+
empty_stubs += 1
|
|
773
|
+
|
|
774
|
+
code_smells = ["Empty stub detected"] if is_stub else []
|
|
775
|
+
mi = max(0.0, round(100.0 - (cc * 2.0) - (loc * 0.2), 1))
|
|
776
|
+
functions.append({
|
|
777
|
+
"functionName": node.name,
|
|
778
|
+
"filePath": filepath,
|
|
779
|
+
"line": start,
|
|
780
|
+
"loc": loc,
|
|
781
|
+
"cyclomatic": cc,
|
|
782
|
+
"cognitive": cc,
|
|
783
|
+
"maintainability": mi,
|
|
784
|
+
"codeSmells": code_smells
|
|
785
|
+
})
|
|
786
|
+
return {
|
|
787
|
+
"language": "python",
|
|
788
|
+
"filePath": filepath,
|
|
789
|
+
"functions": functions,
|
|
790
|
+
"unsafeTypesCount": unsafe_types,
|
|
791
|
+
"emptyStubsCount": empty_stubs
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
code = sys.stdin.read()
|
|
795
|
+
filepath = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
796
|
+
print(json.dumps(analyze(code, filepath)))
|
|
797
|
+
`;
|
|
798
|
+
function extractFunctionsPythonAst(content, filePath) {
|
|
509
799
|
try {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
} else if (file.endsWith(".md")) {
|
|
520
|
-
fileList.push(fullPath);
|
|
521
|
-
}
|
|
522
|
-
} catch {
|
|
523
|
-
}
|
|
800
|
+
const res = spawnSync("python", ["-c", PYTHON_AST_RUNNER_SCRIPT, filePath], {
|
|
801
|
+
input: content,
|
|
802
|
+
encoding: "utf-8",
|
|
803
|
+
timeout: 1e4
|
|
804
|
+
});
|
|
805
|
+
if (res.status === 0 && res.stdout) {
|
|
806
|
+
const parsed = JSON.parse(res.stdout);
|
|
807
|
+
return parsed;
|
|
524
808
|
}
|
|
525
809
|
} catch {
|
|
526
810
|
}
|
|
527
|
-
|
|
811
|
+
const fallbackFns = extractPythonFunctionsPolyglot(content, filePath);
|
|
812
|
+
return {
|
|
813
|
+
language: "python",
|
|
814
|
+
filePath,
|
|
815
|
+
functions: fallbackFns,
|
|
816
|
+
unsafeTypesCount: 0,
|
|
817
|
+
emptyStubsCount: 0
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/verifiers/ast/native-ast-orchestrator.ts
|
|
822
|
+
function isCliInstalled(bin) {
|
|
823
|
+
try {
|
|
824
|
+
const res = spawnSync(bin, ["--version"], { encoding: "utf-8", timeout: 5e3, shell: false });
|
|
825
|
+
return res.status === 0 && !res.error;
|
|
826
|
+
} catch {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function assertRequiredStack(stack) {
|
|
831
|
+
if (!stack) return;
|
|
832
|
+
if (!isCliInstalled(stack)) {
|
|
833
|
+
throw new Error(`[AI-SDLC Fail-Closed] Required toolchain CLI '${stack}' is not installed in the environment`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function analyzeTypeScript(content, filePath) {
|
|
837
|
+
return {
|
|
838
|
+
language: "typescript",
|
|
839
|
+
filePath,
|
|
840
|
+
functions: extractFunctionsTypeScriptAst(content, filePath),
|
|
841
|
+
unsafeTypesCount: 0,
|
|
842
|
+
emptyStubsCount: 0
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function analyzePolyglot(content, filePath, ext) {
|
|
846
|
+
return {
|
|
847
|
+
language: ext.replace(/^\./, ""),
|
|
848
|
+
filePath,
|
|
849
|
+
functions: extractFunctionsPolyglot(content, filePath),
|
|
850
|
+
unsafeTypesCount: 0,
|
|
851
|
+
emptyStubsCount: 0
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function orchestrateNativeAstAnalysis(content, filePath, options = {}) {
|
|
855
|
+
assertRequiredStack(options.requiredStack);
|
|
856
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
857
|
+
if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
|
|
858
|
+
return analyzeTypeScript(content, filePath);
|
|
859
|
+
}
|
|
860
|
+
if (ext === ".py") {
|
|
861
|
+
return extractFunctionsPythonAst(content, filePath);
|
|
862
|
+
}
|
|
863
|
+
return analyzePolyglot(content, filePath, ext);
|
|
528
864
|
}
|
|
529
865
|
function parseMarkdownFrontmatter(content) {
|
|
530
866
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -556,41 +892,41 @@ function extractGherkinBlock(body) {
|
|
|
556
892
|
}
|
|
557
893
|
function resolveTargetFiles(rootDir, targetPath) {
|
|
558
894
|
if (targetPath) {
|
|
559
|
-
const fullTarget =
|
|
560
|
-
if (!
|
|
561
|
-
return
|
|
895
|
+
const fullTarget = path5.isAbsolute(targetPath) ? targetPath : path5.join(rootDir, targetPath);
|
|
896
|
+
if (!fs8.existsSync(fullTarget)) return [];
|
|
897
|
+
return fs8.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
562
898
|
}
|
|
563
899
|
const searchDirs = [
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
900
|
+
path5.join(rootDir, "product"),
|
|
901
|
+
path5.join(rootDir, "security"),
|
|
902
|
+
path5.join(rootDir, "privacy"),
|
|
903
|
+
path5.join(rootDir, "specs"),
|
|
904
|
+
path5.join(rootDir, "examples", "product"),
|
|
905
|
+
path5.join(rootDir, "examples", "security"),
|
|
906
|
+
path5.join(rootDir, "examples", "privacy"),
|
|
907
|
+
path5.join(rootDir, "examples", "specs")
|
|
572
908
|
];
|
|
573
|
-
return searchDirs.filter((d) =>
|
|
909
|
+
return searchDirs.filter((d) => fs8.existsSync(d)).flatMap((d) => walkMdFiles(d));
|
|
574
910
|
}
|
|
575
911
|
function resolveFeatureFilePath(rootDir, filePath, id, customTarget) {
|
|
576
912
|
if (customTarget) {
|
|
577
|
-
return
|
|
578
|
-
}
|
|
579
|
-
const baseDir =
|
|
580
|
-
const candidateSecurity =
|
|
581
|
-
const candidatePrivacy =
|
|
582
|
-
const candidateRoot =
|
|
583
|
-
if (
|
|
584
|
-
if (
|
|
585
|
-
if (
|
|
913
|
+
return path5.isAbsolute(customTarget) ? customTarget : path5.join(rootDir, customTarget);
|
|
914
|
+
}
|
|
915
|
+
const baseDir = path5.join(rootDir, "tests", "features");
|
|
916
|
+
const candidateSecurity = path5.join(baseDir, "security", `${id.toLowerCase()}.feature`);
|
|
917
|
+
const candidatePrivacy = path5.join(baseDir, "privacy", `${id.toLowerCase()}.feature`);
|
|
918
|
+
const candidateRoot = path5.join(baseDir, `${id.toLowerCase()}.feature`);
|
|
919
|
+
if (fs8.existsSync(candidateSecurity)) return candidateSecurity;
|
|
920
|
+
if (fs8.existsSync(candidatePrivacy)) return candidatePrivacy;
|
|
921
|
+
if (fs8.existsSync(candidateRoot)) return candidateRoot;
|
|
586
922
|
const subDir = filePath.includes("security") ? "security" : filePath.includes("privacy") ? "privacy" : "";
|
|
587
|
-
return
|
|
923
|
+
return path5.join(baseDir, subDir, `${id.toLowerCase()}.feature`);
|
|
588
924
|
}
|
|
589
925
|
function buildFeatureFileContent(rootDir, filePath, id, version, gherkinBlocks) {
|
|
590
926
|
const banner = [
|
|
591
927
|
`# ==============================================================================`,
|
|
592
928
|
`# AUTO-GENERADO POR AI-SDLC (Cucumber Integration)`,
|
|
593
|
-
`# Origen: ${
|
|
929
|
+
`# Origen: ${path5.relative(rootDir, filePath).replace(/\\/g, "/")}`,
|
|
594
930
|
`# ID Requerimiento: ${id}`,
|
|
595
931
|
`# Versi\xF3n: ${version || "1.0.0"}`,
|
|
596
932
|
`# NO EDITAR MANUALMENTE: Cualquier cambio debe realizarse en el Markdown origen.`,
|
|
@@ -615,20 +951,20 @@ function extractGherkinFeatures(options = {}) {
|
|
|
615
951
|
const targetFiles = resolveTargetFiles(rootDir, options.targetPath);
|
|
616
952
|
for (const filePath of targetFiles) {
|
|
617
953
|
try {
|
|
618
|
-
const content =
|
|
954
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
619
955
|
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
620
956
|
const gherkinBlocks = extractGherkinBlock(body);
|
|
621
957
|
if (gherkinBlocks.length === 0) continue;
|
|
622
|
-
const id = frontmatter.id ||
|
|
958
|
+
const id = frontmatter.id || path5.basename(filePath, path5.extname(filePath));
|
|
623
959
|
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
624
|
-
|
|
960
|
+
fs8.mkdirSync(path5.dirname(targetPath), { recursive: true });
|
|
625
961
|
const fullContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
626
|
-
|
|
962
|
+
fs8.writeFileSync(targetPath, fullContent, "utf-8");
|
|
627
963
|
const scenarioCount = countScenarios(gherkinBlocks);
|
|
628
964
|
totalScenarios += scenarioCount;
|
|
629
965
|
features.push({
|
|
630
|
-
sourceFile:
|
|
631
|
-
outputFile:
|
|
966
|
+
sourceFile: path5.relative(rootDir, filePath).replace(/\\/g, "/"),
|
|
967
|
+
outputFile: path5.relative(rootDir, targetPath).replace(/\\/g, "/"),
|
|
632
968
|
featureName: id,
|
|
633
969
|
scenarioCount
|
|
634
970
|
});
|
|
@@ -645,20 +981,20 @@ function checkGherkinInSync(options = {}) {
|
|
|
645
981
|
let totalFeatures = 0;
|
|
646
982
|
for (const filePath of targetFiles) {
|
|
647
983
|
try {
|
|
648
|
-
const content =
|
|
984
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
649
985
|
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
650
986
|
const gherkinBlocks = extractGherkinBlock(body);
|
|
651
987
|
if (gherkinBlocks.length === 0) continue;
|
|
652
988
|
totalFeatures++;
|
|
653
|
-
const id = frontmatter.id ||
|
|
989
|
+
const id = frontmatter.id || path5.basename(filePath, path5.extname(filePath));
|
|
654
990
|
const targetPath = resolveFeatureFilePath(rootDir, filePath, id, frontmatter["cucumber-feature-file"]);
|
|
655
|
-
const relTarget =
|
|
656
|
-
if (!
|
|
991
|
+
const relTarget = path5.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
992
|
+
if (!fs8.existsSync(targetPath)) {
|
|
657
993
|
missingFiles.push(relTarget);
|
|
658
994
|
continue;
|
|
659
995
|
}
|
|
660
996
|
const expectedContent = buildFeatureFileContent(rootDir, filePath, id, frontmatter.version, gherkinBlocks);
|
|
661
|
-
const actualContent =
|
|
997
|
+
const actualContent = fs8.readFileSync(targetPath, "utf-8");
|
|
662
998
|
if (actualContent.replace(/\r\n/g, "\n") !== expectedContent.replace(/\r\n/g, "\n")) {
|
|
663
999
|
outOfSyncFiles.push(relTarget);
|
|
664
1000
|
}
|
|
@@ -757,7 +1093,7 @@ function generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, t
|
|
|
757
1093
|
const reportLines = [
|
|
758
1094
|
`# \u{1F9EA} Deterministic Test Coverage Audit for Requirements and Tasks`,
|
|
759
1095
|
``,
|
|
760
|
-
`> **Audit Date:** ${(
|
|
1096
|
+
`> **Audit Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
761
1097
|
`> **Overall Verdict:** ${verdict}`,
|
|
762
1098
|
``,
|
|
763
1099
|
`---`,
|
|
@@ -820,8 +1156,8 @@ function parseCodeTestIds(content, relTest, reqToTestsMap) {
|
|
|
820
1156
|
}
|
|
821
1157
|
function processTestFile(fullPath, rootDir, reqToTestsMap) {
|
|
822
1158
|
try {
|
|
823
|
-
const content =
|
|
824
|
-
const relTest =
|
|
1159
|
+
const content = fs8.readFileSync(fullPath, "utf-8");
|
|
1160
|
+
const relTest = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
825
1161
|
if (fullPath.endsWith(".feature")) {
|
|
826
1162
|
parseFeatureTags(content, relTest, reqToTestsMap);
|
|
827
1163
|
}
|
|
@@ -830,16 +1166,16 @@ function processTestFile(fullPath, rootDir, reqToTestsMap) {
|
|
|
830
1166
|
}
|
|
831
1167
|
}
|
|
832
1168
|
function collectTestFiles(dir, files = []) {
|
|
833
|
-
if (!
|
|
1169
|
+
if (!fs8.existsSync(dir)) return files;
|
|
834
1170
|
try {
|
|
835
|
-
const entries =
|
|
1171
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true }).sort((a, b) => canonicalSort(a.name, b.name));
|
|
836
1172
|
for (const e of entries) {
|
|
837
1173
|
if (e.isDirectory()) {
|
|
838
1174
|
if (!EXCLUDED_TEST_DIRS.has(e.name)) {
|
|
839
|
-
collectTestFiles(
|
|
1175
|
+
collectTestFiles(path5.join(dir, e.name), files);
|
|
840
1176
|
}
|
|
841
1177
|
} else if (isTestFile(e.name)) {
|
|
842
|
-
files.push(
|
|
1178
|
+
files.push(path5.join(dir, e.name));
|
|
843
1179
|
}
|
|
844
1180
|
}
|
|
845
1181
|
} catch {
|
|
@@ -1048,7 +1384,7 @@ function collectTestCorpus(rootDir, allMdFiles) {
|
|
|
1048
1384
|
const testContents = [];
|
|
1049
1385
|
for (const tf of collectTestFiles(rootDir)) {
|
|
1050
1386
|
try {
|
|
1051
|
-
const content =
|
|
1387
|
+
const content = fs8.readFileSync(tf, "utf-8");
|
|
1052
1388
|
testContents.push(content);
|
|
1053
1389
|
if (tf.endsWith(".feature")) gherkinSources.push(content);
|
|
1054
1390
|
} catch {
|
|
@@ -1056,7 +1392,7 @@ function collectTestCorpus(rootDir, allMdFiles) {
|
|
|
1056
1392
|
}
|
|
1057
1393
|
for (const md of allMdFiles) {
|
|
1058
1394
|
try {
|
|
1059
|
-
const blocks = extractGherkinBlock(
|
|
1395
|
+
const blocks = extractGherkinBlock(fs8.readFileSync(md, "utf-8"));
|
|
1060
1396
|
gherkinSources.push(...blocks);
|
|
1061
1397
|
testContents.push(...blocks);
|
|
1062
1398
|
} catch {
|
|
@@ -1064,9 +1400,54 @@ function collectTestCorpus(rootDir, allMdFiles) {
|
|
|
1064
1400
|
}
|
|
1065
1401
|
return { gherkinSources, combinedTests: testContents.join("\n") };
|
|
1066
1402
|
}
|
|
1067
|
-
function
|
|
1403
|
+
function isCucumberScenarioPassed(element) {
|
|
1404
|
+
const steps = Array.isArray(element.steps) ? element.steps : [];
|
|
1405
|
+
if (steps.length === 0) return false;
|
|
1406
|
+
return steps.every((s) => {
|
|
1407
|
+
const res = s.result;
|
|
1408
|
+
return res && res.status === "passed";
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
function countCucumberScenarios(raw) {
|
|
1412
|
+
let passed = 0;
|
|
1413
|
+
let failed = 0;
|
|
1414
|
+
for (const feat of raw) {
|
|
1415
|
+
const elements = Array.isArray(feat.elements) ? feat.elements : [];
|
|
1416
|
+
for (const el of elements) {
|
|
1417
|
+
if (isCucumberScenarioPassed(el)) passed++;
|
|
1418
|
+
else failed++;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return { passed, failed };
|
|
1422
|
+
}
|
|
1423
|
+
function parseCucumberReportFile(reportPath) {
|
|
1424
|
+
if (!fs8.existsSync(reportPath)) return void 0;
|
|
1425
|
+
try {
|
|
1426
|
+
const raw = JSON.parse(fs8.readFileSync(reportPath, "utf-8"));
|
|
1427
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1428
|
+
const { passed, failed } = countCucumberScenarios(raw);
|
|
1429
|
+
return {
|
|
1430
|
+
evaluatedFeatures: raw.length,
|
|
1431
|
+
passedScenarios: passed,
|
|
1432
|
+
failedScenarios: failed,
|
|
1433
|
+
passed: passed + failed > 0 && failed === 0
|
|
1434
|
+
};
|
|
1435
|
+
} catch {
|
|
1436
|
+
return void 0;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function resolveCucumberReport(rootDir, options) {
|
|
1440
|
+
const candidate = options.cucumberReportPath ? path5.isAbsolute(options.cucumberReportPath) ? options.cucumberReportPath : path5.join(rootDir, options.cucumberReportPath) : path5.join(rootDir, "reports", "cucumber-report.json");
|
|
1441
|
+
const parsed = parseCucumberReportFile(candidate);
|
|
1442
|
+
if (parsed) return parsed;
|
|
1443
|
+
if (options.enforceCucumberExecution) {
|
|
1444
|
+
return { evaluatedFeatures: 0, passedScenarios: 0, failedScenarios: 1, passed: false };
|
|
1445
|
+
}
|
|
1446
|
+
return void 0;
|
|
1447
|
+
}
|
|
1448
|
+
function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, taskErrors, ucErrors, rootDir, options) {
|
|
1068
1449
|
return {
|
|
1069
|
-
success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0,
|
|
1450
|
+
success: reqErrors === 0 && taskErrors === 0 && ucErrors === 0 && (!resolveCucumberReport(rootDir, options) || resolveCucumberReport(rootDir, options).passed),
|
|
1070
1451
|
totalRequirements: reqAudit.length,
|
|
1071
1452
|
passedRequirements: reqAudit.length - reqErrors,
|
|
1072
1453
|
failedRequirements: reqErrors,
|
|
@@ -1077,6 +1458,7 @@ function assembleCoverageResult(reqAudit, taskAudit, useCaseFlows, reqErrors, ta
|
|
|
1077
1458
|
tasks: taskAudit,
|
|
1078
1459
|
useCaseFlows,
|
|
1079
1460
|
failedUseCaseFlows: ucErrors,
|
|
1461
|
+
...resolveCucumberReport(rootDir, options) ? { cucumberExecution: resolveCucumberReport(rootDir, options) } : {},
|
|
1080
1462
|
reportMarkdown: generateTestingCoverageReportMarkdown(reqAudit, taskAudit, reqErrors, taskErrors)
|
|
1081
1463
|
};
|
|
1082
1464
|
}
|
|
@@ -1089,8 +1471,8 @@ function recordUseCaseAuditIfPresent(frontmatter, id, relPath, isTemplate, combi
|
|
|
1089
1471
|
if (flowAudit.status === "FALLO_SIN_PRUEBA") state.ucErrors++;
|
|
1090
1472
|
}
|
|
1091
1473
|
function processMdFileForCoverage(file, rootDir, reqToTestsMap, corpus, options, state) {
|
|
1092
|
-
const { frontmatter } = parseGenericDoc(
|
|
1093
|
-
const relPath =
|
|
1474
|
+
const { frontmatter } = parseGenericDoc(fs8.readFileSync(file, "utf-8"));
|
|
1475
|
+
const relPath = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
1094
1476
|
const isTemplate = relPath.includes("templates/");
|
|
1095
1477
|
const id = frontmatter.id || "";
|
|
1096
1478
|
if (isRequirementArtifact(frontmatter, id)) {
|
|
@@ -1128,7 +1510,9 @@ function verifyTestingCoverage(options = {}) {
|
|
|
1128
1510
|
state.useCaseFlows,
|
|
1129
1511
|
state.reqErrors,
|
|
1130
1512
|
state.taskErrors,
|
|
1131
|
-
state.ucErrors
|
|
1513
|
+
state.ucErrors,
|
|
1514
|
+
rootDir,
|
|
1515
|
+
options
|
|
1132
1516
|
);
|
|
1133
1517
|
}
|
|
1134
1518
|
|
|
@@ -1162,7 +1546,6 @@ var DEFAULT_EXCLUDES = [
|
|
|
1162
1546
|
"templates",
|
|
1163
1547
|
"fixtures"
|
|
1164
1548
|
];
|
|
1165
|
-
var TS_JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1166
1549
|
function createDefaultQualityPolicy() {
|
|
1167
1550
|
return {
|
|
1168
1551
|
max_cyclomatic: 10,
|
|
@@ -1243,27 +1626,38 @@ function applyOptionOverrides(policy, overrides) {
|
|
|
1243
1626
|
if (overrides.max_function_lines !== void 0) policy.max_function_lines = Number(overrides.max_function_lines);
|
|
1244
1627
|
if (overrides.enforce_mode !== void 0) policy.enforce_mode = overrides.enforce_mode;
|
|
1245
1628
|
}
|
|
1629
|
+
function parsePolicyYaml(content) {
|
|
1630
|
+
let doc;
|
|
1631
|
+
try {
|
|
1632
|
+
doc = yaml3.load(content);
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid YAML in quality-policy: ${err instanceof Error ? err.message : String(err)}`);
|
|
1635
|
+
}
|
|
1636
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
1637
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid quality policy structure: root must be a YAML mapping`);
|
|
1638
|
+
}
|
|
1639
|
+
return doc;
|
|
1640
|
+
}
|
|
1246
1641
|
function parseQualityPolicy(content, overrides) {
|
|
1247
1642
|
const policy = createDefaultQualityPolicy();
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
applyDocScope(policy, record);
|
|
1253
|
-
applyDocThresholds(policy, record);
|
|
1254
|
-
if (record.max_per_function !== void 0) policy.max_cyclomatic = Number(record.max_per_function);
|
|
1255
|
-
if (record.min_acceptable_score !== void 0) policy.min_maintainability = Number(record.min_acceptable_score);
|
|
1256
|
-
if (record.max_function_lines !== void 0) policy.max_function_lines = Number(record.max_function_lines);
|
|
1257
|
-
}
|
|
1258
|
-
} catch {
|
|
1643
|
+
if (!content || !content.trim()) {
|
|
1644
|
+
applyEnvThresholds(policy);
|
|
1645
|
+
applyOptionOverrides(policy, overrides);
|
|
1646
|
+
return policy;
|
|
1259
1647
|
}
|
|
1648
|
+
const record = parsePolicyYaml(content);
|
|
1649
|
+
applyDocScope(policy, record);
|
|
1650
|
+
applyDocThresholds(policy, record);
|
|
1651
|
+
if (record.max_per_function !== void 0) policy.max_cyclomatic = Number(record.max_per_function);
|
|
1652
|
+
if (record.min_acceptable_score !== void 0) policy.min_maintainability = Number(record.min_acceptable_score);
|
|
1653
|
+
if (record.max_function_lines !== void 0) policy.max_function_lines = Number(record.max_function_lines);
|
|
1260
1654
|
applyEnvThresholds(policy);
|
|
1261
1655
|
applyOptionOverrides(policy, overrides);
|
|
1262
1656
|
return policy;
|
|
1263
1657
|
}
|
|
1264
1658
|
function extractFunctions(content, filePath) {
|
|
1265
|
-
const
|
|
1266
|
-
return
|
|
1659
|
+
const report = orchestrateNativeAstAnalysis(content, filePath);
|
|
1660
|
+
return report.functions;
|
|
1267
1661
|
}
|
|
1268
1662
|
function extractFunctionsBraceLanguages(content, filePath) {
|
|
1269
1663
|
return extractFunctions(content, filePath);
|
|
@@ -1293,55 +1687,55 @@ function isExcludedFile(file, normalized, excludePatterns) {
|
|
|
1293
1687
|
);
|
|
1294
1688
|
}
|
|
1295
1689
|
function walkDir(dir, extensions = DEFAULT_EXTENSIONS, excludePatterns = DEFAULT_EXCLUDES) {
|
|
1296
|
-
if (!
|
|
1690
|
+
if (!fs8.existsSync(dir)) return [];
|
|
1297
1691
|
const results = [];
|
|
1298
|
-
for (const file of
|
|
1299
|
-
const fullPath =
|
|
1692
|
+
for (const file of fs8.readdirSync(dir).sort(canonicalSort)) {
|
|
1693
|
+
const fullPath = path5.join(dir, file);
|
|
1300
1694
|
const normalized = fullPath.replace(/\\/g, "/");
|
|
1301
1695
|
if (isExcludedFile(file, normalized, excludePatterns)) continue;
|
|
1302
|
-
const stat =
|
|
1696
|
+
const stat = fs8.statSync(fullPath);
|
|
1303
1697
|
if (stat.isDirectory()) {
|
|
1304
1698
|
results.push(...walkDir(fullPath, extensions, excludePatterns));
|
|
1305
|
-
} else if (extensions.includes(
|
|
1699
|
+
} else if (extensions.includes(path5.extname(file).toLowerCase())) {
|
|
1306
1700
|
results.push(fullPath);
|
|
1307
1701
|
}
|
|
1308
1702
|
}
|
|
1309
1703
|
return results;
|
|
1310
1704
|
}
|
|
1311
1705
|
function resolvePolicy(rootDir, options) {
|
|
1312
|
-
const policyFile = options.policyPath ||
|
|
1706
|
+
const policyFile = options.policyPath || path5.join(rootDir, "quality-policy.yaml");
|
|
1313
1707
|
if (options.policy) {
|
|
1314
1708
|
const policy = { ...options.policy };
|
|
1315
1709
|
applyOptionOverrides(policy, options.thresholds);
|
|
1316
1710
|
return policy;
|
|
1317
1711
|
}
|
|
1318
|
-
if (
|
|
1319
|
-
return parseQualityPolicy(
|
|
1712
|
+
if (fs8.existsSync(policyFile)) {
|
|
1713
|
+
return parseQualityPolicy(fs8.readFileSync(policyFile, "utf-8"), options.thresholds);
|
|
1320
1714
|
}
|
|
1321
1715
|
return parseQualityPolicy("", options.thresholds);
|
|
1322
1716
|
}
|
|
1323
1717
|
function isPathExcluded(targetPath, rootDir, excludePatterns) {
|
|
1324
|
-
const rel =
|
|
1325
|
-
const base =
|
|
1718
|
+
const rel = path5.relative(rootDir, targetPath).replace(/\\/g, "/");
|
|
1719
|
+
const base = path5.basename(targetPath);
|
|
1326
1720
|
return excludePatterns.some(
|
|
1327
1721
|
(pat) => base === pat || rel === pat || rel.startsWith(pat + "/") || rel.includes("/" + pat + "/") || rel.endsWith("/" + pat) || rel.includes(pat)
|
|
1328
1722
|
);
|
|
1329
1723
|
}
|
|
1330
1724
|
function scanContainerSubdir(containerDir, subdirName, targetDir, rootDir, policy, files) {
|
|
1331
1725
|
if (policy.exclude_patterns.includes(subdirName)) return;
|
|
1332
|
-
const subTarget =
|
|
1333
|
-
if (!
|
|
1726
|
+
const subTarget = path5.join(containerDir, subdirName, targetDir);
|
|
1727
|
+
if (!fs8.existsSync(subTarget) || !fs8.statSync(subTarget).isDirectory()) return;
|
|
1334
1728
|
if (isPathExcluded(subTarget, rootDir, policy.exclude_patterns)) return;
|
|
1335
1729
|
for (const f of walkDir(subTarget, policy.supported_extensions, policy.exclude_patterns)) {
|
|
1336
1730
|
files.add(f);
|
|
1337
1731
|
}
|
|
1338
1732
|
}
|
|
1339
1733
|
function scanMonorepoContainer(rootDir, container, targetDir, policy, files) {
|
|
1340
|
-
const containerDir =
|
|
1341
|
-
if (!
|
|
1734
|
+
const containerDir = path5.join(rootDir, container);
|
|
1735
|
+
if (!fs8.existsSync(containerDir) || !fs8.statSync(containerDir).isDirectory()) return;
|
|
1342
1736
|
if (isPathExcluded(containerDir, rootDir, policy.exclude_patterns)) return;
|
|
1343
1737
|
try {
|
|
1344
|
-
for (const subdir of
|
|
1738
|
+
for (const subdir of fs8.readdirSync(containerDir, { withFileTypes: true })) {
|
|
1345
1739
|
if (subdir.isDirectory()) {
|
|
1346
1740
|
scanContainerSubdir(containerDir, subdir.name, targetDir, rootDir, policy, files);
|
|
1347
1741
|
}
|
|
@@ -1350,13 +1744,14 @@ function scanMonorepoContainer(rootDir, container, targetDir, policy, files) {
|
|
|
1350
1744
|
}
|
|
1351
1745
|
}
|
|
1352
1746
|
function scanDirectTarget(rootDir, targetDir, policy, files) {
|
|
1353
|
-
const fullDir =
|
|
1354
|
-
if (
|
|
1747
|
+
const fullDir = path5.join(rootDir, targetDir);
|
|
1748
|
+
if (fs8.existsSync(fullDir) && !isPathExcluded(fullDir, rootDir, policy.exclude_patterns)) {
|
|
1355
1749
|
for (const f of walkDir(fullDir, policy.supported_extensions, policy.exclude_patterns)) {
|
|
1356
1750
|
files.add(f);
|
|
1357
1751
|
}
|
|
1358
1752
|
}
|
|
1359
1753
|
}
|
|
1754
|
+
var UNIT_TEST_SUFFIXES = [".spec.ts", ".test.ts", ".spec.js", ".test.js", "_test.py", "_test.go"];
|
|
1360
1755
|
function collectFilesToScan(rootDir, policy) {
|
|
1361
1756
|
const files = /* @__PURE__ */ new Set();
|
|
1362
1757
|
const monorepoContainers = ["packages", "apps", "libs", "modules", "services"];
|
|
@@ -1366,7 +1761,7 @@ function collectFilesToScan(rootDir, policy) {
|
|
|
1366
1761
|
scanMonorepoContainer(rootDir, container, targetDir, policy, files);
|
|
1367
1762
|
}
|
|
1368
1763
|
}
|
|
1369
|
-
return Array.from(files);
|
|
1764
|
+
return Array.from(files).filter((f) => !UNIT_TEST_SUFFIXES.some((s) => f.endsWith(s)));
|
|
1370
1765
|
}
|
|
1371
1766
|
function evaluateViolations(fn, policy) {
|
|
1372
1767
|
const violations = [];
|
|
@@ -1388,38 +1783,67 @@ function evaluateViolations(fn, policy) {
|
|
|
1388
1783
|
return violations;
|
|
1389
1784
|
}
|
|
1390
1785
|
function evaluateFileQuality(file, rootDir, policy) {
|
|
1391
|
-
const content =
|
|
1786
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
1392
1787
|
const fns = extractFunctions(content, file);
|
|
1393
1788
|
return fns.map((fn) => {
|
|
1394
1789
|
const violations = evaluateViolations(fn, policy);
|
|
1395
1790
|
return {
|
|
1396
1791
|
...fn,
|
|
1397
|
-
relPath:
|
|
1792
|
+
relPath: path5.relative(rootDir, file).replace(/\\/g, "/"),
|
|
1398
1793
|
status: violations.length === 0 ? "PASS" : "FAIL",
|
|
1399
1794
|
violations
|
|
1400
1795
|
};
|
|
1401
1796
|
});
|
|
1402
1797
|
}
|
|
1403
|
-
var UNIT_TEST_SUFFIXES = [".spec.ts", ".test.ts", ".spec.js", ".test.js", "_test.py", "_test.go"];
|
|
1404
1798
|
function collectUnitTestCorpus(rootDir) {
|
|
1405
1799
|
const allCodeFiles = walkDir(rootDir, DEFAULT_EXTENSIONS, DEFAULT_EXCLUDES);
|
|
1406
1800
|
const chunks = [];
|
|
1407
1801
|
for (const tf of allCodeFiles) {
|
|
1408
1802
|
if (!UNIT_TEST_SUFFIXES.some((s) => tf.endsWith(s))) continue;
|
|
1409
1803
|
try {
|
|
1410
|
-
chunks.push(
|
|
1804
|
+
chunks.push(fs8.readFileSync(tf, "utf-8"));
|
|
1411
1805
|
} catch {
|
|
1412
1806
|
}
|
|
1413
1807
|
}
|
|
1414
1808
|
return chunks.join("\n");
|
|
1415
1809
|
}
|
|
1810
|
+
function sanitizeTestCorpus(corpus, fnName) {
|
|
1811
|
+
let cleaned = corpus.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
1812
|
+
cleaned = cleaned.replace(/(?:\/\/|#)[^\r\n]*/g, " ");
|
|
1813
|
+
cleaned = cleaned.replace(/(['"`])((?:\\.|(?!\1)[^\\])*)\1/g, (match, quote, inner) => {
|
|
1814
|
+
const trimmed = inner.trim();
|
|
1815
|
+
if (trimmed === fnName || trimmed === `#${fnName}` || trimmed === `.${fnName}` || trimmed === `${fnName}()`) {
|
|
1816
|
+
return match;
|
|
1817
|
+
}
|
|
1818
|
+
return `${quote}${quote}`;
|
|
1819
|
+
});
|
|
1820
|
+
return cleaned;
|
|
1821
|
+
}
|
|
1822
|
+
function buildTestReferencePatterns(escaped) {
|
|
1823
|
+
return [
|
|
1824
|
+
new RegExp(`(?:\\.|\\b)${escaped}\\s*\\(`, "m"),
|
|
1825
|
+
new RegExp(`\\b(?:import|require)\\b[^;\\n]*\\b${escaped}\\b`, "m"),
|
|
1826
|
+
new RegExp(`(?:\\bexpect|\\bassert|\\bshould)\\s*\\([^;\\n]*\\b${escaped}\\b`, "m"),
|
|
1827
|
+
new RegExp("(?:\\bspyOn|\\bmock)\\s*\\([^;\\n]*['\"`]" + escaped + "['\"`]", "m"),
|
|
1828
|
+
new RegExp(`\\b${escaped}\\s*\\.`, "m"),
|
|
1829
|
+
new RegExp("\\b(?:describe|it|test|context)\\s*\\(\\s*['\"`](?:#|\\.)?" + escaped + "['\"`]", "m"),
|
|
1830
|
+
new RegExp(`\\btest_?${escaped}\\b`, "im")
|
|
1831
|
+
];
|
|
1832
|
+
}
|
|
1833
|
+
function isFunctionReferencedInTests(fnName, rawCorpus) {
|
|
1834
|
+
if (!fnName || fnName === "anonymous" || fnName === "constructor") return true;
|
|
1835
|
+
const corpus = sanitizeTestCorpus(rawCorpus, fnName);
|
|
1836
|
+
const escaped = fnName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1837
|
+
const patterns = buildTestReferencePatterns(escaped);
|
|
1838
|
+
return patterns.some((p) => p.test(corpus));
|
|
1839
|
+
}
|
|
1416
1840
|
function auditUnitTestsPerFunction(results, rootDir) {
|
|
1417
1841
|
const corpus = collectUnitTestCorpus(rootDir);
|
|
1418
1842
|
const untested = [];
|
|
1419
1843
|
for (const r of results) {
|
|
1420
1844
|
const fnName = r.functionName.includes(".") ? r.functionName.split(".").pop() : r.functionName;
|
|
1421
1845
|
if (!fnName || fnName === "anonymous" || fnName === "constructor") continue;
|
|
1422
|
-
if (!
|
|
1846
|
+
if (!isFunctionReferencedInTests(fnName, corpus)) {
|
|
1423
1847
|
untested.push({ file: r.relPath, functionName: r.functionName });
|
|
1424
1848
|
r.violations.push(`Missing unit test for function '${r.functionName}'`);
|
|
1425
1849
|
r.status = "FAIL";
|
|
@@ -1429,11 +1853,11 @@ function auditUnitTestsPerFunction(results, rootDir) {
|
|
|
1429
1853
|
}
|
|
1430
1854
|
function isVitestMidFlightCoverage(rel, fullPath) {
|
|
1431
1855
|
if (!process.env.VITEST || rel !== "coverage/coverage-summary.json") return false;
|
|
1432
|
-
return
|
|
1856
|
+
return path5.resolve(fullPath) === path5.resolve(process.cwd(), rel);
|
|
1433
1857
|
}
|
|
1434
1858
|
function parseCoverageSummaryFile(filePath, source) {
|
|
1435
1859
|
try {
|
|
1436
|
-
const raw = JSON.parse(
|
|
1860
|
+
const raw = JSON.parse(fs8.readFileSync(filePath, "utf-8"));
|
|
1437
1861
|
const total = raw.total;
|
|
1438
1862
|
if (!total) return null;
|
|
1439
1863
|
return {
|
|
@@ -1448,8 +1872,8 @@ function parseCoverageSummaryFile(filePath, source) {
|
|
|
1448
1872
|
}
|
|
1449
1873
|
function readSummaryCoverageJson(rootDir) {
|
|
1450
1874
|
for (const rel of ["coverage/coverage-summary.json", "reports/coverage-summary.json"]) {
|
|
1451
|
-
const full =
|
|
1452
|
-
if (!
|
|
1875
|
+
const full = path5.join(rootDir, rel);
|
|
1876
|
+
if (!fs8.existsSync(full) || isVitestMidFlightCoverage(rel, full)) continue;
|
|
1453
1877
|
const summary = parseCoverageSummaryFile(full, rel);
|
|
1454
1878
|
if (summary) return summary;
|
|
1455
1879
|
}
|
|
@@ -1473,7 +1897,7 @@ function evaluateCoverageThresholds(rootDir, policy, requireReport) {
|
|
|
1473
1897
|
}
|
|
1474
1898
|
function computeViolationSignature(relPath, fnName, metric) {
|
|
1475
1899
|
const normPath = relPath.replace(/\\/g, "/");
|
|
1476
|
-
return
|
|
1900
|
+
return crypto3.createHash("sha256").update(normPath + ":" + fnName + ":" + metric).digest("hex");
|
|
1477
1901
|
}
|
|
1478
1902
|
function classifyMetricFromMessage(msg, fn) {
|
|
1479
1903
|
if (msg.startsWith("Cyclomatic Complexity")) {
|
|
@@ -1502,8 +1926,8 @@ function isMetricWorsened(baselineEntry, current) {
|
|
|
1502
1926
|
}
|
|
1503
1927
|
function loadQualityBaseline(baselineFile) {
|
|
1504
1928
|
try {
|
|
1505
|
-
if (!
|
|
1506
|
-
const content =
|
|
1929
|
+
if (!fs8.existsSync(baselineFile)) return null;
|
|
1930
|
+
const content = fs8.readFileSync(baselineFile, "utf-8");
|
|
1507
1931
|
return JSON.parse(content);
|
|
1508
1932
|
} catch {
|
|
1509
1933
|
return null;
|
|
@@ -1512,11 +1936,11 @@ function loadQualityBaseline(baselineFile) {
|
|
|
1512
1936
|
function writeQualityBaseline(baselineFile, entries) {
|
|
1513
1937
|
const baseline = {
|
|
1514
1938
|
version: "1.0",
|
|
1515
|
-
generatedAt: (
|
|
1939
|
+
generatedAt: resolveDeterministicDate(path5.dirname(baselineFile)).toISOString(),
|
|
1516
1940
|
totalViolations: entries.length,
|
|
1517
1941
|
violations: entries
|
|
1518
1942
|
};
|
|
1519
|
-
|
|
1943
|
+
fs8.writeFileSync(baselineFile, JSON.stringify(baseline, null, 2) + "\n", "utf-8");
|
|
1520
1944
|
return baseline;
|
|
1521
1945
|
}
|
|
1522
1946
|
function extractBaselineEntriesFromResults(results) {
|
|
@@ -1575,7 +1999,7 @@ function applyRatchetMode(results, baseline) {
|
|
|
1575
1999
|
function resolveBaselineFilePath(rootDir, options) {
|
|
1576
2000
|
if (options.baselinePath) return options.baselinePath;
|
|
1577
2001
|
if (typeof options.baseline === "string") return options.baseline;
|
|
1578
|
-
return
|
|
2002
|
+
return path5.join(rootDir, "quality-baseline.json");
|
|
1579
2003
|
}
|
|
1580
2004
|
function isRatchetModeActive(policy, options, baselineExists) {
|
|
1581
2005
|
if (options.ratchet !== void 0) return options.ratchet;
|
|
@@ -1583,7 +2007,7 @@ function isRatchetModeActive(policy, options, baselineExists) {
|
|
|
1583
2007
|
return Boolean(policy.ratchet_mode || options.baseline && baselineExists);
|
|
1584
2008
|
}
|
|
1585
2009
|
function evaluateRatchetIfActive(results, baselineFile, isActive) {
|
|
1586
|
-
if (!isActive || !
|
|
2010
|
+
if (!isActive || !fs8.existsSync(baselineFile)) {
|
|
1587
2011
|
return { toleratedViolationsCount: 0, newViolationsCount: 0, baselineViolationsCount: 0 };
|
|
1588
2012
|
}
|
|
1589
2013
|
const baseline = loadQualityBaseline(baselineFile);
|
|
@@ -1642,7 +2066,7 @@ function verifyQualityGate(options = {}) {
|
|
|
1642
2066
|
baselineViolationsCount: entries.length
|
|
1643
2067
|
});
|
|
1644
2068
|
}
|
|
1645
|
-
const baselineExists =
|
|
2069
|
+
const baselineExists = fs8.existsSync(baselineFile);
|
|
1646
2070
|
const isRatchet = isRatchetModeActive(policy, options, baselineExists);
|
|
1647
2071
|
const ratchetEval = evaluateRatchetIfActive(results, baselineFile, isRatchet);
|
|
1648
2072
|
return buildQualityGateResult(results, allFiles.length, policy, coverageViolations, untestedFunctions, {
|
|
@@ -1660,9 +2084,9 @@ function normalizeHandoffPayload(handoff, changeId) {
|
|
|
1660
2084
|
};
|
|
1661
2085
|
}
|
|
1662
2086
|
function parseChangeProfile(specFile) {
|
|
1663
|
-
if (!
|
|
2087
|
+
if (!fs8.existsSync(specFile)) return "standard";
|
|
1664
2088
|
try {
|
|
1665
|
-
const rawSpec =
|
|
2089
|
+
const rawSpec = fs8.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1666
2090
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1667
2091
|
if (match) {
|
|
1668
2092
|
const parsedFm = yaml3.load(match[1]);
|
|
@@ -1684,12 +2108,12 @@ function checkSpecGherkinFormat(parsedFm, rawSpec, changeWorkspaceDir, errors) {
|
|
|
1684
2108
|
}
|
|
1685
2109
|
}
|
|
1686
2110
|
function validateSpecFileContent(specFile, changeWorkspaceDir, errors) {
|
|
1687
|
-
if (!
|
|
2111
|
+
if (!fs8.existsSync(specFile)) {
|
|
1688
2112
|
errors.push(`Falta el archivo obligatorio 'spec.md' en ${changeWorkspaceDir}.`);
|
|
1689
2113
|
return;
|
|
1690
2114
|
}
|
|
1691
2115
|
try {
|
|
1692
|
-
const rawSpec =
|
|
2116
|
+
const rawSpec = fs8.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1693
2117
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1694
2118
|
if (match) {
|
|
1695
2119
|
const parsedFm = yaml3.load(match[1]);
|
|
@@ -1708,19 +2132,19 @@ function validateHandoffSidecar(handoff, changeWorkspaceDir, errors) {
|
|
|
1708
2132
|
if (!handoff.subgraph || !Array.isArray(handoff.subgraph.requirements)) {
|
|
1709
2133
|
errors.push(`El sidecar '${handoff.id}' no define un subgrafo de requerimientos v\xE1lido.`);
|
|
1710
2134
|
}
|
|
1711
|
-
return
|
|
2135
|
+
return path5.join(changeWorkspaceDir, "handoff.yaml");
|
|
1712
2136
|
}
|
|
1713
2137
|
function validateOpenSpecStructure(profile, changeWorkspaceDir, adapter, errors, warnings) {
|
|
1714
|
-
const proposalFile =
|
|
1715
|
-
const tasksFile =
|
|
2138
|
+
const proposalFile = path5.join(changeWorkspaceDir, "proposal.md");
|
|
2139
|
+
const tasksFile = path5.join(changeWorkspaceDir, "tasks.md");
|
|
1716
2140
|
if (profile === "patch") {
|
|
1717
2141
|
const handoff2 = adapter.loadSidecar(changeWorkspaceDir);
|
|
1718
|
-
return { handoff: handoff2, sidecarFile: handoff2 ?
|
|
2142
|
+
return { handoff: handoff2, sidecarFile: handoff2 ? path5.join(changeWorkspaceDir, "handoff.yaml") : void 0 };
|
|
1719
2143
|
}
|
|
1720
|
-
if (!
|
|
2144
|
+
if (!fs8.existsSync(proposalFile)) {
|
|
1721
2145
|
warnings.push(`Falta 'proposal.md' en el espacio de trabajo OpenSpec.`);
|
|
1722
2146
|
}
|
|
1723
|
-
if (!
|
|
2147
|
+
if (!fs8.existsSync(tasksFile)) {
|
|
1724
2148
|
errors.push(`Falta el archivo obligatorio 'tasks.md' en ${changeWorkspaceDir}.`);
|
|
1725
2149
|
}
|
|
1726
2150
|
const handoff = adapter.loadSidecar(changeWorkspaceDir);
|
|
@@ -1728,20 +2152,20 @@ function validateOpenSpecStructure(profile, changeWorkspaceDir, adapter, errors,
|
|
|
1728
2152
|
return { handoff, sidecarFile };
|
|
1729
2153
|
}
|
|
1730
2154
|
function buildOpenSpecWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecarFile) {
|
|
1731
|
-
const changeId =
|
|
1732
|
-
const proposalFile =
|
|
1733
|
-
const specFile =
|
|
1734
|
-
const designFile =
|
|
1735
|
-
const tasksFile =
|
|
2155
|
+
const changeId = path5.basename(changeWorkspaceDir);
|
|
2156
|
+
const proposalFile = path5.join(changeWorkspaceDir, "proposal.md");
|
|
2157
|
+
const specFile = path5.join(changeWorkspaceDir, "spec.md");
|
|
2158
|
+
const designFile = path5.join(changeWorkspaceDir, "design.md");
|
|
2159
|
+
const tasksFile = path5.join(changeWorkspaceDir, "tasks.md");
|
|
1736
2160
|
return {
|
|
1737
2161
|
framework: "openspec",
|
|
1738
2162
|
changeId,
|
|
1739
2163
|
changeDir: changeWorkspaceDir,
|
|
1740
2164
|
profile,
|
|
1741
|
-
proposalFile:
|
|
1742
|
-
specFile:
|
|
1743
|
-
designFile:
|
|
1744
|
-
tasksFile:
|
|
2165
|
+
proposalFile: fs8.existsSync(proposalFile) ? proposalFile : void 0,
|
|
2166
|
+
specFile: fs8.existsSync(specFile) ? specFile : void 0,
|
|
2167
|
+
designFile: fs8.existsSync(designFile) ? designFile : void 0,
|
|
2168
|
+
tasksFile: fs8.existsSync(tasksFile) ? tasksFile : void 0,
|
|
1745
2169
|
sidecarFile,
|
|
1746
2170
|
handoff: handoff || void 0
|
|
1747
2171
|
};
|
|
@@ -1749,28 +2173,28 @@ function buildOpenSpecWorkspaceInfo(changeWorkspaceDir, profile, handoff, sideca
|
|
|
1749
2173
|
var OpenSpecAdapter = class {
|
|
1750
2174
|
framework = "openspec";
|
|
1751
2175
|
getChangeWorkspaceDir(rootDir, changeId) {
|
|
1752
|
-
const openspecDir =
|
|
1753
|
-
if (
|
|
1754
|
-
const activeChangeDir =
|
|
1755
|
-
if (
|
|
1756
|
-
const completedChangeDir =
|
|
1757
|
-
if (
|
|
2176
|
+
const openspecDir = path5.join(rootDir, "openspec", "changes", changeId);
|
|
2177
|
+
if (fs8.existsSync(openspecDir)) return openspecDir;
|
|
2178
|
+
const activeChangeDir = path5.join(rootDir, "specs", "changes", "active", changeId);
|
|
2179
|
+
if (fs8.existsSync(activeChangeDir)) return activeChangeDir;
|
|
2180
|
+
const completedChangeDir = path5.join(rootDir, "specs", "changes", "completed", changeId);
|
|
2181
|
+
if (fs8.existsSync(completedChangeDir)) return completedChangeDir;
|
|
1758
2182
|
return activeChangeDir;
|
|
1759
2183
|
}
|
|
1760
2184
|
depositSidecar(options) {
|
|
1761
2185
|
const changeDir = this.getChangeWorkspaceDir(options.rootDir, options.changeId);
|
|
1762
|
-
if (!
|
|
1763
|
-
|
|
2186
|
+
if (!fs8.existsSync(changeDir)) {
|
|
2187
|
+
fs8.mkdirSync(changeDir, { recursive: true });
|
|
1764
2188
|
}
|
|
1765
2189
|
const handoff = normalizeHandoffPayload(options.handoff, options.changeId);
|
|
1766
2190
|
const filename = options.format === "json" ? "handoff.json" : "handoff.yaml";
|
|
1767
|
-
const filePath =
|
|
2191
|
+
const filePath = path5.join(changeDir, filename);
|
|
1768
2192
|
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) : yaml3.dump(handoff, { indent: 2, lineWidth: -1 });
|
|
1769
|
-
|
|
2193
|
+
fs8.writeFileSync(filePath, content, "utf-8");
|
|
1770
2194
|
return filePath;
|
|
1771
2195
|
}
|
|
1772
2196
|
loadSidecar(changeWorkspaceDir) {
|
|
1773
|
-
if (!
|
|
2197
|
+
if (!fs8.existsSync(changeWorkspaceDir)) return null;
|
|
1774
2198
|
const candidateFiles = [
|
|
1775
2199
|
"handoff.yaml",
|
|
1776
2200
|
"handoff.yml",
|
|
@@ -1781,10 +2205,10 @@ var OpenSpecAdapter = class {
|
|
|
1781
2205
|
"product-handoff.json"
|
|
1782
2206
|
];
|
|
1783
2207
|
for (const file of candidateFiles) {
|
|
1784
|
-
const fullPath =
|
|
1785
|
-
if (
|
|
2208
|
+
const fullPath = path5.join(changeWorkspaceDir, file);
|
|
2209
|
+
if (fs8.existsSync(fullPath)) {
|
|
1786
2210
|
try {
|
|
1787
|
-
const raw =
|
|
2211
|
+
const raw = fs8.readFileSync(fullPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1788
2212
|
const parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml3.load(raw);
|
|
1789
2213
|
if (parsed && typeof parsed === "object" && parsed.id && String(parsed.id).startsWith("HOF-")) {
|
|
1790
2214
|
return parsed;
|
|
@@ -1796,7 +2220,7 @@ var OpenSpecAdapter = class {
|
|
|
1796
2220
|
return null;
|
|
1797
2221
|
}
|
|
1798
2222
|
validateWorkspace(changeWorkspaceDir) {
|
|
1799
|
-
if (!
|
|
2223
|
+
if (!fs8.existsSync(changeWorkspaceDir)) {
|
|
1800
2224
|
return {
|
|
1801
2225
|
valid: false,
|
|
1802
2226
|
errors: [`El directorio del cambio OpenSpec no existe: ${changeWorkspaceDir}`],
|
|
@@ -1805,7 +2229,7 @@ var OpenSpecAdapter = class {
|
|
|
1805
2229
|
}
|
|
1806
2230
|
const errors = [];
|
|
1807
2231
|
const warnings = [];
|
|
1808
|
-
const specFile =
|
|
2232
|
+
const specFile = path5.join(changeWorkspaceDir, "spec.md");
|
|
1809
2233
|
const profile = parseChangeProfile(specFile);
|
|
1810
2234
|
validateSpecFileContent(specFile, changeWorkspaceDir, errors);
|
|
1811
2235
|
const { handoff, sidecarFile } = validateOpenSpecStructure(profile, changeWorkspaceDir, this, errors, warnings);
|
|
@@ -1819,9 +2243,9 @@ var OpenSpecAdapter = class {
|
|
|
1819
2243
|
}
|
|
1820
2244
|
};
|
|
1821
2245
|
function parseChangeProfile2(specFile) {
|
|
1822
|
-
if (!
|
|
2246
|
+
if (!fs8.existsSync(specFile)) return "standard";
|
|
1823
2247
|
try {
|
|
1824
|
-
const rawSpec =
|
|
2248
|
+
const rawSpec = fs8.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1825
2249
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1826
2250
|
if (match) {
|
|
1827
2251
|
const parsedFm = yaml3.load(match[1]);
|
|
@@ -1843,12 +2267,12 @@ function checkSpecGherkinFormat2(parsedFm, rawSpec, changeWorkspaceDir, errors)
|
|
|
1843
2267
|
}
|
|
1844
2268
|
}
|
|
1845
2269
|
function validateSpecFileContent2(specFile, changeWorkspaceDir, errors) {
|
|
1846
|
-
if (!
|
|
2270
|
+
if (!fs8.existsSync(specFile)) {
|
|
1847
2271
|
errors.push(`Falta el archivo de especificaci\xF3n obligatorio 'spec.md' en ${changeWorkspaceDir}.`);
|
|
1848
2272
|
return;
|
|
1849
2273
|
}
|
|
1850
2274
|
try {
|
|
1851
|
-
const rawSpec =
|
|
2275
|
+
const rawSpec = fs8.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
1852
2276
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1853
2277
|
if (match) {
|
|
1854
2278
|
const parsedFm = yaml3.load(match[1]);
|
|
@@ -1867,19 +2291,19 @@ function validateHandoffSidecar2(handoff, changeWorkspaceDir, errors) {
|
|
|
1867
2291
|
if (!handoff.subgraph || !Array.isArray(handoff.subgraph.requirements)) {
|
|
1868
2292
|
errors.push(`El sidecar '${handoff.id}' no define un subgrafo de requerimientos v\xE1lido.`);
|
|
1869
2293
|
}
|
|
1870
|
-
return
|
|
2294
|
+
return path5.join(changeWorkspaceDir, "handoff.yaml");
|
|
1871
2295
|
}
|
|
1872
2296
|
function validateSpecKitStructure(profile, changeWorkspaceDir, adapter, errors, warnings) {
|
|
1873
|
-
const planFile =
|
|
1874
|
-
const tasksFile =
|
|
2297
|
+
const planFile = path5.join(changeWorkspaceDir, "plan.md");
|
|
2298
|
+
const tasksFile = path5.join(changeWorkspaceDir, "tasks.md");
|
|
1875
2299
|
if (profile === "patch") {
|
|
1876
2300
|
const handoff2 = adapter.loadSidecar(changeWorkspaceDir);
|
|
1877
|
-
return { handoff: handoff2, sidecarFile: handoff2 ?
|
|
2301
|
+
return { handoff: handoff2, sidecarFile: handoff2 ? path5.join(changeWorkspaceDir, "handoff.yaml") : void 0 };
|
|
1878
2302
|
}
|
|
1879
|
-
if (!
|
|
2303
|
+
if (!fs8.existsSync(tasksFile)) {
|
|
1880
2304
|
errors.push(`Falta el archivo de tareas obligatorio 'tasks.md' en ${changeWorkspaceDir}.`);
|
|
1881
2305
|
}
|
|
1882
|
-
if (!
|
|
2306
|
+
if (!fs8.existsSync(planFile)) {
|
|
1883
2307
|
warnings.push(`Se recomienda incluir 'plan.md' en el espacio de trabajo Spec Kit.`);
|
|
1884
2308
|
}
|
|
1885
2309
|
const handoff = adapter.loadSidecar(changeWorkspaceDir);
|
|
@@ -1887,18 +2311,18 @@ function validateSpecKitStructure(profile, changeWorkspaceDir, adapter, errors,
|
|
|
1887
2311
|
return { handoff, sidecarFile };
|
|
1888
2312
|
}
|
|
1889
2313
|
function buildSpecKitWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecarFile) {
|
|
1890
|
-
const changeId =
|
|
1891
|
-
const specFile =
|
|
1892
|
-
const planFile =
|
|
1893
|
-
const tasksFile =
|
|
2314
|
+
const changeId = path5.basename(changeWorkspaceDir);
|
|
2315
|
+
const specFile = path5.join(changeWorkspaceDir, "spec.md");
|
|
2316
|
+
const planFile = path5.join(changeWorkspaceDir, "plan.md");
|
|
2317
|
+
const tasksFile = path5.join(changeWorkspaceDir, "tasks.md");
|
|
1894
2318
|
return {
|
|
1895
2319
|
framework: "speckit",
|
|
1896
2320
|
changeId,
|
|
1897
2321
|
changeDir: changeWorkspaceDir,
|
|
1898
2322
|
profile,
|
|
1899
|
-
specFile:
|
|
1900
|
-
designFile:
|
|
1901
|
-
tasksFile:
|
|
2323
|
+
specFile: fs8.existsSync(specFile) ? specFile : void 0,
|
|
2324
|
+
designFile: fs8.existsSync(planFile) ? planFile : void 0,
|
|
2325
|
+
tasksFile: fs8.existsSync(tasksFile) ? tasksFile : void 0,
|
|
1902
2326
|
sidecarFile,
|
|
1903
2327
|
handoff: handoff || void 0
|
|
1904
2328
|
};
|
|
@@ -1906,18 +2330,18 @@ function buildSpecKitWorkspaceInfo(changeWorkspaceDir, profile, handoff, sidecar
|
|
|
1906
2330
|
var SpecKitAdapter = class {
|
|
1907
2331
|
framework = "speckit";
|
|
1908
2332
|
getChangeWorkspaceDir(rootDir, changeId) {
|
|
1909
|
-
const standardDir =
|
|
1910
|
-
if (
|
|
1911
|
-
const activeChangeDir =
|
|
1912
|
-
if (
|
|
1913
|
-
const completedChangeDir =
|
|
1914
|
-
if (
|
|
2333
|
+
const standardDir = path5.join(rootDir, "specs", changeId);
|
|
2334
|
+
if (fs8.existsSync(standardDir)) return standardDir;
|
|
2335
|
+
const activeChangeDir = path5.join(rootDir, "specs", "changes", "active", changeId);
|
|
2336
|
+
if (fs8.existsSync(activeChangeDir)) return activeChangeDir;
|
|
2337
|
+
const completedChangeDir = path5.join(rootDir, "specs", "changes", "completed", changeId);
|
|
2338
|
+
if (fs8.existsSync(completedChangeDir)) return completedChangeDir;
|
|
1915
2339
|
return standardDir;
|
|
1916
2340
|
}
|
|
1917
2341
|
depositSidecar(options) {
|
|
1918
2342
|
const changeDir = this.getChangeWorkspaceDir(options.rootDir, options.changeId);
|
|
1919
|
-
if (!
|
|
1920
|
-
|
|
2343
|
+
if (!fs8.existsSync(changeDir)) {
|
|
2344
|
+
fs8.mkdirSync(changeDir, { recursive: true });
|
|
1921
2345
|
}
|
|
1922
2346
|
const handoff = { ...options.handoff };
|
|
1923
2347
|
if (!handoff.id.startsWith("HOF-")) {
|
|
@@ -1930,20 +2354,20 @@ var SpecKitAdapter = class {
|
|
|
1930
2354
|
handoff.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1931
2355
|
}
|
|
1932
2356
|
const filename = options.format === "json" ? "handoff.json" : "handoff.yaml";
|
|
1933
|
-
const filePath =
|
|
2357
|
+
const filePath = path5.join(changeDir, filename);
|
|
1934
2358
|
const content = options.format === "json" ? JSON.stringify(handoff, null, 2) : yaml3.dump(handoff, { indent: 2, lineWidth: -1 });
|
|
1935
|
-
|
|
1936
|
-
const specifyMemoryDir =
|
|
1937
|
-
if (
|
|
1938
|
-
if (!
|
|
1939
|
-
|
|
2359
|
+
fs8.writeFileSync(filePath, content, "utf-8");
|
|
2360
|
+
const specifyMemoryDir = path5.join(options.rootDir, ".specify", "memory");
|
|
2361
|
+
if (fs8.existsSync(path5.join(options.rootDir, ".specify"))) {
|
|
2362
|
+
if (!fs8.existsSync(specifyMemoryDir)) {
|
|
2363
|
+
fs8.mkdirSync(specifyMemoryDir, { recursive: true });
|
|
1940
2364
|
}
|
|
1941
|
-
|
|
2365
|
+
fs8.writeFileSync(path5.join(specifyMemoryDir, `${options.changeId}.handoff.yaml`), content, "utf-8");
|
|
1942
2366
|
}
|
|
1943
2367
|
return filePath;
|
|
1944
2368
|
}
|
|
1945
2369
|
loadSidecar(changeWorkspaceDir) {
|
|
1946
|
-
if (!
|
|
2370
|
+
if (!fs8.existsSync(changeWorkspaceDir)) return null;
|
|
1947
2371
|
const candidateFiles = [
|
|
1948
2372
|
"handoff.yaml",
|
|
1949
2373
|
"handoff.yml",
|
|
@@ -1953,10 +2377,10 @@ var SpecKitAdapter = class {
|
|
|
1953
2377
|
"handoff.json"
|
|
1954
2378
|
];
|
|
1955
2379
|
for (const file of candidateFiles) {
|
|
1956
|
-
const fullPath =
|
|
1957
|
-
if (
|
|
2380
|
+
const fullPath = path5.join(changeWorkspaceDir, file);
|
|
2381
|
+
if (fs8.existsSync(fullPath)) {
|
|
1958
2382
|
try {
|
|
1959
|
-
const raw =
|
|
2383
|
+
const raw = fs8.readFileSync(fullPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1960
2384
|
const parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml3.load(raw);
|
|
1961
2385
|
if (parsed && typeof parsed === "object" && parsed.id && String(parsed.id).startsWith("HOF-")) {
|
|
1962
2386
|
return parsed;
|
|
@@ -1968,7 +2392,7 @@ var SpecKitAdapter = class {
|
|
|
1968
2392
|
return null;
|
|
1969
2393
|
}
|
|
1970
2394
|
validateWorkspace(changeWorkspaceDir) {
|
|
1971
|
-
if (!
|
|
2395
|
+
if (!fs8.existsSync(changeWorkspaceDir)) {
|
|
1972
2396
|
return {
|
|
1973
2397
|
valid: false,
|
|
1974
2398
|
errors: [`El directorio del cambio Spec Kit no existe: ${changeWorkspaceDir}`],
|
|
@@ -1977,7 +2401,7 @@ var SpecKitAdapter = class {
|
|
|
1977
2401
|
}
|
|
1978
2402
|
const errors = [];
|
|
1979
2403
|
const warnings = [];
|
|
1980
|
-
const specFile =
|
|
2404
|
+
const specFile = path5.join(changeWorkspaceDir, "spec.md");
|
|
1981
2405
|
const profile = parseChangeProfile2(specFile);
|
|
1982
2406
|
validateSpecFileContent2(specFile, changeWorkspaceDir, errors);
|
|
1983
2407
|
const { handoff, sidecarFile } = validateSpecKitStructure(profile, changeWorkspaceDir, this, errors, warnings);
|
|
@@ -2149,27 +2573,27 @@ ${frontmatter}---
|
|
|
2149
2573
|
${body}`;
|
|
2150
2574
|
}
|
|
2151
2575
|
function materializeCanonicalArtifact(rootDir, item, changeId, author, date) {
|
|
2152
|
-
const targetPath =
|
|
2153
|
-
if (
|
|
2154
|
-
|
|
2576
|
+
const targetPath = path5.join(rootDir, item.targetDir, `${item.id}.md`);
|
|
2577
|
+
if (fs8.existsSync(targetPath)) return targetPath;
|
|
2578
|
+
fs8.mkdirSync(path5.dirname(targetPath), { recursive: true });
|
|
2155
2579
|
const initialContent = buildCanonicalInitialContent(item, changeId, author, date);
|
|
2156
|
-
|
|
2580
|
+
fs8.writeFileSync(targetPath, initialContent, "utf-8");
|
|
2157
2581
|
return targetPath;
|
|
2158
2582
|
}
|
|
2159
2583
|
function findChangeWorkspaceDir(rootDir, changeId) {
|
|
2160
|
-
const activeDir =
|
|
2161
|
-
if (
|
|
2162
|
-
const completedDir =
|
|
2163
|
-
if (
|
|
2164
|
-
const openspecActive =
|
|
2165
|
-
if (
|
|
2584
|
+
const activeDir = path5.join(rootDir, "specs", "changes", "active", changeId);
|
|
2585
|
+
if (fs8.existsSync(activeDir)) return activeDir;
|
|
2586
|
+
const completedDir = path5.join(rootDir, "specs", "changes", "completed", changeId);
|
|
2587
|
+
if (fs8.existsSync(completedDir)) return completedDir;
|
|
2588
|
+
const openspecActive = path5.join(rootDir, "openspec", "changes", changeId);
|
|
2589
|
+
if (fs8.existsSync(openspecActive)) return openspecActive;
|
|
2166
2590
|
return null;
|
|
2167
2591
|
}
|
|
2168
2592
|
function checkTasksExecution(changeDir, changeId) {
|
|
2169
|
-
const tasksFile =
|
|
2593
|
+
const tasksFile = path5.join(changeDir, "tasks.md");
|
|
2170
2594
|
const allTasks = [];
|
|
2171
|
-
if (
|
|
2172
|
-
allTasks.push(...parseTasksFromContent(
|
|
2595
|
+
if (fs8.existsSync(tasksFile)) {
|
|
2596
|
+
allTasks.push(...parseTasksFromContent(fs8.readFileSync(tasksFile, "utf-8")));
|
|
2173
2597
|
}
|
|
2174
2598
|
const completedTasks = allTasks.filter((t) => t.status === "COMPLETED").map((t) => t.id);
|
|
2175
2599
|
const pendingTasks = allTasks.filter((t) => t.status !== "COMPLETED").map((t) => t.id);
|
|
@@ -2201,9 +2625,9 @@ function buildSubgraphFromProposalIds(ids) {
|
|
|
2201
2625
|
function resolveChangeHandoff(changeDir, changeId) {
|
|
2202
2626
|
const sidecar = loadProductHandoffSidecar(changeDir);
|
|
2203
2627
|
if (sidecar) return sidecar;
|
|
2204
|
-
const proposalFile =
|
|
2205
|
-
if (!
|
|
2206
|
-
const propContent =
|
|
2628
|
+
const proposalFile = path5.join(changeDir, "proposal.md");
|
|
2629
|
+
if (!fs8.existsSync(proposalFile)) return null;
|
|
2630
|
+
const propContent = fs8.readFileSync(proposalFile, "utf-8");
|
|
2207
2631
|
const { frontmatter } = parseFrontmatter(propContent);
|
|
2208
2632
|
const citations = Array.isArray(frontmatter.citations) ? frontmatter.citations : [];
|
|
2209
2633
|
const ids = citations.map((c) => c.id || "").filter(Boolean);
|
|
@@ -2217,9 +2641,9 @@ function resolveChangeHandoff(changeDir, changeId) {
|
|
|
2217
2641
|
}
|
|
2218
2642
|
function materializeNewCatalogArtifacts(changeDir, rootDir, changeId, author, date, errors) {
|
|
2219
2643
|
const created = [];
|
|
2220
|
-
const specFile =
|
|
2221
|
-
if (!
|
|
2222
|
-
const specContent =
|
|
2644
|
+
const specFile = path5.join(changeDir, "spec.md");
|
|
2645
|
+
if (!fs8.existsSync(specFile)) return created;
|
|
2646
|
+
const specContent = fs8.readFileSync(specFile, "utf-8");
|
|
2223
2647
|
const { newItems } = parseSpecCatalogScope(specContent);
|
|
2224
2648
|
for (const item of newItems) {
|
|
2225
2649
|
try {
|
|
@@ -2236,10 +2660,10 @@ function loadCanonicalArtifacts(rootDir) {
|
|
|
2236
2660
|
const allMdFiles = walkMdFiles(rootDir);
|
|
2237
2661
|
const canonicalArtifacts = /* @__PURE__ */ new Map();
|
|
2238
2662
|
for (const file of allMdFiles) {
|
|
2239
|
-
const relFile =
|
|
2663
|
+
const relFile = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
2240
2664
|
if (relFile.includes("specs/changes/") || relFile.includes("templates/")) continue;
|
|
2241
2665
|
try {
|
|
2242
|
-
const content =
|
|
2666
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
2243
2667
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
2244
2668
|
if (frontmatter.id) canonicalArtifacts.set(String(frontmatter.id), { file, frontmatter, body });
|
|
2245
2669
|
} catch {
|
|
@@ -2252,7 +2676,7 @@ function writeUpdatedArtifact(file, frontmatter, body) {
|
|
|
2252
2676
|
${yaml3.dump(frontmatter, { indent: 2, lineWidth: -1 }).trim()}
|
|
2253
2677
|
---
|
|
2254
2678
|
`;
|
|
2255
|
-
|
|
2679
|
+
fs8.writeFileSync(file, yamlHeader + body, "utf-8");
|
|
2256
2680
|
}
|
|
2257
2681
|
function updateSingleCanonicalRequirement(canonical, completedTasks, changeId, date, author) {
|
|
2258
2682
|
const fm = canonical.frontmatter;
|
|
@@ -2368,28 +2792,28 @@ function updateArchitectureComponents(citedComponents, canonicalArtifacts, targe
|
|
|
2368
2792
|
return updated;
|
|
2369
2793
|
}
|
|
2370
2794
|
function moveToCompletedFolder(changeDir, changeId, rootDir) {
|
|
2371
|
-
const completedParent =
|
|
2372
|
-
if (!
|
|
2373
|
-
const destDir =
|
|
2374
|
-
if (
|
|
2375
|
-
|
|
2376
|
-
const destProposal =
|
|
2377
|
-
if (
|
|
2378
|
-
const { frontmatter, body } = parseFrontmatter(
|
|
2795
|
+
const completedParent = path5.join(rootDir, "specs", "changes", "completed");
|
|
2796
|
+
if (!fs8.existsSync(completedParent)) fs8.mkdirSync(completedParent, { recursive: true });
|
|
2797
|
+
const destDir = path5.join(completedParent, changeId);
|
|
2798
|
+
if (fs8.existsSync(destDir)) fs8.rmSync(destDir, { recursive: true, force: true });
|
|
2799
|
+
fs8.renameSync(changeDir, destDir);
|
|
2800
|
+
const destProposal = path5.join(destDir, "proposal.md");
|
|
2801
|
+
if (fs8.existsSync(destProposal)) {
|
|
2802
|
+
const { frontmatter, body } = parseFrontmatter(fs8.readFileSync(destProposal, "utf-8"));
|
|
2379
2803
|
frontmatter.status = "applied";
|
|
2380
2804
|
writeUpdatedArtifact(destProposal, frontmatter, body);
|
|
2381
2805
|
}
|
|
2382
2806
|
return destDir;
|
|
2383
2807
|
}
|
|
2384
2808
|
function archiveChangeWorkspace(changeDir, changeId, rootDir, autoArchive, completedAll) {
|
|
2385
|
-
const isActiveFolder = changeDir.includes("specs" +
|
|
2809
|
+
const isActiveFolder = changeDir.includes("specs" + path5.sep + "changes" + path5.sep + "active");
|
|
2386
2810
|
if (completedAll && autoArchive && isActiveFolder) {
|
|
2387
2811
|
const destDir = moveToCompletedFolder(changeDir, changeId, rootDir);
|
|
2388
2812
|
return { archived: true, archivedPath: destDir };
|
|
2389
2813
|
}
|
|
2390
|
-
const proposalFile =
|
|
2391
|
-
if (
|
|
2392
|
-
const { frontmatter, body } = parseFrontmatter(
|
|
2814
|
+
const proposalFile = path5.join(changeDir, "proposal.md");
|
|
2815
|
+
if (fs8.existsSync(proposalFile)) {
|
|
2816
|
+
const { frontmatter, body } = parseFrontmatter(fs8.readFileSync(proposalFile, "utf-8"));
|
|
2393
2817
|
if (completedAll) frontmatter.status = "completed";
|
|
2394
2818
|
writeUpdatedArtifact(proposalFile, frontmatter, body);
|
|
2395
2819
|
}
|
|
@@ -2454,9 +2878,9 @@ function collectSubgraphPromotableIds(subgraph) {
|
|
|
2454
2878
|
return Array.from(new Set(combined));
|
|
2455
2879
|
}
|
|
2456
2880
|
function collectSpecAffectedNonArchIds(changeDir) {
|
|
2457
|
-
const specFile =
|
|
2458
|
-
if (!
|
|
2459
|
-
const { affectedItems } = parseSpecCatalogScope(
|
|
2881
|
+
const specFile = path5.join(changeDir, "spec.md");
|
|
2882
|
+
if (!fs8.existsSync(specFile)) return [];
|
|
2883
|
+
const { affectedItems } = parseSpecCatalogScope(fs8.readFileSync(specFile, "utf-8"));
|
|
2460
2884
|
return affectedItems.map((item) => item.id).filter((id) => !isArchitectureComponentId(id));
|
|
2461
2885
|
}
|
|
2462
2886
|
function resolveIntegrationOptions(options) {
|
|
@@ -2530,8 +2954,8 @@ function resolveAuditStatus(integratedCount, targetCount, gapsCount) {
|
|
|
2530
2954
|
function auditChangeEntry(changeDir, changeName, canonicalMap) {
|
|
2531
2955
|
const handoff = loadProductHandoffSidecar(changeDir);
|
|
2532
2956
|
if (!handoff) return null;
|
|
2533
|
-
const tasksFile =
|
|
2534
|
-
const tasks =
|
|
2957
|
+
const tasksFile = path5.join(changeDir, "tasks.md");
|
|
2958
|
+
const tasks = fs8.existsSync(tasksFile) ? parseTasksFromContent(fs8.readFileSync(tasksFile, "utf-8")) : [];
|
|
2535
2959
|
const completed = tasks.filter((t) => t.status === "COMPLETED");
|
|
2536
2960
|
const targetReqs = collectSubgraphPromotableIds(handoff.subgraph);
|
|
2537
2961
|
const { integrated, missing, gaps } = auditChangeRequirements(targetReqs, canonicalMap);
|
|
@@ -2547,11 +2971,11 @@ function auditChangeEntry(changeDir, changeName, canonicalMap) {
|
|
|
2547
2971
|
};
|
|
2548
2972
|
}
|
|
2549
2973
|
function collectParentDirAudits(parent, canonicalMap, audits, errors) {
|
|
2550
|
-
if (!
|
|
2551
|
-
const entries =
|
|
2974
|
+
if (!fs8.existsSync(parent)) return;
|
|
2975
|
+
const entries = fs8.readdirSync(parent, { withFileTypes: true });
|
|
2552
2976
|
for (const e of entries) {
|
|
2553
2977
|
if (!e.isDirectory()) continue;
|
|
2554
|
-
const audit = auditChangeEntry(
|
|
2978
|
+
const audit = auditChangeEntry(path5.join(parent, e.name), e.name, canonicalMap);
|
|
2555
2979
|
if (audit) {
|
|
2556
2980
|
if (audit.gaps.length > 0) errors.push(`[${e.name}] Desalineaci\xF3n: ${audit.gaps.join("; ")}`);
|
|
2557
2981
|
audits.push(audit);
|
|
@@ -2563,8 +2987,8 @@ function verifySddIntegration(options = {}) {
|
|
|
2563
2987
|
const audits = [];
|
|
2564
2988
|
const errors = [];
|
|
2565
2989
|
const searchDirs = [
|
|
2566
|
-
|
|
2567
|
-
|
|
2990
|
+
path5.join(rootDir, "specs", "changes", "completed"),
|
|
2991
|
+
path5.join(rootDir, "specs", "changes", "active")
|
|
2568
2992
|
];
|
|
2569
2993
|
const canonicalMap = loadCanonicalArtifacts(rootDir);
|
|
2570
2994
|
for (const parent of searchDirs) {
|
|
@@ -2612,10 +3036,10 @@ function resolveMatchFromMetadata(activeFolders, options) {
|
|
|
2612
3036
|
return {};
|
|
2613
3037
|
}
|
|
2614
3038
|
function validateActiveDirectory(activeParent) {
|
|
2615
|
-
if (!
|
|
3039
|
+
if (!fs8.existsSync(activeParent)) {
|
|
2616
3040
|
return { ok: false, folders: [], error: `No existe el directorio de cambios activos en '${activeParent}'.` };
|
|
2617
3041
|
}
|
|
2618
|
-
const entries =
|
|
3042
|
+
const entries = fs8.readdirSync(activeParent, { withFileTypes: true });
|
|
2619
3043
|
const activeFolders = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2620
3044
|
if (activeFolders.length === 0) {
|
|
2621
3045
|
return { ok: false, folders: [], error: "No se encontraron carpetas de cambios activos en specs/changes/active/." };
|
|
@@ -2647,7 +3071,7 @@ function evaluateTasksProgress(changeId, tasks) {
|
|
|
2647
3071
|
}
|
|
2648
3072
|
function detectActiveChangeForIntegration(options = {}) {
|
|
2649
3073
|
const rootDir = options.rootDir || process.cwd();
|
|
2650
|
-
const activeParent =
|
|
3074
|
+
const activeParent = path5.join(rootDir, "specs", "changes", "active");
|
|
2651
3075
|
const dirCheck = validateActiveDirectory(activeParent);
|
|
2652
3076
|
if (!dirCheck.ok) {
|
|
2653
3077
|
return {
|
|
@@ -2662,9 +3086,9 @@ function detectActiveChangeForIntegration(options = {}) {
|
|
|
2662
3086
|
if (!match.changeId) {
|
|
2663
3087
|
return buildUnmatchedActiveResult(dirCheck.folders, options);
|
|
2664
3088
|
}
|
|
2665
|
-
const changeDir =
|
|
2666
|
-
const tasksFile =
|
|
2667
|
-
const tasks =
|
|
3089
|
+
const changeDir = path5.join(activeParent, match.changeId);
|
|
3090
|
+
const tasksFile = path5.join(changeDir, "tasks.md");
|
|
3091
|
+
const tasks = fs8.existsSync(tasksFile) ? parseTasksFromContent(fs8.readFileSync(tasksFile, "utf-8")) : [];
|
|
2668
3092
|
const progress = evaluateTasksProgress(match.changeId, tasks);
|
|
2669
3093
|
const reasons = match.reason ? [match.reason] : [];
|
|
2670
3094
|
if (progress.error) reasons.push(progress.error);
|
|
@@ -2679,9 +3103,46 @@ function detectActiveChangeForIntegration(options = {}) {
|
|
|
2679
3103
|
reasons
|
|
2680
3104
|
};
|
|
2681
3105
|
}
|
|
2682
|
-
function
|
|
3106
|
+
function extractSemanticContent(content) {
|
|
3107
|
+
let normalized = content.replace(/\r\n/g, "\n");
|
|
3108
|
+
normalized = normalized.replace(/(?:\n|^)##?\s+.*(?:Historial de Revisiones|Revision History)(?:[\s\S]*?(?=(?:\n##?\s+[^\n]+)|\s*$))/i, "");
|
|
3109
|
+
normalized = normalized.replace(/(?:\n|^)\|\s*\*?Versi[oó]n\*?\s*\|\s*Fecha\s*\|\s*Autor[\s\S]*?(?=(?:\n##?\s+[^\n]+)|\s*$)/i, "");
|
|
3110
|
+
return normalized.trimEnd() + "\n";
|
|
3111
|
+
}
|
|
3112
|
+
function slugify(text) {
|
|
3113
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
|
|
3114
|
+
}
|
|
3115
|
+
function matchesSlug(line, clean) {
|
|
3116
|
+
const headingText = line.replace(/^#+\s*/, "").trim();
|
|
3117
|
+
const slug = slugify(headingText);
|
|
3118
|
+
if (slug === clean || slug.includes(clean) || clean.includes(slug)) return true;
|
|
3119
|
+
const slugNoNums = slugify(headingText.replace(/^\d+[\.\)]\s*/, "").trim());
|
|
3120
|
+
return slugNoNums === clean || clean === slugNoNums;
|
|
3121
|
+
}
|
|
3122
|
+
function matchesHeading(content, clean) {
|
|
3123
|
+
for (const line of content.split(/\r?\n/)) {
|
|
3124
|
+
const trimmed = line.trim();
|
|
3125
|
+
if (trimmed.startsWith("#") && matchesSlug(trimmed, clean)) {
|
|
3126
|
+
return true;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return false;
|
|
3130
|
+
}
|
|
3131
|
+
function validateAnchorExistence(targetContent, anchor) {
|
|
3132
|
+
if (!anchor) return true;
|
|
3133
|
+
const clean = anchor.replace(/^#/, "").trim().toLowerCase();
|
|
3134
|
+
if (!clean || targetContent.toLowerCase().includes(clean)) return true;
|
|
3135
|
+
if (matchesHeading(targetContent, clean)) return true;
|
|
3136
|
+
const anchorRegex = new RegExp(`(?:id|name)=["']${clean}["']|\\{#${clean}\\}`, "i");
|
|
3137
|
+
return anchorRegex.test(targetContent);
|
|
3138
|
+
}
|
|
3139
|
+
function computeRawSha256(content) {
|
|
2683
3140
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
2684
|
-
return
|
|
3141
|
+
return crypto3.createHash("sha256").update(normalized, "utf-8").digest("hex");
|
|
3142
|
+
}
|
|
3143
|
+
function computeCanonicalSha256(content) {
|
|
3144
|
+
const semantic = extractSemanticContent(content);
|
|
3145
|
+
return crypto3.createHash("sha256").update(semantic, "utf-8").digest("hex");
|
|
2685
3146
|
}
|
|
2686
3147
|
function parseFrontmatterId(content) {
|
|
2687
3148
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -2771,7 +3232,7 @@ function buildPdacReportHeader(isOk, driftsCount, nodesCount) {
|
|
|
2771
3232
|
return [
|
|
2772
3233
|
`# \u{1F512} PDaC Cryptographic Matrix and Drift Detection (Anti-Drift)`,
|
|
2773
3234
|
``,
|
|
2774
|
-
`> **Evaluation Date:** ${(
|
|
3235
|
+
`> **Evaluation Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
2775
3236
|
`> **Verdict:** ${verdict}`,
|
|
2776
3237
|
``,
|
|
2777
3238
|
`---`,
|
|
@@ -2782,8 +3243,8 @@ function buildPdacReportHeader(isOk, driftsCount, nodesCount) {
|
|
|
2782
3243
|
`| :--- | :--- | :--- | :--- |`
|
|
2783
3244
|
];
|
|
2784
3245
|
}
|
|
2785
|
-
function generatePdacReportMarkdown(nodes, drifts, unresolvedCount) {
|
|
2786
|
-
const isOk = drifts.length === 0 && unresolvedCount === 0;
|
|
3246
|
+
function generatePdacReportMarkdown(nodes, drifts, unresolvedCount, invalidAnchors = []) {
|
|
3247
|
+
const isOk = drifts.length === 0 && unresolvedCount === 0 && invalidAnchors.length === 0;
|
|
2787
3248
|
const header = buildPdacReportHeader(isOk, drifts.length, nodes.length);
|
|
2788
3249
|
const nodeRows = nodes.map(
|
|
2789
3250
|
(n) => `| **\`${n.id}\`** | \`${n.type}\` | ${n.title || "Untitled"} | \`sha256:${n.digest.substring(0, 16)}...\` |`
|
|
@@ -2795,9 +3256,9 @@ function isPdacDeliveryDoc(relFile) {
|
|
|
2795
3256
|
}
|
|
2796
3257
|
function processPdacFile(file, rootDir, baselineMap, allCitations) {
|
|
2797
3258
|
try {
|
|
2798
|
-
const content =
|
|
3259
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
2799
3260
|
const { id, type, title } = parseFrontmatterId(content);
|
|
2800
|
-
const relFile =
|
|
3261
|
+
const relFile = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
2801
3262
|
if (id) {
|
|
2802
3263
|
const citations = extractCitations(content, relFile);
|
|
2803
3264
|
baselineMap.set(id, {
|
|
@@ -2806,6 +3267,7 @@ function processPdacFile(file, rootDir, baselineMap, allCitations) {
|
|
|
2806
3267
|
filePath: relFile,
|
|
2807
3268
|
title,
|
|
2808
3269
|
digest: computeCanonicalSha256(content),
|
|
3270
|
+
rawDigest: computeRawSha256(content),
|
|
2809
3271
|
rawContent: content,
|
|
2810
3272
|
citations: citations.map((c) => ({
|
|
2811
3273
|
targetId: c.targetId,
|
|
@@ -2829,38 +3291,65 @@ function scanPdacWorkspace(rootDir) {
|
|
|
2829
3291
|
}
|
|
2830
3292
|
return { baselineMap, allCitations };
|
|
2831
3293
|
}
|
|
3294
|
+
function checkAnchor(citation, targetNode, invalidAnchors) {
|
|
3295
|
+
if (citation.anchor && targetNode.rawContent) {
|
|
3296
|
+
if (!validateAnchorExistence(targetNode.rawContent, citation.anchor)) {
|
|
3297
|
+
invalidAnchors.push({
|
|
3298
|
+
sourceFile: citation.sourceFile,
|
|
3299
|
+
targetId: citation.targetId,
|
|
3300
|
+
anchor: citation.anchor,
|
|
3301
|
+
reason: `Anchor '${citation.anchor}' not found in target artifact '${citation.targetId}' (${targetNode.filePath})`
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
function checkDigest(citation, targetNode, drifts) {
|
|
3307
|
+
if (!citation.expectedDigest) return;
|
|
3308
|
+
const matches = citation.expectedDigest === targetNode.digest || targetNode.rawDigest !== void 0 && citation.expectedDigest === targetNode.rawDigest;
|
|
3309
|
+
if (!matches) {
|
|
3310
|
+
drifts.push({
|
|
3311
|
+
sourceId: citation.sourceFile,
|
|
3312
|
+
targetId: citation.targetId,
|
|
3313
|
+
sourceFile: citation.sourceFile,
|
|
3314
|
+
expectedDigest: citation.expectedDigest,
|
|
3315
|
+
actualDigest: targetNode.digest
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
2832
3319
|
function computePdacDrifts(allCitations, baselineMap) {
|
|
2833
3320
|
const drifts = [];
|
|
3321
|
+
const unresolvedCitations = [];
|
|
3322
|
+
const invalidAnchors = [];
|
|
2834
3323
|
let unresolvedCount = 0;
|
|
2835
3324
|
for (const citation of allCitations) {
|
|
2836
3325
|
const targetNode = baselineMap.get(citation.targetId);
|
|
2837
3326
|
if (!targetNode) {
|
|
2838
3327
|
unresolvedCount++;
|
|
2839
|
-
|
|
2840
|
-
}
|
|
2841
|
-
if (citation.expectedDigest && citation.expectedDigest !== targetNode.digest) {
|
|
2842
|
-
drifts.push({
|
|
2843
|
-
sourceId: citation.sourceFile,
|
|
2844
|
-
targetId: citation.targetId,
|
|
3328
|
+
unresolvedCitations.push({
|
|
2845
3329
|
sourceFile: citation.sourceFile,
|
|
2846
|
-
|
|
2847
|
-
actualDigest: targetNode.digest
|
|
3330
|
+
targetId: citation.targetId
|
|
2848
3331
|
});
|
|
3332
|
+
continue;
|
|
2849
3333
|
}
|
|
3334
|
+
checkAnchor(citation, targetNode, invalidAnchors);
|
|
3335
|
+
checkDigest(citation, targetNode, drifts);
|
|
2850
3336
|
}
|
|
2851
|
-
return { drifts, unresolvedCount };
|
|
3337
|
+
return { drifts, unresolvedCount, unresolvedCitations, invalidAnchors };
|
|
2852
3338
|
}
|
|
2853
3339
|
function verifyPdacGraph(options = {}) {
|
|
2854
3340
|
const rootDir = options.rootDir || process.cwd();
|
|
2855
3341
|
const { baselineMap, allCitations } = scanPdacWorkspace(rootDir);
|
|
2856
|
-
const { drifts, unresolvedCount } = computePdacDrifts(allCitations, baselineMap);
|
|
3342
|
+
const { drifts, unresolvedCount, unresolvedCitations, invalidAnchors } = computePdacDrifts(allCitations, baselineMap);
|
|
2857
3343
|
const nodes = Array.from(baselineMap.values());
|
|
2858
|
-
const reportMarkdown = generatePdacReportMarkdown(nodes, drifts, unresolvedCount);
|
|
3344
|
+
const reportMarkdown = generatePdacReportMarkdown(nodes, drifts, unresolvedCount, invalidAnchors);
|
|
2859
3345
|
return {
|
|
2860
|
-
success: drifts.length === 0,
|
|
3346
|
+
success: drifts.length === 0 && unresolvedCount === 0 && invalidAnchors.length === 0,
|
|
2861
3347
|
totalNodes: nodes.length,
|
|
2862
3348
|
totalEdges: allCitations.length,
|
|
2863
3349
|
drifts,
|
|
3350
|
+
unresolvedCount,
|
|
3351
|
+
unresolvedCitations,
|
|
3352
|
+
invalidAnchors,
|
|
2864
3353
|
reportMarkdown
|
|
2865
3354
|
};
|
|
2866
3355
|
}
|
|
@@ -2925,22 +3414,22 @@ function updateCitationsInContent(content, baselineMap, sourceFile, details) {
|
|
|
2925
3414
|
}
|
|
2926
3415
|
function collectCandidateFilesForSync(rootDir, targetPath) {
|
|
2927
3416
|
if (targetPath) {
|
|
2928
|
-
const fullTarget =
|
|
2929
|
-
if (!
|
|
2930
|
-
return
|
|
3417
|
+
const fullTarget = path5.isAbsolute(targetPath) ? targetPath : path5.join(rootDir, targetPath);
|
|
3418
|
+
if (!fs8.existsSync(fullTarget)) return [];
|
|
3419
|
+
return fs8.statSync(fullTarget).isDirectory() ? walkMdFiles(fullTarget) : [fullTarget];
|
|
2931
3420
|
}
|
|
2932
3421
|
return walkMdFiles(rootDir).filter((f) => {
|
|
2933
|
-
const relFile =
|
|
3422
|
+
const relFile = path5.relative(rootDir, f).replace(/\\/g, "/");
|
|
2934
3423
|
return isPdacDeliveryDoc(relFile);
|
|
2935
3424
|
});
|
|
2936
3425
|
}
|
|
2937
3426
|
function syncSingleFile(file, rootDir, baselineMap, details) {
|
|
2938
3427
|
try {
|
|
2939
|
-
const content =
|
|
2940
|
-
const relFile =
|
|
3428
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
3429
|
+
const relFile = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
2941
3430
|
const { updatedContent, changed } = updateCitationsInContent(content, baselineMap, relFile, details);
|
|
2942
3431
|
if (changed) {
|
|
2943
|
-
|
|
3432
|
+
fs8.writeFileSync(file, updatedContent, "utf-8");
|
|
2944
3433
|
return relFile;
|
|
2945
3434
|
}
|
|
2946
3435
|
} catch {
|
|
@@ -3143,6 +3632,36 @@ var STARTER_SCHEMAS = Object.entries(STARTER_SCHEMAS_MAP).map(([relPath, schema]
|
|
|
3143
3632
|
}));
|
|
3144
3633
|
|
|
3145
3634
|
// src/verifiers/schemas.ts
|
|
3635
|
+
var ajvInstance = new Ajv2020({
|
|
3636
|
+
allErrors: true,
|
|
3637
|
+
strict: false,
|
|
3638
|
+
validateFormats: true,
|
|
3639
|
+
verbose: true
|
|
3640
|
+
});
|
|
3641
|
+
addFormats(ajvInstance);
|
|
3642
|
+
var compiledSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
3643
|
+
function formatFieldPath(err, propPath) {
|
|
3644
|
+
const field = err.instancePath ? err.instancePath.replace(/^\//, "").replace(/\//g, ".") : propPath || "root";
|
|
3645
|
+
return propPath && field !== "root" && field ? `${propPath}.${field}` : field || "root";
|
|
3646
|
+
}
|
|
3647
|
+
var KEYWORD_FORMATTERS = {
|
|
3648
|
+
required: (p, _err, fullPath) => `'${fullPath}' missing required field '${String(p?.missingProperty)}'`,
|
|
3649
|
+
additionalProperties: (p, _err, fullPath) => `'${fullPath}' contains disallowed extra property '${String(p?.additionalProperty)}'`,
|
|
3650
|
+
minLength: (p, _err, fullPath) => `'${fullPath}' length must be >= ${String(p?.limit)}`,
|
|
3651
|
+
maxLength: (p, _err, fullPath) => `'${fullPath}' length must be <= ${String(p?.limit)}`,
|
|
3652
|
+
enum: (p, err, fullPath) => `'${fullPath}' must be one of [${Array.isArray(p?.allowedValues) ? p.allowedValues.join(", ") : ""}], received '${String(err.data)}'`,
|
|
3653
|
+
uniqueItems: (_p, _err, fullPath) => `'${fullPath}' contains duplicate items (must have unique items)`,
|
|
3654
|
+
pattern: (p, err, fullPath) => `'${fullPath}' value '${String(err.data)}' does not match pattern ${String(p?.pattern)}`
|
|
3655
|
+
};
|
|
3656
|
+
function formatKeywordMessage(err, fullPath) {
|
|
3657
|
+
const p = err.params;
|
|
3658
|
+
const formatter = KEYWORD_FORMATTERS[err.keyword];
|
|
3659
|
+
return formatter ? formatter(p, err, fullPath) : `'${fullPath}' ${err.message || "validation failed"}`;
|
|
3660
|
+
}
|
|
3661
|
+
function formatAjvError(err, propPath) {
|
|
3662
|
+
const fullPath = formatFieldPath(err, propPath);
|
|
3663
|
+
return formatKeywordMessage(err, fullPath);
|
|
3664
|
+
}
|
|
3146
3665
|
var TYPE_TO_SCHEMA_MAP = {
|
|
3147
3666
|
// Product
|
|
3148
3667
|
actor: "schemas/product/actor.schema.json",
|
|
@@ -3287,10 +3806,10 @@ var EXCLUDED_SCHEMA_DIRS = /* @__PURE__ */ new Set([
|
|
|
3287
3806
|
]);
|
|
3288
3807
|
function walkHandoffSidecars(dir, fileList = []) {
|
|
3289
3808
|
try {
|
|
3290
|
-
if (!
|
|
3291
|
-
for (const file of
|
|
3292
|
-
const fullPath =
|
|
3293
|
-
if (
|
|
3809
|
+
if (!fs8.existsSync(dir)) return fileList;
|
|
3810
|
+
for (const file of fs8.readdirSync(dir)) {
|
|
3811
|
+
const fullPath = path5.join(dir, file);
|
|
3812
|
+
if (fs8.statSync(fullPath).isDirectory()) {
|
|
3294
3813
|
if (!EXCLUDED_SCHEMA_DIRS.has(file) && !file.startsWith("temp-") && !file.startsWith(".temp-")) {
|
|
3295
3814
|
walkHandoffSidecars(fullPath, fileList);
|
|
3296
3815
|
}
|
|
@@ -3313,157 +3832,41 @@ function extractArtifactData(filePath, content) {
|
|
|
3313
3832
|
}
|
|
3314
3833
|
return extractFrontmatter(content);
|
|
3315
3834
|
}
|
|
3316
|
-
function
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
function validateEnum(value, schema, currentProp) {
|
|
3323
|
-
if (schema.enum !== void 0 && !schema.enum.includes(value)) {
|
|
3324
|
-
return [`'${currentProp}' must be one of [${schema.enum.join(", ")}], received '${String(value)}'`];
|
|
3325
|
-
}
|
|
3326
|
-
return [];
|
|
3327
|
-
}
|
|
3328
|
-
function validateCombinators(value, schema, propPath, currentProp) {
|
|
3329
|
-
if (schema.oneOf) {
|
|
3330
|
-
const matchCount = schema.oneOf.filter((sub) => validateAgainstSchema(value, sub, propPath).length === 0).length;
|
|
3331
|
-
if (matchCount !== 1) {
|
|
3332
|
-
return [`'${currentProp}' must match exactly one sub-schema in oneOf`];
|
|
3333
|
-
}
|
|
3334
|
-
}
|
|
3335
|
-
if (schema.anyOf) {
|
|
3336
|
-
const anyMatched = schema.anyOf.some((sub) => validateAgainstSchema(value, sub, propPath).length === 0);
|
|
3337
|
-
if (!anyMatched) {
|
|
3338
|
-
return [`'${currentProp}' did not match any allowed alternative in anyOf`];
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
return [];
|
|
3342
|
-
}
|
|
3343
|
-
function getActualJsonType(value) {
|
|
3344
|
-
if (value === null) return "null";
|
|
3345
|
-
if (Array.isArray(value)) return "array";
|
|
3346
|
-
const t = typeof value;
|
|
3347
|
-
if (t === "number" && Number.isInteger(value)) return "integer";
|
|
3348
|
-
return t;
|
|
3349
|
-
}
|
|
3350
|
-
function validateType(value, schema, currentProp) {
|
|
3351
|
-
if (schema.type === void 0) return [];
|
|
3352
|
-
const allowed = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
3353
|
-
const actual = getActualJsonType(value);
|
|
3354
|
-
const matches = allowed.some((t) => t === actual || t === "number" && actual === "integer");
|
|
3355
|
-
if (!matches) {
|
|
3356
|
-
return [`'${currentProp}' expected type [${allowed.join("|")}], received '${actual}'`];
|
|
3835
|
+
function getOrCompileSchema(schema) {
|
|
3836
|
+
const schemaObj = schema;
|
|
3837
|
+
if (typeof schemaObj.$id === "string") {
|
|
3838
|
+
const existing = ajvInstance.getSchema(schemaObj.$id);
|
|
3839
|
+
if (existing) return existing;
|
|
3840
|
+
return ajvInstance.compile(schema);
|
|
3357
3841
|
}
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
errors.push(`'${currentProp}' length must be >= ${schema.minLength}`);
|
|
3364
|
-
}
|
|
3365
|
-
if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
|
|
3366
|
-
errors.push(`'${currentProp}' length must be <= ${schema.maxLength}`);
|
|
3367
|
-
}
|
|
3368
|
-
if (schema.pattern !== void 0 && !new RegExp(schema.pattern).test(value)) {
|
|
3369
|
-
errors.push(`'${currentProp}' value '${value}' does not match pattern ${schema.pattern}`);
|
|
3370
|
-
}
|
|
3371
|
-
if (schema.format === "date") {
|
|
3372
|
-
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
3373
|
-
if (!dateRegex.test(value) || isNaN(Date.parse(value))) {
|
|
3374
|
-
errors.push(`'${currentProp}' must be a valid ISO-8601 date string (YYYY-MM-DD)`);
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
return errors;
|
|
3378
|
-
}
|
|
3379
|
-
function validateNumber(value, schema, currentProp) {
|
|
3380
|
-
const errors = [];
|
|
3381
|
-
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
3382
|
-
errors.push(`'${currentProp}' must be >= ${schema.minimum}`);
|
|
3383
|
-
}
|
|
3384
|
-
if (schema.maximum !== void 0 && value > schema.maximum) {
|
|
3385
|
-
errors.push(`'${currentProp}' must be <= ${schema.maximum}`);
|
|
3386
|
-
}
|
|
3387
|
-
return errors;
|
|
3388
|
-
}
|
|
3389
|
-
function validateArray(value, schema, currentProp) {
|
|
3390
|
-
const errors = [];
|
|
3391
|
-
if (schema.minItems !== void 0 && value.length < schema.minItems) {
|
|
3392
|
-
errors.push(`'${currentProp}' items count must be >= ${schema.minItems}`);
|
|
3393
|
-
}
|
|
3394
|
-
if (schema.maxItems !== void 0 && value.length > schema.maxItems) {
|
|
3395
|
-
errors.push(`'${currentProp}' items count must be <= ${schema.maxItems}`);
|
|
3396
|
-
}
|
|
3397
|
-
if (schema.items) {
|
|
3398
|
-
value.forEach((item, idx) => {
|
|
3399
|
-
errors.push(...validateAgainstSchema(item, schema.items, `${currentProp}[${idx}]`));
|
|
3400
|
-
});
|
|
3401
|
-
}
|
|
3402
|
-
return errors;
|
|
3403
|
-
}
|
|
3404
|
-
function checkRequiredProperties(record, required, currentProp) {
|
|
3405
|
-
if (!required) return [];
|
|
3406
|
-
const errors = [];
|
|
3407
|
-
for (const reqKey of required) {
|
|
3408
|
-
if (!(reqKey in record) || record[reqKey] === void 0) {
|
|
3409
|
-
errors.push(`'${currentProp}' missing required field '${reqKey}'`);
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
return errors;
|
|
3413
|
-
}
|
|
3414
|
-
function checkObjectProperties(record, schema, propPath, currentProp) {
|
|
3415
|
-
const errors = [];
|
|
3416
|
-
const properties = schema.properties || {};
|
|
3417
|
-
for (const [key, propVal] of Object.entries(record)) {
|
|
3418
|
-
const subPath = propPath ? `${propPath}.${key}` : key;
|
|
3419
|
-
if (key in properties) {
|
|
3420
|
-
errors.push(...validateAgainstSchema(propVal, properties[key], subPath));
|
|
3421
|
-
} else if (schema.additionalProperties === false) {
|
|
3422
|
-
errors.push(`'${currentProp}' contains disallowed extra property '${key}'`);
|
|
3423
|
-
} else if (typeof schema.additionalProperties === "object") {
|
|
3424
|
-
errors.push(...validateAgainstSchema(propVal, schema.additionalProperties, subPath));
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
|
-
return errors;
|
|
3428
|
-
}
|
|
3429
|
-
function validateObject(record, schema, propPath, currentProp) {
|
|
3430
|
-
return [
|
|
3431
|
-
...checkRequiredProperties(record, schema.required, currentProp),
|
|
3432
|
-
...checkObjectProperties(record, schema, propPath, currentProp)
|
|
3433
|
-
];
|
|
3434
|
-
}
|
|
3435
|
-
function validateTypeSpecificRules(value, schema, propPath, currentProp) {
|
|
3436
|
-
if (typeof value === "string") return validateString(value, schema, currentProp);
|
|
3437
|
-
if (typeof value === "number") return validateNumber(value, schema, currentProp);
|
|
3438
|
-
if (Array.isArray(value)) return validateArray(value, schema, currentProp);
|
|
3439
|
-
if (typeof value === "object" && value !== null) {
|
|
3440
|
-
return validateObject(value, schema, propPath, currentProp);
|
|
3441
|
-
}
|
|
3442
|
-
return [];
|
|
3842
|
+
const cached = compiledSchemaCache.get(schema);
|
|
3843
|
+
if (cached) return cached;
|
|
3844
|
+
const compiled = ajvInstance.compile(schema);
|
|
3845
|
+
compiledSchemaCache.set(schema, compiled);
|
|
3846
|
+
return compiled;
|
|
3443
3847
|
}
|
|
3444
3848
|
function validateAgainstSchema(value, schema, propPath = "") {
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
return validateTypeSpecificRules(value, schema, propPath, currentProp);
|
|
3849
|
+
if (!schema || typeof schema !== "object") return [];
|
|
3850
|
+
try {
|
|
3851
|
+
const validate = getOrCompileSchema(schema);
|
|
3852
|
+
const valid = validate(value);
|
|
3853
|
+
if (valid || !validate.errors) return [];
|
|
3854
|
+
return validate.errors.map((err) => formatAjvError(err, propPath));
|
|
3855
|
+
} catch (err) {
|
|
3856
|
+
return [`Schema error: ${err instanceof Error ? err.message : String(err)}`];
|
|
3857
|
+
}
|
|
3455
3858
|
}
|
|
3456
3859
|
function findSchemaFile(relSchemaPath, rootDir) {
|
|
3457
|
-
const fullPath =
|
|
3458
|
-
if (
|
|
3459
|
-
const fallbackPath =
|
|
3460
|
-
if (
|
|
3860
|
+
const fullPath = path5.join(rootDir, relSchemaPath);
|
|
3861
|
+
if (fs8.existsSync(fullPath)) return fullPath;
|
|
3862
|
+
const fallbackPath = path5.join(process.cwd(), relSchemaPath);
|
|
3863
|
+
if (fs8.existsSync(fallbackPath)) return fallbackPath;
|
|
3461
3864
|
return null;
|
|
3462
3865
|
}
|
|
3463
3866
|
function loadJsonSchema(fullPath, relSchemaPath) {
|
|
3464
|
-
if (fullPath &&
|
|
3867
|
+
if (fullPath && fs8.existsSync(fullPath)) {
|
|
3465
3868
|
try {
|
|
3466
|
-
return JSON.parse(
|
|
3869
|
+
return JSON.parse(fs8.readFileSync(fullPath, "utf-8"));
|
|
3467
3870
|
} catch {
|
|
3468
3871
|
}
|
|
3469
3872
|
}
|
|
@@ -3478,7 +3881,7 @@ function resolveSchemaForArtifact(frontmatter, rootDir) {
|
|
|
3478
3881
|
}
|
|
3479
3882
|
function loadSchemaFromPathOrType(schemaOrType, rootDir) {
|
|
3480
3883
|
if (schemaOrType.endsWith(".json")) {
|
|
3481
|
-
const fullPath =
|
|
3884
|
+
const fullPath = path5.isAbsolute(schemaOrType) ? schemaOrType : findSchemaFile(schemaOrType, rootDir);
|
|
3482
3885
|
const relNormalized = schemaOrType.replace(/\\/g, "/");
|
|
3483
3886
|
const schema = loadJsonSchema(fullPath, relNormalized);
|
|
3484
3887
|
return { schema, schemaId: schemaOrType };
|
|
@@ -3523,9 +3926,9 @@ function collectCandidateSchemaFiles(options, rootDir) {
|
|
|
3523
3926
|
if (!options.targetPath) {
|
|
3524
3927
|
return [...walkMdFiles(rootDir), ...walkHandoffSidecars(rootDir)];
|
|
3525
3928
|
}
|
|
3526
|
-
const target =
|
|
3527
|
-
if (!
|
|
3528
|
-
if (
|
|
3929
|
+
const target = path5.isAbsolute(options.targetPath) ? options.targetPath : path5.join(rootDir, options.targetPath);
|
|
3930
|
+
if (!fs8.existsSync(target)) return [];
|
|
3931
|
+
if (fs8.statSync(target).isDirectory()) {
|
|
3529
3932
|
return [...walkMdFiles(target), ...walkHandoffSidecars(target)];
|
|
3530
3933
|
}
|
|
3531
3934
|
if (target.endsWith(".md") || target.endsWith(".yaml") || target.endsWith(".yml")) {
|
|
@@ -3576,7 +3979,7 @@ function buildEvaluationFailure(relPath, err) {
|
|
|
3576
3979
|
}
|
|
3577
3980
|
function evaluateArtifactFile(file, relPath, rootDir) {
|
|
3578
3981
|
try {
|
|
3579
|
-
const content =
|
|
3982
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
3580
3983
|
const frontmatter = extractArtifactData(file, content);
|
|
3581
3984
|
if (!frontmatter || !frontmatter.id) return null;
|
|
3582
3985
|
const schemaInfo = resolveSchemaForArtifact(frontmatter, rootDir);
|
|
@@ -3611,7 +4014,7 @@ function verifyArtifactsSchemas(options = {}) {
|
|
|
3611
4014
|
let validCount = 0;
|
|
3612
4015
|
let totalEvaluated = 0;
|
|
3613
4016
|
for (const file of filesToScan) {
|
|
3614
|
-
const relPath =
|
|
4017
|
+
const relPath = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
3615
4018
|
if (isIgnoredArtifactPath(relPath, Boolean(options.targetPath))) continue;
|
|
3616
4019
|
const evaluation = evaluateArtifactFile(file, relPath, rootDir);
|
|
3617
4020
|
if (!evaluation) continue;
|
|
@@ -3626,14 +4029,14 @@ function verifyArtifactsSchemas(options = {}) {
|
|
|
3626
4029
|
}
|
|
3627
4030
|
|
|
3628
4031
|
// src/adapters/sdd/scaffold.ts
|
|
3629
|
-
function
|
|
4032
|
+
function slugify2(text) {
|
|
3630
4033
|
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3631
4034
|
}
|
|
3632
4035
|
function extractDirMaxNum(dir) {
|
|
3633
|
-
if (!
|
|
4036
|
+
if (!fs8.existsSync(dir)) return 0;
|
|
3634
4037
|
let max = 0;
|
|
3635
4038
|
try {
|
|
3636
|
-
const entries =
|
|
4039
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
3637
4040
|
for (const e of entries) {
|
|
3638
4041
|
if (!e.isDirectory()) continue;
|
|
3639
4042
|
const match = e.name.match(/^(?:chg|CHG|patch|PATCH)-(\d+)/i);
|
|
@@ -3650,10 +4053,10 @@ function extractDirMaxNum(dir) {
|
|
|
3650
4053
|
}
|
|
3651
4054
|
function getNextCorrelativeNumber(rootDir) {
|
|
3652
4055
|
const candidateDirs = [
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
4056
|
+
path5.join(rootDir, "specs", "changes", "active"),
|
|
4057
|
+
path5.join(rootDir, "specs", "changes", "completed"),
|
|
4058
|
+
path5.join(rootDir, "specs"),
|
|
4059
|
+
path5.join(rootDir, "openspec", "changes")
|
|
3657
4060
|
];
|
|
3658
4061
|
let maxNum = 0;
|
|
3659
4062
|
for (const dir of candidateDirs) {
|
|
@@ -3666,7 +4069,7 @@ function resolveChangeIdentifiers(rootDir, name, profile, options) {
|
|
|
3666
4069
|
const targetChangeId = options.changeId || options.id;
|
|
3667
4070
|
if (targetChangeId) {
|
|
3668
4071
|
const rawId = targetChangeId.trim();
|
|
3669
|
-
const slug =
|
|
4072
|
+
const slug = slugify2(rawId);
|
|
3670
4073
|
const canonicalId = rawId.toUpperCase();
|
|
3671
4074
|
const match = rawId.match(/^(?:chg|CHG|patch|PATCH)-(\d+)/i);
|
|
3672
4075
|
const padNum2 = match ? match[1] : "001";
|
|
@@ -3674,7 +4077,7 @@ function resolveChangeIdentifiers(rootDir, name, profile, options) {
|
|
|
3674
4077
|
}
|
|
3675
4078
|
const nextNum = getNextCorrelativeNumber(rootDir);
|
|
3676
4079
|
const padNum = String(nextNum).padStart(3, "0");
|
|
3677
|
-
const nameSlug =
|
|
4080
|
+
const nameSlug = slugify2(name);
|
|
3678
4081
|
const prefix = profile === "patch" ? "patch" : "chg";
|
|
3679
4082
|
return {
|
|
3680
4083
|
slug: `${prefix}-${padNum}-${nameSlug}`,
|
|
@@ -3683,13 +4086,13 @@ function resolveChangeIdentifiers(rootDir, name, profile, options) {
|
|
|
3683
4086
|
};
|
|
3684
4087
|
}
|
|
3685
4088
|
function ensureEmptyChangeDir(changeDir) {
|
|
3686
|
-
if (
|
|
3687
|
-
const existing =
|
|
4089
|
+
if (fs8.existsSync(changeDir)) {
|
|
4090
|
+
const existing = fs8.readdirSync(changeDir);
|
|
3688
4091
|
if (existing.length > 0) {
|
|
3689
4092
|
return `El directorio del cambio '${changeDir}' ya existe y no est\xE1 vac\xEDo.`;
|
|
3690
4093
|
}
|
|
3691
4094
|
} else {
|
|
3692
|
-
|
|
4095
|
+
fs8.mkdirSync(changeDir, { recursive: true });
|
|
3693
4096
|
}
|
|
3694
4097
|
return null;
|
|
3695
4098
|
}
|
|
@@ -3698,7 +4101,7 @@ function scanCanonicalArtifacts(rootDir) {
|
|
|
3698
4101
|
const artifactMap = /* @__PURE__ */ new Map();
|
|
3699
4102
|
for (const file of allMdFiles) {
|
|
3700
4103
|
try {
|
|
3701
|
-
const content =
|
|
4104
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
3702
4105
|
const { frontmatter } = parseFrontmatter(content);
|
|
3703
4106
|
if (frontmatter?.id) {
|
|
3704
4107
|
const id = String(frontmatter.id);
|
|
@@ -4077,7 +4480,7 @@ function buildPatchSuccess(slug, canonicalId, specPath, citedArtifacts, errors)
|
|
|
4077
4480
|
changeId: slug,
|
|
4078
4481
|
canonicalId,
|
|
4079
4482
|
profile: "patch",
|
|
4080
|
-
changeDir:
|
|
4483
|
+
changeDir: path5.dirname(specPath),
|
|
4081
4484
|
createdFiles: [specPath],
|
|
4082
4485
|
citedArtifacts,
|
|
4083
4486
|
errors
|
|
@@ -4088,10 +4491,10 @@ function scaffoldPatchChange(rootDir, changeDir, slug, canonicalId, name, inputF
|
|
|
4088
4491
|
const citationsSection = citedArtifacts.length > 0 ? `citations:
|
|
4089
4492
|
${renderCitationsYamlBlock(citedArtifacts)}
|
|
4090
4493
|
` : "";
|
|
4091
|
-
const specPath =
|
|
4494
|
+
const specPath = path5.join(changeDir, "spec.md");
|
|
4092
4495
|
const specFmObj = buildPatchFmObj(canonicalId);
|
|
4093
4496
|
const specContent = buildPatchSpecContent(canonicalId, name, citationsSection);
|
|
4094
|
-
|
|
4497
|
+
fs8.writeFileSync(specPath, specContent, "utf-8");
|
|
4095
4498
|
const errors = validatePatchArtifacts(rootDir, changeDir, specContent, specFmObj, adapter);
|
|
4096
4499
|
return buildPatchSuccess(slug, canonicalId, specPath, citedArtifacts, errors);
|
|
4097
4500
|
}
|
|
@@ -4102,46 +4505,46 @@ function buildGreenfieldContent(reqId, ucId, name, canonicalId, author, dateStr)
|
|
|
4102
4505
|
${body}`;
|
|
4103
4506
|
}
|
|
4104
4507
|
function createGreenfieldRequirement(rootDir, name, padNum, canonicalId, author, dateStr, createdFiles) {
|
|
4105
|
-
const nameSlugUpper =
|
|
4508
|
+
const nameSlugUpper = slugify2(name).toUpperCase();
|
|
4106
4509
|
const reqId = `FR-${padNum}-${nameSlugUpper}-001`;
|
|
4107
4510
|
const ucId = `UC-${padNum}-${nameSlugUpper}`;
|
|
4108
|
-
const productDir =
|
|
4109
|
-
if (!
|
|
4110
|
-
const reqFilePath =
|
|
4111
|
-
if (
|
|
4511
|
+
const productDir = path5.join(rootDir, "specs", "product");
|
|
4512
|
+
if (!fs8.existsSync(productDir)) fs8.mkdirSync(productDir, { recursive: true });
|
|
4513
|
+
const reqFilePath = path5.join(productDir, `${reqId}.md`);
|
|
4514
|
+
if (fs8.existsSync(reqFilePath)) return null;
|
|
4112
4515
|
const reqContent = buildGreenfieldContent(reqId, ucId, name, canonicalId, author, dateStr);
|
|
4113
|
-
|
|
4516
|
+
fs8.writeFileSync(reqFilePath, reqContent, "utf-8");
|
|
4114
4517
|
createdFiles.push(reqFilePath);
|
|
4115
4518
|
const digest = computeCanonicalSha256(reqContent);
|
|
4116
4519
|
return { reqId, ucId, reqFilePath, digest };
|
|
4117
4520
|
}
|
|
4118
4521
|
function resolveMissingDraftDestination(rootDir, id) {
|
|
4119
4522
|
if (id.startsWith("PRIV-REQ-")) {
|
|
4120
|
-
return { dir:
|
|
4523
|
+
return { dir: path5.join(rootDir, "specs", "privacy", "requirements"), type: "privacy-requirement" };
|
|
4121
4524
|
}
|
|
4122
4525
|
if (id.startsWith("DATA-ASSET-")) {
|
|
4123
|
-
return { dir:
|
|
4526
|
+
return { dir: path5.join(rootDir, "specs", "privacy", "assets"), type: "data-asset" };
|
|
4124
4527
|
}
|
|
4125
4528
|
if (id.startsWith("PRIV-ROPA-")) {
|
|
4126
|
-
return { dir:
|
|
4529
|
+
return { dir: path5.join(rootDir, "specs", "privacy", "ropa"), type: "privacy-ropa" };
|
|
4127
4530
|
}
|
|
4128
4531
|
if (id.startsWith("UC-")) {
|
|
4129
|
-
return { dir:
|
|
4532
|
+
return { dir: path5.join(rootDir, "specs", "product"), type: "use-case" };
|
|
4130
4533
|
}
|
|
4131
4534
|
if (id.startsWith("SEC-REQ-") || id.startsWith("SAF-REQ-")) {
|
|
4132
|
-
return { dir:
|
|
4535
|
+
return { dir: path5.join(rootDir, "specs", "product"), type: "security-requirement" };
|
|
4133
4536
|
}
|
|
4134
|
-
return { dir:
|
|
4537
|
+
return { dir: path5.join(rootDir, "specs", "product"), type: "requirement" };
|
|
4135
4538
|
}
|
|
4136
4539
|
function createMissingDraftRequirement(rootDir, name, id, canonicalId, createdFiles) {
|
|
4137
4540
|
const { dir, type: reqType } = resolveMissingDraftDestination(rootDir, id);
|
|
4138
|
-
if (!
|
|
4139
|
-
const reqFilePath =
|
|
4541
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
4542
|
+
const reqFilePath = path5.join(dir, `${id}.md`);
|
|
4140
4543
|
const header = MISSING_DRAFT_HEADER.replaceAll("{{ID}}", id).replaceAll("{{TYPE}}", reqType).replaceAll("{{NAME}}", name);
|
|
4141
4544
|
const body = MISSING_DRAFT_BODY.replaceAll("{{ID}}", id).replaceAll("{{NAME}}", name).replaceAll("{{CANONICAL_ID}}", canonicalId);
|
|
4142
4545
|
const reqContent = `${header}
|
|
4143
4546
|
${body}`;
|
|
4144
|
-
|
|
4547
|
+
fs8.writeFileSync(reqFilePath, reqContent, "utf-8");
|
|
4145
4548
|
createdFiles.push(reqFilePath);
|
|
4146
4549
|
const digest = computeCanonicalSha256(reqContent);
|
|
4147
4550
|
return { reqFilePath, digest };
|
|
@@ -4256,7 +4659,7 @@ function initEmptyCitations() {
|
|
|
4256
4659
|
}
|
|
4257
4660
|
function ensureFallbackRequirement(col, padNum, name) {
|
|
4258
4661
|
if (col.subRequirements.length === 0) {
|
|
4259
|
-
col.subRequirements.push(`FR-${padNum}-${
|
|
4662
|
+
col.subRequirements.push(`FR-${padNum}-${slugify2(name).toUpperCase()}-001`);
|
|
4260
4663
|
}
|
|
4261
4664
|
}
|
|
4262
4665
|
function collectChangeCitations(rootDir, name, padNum, canonicalId, author, dateStr, inputFromIds, artifactMap, createdFiles) {
|
|
@@ -4270,7 +4673,7 @@ function collectChangeCitations(rootDir, name, padNum, canonicalId, author, date
|
|
|
4270
4673
|
return col;
|
|
4271
4674
|
}
|
|
4272
4675
|
function renderProposalFile(changeDir, canonicalId, name, author, citationsYamlBlock) {
|
|
4273
|
-
const proposalPath =
|
|
4676
|
+
const proposalPath = path5.join(changeDir, "proposal.md");
|
|
4274
4677
|
const citationsSection = citationsYamlBlock ? `citations:
|
|
4275
4678
|
${citationsYamlBlock}
|
|
4276
4679
|
` : "";
|
|
@@ -4290,11 +4693,11 @@ ${name}
|
|
|
4290
4693
|
## 2. Dependencias Externas Evaluadas
|
|
4291
4694
|
- Lista de dependencias nuevas a incorporar y verificaci\xF3n de licencia seg\xFAn \`license-policy.yaml\`.
|
|
4292
4695
|
`;
|
|
4293
|
-
|
|
4696
|
+
fs8.writeFileSync(proposalPath, proposalContent, "utf-8");
|
|
4294
4697
|
return proposalPath;
|
|
4295
4698
|
}
|
|
4296
4699
|
function renderSpecFile(changeDir, canonicalId, name, profile) {
|
|
4297
|
-
const specPath =
|
|
4700
|
+
const specPath = path5.join(changeDir, "spec.md");
|
|
4298
4701
|
const specFmObj = {
|
|
4299
4702
|
id: `SPEC-${canonicalId}`,
|
|
4300
4703
|
type: "delivery-spec",
|
|
@@ -4315,12 +4718,12 @@ cucumber-tags:
|
|
|
4315
4718
|
---
|
|
4316
4719
|
`;
|
|
4317
4720
|
const body = SPEC_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name);
|
|
4318
|
-
|
|
4721
|
+
fs8.writeFileSync(specPath, `${header}
|
|
4319
4722
|
${body}`, "utf-8");
|
|
4320
4723
|
return { specPath, specFmObj };
|
|
4321
4724
|
}
|
|
4322
4725
|
function renderDesignFile(changeDir, canonicalId, name, author, dateStr, citationsYamlBlock) {
|
|
4323
|
-
const designPath =
|
|
4726
|
+
const designPath = path5.join(changeDir, "design.md");
|
|
4324
4727
|
const citationsSection = citationsYamlBlock ? `citations:
|
|
4325
4728
|
${citationsYamlBlock}
|
|
4326
4729
|
` : "";
|
|
@@ -4339,7 +4742,7 @@ contracts: []
|
|
|
4339
4742
|
${citationsSection}---
|
|
4340
4743
|
`;
|
|
4341
4744
|
const body = DESIGN_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name).replaceAll("{{AUTHOR}}", author).replaceAll("{{DATE}}", dateStr);
|
|
4342
|
-
|
|
4745
|
+
fs8.writeFileSync(designPath, `${header}
|
|
4343
4746
|
${body}`, "utf-8");
|
|
4344
4747
|
return designPath;
|
|
4345
4748
|
}
|
|
@@ -4364,14 +4767,14 @@ function buildTasksFrontmatter(canonicalId, name, reqIds) {
|
|
|
4364
4767
|
};
|
|
4365
4768
|
}
|
|
4366
4769
|
function renderTasksFile(changeDir, canonicalId, name, author, dateStr, reqIds) {
|
|
4367
|
-
const tasksPath =
|
|
4770
|
+
const tasksPath = path5.join(changeDir, "tasks.md");
|
|
4368
4771
|
const tasksFm = buildTasksFrontmatter(canonicalId, name, reqIds);
|
|
4369
4772
|
const header = `---
|
|
4370
4773
|
${yaml3.dump(tasksFm, { indent: 2, lineWidth: -1 }).trim()}
|
|
4371
4774
|
---
|
|
4372
4775
|
`;
|
|
4373
4776
|
const body = TASKS_BODY.replaceAll("{{CANONICAL_ID}}", canonicalId).replaceAll("{{NAME}}", name).replaceAll("{{AUTHOR}}", author).replaceAll("{{DATE}}", dateStr);
|
|
4374
|
-
|
|
4777
|
+
fs8.writeFileSync(tasksPath, `${header}
|
|
4375
4778
|
${body}`, "utf-8");
|
|
4376
4779
|
return { tasksPath, tasksFm };
|
|
4377
4780
|
}
|
|
@@ -4550,7 +4953,7 @@ function resolveCiEnvironment(env = process.env) {
|
|
|
4550
4953
|
};
|
|
4551
4954
|
}
|
|
4552
4955
|
function updateExistingEnvFile(filePath, envString, uppercaseKey) {
|
|
4553
|
-
const content =
|
|
4956
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
4554
4957
|
const lines = content.split("\n");
|
|
4555
4958
|
let found = false;
|
|
4556
4959
|
const newLines = lines.map((line) => {
|
|
@@ -4561,15 +4964,15 @@ function updateExistingEnvFile(filePath, envString, uppercaseKey) {
|
|
|
4561
4964
|
return line;
|
|
4562
4965
|
});
|
|
4563
4966
|
if (!found) newLines.push(envString);
|
|
4564
|
-
|
|
4967
|
+
fs8.writeFileSync(filePath, newLines.filter((l) => l.trim() !== "").join("\n") + "\n", "utf-8");
|
|
4565
4968
|
}
|
|
4566
4969
|
function writeEnvOutputFile(envOutputFile, key, value) {
|
|
4567
4970
|
const uppercaseKey = key.toUpperCase();
|
|
4568
4971
|
const envString = `${uppercaseKey}=${value}`;
|
|
4569
|
-
if (
|
|
4972
|
+
if (fs8.existsSync(envOutputFile)) {
|
|
4570
4973
|
updateExistingEnvFile(envOutputFile, envString, uppercaseKey);
|
|
4571
4974
|
} else {
|
|
4572
|
-
|
|
4975
|
+
fs8.writeFileSync(envOutputFile, envString + "\n", "utf-8");
|
|
4573
4976
|
}
|
|
4574
4977
|
}
|
|
4575
4978
|
function resolveEnvOutputFilePath(env) {
|
|
@@ -4579,8 +4982,8 @@ function resolveEnvOutputFilePath(env) {
|
|
|
4579
4982
|
return void 0;
|
|
4580
4983
|
}
|
|
4581
4984
|
function emitCiOutput(key, value, env = process.env) {
|
|
4582
|
-
if (env.GITHUB_OUTPUT &&
|
|
4583
|
-
|
|
4985
|
+
if (env.GITHUB_OUTPUT && fs8.existsSync(env.GITHUB_OUTPUT)) {
|
|
4986
|
+
fs8.appendFileSync(env.GITHUB_OUTPUT, `${key}=${value}
|
|
4584
4987
|
`, "utf-8");
|
|
4585
4988
|
}
|
|
4586
4989
|
if (env.TF_BUILD === "true") {
|
|
@@ -4622,11 +5025,11 @@ function isIgnoredEntry(name) {
|
|
|
4622
5025
|
return name === "changes" || name === "active" || name === "node_modules";
|
|
4623
5026
|
}
|
|
4624
5027
|
function scanParentDirectoryHandoffs(parentDir, handoffsMap) {
|
|
4625
|
-
if (!
|
|
4626
|
-
const entries =
|
|
5028
|
+
if (!fs8.existsSync(parentDir)) return;
|
|
5029
|
+
const entries = fs8.readdirSync(parentDir, { withFileTypes: true });
|
|
4627
5030
|
for (const entry of entries) {
|
|
4628
5031
|
if (!entry.isDirectory() || isIgnoredEntry(entry.name)) continue;
|
|
4629
|
-
const changeDir =
|
|
5032
|
+
const changeDir = path5.join(parentDir, entry.name);
|
|
4630
5033
|
if (handooffsHasDir(handoffsMap, changeDir)) continue;
|
|
4631
5034
|
const handoff = loadProductHandoffSidecar(changeDir);
|
|
4632
5035
|
if (handoff?.id) {
|
|
@@ -4637,10 +5040,10 @@ function scanParentDirectoryHandoffs(parentDir, handoffsMap) {
|
|
|
4637
5040
|
function scanAllProductHandoffs(rootDir) {
|
|
4638
5041
|
const handoffsMap = /* @__PURE__ */ new Map();
|
|
4639
5042
|
const candidateParentDirs = [
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
5043
|
+
path5.join(rootDir, "specs", "changes", "active"),
|
|
5044
|
+
path5.join(rootDir, "specs", "changes", "completed"),
|
|
5045
|
+
path5.join(rootDir, "specs"),
|
|
5046
|
+
path5.join(rootDir, "openspec", "changes")
|
|
4644
5047
|
];
|
|
4645
5048
|
for (const parentDir of candidateParentDirs) {
|
|
4646
5049
|
scanParentDirectoryHandoffs(parentDir, handoffsMap);
|
|
@@ -4648,9 +5051,9 @@ function scanAllProductHandoffs(rootDir) {
|
|
|
4648
5051
|
return handoffsMap;
|
|
4649
5052
|
}
|
|
4650
5053
|
function handooffsHasDir(map, dir) {
|
|
4651
|
-
const norm =
|
|
5054
|
+
const norm = path5.resolve(dir);
|
|
4652
5055
|
for (const key of map.keys()) {
|
|
4653
|
-
if (
|
|
5056
|
+
if (path5.resolve(key) === norm) return true;
|
|
4654
5057
|
}
|
|
4655
5058
|
return false;
|
|
4656
5059
|
}
|
|
@@ -4685,7 +5088,7 @@ function buildTraceabilityHeader(isOk, errors) {
|
|
|
4685
5088
|
return [
|
|
4686
5089
|
"# 360\xB0 Requirements Traceability Matrix (RTM)",
|
|
4687
5090
|
"",
|
|
4688
|
-
`*Verification Date: ${(
|
|
5091
|
+
`*Verification Date: ${resolveDeterministicDate().toISOString()}*`,
|
|
4689
5092
|
`*Overall Status: ${isOk ? "100% TRACEABLE (PASSED)" : `ERRORS DETECTED (${errors} gaps)`}*`,
|
|
4690
5093
|
"",
|
|
4691
5094
|
"## 1. End-to-End Coverage (PDaC Handoff \u2794 arc42/NAF v4 \u2794 BDD/Gherkin)",
|
|
@@ -4747,9 +5150,9 @@ function isCodeTestFile(name) {
|
|
|
4747
5150
|
}
|
|
4748
5151
|
function indexFeatureFile(fullPath, rootDir, map) {
|
|
4749
5152
|
try {
|
|
4750
|
-
const content =
|
|
5153
|
+
const content = fs8.readFileSync(fullPath, "utf-8");
|
|
4751
5154
|
const tags = extractFeatureTags(content);
|
|
4752
|
-
const relFeature =
|
|
5155
|
+
const relFeature = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
4753
5156
|
for (const tag of tags) {
|
|
4754
5157
|
const existing = map.get(tag) || [];
|
|
4755
5158
|
if (!existing.includes(relFeature)) existing.push(relFeature);
|
|
@@ -4759,11 +5162,11 @@ function indexFeatureFile(fullPath, rootDir, map) {
|
|
|
4759
5162
|
}
|
|
4760
5163
|
}
|
|
4761
5164
|
function scanFeatureFiles(dir, rootDir, map) {
|
|
4762
|
-
if (!
|
|
5165
|
+
if (!fs8.existsSync(dir)) return;
|
|
4763
5166
|
try {
|
|
4764
|
-
const entries =
|
|
5167
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
4765
5168
|
for (const e of entries) {
|
|
4766
|
-
const fullPath =
|
|
5169
|
+
const fullPath = path5.join(dir, e.name);
|
|
4767
5170
|
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
4768
5171
|
scanFeatureFiles(fullPath, rootDir, map);
|
|
4769
5172
|
} else if (e.name.endsWith(".feature")) {
|
|
@@ -4775,8 +5178,8 @@ function scanFeatureFiles(dir, rootDir, map) {
|
|
|
4775
5178
|
}
|
|
4776
5179
|
function indexCodeTestFile(fullPath, rootDir, reqIds, map) {
|
|
4777
5180
|
try {
|
|
4778
|
-
const content =
|
|
4779
|
-
const relTest =
|
|
5181
|
+
const content = fs8.readFileSync(fullPath, "utf-8");
|
|
5182
|
+
const relTest = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
4780
5183
|
for (const artId of reqIds) {
|
|
4781
5184
|
if (content.includes(artId)) {
|
|
4782
5185
|
const existing = map.get(artId) || [];
|
|
@@ -4788,11 +5191,11 @@ function indexCodeTestFile(fullPath, rootDir, reqIds, map) {
|
|
|
4788
5191
|
}
|
|
4789
5192
|
}
|
|
4790
5193
|
function scanCodeTestFiles(dir, rootDir, reqIds, map) {
|
|
4791
|
-
if (!
|
|
5194
|
+
if (!fs8.existsSync(dir)) return;
|
|
4792
5195
|
try {
|
|
4793
|
-
const entries =
|
|
5196
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
4794
5197
|
for (const e of entries) {
|
|
4795
|
-
const fullPath =
|
|
5198
|
+
const fullPath = path5.join(dir, e.name);
|
|
4796
5199
|
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
4797
5200
|
scanCodeTestFiles(fullPath, rootDir, reqIds, map);
|
|
4798
5201
|
} else if (isCodeTestFile(e.name)) {
|
|
@@ -4826,9 +5229,9 @@ function isArchitectureViewFile(relFile, frontmatter) {
|
|
|
4826
5229
|
}
|
|
4827
5230
|
function processSingleMdFile(file, rootDir, artifactMap, componentsList, architectureViews) {
|
|
4828
5231
|
try {
|
|
4829
|
-
const content =
|
|
5232
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
4830
5233
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
4831
|
-
const relFile =
|
|
5234
|
+
const relFile = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
4832
5235
|
if (frontmatter.id) {
|
|
4833
5236
|
artifactMap.set(frontmatter.id, {
|
|
4834
5237
|
file: relFile,
|
|
@@ -4993,7 +5396,7 @@ function collectArchitectureViewTraces(reqId, architectureViews) {
|
|
|
4993
5396
|
const traces = [];
|
|
4994
5397
|
for (const view of architectureViews) {
|
|
4995
5398
|
if (view.content.includes(reqId)) {
|
|
4996
|
-
const viewIdentifier = isEntityWithId(view) && view.id ? view.id :
|
|
5399
|
+
const viewIdentifier = isEntityWithId(view) && view.id ? view.id : path5.basename(view.file);
|
|
4997
5400
|
traces.push(viewIdentifier);
|
|
4998
5401
|
}
|
|
4999
5402
|
}
|
|
@@ -5081,16 +5484,16 @@ function isProductionSourceFile(name) {
|
|
|
5081
5484
|
return SOURCE_CODE_EXTS.some((ext) => name.endsWith(ext));
|
|
5082
5485
|
}
|
|
5083
5486
|
function collectCodeAndTestCorpora(dir, prodChunks = [], testChunks = []) {
|
|
5084
|
-
if (!
|
|
5487
|
+
if (!fs8.existsSync(dir)) return { prodCorpus: "", testCorpus: "" };
|
|
5085
5488
|
try {
|
|
5086
|
-
for (const e of
|
|
5087
|
-
const fullPath =
|
|
5489
|
+
for (const e of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
5490
|
+
const fullPath = path5.join(dir, e.name);
|
|
5088
5491
|
if (e.isDirectory() && !shouldSkipDir(e.name)) {
|
|
5089
5492
|
collectCodeAndTestCorpora(fullPath, prodChunks, testChunks);
|
|
5090
5493
|
} else if (isCodeTestFile(e.name) || e.name.endsWith(".feature")) {
|
|
5091
|
-
testChunks.push(
|
|
5494
|
+
testChunks.push(fs8.readFileSync(fullPath, "utf-8"));
|
|
5092
5495
|
} else if (isProductionSourceFile(e.name)) {
|
|
5093
|
-
prodChunks.push(
|
|
5496
|
+
prodChunks.push(fs8.readFileSync(fullPath, "utf-8"));
|
|
5094
5497
|
}
|
|
5095
5498
|
}
|
|
5096
5499
|
} catch {
|
|
@@ -5244,12 +5647,12 @@ function walkTaskFiles(dir, fileList = [], excludeDirs = [
|
|
|
5244
5647
|
"examples"
|
|
5245
5648
|
]) {
|
|
5246
5649
|
try {
|
|
5247
|
-
if (!
|
|
5248
|
-
const files =
|
|
5650
|
+
if (!fs8.existsSync(dir)) return fileList;
|
|
5651
|
+
const files = fs8.readdirSync(dir).sort(canonicalSort);
|
|
5249
5652
|
for (const file of files) {
|
|
5250
|
-
const fullPath =
|
|
5653
|
+
const fullPath = path5.join(dir, file);
|
|
5251
5654
|
try {
|
|
5252
|
-
if (
|
|
5655
|
+
if (fs8.statSync(fullPath).isDirectory()) {
|
|
5253
5656
|
if (!excludeDirs.includes(file)) {
|
|
5254
5657
|
walkTaskFiles(fullPath, fileList, excludeDirs);
|
|
5255
5658
|
}
|
|
@@ -5301,7 +5704,7 @@ function generateGovernanceReportMarkdown(summaries, violationsCount, modeCounts
|
|
|
5301
5704
|
const sections = [
|
|
5302
5705
|
`# \u{1F4CB} Task Governance and Human Autonomy Classification Report
|
|
5303
5706
|
|
|
5304
|
-
> **Audit Date:** ${(
|
|
5707
|
+
> **Audit Date:** ${resolveDeterministicDate().toISOString()}
|
|
5305
5708
|
> **Overall Verdict:** ${verdict}
|
|
5306
5709
|
|
|
5307
5710
|
---`,
|
|
@@ -5362,9 +5765,9 @@ function updateGovernanceCounts(t, modeCounts, riskCounts) {
|
|
|
5362
5765
|
}
|
|
5363
5766
|
}
|
|
5364
5767
|
function parseTasksFromFile(file) {
|
|
5365
|
-
if (!
|
|
5768
|
+
if (!fs8.existsSync(file)) return [];
|
|
5366
5769
|
try {
|
|
5367
|
-
const content =
|
|
5770
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
5368
5771
|
const { frontmatter } = parseTasksDoc(content);
|
|
5369
5772
|
return frontmatter.tasks || [];
|
|
5370
5773
|
} catch {
|
|
@@ -5372,11 +5775,11 @@ function parseTasksFromFile(file) {
|
|
|
5372
5775
|
}
|
|
5373
5776
|
}
|
|
5374
5777
|
function extractDesignExpectedScope(changeDir) {
|
|
5375
|
-
const designFile =
|
|
5376
|
-
if (!
|
|
5778
|
+
const designFile = path5.join(changeDir, "design.md");
|
|
5779
|
+
if (!fs8.existsSync(designFile)) return [];
|
|
5377
5780
|
const expected = [];
|
|
5378
5781
|
try {
|
|
5379
|
-
const { frontmatter } = parseTasksDoc(
|
|
5782
|
+
const { frontmatter } = parseTasksDoc(fs8.readFileSync(designFile, "utf-8"));
|
|
5380
5783
|
if (typeof frontmatter["architecture-component"] === "string") {
|
|
5381
5784
|
expected.push(frontmatter["architecture-component"]);
|
|
5382
5785
|
}
|
|
@@ -5416,7 +5819,7 @@ function hasExplicitScopeBindings(tasks) {
|
|
|
5416
5819
|
);
|
|
5417
5820
|
}
|
|
5418
5821
|
function auditDesignScopeForFile(file, tasks, options, unassignedList, violationsList) {
|
|
5419
|
-
const changeDir =
|
|
5822
|
+
const changeDir = path5.dirname(file);
|
|
5420
5823
|
const expectedItems = [
|
|
5421
5824
|
...extractDesignExpectedScope(changeDir),
|
|
5422
5825
|
...extractHandoffExpectedScope(changeDir, options.maxUseCasesPerChange, violationsList)
|
|
@@ -5427,13 +5830,13 @@ function auditDesignScopeForFile(file, tasks, options, unassignedList, violation
|
|
|
5427
5830
|
for (const item of new Set(expectedItems)) {
|
|
5428
5831
|
if (!covered.has(item)) {
|
|
5429
5832
|
unassignedList.push(item);
|
|
5430
|
-
violationsList.push(`[${
|
|
5833
|
+
violationsList.push(`[${path5.basename(changeDir)}] Unassigned design/handoff scope in tasks.md: missing task coverage for ${item}`);
|
|
5431
5834
|
}
|
|
5432
5835
|
}
|
|
5433
5836
|
}
|
|
5434
5837
|
function processTasksFile(file, rootDir, options, acc) {
|
|
5435
5838
|
const tasks = parseTasksFromFile(file);
|
|
5436
|
-
const relPath =
|
|
5839
|
+
const relPath = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
5437
5840
|
let unverifiedDelta = 0;
|
|
5438
5841
|
for (const t of tasks) {
|
|
5439
5842
|
const { summary, violation, unverified } = evaluateTaskItem(t, relPath);
|
|
@@ -5504,6 +5907,277 @@ function verifyTasksGovernance(options = {}) {
|
|
|
5504
5907
|
);
|
|
5505
5908
|
return assembleGovernanceResult(data, reportMarkdown);
|
|
5506
5909
|
}
|
|
5910
|
+
var BddStepRegistry = class {
|
|
5911
|
+
definitions = [];
|
|
5912
|
+
register(pattern, handler) {
|
|
5913
|
+
this.definitions.push({ pattern, handler });
|
|
5914
|
+
return this;
|
|
5915
|
+
}
|
|
5916
|
+
given(pattern, handler) {
|
|
5917
|
+
return this.register(pattern, handler);
|
|
5918
|
+
}
|
|
5919
|
+
when(pattern, handler) {
|
|
5920
|
+
return this.register(pattern, handler);
|
|
5921
|
+
}
|
|
5922
|
+
then(pattern, handler) {
|
|
5923
|
+
return this.register(pattern, handler);
|
|
5924
|
+
}
|
|
5925
|
+
getDefinitions() {
|
|
5926
|
+
return this.definitions;
|
|
5927
|
+
}
|
|
5928
|
+
};
|
|
5929
|
+
var STEP_PREFIX_RE = /^(Given|When|Then|And|But|Dado|Cuando|Entonces|Y|Pero)\s+(.+)$/i;
|
|
5930
|
+
var SCENARIO_HEADER_RE = /^(Scenario|Escenario|Scenario Outline|Esquema del escenario):\s*(.+)$/i;
|
|
5931
|
+
function parseTableRowCells(line) {
|
|
5932
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
5933
|
+
}
|
|
5934
|
+
function attachStepTableRow(lastStep, cells) {
|
|
5935
|
+
if (!lastStep || cells.length < 2) return;
|
|
5936
|
+
if (!lastStep.table) lastStep.table = {};
|
|
5937
|
+
lastStep.table[cells[0]] = cells[1];
|
|
5938
|
+
}
|
|
5939
|
+
function attachExampleRow(scenario, headers, cells) {
|
|
5940
|
+
if (!scenario) return headers;
|
|
5941
|
+
if (headers.length === 0) return cells;
|
|
5942
|
+
const row = {};
|
|
5943
|
+
for (let i = 0; i < headers.length; i++) {
|
|
5944
|
+
row[headers[i]] = cells[i] ?? "";
|
|
5945
|
+
}
|
|
5946
|
+
scenario.examples.push(row);
|
|
5947
|
+
return headers;
|
|
5948
|
+
}
|
|
5949
|
+
function createInitialParseState() {
|
|
5950
|
+
return {
|
|
5951
|
+
featureName: "Unnamed Feature",
|
|
5952
|
+
featureTags: [],
|
|
5953
|
+
pendingTags: [],
|
|
5954
|
+
backgroundSteps: [],
|
|
5955
|
+
scenarios: [],
|
|
5956
|
+
currentScenario: null,
|
|
5957
|
+
inBackground: false,
|
|
5958
|
+
inExamples: false,
|
|
5959
|
+
exampleHeaders: []
|
|
5960
|
+
};
|
|
5961
|
+
}
|
|
5962
|
+
function handleStructuralHeaderLine(line, state) {
|
|
5963
|
+
if (/^(Feature|Caracter[ií]stica):/i.test(line)) {
|
|
5964
|
+
state.featureName = line.replace(/^(Feature|Caracter[ií]stica):\s*/i, "").trim();
|
|
5965
|
+
state.featureTags.push(...state.pendingTags);
|
|
5966
|
+
state.pendingTags = [];
|
|
5967
|
+
return true;
|
|
5968
|
+
}
|
|
5969
|
+
if (/^(Background|Antecedentes):/i.test(line)) {
|
|
5970
|
+
state.inBackground = true;
|
|
5971
|
+
state.inExamples = false;
|
|
5972
|
+
state.currentScenario = null;
|
|
5973
|
+
return true;
|
|
5974
|
+
}
|
|
5975
|
+
if (/^(Examples|Ejemplos):/i.test(line)) {
|
|
5976
|
+
state.inExamples = true;
|
|
5977
|
+
state.exampleHeaders = [];
|
|
5978
|
+
return true;
|
|
5979
|
+
}
|
|
5980
|
+
return false;
|
|
5981
|
+
}
|
|
5982
|
+
function startScenarioBlock(scenMatch, state) {
|
|
5983
|
+
state.inBackground = false;
|
|
5984
|
+
state.inExamples = false;
|
|
5985
|
+
const block = {
|
|
5986
|
+
name: scenMatch[2].trim(),
|
|
5987
|
+
tags: [...state.featureTags, ...state.pendingTags],
|
|
5988
|
+
isOutline: /outline|esquema/i.test(scenMatch[1]),
|
|
5989
|
+
steps: [],
|
|
5990
|
+
examples: []
|
|
5991
|
+
};
|
|
5992
|
+
state.pendingTags = [];
|
|
5993
|
+
state.scenarios.push(block);
|
|
5994
|
+
state.currentScenario = block;
|
|
5995
|
+
}
|
|
5996
|
+
function appendParsedStep(stepMatch, state) {
|
|
5997
|
+
const step = { keyword: stepMatch[1], text: stepMatch[2].trim() };
|
|
5998
|
+
if (state.inBackground) {
|
|
5999
|
+
state.backgroundSteps.push(step);
|
|
6000
|
+
} else if (state.currentScenario) {
|
|
6001
|
+
state.currentScenario.steps.push(step);
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
function handleScenarioOrStepLine(line, state) {
|
|
6005
|
+
const scenMatch = line.match(SCENARIO_HEADER_RE);
|
|
6006
|
+
if (scenMatch) {
|
|
6007
|
+
startScenarioBlock(scenMatch, state);
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
6010
|
+
const stepMatch = line.match(STEP_PREFIX_RE);
|
|
6011
|
+
if (stepMatch) {
|
|
6012
|
+
appendParsedStep(stepMatch, state);
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
function processFeatureLine(rawLine, state) {
|
|
6016
|
+
const line = rawLine.trim();
|
|
6017
|
+
if (!line || line.startsWith("#")) return;
|
|
6018
|
+
if (line.startsWith("@")) {
|
|
6019
|
+
const tags = line.split(/\s+/).filter((t) => t.startsWith("@"));
|
|
6020
|
+
state.pendingTags.push(...tags);
|
|
6021
|
+
return;
|
|
6022
|
+
}
|
|
6023
|
+
if (line.startsWith("|")) {
|
|
6024
|
+
const cells = parseTableRowCells(line);
|
|
6025
|
+
if (state.inExamples) {
|
|
6026
|
+
state.exampleHeaders = attachExampleRow(state.currentScenario, state.exampleHeaders, cells);
|
|
6027
|
+
} else {
|
|
6028
|
+
const targetList = state.inBackground ? state.backgroundSteps : state.currentScenario?.steps || [];
|
|
6029
|
+
attachStepTableRow(targetList[targetList.length - 1], cells);
|
|
6030
|
+
}
|
|
6031
|
+
return;
|
|
6032
|
+
}
|
|
6033
|
+
if (!handleStructuralHeaderLine(line, state)) {
|
|
6034
|
+
handleScenarioOrStepLine(line, state);
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
6037
|
+
function parseFeatureDocument(content) {
|
|
6038
|
+
const state = createInitialParseState();
|
|
6039
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
6040
|
+
processFeatureLine(rawLine, state);
|
|
6041
|
+
}
|
|
6042
|
+
return {
|
|
6043
|
+
featureName: state.featureName,
|
|
6044
|
+
tags: state.featureTags,
|
|
6045
|
+
backgroundSteps: state.backgroundSteps,
|
|
6046
|
+
scenarios: state.scenarios
|
|
6047
|
+
};
|
|
6048
|
+
}
|
|
6049
|
+
function interpolatePlaceholders(text, row) {
|
|
6050
|
+
let out = text;
|
|
6051
|
+
for (const [k, v] of Object.entries(row)) {
|
|
6052
|
+
out = out.replace(new RegExp(`<${k}>`, "g"), v);
|
|
6053
|
+
}
|
|
6054
|
+
return out;
|
|
6055
|
+
}
|
|
6056
|
+
function expandScenarioOutlines(scenarios) {
|
|
6057
|
+
const expanded = [];
|
|
6058
|
+
for (const sc of scenarios) {
|
|
6059
|
+
if (!sc.isOutline || sc.examples.length === 0) {
|
|
6060
|
+
expanded.push(sc);
|
|
6061
|
+
continue;
|
|
6062
|
+
}
|
|
6063
|
+
sc.examples.forEach((row, idx) => {
|
|
6064
|
+
expanded.push({
|
|
6065
|
+
name: `${sc.name} (Example #${idx + 1})`,
|
|
6066
|
+
tags: sc.tags,
|
|
6067
|
+
isOutline: false,
|
|
6068
|
+
steps: sc.steps.map((st) => ({
|
|
6069
|
+
keyword: st.keyword,
|
|
6070
|
+
text: interpolatePlaceholders(st.text, row),
|
|
6071
|
+
table: st.table
|
|
6072
|
+
})),
|
|
6073
|
+
examples: []
|
|
6074
|
+
});
|
|
6075
|
+
});
|
|
6076
|
+
}
|
|
6077
|
+
return expanded;
|
|
6078
|
+
}
|
|
6079
|
+
function matchStepDefinition(text, definitions) {
|
|
6080
|
+
for (const entry of definitions) {
|
|
6081
|
+
if (typeof entry.pattern === "string") {
|
|
6082
|
+
if (entry.pattern === text) return { entry, args: [] };
|
|
6083
|
+
} else {
|
|
6084
|
+
const m = text.match(entry.pattern);
|
|
6085
|
+
if (m) return { entry, args: m.slice(1) };
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
return null;
|
|
6089
|
+
}
|
|
6090
|
+
async function executeSingleStep(step, definitions) {
|
|
6091
|
+
const matched = matchStepDefinition(step.text, definitions);
|
|
6092
|
+
if (!matched) {
|
|
6093
|
+
return { keyword: step.keyword, text: step.text, status: "undefined", durationMs: 0 };
|
|
6094
|
+
}
|
|
6095
|
+
const start = Date.now();
|
|
6096
|
+
try {
|
|
6097
|
+
await matched.entry.handler(matched.args, step.table);
|
|
6098
|
+
return { keyword: step.keyword, text: step.text, status: "passed", durationMs: Date.now() - start };
|
|
6099
|
+
} catch (err) {
|
|
6100
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6101
|
+
return {
|
|
6102
|
+
keyword: step.keyword,
|
|
6103
|
+
text: step.text,
|
|
6104
|
+
status: "failed",
|
|
6105
|
+
durationMs: Date.now() - start,
|
|
6106
|
+
errorMessage: msg
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
async function executeScenarioSteps(allSteps, definitions) {
|
|
6111
|
+
const stepResults = [];
|
|
6112
|
+
let status = "passed";
|
|
6113
|
+
for (const step of allSteps) {
|
|
6114
|
+
if (status !== "passed") {
|
|
6115
|
+
stepResults.push({ keyword: step.keyword, text: step.text, status: "skipped", durationMs: 0 });
|
|
6116
|
+
continue;
|
|
6117
|
+
}
|
|
6118
|
+
const res = await executeSingleStep(step, definitions);
|
|
6119
|
+
stepResults.push(res);
|
|
6120
|
+
if (res.status === "failed" || res.status === "undefined") {
|
|
6121
|
+
status = res.status;
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
return { stepResults, status };
|
|
6125
|
+
}
|
|
6126
|
+
async function runAllExpandedScenarios(parsed, definitions) {
|
|
6127
|
+
const expanded = expandScenarioOutlines(parsed.scenarios);
|
|
6128
|
+
const out = [];
|
|
6129
|
+
for (const sc of expanded) {
|
|
6130
|
+
const combinedSteps = [...parsed.backgroundSteps, ...sc.steps];
|
|
6131
|
+
const { stepResults, status } = await executeScenarioSteps(combinedSteps, definitions);
|
|
6132
|
+
out.push({ name: sc.name, tags: sc.tags, status, steps: stepResults });
|
|
6133
|
+
}
|
|
6134
|
+
return out;
|
|
6135
|
+
}
|
|
6136
|
+
async function executeBddFeature(featurePath, registry, options = {}) {
|
|
6137
|
+
const rootDir = options.rootDir || process.cwd();
|
|
6138
|
+
const fullPath = path5.isAbsolute(featurePath) ? featurePath : path5.join(rootDir, featurePath);
|
|
6139
|
+
const parsed = parseFeatureDocument(fs8.readFileSync(fullPath, "utf-8"));
|
|
6140
|
+
const scenarios = await runAllExpandedScenarios(parsed, registry.getDefinitions());
|
|
6141
|
+
const report = {
|
|
6142
|
+
uri: path5.relative(rootDir, fullPath).replace(/\\/g, "/"),
|
|
6143
|
+
featureName: parsed.featureName,
|
|
6144
|
+
tags: parsed.tags,
|
|
6145
|
+
scenarios,
|
|
6146
|
+
passed: scenarios.length > 0 && scenarios.every((s) => s.status === "passed")
|
|
6147
|
+
};
|
|
6148
|
+
if (options.writeReportPath) writeCucumberJsonReport([report], options.writeReportPath);
|
|
6149
|
+
return report;
|
|
6150
|
+
}
|
|
6151
|
+
function mapStepToCucumberJson(st) {
|
|
6152
|
+
return {
|
|
6153
|
+
keyword: `${st.keyword} `,
|
|
6154
|
+
name: st.text,
|
|
6155
|
+
result: {
|
|
6156
|
+
status: st.status,
|
|
6157
|
+
duration: st.durationMs * 1e6,
|
|
6158
|
+
...st.errorMessage ? { error_message: st.errorMessage } : {}
|
|
6159
|
+
}
|
|
6160
|
+
};
|
|
6161
|
+
}
|
|
6162
|
+
function mapFeatureToCucumberJson(r) {
|
|
6163
|
+
return {
|
|
6164
|
+
uri: r.uri,
|
|
6165
|
+
name: r.featureName,
|
|
6166
|
+
tags: r.tags.map((t) => ({ name: t })),
|
|
6167
|
+
elements: r.scenarios.map((sc) => ({
|
|
6168
|
+
type: "scenario",
|
|
6169
|
+
name: sc.name,
|
|
6170
|
+
tags: sc.tags.map((t) => ({ name: t })),
|
|
6171
|
+
steps: sc.steps.map(mapStepToCucumberJson)
|
|
6172
|
+
}))
|
|
6173
|
+
};
|
|
6174
|
+
}
|
|
6175
|
+
function writeCucumberJsonReport(reports, outputPath) {
|
|
6176
|
+
const dir = path5.dirname(outputPath);
|
|
6177
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
6178
|
+
const payload = JSON.stringify(reports.map(mapFeatureToCucumberJson), null, 2);
|
|
6179
|
+
fs8.writeFileSync(outputPath, payload, "utf-8");
|
|
6180
|
+
}
|
|
5507
6181
|
function isToolAvailable(toolName) {
|
|
5508
6182
|
try {
|
|
5509
6183
|
const cmd = process.platform === "win32" ? "where" : "which";
|
|
@@ -5548,8 +6222,8 @@ function findLicenseFile(pkgDir) {
|
|
|
5548
6222
|
"license.txt"
|
|
5549
6223
|
];
|
|
5550
6224
|
for (const c of candidates) {
|
|
5551
|
-
const full =
|
|
5552
|
-
if (
|
|
6225
|
+
const full = path5.join(pkgDir, c);
|
|
6226
|
+
if (fs8.existsSync(full)) {
|
|
5553
6227
|
return full;
|
|
5554
6228
|
}
|
|
5555
6229
|
}
|
|
@@ -5566,9 +6240,9 @@ var LICENSE_FILE_PATTERNS = [
|
|
|
5566
6240
|
[(t) => t.includes("Microsoft Reciprocal License") || t.includes("MS-RL"), "MS-RL"]
|
|
5567
6241
|
];
|
|
5568
6242
|
function resolveLicenseFromFile(licenseFilePath) {
|
|
5569
|
-
if (!licenseFilePath || !
|
|
6243
|
+
if (!licenseFilePath || !fs8.existsSync(licenseFilePath)) return void 0;
|
|
5570
6244
|
try {
|
|
5571
|
-
const text =
|
|
6245
|
+
const text = fs8.readFileSync(licenseFilePath, "utf-8");
|
|
5572
6246
|
for (const [matches, spdx] of LICENSE_FILE_PATTERNS) {
|
|
5573
6247
|
if (matches(text)) return spdx;
|
|
5574
6248
|
}
|
|
@@ -5577,16 +6251,16 @@ function resolveLicenseFromFile(licenseFilePath) {
|
|
|
5577
6251
|
return void 0;
|
|
5578
6252
|
}
|
|
5579
6253
|
function readPackageJson(filePath) {
|
|
5580
|
-
if (!
|
|
6254
|
+
if (!fs8.existsSync(filePath)) return null;
|
|
5581
6255
|
try {
|
|
5582
|
-
return JSON.parse(
|
|
6256
|
+
return JSON.parse(fs8.readFileSync(filePath, "utf-8"));
|
|
5583
6257
|
} catch {
|
|
5584
6258
|
return null;
|
|
5585
6259
|
}
|
|
5586
6260
|
}
|
|
5587
6261
|
function extractDirectDepNames(rootDir, productionOnly) {
|
|
5588
6262
|
const names = /* @__PURE__ */ new Set();
|
|
5589
|
-
const pkgPath =
|
|
6263
|
+
const pkgPath = path5.join(rootDir, "package.json");
|
|
5590
6264
|
const pkg = readPackageJson(pkgPath);
|
|
5591
6265
|
if (!pkg) return names;
|
|
5592
6266
|
if (pkg.dependencies && typeof pkg.dependencies === "object") {
|
|
@@ -5606,7 +6280,7 @@ function resolveSpdxForPackage(pkg, licFile) {
|
|
|
5606
6280
|
return spdx;
|
|
5607
6281
|
}
|
|
5608
6282
|
function processPackageDir(pkgDir, directNames, seen) {
|
|
5609
|
-
const pkg = readPackageJson(
|
|
6283
|
+
const pkg = readPackageJson(path5.join(pkgDir, "package.json"));
|
|
5610
6284
|
if (!pkg?.name || !pkg.version) return;
|
|
5611
6285
|
const key = `${pkg.name}@${pkg.version}`;
|
|
5612
6286
|
if (seen.has(key)) return;
|
|
@@ -5627,40 +6301,40 @@ function processPackageDir(pkgDir, directNames, seen) {
|
|
|
5627
6301
|
});
|
|
5628
6302
|
}
|
|
5629
6303
|
function scanScopeSubdirectory(scopeDir, directNames, seen) {
|
|
5630
|
-
const scopedEntries =
|
|
6304
|
+
const scopedEntries = fs8.readdirSync(scopeDir, { withFileTypes: true });
|
|
5631
6305
|
for (const scoped of scopedEntries) {
|
|
5632
6306
|
if (scoped.isDirectory()) {
|
|
5633
|
-
processPackageDir(
|
|
6307
|
+
processPackageDir(path5.join(scopeDir, scoped.name), directNames, seen);
|
|
5634
6308
|
}
|
|
5635
6309
|
}
|
|
5636
6310
|
}
|
|
5637
6311
|
function scanSubdirectories(baseDir, directNames, seen) {
|
|
5638
|
-
if (!
|
|
5639
|
-
const entries =
|
|
6312
|
+
if (!fs8.existsSync(baseDir)) return;
|
|
6313
|
+
const entries = fs8.readdirSync(baseDir, { withFileTypes: true });
|
|
5640
6314
|
for (const entry of entries) {
|
|
5641
6315
|
if (!entry.isDirectory()) continue;
|
|
5642
6316
|
if (entry.name.startsWith("@")) {
|
|
5643
|
-
scanScopeSubdirectory(
|
|
6317
|
+
scanScopeSubdirectory(path5.join(baseDir, entry.name), directNames, seen);
|
|
5644
6318
|
} else if (entry.name !== ".bin" && entry.name !== ".pnpm") {
|
|
5645
|
-
processPackageDir(
|
|
6319
|
+
processPackageDir(path5.join(baseDir, entry.name), directNames, seen);
|
|
5646
6320
|
}
|
|
5647
6321
|
}
|
|
5648
6322
|
}
|
|
5649
6323
|
function scanPnpmStore(pnpmDir, directNames, seen) {
|
|
5650
|
-
if (!
|
|
5651
|
-
const entries =
|
|
6324
|
+
if (!fs8.existsSync(pnpmDir)) return;
|
|
6325
|
+
const entries = fs8.readdirSync(pnpmDir, { withFileTypes: true });
|
|
5652
6326
|
for (const entry of entries) {
|
|
5653
6327
|
if (!entry.isDirectory()) continue;
|
|
5654
|
-
const candidateModules =
|
|
5655
|
-
if (
|
|
6328
|
+
const candidateModules = path5.join(pnpmDir, entry.name, "node_modules");
|
|
6329
|
+
if (fs8.existsSync(candidateModules)) {
|
|
5656
6330
|
scanSubdirectories(candidateModules, directNames, seen);
|
|
5657
6331
|
}
|
|
5658
6332
|
}
|
|
5659
6333
|
}
|
|
5660
6334
|
function readTextIfExists(filePath) {
|
|
5661
|
-
if (!
|
|
6335
|
+
if (!fs8.existsSync(filePath)) return null;
|
|
5662
6336
|
try {
|
|
5663
|
-
return
|
|
6337
|
+
return fs8.readFileSync(filePath, "utf-8");
|
|
5664
6338
|
} catch {
|
|
5665
6339
|
return null;
|
|
5666
6340
|
}
|
|
@@ -5720,14 +6394,14 @@ function parsePipfileSection(pkgs, isDirect, lockPath, seen) {
|
|
|
5720
6394
|
}
|
|
5721
6395
|
}
|
|
5722
6396
|
function scanPythonDependencies(rootDir, seen) {
|
|
5723
|
-
const pyproject = readTextIfExists(
|
|
6397
|
+
const pyproject = readTextIfExists(path5.join(rootDir, "pyproject.toml")) || "";
|
|
5724
6398
|
const directNames = extractPyprojectDirectDeps(pyproject);
|
|
5725
6399
|
for (const lockName of ["poetry.lock", "uv.lock"]) {
|
|
5726
|
-
const lockPath =
|
|
6400
|
+
const lockPath = path5.join(rootDir, lockName);
|
|
5727
6401
|
const content = readTextIfExists(lockPath);
|
|
5728
6402
|
if (content) parseTomlPackageBlocks(content, lockPath, directNames, seen);
|
|
5729
6403
|
}
|
|
5730
|
-
const pipfileLockPath =
|
|
6404
|
+
const pipfileLockPath = path5.join(rootDir, "Pipfile.lock");
|
|
5731
6405
|
const pipfileRaw = readTextIfExists(pipfileLockPath);
|
|
5732
6406
|
if (!pipfileRaw) return;
|
|
5733
6407
|
try {
|
|
@@ -5770,7 +6444,7 @@ function extractGoCandidateLines(content) {
|
|
|
5770
6444
|
return out;
|
|
5771
6445
|
}
|
|
5772
6446
|
function scanGoDependencies(rootDir, seen) {
|
|
5773
|
-
const goModPath =
|
|
6447
|
+
const goModPath = path5.join(rootDir, "go.mod");
|
|
5774
6448
|
const content = readTextIfExists(goModPath);
|
|
5775
6449
|
if (!content) return;
|
|
5776
6450
|
for (const line of extractGoCandidateLines(content)) {
|
|
@@ -5813,10 +6487,10 @@ function parseCargoLockBlock(block, rootName, directMeta, cargoLockPath) {
|
|
|
5813
6487
|
};
|
|
5814
6488
|
}
|
|
5815
6489
|
function scanRustDependencies(rootDir, seen) {
|
|
5816
|
-
const cargoLockPath =
|
|
6490
|
+
const cargoLockPath = path5.join(rootDir, "Cargo.lock");
|
|
5817
6491
|
const lockContent = readTextIfExists(cargoLockPath);
|
|
5818
6492
|
if (!lockContent) return;
|
|
5819
|
-
const cargoToml = readTextIfExists(
|
|
6493
|
+
const cargoToml = readTextIfExists(path5.join(rootDir, "Cargo.toml")) || "";
|
|
5820
6494
|
const directMeta = extractCargoDirectDeps(cargoToml);
|
|
5821
6495
|
const rootName = extractTomlField(cargoToml, "name");
|
|
5822
6496
|
for (const block of lockContent.split(/\[\[package\]\]/g).slice(1)) {
|
|
@@ -5852,7 +6526,7 @@ function parseMavenDepBlock(block, defaultLic, pomPath) {
|
|
|
5852
6526
|
};
|
|
5853
6527
|
}
|
|
5854
6528
|
function scanMavenDependencies(rootDir, seen) {
|
|
5855
|
-
const pomPath =
|
|
6529
|
+
const pomPath = path5.join(rootDir, "pom.xml");
|
|
5856
6530
|
const content = readTextIfExists(pomPath);
|
|
5857
6531
|
if (!content) return;
|
|
5858
6532
|
const projectLicBlock = content.match(/<licenses>([\s\S]*?)<\/licenses>/)?.[1] || "";
|
|
@@ -5868,11 +6542,11 @@ function scanMavenDependencies(rootDir, seen) {
|
|
|
5868
6542
|
function scanWithNativeFs(rootDir, options = {}) {
|
|
5869
6543
|
const directNames = extractDirectDepNames(rootDir, options.productionOnly);
|
|
5870
6544
|
const seen = /* @__PURE__ */ new Map();
|
|
5871
|
-
const nmDir =
|
|
5872
|
-
if (
|
|
6545
|
+
const nmDir = path5.join(rootDir, "node_modules");
|
|
6546
|
+
if (fs8.existsSync(nmDir)) {
|
|
5873
6547
|
scanSubdirectories(nmDir, directNames, seen);
|
|
5874
6548
|
if (options.depth !== "direct") {
|
|
5875
|
-
scanPnpmStore(
|
|
6549
|
+
scanPnpmStore(path5.join(nmDir, ".pnpm"), directNames, seen);
|
|
5876
6550
|
}
|
|
5877
6551
|
}
|
|
5878
6552
|
scanPythonDependencies(rootDir, seen);
|
|
@@ -6131,20 +6805,20 @@ function resolveProjectIdentity(options) {
|
|
|
6131
6805
|
let name = options.projectName || "ai-sdlc-project";
|
|
6132
6806
|
let version = options.projectVersion || "1.0.0";
|
|
6133
6807
|
if (!options.rootDir) return { name, version };
|
|
6134
|
-
const pkgPath =
|
|
6135
|
-
if (!
|
|
6808
|
+
const pkgPath = path5.join(options.rootDir, "package.json");
|
|
6809
|
+
if (!fs8.existsSync(pkgPath)) return { name, version };
|
|
6136
6810
|
try {
|
|
6137
|
-
const pkg = JSON.parse(
|
|
6811
|
+
const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
|
|
6138
6812
|
if (pkg.name) name = pkg.name;
|
|
6139
6813
|
if (pkg.version) version = pkg.version;
|
|
6140
6814
|
} catch {
|
|
6141
6815
|
}
|
|
6142
6816
|
return { name, version };
|
|
6143
6817
|
}
|
|
6144
|
-
function buildBomMetadata(name, version) {
|
|
6818
|
+
function buildBomMetadata(name, version, rootDir) {
|
|
6145
6819
|
return {
|
|
6146
|
-
timestamp: (
|
|
6147
|
-
tools: [{ vendor: "AI-SDLC", name: "@aisdlc/core", version: "2.
|
|
6820
|
+
timestamp: resolveDeterministicDate(rootDir).toISOString(),
|
|
6821
|
+
tools: [{ vendor: "AI-SDLC", name: "@aisdlc/core", version: "2.5.0" }],
|
|
6148
6822
|
component: { type: "application", name, version }
|
|
6149
6823
|
};
|
|
6150
6824
|
}
|
|
@@ -6155,52 +6829,21 @@ function generateCycloneDxSbom(dependencies, options = {}) {
|
|
|
6155
6829
|
bomFormat: "CycloneDX",
|
|
6156
6830
|
specVersion: "1.5",
|
|
6157
6831
|
version: 1,
|
|
6158
|
-
serialNumber: `urn:uuid:${
|
|
6159
|
-
metadata: buildBomMetadata(name, version),
|
|
6832
|
+
serialNumber: `urn:uuid:${generateDeterministicUuidV5(`${name}@${version}|${dependencies.map((d) => `${d.name}@${d.version}:${d.purl || ""}`).sort(canonicalSort).join(";")}`)}`,
|
|
6833
|
+
metadata: buildBomMetadata(name, version, options.rootDir),
|
|
6160
6834
|
components: dependencies.map(buildCycloneDxComponent),
|
|
6161
6835
|
...cdxVulns.length > 0 ? { vulnerabilities: cdxVulns } : {}
|
|
6162
6836
|
};
|
|
6163
6837
|
}
|
|
6164
|
-
function tryAtomicWrite(filePath, content) {
|
|
6165
|
-
const tempPath = `${filePath}.${process.pid}.${Math.random().toString(36).substring(2)}.tmp`;
|
|
6166
|
-
try {
|
|
6167
|
-
fs7.writeFileSync(tempPath, content, "utf-8");
|
|
6168
|
-
try {
|
|
6169
|
-
if (fs7.existsSync(filePath)) {
|
|
6170
|
-
fs7.rmSync(filePath, { force: true });
|
|
6171
|
-
}
|
|
6172
|
-
fs7.renameSync(tempPath, filePath);
|
|
6173
|
-
} catch {
|
|
6174
|
-
fs7.copyFileSync(tempPath, filePath);
|
|
6175
|
-
fs7.unlinkSync(tempPath);
|
|
6176
|
-
}
|
|
6177
|
-
return true;
|
|
6178
|
-
} catch {
|
|
6179
|
-
return false;
|
|
6180
|
-
}
|
|
6181
|
-
}
|
|
6182
|
-
function safeWriteFileSync(filePath, content) {
|
|
6183
|
-
const dir = path4.dirname(filePath);
|
|
6184
|
-
if (!fs7.existsSync(dir)) {
|
|
6185
|
-
fs7.mkdirSync(dir, { recursive: true });
|
|
6186
|
-
}
|
|
6187
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
6188
|
-
if (tryAtomicWrite(filePath, content)) return;
|
|
6189
|
-
}
|
|
6190
|
-
try {
|
|
6191
|
-
fs7.writeFileSync(filePath, content, "utf-8");
|
|
6192
|
-
} catch {
|
|
6193
|
-
}
|
|
6194
|
-
}
|
|
6195
6838
|
function writeCycloneDxSbom(sbom, outputPath) {
|
|
6196
|
-
|
|
6839
|
+
writeReportFileIfChanged(outputPath, JSON.stringify(sbom, null, 2));
|
|
6197
6840
|
}
|
|
6198
6841
|
function buildThirdPartyHeader(projectName, count) {
|
|
6199
6842
|
return [
|
|
6200
6843
|
`# \u{1F4DC} Third-Party Software Notices and Information`,
|
|
6201
6844
|
``,
|
|
6202
6845
|
`> **Proyecto:** ${projectName}`,
|
|
6203
|
-
`> **Fecha:** ${(
|
|
6846
|
+
`> **Fecha:** ${resolveDeterministicDate().toISOString().split("T")[0]}`,
|
|
6204
6847
|
`> **Total Componentes:** ${count}`,
|
|
6205
6848
|
``,
|
|
6206
6849
|
`Este documento contiene los avisos de copyright y t\xE9rminos de licencia aplicables a los componentes de terceros utilizados en este proyecto.`,
|
|
@@ -6214,9 +6857,9 @@ function buildThirdPartyHeader(projectName, count) {
|
|
|
6214
6857
|
];
|
|
6215
6858
|
}
|
|
6216
6859
|
function readLicenseTextPreview(licenseFile) {
|
|
6217
|
-
if (!licenseFile || !
|
|
6860
|
+
if (!licenseFile || !fs8.existsSync(licenseFile)) return null;
|
|
6218
6861
|
try {
|
|
6219
|
-
const text =
|
|
6862
|
+
const text = fs8.readFileSync(licenseFile, "utf-8").trim();
|
|
6220
6863
|
return text.slice(0, 1e3) + (text.length > 1e3 ? "\n... (texto truncado)" : "");
|
|
6221
6864
|
} catch {
|
|
6222
6865
|
return null;
|
|
@@ -6248,13 +6891,13 @@ function generateThirdPartyNotices(dependencies, projectName = "AI-SDLC") {
|
|
|
6248
6891
|
"",
|
|
6249
6892
|
"---",
|
|
6250
6893
|
"",
|
|
6251
|
-
"##
|
|
6894
|
+
"## 2. Atribuciones y T\xE9rminos de Licencia Individuales",
|
|
6252
6895
|
"",
|
|
6253
6896
|
...attributions
|
|
6254
6897
|
].join("\n");
|
|
6255
6898
|
}
|
|
6256
|
-
function writeThirdPartyNotices(
|
|
6257
|
-
|
|
6899
|
+
function writeThirdPartyNotices(noticesContent, outputPath) {
|
|
6900
|
+
writeReportFileIfChanged(outputPath, noticesContent);
|
|
6258
6901
|
}
|
|
6259
6902
|
|
|
6260
6903
|
// src/verifiers/licenses.ts
|
|
@@ -6331,22 +6974,20 @@ function parseVexStatements(doc) {
|
|
|
6331
6974
|
if (!Array.isArray(rawList)) return [];
|
|
6332
6975
|
return rawList.filter((item) => Boolean(item && typeof item.id === "string" && typeof item.state === "string"));
|
|
6333
6976
|
}
|
|
6334
|
-
function
|
|
6977
|
+
function parseLicenseDoc(content) {
|
|
6978
|
+
let doc;
|
|
6335
6979
|
try {
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
rawPolicy: doc
|
|
6348
|
-
};
|
|
6349
|
-
} catch {
|
|
6980
|
+
doc = yaml3.load(content);
|
|
6981
|
+
} catch (err) {
|
|
6982
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid YAML in license-policy: ${err instanceof Error ? err.message : String(err)}`);
|
|
6983
|
+
}
|
|
6984
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
6985
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid license policy structure: root must be a YAML mapping`);
|
|
6986
|
+
}
|
|
6987
|
+
return doc;
|
|
6988
|
+
}
|
|
6989
|
+
function parseLicensePolicy(content) {
|
|
6990
|
+
if (!content || !content.trim()) {
|
|
6350
6991
|
return {
|
|
6351
6992
|
version: "1.0",
|
|
6352
6993
|
permitted: DEFAULT_PERMITTED,
|
|
@@ -6356,6 +6997,17 @@ function parseLicensePolicy(content) {
|
|
|
6356
6997
|
vex_statements: []
|
|
6357
6998
|
};
|
|
6358
6999
|
}
|
|
7000
|
+
const doc = parseLicenseDoc(content);
|
|
7001
|
+
const { permitted, restricted, blocked } = classifyCategories(doc?.categories);
|
|
7002
|
+
return {
|
|
7003
|
+
version: doc?.version || "1.0",
|
|
7004
|
+
permitted: permitted.length > 0 ? permitted : DEFAULT_PERMITTED,
|
|
7005
|
+
restricted: restricted.length > 0 ? restricted : DEFAULT_RESTRICTED,
|
|
7006
|
+
blocked: blocked.length > 0 ? blocked : DEFAULT_BLOCKED,
|
|
7007
|
+
exception_packages: parseExceptions(doc?.exceptions),
|
|
7008
|
+
vex_statements: parseVexStatements(doc),
|
|
7009
|
+
rawPolicy: doc
|
|
7010
|
+
};
|
|
6359
7011
|
}
|
|
6360
7012
|
function evaluateCompoundLicense(lic, policy) {
|
|
6361
7013
|
const cleanLic = lic.replace(/[()]/g, "").trim();
|
|
@@ -6447,7 +7099,7 @@ function buildReportHeader(isOk, depCount, violationCount, sbomPath) {
|
|
|
6447
7099
|
return [
|
|
6448
7100
|
`# \u{1F4DC} Open Source License Compliance Audit (Dynamic SCA)`,
|
|
6449
7101
|
``,
|
|
6450
|
-
`> **Verification Date:** ${(
|
|
7102
|
+
`> **Verification Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
6451
7103
|
`> **Status:** ${statusStr}`,
|
|
6452
7104
|
`> **Total Audited Dependencies:** ${depCount}`,
|
|
6453
7105
|
sbomPath ? `> **Generated CycloneDX SBOM:** \`${sbomPath}\`` : "",
|
|
@@ -6472,9 +7124,9 @@ function generateLicenseReportMarkdown(dependencies, violations, policy, sbomPat
|
|
|
6472
7124
|
return lines.filter(Boolean).join("\n");
|
|
6473
7125
|
}
|
|
6474
7126
|
function parseManifestItems(manifestFile) {
|
|
6475
|
-
if (!
|
|
7127
|
+
if (!fs8.existsSync(manifestFile)) return [];
|
|
6476
7128
|
try {
|
|
6477
|
-
const doc = yaml3.load(
|
|
7129
|
+
const doc = yaml3.load(fs8.readFileSync(manifestFile, "utf-8"));
|
|
6478
7130
|
return Array.isArray(doc?.evaluated_dependencies) ? doc.evaluated_dependencies : [];
|
|
6479
7131
|
} catch {
|
|
6480
7132
|
return [];
|
|
@@ -6506,10 +7158,10 @@ function verifyStaticManifest(manifestFile, policy) {
|
|
|
6506
7158
|
function resolveManifestPath(rootDir, customPath) {
|
|
6507
7159
|
if (customPath) return customPath;
|
|
6508
7160
|
const candidates = [
|
|
6509
|
-
|
|
6510
|
-
|
|
7161
|
+
path5.join(rootDir, "compliance", "license-manifest.yaml"),
|
|
7162
|
+
path5.join(rootDir, "license-manifest.yaml")
|
|
6511
7163
|
];
|
|
6512
|
-
return candidates.find((p) =>
|
|
7164
|
+
return candidates.find((p) => fs8.existsSync(p)) || candidates[0];
|
|
6513
7165
|
}
|
|
6514
7166
|
function handleStaticManifestVerification(options, rootDir, policy) {
|
|
6515
7167
|
const manifestFile = resolveManifestPath(rootDir, options.manifestPath);
|
|
@@ -6530,7 +7182,7 @@ function shouldGenerateSbom(options, rawPolicy) {
|
|
|
6530
7182
|
}
|
|
6531
7183
|
function maybeGenerateSbom(options, rootDir, rawPolicy, dependencies, vulnerabilities, vexStatements) {
|
|
6532
7184
|
if (!shouldGenerateSbom(options, rawPolicy)) return void 0;
|
|
6533
|
-
const sbomPath = options.sbomPath ||
|
|
7185
|
+
const sbomPath = options.sbomPath || path5.join(rootDir, "reports", "sbom.cdx.json");
|
|
6534
7186
|
const sbom = generateCycloneDxSbom(dependencies, { rootDir, vulnerabilities, vexStatements });
|
|
6535
7187
|
writeCycloneDxSbom(sbom, sbomPath);
|
|
6536
7188
|
return sbomPath;
|
|
@@ -6539,7 +7191,7 @@ function maybeGenerateNotices(options, rootDir, rawPolicy, dependencies) {
|
|
|
6539
7191
|
const shouldGen = options.generateNotices === true || Boolean(options.noticesPath || rawPolicy?.ci_gates?.output_notices_file);
|
|
6540
7192
|
if (!shouldGen) return void 0;
|
|
6541
7193
|
const noticesFileName = rawPolicy?.ci_gates?.output_notices_file || "THIRD_PARTY_NOTICES.md";
|
|
6542
|
-
const noticesPath = options.noticesPath ||
|
|
7194
|
+
const noticesPath = options.noticesPath || path5.join(rootDir, noticesFileName);
|
|
6543
7195
|
const noticesContent = generateThirdPartyNotices(dependencies);
|
|
6544
7196
|
writeThirdPartyNotices(noticesContent, noticesPath);
|
|
6545
7197
|
return noticesPath;
|
|
@@ -6551,8 +7203,8 @@ function handleDynamicScaArtifacts(options, rootDir, rawPolicy, dependencies, vu
|
|
|
6551
7203
|
};
|
|
6552
7204
|
}
|
|
6553
7205
|
function loadLicensePolicy(rootDir, customPath) {
|
|
6554
|
-
const policyFile = customPath ||
|
|
6555
|
-
return
|
|
7206
|
+
const policyFile = customPath || path5.join(rootDir, "license-policy.yaml");
|
|
7207
|
+
return fs8.existsSync(policyFile) ? parseLicensePolicy(fs8.readFileSync(policyFile, "utf-8")) : parseLicensePolicy("");
|
|
6556
7208
|
}
|
|
6557
7209
|
function evaluateDependencyList(dependencies, policy) {
|
|
6558
7210
|
const violations = [];
|
|
@@ -6656,11 +7308,11 @@ var PROTECTED_BYPASS_PATTERNS = [
|
|
|
6656
7308
|
/migrations?[\\/]/i
|
|
6657
7309
|
];
|
|
6658
7310
|
function detectProfileFromChange(changeDir) {
|
|
6659
|
-
if (!
|
|
6660
|
-
const specFile =
|
|
6661
|
-
if (
|
|
7311
|
+
if (!fs8.existsSync(changeDir)) return "standard";
|
|
7312
|
+
const specFile = path5.join(changeDir, "spec.md");
|
|
7313
|
+
if (fs8.existsSync(specFile)) {
|
|
6662
7314
|
try {
|
|
6663
|
-
const rawSpec =
|
|
7315
|
+
const rawSpec = fs8.readFileSync(specFile, "utf-8").replace(/^\uFEFF/, "");
|
|
6664
7316
|
const match = rawSpec.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
6665
7317
|
if (match) {
|
|
6666
7318
|
const parsedFm = yaml3.load(match[1]);
|
|
@@ -6678,24 +7330,29 @@ function isPathProtectedFromPatch(filePath) {
|
|
|
6678
7330
|
return PROTECTED_BYPASS_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
6679
7331
|
}
|
|
6680
7332
|
function detectBranchChangeMatch(rootDir, dirs) {
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
7333
|
+
let branch = "";
|
|
7334
|
+
if (fs8.existsSync(path5.join(rootDir, ".git"))) {
|
|
7335
|
+
try {
|
|
7336
|
+
branch = execFileSync("git", ["branch", "--show-current"], {
|
|
7337
|
+
cwd: rootDir,
|
|
7338
|
+
encoding: "utf-8",
|
|
7339
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
7340
|
+
}).trim().toLowerCase();
|
|
7341
|
+
} catch {
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
if (!branch) {
|
|
7345
|
+
const ciBranch = (process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_REF_NAME || process.env.BRANCH_NAME || "").toLowerCase();
|
|
7346
|
+
branch = ciBranch;
|
|
6692
7347
|
}
|
|
7348
|
+
if (!branch) return void 0;
|
|
7349
|
+
return dirs.find((d) => branch.includes(path5.basename(d).toLowerCase()));
|
|
6693
7350
|
}
|
|
6694
7351
|
function resolveActiveChangeDir(rootDir) {
|
|
6695
|
-
const activeRoot =
|
|
6696
|
-
if (!
|
|
7352
|
+
const activeRoot = path5.join(rootDir, "specs", "changes", "active");
|
|
7353
|
+
if (!fs8.existsSync(activeRoot)) return void 0;
|
|
6697
7354
|
try {
|
|
6698
|
-
const dirs =
|
|
7355
|
+
const dirs = fs8.readdirSync(activeRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path5.join(activeRoot, d.name)).sort((a, b) => canonicalSort(a, b));
|
|
6699
7356
|
const branchMatch = detectBranchChangeMatch(rootDir, dirs);
|
|
6700
7357
|
if (branchMatch) return branchMatch;
|
|
6701
7358
|
return dirs.find((d) => detectProfileFromChange(d) === "patch") || dirs[0];
|
|
@@ -6703,8 +7360,12 @@ function resolveActiveChangeDir(rootDir) {
|
|
|
6703
7360
|
return void 0;
|
|
6704
7361
|
}
|
|
6705
7362
|
}
|
|
7363
|
+
function isEphemeralOrIgnored(file) {
|
|
7364
|
+
const norm = file.replace(/\\/g, "/");
|
|
7365
|
+
return norm.startsWith("scratch/") || norm.startsWith("reports/") || norm.startsWith("coverage/") || norm.startsWith("dist/") || norm.startsWith("temp/") || norm.startsWith("tmp/");
|
|
7366
|
+
}
|
|
6706
7367
|
function detectGitDiffFiles(rootDir) {
|
|
6707
|
-
if (!
|
|
7368
|
+
if (!fs8.existsSync(path5.join(rootDir, ".git"))) return [];
|
|
6708
7369
|
try {
|
|
6709
7370
|
const tracked = execFileSync("git", ["diff", "--name-only", "HEAD"], {
|
|
6710
7371
|
cwd: rootDir,
|
|
@@ -6717,7 +7378,7 @@ function detectGitDiffFiles(rootDir) {
|
|
|
6717
7378
|
stdio: ["pipe", "pipe", "ignore"]
|
|
6718
7379
|
});
|
|
6719
7380
|
const combined = `${tracked}
|
|
6720
|
-
${untracked}`.split(/\r?\n/).map((f) => f.trim()).filter(Boolean);
|
|
7381
|
+
${untracked}`.split(/\r?\n/).map((f) => f.trim()).filter((f) => Boolean(f) && !isEphemeralOrIgnored(f));
|
|
6721
7382
|
return Array.from(new Set(combined));
|
|
6722
7383
|
} catch {
|
|
6723
7384
|
return [];
|
|
@@ -6727,19 +7388,19 @@ function resolveChangeDirectory(rootDir, options) {
|
|
|
6727
7388
|
if (options.changeDir) return options.changeDir;
|
|
6728
7389
|
if (options.changeId) {
|
|
6729
7390
|
const candidates = [
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
7391
|
+
path5.join(rootDir, "specs", options.changeId),
|
|
7392
|
+
path5.join(rootDir, "specs", "changes", "active", options.changeId),
|
|
7393
|
+
path5.join(rootDir, "examples", "specs", options.changeId),
|
|
7394
|
+
path5.join(rootDir, "specs", "changes", "completed", options.changeId)
|
|
6734
7395
|
];
|
|
6735
|
-
return candidates.find((c) =>
|
|
7396
|
+
return candidates.find((c) => fs8.existsSync(c));
|
|
6736
7397
|
}
|
|
6737
7398
|
return resolveActiveChangeDir(rootDir);
|
|
6738
7399
|
}
|
|
6739
7400
|
function collectPatchViolations(rootDir, modifiedFiles) {
|
|
6740
7401
|
const violations = [];
|
|
6741
7402
|
for (const rawFile of modifiedFiles) {
|
|
6742
|
-
const relPath =
|
|
7403
|
+
const relPath = path5.isAbsolute(rawFile) ? path5.relative(rootDir, rawFile) : rawFile;
|
|
6743
7404
|
if (isPathProtectedFromPatch(relPath)) {
|
|
6744
7405
|
violations.push(
|
|
6745
7406
|
`[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'.`
|
|
@@ -6752,7 +7413,7 @@ function buildFrictionHeader(profile, success, fileCount) {
|
|
|
6752
7413
|
return [
|
|
6753
7414
|
`# \u{1F6E1}\uFE0F Progressive Friction and Anti-Bypass Verification Report`,
|
|
6754
7415
|
``,
|
|
6755
|
-
`> **Date:** ${(
|
|
7416
|
+
`> **Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
6756
7417
|
`> **Evaluated Profile:** \`${profile.toUpperCase()}\``,
|
|
6757
7418
|
`> **Verdict:** ${success ? "COMPLIANT \u2705" : "BLOCKED \u274C"}`,
|
|
6758
7419
|
``,
|
|
@@ -6845,13 +7506,13 @@ function extractManualMetadata(fm, id, manualType, relPath, body) {
|
|
|
6845
7506
|
}
|
|
6846
7507
|
function buildManualItem(rootDir, filePath) {
|
|
6847
7508
|
try {
|
|
6848
|
-
const raw =
|
|
7509
|
+
const raw = fs8.readFileSync(filePath, "utf-8");
|
|
6849
7510
|
const { fm, body } = parseMdFrontmatter(raw);
|
|
6850
7511
|
const id = typeof fm.id === "string" ? fm.id.trim() : "";
|
|
6851
7512
|
const rawType = typeof fm.type === "string" ? fm.type.trim() : "";
|
|
6852
7513
|
const manualType = resolveManualType(id, rawType);
|
|
6853
7514
|
if (!id || !manualType) return null;
|
|
6854
|
-
const relPath =
|
|
7515
|
+
const relPath = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
6855
7516
|
return extractManualMetadata(fm, id, manualType, relPath, body);
|
|
6856
7517
|
} catch {
|
|
6857
7518
|
return null;
|
|
@@ -6862,8 +7523,8 @@ function scanManualsInWorkspace(rootDir) {
|
|
|
6862
7523
|
const seenIds = /* @__PURE__ */ new Set();
|
|
6863
7524
|
const items = [];
|
|
6864
7525
|
for (const relDir of manualDirs) {
|
|
6865
|
-
const fullDir =
|
|
6866
|
-
if (!
|
|
7526
|
+
const fullDir = path5.join(rootDir, relDir);
|
|
7527
|
+
if (!fs8.existsSync(fullDir)) continue;
|
|
6867
7528
|
for (const file of walkMdFiles(fullDir)) {
|
|
6868
7529
|
const item = buildManualItem(rootDir, file);
|
|
6869
7530
|
if (item && !seenIds.has(item.id)) {
|
|
@@ -6886,10 +7547,10 @@ function classifyBaselineId(id, type, buckets) {
|
|
|
6886
7547
|
}
|
|
6887
7548
|
}
|
|
6888
7549
|
function processBaselineFile(file, rootDir, buckets) {
|
|
6889
|
-
const rel =
|
|
7550
|
+
const rel = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
6890
7551
|
if (rel.includes("/changes/") || rel.startsWith("changes/")) return;
|
|
6891
7552
|
try {
|
|
6892
|
-
const { fm } = parseMdFrontmatter(
|
|
7553
|
+
const { fm } = parseMdFrontmatter(fs8.readFileSync(file, "utf-8"));
|
|
6893
7554
|
const id = typeof fm.id === "string" ? fm.id.trim() : "";
|
|
6894
7555
|
const status = typeof fm.status === "string" ? fm.status.trim().toLowerCase() : "active";
|
|
6895
7556
|
if (!id || status === "retired" || status === "deprecated") return;
|
|
@@ -6910,8 +7571,8 @@ function collectBaselineTargets(rootDir) {
|
|
|
6910
7571
|
enclaves: /* @__PURE__ */ new Set()
|
|
6911
7572
|
};
|
|
6912
7573
|
for (const dir of ["product", "architecture", "security", "specs"]) {
|
|
6913
|
-
const fullDir =
|
|
6914
|
-
if (
|
|
7574
|
+
const fullDir = path5.join(rootDir, dir);
|
|
7575
|
+
if (fs8.existsSync(fullDir)) {
|
|
6915
7576
|
scanDirectoryForBaseline(rootDir, fullDir, buckets);
|
|
6916
7577
|
}
|
|
6917
7578
|
}
|
|
@@ -6968,10 +7629,10 @@ function detectCurrentGitBranch(rootDir) {
|
|
|
6968
7629
|
}
|
|
6969
7630
|
}
|
|
6970
7631
|
function resolveActiveChangeProfile(rootDir) {
|
|
6971
|
-
const activeRoot =
|
|
6972
|
-
if (!
|
|
7632
|
+
const activeRoot = path5.join(rootDir, "specs", "changes", "active");
|
|
7633
|
+
if (!fs8.existsSync(activeRoot)) return "standard";
|
|
6973
7634
|
try {
|
|
6974
|
-
const dirs =
|
|
7635
|
+
const dirs = fs8.readdirSync(activeRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path5.join(activeRoot, d.name));
|
|
6975
7636
|
const profiles = dirs.map((d) => detectProfileFromChange(d));
|
|
6976
7637
|
if (profiles.includes("critical")) return "critical";
|
|
6977
7638
|
if (profiles.includes("patch") && profiles.length === 1) return "patch";
|
|
@@ -7108,7 +7769,7 @@ var REQ_TYPES = /* @__PURE__ */ new Set(["requirement", "security-requirement",
|
|
|
7108
7769
|
var REQ_CATEGORIES = /* @__PURE__ */ new Set(["functional", "quality", "constraint", "security", "safety"]);
|
|
7109
7770
|
function computeSha256(content) {
|
|
7110
7771
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
7111
|
-
return
|
|
7772
|
+
return crypto3.createHash("sha256").update(normalized, "utf-8").digest("hex");
|
|
7112
7773
|
}
|
|
7113
7774
|
function tokenizeTitle(title) {
|
|
7114
7775
|
const words = title.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
@@ -7184,11 +7845,11 @@ function buildRequirementObject(content, fm, body, filePath, relFile) {
|
|
|
7184
7845
|
}
|
|
7185
7846
|
function parseRequirementFile(filePath, rootDir) {
|
|
7186
7847
|
try {
|
|
7187
|
-
const content =
|
|
7848
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
7188
7849
|
const extracted = extractParsedFrontmatter(content);
|
|
7189
7850
|
if (!extracted) return null;
|
|
7190
7851
|
const body = content.substring(extracted.matchLen);
|
|
7191
|
-
const relFile =
|
|
7852
|
+
const relFile = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
7192
7853
|
return buildRequirementObject(content, extracted.fm, body, filePath, relFile);
|
|
7193
7854
|
} catch {
|
|
7194
7855
|
return null;
|
|
@@ -7200,7 +7861,7 @@ function isIgnoredScanPath(relFile) {
|
|
|
7200
7861
|
function collectRequirements(rootDir) {
|
|
7201
7862
|
const reqs = [];
|
|
7202
7863
|
for (const file of walkMdFiles(rootDir)) {
|
|
7203
|
-
const relFile =
|
|
7864
|
+
const relFile = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
7204
7865
|
if (isIgnoredScanPath(relFile)) continue;
|
|
7205
7866
|
const req = parseRequirementFile(file, rootDir);
|
|
7206
7867
|
if (req) reqs.push(req);
|
|
@@ -7334,7 +7995,7 @@ function buildDuplicatesHeader(result) {
|
|
|
7334
7995
|
return [
|
|
7335
7996
|
`# \u{1F6E1}\uFE0F Duplicate Requirements and Redundancy Audit (Shift-Left Pre-Flight)`,
|
|
7336
7997
|
``,
|
|
7337
|
-
`> **Evaluation Date:** ${(
|
|
7998
|
+
`> **Evaluation Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
7338
7999
|
`> **Verdict:** ${isOk ? "\u2705 COMPLIANT (No blocking duplicates)" : `\u274C BLOCKED (${result.errorCount} duplicate errors detected)`}`,
|
|
7339
8000
|
`> **Audited Requirements:** ${result.totalRequirements} | **Errors:** ${result.errorCount} | **Warnings:** ${result.warningCount}`,
|
|
7340
8001
|
``,
|
|
@@ -7457,7 +8118,7 @@ function buildAuditHeader(total, active, expired, invalid) {
|
|
|
7457
8118
|
return [
|
|
7458
8119
|
"# \u{1F4CB} Security Suppressions and Audit Exceptions Inventory",
|
|
7459
8120
|
"",
|
|
7460
|
-
`> **Generated at:** ${(
|
|
8121
|
+
`> **Generated at:** ${resolveDeterministicDate().toISOString()}`,
|
|
7461
8122
|
`> **Total Audited Suppressions:** ${total} | **Active:** ${active} | **Expired:** ${expired} | **Invalid:** ${invalid}`,
|
|
7462
8123
|
"",
|
|
7463
8124
|
"## 1. Executive Summary",
|
|
@@ -7688,10 +8349,10 @@ function isPathExcluded2(relPath, patterns) {
|
|
|
7688
8349
|
function walkProjectFiles(dir, rootDir, excludedPatterns = DEFAULT_EXCLUDED_PATTERNS) {
|
|
7689
8350
|
const results = [];
|
|
7690
8351
|
try {
|
|
7691
|
-
const entries =
|
|
8352
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true }).sort((a, b) => canonicalSort(a.name, b.name));
|
|
7692
8353
|
for (const entry of entries) {
|
|
7693
|
-
const fullPath =
|
|
7694
|
-
const relPath =
|
|
8354
|
+
const fullPath = path5.join(dir, entry.name);
|
|
8355
|
+
const relPath = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
7695
8356
|
if (isPathExcluded2(relPath, excludedPatterns)) continue;
|
|
7696
8357
|
if (entry.isDirectory()) {
|
|
7697
8358
|
results.push(...walkProjectFiles(fullPath, rootDir, excludedPatterns));
|
|
@@ -7720,8 +8381,8 @@ function pickFirstString(...vals) {
|
|
|
7720
8381
|
}
|
|
7721
8382
|
function parseGitleaksItem(item, rootDir) {
|
|
7722
8383
|
const rawFile = pickFirstString(item.File, item.file);
|
|
7723
|
-
const filePath =
|
|
7724
|
-
const relPath =
|
|
8384
|
+
const filePath = path5.resolve(rootDir, rawFile);
|
|
8385
|
+
const relPath = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
7725
8386
|
const ruleId = pickFirstString(item.RuleID, item.ruleID) || "GITLEAKS-MATCH";
|
|
7726
8387
|
const rawSecret = pickFirstString(item.Secret, item.secret, item.Match);
|
|
7727
8388
|
const line = item.StartLine ?? item.line ?? 1;
|
|
@@ -7794,7 +8455,7 @@ function scanLineForSecrets(line, lineNumber, filePath, relPath, entropyThreshol
|
|
|
7794
8455
|
function scanContentForSecrets(content, filePath, rootDir, entropyThreshold = 4.3, collectedSuppressions) {
|
|
7795
8456
|
const violations = [];
|
|
7796
8457
|
const lines = content.split(/\r?\n/);
|
|
7797
|
-
const relPath =
|
|
8458
|
+
const relPath = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
7798
8459
|
for (let i = 0; i < lines.length; i++) {
|
|
7799
8460
|
violations.push(...scanLineForSecrets(lines[i], i + 1, filePath, relPath, entropyThreshold, collectedSuppressions));
|
|
7800
8461
|
}
|
|
@@ -7815,7 +8476,7 @@ function scanDiffItems(diffLines, rootDir, excludePatterns, entropyThreshold) {
|
|
|
7815
8476
|
const suppressions = [];
|
|
7816
8477
|
for (const [relFile, items] of fileGroups) {
|
|
7817
8478
|
if (isPathExcluded2(relFile, excludePatterns)) continue;
|
|
7818
|
-
const absPath =
|
|
8479
|
+
const absPath = path5.resolve(rootDir, relFile);
|
|
7819
8480
|
for (const item of items) {
|
|
7820
8481
|
const lineViolations = scanLineForSecrets(item.content, item.lineNumber, absPath, relFile, entropyThreshold, suppressions);
|
|
7821
8482
|
for (const v of lineViolations) {
|
|
@@ -7831,7 +8492,7 @@ function scanAllProjectFiles(rootDir, excludePatterns, entropyThreshold) {
|
|
|
7831
8492
|
const suppressions = [];
|
|
7832
8493
|
for (const file of allFiles) {
|
|
7833
8494
|
try {
|
|
7834
|
-
const content =
|
|
8495
|
+
const content = fs8.readFileSync(file, "utf-8");
|
|
7835
8496
|
findings.push(...scanContentForSecrets(content, file, rootDir, entropyThreshold, suppressions));
|
|
7836
8497
|
} catch {
|
|
7837
8498
|
}
|
|
@@ -7888,7 +8549,7 @@ function buildReportHeader2(result) {
|
|
|
7888
8549
|
return [
|
|
7889
8550
|
`# \u{1F510} Deterministic Secrets and Credentials Audit (Gitleaks Gate)`,
|
|
7890
8551
|
``,
|
|
7891
|
-
`> **Evaluation Date:** ${(
|
|
8552
|
+
`> **Evaluation Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
7892
8553
|
`> **Verdict:** ${isOk ? "\u2705 COMPLIANT (No exposed secrets or credentials)" : `\u274C BLOCKED (${result.findingsCount} exposed credentials detected)`}`,
|
|
7893
8554
|
`> **Audited Files:** ${result.totalFilesScanned} | **Security Violations:** ${result.findingsCount}`,
|
|
7894
8555
|
`> **Native Gitleaks Engine:** ${result.scannedWithGitleaks ? "\u{1F7E2} Active / Integrated" : "\u26AA Deterministic Native Scanner (@aisdlc/core)"}`,
|
|
@@ -8057,8 +8718,8 @@ function runSemgrepCli(rootDir) {
|
|
|
8057
8718
|
const parsed = JSON.parse(output);
|
|
8058
8719
|
if (Array.isArray(parsed.results)) {
|
|
8059
8720
|
for (const item of parsed.results) {
|
|
8060
|
-
const filePath =
|
|
8061
|
-
const relPath =
|
|
8721
|
+
const filePath = path5.resolve(rootDir, item.path);
|
|
8722
|
+
const relPath = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
8062
8723
|
violations.push({
|
|
8063
8724
|
filePath,
|
|
8064
8725
|
relPath,
|
|
@@ -8085,15 +8746,15 @@ function isSastExcluded(relPath, excludes) {
|
|
|
8085
8746
|
function walkSourceFiles(dir, rootDir, excludes = DEFAULT_SAST_EXCLUDES) {
|
|
8086
8747
|
const files = [];
|
|
8087
8748
|
try {
|
|
8088
|
-
const entries =
|
|
8749
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true }).sort((a, b) => canonicalSort(a.name, b.name));
|
|
8089
8750
|
for (const entry of entries) {
|
|
8090
|
-
const fullPath =
|
|
8091
|
-
const relPath =
|
|
8751
|
+
const fullPath = path5.join(dir, entry.name);
|
|
8752
|
+
const relPath = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
8092
8753
|
if (isSastExcluded(relPath, excludes)) continue;
|
|
8093
8754
|
if (entry.isDirectory()) {
|
|
8094
8755
|
files.push(...walkSourceFiles(fullPath, rootDir, excludes));
|
|
8095
8756
|
} else if (entry.isFile()) {
|
|
8096
|
-
const ext =
|
|
8757
|
+
const ext = path5.extname(entry.name).toLowerCase();
|
|
8097
8758
|
if (GENERAL_SOURCE_EXTENSIONS.includes(ext)) {
|
|
8098
8759
|
files.push(fullPath);
|
|
8099
8760
|
}
|
|
@@ -8118,11 +8779,11 @@ function checkRuleMatch(rule, line, ext, minSeverity) {
|
|
|
8118
8779
|
}
|
|
8119
8780
|
function scanFileForSast(filePath, rootDir, minSeverity = "HIGH", collectedSuppressions) {
|
|
8120
8781
|
const violations = [];
|
|
8121
|
-
const ext =
|
|
8782
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
8122
8783
|
try {
|
|
8123
|
-
const content =
|
|
8784
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
8124
8785
|
const lines = content.split(/\r?\n/);
|
|
8125
|
-
const relPath =
|
|
8786
|
+
const relPath = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
8126
8787
|
for (let i = 0; i < lines.length; i++) {
|
|
8127
8788
|
const line = lines[i];
|
|
8128
8789
|
if (checkLineSuppression(line, relPath, i + 1, collectedSuppressions)) continue;
|
|
@@ -8148,8 +8809,8 @@ function scanFileForSast(filePath, rootDir, minSeverity = "HIGH", collectedSuppr
|
|
|
8148
8809
|
function collectSastFiles(rootDir, targetDirs) {
|
|
8149
8810
|
const files = [];
|
|
8150
8811
|
for (const tDir of targetDirs) {
|
|
8151
|
-
const fullDir =
|
|
8152
|
-
if (
|
|
8812
|
+
const fullDir = path5.join(rootDir, tDir);
|
|
8813
|
+
if (fs8.existsSync(fullDir)) {
|
|
8153
8814
|
files.push(...walkSourceFiles(fullDir, rootDir));
|
|
8154
8815
|
}
|
|
8155
8816
|
}
|
|
@@ -8195,7 +8856,7 @@ function buildSastHeader(isOk, violationsCount, totalFilesScanned) {
|
|
|
8195
8856
|
return [
|
|
8196
8857
|
`# \u{1F6E1}\uFE0F Shift-Left SAST Audit (Static Application Security Testing)`,
|
|
8197
8858
|
``,
|
|
8198
|
-
`> **Evaluation Date:** ${(
|
|
8859
|
+
`> **Evaluation Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
8199
8860
|
`> **Verdict:** ${verdict}`,
|
|
8200
8861
|
`> **Audited Code Files:** ${totalFilesScanned} | **SAST Violations:** ${violationsCount}`,
|
|
8201
8862
|
``,
|
|
@@ -8384,22 +9045,31 @@ var PII_SAST_RULES = [
|
|
|
8384
9045
|
pattern: /\b(?:ssn|dni|nif|tax_?id|national_?id)\s*[:=]\s*['"](?:\d{3}-\d{2}-\d{4}|\d{8}[A-HJ-NP-TV-Z])['"]/i
|
|
8385
9046
|
}
|
|
8386
9047
|
];
|
|
8387
|
-
function
|
|
8388
|
-
|
|
8389
|
-
if (!fs7.existsSync(candidate)) return DEFAULT_PRIVACY_POLICY;
|
|
9048
|
+
function parsePrivacyDoc(raw, candidate) {
|
|
9049
|
+
let parsed;
|
|
8390
9050
|
try {
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
agent_guardrails: { ...DEFAULT_PRIVACY_POLICY.agent_guardrails, ...parsed.agent_guardrails || {} },
|
|
8398
|
-
ci_gates: { ...DEFAULT_PRIVACY_POLICY.ci_gates, ...parsed.ci_gates || {} }
|
|
8399
|
-
};
|
|
8400
|
-
} catch {
|
|
8401
|
-
return DEFAULT_PRIVACY_POLICY;
|
|
9051
|
+
parsed = yaml3.load(raw);
|
|
9052
|
+
} catch (err) {
|
|
9053
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid YAML in privacy-policy '${candidate}': ${err instanceof Error ? err.message : String(err)}`);
|
|
9054
|
+
}
|
|
9055
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9056
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid privacy policy structure in '${candidate}': root must be a YAML mapping`);
|
|
8402
9057
|
}
|
|
9058
|
+
return parsed;
|
|
9059
|
+
}
|
|
9060
|
+
function loadPrivacyPolicy(rootDir = process.cwd(), policyPath) {
|
|
9061
|
+
const candidate = policyPath ? path5.isAbsolute(policyPath) ? policyPath : path5.join(rootDir, policyPath) : path5.join(rootDir, "privacy-policy.yaml");
|
|
9062
|
+
if (!fs8.existsSync(candidate)) return DEFAULT_PRIVACY_POLICY;
|
|
9063
|
+
const raw = fs8.readFileSync(candidate, "utf-8");
|
|
9064
|
+
if (!raw || !raw.trim()) return DEFAULT_PRIVACY_POLICY;
|
|
9065
|
+
const parsed = parsePrivacyDoc(raw, candidate);
|
|
9066
|
+
return {
|
|
9067
|
+
...DEFAULT_PRIVACY_POLICY,
|
|
9068
|
+
...parsed,
|
|
9069
|
+
classifications: { ...DEFAULT_PRIVACY_POLICY.classifications, ...parsed.classifications || {} },
|
|
9070
|
+
agent_guardrails: { ...DEFAULT_PRIVACY_POLICY.agent_guardrails, ...parsed.agent_guardrails || {} },
|
|
9071
|
+
ci_gates: { ...DEFAULT_PRIVACY_POLICY.ci_gates, ...parsed.ci_gates || {} }
|
|
9072
|
+
};
|
|
8403
9073
|
}
|
|
8404
9074
|
function isLineExemptFromPrivacyScan(line) {
|
|
8405
9075
|
return line.includes("ai-sdlc:allow-pii") || line.includes("ai-sdlc:allow-sast") || line.includes("nosec");
|
|
@@ -8416,9 +9086,9 @@ function shouldApplyPiiRule(rule, policy) {
|
|
|
8416
9086
|
function scanFileForPrivacyViolations(filePath, rootDir, policy = DEFAULT_PRIVACY_POLICY) {
|
|
8417
9087
|
const violations = [];
|
|
8418
9088
|
try {
|
|
8419
|
-
const content =
|
|
9089
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
8420
9090
|
const lines = content.split(/\r?\n/);
|
|
8421
|
-
const relPath =
|
|
9091
|
+
const relPath = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
8422
9092
|
for (let i = 0; i < lines.length; i++) {
|
|
8423
9093
|
const line = lines[i];
|
|
8424
9094
|
if (isLineExemptFromPrivacyScan(line)) continue;
|
|
@@ -8527,10 +9197,10 @@ function discoverPrivacyArtifacts(rootDir) {
|
|
|
8527
9197
|
dpiaStatuses: /* @__PURE__ */ new Map()
|
|
8528
9198
|
};
|
|
8529
9199
|
for (const file of walkMdFiles(rootDir)) {
|
|
8530
|
-
const relPath =
|
|
9200
|
+
const relPath = path5.relative(rootDir, file).replace(/\\/g, "/");
|
|
8531
9201
|
if (isExcludedPrivacyMd(relPath)) continue;
|
|
8532
9202
|
try {
|
|
8533
|
-
const fm = extractFrontmatter(
|
|
9203
|
+
const fm = extractFrontmatter(fs8.readFileSync(file, "utf-8"));
|
|
8534
9204
|
if (fm && fm.id) classifyPrivacyMarkdown(fm, relPath, acc);
|
|
8535
9205
|
} catch {
|
|
8536
9206
|
}
|
|
@@ -8575,7 +9245,7 @@ function checkAssetRopaRegistration(asset, ropaRecords, policy, fullPath) {
|
|
|
8575
9245
|
}
|
|
8576
9246
|
function auditDataAssetGovernance(asset, ropaRecords, policy, rootDir) {
|
|
8577
9247
|
if (!isSensitiveClassification(asset.classification)) return [];
|
|
8578
|
-
const fullPath =
|
|
9248
|
+
const fullPath = path5.join(rootDir, asset.filePath);
|
|
8579
9249
|
const v1 = checkAssetRetention(asset, policy, fullPath);
|
|
8580
9250
|
const v2 = checkAssetRopaRegistration(asset, ropaRecords, policy, fullPath);
|
|
8581
9251
|
return [v1, v2].filter((v) => v !== null);
|
|
@@ -8647,8 +9317,8 @@ function checkRopaDpiaApproval(ropa, dataAssets, dpiaStatuses, fullPath) {
|
|
|
8647
9317
|
function collectSourceFilesForPrivacy(rootDir, targetDirs) {
|
|
8648
9318
|
const files = [];
|
|
8649
9319
|
for (const dir of targetDirs) {
|
|
8650
|
-
const fullDir =
|
|
8651
|
-
if (
|
|
9320
|
+
const fullDir = path5.join(rootDir, dir);
|
|
9321
|
+
if (fs8.existsSync(fullDir)) {
|
|
8652
9322
|
files.push(...walkSourceFiles(fullDir, rootDir));
|
|
8653
9323
|
}
|
|
8654
9324
|
}
|
|
@@ -8738,15 +9408,13 @@ function generatePrivacyReportMarkdown(result, policy = DEFAULT_PRIVACY_POLICY)
|
|
|
8738
9408
|
function writePrivacyOutputsIfNeeded(result, options, policy, rootDir) {
|
|
8739
9409
|
if (options.generateRopa) {
|
|
8740
9410
|
const relRopa = options.ropaOutputPath || policy.ci_gates.output_ropa_file || "reports/ROPA_INVENTORY.md";
|
|
8741
|
-
const fullRopa =
|
|
8742
|
-
|
|
8743
|
-
fs7.writeFileSync(fullRopa, result.ropaMarkdown, "utf-8");
|
|
9411
|
+
const fullRopa = path5.isAbsolute(relRopa) ? relRopa : path5.join(rootDir, relRopa);
|
|
9412
|
+
writeReportFileIfChanged(fullRopa, result.ropaMarkdown);
|
|
8744
9413
|
result.ropaOutputPath = fullRopa;
|
|
8745
9414
|
}
|
|
8746
9415
|
if (options.reportOutputPath) {
|
|
8747
|
-
const fullReport =
|
|
8748
|
-
|
|
8749
|
-
fs7.writeFileSync(fullReport, result.reportMarkdown, "utf-8");
|
|
9416
|
+
const fullReport = path5.isAbsolute(options.reportOutputPath) ? options.reportOutputPath : path5.join(rootDir, options.reportOutputPath);
|
|
9417
|
+
writeReportFileIfChanged(fullReport, result.reportMarkdown);
|
|
8750
9418
|
result.reportOutputPath = fullReport;
|
|
8751
9419
|
}
|
|
8752
9420
|
}
|
|
@@ -8758,7 +9426,7 @@ function evaluateDiscoveredPrivacyGovernance(discovered, policy, rootDir) {
|
|
|
8758
9426
|
}
|
|
8759
9427
|
let dpiaRequiredCount = 0;
|
|
8760
9428
|
for (const ropa of discovered.ropaRecords) {
|
|
8761
|
-
const fullPath =
|
|
9429
|
+
const fullPath = path5.join(rootDir, ropa.filePath);
|
|
8762
9430
|
violations.push(...checkRopaControlsAndSubprocessors(ropa, governedRopaIds, policy, fullPath));
|
|
8763
9431
|
const dpiaCheck = checkRopaDpiaApproval(ropa, discovered.dataAssets, discovered.dpiaStatuses, fullPath);
|
|
8764
9432
|
if (dpiaCheck.dpiaRequired) dpiaRequiredCount++;
|
|
@@ -8804,7 +9472,7 @@ function calculateRating(mi, cc) {
|
|
|
8804
9472
|
return "F";
|
|
8805
9473
|
}
|
|
8806
9474
|
function detectLanguage(filePath) {
|
|
8807
|
-
const ext =
|
|
9475
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
8808
9476
|
switch (ext) {
|
|
8809
9477
|
case ".ts":
|
|
8810
9478
|
return "TypeScript";
|
|
@@ -8829,7 +9497,7 @@ function detectLanguage(filePath) {
|
|
|
8829
9497
|
}
|
|
8830
9498
|
function parseJsonSummaryFile(filePath, relSource) {
|
|
8831
9499
|
try {
|
|
8832
|
-
const raw = JSON.parse(
|
|
9500
|
+
const raw = JSON.parse(fs8.readFileSync(filePath, "utf-8"));
|
|
8833
9501
|
const total = raw.total;
|
|
8834
9502
|
if (!total || typeof total !== "object") return null;
|
|
8835
9503
|
const linesPct = Number(total.lines?.pct);
|
|
@@ -8852,7 +9520,7 @@ function updateLcovCounts(line, counts) {
|
|
|
8852
9520
|
}
|
|
8853
9521
|
function parseLcovFile(filePath, relSource) {
|
|
8854
9522
|
try {
|
|
8855
|
-
const lines =
|
|
9523
|
+
const lines = fs8.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
8856
9524
|
const counts = { LF: 0, LH: 0, BRF: 0, BRH: 0, FNF: 0, FNH: 0 };
|
|
8857
9525
|
for (const line of lines) {
|
|
8858
9526
|
updateLcovCounts(line, counts);
|
|
@@ -8872,16 +9540,16 @@ function parseLcovFile(filePath, relSource) {
|
|
|
8872
9540
|
function readCodeCoverageSummary(rootDir = process.cwd()) {
|
|
8873
9541
|
const jsonCandidates = ["coverage/coverage-summary.json", "reports/coverage-summary.json"];
|
|
8874
9542
|
for (const rel of jsonCandidates) {
|
|
8875
|
-
const full =
|
|
8876
|
-
if (
|
|
9543
|
+
const full = path5.join(rootDir, rel);
|
|
9544
|
+
if (fs8.existsSync(full)) {
|
|
8877
9545
|
const parsed = parseJsonSummaryFile(full, rel);
|
|
8878
9546
|
if (parsed) return parsed;
|
|
8879
9547
|
}
|
|
8880
9548
|
}
|
|
8881
9549
|
const lcovCandidates = ["coverage/lcov.info", "lcov.info", "reports/lcov.info"];
|
|
8882
9550
|
for (const rel of lcovCandidates) {
|
|
8883
|
-
const full =
|
|
8884
|
-
if (
|
|
9551
|
+
const full = path5.join(rootDir, rel);
|
|
9552
|
+
if (fs8.existsSync(full)) {
|
|
8885
9553
|
const parsed = parseLcovFile(full, rel);
|
|
8886
9554
|
if (parsed) return parsed;
|
|
8887
9555
|
}
|
|
@@ -8966,7 +9634,7 @@ function buildReportHeader3(verdict, rating, mi, cc) {
|
|
|
8966
9634
|
return [
|
|
8967
9635
|
`# \u{1F4CA} Formal Quality and Release Gate Report (AI-SDLC)`,
|
|
8968
9636
|
"",
|
|
8969
|
-
`> **Generation Date:** ${(
|
|
9637
|
+
`> **Generation Date:** ${resolveDeterministicDate().toISOString()}`,
|
|
8970
9638
|
`> **Release Gate Verdict:** ${status}`,
|
|
8971
9639
|
`> **Global Rating:** **\`${rating}\`** (MI Index: ${mi}/100, Average CC: ${cc})`
|
|
8972
9640
|
];
|
|
@@ -9017,9 +9685,8 @@ function renderQualityReportMarkdown(gateResult, coverage, accumulated, averages
|
|
|
9017
9685
|
return [...header, "", "---", "", ...table, "", "---", "", ...polyglot, ...violationsSection, "", EVALUATION_STANDARDS_MD].join("\n");
|
|
9018
9686
|
}
|
|
9019
9687
|
function writeQualityReportFile(rootDir, markdown) {
|
|
9020
|
-
const
|
|
9021
|
-
|
|
9022
|
-
fs7.writeFileSync(path4.join(reportsDir, "QUALITY_REPORT.md"), markdown, "utf-8");
|
|
9688
|
+
const targetPath = path5.join(rootDir, "reports", "QUALITY_REPORT.md");
|
|
9689
|
+
return writeReportFileIfChanged(targetPath, markdown);
|
|
9023
9690
|
}
|
|
9024
9691
|
function generateQualityReport(options = {}) {
|
|
9025
9692
|
const rootDir = options.rootDir || process.cwd();
|
|
@@ -9161,9 +9828,9 @@ Flujo de Integraci\xF3n (Merges):`,
|
|
|
9161
9828
|
};
|
|
9162
9829
|
}
|
|
9163
9830
|
function resolveVersionFromProposal(changeDir) {
|
|
9164
|
-
const proposalFile =
|
|
9165
|
-
if (!
|
|
9166
|
-
const propContent =
|
|
9831
|
+
const proposalFile = path5.join(changeDir, "proposal.md");
|
|
9832
|
+
if (!fs8.existsSync(proposalFile)) return "1.0.0";
|
|
9833
|
+
const propContent = fs8.readFileSync(proposalFile, "utf-8");
|
|
9167
9834
|
const propFm = propContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
9168
9835
|
if (!propFm) return "1.0.0";
|
|
9169
9836
|
const parsedProp = yaml3.load(propFm[1]);
|
|
@@ -9180,9 +9847,9 @@ function isTaskMatchingId(currentId, cleanTaskId) {
|
|
|
9180
9847
|
return currentLower === targetLower || currentLower.startsWith(`${targetLower}-`) || targetLower.startsWith(`${currentLower}-`);
|
|
9181
9848
|
}
|
|
9182
9849
|
function parseTasksFrontmatter(tasksFile) {
|
|
9183
|
-
if (!
|
|
9850
|
+
if (!fs8.existsSync(tasksFile)) return null;
|
|
9184
9851
|
try {
|
|
9185
|
-
const content =
|
|
9852
|
+
const content = fs8.readFileSync(tasksFile, "utf-8");
|
|
9186
9853
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
9187
9854
|
if (!fmMatch) return null;
|
|
9188
9855
|
const fm = yaml3.load(fmMatch[1]);
|
|
@@ -9204,7 +9871,7 @@ function buildTaskMatch(currentId, currentTitle, rawChangeId, changeDir, version
|
|
|
9204
9871
|
};
|
|
9205
9872
|
}
|
|
9206
9873
|
function inspectChangeTasks(changeDir, entryName, cleanTaskId, availableTasks) {
|
|
9207
|
-
const fm = parseTasksFrontmatter(
|
|
9874
|
+
const fm = parseTasksFrontmatter(path5.join(changeDir, "tasks.md"));
|
|
9208
9875
|
if (!fm || !fm.tasks) return void 0;
|
|
9209
9876
|
const rawChangeId = resolveRawChangeId(fm, entryName);
|
|
9210
9877
|
const versionStr = resolveChangeVersion(fm, changeDir);
|
|
@@ -9225,16 +9892,16 @@ function locateTaskInActiveChanges(taskId, rootDir = process.cwd()) {
|
|
|
9225
9892
|
if (!cleanTaskId) {
|
|
9226
9893
|
return { found: false, error: "Debe especificar un identificador de tarea v\xE1lido (ej. TSK-001)." };
|
|
9227
9894
|
}
|
|
9228
|
-
const activeChangesDir =
|
|
9229
|
-
if (!
|
|
9895
|
+
const activeChangesDir = path5.join(rootDir, "specs", "changes", "active");
|
|
9896
|
+
if (!fs8.existsSync(activeChangesDir)) {
|
|
9230
9897
|
return { found: false, error: `No se encontr\xF3 el directorio de cambios activos en '${activeChangesDir}'.` };
|
|
9231
9898
|
}
|
|
9232
9899
|
const availableTasks = [];
|
|
9233
|
-
const candidateDirs =
|
|
9900
|
+
const candidateDirs = fs8.readdirSync(activeChangesDir, { withFileTypes: true });
|
|
9234
9901
|
let matchedTask;
|
|
9235
9902
|
for (const entry of candidateDirs) {
|
|
9236
9903
|
if (!entry.isDirectory()) continue;
|
|
9237
|
-
const changeDir =
|
|
9904
|
+
const changeDir = path5.join(activeChangesDir, entry.name);
|
|
9238
9905
|
const m = inspectChangeTasks(changeDir, entry.name, cleanTaskId, availableTasks);
|
|
9239
9906
|
if (m && !matchedTask) {
|
|
9240
9907
|
matchedTask = m;
|
|
@@ -9271,14 +9938,14 @@ function detectExistingFeatureBranch(branches, dirName, changeId, changeSlug) {
|
|
|
9271
9938
|
(b) => b.startsWith("feat/") && (b.toLowerCase().includes(dirName.toLowerCase()) || b.toLowerCase().includes(changeId.toLowerCase())) && classifyBranch(b).valid
|
|
9272
9939
|
);
|
|
9273
9940
|
if (existing) return existing;
|
|
9274
|
-
const normalizedName =
|
|
9941
|
+
const normalizedName = slugify2(changeSlug).replace(/^(chg|bug|feat)-(\d+)/i, (_, p, n) => `${p.toUpperCase()}-${n}`);
|
|
9275
9942
|
return `feat/${normalizedName}`;
|
|
9276
9943
|
}
|
|
9277
9944
|
function buildDefaultTaskSuffix(taskId, taskTitle) {
|
|
9278
9945
|
let taskSuffix = taskId.toLowerCase();
|
|
9279
9946
|
const hasSlug = taskSuffix.includes("-") && taskSuffix.split("-").length > 2;
|
|
9280
9947
|
if (!hasSlug && taskTitle) {
|
|
9281
|
-
const titleSlug =
|
|
9948
|
+
const titleSlug = slugify2(taskTitle).split("-").slice(0, 4).join("-");
|
|
9282
9949
|
if (titleSlug) {
|
|
9283
9950
|
taskSuffix = `${taskSuffix}-${titleSlug}`;
|
|
9284
9951
|
}
|
|
@@ -9298,7 +9965,7 @@ function resolveBranchHierarchy(match, rootDir = process.cwd()) {
|
|
|
9298
9965
|
const branches = getGitBranchList(rootDir);
|
|
9299
9966
|
const cleanVersion = (match.version || "1.0.0").replace(/^v/, "");
|
|
9300
9967
|
const releaseBranch = detectExistingReleaseBranch(branches, cleanVersion);
|
|
9301
|
-
const dirName =
|
|
9968
|
+
const dirName = path5.basename(match.changeDir);
|
|
9302
9969
|
const changeSlug = dirName.replace(/^(chg|CHG)-/i, (m) => m.toUpperCase());
|
|
9303
9970
|
const featureBranch = detectExistingFeatureBranch(branches, dirName, match.changeId, changeSlug);
|
|
9304
9971
|
const parentIdMatch = featureBranch.replace(/^(feat|feature)\//i, "").match(/^[a-zA-Z0-9]+-[a-zA-Z0-9]+/);
|
|
@@ -9605,13 +10272,13 @@ function getGitConfigValue(key, cwd) {
|
|
|
9605
10272
|
}
|
|
9606
10273
|
function readSessionTelemetry(cwd) {
|
|
9607
10274
|
const possiblePaths = [
|
|
9608
|
-
|
|
9609
|
-
|
|
10275
|
+
path5.join(cwd, ".ai-sdlc", "session-telemetry.json"),
|
|
10276
|
+
path5.join(cwd, ".ai-sdlc", "agent-session.json")
|
|
9610
10277
|
];
|
|
9611
10278
|
for (const p of possiblePaths) {
|
|
9612
|
-
if (
|
|
10279
|
+
if (fs8.existsSync(p)) {
|
|
9613
10280
|
try {
|
|
9614
|
-
const raw =
|
|
10281
|
+
const raw = fs8.readFileSync(p, "utf-8");
|
|
9615
10282
|
return JSON.parse(raw);
|
|
9616
10283
|
} catch {
|
|
9617
10284
|
}
|
|
@@ -9831,20 +10498,20 @@ if [ -f "$TELEMETRY_FILE" ]; then
|
|
|
9831
10498
|
fi
|
|
9832
10499
|
`;
|
|
9833
10500
|
function installGitHooks(rootDir = process.cwd()) {
|
|
9834
|
-
const gitDir =
|
|
9835
|
-
if (!
|
|
10501
|
+
const gitDir = path5.join(rootDir, ".git");
|
|
10502
|
+
if (!fs8.existsSync(gitDir)) {
|
|
9836
10503
|
return {
|
|
9837
10504
|
success: false,
|
|
9838
10505
|
error: `No se encontr\xF3 el directorio Git en '${gitDir}'.`
|
|
9839
10506
|
};
|
|
9840
10507
|
}
|
|
9841
|
-
const hooksDir =
|
|
9842
|
-
if (!
|
|
9843
|
-
|
|
10508
|
+
const hooksDir = path5.join(gitDir, "hooks");
|
|
10509
|
+
if (!fs8.existsSync(hooksDir)) {
|
|
10510
|
+
fs8.mkdirSync(hooksDir, { recursive: true });
|
|
9844
10511
|
}
|
|
9845
|
-
const hookPath =
|
|
10512
|
+
const hookPath = path5.join(hooksDir, "prepare-commit-msg");
|
|
9846
10513
|
try {
|
|
9847
|
-
|
|
10514
|
+
fs8.writeFileSync(hookPath, PREPARE_COMMIT_MSG_SCRIPT, { encoding: "utf-8", mode: 493 });
|
|
9848
10515
|
return {
|
|
9849
10516
|
success: true,
|
|
9850
10517
|
hookPath
|
|
@@ -10185,16 +10852,16 @@ function formatReleaseKpiMarkdown(report) {
|
|
|
10185
10852
|
].join("\n");
|
|
10186
10853
|
}
|
|
10187
10854
|
function writeReleaseKpiReport(report, options = {}) {
|
|
10188
|
-
const root = options.outputDir || (options.rootDir ?
|
|
10189
|
-
if (!
|
|
10190
|
-
|
|
10855
|
+
const root = options.outputDir || (options.rootDir ? path5.join(options.rootDir, "reports", "releases") : path5.join(process.cwd(), "reports", "releases"));
|
|
10856
|
+
if (!fs8.existsSync(root)) {
|
|
10857
|
+
fs8.mkdirSync(root, { recursive: true });
|
|
10191
10858
|
}
|
|
10192
10859
|
const cleanBranchName = report.releaseBranch.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
10193
|
-
const mdPath =
|
|
10194
|
-
const jsonPath =
|
|
10860
|
+
const mdPath = path5.join(root, `RELEASE_KPIS_${cleanBranchName}.md`);
|
|
10861
|
+
const jsonPath = path5.join(root, `release_${cleanBranchName}.kpis.json`);
|
|
10195
10862
|
const mdContent = formatReleaseKpiMarkdown(report);
|
|
10196
|
-
|
|
10197
|
-
|
|
10863
|
+
fs8.writeFileSync(mdPath, mdContent, "utf-8");
|
|
10864
|
+
fs8.writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
10198
10865
|
return { markdownPath: mdPath, jsonPath };
|
|
10199
10866
|
}
|
|
10200
10867
|
var cachedScript = null;
|
|
@@ -10203,19 +10870,19 @@ function resolveCytoscapePath() {
|
|
|
10203
10870
|
try {
|
|
10204
10871
|
const req = createRequire(import.meta.url);
|
|
10205
10872
|
const entryPath = req.resolve("cytoscape");
|
|
10206
|
-
const dir =
|
|
10207
|
-
const minPath =
|
|
10208
|
-
if (
|
|
10873
|
+
const dir = path5.dirname(entryPath);
|
|
10874
|
+
const minPath = path5.join(dir, "cytoscape.min.js");
|
|
10875
|
+
if (fs8.existsSync(minPath)) {
|
|
10209
10876
|
return minPath;
|
|
10210
10877
|
}
|
|
10211
10878
|
} catch {
|
|
10212
10879
|
}
|
|
10213
10880
|
const searchDirs = [
|
|
10214
|
-
|
|
10215
|
-
|
|
10881
|
+
path5.join(process.cwd(), "node_modules", "cytoscape", "dist", "cytoscape.min.js"),
|
|
10882
|
+
path5.join(process.cwd(), "packages", "core", "node_modules", "cytoscape", "dist", "cytoscape.min.js")
|
|
10216
10883
|
];
|
|
10217
10884
|
for (const candidate of searchDirs) {
|
|
10218
|
-
if (
|
|
10885
|
+
if (fs8.existsSync(candidate)) {
|
|
10219
10886
|
return candidate;
|
|
10220
10887
|
}
|
|
10221
10888
|
}
|
|
@@ -10228,7 +10895,7 @@ function getCytoscapeScript() {
|
|
|
10228
10895
|
const scriptPath = resolveCytoscapePath();
|
|
10229
10896
|
if (scriptPath) {
|
|
10230
10897
|
try {
|
|
10231
|
-
cachedScript =
|
|
10898
|
+
cachedScript = fs8.readFileSync(scriptPath, "utf-8");
|
|
10232
10899
|
return cachedScript;
|
|
10233
10900
|
} catch {
|
|
10234
10901
|
}
|
|
@@ -10328,7 +10995,7 @@ function extractFrontmatter2(fileContent) {
|
|
|
10328
10995
|
}
|
|
10329
10996
|
}
|
|
10330
10997
|
function toScannedArtifact(filePath, relPath) {
|
|
10331
|
-
const fileContent =
|
|
10998
|
+
const fileContent = fs8.readFileSync(filePath, "utf-8");
|
|
10332
10999
|
const fm = extractFrontmatter2(fileContent);
|
|
10333
11000
|
const id = typeof fm?.id === "string" ? fm.id.trim() : "";
|
|
10334
11001
|
if (!id) return null;
|
|
@@ -10340,12 +11007,12 @@ function toScannedArtifact(filePath, relPath) {
|
|
|
10340
11007
|
return { id, type: classified.type, title, layer: classified.layer, filePath: relPath, content, frontmatter: fm };
|
|
10341
11008
|
}
|
|
10342
11009
|
function parseAndCatalogArtifactFile(rootDir, filePath, scannedMap, archViewByBasename) {
|
|
10343
|
-
const relPath =
|
|
11010
|
+
const relPath = path5.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
10344
11011
|
if (relPath.includes("/changes/") || relPath.startsWith("changes/")) return;
|
|
10345
11012
|
const scannedArt = toScannedArtifact(filePath, relPath);
|
|
10346
11013
|
if (!scannedArt) return;
|
|
10347
11014
|
if (scannedArt.layer === "architecture" && scannedArt.type === "architecture") {
|
|
10348
|
-
archViewByBasename.set(
|
|
11015
|
+
archViewByBasename.set(path5.basename(relPath), scannedArt);
|
|
10349
11016
|
} else {
|
|
10350
11017
|
scannedMap.set(scannedArt.id, scannedArt);
|
|
10351
11018
|
}
|
|
@@ -10355,8 +11022,8 @@ function scanWorkspaceArtifacts(rootDir) {
|
|
|
10355
11022
|
const archViewByBasename = /* @__PURE__ */ new Map();
|
|
10356
11023
|
const artifactDirs = ["product", "architecture", "specs", "security", "privacy"];
|
|
10357
11024
|
for (const dirName of artifactDirs) {
|
|
10358
|
-
const fullDirPath =
|
|
10359
|
-
if (
|
|
11025
|
+
const fullDirPath = path5.join(rootDir, dirName);
|
|
11026
|
+
if (fs8.existsSync(fullDirPath)) {
|
|
10360
11027
|
for (const f of walkMdFiles(fullDirPath)) {
|
|
10361
11028
|
parseAndCatalogArtifactFile(rootDir, f, scannedArtifactsMap, archViewByBasename);
|
|
10362
11029
|
}
|
|
@@ -10416,9 +11083,9 @@ function addTraceArchEdges(r, status, archViewByBasename, nodeMap, addEdge) {
|
|
|
10416
11083
|
}
|
|
10417
11084
|
}
|
|
10418
11085
|
function loadTestContentPreview(filePath) {
|
|
10419
|
-
if (!
|
|
11086
|
+
if (!fs8.existsSync(filePath)) return void 0;
|
|
10420
11087
|
try {
|
|
10421
|
-
const testContent =
|
|
11088
|
+
const testContent = fs8.readFileSync(filePath, "utf-8");
|
|
10422
11089
|
const lang = filePath.endsWith(".feature") ? "gherkin" : "typescript";
|
|
10423
11090
|
const preview = testContent.substring(0, 1500) + (testContent.length > 1500 ? "\n... (truncated)" : "");
|
|
10424
11091
|
return "```" + lang + "\n" + preview + "\n```";
|
|
@@ -10428,10 +11095,10 @@ function loadTestContentPreview(filePath) {
|
|
|
10428
11095
|
}
|
|
10429
11096
|
function ensureTestNode(t, testId, rootDir, nodeMap) {
|
|
10430
11097
|
if (nodeMap.has(testId)) return;
|
|
10431
|
-
const fullTestPath =
|
|
11098
|
+
const fullTestPath = path5.isAbsolute(t) ? t : path5.join(rootDir, t);
|
|
10432
11099
|
nodeMap.set(testId, {
|
|
10433
11100
|
id: testId,
|
|
10434
|
-
label:
|
|
11101
|
+
label: path5.basename(t),
|
|
10435
11102
|
title: t,
|
|
10436
11103
|
type: t.endsWith(".feature") ? "bdd-feature" : "code-test",
|
|
10437
11104
|
layer: "test",
|
|
@@ -12576,9 +13243,10 @@ ${manualsTab}
|
|
|
12576
13243
|
${kpisTab}
|
|
12577
13244
|
</main>`;
|
|
12578
13245
|
}
|
|
12579
|
-
function renderDashboardHtml(graphData, metricsData, title = "AI-SDLC: Dashboard Interactivo de Trazabilidad y Calidad") {
|
|
13246
|
+
function renderDashboardHtml(graphData, metricsData, title = "AI-SDLC: Dashboard Interactivo de Trazabilidad y Calidad", generationDate) {
|
|
12580
13247
|
const head = renderDashboardHead(title);
|
|
12581
|
-
const
|
|
13248
|
+
const effectiveDate = generationDate || resolveDeterministicDate().toISOString();
|
|
13249
|
+
const header = renderHeaderHtml(graphData.issueCount, effectiveDate);
|
|
12582
13250
|
const nav = renderNavTabsHtml();
|
|
12583
13251
|
const main = renderDashboardMain(metricsData);
|
|
12584
13252
|
const sidebar = renderTaskSidebarHtml();
|
|
@@ -12608,9 +13276,9 @@ var IGNORED_SCAN_DIRS = /* @__PURE__ */ new Set([
|
|
|
12608
13276
|
".agent"
|
|
12609
13277
|
]);
|
|
12610
13278
|
function readKpiFilesInDir(dir) {
|
|
12611
|
-
if (!
|
|
13279
|
+
if (!fs8.existsSync(dir)) return [];
|
|
12612
13280
|
try {
|
|
12613
|
-
return
|
|
13281
|
+
return fs8.readdirSync(dir).filter((f) => f.endsWith(".kpis.json")).map((f) => path5.join(dir, f));
|
|
12614
13282
|
} catch {
|
|
12615
13283
|
return [];
|
|
12616
13284
|
}
|
|
@@ -12618,11 +13286,11 @@ function readKpiFilesInDir(dir) {
|
|
|
12618
13286
|
function scanSubdirKpiFiles(dir, depth, acc) {
|
|
12619
13287
|
if (depth > 4) return;
|
|
12620
13288
|
try {
|
|
12621
|
-
const entries =
|
|
13289
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
12622
13290
|
for (const entry of entries) {
|
|
12623
13291
|
if (!entry.isDirectory() || IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
12624
|
-
const sub =
|
|
12625
|
-
if (entry.name === "releases" &&
|
|
13292
|
+
const sub = path5.join(dir, entry.name);
|
|
13293
|
+
if (entry.name === "releases" && path5.basename(dir) === "reports") {
|
|
12626
13294
|
acc.push(...readKpiFilesInDir(sub));
|
|
12627
13295
|
} else {
|
|
12628
13296
|
scanSubdirKpiFiles(sub, depth + 1, acc);
|
|
@@ -12632,7 +13300,7 @@ function scanSubdirKpiFiles(dir, depth, acc) {
|
|
|
12632
13300
|
}
|
|
12633
13301
|
}
|
|
12634
13302
|
function findKpiJsonFiles(rootDir) {
|
|
12635
|
-
const primaryFiles = readKpiFilesInDir(
|
|
13303
|
+
const primaryFiles = readKpiFilesInDir(path5.join(rootDir, "reports", "releases"));
|
|
12636
13304
|
if (primaryFiles.length > 0) return primaryFiles;
|
|
12637
13305
|
const found = [];
|
|
12638
13306
|
scanSubdirKpiFiles(rootDir, 0, found);
|
|
@@ -12643,7 +13311,7 @@ function collectHistoricalKpis(rootDir) {
|
|
|
12643
13311
|
const kpiFiles = findKpiJsonFiles(rootDir);
|
|
12644
13312
|
for (const fullPath of kpiFiles) {
|
|
12645
13313
|
try {
|
|
12646
|
-
const content =
|
|
13314
|
+
const content = fs8.readFileSync(fullPath, "utf-8");
|
|
12647
13315
|
const report = JSON.parse(content);
|
|
12648
13316
|
if (!items.some((it) => it.id === report.releaseBranch && it.timestamp === report.generatedAt)) {
|
|
12649
13317
|
items.push({
|
|
@@ -12849,15 +13517,11 @@ function computeGateAverages(gateResults) {
|
|
|
12849
13517
|
}
|
|
12850
13518
|
function generateDashboardReport(options = {}) {
|
|
12851
13519
|
const rootDir = options.rootDir || process.cwd();
|
|
12852
|
-
const outputPath = options.outputPath ||
|
|
13520
|
+
const outputPath = options.outputPath || path5.join(rootDir, "reports", "dashboard.html");
|
|
12853
13521
|
const graphData = buildGraphElements(rootDir);
|
|
12854
13522
|
const metricsData = collectDashboardMetrics(rootDir);
|
|
12855
|
-
const html = renderDashboardHtml(graphData, metricsData, options.title);
|
|
12856
|
-
|
|
12857
|
-
if (!fs7.existsSync(outDir)) {
|
|
12858
|
-
fs7.mkdirSync(outDir, { recursive: true });
|
|
12859
|
-
}
|
|
12860
|
-
fs7.writeFileSync(outputPath, html, "utf-8");
|
|
13523
|
+
const html = renderDashboardHtml(graphData, metricsData, options.title, resolveDeterministicDate(rootDir).toISOString());
|
|
13524
|
+
writeReportFileIfChanged(outputPath, html);
|
|
12861
13525
|
const { bddCoverageRatio, traceabilityRatio } = computeSummaryRatios(metricsData);
|
|
12862
13526
|
const { avgMi, avgCc } = computeGateAverages(metricsData.gate.results);
|
|
12863
13527
|
return {
|
|
@@ -12883,6 +13547,8 @@ function generateDashboardReport(options = {}) {
|
|
|
12883
13547
|
html
|
|
12884
13548
|
};
|
|
12885
13549
|
}
|
|
13550
|
+
|
|
13551
|
+
// src/reporters/sarif.ts
|
|
12886
13552
|
var SARIF_SCHEMA_2_1_0 = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";
|
|
12887
13553
|
var SARIF_VERSION = "2.1.0";
|
|
12888
13554
|
function normalizeUri(filePath) {
|
|
@@ -13030,11 +13696,7 @@ function serializeSarifDocument(doc, pretty = true) {
|
|
|
13030
13696
|
return JSON.stringify(doc, null, pretty ? 2 : 0);
|
|
13031
13697
|
}
|
|
13032
13698
|
function writeSecuritySarifReport(outputPath, doc, pretty = true) {
|
|
13033
|
-
|
|
13034
|
-
if (!fs7.existsSync(dir)) {
|
|
13035
|
-
fs7.mkdirSync(dir, { recursive: true });
|
|
13036
|
-
}
|
|
13037
|
-
fs7.writeFileSync(outputPath, serializeSarifDocument(doc, pretty), "utf-8");
|
|
13699
|
+
writeReportFileIfChanged(outputPath, serializeSarifDocument(doc, pretty));
|
|
13038
13700
|
}
|
|
13039
13701
|
|
|
13040
13702
|
// src/init/agent-templates.ts
|
|
@@ -15955,14 +16617,14 @@ function collectAgentFiles(targets) {
|
|
|
15955
16617
|
function writeProjectFiles(destDir, files, dryRun) {
|
|
15956
16618
|
const filesCreated = [];
|
|
15957
16619
|
for (const f of files) {
|
|
15958
|
-
const fullFilePath =
|
|
15959
|
-
const parentDir =
|
|
16620
|
+
const fullFilePath = path5.join(destDir, f.relPath);
|
|
16621
|
+
const parentDir = path5.dirname(fullFilePath);
|
|
15960
16622
|
if (!dryRun) {
|
|
15961
|
-
if (!
|
|
15962
|
-
|
|
16623
|
+
if (!fs8.existsSync(parentDir)) {
|
|
16624
|
+
fs8.mkdirSync(parentDir, { recursive: true });
|
|
15963
16625
|
}
|
|
15964
|
-
if (!
|
|
15965
|
-
|
|
16626
|
+
if (!fs8.existsSync(fullFilePath)) {
|
|
16627
|
+
fs8.writeFileSync(fullFilePath, f.content, "utf-8");
|
|
15966
16628
|
filesCreated.push(f.relPath);
|
|
15967
16629
|
}
|
|
15968
16630
|
} else {
|
|
@@ -16046,7 +16708,7 @@ function getCiWorkflowFiles(ciProvider) {
|
|
|
16046
16708
|
function createDirectoriesOnDisk(destDir, directories, dryRun) {
|
|
16047
16709
|
const created = [];
|
|
16048
16710
|
for (const d of directories) {
|
|
16049
|
-
if (!dryRun)
|
|
16711
|
+
if (!dryRun) fs8.mkdirSync(path5.join(destDir, d), { recursive: true });
|
|
16050
16712
|
created.push(d);
|
|
16051
16713
|
}
|
|
16052
16714
|
return created;
|
|
@@ -16061,7 +16723,7 @@ function collectStarterFiles(ciProvider, agentFiles = [], archGranularity = "min
|
|
|
16061
16723
|
...getCiWorkflowFiles(ciProvider),
|
|
16062
16724
|
...agentFiles,
|
|
16063
16725
|
...getArchitectureTemplates(archGranularity).map((t) => ({
|
|
16064
|
-
relPath:
|
|
16726
|
+
relPath: path5.join("templates", "architecture", t.filename).replace(/\\/g, "/"),
|
|
16065
16727
|
content: t.content
|
|
16066
16728
|
}))
|
|
16067
16729
|
];
|
|
@@ -16092,15 +16754,15 @@ function injectMissingScripts(scripts) {
|
|
|
16092
16754
|
return modified;
|
|
16093
16755
|
}
|
|
16094
16756
|
function updateConsumerPackageJson(destDir, dryRun) {
|
|
16095
|
-
const pkgPath =
|
|
16096
|
-
if (!
|
|
16757
|
+
const pkgPath = path5.join(destDir, "package.json");
|
|
16758
|
+
if (!fs8.existsSync(pkgPath)) return false;
|
|
16097
16759
|
try {
|
|
16098
|
-
const pkg = JSON.parse(
|
|
16760
|
+
const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
|
|
16099
16761
|
if (!pkg || typeof pkg !== "object") return false;
|
|
16100
16762
|
pkg.scripts = pkg.scripts || {};
|
|
16101
16763
|
const modified = injectMissingScripts(pkg.scripts);
|
|
16102
16764
|
if (modified && !dryRun) {
|
|
16103
|
-
|
|
16765
|
+
fs8.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
16104
16766
|
}
|
|
16105
16767
|
return modified;
|
|
16106
16768
|
} catch {
|
|
@@ -16108,7 +16770,7 @@ function updateConsumerPackageJson(destDir, dryRun) {
|
|
|
16108
16770
|
}
|
|
16109
16771
|
}
|
|
16110
16772
|
function tryInstallGitHook(destDir, dryRun) {
|
|
16111
|
-
if (!dryRun &&
|
|
16773
|
+
if (!dryRun && fs8.existsSync(path5.join(destDir, ".git"))) {
|
|
16112
16774
|
return installGitHooks(destDir).success;
|
|
16113
16775
|
}
|
|
16114
16776
|
return false;
|
|
@@ -16157,7 +16819,7 @@ function validateInitOptions(options) {
|
|
|
16157
16819
|
}
|
|
16158
16820
|
function initProject(options = {}) {
|
|
16159
16821
|
const baseDir = options.rootDir || process.cwd();
|
|
16160
|
-
const destDir =
|
|
16822
|
+
const destDir = path5.resolve(baseDir, options.targetDir || ".");
|
|
16161
16823
|
const validated = validateInitOptions(options);
|
|
16162
16824
|
if (!validated.valid) return buildInitError(destDir, validated.error);
|
|
16163
16825
|
const agentTargets = resolveAgentTargets(options.agents);
|
|
@@ -16241,33 +16903,40 @@ function normalizeParsedRule(raw, idx) {
|
|
|
16241
16903
|
enabled: raw.enabled !== false
|
|
16242
16904
|
};
|
|
16243
16905
|
}
|
|
16906
|
+
function parseDlpDoc(yamlContent) {
|
|
16907
|
+
let doc;
|
|
16908
|
+
try {
|
|
16909
|
+
doc = yaml3.load(yamlContent);
|
|
16910
|
+
} catch (err) {
|
|
16911
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid YAML in dlp-policy: ${err instanceof Error ? err.message : String(err)}`);
|
|
16912
|
+
}
|
|
16913
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
16914
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid dlp-policy structure: root must be a YAML mapping`);
|
|
16915
|
+
}
|
|
16916
|
+
const record = doc;
|
|
16917
|
+
if (record.rules !== void 0 && !Array.isArray(record.rules)) {
|
|
16918
|
+
throw new Error(`[AI-SDLC Fail-Closed] Invalid dlp-policy: rules must be an array`);
|
|
16919
|
+
}
|
|
16920
|
+
return record;
|
|
16921
|
+
}
|
|
16244
16922
|
function parseDlpPolicy(yamlContent) {
|
|
16245
16923
|
const fallback = createDefaultDlpPolicy();
|
|
16246
16924
|
if (!yamlContent || !yamlContent.trim()) return fallback;
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
rules: parsedRules.length > 0 ? parsedRules : fallback.rules
|
|
16257
|
-
};
|
|
16258
|
-
} catch {
|
|
16259
|
-
return fallback;
|
|
16260
|
-
}
|
|
16925
|
+
const record = parseDlpDoc(yamlContent);
|
|
16926
|
+
const rawRules = Array.isArray(record.rules) ? record.rules : [];
|
|
16927
|
+
const parsedRules = rawRules.map((r, i) => normalizeParsedRule(r, i)).filter((r) => r !== null);
|
|
16928
|
+
return {
|
|
16929
|
+
version: typeof record.version === "string" ? record.version : "1.0",
|
|
16930
|
+
enabled: record.enabled !== false,
|
|
16931
|
+
zero_data_retention_required: Boolean(record.zero_data_retention_required ?? true),
|
|
16932
|
+
rules: parsedRules.length > 0 ? parsedRules : fallback.rules
|
|
16933
|
+
};
|
|
16261
16934
|
}
|
|
16262
16935
|
function loadDlpPolicy(rootDir, customPolicyPath) {
|
|
16263
16936
|
const base = rootDir || process.cwd();
|
|
16264
|
-
const filePath = customPolicyPath ?
|
|
16265
|
-
if (!
|
|
16266
|
-
|
|
16267
|
-
return parseDlpPolicy(fs7.readFileSync(filePath, "utf-8"));
|
|
16268
|
-
} catch {
|
|
16269
|
-
return createDefaultDlpPolicy();
|
|
16270
|
-
}
|
|
16937
|
+
const filePath = customPolicyPath ? path5.isAbsolute(customPolicyPath) ? customPolicyPath : path5.join(base, customPolicyPath) : path5.join(base, "dlp-policy.yaml");
|
|
16938
|
+
if (!fs8.existsSync(filePath)) return createDefaultDlpPolicy();
|
|
16939
|
+
return parseDlpPolicy(fs8.readFileSync(filePath, "utf-8"));
|
|
16271
16940
|
}
|
|
16272
16941
|
function applySingleDlpRule(text, rule) {
|
|
16273
16942
|
if (rule.enabled === false) return { output: text, matches: 0 };
|
|
@@ -16318,6 +16987,6 @@ function sanitizeDlpPayload(payload, policy = createDefaultDlpPolicy()) {
|
|
|
16318
16987
|
}
|
|
16319
16988
|
}
|
|
16320
16989
|
|
|
16321
|
-
export { ALL_ARCHITECTURE_TEMPLATES, BRANCH_PATTERNS, DEFAULT_DLP_RULES, DEFAULT_EXCLUDED_PATTERNS, DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS, DEFAULT_PRIVACY_POLICY, DEFAULT_SAST_EXCLUDES, GENERAL_SOURCE_EXTENSIONS, KPI_SUMMARY_END_TAG, KPI_SUMMARY_START_TAG, LIFECYCLE_SKILL_TEMPLATES, OpenSpecAdapter, PII_SAST_RULES, PREPARE_COMMIT_MSG_SCRIPT, PROMPT_INJECTION_SIGNATURES, PROTECTED_BYPASS_PATTERNS, SARIF_SCHEMA_2_1_0, SARIF_VERSION, SAST_RULES, SCHEMA_SCHEMAS_ARCHITECTURE_ADR_SCHEMA_JSON, SCHEMA_SCHEMAS_ARCHITECTURE_COMPONENT_SCHEMA_JSON, SCHEMA_SCHEMAS_ARCHITECTURE_INTRODUCTION_AND_GOALS_SCHEMA_JSON, SCHEMA_SCHEMAS_COMPLIANCE_LICENSE_MANIFEST_SCHEMA_JSON, SCHEMA_SCHEMAS_COMPLIANCE_LICENSE_POLICY_SCHEMA_JSON, SCHEMA_SCHEMAS_COMPLIANCE_QUALITY_POLICY_SCHEMA_JSON, SCHEMA_SCHEMAS_IDE_MCP_CONFIG_SCHEMA_JSON, SCHEMA_SCHEMAS_MANUALS_PRODUCTION_MANUAL_SCHEMA_JSON, SCHEMA_SCHEMAS_MANUALS_USER_MANUAL_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_DATA_ASSET_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_PRIVACY_POLICY_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_PRIVACY_REQ_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_PRIVACY_ROPA_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_ACTOR_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_BOUNDED_CONTEXT_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_BUSINESS_RULE_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_JOURNEY_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_REQUIREMENT_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_TERM_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_USE_CASE_SCHEMA_JSON, SCHEMA_SCHEMAS_SAFETY_HAZARD_SCHEMA_JSON, SCHEMA_SCHEMAS_SAFETY_SAFETY_REQ_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_DESIGN_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_HANDOFF_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_PROPOSAL_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_RELEASE_KPIS_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_SESSION_TELEMETRY_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_SPEC_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_TASKS_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_ABUSE_CASE_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_ENCLAVE_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_SECURITY_REQ_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_THREAT_ACTOR_SCHEMA_JSON, SCHEMA_SCHEMAS_WORKFLOW_AGENT_HANDOFF_SCHEMA_JSON, SECRET_RULES, SPECIALIZED_AGENT_TEMPLATES, STARTER_ANTIGRAVITY_MCP, STARTER_ANTIGRAVITY_RULES, STARTER_ARCH_ADR, STARTER_ARCH_ARCHITECTURE_CONSTRAINTS, STARTER_ARCH_COMPONENT, STARTER_ARCH_CONTEXT_AND_SCOPE, STARTER_ARCH_CROSS_CUTTING_CONCEPTS, STARTER_ARCH_DEPLOYMENT_VIEW, STARTER_ARCH_GLOSSARY, STARTER_ARCH_INTRODUCTION_AND_GOALS, STARTER_ARCH_LEVEL_1_WHITEBOX, STARTER_ARCH_QUALITY_REQUIREMENTS, STARTER_ARCH_RISKS_AND_TECHNICAL_DEBT, STARTER_ARCH_RUNTIME_VIEW, STARTER_ARCH_SOLUTION_STRATEGY, STARTER_AZURE_PIPELINES, STARTER_BASE_TEMPLATES, STARTER_BITBUCKET_PIPELINES, STARTER_CLAUDE_RULES, STARTER_COMPLIANCE_LICENSE_MANIFEST_YAML, STARTER_COPILOT_RULES, STARTER_CURSOR_CORE_RULES, STARTER_CURSOR_MCP, STARTER_CURSOR_PRODUCT_RULES, STARTER_CURSOR_QUALITY_RULES, STARTER_GITHUB_CI, STARTER_GITHUB_DEPLOY_DASHBOARD, STARTER_GITHUB_PR_KPI, STARTER_GITHUB_PULL_REQUEST_TEMPLATE, STARTER_GITHUB_SDD_INTEGRATE, STARTER_GITLAB_CI, STARTER_LICENSE_POLICY, STARTER_PRIVACY_POLICY, STARTER_PROCESS_README, STARTER_PULL_REQUEST_TEMPLATE, STARTER_QUALITY_POLICY, STARTER_REQUIREMENT_TEMPLATE, STARTER_SCHEMAS, STARTER_SCHEMAS_MAP, STARTER_TASK_TEMPLATE, STARTER_TEMPLATES_CI_AZURE_PIPELINES_YML, STARTER_TEMPLATES_CI_BITBUCKET_PIPELINES_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_CI_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_DEPLOY_DASHBOARD_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_PR_KPI_SUMMARY_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_SDD_INTEGRATE_ON_MERGE_YML, STARTER_TEMPLATES_CI__GITLAB_CI_YML, STARTER_TEMPLATES_COMPLIANCE_COMMERCIAL_ACQUISITION_REQUEST_TEMPLATE_MD, STARTER_TEMPLATES_MANUALS_PRODUCTION_MANUAL_TEMPLATE_MD, STARTER_TEMPLATES_MANUALS_USER_MANUAL_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_DATA_ASSET_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_DPIA_REQUEST_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_PRIVACY_REQ_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_PRIVACY_ROPA_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_ACTOR_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_BOUNDED_CONTEXT_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_BUSINESS_RULE_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_JOURNEY_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_REQUIREMENT_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_TERM_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_USER_DESIGN_FEEDBACK_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_USE_CASE_TEMPLATE_MD, STARTER_TEMPLATES_SAFETY_HAZARD_TEMPLATE_MD, STARTER_TEMPLATES_SAFETY_SAFETY_REQ_TEMPLATE_MD, STARTER_TEMPLATES_SDD_DESIGN_TEMPLATE_MD, STARTER_TEMPLATES_SDD_HANDOFF_TEMPLATE_YAML, STARTER_TEMPLATES_SDD_PROPOSAL_TEMPLATE_MD, STARTER_TEMPLATES_SDD_PULL_REQUEST_TEMPLATE_MD, STARTER_TEMPLATES_SDD_SPEC_TEMPLATE_MD, STARTER_TEMPLATES_SDD_TASKS_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_ABUSE_CASE_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_ENCLAVE_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_SECURITY_REQ_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_THREAT_ACTOR_TEMPLATE_MD, STARTER_TEMPLATES_WORKFLOW_AGENT_HANDOFF_TEMPLATE_MD, STARTER_VSCODE_MCP, STARTER_WORKFLOW_HANDOFF_TEMPLATE, STARTER__GITHUB_PULL_REQUEST_TEMPLATE_MD, SpecKitAdapter, TYPE_TO_SCHEMA_MAP, VALID_AGENT_TARGETS, VALID_ARCHITECTURE_GRANULARITIES, VALID_AUTONOMY_MODES, VALID_CHANGE_PROFILES, VALID_CI_PROVIDERS, aggregateCommitKpiRecords, aggregatePrKpis, aggregateReleaseKpis, analyzeBddVariantsForRequirement, appendRevisionHistoryRow, applyRatchetMode, buildCycloneDxComponent, buildCycloneDxVulnerabilities, buildFeatureFileContent, buildGraphElements, calculateJaccardSimilarity, calculateMetrics, calculateRating, calculateShannonEntropy, checkGherkinInSync, checkoutTaskBranch, classifyBranch, classifyMetricFromMessage, collectAgentFiles, collectDashboardMetrics, collectHistoricalKpis, computeCanonicalSha256, computeSha256, computeViolationSignature, createDefaultDlpPolicy, createDefaultQualityPolicy, depositProductHandoffSidecar, detectActiveChangeForIntegration, detectAuthorIdentity, detectLanguage, detectProfileFromChange, detectPromptInjection, emitCiOutput, evaluateCompoundLicense, evaluateSingleDependency, evaluateVulnerabilityThresholds, extractBaselineEntriesFromResults, extractCitations, extractCommitKpisFromRange, extractDirectDepNames, extractFeatureTags, extractFrontmatter, extractFunctions, extractFunctionsBraceLanguages, extractFunctionsPolyglot, extractFunctionsPython, extractFunctionsTypeScriptAst, extractGherkinBlock, extractGherkinFeatures, extractNormativeText, findChangeWorkspaceDir, findLicenseFile, formatCommitTrailers, formatPrKpiMarkdown, formatReleaseKpiMarkdown, generateBranchHierarchyPlan, generateCycloneDxSbom, generateDashboardReport, generateDuplicatesReportMarkdown, generateGovernanceReportMarkdown, generateLicenseReportMarkdown, generatePdacReportMarkdown, generatePrivacyReportMarkdown, generateQualityReport, generateRopaInventoryMarkdown, generateSastReportMarkdown, generateSecretsReportMarkdown, generateSecuritySarifReport, generateSuppressionsAuditReport, generateTestingCoverageReportMarkdown, generateThirdPartyNotices, generateTraceabilityReportMarkdown, getArchitectureTemplates, getCytoscapeScript, getGitDiffLines, getNextCorrelativeNumber, getSddAdapter, inferArtifactTypeFromId, inferMissingTrailers, initProject, injectKpisIntoPrBody, installGitHooks, integrateSddChange, isGitleaksInstalled, isMetricWorsened, isPathProtectedFromPatch, isPlaceholderOrIgnored, isRequirementArtifact2 as isRequirementArtifact, isSemgrepInstalled, isToolAvailable, loadDlpPolicy, loadPrivacyPolicy, loadProductHandoffSidecar, loadQualityBaseline, locateTaskInActiveChanges, mapSastSeverityToSarifLevel, maskSecret, normalizeSpdxLicense, normalizeUri, parseCommitTrailers, parseDlpPolicy, parseFrontmatter, parseFrontmatterId, parseGenericDoc, parseLicensePolicy, parseMarkdownFrontmatter, parseQualityPolicy, parseRequirementFile, parseSecuritySuppression, parseSpecCatalogScope, parseTasksDoc, parseTasksFromContent, parseTrivyVulnerabilities, processPackageDir, readCodeCoverageSummary, readPackageJson, renderAntigravityAgentFile, renderAntigravitySkillFile, renderAutonomyModeSection, renderClaudeAgentFile, renderClaudeSkillFile, renderCopilotAgentFile, renderCopilotSkillFile, renderCursorAgentFile, renderCursorSkillFile, renderDashboardHtml, renderManualsTab, resolveAgentTargets, resolveBranchHierarchy, resolveCiEnvironment, resolveCytoscapePath, resolveDefaultBranchBase, resolveFeatureFilePath, resolveGitRef, resolveLicenseFromFile, resolveReleaseBaseBranch, resolveSastCwe, resolveSastOwasp, resolveSchemaForArtifact, resolveTargetFiles, runGitleaksCli, runSemgrepCli, sanitizeDlpPayload, sanitizeDlpText, scaffoldSddChange, scanAllProductHandoffs, scanAllTestArtifacts, scanContentForSecrets, scanFileForPrivacyViolations, scanFileForSast, scanGoDependencies, scanInstalledLicenses, scanManualsInWorkspace, scanMavenDependencies, scanPnpmStore, scanPythonDependencies, scanRustDependencies, scanSubdirectories, scanWithNativeFs, scanWithSyft, scanWithTrivy, serializeSarifDocument, slugify, syncPdacDigests, tokenizeTitle, updateCitationsInContent, validateAgainstSchema, validateArtifactSchema, verifyArtifactDuplicates, verifyArtifactsSchemas, verifyLicenses, verifyManuals, verifyPdacGraph, verifyPrivacy, verifyProgressiveFriction, verifyQualityGate, verifySast, verifySddIntegration, verifySecrets, verifyTasksGovernance, verifyTestingCoverage, verifyTraceability, walkDir, walkMdFiles, walkProjectFiles, walkSourceFiles, walkTaskFiles, writeCycloneDxSbom, writeQualityBaseline, writeReleaseKpiReport, writeSecuritySarifReport, writeThirdPartyNotices };
|
|
16990
|
+
export { ALL_ARCHITECTURE_TEMPLATES, BRANCH_PATTERNS, BddStepRegistry, DEFAULT_DLP_RULES, DEFAULT_EXCLUDED_PATTERNS, DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS, DEFAULT_PRIVACY_POLICY, DEFAULT_SAST_EXCLUDES, GENERAL_SOURCE_EXTENSIONS, KPI_SUMMARY_END_TAG, KPI_SUMMARY_START_TAG, LIFECYCLE_SKILL_TEMPLATES, OpenSpecAdapter, PII_SAST_RULES, PREPARE_COMMIT_MSG_SCRIPT, PROMPT_INJECTION_SIGNATURES, PROTECTED_BYPASS_PATTERNS, SARIF_SCHEMA_2_1_0, SARIF_VERSION, SAST_RULES, SCHEMA_SCHEMAS_ARCHITECTURE_ADR_SCHEMA_JSON, SCHEMA_SCHEMAS_ARCHITECTURE_COMPONENT_SCHEMA_JSON, SCHEMA_SCHEMAS_ARCHITECTURE_INTRODUCTION_AND_GOALS_SCHEMA_JSON, SCHEMA_SCHEMAS_COMPLIANCE_LICENSE_MANIFEST_SCHEMA_JSON, SCHEMA_SCHEMAS_COMPLIANCE_LICENSE_POLICY_SCHEMA_JSON, SCHEMA_SCHEMAS_COMPLIANCE_QUALITY_POLICY_SCHEMA_JSON, SCHEMA_SCHEMAS_IDE_MCP_CONFIG_SCHEMA_JSON, SCHEMA_SCHEMAS_MANUALS_PRODUCTION_MANUAL_SCHEMA_JSON, SCHEMA_SCHEMAS_MANUALS_USER_MANUAL_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_DATA_ASSET_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_PRIVACY_POLICY_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_PRIVACY_REQ_SCHEMA_JSON, SCHEMA_SCHEMAS_PRIVACY_PRIVACY_ROPA_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_ACTOR_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_BOUNDED_CONTEXT_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_BUSINESS_RULE_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_JOURNEY_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_REQUIREMENT_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_TERM_SCHEMA_JSON, SCHEMA_SCHEMAS_PRODUCT_USE_CASE_SCHEMA_JSON, SCHEMA_SCHEMAS_SAFETY_HAZARD_SCHEMA_JSON, SCHEMA_SCHEMAS_SAFETY_SAFETY_REQ_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_DESIGN_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_HANDOFF_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_PROPOSAL_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_RELEASE_KPIS_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_SESSION_TELEMETRY_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_SPEC_SCHEMA_JSON, SCHEMA_SCHEMAS_SDD_TASKS_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_ABUSE_CASE_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_ENCLAVE_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_SECURITY_REQ_SCHEMA_JSON, SCHEMA_SCHEMAS_SECURITY_THREAT_ACTOR_SCHEMA_JSON, SCHEMA_SCHEMAS_WORKFLOW_AGENT_HANDOFF_SCHEMA_JSON, SECRET_RULES, SPECIALIZED_AGENT_TEMPLATES, STARTER_ANTIGRAVITY_MCP, STARTER_ANTIGRAVITY_RULES, STARTER_ARCH_ADR, STARTER_ARCH_ARCHITECTURE_CONSTRAINTS, STARTER_ARCH_COMPONENT, STARTER_ARCH_CONTEXT_AND_SCOPE, STARTER_ARCH_CROSS_CUTTING_CONCEPTS, STARTER_ARCH_DEPLOYMENT_VIEW, STARTER_ARCH_GLOSSARY, STARTER_ARCH_INTRODUCTION_AND_GOALS, STARTER_ARCH_LEVEL_1_WHITEBOX, STARTER_ARCH_QUALITY_REQUIREMENTS, STARTER_ARCH_RISKS_AND_TECHNICAL_DEBT, STARTER_ARCH_RUNTIME_VIEW, STARTER_ARCH_SOLUTION_STRATEGY, STARTER_AZURE_PIPELINES, STARTER_BASE_TEMPLATES, STARTER_BITBUCKET_PIPELINES, STARTER_CLAUDE_RULES, STARTER_COMPLIANCE_LICENSE_MANIFEST_YAML, STARTER_COPILOT_RULES, STARTER_CURSOR_CORE_RULES, STARTER_CURSOR_MCP, STARTER_CURSOR_PRODUCT_RULES, STARTER_CURSOR_QUALITY_RULES, STARTER_GITHUB_CI, STARTER_GITHUB_DEPLOY_DASHBOARD, STARTER_GITHUB_PR_KPI, STARTER_GITHUB_PULL_REQUEST_TEMPLATE, STARTER_GITHUB_SDD_INTEGRATE, STARTER_GITLAB_CI, STARTER_LICENSE_POLICY, STARTER_PRIVACY_POLICY, STARTER_PROCESS_README, STARTER_PULL_REQUEST_TEMPLATE, STARTER_QUALITY_POLICY, STARTER_REQUIREMENT_TEMPLATE, STARTER_SCHEMAS, STARTER_SCHEMAS_MAP, STARTER_TASK_TEMPLATE, STARTER_TEMPLATES_CI_AZURE_PIPELINES_YML, STARTER_TEMPLATES_CI_BITBUCKET_PIPELINES_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_CI_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_DEPLOY_DASHBOARD_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_PR_KPI_SUMMARY_YML, STARTER_TEMPLATES_CI_GITHUB_WORKFLOWS_SDD_INTEGRATE_ON_MERGE_YML, STARTER_TEMPLATES_CI__GITLAB_CI_YML, STARTER_TEMPLATES_COMPLIANCE_COMMERCIAL_ACQUISITION_REQUEST_TEMPLATE_MD, STARTER_TEMPLATES_MANUALS_PRODUCTION_MANUAL_TEMPLATE_MD, STARTER_TEMPLATES_MANUALS_USER_MANUAL_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_DATA_ASSET_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_DPIA_REQUEST_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_PRIVACY_REQ_TEMPLATE_MD, STARTER_TEMPLATES_PRIVACY_PRIVACY_ROPA_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_ACTOR_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_BOUNDED_CONTEXT_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_BUSINESS_RULE_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_JOURNEY_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_REQUIREMENT_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_TERM_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_USER_DESIGN_FEEDBACK_TEMPLATE_MD, STARTER_TEMPLATES_PRODUCT_USE_CASE_TEMPLATE_MD, STARTER_TEMPLATES_SAFETY_HAZARD_TEMPLATE_MD, STARTER_TEMPLATES_SAFETY_SAFETY_REQ_TEMPLATE_MD, STARTER_TEMPLATES_SDD_DESIGN_TEMPLATE_MD, STARTER_TEMPLATES_SDD_HANDOFF_TEMPLATE_YAML, STARTER_TEMPLATES_SDD_PROPOSAL_TEMPLATE_MD, STARTER_TEMPLATES_SDD_PULL_REQUEST_TEMPLATE_MD, STARTER_TEMPLATES_SDD_SPEC_TEMPLATE_MD, STARTER_TEMPLATES_SDD_TASKS_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_ABUSE_CASE_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_ENCLAVE_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_SECURITY_REQ_TEMPLATE_MD, STARTER_TEMPLATES_SECURITY_THREAT_ACTOR_TEMPLATE_MD, STARTER_TEMPLATES_WORKFLOW_AGENT_HANDOFF_TEMPLATE_MD, STARTER_VSCODE_MCP, STARTER_WORKFLOW_HANDOFF_TEMPLATE, STARTER__GITHUB_PULL_REQUEST_TEMPLATE_MD, SpecKitAdapter, TYPE_TO_SCHEMA_MAP, UNIT_TEST_SUFFIXES, VALID_AGENT_TARGETS, VALID_ARCHITECTURE_GRANULARITIES, VALID_AUTONOMY_MODES, VALID_CHANGE_PROFILES, VALID_CI_PROVIDERS, aggregateCommitKpiRecords, aggregatePrKpis, aggregateReleaseKpis, analyzeBddVariantsForRequirement, appendRevisionHistoryRow, applyRatchetMode, buildCycloneDxComponent, buildCycloneDxVulnerabilities, buildFeatureFileContent, buildGraphElements, calculateJaccardSimilarity, calculateMetrics, calculateRating, calculateShannonEntropy, canonicalSort, checkGherkinInSync, checkoutTaskBranch, classifyBranch, classifyMetricFromMessage, collectAgentFiles, collectDashboardMetrics, collectHistoricalKpis, computeCanonicalSha256, computeRawSha256, computeSha256, computeViolationSignature, createDefaultDlpPolicy, createDefaultQualityPolicy, depositProductHandoffSidecar, detectActiveChangeForIntegration, detectAuthorIdentity, detectLanguage, detectProfileFromChange, detectPromptInjection, emitCiOutput, evaluateCompoundLicense, evaluateSingleDependency, evaluateVulnerabilityThresholds, executeBddFeature, extractBaselineEntriesFromResults, extractCitations, extractCommitKpisFromRange, extractDirectDepNames, extractFeatureTags, extractFrontmatter, extractFunctions, extractFunctionsBraceLanguages, extractFunctionsPolyglot, extractFunctionsPython, extractFunctionsPythonAst, extractFunctionsTypeScriptAst, extractGherkinBlock, extractGherkinFeatures, extractNormativeText, extractPythonFunctionsPolyglot, extractSemanticContent, findChangeWorkspaceDir, findLicenseFile, formatCommitTrailers, formatPrKpiMarkdown, formatReleaseKpiMarkdown, generateBranchHierarchyPlan, generateCycloneDxSbom, generateDashboardReport, generateDeterministicUuidV5, generateDuplicatesReportMarkdown, generateGovernanceReportMarkdown, generateLicenseReportMarkdown, generatePdacReportMarkdown, generatePrivacyReportMarkdown, generateQualityReport, generateRopaInventoryMarkdown, generateSastReportMarkdown, generateSecretsReportMarkdown, generateSecuritySarifReport, generateSuppressionsAuditReport, generateTestingCoverageReportMarkdown, generateThirdPartyNotices, generateTraceabilityReportMarkdown, getArchitectureTemplates, getCytoscapeScript, getGitDiffLines, getNextCorrelativeNumber, getSddAdapter, inferArtifactTypeFromId, inferMissingTrailers, initProject, injectKpisIntoPrBody, installGitHooks, integrateSddChange, isCliInstalled, isFunctionReferencedInTests, isGitleaksInstalled, isMetricWorsened, isPathProtectedFromPatch, isPlaceholderOrIgnored, isReportContentSemanticallyEqual, isRequirementArtifact2 as isRequirementArtifact, isSemgrepInstalled, isToolAvailable, loadDlpPolicy, loadPrivacyPolicy, loadProductHandoffSidecar, loadQualityBaseline, locateTaskInActiveChanges, mapSastSeverityToSarifLevel, maskSecret, normalizePath, normalizeSpdxLicense, normalizeUri, orchestrateNativeAstAnalysis, parseCommitTrailers, parseDlpPolicy, parseFeatureDocument, parseFrontmatter, parseFrontmatterId, parseGenericDoc, parseLicensePolicy, parseMarkdownFrontmatter, parseQualityPolicy, parseRequirementFile, parseSecuritySuppression, parseSpecCatalogScope, parseTasksDoc, parseTasksFromContent, parseTrivyVulnerabilities, processPackageDir, readCodeCoverageSummary, readPackageJson, renderAntigravityAgentFile, renderAntigravitySkillFile, renderAutonomyModeSection, renderClaudeAgentFile, renderClaudeSkillFile, renderCopilotAgentFile, renderCopilotSkillFile, renderCursorAgentFile, renderCursorSkillFile, renderDashboardHtml, renderManualsTab, resolveAgentTargets, resolveBranchHierarchy, resolveCiEnvironment, resolveCytoscapePath, resolveDefaultBranchBase, resolveDeterministicDate, resolveFeatureFilePath, resolveGitRef, resolveLicenseFromFile, resolveReleaseBaseBranch, resolveSastCwe, resolveSastOwasp, resolveSchemaForArtifact, resolveTargetFiles, runGitleaksCli, runSemgrepCli, sanitizeDlpPayload, sanitizeDlpText, sanitizeTestCorpus, scaffoldSddChange, scanAllProductHandoffs, scanAllTestArtifacts, scanContentForSecrets, scanFileForPrivacyViolations, scanFileForSast, scanGoDependencies, scanInstalledLicenses, scanManualsInWorkspace, scanMavenDependencies, scanPnpmStore, scanPythonDependencies, scanRustDependencies, scanSubdirectories, scanWithNativeFs, scanWithSyft, scanWithTrivy, serializeSarifDocument, slugify2 as slugify, stripVolatileReportMetadata, syncPdacDigests, tokenizeTitle, updateCitationsInContent, validateAgainstSchema, validateAnchorExistence, validateArtifactSchema, verifyArtifactDuplicates, verifyArtifactsSchemas, verifyLicenses, verifyManuals, verifyPdacGraph, verifyPrivacy, verifyProgressiveFriction, verifyQualityGate, verifySast, verifySddIntegration, verifySecrets, verifyTasksGovernance, verifyTestingCoverage, verifyTraceability, walkDir, walkMdFiles, walkProjectFiles, walkSourceFiles, walkTaskFiles, writeCucumberJsonReport, writeCycloneDxSbom, writeQualityBaseline, writeReleaseKpiReport, writeReportFileIfChanged, writeSecuritySarifReport, writeThirdPartyNotices };
|
|
16322
16991
|
//# sourceMappingURL=index.js.map
|
|
16323
16992
|
//# sourceMappingURL=index.js.map
|