@harness-engineering/core 0.21.1 → 0.21.2
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/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/architecture/matchers.js +383 -332
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-BQUWXBGR.mjs → chunk-4W4FRAA6.mjs} +383 -332
- package/dist/index.d.mts +174 -181
- package/dist/index.d.ts +174 -181
- package/dist/index.js +1511 -1329
- package/dist/index.mjs +1122 -990
- package/dist/{matchers-D20x48U9.d.mts → matchers-Dj1t5vpg.d.mts} +46 -46
- package/dist/{matchers-D20x48U9.d.ts → matchers-Dj1t5vpg.d.ts} +46 -46
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -59,7 +59,6 @@ __export(index_exports, {
|
|
|
59
59
|
ConfirmationSchema: () => ConfirmationSchema,
|
|
60
60
|
ConsoleSink: () => ConsoleSink,
|
|
61
61
|
ConstraintRuleSchema: () => ConstraintRuleSchema,
|
|
62
|
-
ContentPipeline: () => ContentPipeline,
|
|
63
62
|
ContributingFeatureSchema: () => ContributingFeatureSchema,
|
|
64
63
|
ContributionsSchema: () => ContributionsSchema,
|
|
65
64
|
CouplingCollector: () => CouplingCollector,
|
|
@@ -298,7 +297,7 @@ __export(index_exports, {
|
|
|
298
297
|
resolveRuleSeverity: () => resolveRuleSeverity,
|
|
299
298
|
resolveSessionDir: () => resolveSessionDir,
|
|
300
299
|
resolveStreamPath: () => resolveStreamPath,
|
|
301
|
-
resolveThresholds: () =>
|
|
300
|
+
resolveThresholds: () => resolveThresholds3,
|
|
302
301
|
runAll: () => runAll,
|
|
303
302
|
runArchitectureAgent: () => runArchitectureAgent,
|
|
304
303
|
runBugDetectionAgent: () => runBugDetectionAgent,
|
|
@@ -499,21 +498,11 @@ function validateCommitMessage(message, format = "conventional") {
|
|
|
499
498
|
issues: []
|
|
500
499
|
});
|
|
501
500
|
}
|
|
502
|
-
function
|
|
503
|
-
const lines = message.split("\n");
|
|
504
|
-
const headerLine = lines[0];
|
|
505
|
-
if (!headerLine) {
|
|
506
|
-
const error = createError(
|
|
507
|
-
"VALIDATION_FAILED",
|
|
508
|
-
"Commit message header cannot be empty",
|
|
509
|
-
{ message },
|
|
510
|
-
["Provide a commit message with at least a header line"]
|
|
511
|
-
);
|
|
512
|
-
return (0, import_types.Err)(error);
|
|
513
|
-
}
|
|
501
|
+
function parseConventionalHeader(message, headerLine) {
|
|
514
502
|
const match = headerLine.match(CONVENTIONAL_PATTERN);
|
|
515
|
-
if (
|
|
516
|
-
|
|
503
|
+
if (match) return (0, import_types.Ok)(match);
|
|
504
|
+
return (0, import_types.Err)(
|
|
505
|
+
createError(
|
|
517
506
|
"VALIDATION_FAILED",
|
|
518
507
|
"Commit message does not follow conventional format",
|
|
519
508
|
{ message, header: headerLine },
|
|
@@ -522,13 +511,10 @@ function validateConventionalCommit(message) {
|
|
|
522
511
|
"Valid types: " + VALID_TYPES.join(", "),
|
|
523
512
|
"Example: feat(core): add new feature"
|
|
524
513
|
]
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const scope = match[3];
|
|
530
|
-
const breaking = match[4] === "!";
|
|
531
|
-
const description = match[5];
|
|
514
|
+
)
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
function collectCommitIssues(type, description) {
|
|
532
518
|
const issues = [];
|
|
533
519
|
if (!VALID_TYPES.includes(type)) {
|
|
534
520
|
issues.push(`Invalid commit type "${type}". Valid types: ${VALID_TYPES.join(", ")}`);
|
|
@@ -536,34 +522,50 @@ function validateConventionalCommit(message) {
|
|
|
536
522
|
if (!description || description.trim() === "") {
|
|
537
523
|
issues.push("Commit description cannot be empty");
|
|
538
524
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
525
|
+
return issues;
|
|
526
|
+
}
|
|
527
|
+
function hasBreakingChangeInBody(lines) {
|
|
528
|
+
if (lines.length <= 1) return false;
|
|
529
|
+
return lines.slice(1).join("\n").includes("BREAKING CHANGE:");
|
|
530
|
+
}
|
|
531
|
+
function validateConventionalCommit(message) {
|
|
532
|
+
const lines = message.split("\n");
|
|
533
|
+
const headerLine = lines[0];
|
|
534
|
+
if (!headerLine) {
|
|
535
|
+
return (0, import_types.Err)(
|
|
536
|
+
createError(
|
|
537
|
+
"VALIDATION_FAILED",
|
|
538
|
+
"Commit message header cannot be empty",
|
|
539
|
+
{ message },
|
|
540
|
+
["Provide a commit message with at least a header line"]
|
|
541
|
+
)
|
|
542
|
+
);
|
|
545
543
|
}
|
|
544
|
+
const matchResult = parseConventionalHeader(message, headerLine);
|
|
545
|
+
if (!matchResult.ok) return matchResult;
|
|
546
|
+
const match = matchResult.value;
|
|
547
|
+
const type = match[1];
|
|
548
|
+
const scope = match[3];
|
|
549
|
+
const breaking = match[4] === "!";
|
|
550
|
+
const description = match[5];
|
|
551
|
+
const issues = collectCommitIssues(type, description);
|
|
546
552
|
if (issues.length > 0) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
{ message, issues, type, scope },
|
|
555
|
-
["Review and fix the validation issues above"]
|
|
553
|
+
return (0, import_types.Err)(
|
|
554
|
+
createError(
|
|
555
|
+
"VALIDATION_FAILED",
|
|
556
|
+
`Commit message validation failed: ${issues.join("; ")}`,
|
|
557
|
+
{ message, issues, type, scope },
|
|
558
|
+
["Review and fix the validation issues above"]
|
|
559
|
+
)
|
|
556
560
|
);
|
|
557
|
-
return (0, import_types.Err)(error);
|
|
558
561
|
}
|
|
559
|
-
|
|
562
|
+
return (0, import_types.Ok)({
|
|
560
563
|
valid: true,
|
|
561
564
|
type,
|
|
562
565
|
...scope && { scope },
|
|
563
|
-
breaking:
|
|
566
|
+
breaking: breaking || hasBreakingChangeInBody(lines),
|
|
564
567
|
issues: []
|
|
565
|
-
};
|
|
566
|
-
return (0, import_types.Ok)(result);
|
|
568
|
+
});
|
|
567
569
|
}
|
|
568
570
|
|
|
569
571
|
// src/context/types.ts
|
|
@@ -1021,6 +1023,47 @@ var NODE_TYPE_TO_CATEGORY = {
|
|
|
1021
1023
|
prompt: "systemPrompt",
|
|
1022
1024
|
system: "systemPrompt"
|
|
1023
1025
|
};
|
|
1026
|
+
function makeZeroWeights() {
|
|
1027
|
+
return {
|
|
1028
|
+
systemPrompt: 0,
|
|
1029
|
+
projectManifest: 0,
|
|
1030
|
+
taskSpec: 0,
|
|
1031
|
+
activeCode: 0,
|
|
1032
|
+
interfaces: 0,
|
|
1033
|
+
reserve: 0
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function normalizeRatios(ratios) {
|
|
1037
|
+
const sum = Object.values(ratios).reduce((s, r) => s + r, 0);
|
|
1038
|
+
if (sum === 0) return;
|
|
1039
|
+
for (const key of Object.keys(ratios)) {
|
|
1040
|
+
ratios[key] = ratios[key] / sum;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function enforceMinimumRatios(ratios, min) {
|
|
1044
|
+
for (const key of Object.keys(ratios)) {
|
|
1045
|
+
if (ratios[key] < min) ratios[key] = min;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function applyGraphDensity(ratios, graphDensity) {
|
|
1049
|
+
const weights = makeZeroWeights();
|
|
1050
|
+
for (const [nodeType, count] of Object.entries(graphDensity)) {
|
|
1051
|
+
const category = NODE_TYPE_TO_CATEGORY[nodeType];
|
|
1052
|
+
if (category) weights[category] += count;
|
|
1053
|
+
}
|
|
1054
|
+
const totalWeight = Object.values(weights).reduce((s, w) => s + w, 0);
|
|
1055
|
+
if (totalWeight === 0) return;
|
|
1056
|
+
const MIN = 0.01;
|
|
1057
|
+
for (const key of Object.keys(ratios)) {
|
|
1058
|
+
ratios[key] = weights[key] > 0 ? weights[key] / totalWeight : MIN;
|
|
1059
|
+
}
|
|
1060
|
+
if (ratios.reserve < DEFAULT_RATIOS.reserve) ratios.reserve = DEFAULT_RATIOS.reserve;
|
|
1061
|
+
if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt)
|
|
1062
|
+
ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
|
|
1063
|
+
normalizeRatios(ratios);
|
|
1064
|
+
enforceMinimumRatios(ratios, MIN);
|
|
1065
|
+
normalizeRatios(ratios);
|
|
1066
|
+
}
|
|
1024
1067
|
function contextBudget(totalTokens, overrides, graphDensity) {
|
|
1025
1068
|
const ratios = {
|
|
1026
1069
|
systemPrompt: DEFAULT_RATIOS.systemPrompt,
|
|
@@ -1031,50 +1074,7 @@ function contextBudget(totalTokens, overrides, graphDensity) {
|
|
|
1031
1074
|
reserve: DEFAULT_RATIOS.reserve
|
|
1032
1075
|
};
|
|
1033
1076
|
if (graphDensity) {
|
|
1034
|
-
|
|
1035
|
-
systemPrompt: 0,
|
|
1036
|
-
projectManifest: 0,
|
|
1037
|
-
taskSpec: 0,
|
|
1038
|
-
activeCode: 0,
|
|
1039
|
-
interfaces: 0,
|
|
1040
|
-
reserve: 0
|
|
1041
|
-
};
|
|
1042
|
-
for (const [nodeType, count] of Object.entries(graphDensity)) {
|
|
1043
|
-
const category = NODE_TYPE_TO_CATEGORY[nodeType];
|
|
1044
|
-
if (category) {
|
|
1045
|
-
categoryWeights[category] += count;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
const totalWeight = Object.values(categoryWeights).reduce((sum, w) => sum + w, 0);
|
|
1049
|
-
if (totalWeight > 0) {
|
|
1050
|
-
const MIN_ALLOCATION = 0.01;
|
|
1051
|
-
for (const key of Object.keys(ratios)) {
|
|
1052
|
-
if (categoryWeights[key] > 0) {
|
|
1053
|
-
ratios[key] = categoryWeights[key] / totalWeight;
|
|
1054
|
-
} else {
|
|
1055
|
-
ratios[key] = MIN_ALLOCATION;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
if (ratios.reserve < DEFAULT_RATIOS.reserve) {
|
|
1059
|
-
ratios.reserve = DEFAULT_RATIOS.reserve;
|
|
1060
|
-
}
|
|
1061
|
-
if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt) {
|
|
1062
|
-
ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
|
|
1063
|
-
}
|
|
1064
|
-
const ratioSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
|
|
1065
|
-
for (const key of Object.keys(ratios)) {
|
|
1066
|
-
ratios[key] = ratios[key] / ratioSum;
|
|
1067
|
-
}
|
|
1068
|
-
for (const key of Object.keys(ratios)) {
|
|
1069
|
-
if (ratios[key] < MIN_ALLOCATION) {
|
|
1070
|
-
ratios[key] = MIN_ALLOCATION;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
const finalSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
|
|
1074
|
-
for (const key of Object.keys(ratios)) {
|
|
1075
|
-
ratios[key] = ratios[key] / finalSum;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1077
|
+
applyGraphDensity(ratios, graphDensity);
|
|
1078
1078
|
}
|
|
1079
1079
|
if (overrides) {
|
|
1080
1080
|
let overrideSum = 0;
|
|
@@ -1635,21 +1635,23 @@ function extractBundle(manifest, config) {
|
|
|
1635
1635
|
}
|
|
1636
1636
|
|
|
1637
1637
|
// src/constraints/sharing/merge.ts
|
|
1638
|
+
function arraysEqual(a, b) {
|
|
1639
|
+
if (a.length !== b.length) return false;
|
|
1640
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
1641
|
+
}
|
|
1642
|
+
function objectsEqual(a, b) {
|
|
1643
|
+
const keysA = Object.keys(a);
|
|
1644
|
+
const keysB = Object.keys(b);
|
|
1645
|
+
if (keysA.length !== keysB.length) return false;
|
|
1646
|
+
return keysA.every((key) => deepEqual(a[key], b[key]));
|
|
1647
|
+
}
|
|
1638
1648
|
function deepEqual(a, b) {
|
|
1639
1649
|
if (a === b) return true;
|
|
1640
1650
|
if (typeof a !== typeof b) return false;
|
|
1641
1651
|
if (typeof a !== "object" || a === null || b === null) return false;
|
|
1642
|
-
if (Array.isArray(a) && Array.isArray(b))
|
|
1643
|
-
if (a.length !== b.length) return false;
|
|
1644
|
-
return a.every((val, i) => deepEqual(val, b[i]));
|
|
1645
|
-
}
|
|
1652
|
+
if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
|
|
1646
1653
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1647
|
-
|
|
1648
|
-
const keysB = Object.keys(b);
|
|
1649
|
-
if (keysA.length !== keysB.length) return false;
|
|
1650
|
-
return keysA.every(
|
|
1651
|
-
(key) => deepEqual(a[key], b[key])
|
|
1652
|
-
);
|
|
1654
|
+
return objectsEqual(a, b);
|
|
1653
1655
|
}
|
|
1654
1656
|
function stringArraysEqual(a, b) {
|
|
1655
1657
|
if (a.length !== b.length) return false;
|
|
@@ -2302,17 +2304,22 @@ async function parseDocumentationFile(path31) {
|
|
|
2302
2304
|
function makeInternalSymbol(name, type, line) {
|
|
2303
2305
|
return { name, type, line, references: 0, calledBy: [] };
|
|
2304
2306
|
}
|
|
2307
|
+
function extractFunctionSymbol(node, line) {
|
|
2308
|
+
if (node.id?.name) return [makeInternalSymbol(node.id.name, "function", line)];
|
|
2309
|
+
return [];
|
|
2310
|
+
}
|
|
2311
|
+
function extractVariableSymbols(node, line) {
|
|
2312
|
+
return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
|
|
2313
|
+
}
|
|
2314
|
+
function extractClassSymbol(node, line) {
|
|
2315
|
+
if (node.id?.name) return [makeInternalSymbol(node.id.name, "class", line)];
|
|
2316
|
+
return [];
|
|
2317
|
+
}
|
|
2305
2318
|
function extractSymbolsFromNode(node) {
|
|
2306
2319
|
const line = node.loc?.start?.line || 0;
|
|
2307
|
-
if (node.type === "FunctionDeclaration"
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
if (node.type === "VariableDeclaration") {
|
|
2311
|
-
return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
|
|
2312
|
-
}
|
|
2313
|
-
if (node.type === "ClassDeclaration" && node.id?.name) {
|
|
2314
|
-
return [makeInternalSymbol(node.id.name, "class", line)];
|
|
2315
|
-
}
|
|
2320
|
+
if (node.type === "FunctionDeclaration") return extractFunctionSymbol(node, line);
|
|
2321
|
+
if (node.type === "VariableDeclaration") return extractVariableSymbols(node, line);
|
|
2322
|
+
if (node.type === "ClassDeclaration") return extractClassSymbol(node, line);
|
|
2316
2323
|
return [];
|
|
2317
2324
|
}
|
|
2318
2325
|
function extractInternalSymbols(ast) {
|
|
@@ -2321,21 +2328,17 @@ function extractInternalSymbols(ast) {
|
|
|
2321
2328
|
const nodes = body.body;
|
|
2322
2329
|
return nodes.flatMap(extractSymbolsFromNode);
|
|
2323
2330
|
}
|
|
2331
|
+
function toJSDocComment(comment) {
|
|
2332
|
+
if (comment.type !== "Block" || !comment.value?.startsWith("*")) return null;
|
|
2333
|
+
return { content: comment.value, line: comment.loc?.start?.line || 0 };
|
|
2334
|
+
}
|
|
2324
2335
|
function extractJSDocComments(ast) {
|
|
2325
|
-
const comments = [];
|
|
2326
2336
|
const body = ast.body;
|
|
2327
|
-
if (body?.comments)
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
line: comment.loc?.start?.line || 0
|
|
2333
|
-
};
|
|
2334
|
-
comments.push(jsDocComment);
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
return comments;
|
|
2337
|
+
if (!body?.comments) return [];
|
|
2338
|
+
return body.comments.flatMap((c) => {
|
|
2339
|
+
const doc = toJSDocComment(c);
|
|
2340
|
+
return doc ? [doc] : [];
|
|
2341
|
+
});
|
|
2339
2342
|
}
|
|
2340
2343
|
function buildExportMap(files) {
|
|
2341
2344
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -2350,41 +2353,42 @@ function buildExportMap(files) {
|
|
|
2350
2353
|
}
|
|
2351
2354
|
return { byFile, byName };
|
|
2352
2355
|
}
|
|
2353
|
-
|
|
2356
|
+
var CODE_BLOCK_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "ts", "javascript", "js"]);
|
|
2357
|
+
function refsFromInlineRefs(doc) {
|
|
2358
|
+
return doc.inlineRefs.map((inlineRef) => ({
|
|
2359
|
+
docFile: doc.path,
|
|
2360
|
+
line: inlineRef.line,
|
|
2361
|
+
column: inlineRef.column,
|
|
2362
|
+
reference: inlineRef.reference,
|
|
2363
|
+
context: "inline"
|
|
2364
|
+
}));
|
|
2365
|
+
}
|
|
2366
|
+
function refsFromCodeBlock(docPath, block) {
|
|
2367
|
+
if (!CODE_BLOCK_LANGUAGES.has(block.language)) return [];
|
|
2354
2368
|
const refs = [];
|
|
2355
|
-
|
|
2356
|
-
|
|
2369
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
2370
|
+
let match;
|
|
2371
|
+
while ((match = importRegex.exec(block.content)) !== null) {
|
|
2372
|
+
const group = match[1];
|
|
2373
|
+
if (group === void 0) continue;
|
|
2374
|
+
for (const name of group.split(",").map((n) => n.trim())) {
|
|
2357
2375
|
refs.push({
|
|
2358
|
-
docFile:
|
|
2359
|
-
line:
|
|
2360
|
-
column:
|
|
2361
|
-
reference:
|
|
2362
|
-
context: "
|
|
2376
|
+
docFile: docPath,
|
|
2377
|
+
line: block.line,
|
|
2378
|
+
column: 0,
|
|
2379
|
+
reference: name,
|
|
2380
|
+
context: "code-block"
|
|
2363
2381
|
});
|
|
2364
2382
|
}
|
|
2365
|
-
for (const block of doc.codeBlocks) {
|
|
2366
|
-
if (block.language === "typescript" || block.language === "ts" || block.language === "javascript" || block.language === "js") {
|
|
2367
|
-
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
2368
|
-
let match;
|
|
2369
|
-
while ((match = importRegex.exec(block.content)) !== null) {
|
|
2370
|
-
const matchedGroup = match[1];
|
|
2371
|
-
if (matchedGroup === void 0) continue;
|
|
2372
|
-
const names = matchedGroup.split(",").map((n) => n.trim());
|
|
2373
|
-
for (const name of names) {
|
|
2374
|
-
refs.push({
|
|
2375
|
-
docFile: doc.path,
|
|
2376
|
-
line: block.line,
|
|
2377
|
-
column: 0,
|
|
2378
|
-
reference: name,
|
|
2379
|
-
context: "code-block"
|
|
2380
|
-
});
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
2383
|
}
|
|
2386
2384
|
return refs;
|
|
2387
2385
|
}
|
|
2386
|
+
function refsFromCodeBlocks(doc) {
|
|
2387
|
+
return doc.codeBlocks.flatMap((block) => refsFromCodeBlock(doc.path, block));
|
|
2388
|
+
}
|
|
2389
|
+
function extractAllCodeReferences(docs) {
|
|
2390
|
+
return docs.flatMap((doc) => [...refsFromInlineRefs(doc), ...refsFromCodeBlocks(doc)]);
|
|
2391
|
+
}
|
|
2388
2392
|
async function buildSnapshot(config) {
|
|
2389
2393
|
const startTime = Date.now();
|
|
2390
2394
|
const parser = config.parser || new TypeScriptParser();
|
|
@@ -2590,44 +2594,52 @@ async function checkStructureDrift(snapshot, _config) {
|
|
|
2590
2594
|
}
|
|
2591
2595
|
return drifts;
|
|
2592
2596
|
}
|
|
2597
|
+
function computeDriftSeverity(driftCount) {
|
|
2598
|
+
if (driftCount === 0) return "none";
|
|
2599
|
+
if (driftCount <= 3) return "low";
|
|
2600
|
+
if (driftCount <= 10) return "medium";
|
|
2601
|
+
return "high";
|
|
2602
|
+
}
|
|
2603
|
+
function buildGraphDriftReport(graphDriftData) {
|
|
2604
|
+
const drifts = [];
|
|
2605
|
+
for (const target of graphDriftData.missingTargets) {
|
|
2606
|
+
drifts.push({
|
|
2607
|
+
type: "api-signature",
|
|
2608
|
+
docFile: target,
|
|
2609
|
+
line: 0,
|
|
2610
|
+
reference: target,
|
|
2611
|
+
context: "graph-missing-target",
|
|
2612
|
+
issue: "NOT_FOUND",
|
|
2613
|
+
details: `Graph node "${target}" has no matching code target`,
|
|
2614
|
+
confidence: "high"
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
for (const edge of graphDriftData.staleEdges) {
|
|
2618
|
+
drifts.push({
|
|
2619
|
+
type: "api-signature",
|
|
2620
|
+
docFile: edge.docNodeId,
|
|
2621
|
+
line: 0,
|
|
2622
|
+
reference: edge.codeNodeId,
|
|
2623
|
+
context: `graph-stale-edge:${edge.edgeType}`,
|
|
2624
|
+
issue: "NOT_FOUND",
|
|
2625
|
+
details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
|
|
2626
|
+
confidence: "medium"
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
return (0, import_types.Ok)({
|
|
2630
|
+
drifts,
|
|
2631
|
+
stats: {
|
|
2632
|
+
docsScanned: graphDriftData.staleEdges.length,
|
|
2633
|
+
referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
|
|
2634
|
+
driftsFound: drifts.length,
|
|
2635
|
+
byType: { api: drifts.length, example: 0, structure: 0 }
|
|
2636
|
+
},
|
|
2637
|
+
severity: computeDriftSeverity(drifts.length)
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2593
2640
|
async function detectDocDrift(snapshot, config, graphDriftData) {
|
|
2594
2641
|
if (graphDriftData) {
|
|
2595
|
-
|
|
2596
|
-
for (const target of graphDriftData.missingTargets) {
|
|
2597
|
-
drifts2.push({
|
|
2598
|
-
type: "api-signature",
|
|
2599
|
-
docFile: target,
|
|
2600
|
-
line: 0,
|
|
2601
|
-
reference: target,
|
|
2602
|
-
context: "graph-missing-target",
|
|
2603
|
-
issue: "NOT_FOUND",
|
|
2604
|
-
details: `Graph node "${target}" has no matching code target`,
|
|
2605
|
-
confidence: "high"
|
|
2606
|
-
});
|
|
2607
|
-
}
|
|
2608
|
-
for (const edge of graphDriftData.staleEdges) {
|
|
2609
|
-
drifts2.push({
|
|
2610
|
-
type: "api-signature",
|
|
2611
|
-
docFile: edge.docNodeId,
|
|
2612
|
-
line: 0,
|
|
2613
|
-
reference: edge.codeNodeId,
|
|
2614
|
-
context: `graph-stale-edge:${edge.edgeType}`,
|
|
2615
|
-
issue: "NOT_FOUND",
|
|
2616
|
-
details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
|
|
2617
|
-
confidence: "medium"
|
|
2618
|
-
});
|
|
2619
|
-
}
|
|
2620
|
-
const severity2 = drifts2.length === 0 ? "none" : drifts2.length <= 3 ? "low" : drifts2.length <= 10 ? "medium" : "high";
|
|
2621
|
-
return (0, import_types.Ok)({
|
|
2622
|
-
drifts: drifts2,
|
|
2623
|
-
stats: {
|
|
2624
|
-
docsScanned: graphDriftData.staleEdges.length,
|
|
2625
|
-
referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
|
|
2626
|
-
driftsFound: drifts2.length,
|
|
2627
|
-
byType: { api: drifts2.length, example: 0, structure: 0 }
|
|
2628
|
-
},
|
|
2629
|
-
severity: severity2
|
|
2630
|
-
});
|
|
2642
|
+
return buildGraphDriftReport(graphDriftData);
|
|
2631
2643
|
}
|
|
2632
2644
|
const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
|
|
2633
2645
|
const drifts = [];
|
|
@@ -2676,6 +2688,23 @@ function resolveImportToFile(importSource, fromFile, snapshot) {
|
|
|
2676
2688
|
}
|
|
2677
2689
|
return null;
|
|
2678
2690
|
}
|
|
2691
|
+
function enqueueResolved(sources, current, snapshot, visited, queue) {
|
|
2692
|
+
for (const item of sources) {
|
|
2693
|
+
if (!item.source) continue;
|
|
2694
|
+
const resolved = resolveImportToFile(item.source, current, snapshot);
|
|
2695
|
+
if (resolved && !visited.has(resolved)) {
|
|
2696
|
+
queue.push(resolved);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function processReachabilityNode(current, snapshot, reachability, visited, queue) {
|
|
2701
|
+
reachability.set(current, true);
|
|
2702
|
+
const sourceFile = snapshot.files.find((f) => f.path === current);
|
|
2703
|
+
if (!sourceFile) return;
|
|
2704
|
+
enqueueResolved(sourceFile.imports, current, snapshot, visited, queue);
|
|
2705
|
+
const reExports = sourceFile.exports.filter((e) => e.isReExport);
|
|
2706
|
+
enqueueResolved(reExports, current, snapshot, visited, queue);
|
|
2707
|
+
}
|
|
2679
2708
|
function buildReachabilityMap(snapshot) {
|
|
2680
2709
|
const reachability = /* @__PURE__ */ new Map();
|
|
2681
2710
|
for (const file of snapshot.files) {
|
|
@@ -2687,23 +2716,7 @@ function buildReachabilityMap(snapshot) {
|
|
|
2687
2716
|
const current = queue.shift();
|
|
2688
2717
|
if (visited.has(current)) continue;
|
|
2689
2718
|
visited.add(current);
|
|
2690
|
-
|
|
2691
|
-
const sourceFile = snapshot.files.find((f) => f.path === current);
|
|
2692
|
-
if (!sourceFile) continue;
|
|
2693
|
-
for (const imp of sourceFile.imports) {
|
|
2694
|
-
const resolved = resolveImportToFile(imp.source, current, snapshot);
|
|
2695
|
-
if (resolved && !visited.has(resolved)) {
|
|
2696
|
-
queue.push(resolved);
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
for (const exp of sourceFile.exports) {
|
|
2700
|
-
if (exp.isReExport && exp.source) {
|
|
2701
|
-
const resolved = resolveImportToFile(exp.source, current, snapshot);
|
|
2702
|
-
if (resolved && !visited.has(resolved)) {
|
|
2703
|
-
queue.push(resolved);
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2719
|
+
processReachabilityNode(current, snapshot, reachability, visited, queue);
|
|
2707
2720
|
}
|
|
2708
2721
|
return reachability;
|
|
2709
2722
|
}
|
|
@@ -2773,21 +2786,27 @@ function findDeadExports(snapshot, usageMap, reachability) {
|
|
|
2773
2786
|
}
|
|
2774
2787
|
return deadExports;
|
|
2775
2788
|
}
|
|
2789
|
+
function maxLineOfValue(value) {
|
|
2790
|
+
if (Array.isArray(value)) {
|
|
2791
|
+
return value.reduce((m, item) => Math.max(m, findMaxLineInNode(item)), 0);
|
|
2792
|
+
}
|
|
2793
|
+
if (value && typeof value === "object") {
|
|
2794
|
+
return findMaxLineInNode(value);
|
|
2795
|
+
}
|
|
2796
|
+
return 0;
|
|
2797
|
+
}
|
|
2798
|
+
function maxLineOfNodeKeys(node) {
|
|
2799
|
+
let max = 0;
|
|
2800
|
+
for (const key of Object.keys(node)) {
|
|
2801
|
+
max = Math.max(max, maxLineOfValue(node[key]));
|
|
2802
|
+
}
|
|
2803
|
+
return max;
|
|
2804
|
+
}
|
|
2776
2805
|
function findMaxLineInNode(node) {
|
|
2777
2806
|
if (!node || typeof node !== "object") return 0;
|
|
2778
2807
|
const n = node;
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
const value = node[key];
|
|
2782
|
-
if (Array.isArray(value)) {
|
|
2783
|
-
for (const item of value) {
|
|
2784
|
-
maxLine = Math.max(maxLine, findMaxLineInNode(item));
|
|
2785
|
-
}
|
|
2786
|
-
} else if (value && typeof value === "object") {
|
|
2787
|
-
maxLine = Math.max(maxLine, findMaxLineInNode(value));
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
return maxLine;
|
|
2808
|
+
const locLine = n.loc?.end?.line ?? 0;
|
|
2809
|
+
return Math.max(locLine, maxLineOfNodeKeys(node));
|
|
2791
2810
|
}
|
|
2792
2811
|
function countLinesFromAST(ast) {
|
|
2793
2812
|
if (!ast.body || !Array.isArray(ast.body)) return 1;
|
|
@@ -2861,54 +2880,59 @@ function findDeadInternals(snapshot, _reachability) {
|
|
|
2861
2880
|
}
|
|
2862
2881
|
return deadInternals;
|
|
2863
2882
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
type: exportType,
|
|
2885
|
-
isDefault: false,
|
|
2886
|
-
reason: "NO_IMPORTERS"
|
|
2887
|
-
});
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
const reachableCount = graphDeadCodeData.reachableNodeIds instanceof Set ? graphDeadCodeData.reachableNodeIds.size : graphDeadCodeData.reachableNodeIds.length;
|
|
2891
|
-
const fileNodes = graphDeadCodeData.unreachableNodes.filter((n) => fileTypes.has(n.type));
|
|
2892
|
-
const exportNodes = graphDeadCodeData.unreachableNodes.filter((n) => exportTypes.has(n.type));
|
|
2893
|
-
const totalFiles = reachableCount + fileNodes.length;
|
|
2894
|
-
const totalExports2 = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
|
|
2895
|
-
const report2 = {
|
|
2896
|
-
deadExports: deadExports2,
|
|
2897
|
-
deadFiles: deadFiles2,
|
|
2898
|
-
deadInternals: [],
|
|
2899
|
-
unusedImports: [],
|
|
2900
|
-
stats: {
|
|
2901
|
-
filesAnalyzed: totalFiles,
|
|
2902
|
-
entryPointsUsed: [],
|
|
2903
|
-
totalExports: totalExports2,
|
|
2904
|
-
deadExportCount: deadExports2.length,
|
|
2905
|
-
totalFiles,
|
|
2906
|
-
deadFileCount: deadFiles2.length,
|
|
2907
|
-
estimatedDeadLines: 0
|
|
2908
|
-
}
|
|
2909
|
-
};
|
|
2910
|
-
return (0, import_types.Ok)(report2);
|
|
2883
|
+
var FILE_TYPES = /* @__PURE__ */ new Set(["file", "module"]);
|
|
2884
|
+
var EXPORT_TYPES = /* @__PURE__ */ new Set(["function", "class", "method", "interface", "variable"]);
|
|
2885
|
+
function classifyUnreachableNode(node, deadFiles, deadExports) {
|
|
2886
|
+
if (FILE_TYPES.has(node.type)) {
|
|
2887
|
+
deadFiles.push({
|
|
2888
|
+
path: node.path || node.id,
|
|
2889
|
+
reason: "NO_IMPORTERS",
|
|
2890
|
+
exportCount: 0,
|
|
2891
|
+
lineCount: 0
|
|
2892
|
+
});
|
|
2893
|
+
} else if (EXPORT_TYPES.has(node.type)) {
|
|
2894
|
+
const exportType = node.type === "method" ? "function" : node.type;
|
|
2895
|
+
deadExports.push({
|
|
2896
|
+
file: node.path || node.id,
|
|
2897
|
+
name: node.name,
|
|
2898
|
+
line: 0,
|
|
2899
|
+
type: exportType,
|
|
2900
|
+
isDefault: false,
|
|
2901
|
+
reason: "NO_IMPORTERS"
|
|
2902
|
+
});
|
|
2911
2903
|
}
|
|
2904
|
+
}
|
|
2905
|
+
function computeGraphReportStats(data, deadFiles, deadExports) {
|
|
2906
|
+
const reachableCount = data.reachableNodeIds instanceof Set ? data.reachableNodeIds.size : data.reachableNodeIds.length;
|
|
2907
|
+
const fileNodes = data.unreachableNodes.filter((n) => FILE_TYPES.has(n.type));
|
|
2908
|
+
const exportNodes = data.unreachableNodes.filter((n) => EXPORT_TYPES.has(n.type));
|
|
2909
|
+
const totalFiles = reachableCount + fileNodes.length;
|
|
2910
|
+
const totalExports = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
|
|
2911
|
+
return {
|
|
2912
|
+
filesAnalyzed: totalFiles,
|
|
2913
|
+
entryPointsUsed: [],
|
|
2914
|
+
totalExports,
|
|
2915
|
+
deadExportCount: deadExports.length,
|
|
2916
|
+
totalFiles,
|
|
2917
|
+
deadFileCount: deadFiles.length,
|
|
2918
|
+
estimatedDeadLines: 0
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
function buildReportFromGraph(data) {
|
|
2922
|
+
const deadFiles = [];
|
|
2923
|
+
const deadExports = [];
|
|
2924
|
+
for (const node of data.unreachableNodes) {
|
|
2925
|
+
classifyUnreachableNode(node, deadFiles, deadExports);
|
|
2926
|
+
}
|
|
2927
|
+
return {
|
|
2928
|
+
deadExports,
|
|
2929
|
+
deadFiles,
|
|
2930
|
+
deadInternals: [],
|
|
2931
|
+
unusedImports: [],
|
|
2932
|
+
stats: computeGraphReportStats(data, deadFiles, deadExports)
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
function buildReportFromSnapshot(snapshot) {
|
|
2912
2936
|
const reachability = buildReachabilityMap(snapshot);
|
|
2913
2937
|
const usageMap = buildExportUsageMap(snapshot);
|
|
2914
2938
|
const deadExports = findDeadExports(snapshot, usageMap, reachability);
|
|
@@ -2920,7 +2944,7 @@ async function detectDeadCode(snapshot, graphDeadCodeData) {
|
|
|
2920
2944
|
0
|
|
2921
2945
|
);
|
|
2922
2946
|
const estimatedDeadLines = deadFiles.reduce((acc, file) => acc + file.lineCount, 0);
|
|
2923
|
-
|
|
2947
|
+
return {
|
|
2924
2948
|
deadExports,
|
|
2925
2949
|
deadFiles,
|
|
2926
2950
|
deadInternals,
|
|
@@ -2935,6 +2959,9 @@ async function detectDeadCode(snapshot, graphDeadCodeData) {
|
|
|
2935
2959
|
estimatedDeadLines
|
|
2936
2960
|
}
|
|
2937
2961
|
};
|
|
2962
|
+
}
|
|
2963
|
+
async function detectDeadCode(snapshot, graphDeadCodeData) {
|
|
2964
|
+
const report = graphDeadCodeData ? buildReportFromGraph(graphDeadCodeData) : buildReportFromSnapshot(snapshot);
|
|
2938
2965
|
return (0, import_types.Ok)(report);
|
|
2939
2966
|
}
|
|
2940
2967
|
|
|
@@ -3205,26 +3232,28 @@ function findFunctionEnd(lines, startIdx) {
|
|
|
3205
3232
|
}
|
|
3206
3233
|
return lines.length - 1;
|
|
3207
3234
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
for (const pattern of
|
|
3235
|
+
var DECISION_PATTERNS = [
|
|
3236
|
+
/\bif\s*\(/g,
|
|
3237
|
+
/\belse\s+if\s*\(/g,
|
|
3238
|
+
/\bwhile\s*\(/g,
|
|
3239
|
+
/\bfor\s*\(/g,
|
|
3240
|
+
/\bcase\s+/g,
|
|
3241
|
+
/&&/g,
|
|
3242
|
+
/\|\|/g,
|
|
3243
|
+
/\?(?!=)/g,
|
|
3244
|
+
// Ternary ? but not ?. or ??
|
|
3245
|
+
/\bcatch\s*\(/g
|
|
3246
|
+
];
|
|
3247
|
+
function countDecisionPoints(body) {
|
|
3248
|
+
let count = 0;
|
|
3249
|
+
for (const pattern of DECISION_PATTERNS) {
|
|
3223
3250
|
const matches = body.match(pattern);
|
|
3224
|
-
if (matches)
|
|
3225
|
-
complexity += matches.length;
|
|
3226
|
-
}
|
|
3251
|
+
if (matches) count += matches.length;
|
|
3227
3252
|
}
|
|
3253
|
+
return count;
|
|
3254
|
+
}
|
|
3255
|
+
function computeCyclomaticComplexity(body) {
|
|
3256
|
+
let complexity = 1 + countDecisionPoints(body);
|
|
3228
3257
|
const elseIfMatches = body.match(/\belse\s+if\s*\(/g);
|
|
3229
3258
|
if (elseIfMatches) {
|
|
3230
3259
|
complexity -= elseIfMatches.length;
|
|
@@ -3502,8 +3531,8 @@ function resolveImportSource(source, fromFile, snapshot) {
|
|
|
3502
3531
|
}
|
|
3503
3532
|
return void 0;
|
|
3504
3533
|
}
|
|
3505
|
-
function
|
|
3506
|
-
|
|
3534
|
+
function resolveThresholds2(config) {
|
|
3535
|
+
return {
|
|
3507
3536
|
fanOut: { ...DEFAULT_THRESHOLDS2.fanOut, ...config?.thresholds?.fanOut },
|
|
3508
3537
|
fanIn: { ...DEFAULT_THRESHOLDS2.fanIn, ...config?.thresholds?.fanIn },
|
|
3509
3538
|
couplingRatio: { ...DEFAULT_THRESHOLDS2.couplingRatio, ...config?.thresholds?.couplingRatio },
|
|
@@ -3512,53 +3541,70 @@ function checkViolations(metrics, config) {
|
|
|
3512
3541
|
...config?.thresholds?.transitiveDependencyDepth
|
|
3513
3542
|
}
|
|
3514
3543
|
};
|
|
3544
|
+
}
|
|
3545
|
+
function checkFanOut(m, threshold) {
|
|
3546
|
+
if (m.fanOut <= threshold) return null;
|
|
3547
|
+
return {
|
|
3548
|
+
file: m.file,
|
|
3549
|
+
metric: "fanOut",
|
|
3550
|
+
value: m.fanOut,
|
|
3551
|
+
threshold,
|
|
3552
|
+
tier: 2,
|
|
3553
|
+
severity: "warning",
|
|
3554
|
+
message: `File has ${m.fanOut} imports (threshold: ${threshold})`
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
function checkFanIn(m, threshold) {
|
|
3558
|
+
if (m.fanIn <= threshold) return null;
|
|
3559
|
+
return {
|
|
3560
|
+
file: m.file,
|
|
3561
|
+
metric: "fanIn",
|
|
3562
|
+
value: m.fanIn,
|
|
3563
|
+
threshold,
|
|
3564
|
+
tier: 3,
|
|
3565
|
+
severity: "info",
|
|
3566
|
+
message: `File is imported by ${m.fanIn} files (threshold: ${threshold})`
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
function checkCouplingRatio(m, threshold) {
|
|
3570
|
+
const totalConnections = m.fanIn + m.fanOut;
|
|
3571
|
+
if (totalConnections <= 5 || m.couplingRatio <= threshold) return null;
|
|
3572
|
+
return {
|
|
3573
|
+
file: m.file,
|
|
3574
|
+
metric: "couplingRatio",
|
|
3575
|
+
value: m.couplingRatio,
|
|
3576
|
+
threshold,
|
|
3577
|
+
tier: 2,
|
|
3578
|
+
severity: "warning",
|
|
3579
|
+
message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${threshold})`
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
function checkTransitiveDepth(m, threshold) {
|
|
3583
|
+
if (m.transitiveDepth <= threshold) return null;
|
|
3584
|
+
return {
|
|
3585
|
+
file: m.file,
|
|
3586
|
+
metric: "transitiveDependencyDepth",
|
|
3587
|
+
value: m.transitiveDepth,
|
|
3588
|
+
threshold,
|
|
3589
|
+
tier: 3,
|
|
3590
|
+
severity: "info",
|
|
3591
|
+
message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${threshold})`
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
function checkMetricViolations(m, thresholds) {
|
|
3595
|
+
const candidates = [
|
|
3596
|
+
checkFanOut(m, thresholds.fanOut.warn),
|
|
3597
|
+
checkFanIn(m, thresholds.fanIn.info),
|
|
3598
|
+
checkCouplingRatio(m, thresholds.couplingRatio.warn),
|
|
3599
|
+
checkTransitiveDepth(m, thresholds.transitiveDependencyDepth.info)
|
|
3600
|
+
];
|
|
3601
|
+
return candidates.filter((v) => v !== null);
|
|
3602
|
+
}
|
|
3603
|
+
function checkViolations(metrics, config) {
|
|
3604
|
+
const thresholds = resolveThresholds2(config);
|
|
3515
3605
|
const violations = [];
|
|
3516
3606
|
for (const m of metrics) {
|
|
3517
|
-
|
|
3518
|
-
violations.push({
|
|
3519
|
-
file: m.file,
|
|
3520
|
-
metric: "fanOut",
|
|
3521
|
-
value: m.fanOut,
|
|
3522
|
-
threshold: thresholds.fanOut.warn,
|
|
3523
|
-
tier: 2,
|
|
3524
|
-
severity: "warning",
|
|
3525
|
-
message: `File has ${m.fanOut} imports (threshold: ${thresholds.fanOut.warn})`
|
|
3526
|
-
});
|
|
3527
|
-
}
|
|
3528
|
-
if (thresholds.fanIn.info !== void 0 && m.fanIn > thresholds.fanIn.info) {
|
|
3529
|
-
violations.push({
|
|
3530
|
-
file: m.file,
|
|
3531
|
-
metric: "fanIn",
|
|
3532
|
-
value: m.fanIn,
|
|
3533
|
-
threshold: thresholds.fanIn.info,
|
|
3534
|
-
tier: 3,
|
|
3535
|
-
severity: "info",
|
|
3536
|
-
message: `File is imported by ${m.fanIn} files (threshold: ${thresholds.fanIn.info})`
|
|
3537
|
-
});
|
|
3538
|
-
}
|
|
3539
|
-
const totalConnections = m.fanIn + m.fanOut;
|
|
3540
|
-
if (totalConnections > 5 && thresholds.couplingRatio.warn !== void 0 && m.couplingRatio > thresholds.couplingRatio.warn) {
|
|
3541
|
-
violations.push({
|
|
3542
|
-
file: m.file,
|
|
3543
|
-
metric: "couplingRatio",
|
|
3544
|
-
value: m.couplingRatio,
|
|
3545
|
-
threshold: thresholds.couplingRatio.warn,
|
|
3546
|
-
tier: 2,
|
|
3547
|
-
severity: "warning",
|
|
3548
|
-
message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${thresholds.couplingRatio.warn})`
|
|
3549
|
-
});
|
|
3550
|
-
}
|
|
3551
|
-
if (thresholds.transitiveDependencyDepth.info !== void 0 && m.transitiveDepth > thresholds.transitiveDependencyDepth.info) {
|
|
3552
|
-
violations.push({
|
|
3553
|
-
file: m.file,
|
|
3554
|
-
metric: "transitiveDependencyDepth",
|
|
3555
|
-
value: m.transitiveDepth,
|
|
3556
|
-
threshold: thresholds.transitiveDependencyDepth.info,
|
|
3557
|
-
tier: 3,
|
|
3558
|
-
severity: "info",
|
|
3559
|
-
message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${thresholds.transitiveDependencyDepth.info})`
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3607
|
+
violations.push(...checkMetricViolations(m, thresholds));
|
|
3562
3608
|
}
|
|
3563
3609
|
return violations;
|
|
3564
3610
|
}
|
|
@@ -3668,48 +3714,52 @@ async function detectSizeBudgetViolations(rootDir, config) {
|
|
|
3668
3714
|
}
|
|
3669
3715
|
|
|
3670
3716
|
// src/entropy/fixers/suggestions.ts
|
|
3717
|
+
function deadFileSuggestion(file) {
|
|
3718
|
+
return {
|
|
3719
|
+
type: "delete",
|
|
3720
|
+
priority: "high",
|
|
3721
|
+
source: "dead-code",
|
|
3722
|
+
relatedIssues: [`dead-file:${file.path}`],
|
|
3723
|
+
title: `Remove dead file: ${file.path.split("/").pop()}`,
|
|
3724
|
+
description: `This file is not imported by any other file and can be safely removed.`,
|
|
3725
|
+
files: [file.path],
|
|
3726
|
+
steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
|
|
3727
|
+
whyManual: "File deletion requires verification that no dynamic imports exist"
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
function deadExportSuggestion(exp) {
|
|
3731
|
+
return {
|
|
3732
|
+
type: "refactor",
|
|
3733
|
+
priority: "medium",
|
|
3734
|
+
source: "dead-code",
|
|
3735
|
+
relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
|
|
3736
|
+
title: `Remove unused export: ${exp.name}`,
|
|
3737
|
+
description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
|
|
3738
|
+
files: [exp.file],
|
|
3739
|
+
steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
|
|
3740
|
+
whyManual: "Export removal may affect external consumers not in scope"
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
function unusedImportSuggestion(imp) {
|
|
3744
|
+
const plural = imp.specifiers.length > 1;
|
|
3745
|
+
return {
|
|
3746
|
+
type: "delete",
|
|
3747
|
+
priority: "medium",
|
|
3748
|
+
source: "dead-code",
|
|
3749
|
+
relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
|
|
3750
|
+
title: `Remove unused import${plural ? "s" : ""}: ${imp.specifiers.join(", ")}`,
|
|
3751
|
+
description: `The import${plural ? "s" : ""} from "${imp.source}" ${plural ? "are" : "is"} not used.`,
|
|
3752
|
+
files: [imp.file],
|
|
3753
|
+
steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
|
|
3754
|
+
whyManual: "Import removal can be auto-fixed"
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3671
3757
|
function generateDeadCodeSuggestions(report) {
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
source: "dead-code",
|
|
3678
|
-
relatedIssues: [`dead-file:${file.path}`],
|
|
3679
|
-
title: `Remove dead file: ${file.path.split("/").pop()}`,
|
|
3680
|
-
description: `This file is not imported by any other file and can be safely removed.`,
|
|
3681
|
-
files: [file.path],
|
|
3682
|
-
steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
|
|
3683
|
-
whyManual: "File deletion requires verification that no dynamic imports exist"
|
|
3684
|
-
});
|
|
3685
|
-
}
|
|
3686
|
-
for (const exp of report.deadExports) {
|
|
3687
|
-
suggestions.push({
|
|
3688
|
-
type: "refactor",
|
|
3689
|
-
priority: "medium",
|
|
3690
|
-
source: "dead-code",
|
|
3691
|
-
relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
|
|
3692
|
-
title: `Remove unused export: ${exp.name}`,
|
|
3693
|
-
description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
|
|
3694
|
-
files: [exp.file],
|
|
3695
|
-
steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
|
|
3696
|
-
whyManual: "Export removal may affect external consumers not in scope"
|
|
3697
|
-
});
|
|
3698
|
-
}
|
|
3699
|
-
for (const imp of report.unusedImports) {
|
|
3700
|
-
suggestions.push({
|
|
3701
|
-
type: "delete",
|
|
3702
|
-
priority: "medium",
|
|
3703
|
-
source: "dead-code",
|
|
3704
|
-
relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
|
|
3705
|
-
title: `Remove unused import${imp.specifiers.length > 1 ? "s" : ""}: ${imp.specifiers.join(", ")}`,
|
|
3706
|
-
description: `The import${imp.specifiers.length > 1 ? "s" : ""} from "${imp.source}" ${imp.specifiers.length > 1 ? "are" : "is"} not used.`,
|
|
3707
|
-
files: [imp.file],
|
|
3708
|
-
steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
|
|
3709
|
-
whyManual: "Import removal can be auto-fixed"
|
|
3710
|
-
});
|
|
3711
|
-
}
|
|
3712
|
-
return suggestions;
|
|
3758
|
+
return [
|
|
3759
|
+
...report.deadFiles.map(deadFileSuggestion),
|
|
3760
|
+
...report.deadExports.map(deadExportSuggestion),
|
|
3761
|
+
...report.unusedImports.map(unusedImportSuggestion)
|
|
3762
|
+
];
|
|
3713
3763
|
}
|
|
3714
3764
|
function generateDriftSuggestions(report) {
|
|
3715
3765
|
const suggestions = [];
|
|
@@ -4152,43 +4202,55 @@ async function createBackup(filePath, backupDir) {
|
|
|
4152
4202
|
);
|
|
4153
4203
|
}
|
|
4154
4204
|
}
|
|
4155
|
-
async function
|
|
4205
|
+
async function applyDeleteFile(fix, config) {
|
|
4206
|
+
if (config.createBackup && config.backupDir) {
|
|
4207
|
+
const backupResult = await createBackup(fix.file, config.backupDir);
|
|
4208
|
+
if (!backupResult.ok) return (0, import_types.Err)({ fix, error: backupResult.error.message });
|
|
4209
|
+
}
|
|
4210
|
+
await unlink2(fix.file);
|
|
4211
|
+
return (0, import_types.Ok)(void 0);
|
|
4212
|
+
}
|
|
4213
|
+
async function applyDeleteLines(fix) {
|
|
4214
|
+
if (fix.line !== void 0) {
|
|
4215
|
+
const content = await readFile5(fix.file, "utf-8");
|
|
4216
|
+
const lines = content.split("\n");
|
|
4217
|
+
lines.splice(fix.line - 1, 1);
|
|
4218
|
+
await writeFile3(fix.file, lines.join("\n"));
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
async function applyReplace(fix) {
|
|
4222
|
+
if (fix.oldContent && fix.newContent !== void 0) {
|
|
4223
|
+
const content = await readFile5(fix.file, "utf-8");
|
|
4224
|
+
await writeFile3(fix.file, content.replace(fix.oldContent, fix.newContent));
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
async function applyInsert(fix) {
|
|
4228
|
+
if (fix.line !== void 0 && fix.newContent) {
|
|
4229
|
+
const content = await readFile5(fix.file, "utf-8");
|
|
4230
|
+
const lines = content.split("\n");
|
|
4231
|
+
lines.splice(fix.line - 1, 0, fix.newContent);
|
|
4232
|
+
await writeFile3(fix.file, lines.join("\n"));
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
async function applySingleFix(fix, config) {
|
|
4156
4236
|
if (config.dryRun) {
|
|
4157
4237
|
return (0, import_types.Ok)(fix);
|
|
4158
4238
|
}
|
|
4159
4239
|
try {
|
|
4160
4240
|
switch (fix.action) {
|
|
4161
|
-
case "delete-file":
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
if (!backupResult.ok) {
|
|
4165
|
-
return (0, import_types.Err)({ fix, error: backupResult.error.message });
|
|
4166
|
-
}
|
|
4167
|
-
}
|
|
4168
|
-
await unlink2(fix.file);
|
|
4241
|
+
case "delete-file": {
|
|
4242
|
+
const result = await applyDeleteFile(fix, config);
|
|
4243
|
+
if (!result.ok) return result;
|
|
4169
4244
|
break;
|
|
4245
|
+
}
|
|
4170
4246
|
case "delete-lines":
|
|
4171
|
-
|
|
4172
|
-
const content = await readFile5(fix.file, "utf-8");
|
|
4173
|
-
const lines = content.split("\n");
|
|
4174
|
-
lines.splice(fix.line - 1, 1);
|
|
4175
|
-
await writeFile3(fix.file, lines.join("\n"));
|
|
4176
|
-
}
|
|
4247
|
+
await applyDeleteLines(fix);
|
|
4177
4248
|
break;
|
|
4178
4249
|
case "replace":
|
|
4179
|
-
|
|
4180
|
-
const content = await readFile5(fix.file, "utf-8");
|
|
4181
|
-
const newContent = content.replace(fix.oldContent, fix.newContent);
|
|
4182
|
-
await writeFile3(fix.file, newContent);
|
|
4183
|
-
}
|
|
4250
|
+
await applyReplace(fix);
|
|
4184
4251
|
break;
|
|
4185
4252
|
case "insert":
|
|
4186
|
-
|
|
4187
|
-
const content = await readFile5(fix.file, "utf-8");
|
|
4188
|
-
const lines = content.split("\n");
|
|
4189
|
-
lines.splice(fix.line - 1, 0, fix.newContent);
|
|
4190
|
-
await writeFile3(fix.file, lines.join("\n"));
|
|
4191
|
-
}
|
|
4253
|
+
await applyInsert(fix);
|
|
4192
4254
|
break;
|
|
4193
4255
|
}
|
|
4194
4256
|
return (0, import_types.Ok)(fix);
|
|
@@ -4361,6 +4423,21 @@ function applyHotspotDowngrade(finding, hotspot) {
|
|
|
4361
4423
|
}
|
|
4362
4424
|
return finding;
|
|
4363
4425
|
}
|
|
4426
|
+
function mergeGroup(group) {
|
|
4427
|
+
if (group.length === 1) return [group[0]];
|
|
4428
|
+
const deadCode = group.find((f) => f.concern === "dead-code");
|
|
4429
|
+
const arch = group.find((f) => f.concern === "architecture");
|
|
4430
|
+
if (deadCode && arch) {
|
|
4431
|
+
return [
|
|
4432
|
+
{
|
|
4433
|
+
...deadCode,
|
|
4434
|
+
description: `${deadCode.description} (also violates architecture: ${arch.type})`,
|
|
4435
|
+
suggestion: deadCode.fixAction ? `${deadCode.fixAction} (resolves both dead code and architecture violation)` : deadCode.suggestion
|
|
4436
|
+
}
|
|
4437
|
+
];
|
|
4438
|
+
}
|
|
4439
|
+
return group;
|
|
4440
|
+
}
|
|
4364
4441
|
function deduplicateCleanupFindings(findings) {
|
|
4365
4442
|
const byFileAndLine = /* @__PURE__ */ new Map();
|
|
4366
4443
|
for (const f of findings) {
|
|
@@ -4371,21 +4448,7 @@ function deduplicateCleanupFindings(findings) {
|
|
|
4371
4448
|
}
|
|
4372
4449
|
const result = [];
|
|
4373
4450
|
for (const group of byFileAndLine.values()) {
|
|
4374
|
-
|
|
4375
|
-
result.push(group[0]);
|
|
4376
|
-
continue;
|
|
4377
|
-
}
|
|
4378
|
-
const deadCode = group.find((f) => f.concern === "dead-code");
|
|
4379
|
-
const arch = group.find((f) => f.concern === "architecture");
|
|
4380
|
-
if (deadCode && arch) {
|
|
4381
|
-
result.push({
|
|
4382
|
-
...deadCode,
|
|
4383
|
-
description: `${deadCode.description} (also violates architecture: ${arch.type})`,
|
|
4384
|
-
suggestion: deadCode.fixAction ? `${deadCode.fixAction} (resolves both dead code and architecture violation)` : deadCode.suggestion
|
|
4385
|
-
});
|
|
4386
|
-
} else {
|
|
4387
|
-
result.push(...group);
|
|
4388
|
-
}
|
|
4451
|
+
result.push(...mergeGroup(group));
|
|
4389
4452
|
}
|
|
4390
4453
|
return result;
|
|
4391
4454
|
}
|
|
@@ -4758,6 +4821,32 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
|
4758
4821
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
4759
4822
|
var FUNCTION_DECL_RE = /(?:export\s+)?(?:async\s+)?function\s+(\w+)/;
|
|
4760
4823
|
var CONST_DECL_RE = /(?:export\s+)?(?:const|let)\s+(\w+)\s*=/;
|
|
4824
|
+
function mergeGraphInferred(highFanInFunctions, seen) {
|
|
4825
|
+
let added = 0;
|
|
4826
|
+
for (const item of highFanInFunctions) {
|
|
4827
|
+
const key = `${item.file}::${item.function}`;
|
|
4828
|
+
if (!seen.has(key)) {
|
|
4829
|
+
seen.set(key, {
|
|
4830
|
+
file: item.file,
|
|
4831
|
+
function: item.function,
|
|
4832
|
+
source: "graph-inferred",
|
|
4833
|
+
fanIn: item.fanIn
|
|
4834
|
+
});
|
|
4835
|
+
added++;
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
return added;
|
|
4839
|
+
}
|
|
4840
|
+
function isCommentOrBlank(line) {
|
|
4841
|
+
return line === "" || line === "*/" || line === "*" || line.startsWith("*") || line.startsWith("//");
|
|
4842
|
+
}
|
|
4843
|
+
function matchDeclarationName(line) {
|
|
4844
|
+
const funcMatch = line.match(FUNCTION_DECL_RE);
|
|
4845
|
+
if (funcMatch?.[1]) return funcMatch[1];
|
|
4846
|
+
const constMatch = line.match(CONST_DECL_RE);
|
|
4847
|
+
if (constMatch?.[1]) return constMatch[1];
|
|
4848
|
+
return null;
|
|
4849
|
+
}
|
|
4761
4850
|
var CriticalPathResolver = class {
|
|
4762
4851
|
projectRoot;
|
|
4763
4852
|
constructor(projectRoot) {
|
|
@@ -4770,27 +4859,12 @@ var CriticalPathResolver = class {
|
|
|
4770
4859
|
const key = `${entry.file}::${entry.function}`;
|
|
4771
4860
|
seen.set(key, entry);
|
|
4772
4861
|
}
|
|
4773
|
-
|
|
4774
|
-
if (graphData) {
|
|
4775
|
-
for (const item of graphData.highFanInFunctions) {
|
|
4776
|
-
const key = `${item.file}::${item.function}`;
|
|
4777
|
-
if (!seen.has(key)) {
|
|
4778
|
-
seen.set(key, {
|
|
4779
|
-
file: item.file,
|
|
4780
|
-
function: item.function,
|
|
4781
|
-
source: "graph-inferred",
|
|
4782
|
-
fanIn: item.fanIn
|
|
4783
|
-
});
|
|
4784
|
-
graphInferred++;
|
|
4785
|
-
}
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4862
|
+
const graphInferred = graphData ? mergeGraphInferred(graphData.highFanInFunctions, seen) : 0;
|
|
4788
4863
|
const entries = Array.from(seen.values());
|
|
4789
|
-
const annotatedCount = annotated.length;
|
|
4790
4864
|
return {
|
|
4791
4865
|
entries,
|
|
4792
4866
|
stats: {
|
|
4793
|
-
annotated:
|
|
4867
|
+
annotated: annotated.length,
|
|
4794
4868
|
graphInferred,
|
|
4795
4869
|
total: entries.length
|
|
4796
4870
|
}
|
|
@@ -4817,6 +4891,14 @@ var CriticalPathResolver = class {
|
|
|
4817
4891
|
}
|
|
4818
4892
|
}
|
|
4819
4893
|
}
|
|
4894
|
+
resolveFunctionName(lines, fromIndex) {
|
|
4895
|
+
for (let j = fromIndex; j < lines.length; j++) {
|
|
4896
|
+
const nextLine = lines[j].trim();
|
|
4897
|
+
if (isCommentOrBlank(nextLine)) continue;
|
|
4898
|
+
return matchDeclarationName(nextLine);
|
|
4899
|
+
}
|
|
4900
|
+
return null;
|
|
4901
|
+
}
|
|
4820
4902
|
scanFile(filePath, entries) {
|
|
4821
4903
|
let content;
|
|
4822
4904
|
try {
|
|
@@ -4827,30 +4909,10 @@ var CriticalPathResolver = class {
|
|
|
4827
4909
|
const lines = content.split("\n");
|
|
4828
4910
|
const relativePath = path.relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
4829
4911
|
for (let i = 0; i < lines.length; i++) {
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
if (nextLine === "" || nextLine === "*/" || nextLine === "*") continue;
|
|
4835
|
-
if (nextLine.startsWith("*") || nextLine.startsWith("//")) continue;
|
|
4836
|
-
const funcMatch = nextLine.match(FUNCTION_DECL_RE);
|
|
4837
|
-
if (funcMatch && funcMatch[1]) {
|
|
4838
|
-
entries.push({
|
|
4839
|
-
file: relativePath,
|
|
4840
|
-
function: funcMatch[1],
|
|
4841
|
-
source: "annotation"
|
|
4842
|
-
});
|
|
4843
|
-
} else {
|
|
4844
|
-
const constMatch = nextLine.match(CONST_DECL_RE);
|
|
4845
|
-
if (constMatch && constMatch[1]) {
|
|
4846
|
-
entries.push({
|
|
4847
|
-
file: relativePath,
|
|
4848
|
-
function: constMatch[1],
|
|
4849
|
-
source: "annotation"
|
|
4850
|
-
});
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4853
|
-
break;
|
|
4912
|
+
if (!lines[i].includes("@perf-critical")) continue;
|
|
4913
|
+
const fnName = this.resolveFunctionName(lines, i + 1);
|
|
4914
|
+
if (fnName) {
|
|
4915
|
+
entries.push({ file: relativePath, function: fnName, source: "annotation" });
|
|
4854
4916
|
}
|
|
4855
4917
|
}
|
|
4856
4918
|
}
|
|
@@ -5005,14 +5067,19 @@ function detectFileStatus(part) {
|
|
|
5005
5067
|
if (part.includes("rename from")) return "renamed";
|
|
5006
5068
|
return "modified";
|
|
5007
5069
|
}
|
|
5008
|
-
function
|
|
5070
|
+
function parseDiffHeader(part) {
|
|
5009
5071
|
if (!part.trim()) return null;
|
|
5010
5072
|
const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
|
|
5011
5073
|
if (!headerMatch || !headerMatch[2]) return null;
|
|
5074
|
+
return headerMatch[2];
|
|
5075
|
+
}
|
|
5076
|
+
function parseDiffPart(part) {
|
|
5077
|
+
const path31 = parseDiffHeader(part);
|
|
5078
|
+
if (!path31) return null;
|
|
5012
5079
|
const additionRegex = /^\+(?!\+\+)/gm;
|
|
5013
5080
|
const deletionRegex = /^-(?!--)/gm;
|
|
5014
5081
|
return {
|
|
5015
|
-
path:
|
|
5082
|
+
path: path31,
|
|
5016
5083
|
status: detectFileStatus(part),
|
|
5017
5084
|
additions: (part.match(additionRegex) || []).length,
|
|
5018
5085
|
deletions: (part.match(deletionRegex) || []).length
|
|
@@ -5034,100 +5101,136 @@ function parseDiff(diff2) {
|
|
|
5034
5101
|
});
|
|
5035
5102
|
}
|
|
5036
5103
|
}
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5104
|
+
function checkForbiddenPatterns(diff2, forbiddenPatterns, nextId) {
|
|
5105
|
+
const items = [];
|
|
5106
|
+
if (!forbiddenPatterns) return items;
|
|
5107
|
+
for (const forbidden of forbiddenPatterns) {
|
|
5108
|
+
const pattern = typeof forbidden.pattern === "string" ? new RegExp(forbidden.pattern, "g") : forbidden.pattern;
|
|
5109
|
+
if (!pattern.test(diff2)) continue;
|
|
5110
|
+
items.push({
|
|
5111
|
+
id: nextId(),
|
|
5112
|
+
category: "diff",
|
|
5113
|
+
check: `Forbidden pattern: ${forbidden.pattern}`,
|
|
5114
|
+
passed: false,
|
|
5115
|
+
severity: forbidden.severity,
|
|
5116
|
+
details: forbidden.message,
|
|
5117
|
+
suggestion: `Remove occurrences of ${forbidden.pattern}`
|
|
5118
|
+
});
|
|
5040
5119
|
}
|
|
5120
|
+
return items;
|
|
5121
|
+
}
|
|
5122
|
+
function checkMaxChangedFiles(files, maxChangedFiles, nextId) {
|
|
5123
|
+
if (!maxChangedFiles || files.length <= maxChangedFiles) return [];
|
|
5124
|
+
return [
|
|
5125
|
+
{
|
|
5126
|
+
id: nextId(),
|
|
5127
|
+
category: "diff",
|
|
5128
|
+
check: `PR size: ${files.length} files changed`,
|
|
5129
|
+
passed: false,
|
|
5130
|
+
severity: "warning",
|
|
5131
|
+
details: `This PR changes ${files.length} files, which exceeds the recommended maximum of ${maxChangedFiles}`,
|
|
5132
|
+
suggestion: "Consider breaking this into smaller PRs"
|
|
5133
|
+
}
|
|
5134
|
+
];
|
|
5135
|
+
}
|
|
5136
|
+
function checkFileSizes(files, maxFileSize, nextId) {
|
|
5041
5137
|
const items = [];
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5138
|
+
if (!maxFileSize) return items;
|
|
5139
|
+
for (const file of files) {
|
|
5140
|
+
const totalLines = file.additions + file.deletions;
|
|
5141
|
+
if (totalLines <= maxFileSize) continue;
|
|
5142
|
+
items.push({
|
|
5143
|
+
id: nextId(),
|
|
5144
|
+
category: "diff",
|
|
5145
|
+
check: `File size: ${file.path}`,
|
|
5146
|
+
passed: false,
|
|
5147
|
+
severity: "warning",
|
|
5148
|
+
details: `File has ${totalLines} lines changed, exceeding limit of ${maxFileSize}`,
|
|
5149
|
+
file: file.path,
|
|
5150
|
+
suggestion: "Consider splitting this file into smaller modules"
|
|
5151
|
+
});
|
|
5152
|
+
}
|
|
5153
|
+
return items;
|
|
5154
|
+
}
|
|
5155
|
+
function checkTestCoverageGraph(files, graphImpactData) {
|
|
5156
|
+
const items = [];
|
|
5157
|
+
for (const file of files) {
|
|
5158
|
+
if (file.status !== "added" || !file.path.endsWith(".ts") || file.path.includes(".test.")) {
|
|
5159
|
+
continue;
|
|
5057
5160
|
}
|
|
5161
|
+
const hasGraphTest = graphImpactData.affectedTests.some((t) => t.coversFile === file.path);
|
|
5162
|
+
if (hasGraphTest) continue;
|
|
5163
|
+
items.push({
|
|
5164
|
+
id: `test-coverage-${file.path}`,
|
|
5165
|
+
category: "diff",
|
|
5166
|
+
check: "Test coverage (graph)",
|
|
5167
|
+
passed: false,
|
|
5168
|
+
severity: "warning",
|
|
5169
|
+
details: `New file ${file.path} has no test file linked in the graph`,
|
|
5170
|
+
file: file.path
|
|
5171
|
+
});
|
|
5058
5172
|
}
|
|
5059
|
-
|
|
5173
|
+
return items;
|
|
5174
|
+
}
|
|
5175
|
+
function checkTestCoverageFilename(files, nextId) {
|
|
5176
|
+
const items = [];
|
|
5177
|
+
const addedSourceFiles = files.filter(
|
|
5178
|
+
(f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
|
|
5179
|
+
);
|
|
5180
|
+
const testFiles = files.filter((f) => f.path.includes(".test."));
|
|
5181
|
+
for (const sourceFile of addedSourceFiles) {
|
|
5182
|
+
const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
|
|
5183
|
+
const hasTest = testFiles.some(
|
|
5184
|
+
(t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
|
|
5185
|
+
);
|
|
5186
|
+
if (hasTest) continue;
|
|
5060
5187
|
items.push({
|
|
5061
|
-
id:
|
|
5188
|
+
id: nextId(),
|
|
5062
5189
|
category: "diff",
|
|
5063
|
-
check: `
|
|
5190
|
+
check: `Test coverage: ${sourceFile.path}`,
|
|
5064
5191
|
passed: false,
|
|
5065
5192
|
severity: "warning",
|
|
5066
|
-
details:
|
|
5067
|
-
|
|
5193
|
+
details: "New source file added without corresponding test file",
|
|
5194
|
+
file: sourceFile.path,
|
|
5195
|
+
suggestion: `Add tests in ${expectedTestPath}`
|
|
5068
5196
|
});
|
|
5069
5197
|
}
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
check: `File size: ${file.path}`,
|
|
5078
|
-
passed: false,
|
|
5079
|
-
severity: "warning",
|
|
5080
|
-
details: `File has ${totalLines} lines changed, exceeding limit of ${options.maxFileSize}`,
|
|
5081
|
-
file: file.path,
|
|
5082
|
-
suggestion: "Consider splitting this file into smaller modules"
|
|
5083
|
-
});
|
|
5084
|
-
}
|
|
5198
|
+
return items;
|
|
5199
|
+
}
|
|
5200
|
+
function checkDocCoverage2(files, graphImpactData) {
|
|
5201
|
+
const items = [];
|
|
5202
|
+
for (const file of files) {
|
|
5203
|
+
if (file.status !== "modified" || !file.path.endsWith(".ts") || file.path.includes(".test.")) {
|
|
5204
|
+
continue;
|
|
5085
5205
|
}
|
|
5206
|
+
const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
|
|
5207
|
+
if (hasDoc) continue;
|
|
5208
|
+
items.push({
|
|
5209
|
+
id: `doc-coverage-${file.path}`,
|
|
5210
|
+
category: "diff",
|
|
5211
|
+
check: "Documentation coverage (graph)",
|
|
5212
|
+
passed: true,
|
|
5213
|
+
severity: "info",
|
|
5214
|
+
details: `Modified file ${file.path} has no documentation linked in the graph`,
|
|
5215
|
+
file: file.path
|
|
5216
|
+
});
|
|
5086
5217
|
}
|
|
5218
|
+
return items;
|
|
5219
|
+
}
|
|
5220
|
+
async function analyzeDiff(changes, options, graphImpactData) {
|
|
5221
|
+
if (!options?.enabled) {
|
|
5222
|
+
return (0, import_types.Ok)([]);
|
|
5223
|
+
}
|
|
5224
|
+
let itemId = 0;
|
|
5225
|
+
const nextId = () => `diff-${++itemId}`;
|
|
5226
|
+
const items = [
|
|
5227
|
+
...checkForbiddenPatterns(changes.diff, options.forbiddenPatterns, nextId),
|
|
5228
|
+
...checkMaxChangedFiles(changes.files, options.maxChangedFiles, nextId),
|
|
5229
|
+
...checkFileSizes(changes.files, options.maxFileSize, nextId)
|
|
5230
|
+
];
|
|
5087
5231
|
if (options.checkTestCoverage) {
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
if (file.status === "added" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
|
|
5091
|
-
const hasGraphTest = graphImpactData.affectedTests.some(
|
|
5092
|
-
(t) => t.coversFile === file.path
|
|
5093
|
-
);
|
|
5094
|
-
if (!hasGraphTest) {
|
|
5095
|
-
items.push({
|
|
5096
|
-
id: `test-coverage-${file.path}`,
|
|
5097
|
-
category: "diff",
|
|
5098
|
-
check: "Test coverage (graph)",
|
|
5099
|
-
passed: false,
|
|
5100
|
-
severity: "warning",
|
|
5101
|
-
details: `New file ${file.path} has no test file linked in the graph`,
|
|
5102
|
-
file: file.path
|
|
5103
|
-
});
|
|
5104
|
-
}
|
|
5105
|
-
}
|
|
5106
|
-
}
|
|
5107
|
-
} else {
|
|
5108
|
-
const addedSourceFiles = changes.files.filter(
|
|
5109
|
-
(f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
|
|
5110
|
-
);
|
|
5111
|
-
const testFiles = changes.files.filter((f) => f.path.includes(".test."));
|
|
5112
|
-
for (const sourceFile of addedSourceFiles) {
|
|
5113
|
-
const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
|
|
5114
|
-
const hasTest = testFiles.some(
|
|
5115
|
-
(t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
|
|
5116
|
-
);
|
|
5117
|
-
if (!hasTest) {
|
|
5118
|
-
items.push({
|
|
5119
|
-
id: `diff-${++itemId}`,
|
|
5120
|
-
category: "diff",
|
|
5121
|
-
check: `Test coverage: ${sourceFile.path}`,
|
|
5122
|
-
passed: false,
|
|
5123
|
-
severity: "warning",
|
|
5124
|
-
details: "New source file added without corresponding test file",
|
|
5125
|
-
file: sourceFile.path,
|
|
5126
|
-
suggestion: `Add tests in ${expectedTestPath}`
|
|
5127
|
-
});
|
|
5128
|
-
}
|
|
5129
|
-
}
|
|
5130
|
-
}
|
|
5232
|
+
const coverageItems = graphImpactData ? checkTestCoverageGraph(changes.files, graphImpactData) : checkTestCoverageFilename(changes.files, nextId);
|
|
5233
|
+
items.push(...coverageItems);
|
|
5131
5234
|
}
|
|
5132
5235
|
if (graphImpactData && graphImpactData.impactScope > 20) {
|
|
5133
5236
|
items.push({
|
|
@@ -5140,22 +5243,7 @@ async function analyzeDiff(changes, options, graphImpactData) {
|
|
|
5140
5243
|
});
|
|
5141
5244
|
}
|
|
5142
5245
|
if (graphImpactData) {
|
|
5143
|
-
|
|
5144
|
-
if (file.status === "modified" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
|
|
5145
|
-
const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
|
|
5146
|
-
if (!hasDoc) {
|
|
5147
|
-
items.push({
|
|
5148
|
-
id: `doc-coverage-${file.path}`,
|
|
5149
|
-
category: "diff",
|
|
5150
|
-
check: "Documentation coverage (graph)",
|
|
5151
|
-
passed: true,
|
|
5152
|
-
severity: "info",
|
|
5153
|
-
details: `Modified file ${file.path} has no documentation linked in the graph`,
|
|
5154
|
-
file: file.path
|
|
5155
|
-
});
|
|
5156
|
-
}
|
|
5157
|
-
}
|
|
5158
|
-
}
|
|
5246
|
+
items.push(...checkDocCoverage2(changes.files, graphImpactData));
|
|
5159
5247
|
}
|
|
5160
5248
|
return (0, import_types.Ok)(items);
|
|
5161
5249
|
}
|
|
@@ -5739,6 +5827,28 @@ function constraintRuleId(category, scope, description) {
|
|
|
5739
5827
|
}
|
|
5740
5828
|
|
|
5741
5829
|
// src/architecture/collectors/circular-deps.ts
|
|
5830
|
+
function makeStubParser() {
|
|
5831
|
+
return {
|
|
5832
|
+
name: "typescript",
|
|
5833
|
+
extensions: [".ts", ".tsx"],
|
|
5834
|
+
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
|
|
5835
|
+
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
|
|
5836
|
+
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
|
|
5837
|
+
health: async () => ({ ok: true, value: { available: true } })
|
|
5838
|
+
};
|
|
5839
|
+
}
|
|
5840
|
+
function mapCycleViolations(cycles, rootDir, category) {
|
|
5841
|
+
return cycles.map((cycle) => {
|
|
5842
|
+
const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
|
|
5843
|
+
const firstFile = relativePosix(rootDir, cycle.cycle[0]);
|
|
5844
|
+
return {
|
|
5845
|
+
id: violationId(firstFile, category, cyclePath),
|
|
5846
|
+
file: firstFile,
|
|
5847
|
+
detail: `Circular dependency: ${cyclePath}`,
|
|
5848
|
+
severity: cycle.severity
|
|
5849
|
+
};
|
|
5850
|
+
});
|
|
5851
|
+
}
|
|
5742
5852
|
var CircularDepsCollector = class {
|
|
5743
5853
|
category = "circular-deps";
|
|
5744
5854
|
getRules(_config, _rootDir) {
|
|
@@ -5754,21 +5864,7 @@ var CircularDepsCollector = class {
|
|
|
5754
5864
|
}
|
|
5755
5865
|
async collect(_config, rootDir) {
|
|
5756
5866
|
const files = await findFiles("**/*.ts", rootDir);
|
|
5757
|
-
const
|
|
5758
|
-
name: "typescript",
|
|
5759
|
-
extensions: [".ts", ".tsx"],
|
|
5760
|
-
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
|
|
5761
|
-
extractImports: () => ({
|
|
5762
|
-
ok: false,
|
|
5763
|
-
error: { code: "EXTRACT_ERROR", message: "not needed" }
|
|
5764
|
-
}),
|
|
5765
|
-
extractExports: () => ({
|
|
5766
|
-
ok: false,
|
|
5767
|
-
error: { code: "EXTRACT_ERROR", message: "not needed" }
|
|
5768
|
-
}),
|
|
5769
|
-
health: async () => ({ ok: true, value: { available: true } })
|
|
5770
|
-
};
|
|
5771
|
-
const graphResult = await buildDependencyGraph(files, stubParser);
|
|
5867
|
+
const graphResult = await buildDependencyGraph(files, makeStubParser());
|
|
5772
5868
|
if (!graphResult.ok) {
|
|
5773
5869
|
return [
|
|
5774
5870
|
{
|
|
@@ -5793,16 +5889,7 @@ var CircularDepsCollector = class {
|
|
|
5793
5889
|
];
|
|
5794
5890
|
}
|
|
5795
5891
|
const { cycles, largestCycle } = result.value;
|
|
5796
|
-
const violations = cycles.
|
|
5797
|
-
const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
|
|
5798
|
-
const firstFile = relativePosix(rootDir, cycle.cycle[0]);
|
|
5799
|
-
return {
|
|
5800
|
-
id: violationId(firstFile, this.category, cyclePath),
|
|
5801
|
-
file: firstFile,
|
|
5802
|
-
detail: `Circular dependency: ${cyclePath}`,
|
|
5803
|
-
severity: cycle.severity
|
|
5804
|
-
};
|
|
5805
|
-
});
|
|
5892
|
+
const violations = mapCycleViolations(cycles, rootDir, this.category);
|
|
5806
5893
|
return [
|
|
5807
5894
|
{
|
|
5808
5895
|
category: this.category,
|
|
@@ -5816,6 +5903,30 @@ var CircularDepsCollector = class {
|
|
|
5816
5903
|
};
|
|
5817
5904
|
|
|
5818
5905
|
// src/architecture/collectors/layer-violations.ts
|
|
5906
|
+
function makeLayerStubParser() {
|
|
5907
|
+
return {
|
|
5908
|
+
name: "typescript",
|
|
5909
|
+
extensions: [".ts", ".tsx"],
|
|
5910
|
+
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
5911
|
+
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
5912
|
+
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
5913
|
+
health: async () => ({ ok: true, value: { available: true } })
|
|
5914
|
+
};
|
|
5915
|
+
}
|
|
5916
|
+
function mapLayerViolations(layerViolations, rootDir, category) {
|
|
5917
|
+
return layerViolations.map((v) => {
|
|
5918
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
5919
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
5920
|
+
const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
|
|
5921
|
+
return {
|
|
5922
|
+
id: violationId(relFile, category ?? "", detail),
|
|
5923
|
+
file: relFile,
|
|
5924
|
+
category,
|
|
5925
|
+
detail,
|
|
5926
|
+
severity: "error"
|
|
5927
|
+
};
|
|
5928
|
+
});
|
|
5929
|
+
}
|
|
5819
5930
|
var LayerViolationCollector = class {
|
|
5820
5931
|
category = "layer-violations";
|
|
5821
5932
|
getRules(_config, _rootDir) {
|
|
@@ -5830,18 +5941,10 @@ var LayerViolationCollector = class {
|
|
|
5830
5941
|
];
|
|
5831
5942
|
}
|
|
5832
5943
|
async collect(_config, rootDir) {
|
|
5833
|
-
const stubParser = {
|
|
5834
|
-
name: "typescript",
|
|
5835
|
-
extensions: [".ts", ".tsx"],
|
|
5836
|
-
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
5837
|
-
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
5838
|
-
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
5839
|
-
health: async () => ({ ok: true, value: { available: true } })
|
|
5840
|
-
};
|
|
5841
5944
|
const result = await validateDependencies({
|
|
5842
5945
|
layers: [],
|
|
5843
5946
|
rootDir,
|
|
5844
|
-
parser:
|
|
5947
|
+
parser: makeLayerStubParser(),
|
|
5845
5948
|
fallbackBehavior: "skip"
|
|
5846
5949
|
});
|
|
5847
5950
|
if (!result.ok) {
|
|
@@ -5855,33 +5958,52 @@ var LayerViolationCollector = class {
|
|
|
5855
5958
|
}
|
|
5856
5959
|
];
|
|
5857
5960
|
}
|
|
5858
|
-
const
|
|
5859
|
-
(v) => v.reason === "WRONG_LAYER"
|
|
5961
|
+
const violations = mapLayerViolations(
|
|
5962
|
+
result.value.violations.filter((v) => v.reason === "WRONG_LAYER"),
|
|
5963
|
+
rootDir,
|
|
5964
|
+
this.category
|
|
5860
5965
|
);
|
|
5861
|
-
|
|
5862
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
5863
|
-
const relImport = relativePosix(rootDir, v.imports);
|
|
5864
|
-
const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
|
|
5865
|
-
return {
|
|
5866
|
-
id: violationId(relFile, this.category, detail),
|
|
5867
|
-
file: relFile,
|
|
5868
|
-
category: this.category,
|
|
5869
|
-
detail,
|
|
5870
|
-
severity: "error"
|
|
5871
|
-
};
|
|
5872
|
-
});
|
|
5873
|
-
return [
|
|
5874
|
-
{
|
|
5875
|
-
category: this.category,
|
|
5876
|
-
scope: "project",
|
|
5877
|
-
value: violations.length,
|
|
5878
|
-
violations
|
|
5879
|
-
}
|
|
5880
|
-
];
|
|
5966
|
+
return [{ category: this.category, scope: "project", value: violations.length, violations }];
|
|
5881
5967
|
}
|
|
5882
5968
|
};
|
|
5883
5969
|
|
|
5884
5970
|
// src/architecture/collectors/complexity.ts
|
|
5971
|
+
function buildSnapshot2(files, rootDir) {
|
|
5972
|
+
return {
|
|
5973
|
+
files: files.map((f) => ({
|
|
5974
|
+
path: f,
|
|
5975
|
+
ast: { type: "Program", body: null, language: "typescript" },
|
|
5976
|
+
imports: [],
|
|
5977
|
+
exports: [],
|
|
5978
|
+
internalSymbols: [],
|
|
5979
|
+
jsDocComments: []
|
|
5980
|
+
})),
|
|
5981
|
+
dependencyGraph: { nodes: [], edges: [] },
|
|
5982
|
+
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
5983
|
+
docs: [],
|
|
5984
|
+
codeReferences: [],
|
|
5985
|
+
entryPoints: [],
|
|
5986
|
+
rootDir,
|
|
5987
|
+
config: { rootDir, analyze: {} },
|
|
5988
|
+
buildTime: 0
|
|
5989
|
+
};
|
|
5990
|
+
}
|
|
5991
|
+
function resolveMaxComplexity(config) {
|
|
5992
|
+
const threshold = config.thresholds.complexity;
|
|
5993
|
+
return typeof threshold === "number" ? threshold : threshold?.max ?? 15;
|
|
5994
|
+
}
|
|
5995
|
+
function mapComplexityViolations(complexityViolations, rootDir, category) {
|
|
5996
|
+
return complexityViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
|
|
5997
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
5998
|
+
return {
|
|
5999
|
+
id: violationId(relFile, category ?? "", `${v.metric}:${v.function}`),
|
|
6000
|
+
file: relFile,
|
|
6001
|
+
category,
|
|
6002
|
+
detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
|
|
6003
|
+
severity: v.severity
|
|
6004
|
+
};
|
|
6005
|
+
});
|
|
6006
|
+
}
|
|
5885
6007
|
var ComplexityCollector = class {
|
|
5886
6008
|
category = "complexity";
|
|
5887
6009
|
getRules(_config, _rootDir) {
|
|
@@ -5897,32 +6019,11 @@ var ComplexityCollector = class {
|
|
|
5897
6019
|
}
|
|
5898
6020
|
async collect(_config, rootDir) {
|
|
5899
6021
|
const files = await findFiles("**/*.ts", rootDir);
|
|
5900
|
-
const snapshot =
|
|
5901
|
-
|
|
5902
|
-
path: f,
|
|
5903
|
-
ast: { type: "Program", body: null, language: "typescript" },
|
|
5904
|
-
imports: [],
|
|
5905
|
-
exports: [],
|
|
5906
|
-
internalSymbols: [],
|
|
5907
|
-
jsDocComments: []
|
|
5908
|
-
})),
|
|
5909
|
-
dependencyGraph: { nodes: [], edges: [] },
|
|
5910
|
-
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
5911
|
-
docs: [],
|
|
5912
|
-
codeReferences: [],
|
|
5913
|
-
entryPoints: [],
|
|
5914
|
-
rootDir,
|
|
5915
|
-
config: { rootDir, analyze: {} },
|
|
5916
|
-
buildTime: 0
|
|
5917
|
-
};
|
|
5918
|
-
const complexityThreshold = _config.thresholds.complexity;
|
|
5919
|
-
const maxComplexity = typeof complexityThreshold === "number" ? complexityThreshold : complexityThreshold?.max ?? 15;
|
|
6022
|
+
const snapshot = buildSnapshot2(files, rootDir);
|
|
6023
|
+
const maxComplexity = resolveMaxComplexity(_config);
|
|
5920
6024
|
const complexityConfig = {
|
|
5921
6025
|
thresholds: {
|
|
5922
|
-
cyclomaticComplexity: {
|
|
5923
|
-
error: maxComplexity,
|
|
5924
|
-
warn: Math.floor(maxComplexity * 0.7)
|
|
5925
|
-
}
|
|
6026
|
+
cyclomaticComplexity: { error: maxComplexity, warn: Math.floor(maxComplexity * 0.7) }
|
|
5926
6027
|
}
|
|
5927
6028
|
};
|
|
5928
6029
|
const result = await detectComplexityViolations(snapshot, complexityConfig);
|
|
@@ -5938,20 +6039,7 @@ var ComplexityCollector = class {
|
|
|
5938
6039
|
];
|
|
5939
6040
|
}
|
|
5940
6041
|
const { violations: complexityViolations, stats } = result.value;
|
|
5941
|
-
const
|
|
5942
|
-
(v) => v.severity === "error" || v.severity === "warning"
|
|
5943
|
-
);
|
|
5944
|
-
const violations = filtered.map((v) => {
|
|
5945
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
5946
|
-
const idDetail = `${v.metric}:${v.function}`;
|
|
5947
|
-
return {
|
|
5948
|
-
id: violationId(relFile, this.category, idDetail),
|
|
5949
|
-
file: relFile,
|
|
5950
|
-
category: this.category,
|
|
5951
|
-
detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
|
|
5952
|
-
severity: v.severity
|
|
5953
|
-
};
|
|
5954
|
-
});
|
|
6042
|
+
const violations = mapComplexityViolations(complexityViolations, rootDir, this.category);
|
|
5955
6043
|
return [
|
|
5956
6044
|
{
|
|
5957
6045
|
category: this.category,
|
|
@@ -5968,6 +6056,38 @@ var ComplexityCollector = class {
|
|
|
5968
6056
|
};
|
|
5969
6057
|
|
|
5970
6058
|
// src/architecture/collectors/coupling.ts
|
|
6059
|
+
function buildCouplingSnapshot(files, rootDir) {
|
|
6060
|
+
return {
|
|
6061
|
+
files: files.map((f) => ({
|
|
6062
|
+
path: f,
|
|
6063
|
+
ast: { type: "Program", body: null, language: "typescript" },
|
|
6064
|
+
imports: [],
|
|
6065
|
+
exports: [],
|
|
6066
|
+
internalSymbols: [],
|
|
6067
|
+
jsDocComments: []
|
|
6068
|
+
})),
|
|
6069
|
+
dependencyGraph: { nodes: [], edges: [] },
|
|
6070
|
+
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
6071
|
+
docs: [],
|
|
6072
|
+
codeReferences: [],
|
|
6073
|
+
entryPoints: [],
|
|
6074
|
+
rootDir,
|
|
6075
|
+
config: { rootDir, analyze: {} },
|
|
6076
|
+
buildTime: 0
|
|
6077
|
+
};
|
|
6078
|
+
}
|
|
6079
|
+
function mapCouplingViolations(couplingViolations, rootDir, category) {
|
|
6080
|
+
return couplingViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
|
|
6081
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
6082
|
+
return {
|
|
6083
|
+
id: violationId(relFile, category ?? "", v.metric),
|
|
6084
|
+
file: relFile,
|
|
6085
|
+
category,
|
|
6086
|
+
detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
|
|
6087
|
+
severity: v.severity
|
|
6088
|
+
};
|
|
6089
|
+
});
|
|
6090
|
+
}
|
|
5971
6091
|
var CouplingCollector = class {
|
|
5972
6092
|
category = "coupling";
|
|
5973
6093
|
getRules(_config, _rootDir) {
|
|
@@ -5983,24 +6103,7 @@ var CouplingCollector = class {
|
|
|
5983
6103
|
}
|
|
5984
6104
|
async collect(_config, rootDir) {
|
|
5985
6105
|
const files = await findFiles("**/*.ts", rootDir);
|
|
5986
|
-
const snapshot =
|
|
5987
|
-
files: files.map((f) => ({
|
|
5988
|
-
path: f,
|
|
5989
|
-
ast: { type: "Program", body: null, language: "typescript" },
|
|
5990
|
-
imports: [],
|
|
5991
|
-
exports: [],
|
|
5992
|
-
internalSymbols: [],
|
|
5993
|
-
jsDocComments: []
|
|
5994
|
-
})),
|
|
5995
|
-
dependencyGraph: { nodes: [], edges: [] },
|
|
5996
|
-
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
5997
|
-
docs: [],
|
|
5998
|
-
codeReferences: [],
|
|
5999
|
-
entryPoints: [],
|
|
6000
|
-
rootDir,
|
|
6001
|
-
config: { rootDir, analyze: {} },
|
|
6002
|
-
buildTime: 0
|
|
6003
|
-
};
|
|
6106
|
+
const snapshot = buildCouplingSnapshot(files, rootDir);
|
|
6004
6107
|
const result = await detectCouplingViolations(snapshot);
|
|
6005
6108
|
if (!result.ok) {
|
|
6006
6109
|
return [
|
|
@@ -6014,20 +6117,7 @@ var CouplingCollector = class {
|
|
|
6014
6117
|
];
|
|
6015
6118
|
}
|
|
6016
6119
|
const { violations: couplingViolations, stats } = result.value;
|
|
6017
|
-
const
|
|
6018
|
-
(v) => v.severity === "error" || v.severity === "warning"
|
|
6019
|
-
);
|
|
6020
|
-
const violations = filtered.map((v) => {
|
|
6021
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
6022
|
-
const idDetail = `${v.metric}`;
|
|
6023
|
-
return {
|
|
6024
|
-
id: violationId(relFile, this.category, idDetail),
|
|
6025
|
-
file: relFile,
|
|
6026
|
-
category: this.category,
|
|
6027
|
-
detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
|
|
6028
|
-
severity: v.severity
|
|
6029
|
-
};
|
|
6030
|
-
});
|
|
6120
|
+
const violations = mapCouplingViolations(couplingViolations, rootDir, this.category);
|
|
6031
6121
|
return [
|
|
6032
6122
|
{
|
|
6033
6123
|
category: this.category,
|
|
@@ -6041,6 +6131,30 @@ var CouplingCollector = class {
|
|
|
6041
6131
|
};
|
|
6042
6132
|
|
|
6043
6133
|
// src/architecture/collectors/forbidden-imports.ts
|
|
6134
|
+
function makeForbiddenStubParser() {
|
|
6135
|
+
return {
|
|
6136
|
+
name: "typescript",
|
|
6137
|
+
extensions: [".ts", ".tsx"],
|
|
6138
|
+
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
6139
|
+
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
6140
|
+
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
6141
|
+
health: async () => ({ ok: true, value: { available: true } })
|
|
6142
|
+
};
|
|
6143
|
+
}
|
|
6144
|
+
function mapForbiddenImportViolations(forbidden, rootDir, category) {
|
|
6145
|
+
return forbidden.map((v) => {
|
|
6146
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
6147
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
6148
|
+
const detail = `forbidden import: ${relFile} -> ${relImport}`;
|
|
6149
|
+
return {
|
|
6150
|
+
id: violationId(relFile, category ?? "", detail),
|
|
6151
|
+
file: relFile,
|
|
6152
|
+
category,
|
|
6153
|
+
detail,
|
|
6154
|
+
severity: "error"
|
|
6155
|
+
};
|
|
6156
|
+
});
|
|
6157
|
+
}
|
|
6044
6158
|
var ForbiddenImportCollector = class {
|
|
6045
6159
|
category = "forbidden-imports";
|
|
6046
6160
|
getRules(_config, _rootDir) {
|
|
@@ -6055,18 +6169,10 @@ var ForbiddenImportCollector = class {
|
|
|
6055
6169
|
];
|
|
6056
6170
|
}
|
|
6057
6171
|
async collect(_config, rootDir) {
|
|
6058
|
-
const stubParser = {
|
|
6059
|
-
name: "typescript",
|
|
6060
|
-
extensions: [".ts", ".tsx"],
|
|
6061
|
-
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
6062
|
-
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
6063
|
-
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
6064
|
-
health: async () => ({ ok: true, value: { available: true } })
|
|
6065
|
-
};
|
|
6066
6172
|
const result = await validateDependencies({
|
|
6067
6173
|
layers: [],
|
|
6068
6174
|
rootDir,
|
|
6069
|
-
parser:
|
|
6175
|
+
parser: makeForbiddenStubParser(),
|
|
6070
6176
|
fallbackBehavior: "skip"
|
|
6071
6177
|
});
|
|
6072
6178
|
if (!result.ok) {
|
|
@@ -6080,91 +6186,94 @@ var ForbiddenImportCollector = class {
|
|
|
6080
6186
|
}
|
|
6081
6187
|
];
|
|
6082
6188
|
}
|
|
6083
|
-
const
|
|
6084
|
-
(v) => v.reason === "FORBIDDEN_IMPORT"
|
|
6189
|
+
const violations = mapForbiddenImportViolations(
|
|
6190
|
+
result.value.violations.filter((v) => v.reason === "FORBIDDEN_IMPORT"),
|
|
6191
|
+
rootDir,
|
|
6192
|
+
this.category
|
|
6085
6193
|
);
|
|
6086
|
-
|
|
6087
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
6088
|
-
const relImport = relativePosix(rootDir, v.imports);
|
|
6089
|
-
const detail = `forbidden import: ${relFile} -> ${relImport}`;
|
|
6090
|
-
return {
|
|
6091
|
-
id: violationId(relFile, this.category, detail),
|
|
6092
|
-
file: relFile,
|
|
6093
|
-
category: this.category,
|
|
6094
|
-
detail,
|
|
6095
|
-
severity: "error"
|
|
6096
|
-
};
|
|
6097
|
-
});
|
|
6098
|
-
return [
|
|
6099
|
-
{
|
|
6100
|
-
category: this.category,
|
|
6101
|
-
scope: "project",
|
|
6102
|
-
value: violations.length,
|
|
6103
|
-
violations
|
|
6104
|
-
}
|
|
6105
|
-
];
|
|
6194
|
+
return [{ category: this.category, scope: "project", value: violations.length, violations }];
|
|
6106
6195
|
}
|
|
6107
6196
|
};
|
|
6108
6197
|
|
|
6109
6198
|
// src/architecture/collectors/module-size.ts
|
|
6110
6199
|
var import_promises2 = require("fs/promises");
|
|
6111
6200
|
var import_node_path4 = require("path");
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6201
|
+
function isSkippedEntry(name) {
|
|
6202
|
+
return name.startsWith(".") || name === "node_modules" || name === "dist";
|
|
6203
|
+
}
|
|
6204
|
+
function isTsSourceFile(name) {
|
|
6205
|
+
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
|
|
6206
|
+
if (name.endsWith(".test.ts") || name.endsWith(".test.tsx") || name.endsWith(".spec.ts"))
|
|
6207
|
+
return false;
|
|
6208
|
+
return true;
|
|
6209
|
+
}
|
|
6210
|
+
async function countLoc(filePath) {
|
|
6211
|
+
try {
|
|
6212
|
+
const content = await (0, import_promises2.readFile)(filePath, "utf-8");
|
|
6213
|
+
return content.split("\n").filter((line) => line.trim().length > 0).length;
|
|
6214
|
+
} catch {
|
|
6215
|
+
return 0;
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
async function buildModuleStats(rootDir, dir, tsFiles) {
|
|
6219
|
+
let totalLoc = 0;
|
|
6220
|
+
for (const f of tsFiles) {
|
|
6221
|
+
totalLoc += await countLoc(f);
|
|
6222
|
+
}
|
|
6223
|
+
return {
|
|
6224
|
+
modulePath: relativePosix(rootDir, dir),
|
|
6225
|
+
fileCount: tsFiles.length,
|
|
6226
|
+
totalLoc,
|
|
6227
|
+
files: tsFiles.map((f) => relativePosix(rootDir, f))
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
async function scanDir(rootDir, dir, modules) {
|
|
6231
|
+
let entries;
|
|
6232
|
+
try {
|
|
6233
|
+
entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
|
|
6234
|
+
} catch {
|
|
6235
|
+
return;
|
|
6236
|
+
}
|
|
6237
|
+
const tsFiles = [];
|
|
6238
|
+
const subdirs = [];
|
|
6239
|
+
for (const entry of entries) {
|
|
6240
|
+
if (isSkippedEntry(entry.name)) continue;
|
|
6241
|
+
const fullPath = (0, import_node_path4.join)(dir, entry.name);
|
|
6242
|
+
if (entry.isDirectory()) {
|
|
6243
|
+
subdirs.push(fullPath);
|
|
6244
|
+
continue;
|
|
6149
6245
|
}
|
|
6150
|
-
|
|
6151
|
-
|
|
6246
|
+
if (entry.isFile() && isTsSourceFile(entry.name)) {
|
|
6247
|
+
tsFiles.push(fullPath);
|
|
6152
6248
|
}
|
|
6153
6249
|
}
|
|
6154
|
-
|
|
6250
|
+
if (tsFiles.length > 0) {
|
|
6251
|
+
modules.push(await buildModuleStats(rootDir, dir, tsFiles));
|
|
6252
|
+
}
|
|
6253
|
+
for (const sub of subdirs) {
|
|
6254
|
+
await scanDir(rootDir, sub, modules);
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
6257
|
+
async function discoverModules(rootDir) {
|
|
6258
|
+
const modules = [];
|
|
6259
|
+
await scanDir(rootDir, rootDir, modules);
|
|
6155
6260
|
return modules;
|
|
6156
6261
|
}
|
|
6262
|
+
function extractThresholds(config) {
|
|
6263
|
+
const thresholds = config.thresholds["module-size"];
|
|
6264
|
+
let maxLoc = Infinity;
|
|
6265
|
+
let maxFiles = Infinity;
|
|
6266
|
+
if (typeof thresholds === "object" && thresholds !== null) {
|
|
6267
|
+
const t = thresholds;
|
|
6268
|
+
if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
|
|
6269
|
+
if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
|
|
6270
|
+
}
|
|
6271
|
+
return { maxLoc, maxFiles };
|
|
6272
|
+
}
|
|
6157
6273
|
var ModuleSizeCollector = class {
|
|
6158
6274
|
category = "module-size";
|
|
6159
6275
|
getRules(config, _rootDir) {
|
|
6160
|
-
const
|
|
6161
|
-
let maxLoc = Infinity;
|
|
6162
|
-
let maxFiles = Infinity;
|
|
6163
|
-
if (typeof thresholds === "object" && thresholds !== null) {
|
|
6164
|
-
const t = thresholds;
|
|
6165
|
-
if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
|
|
6166
|
-
if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
|
|
6167
|
-
}
|
|
6276
|
+
const { maxLoc, maxFiles } = extractThresholds(config);
|
|
6168
6277
|
const rules = [];
|
|
6169
6278
|
if (maxLoc < Infinity) {
|
|
6170
6279
|
const desc = `Module LOC must not exceed ${maxLoc}`;
|
|
@@ -6197,14 +6306,7 @@ var ModuleSizeCollector = class {
|
|
|
6197
6306
|
}
|
|
6198
6307
|
async collect(config, rootDir) {
|
|
6199
6308
|
const modules = await discoverModules(rootDir);
|
|
6200
|
-
const
|
|
6201
|
-
let maxLoc = Infinity;
|
|
6202
|
-
let maxFiles = Infinity;
|
|
6203
|
-
if (typeof thresholds === "object" && thresholds !== null) {
|
|
6204
|
-
const t = thresholds;
|
|
6205
|
-
if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
|
|
6206
|
-
if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
|
|
6207
|
-
}
|
|
6309
|
+
const { maxLoc, maxFiles } = extractThresholds(config);
|
|
6208
6310
|
return modules.map((mod) => {
|
|
6209
6311
|
const violations = [];
|
|
6210
6312
|
if (mod.totalLoc > maxLoc) {
|
|
@@ -6254,27 +6356,33 @@ function extractImportSources(content, filePath) {
|
|
|
6254
6356
|
}
|
|
6255
6357
|
return sources;
|
|
6256
6358
|
}
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6359
|
+
function isSkippedEntry2(name) {
|
|
6360
|
+
return name.startsWith(".") || name === "node_modules" || name === "dist";
|
|
6361
|
+
}
|
|
6362
|
+
function isTsSourceFile2(name) {
|
|
6363
|
+
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
|
|
6364
|
+
return !name.endsWith(".test.ts") && !name.endsWith(".test.tsx") && !name.endsWith(".spec.ts");
|
|
6365
|
+
}
|
|
6366
|
+
async function scanDir2(d, results) {
|
|
6367
|
+
let entries;
|
|
6368
|
+
try {
|
|
6369
|
+
entries = await (0, import_promises3.readdir)(d, { withFileTypes: true });
|
|
6370
|
+
} catch {
|
|
6371
|
+
return;
|
|
6372
|
+
}
|
|
6373
|
+
for (const entry of entries) {
|
|
6374
|
+
if (isSkippedEntry2(entry.name)) continue;
|
|
6375
|
+
const fullPath = (0, import_node_path5.join)(d, entry.name);
|
|
6376
|
+
if (entry.isDirectory()) {
|
|
6377
|
+
await scanDir2(fullPath, results);
|
|
6378
|
+
} else if (entry.isFile() && isTsSourceFile2(entry.name)) {
|
|
6379
|
+
results.push(fullPath);
|
|
6275
6380
|
}
|
|
6276
6381
|
}
|
|
6277
|
-
|
|
6382
|
+
}
|
|
6383
|
+
async function collectTsFiles(dir) {
|
|
6384
|
+
const results = [];
|
|
6385
|
+
await scanDir2(dir, results);
|
|
6278
6386
|
return results;
|
|
6279
6387
|
}
|
|
6280
6388
|
function computeLongestChain(file, graph, visited, memo) {
|
|
@@ -6305,34 +6413,42 @@ var DepDepthCollector = class {
|
|
|
6305
6413
|
}
|
|
6306
6414
|
];
|
|
6307
6415
|
}
|
|
6308
|
-
async
|
|
6309
|
-
const allFiles = await collectTsFiles(rootDir);
|
|
6416
|
+
async buildImportGraph(allFiles) {
|
|
6310
6417
|
const graph = /* @__PURE__ */ new Map();
|
|
6311
6418
|
const fileSet = new Set(allFiles);
|
|
6312
6419
|
for (const file of allFiles) {
|
|
6313
6420
|
try {
|
|
6314
6421
|
const content = await (0, import_promises3.readFile)(file, "utf-8");
|
|
6315
|
-
|
|
6316
|
-
|
|
6422
|
+
graph.set(
|
|
6423
|
+
file,
|
|
6424
|
+
extractImportSources(content, file).filter((imp) => fileSet.has(imp))
|
|
6425
|
+
);
|
|
6317
6426
|
} catch {
|
|
6318
6427
|
graph.set(file, []);
|
|
6319
6428
|
}
|
|
6320
6429
|
}
|
|
6430
|
+
return graph;
|
|
6431
|
+
}
|
|
6432
|
+
buildModuleMap(allFiles, rootDir) {
|
|
6321
6433
|
const moduleMap = /* @__PURE__ */ new Map();
|
|
6322
6434
|
for (const file of allFiles) {
|
|
6323
6435
|
const relDir = relativePosix(rootDir, (0, import_node_path5.dirname)(file));
|
|
6324
6436
|
if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
|
|
6325
6437
|
moduleMap.get(relDir).push(file);
|
|
6326
6438
|
}
|
|
6439
|
+
return moduleMap;
|
|
6440
|
+
}
|
|
6441
|
+
async collect(config, rootDir) {
|
|
6442
|
+
const allFiles = await collectTsFiles(rootDir);
|
|
6443
|
+
const graph = await this.buildImportGraph(allFiles);
|
|
6444
|
+
const moduleMap = this.buildModuleMap(allFiles, rootDir);
|
|
6327
6445
|
const memo = /* @__PURE__ */ new Map();
|
|
6328
6446
|
const threshold = typeof config.thresholds["dependency-depth"] === "number" ? config.thresholds["dependency-depth"] : Infinity;
|
|
6329
6447
|
const results = [];
|
|
6330
6448
|
for (const [modulePath, files] of moduleMap) {
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
if (depth > longestChain) longestChain = depth;
|
|
6335
|
-
}
|
|
6449
|
+
const longestChain = files.reduce((max, file) => {
|
|
6450
|
+
return Math.max(max, computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo));
|
|
6451
|
+
}, 0);
|
|
6336
6452
|
const violations = [];
|
|
6337
6453
|
if (longestChain > threshold) {
|
|
6338
6454
|
violations.push({
|
|
@@ -6429,10 +6545,26 @@ function hasMatchingViolation(rule, violationsByCategory) {
|
|
|
6429
6545
|
}
|
|
6430
6546
|
|
|
6431
6547
|
// src/architecture/detect-stale.ts
|
|
6548
|
+
function evaluateStaleNode(node, now, cutoff) {
|
|
6549
|
+
const lastViolatedAt = node.lastViolatedAt ?? null;
|
|
6550
|
+
const createdAt = node.createdAt;
|
|
6551
|
+
const comparisonTimestamp = lastViolatedAt ?? createdAt;
|
|
6552
|
+
if (!comparisonTimestamp) return null;
|
|
6553
|
+
const timestampMs = new Date(comparisonTimestamp).getTime();
|
|
6554
|
+
if (timestampMs >= cutoff) return null;
|
|
6555
|
+
const daysSince = Math.floor((now - timestampMs) / (24 * 60 * 60 * 1e3));
|
|
6556
|
+
return {
|
|
6557
|
+
id: node.id,
|
|
6558
|
+
category: node.category,
|
|
6559
|
+
description: node.name ?? "",
|
|
6560
|
+
scope: node.scope ?? "project",
|
|
6561
|
+
lastViolatedAt,
|
|
6562
|
+
daysSinceLastViolation: daysSince
|
|
6563
|
+
};
|
|
6564
|
+
}
|
|
6432
6565
|
function detectStaleConstraints(store, windowDays = 30, category) {
|
|
6433
6566
|
const now = Date.now();
|
|
6434
|
-
const
|
|
6435
|
-
const cutoff = now - windowMs;
|
|
6567
|
+
const cutoff = now - windowDays * 24 * 60 * 60 * 1e3;
|
|
6436
6568
|
let constraints = store.findNodes({ type: "constraint" });
|
|
6437
6569
|
if (category) {
|
|
6438
6570
|
constraints = constraints.filter((n) => n.category === category);
|
|
@@ -6440,22 +6572,8 @@ function detectStaleConstraints(store, windowDays = 30, category) {
|
|
|
6440
6572
|
const totalConstraints = constraints.length;
|
|
6441
6573
|
const staleConstraints = [];
|
|
6442
6574
|
for (const node of constraints) {
|
|
6443
|
-
const
|
|
6444
|
-
|
|
6445
|
-
const comparisonTimestamp = lastViolatedAt ?? createdAt;
|
|
6446
|
-
if (!comparisonTimestamp) continue;
|
|
6447
|
-
const timestampMs = new Date(comparisonTimestamp).getTime();
|
|
6448
|
-
if (timestampMs < cutoff) {
|
|
6449
|
-
const daysSince = Math.floor((now - timestampMs) / (24 * 60 * 60 * 1e3));
|
|
6450
|
-
staleConstraints.push({
|
|
6451
|
-
id: node.id,
|
|
6452
|
-
category: node.category,
|
|
6453
|
-
description: node.name ?? "",
|
|
6454
|
-
scope: node.scope ?? "project",
|
|
6455
|
-
lastViolatedAt,
|
|
6456
|
-
daysSinceLastViolation: daysSince
|
|
6457
|
-
});
|
|
6458
|
-
}
|
|
6575
|
+
const entry = evaluateStaleNode(node, now, cutoff);
|
|
6576
|
+
if (entry) staleConstraints.push(entry);
|
|
6459
6577
|
}
|
|
6460
6578
|
staleConstraints.sort((a, b) => b.daysSinceLastViolation - a.daysSinceLastViolation);
|
|
6461
6579
|
return { staleConstraints, totalConstraints, windowDays };
|
|
@@ -6581,44 +6699,57 @@ function collectOrphanedBaselineViolations(baseline, visitedCategories) {
|
|
|
6581
6699
|
}
|
|
6582
6700
|
return resolved;
|
|
6583
6701
|
}
|
|
6702
|
+
function diffCategory(category, agg, baselineCategory, acc) {
|
|
6703
|
+
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
6704
|
+
const baselineValue = baselineCategory?.value ?? 0;
|
|
6705
|
+
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
6706
|
+
acc.newViolations.push(...classified.newViolations);
|
|
6707
|
+
acc.preExisting.push(...classified.preExisting);
|
|
6708
|
+
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
6709
|
+
acc.resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
6710
|
+
if (baselineCategory && agg.value > baselineValue) {
|
|
6711
|
+
acc.regressions.push({
|
|
6712
|
+
category,
|
|
6713
|
+
baselineValue,
|
|
6714
|
+
currentValue: agg.value,
|
|
6715
|
+
delta: agg.value - baselineValue
|
|
6716
|
+
});
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6584
6719
|
function diff(current, baseline) {
|
|
6585
6720
|
const aggregated = aggregateByCategory(current);
|
|
6586
|
-
const
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6721
|
+
const acc = {
|
|
6722
|
+
newViolations: [],
|
|
6723
|
+
resolvedViolations: [],
|
|
6724
|
+
preExisting: [],
|
|
6725
|
+
regressions: []
|
|
6726
|
+
};
|
|
6590
6727
|
const visitedCategories = /* @__PURE__ */ new Set();
|
|
6591
6728
|
for (const [category, agg] of aggregated) {
|
|
6592
6729
|
visitedCategories.add(category);
|
|
6593
|
-
|
|
6594
|
-
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
6595
|
-
const baselineValue = baselineCategory?.value ?? 0;
|
|
6596
|
-
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
6597
|
-
newViolations.push(...classified.newViolations);
|
|
6598
|
-
preExisting.push(...classified.preExisting);
|
|
6599
|
-
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
6600
|
-
resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
6601
|
-
if (baselineCategory && agg.value > baselineValue) {
|
|
6602
|
-
regressions.push({
|
|
6603
|
-
category,
|
|
6604
|
-
baselineValue,
|
|
6605
|
-
currentValue: agg.value,
|
|
6606
|
-
delta: agg.value - baselineValue
|
|
6607
|
-
});
|
|
6608
|
-
}
|
|
6730
|
+
diffCategory(category, agg, baseline.metrics[category], acc);
|
|
6609
6731
|
}
|
|
6610
|
-
resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
6732
|
+
acc.resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
6611
6733
|
return {
|
|
6612
|
-
passed: newViolations.length === 0 && regressions.length === 0,
|
|
6613
|
-
newViolations,
|
|
6614
|
-
resolvedViolations,
|
|
6615
|
-
preExisting,
|
|
6616
|
-
regressions
|
|
6734
|
+
passed: acc.newViolations.length === 0 && acc.regressions.length === 0,
|
|
6735
|
+
newViolations: acc.newViolations,
|
|
6736
|
+
resolvedViolations: acc.resolvedViolations,
|
|
6737
|
+
preExisting: acc.preExisting,
|
|
6738
|
+
regressions: acc.regressions
|
|
6617
6739
|
};
|
|
6618
6740
|
}
|
|
6619
6741
|
|
|
6620
6742
|
// src/architecture/config.ts
|
|
6621
|
-
function
|
|
6743
|
+
function mergeThresholdCategory(projectValue2, moduleValue) {
|
|
6744
|
+
if (projectValue2 !== void 0 && typeof projectValue2 === "object" && !Array.isArray(projectValue2) && typeof moduleValue === "object" && !Array.isArray(moduleValue)) {
|
|
6745
|
+
return {
|
|
6746
|
+
...projectValue2,
|
|
6747
|
+
...moduleValue
|
|
6748
|
+
};
|
|
6749
|
+
}
|
|
6750
|
+
return moduleValue;
|
|
6751
|
+
}
|
|
6752
|
+
function resolveThresholds3(scope, config) {
|
|
6622
6753
|
const projectThresholds = {};
|
|
6623
6754
|
for (const [key, val] of Object.entries(config.thresholds)) {
|
|
6624
6755
|
projectThresholds[key] = typeof val === "object" && val !== null && !Array.isArray(val) ? { ...val } : val;
|
|
@@ -6633,14 +6764,7 @@ function resolveThresholds2(scope, config) {
|
|
|
6633
6764
|
const merged = { ...projectThresholds };
|
|
6634
6765
|
for (const [category, moduleValue] of Object.entries(moduleOverrides)) {
|
|
6635
6766
|
const projectValue2 = projectThresholds[category];
|
|
6636
|
-
|
|
6637
|
-
merged[category] = {
|
|
6638
|
-
...projectValue2,
|
|
6639
|
-
...moduleValue
|
|
6640
|
-
};
|
|
6641
|
-
} else {
|
|
6642
|
-
merged[category] = moduleValue;
|
|
6643
|
-
}
|
|
6767
|
+
merged[category] = mergeThresholdCategory(projectValue2, moduleValue);
|
|
6644
6768
|
}
|
|
6645
6769
|
return merged;
|
|
6646
6770
|
}
|
|
@@ -7450,18 +7574,10 @@ var PredictionEngine = class {
|
|
|
7450
7574
|
*/
|
|
7451
7575
|
predict(options) {
|
|
7452
7576
|
const opts = this.resolveOptions(options);
|
|
7453
|
-
const
|
|
7454
|
-
const snapshots = timeline.snapshots;
|
|
7455
|
-
if (snapshots.length < 3) {
|
|
7456
|
-
throw new Error(
|
|
7457
|
-
`PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
|
|
7458
|
-
);
|
|
7459
|
-
}
|
|
7577
|
+
const snapshots = this.loadValidatedSnapshots();
|
|
7460
7578
|
const thresholds = this.resolveThresholds(opts);
|
|
7461
7579
|
const categoriesToProcess = opts.categories ?? [...ALL_CATEGORIES2];
|
|
7462
|
-
const firstDate =
|
|
7463
|
-
const lastSnapshot = snapshots[snapshots.length - 1];
|
|
7464
|
-
const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
|
|
7580
|
+
const { firstDate, lastSnapshot, currentT } = this.computeTimeOffsets(snapshots);
|
|
7465
7581
|
const baselines = this.computeBaselines(
|
|
7466
7582
|
categoriesToProcess,
|
|
7467
7583
|
thresholds,
|
|
@@ -7472,27 +7588,32 @@ var PredictionEngine = class {
|
|
|
7472
7588
|
);
|
|
7473
7589
|
const specImpacts = this.computeSpecImpacts(opts);
|
|
7474
7590
|
const categories = this.computeAdjustedForecasts(baselines, thresholds, specImpacts, currentT);
|
|
7475
|
-
const
|
|
7476
|
-
categories,
|
|
7477
|
-
opts.horizon
|
|
7478
|
-
);
|
|
7479
|
-
const stabilityForecast = this.computeStabilityForecast(
|
|
7480
|
-
categories,
|
|
7481
|
-
thresholds,
|
|
7482
|
-
snapshots
|
|
7483
|
-
);
|
|
7591
|
+
const adjustedCategories = categories;
|
|
7484
7592
|
return {
|
|
7485
7593
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7486
7594
|
snapshotsUsed: snapshots.length,
|
|
7487
|
-
timelineRange: {
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
stabilityForecast,
|
|
7492
|
-
categories,
|
|
7493
|
-
warnings
|
|
7595
|
+
timelineRange: { from: snapshots[0].capturedAt, to: lastSnapshot.capturedAt },
|
|
7596
|
+
stabilityForecast: this.computeStabilityForecast(adjustedCategories, thresholds, snapshots),
|
|
7597
|
+
categories: adjustedCategories,
|
|
7598
|
+
warnings: this.generateWarnings(adjustedCategories, opts.horizon)
|
|
7494
7599
|
};
|
|
7495
7600
|
}
|
|
7601
|
+
loadValidatedSnapshots() {
|
|
7602
|
+
const timeline = this.timelineManager.load();
|
|
7603
|
+
const snapshots = timeline.snapshots;
|
|
7604
|
+
if (snapshots.length < 3) {
|
|
7605
|
+
throw new Error(
|
|
7606
|
+
`PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
|
|
7607
|
+
);
|
|
7608
|
+
}
|
|
7609
|
+
return snapshots;
|
|
7610
|
+
}
|
|
7611
|
+
computeTimeOffsets(snapshots) {
|
|
7612
|
+
const firstDate = new Date(snapshots[0].capturedAt).getTime();
|
|
7613
|
+
const lastSnapshot = snapshots[snapshots.length - 1];
|
|
7614
|
+
const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
|
|
7615
|
+
return { firstDate, lastSnapshot, currentT };
|
|
7616
|
+
}
|
|
7496
7617
|
// --- Private helpers ---
|
|
7497
7618
|
resolveOptions(options) {
|
|
7498
7619
|
return {
|
|
@@ -7659,31 +7780,40 @@ var PredictionEngine = class {
|
|
|
7659
7780
|
for (const category of ALL_CATEGORIES2) {
|
|
7660
7781
|
const af = categories[category];
|
|
7661
7782
|
if (!af) continue;
|
|
7662
|
-
const
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
} else if (crossing <= horizon) {
|
|
7671
|
-
severity = "info";
|
|
7672
|
-
}
|
|
7673
|
-
if (severity) {
|
|
7674
|
-
const contributingNames = af.contributingFeatures.map((f) => f.name);
|
|
7675
|
-
warnings.push({
|
|
7676
|
-
severity,
|
|
7677
|
-
category,
|
|
7678
|
-
message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
|
|
7679
|
-
weeksUntil: crossing,
|
|
7680
|
-
confidence: forecast.confidence,
|
|
7681
|
-
contributingFeatures: contributingNames
|
|
7682
|
-
});
|
|
7683
|
-
}
|
|
7783
|
+
const warning = this.buildCategoryWarning(
|
|
7784
|
+
category,
|
|
7785
|
+
af,
|
|
7786
|
+
criticalWindow,
|
|
7787
|
+
warningWindow,
|
|
7788
|
+
horizon
|
|
7789
|
+
);
|
|
7790
|
+
if (warning) warnings.push(warning);
|
|
7684
7791
|
}
|
|
7685
7792
|
return warnings;
|
|
7686
7793
|
}
|
|
7794
|
+
buildCategoryWarning(category, af, criticalWindow, warningWindow, horizon) {
|
|
7795
|
+
const forecast = af.adjusted;
|
|
7796
|
+
const crossing = forecast.thresholdCrossingWeeks;
|
|
7797
|
+
if (crossing === null || crossing <= 0) return null;
|
|
7798
|
+
const isHighConfidence = forecast.confidence === "high" || forecast.confidence === "medium";
|
|
7799
|
+
let severity = null;
|
|
7800
|
+
if (crossing <= criticalWindow && isHighConfidence) {
|
|
7801
|
+
severity = "critical";
|
|
7802
|
+
} else if (crossing <= warningWindow && isHighConfidence) {
|
|
7803
|
+
severity = "warning";
|
|
7804
|
+
} else if (crossing <= horizon) {
|
|
7805
|
+
severity = "info";
|
|
7806
|
+
}
|
|
7807
|
+
if (!severity) return null;
|
|
7808
|
+
return {
|
|
7809
|
+
severity,
|
|
7810
|
+
category,
|
|
7811
|
+
message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
|
|
7812
|
+
weeksUntil: crossing,
|
|
7813
|
+
confidence: forecast.confidence,
|
|
7814
|
+
contributingFeatures: af.contributingFeatures.map((f) => f.name)
|
|
7815
|
+
};
|
|
7816
|
+
}
|
|
7687
7817
|
/**
|
|
7688
7818
|
* Compute composite stability forecast by projecting per-category values
|
|
7689
7819
|
* forward and computing stability scores at each horizon.
|
|
@@ -7752,14 +7882,9 @@ var PredictionEngine = class {
|
|
|
7752
7882
|
const raw = fs5.readFileSync(roadmapPath, "utf-8");
|
|
7753
7883
|
const parseResult = parseRoadmap(raw);
|
|
7754
7884
|
if (!parseResult.ok) return null;
|
|
7755
|
-
const features =
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
if (feature.status === "planned" || feature.status === "in-progress") {
|
|
7759
|
-
features.push({ name: feature.name, spec: feature.spec });
|
|
7760
|
-
}
|
|
7761
|
-
}
|
|
7762
|
-
}
|
|
7885
|
+
const features = parseResult.value.milestones.flatMap(
|
|
7886
|
+
(m) => m.features.filter((f) => f.status === "planned" || f.status === "in-progress").map((f) => ({ name: f.name, spec: f.spec }))
|
|
7887
|
+
);
|
|
7763
7888
|
if (features.length === 0) return null;
|
|
7764
7889
|
return this.estimator.estimateAll(features);
|
|
7765
7890
|
} catch {
|
|
@@ -8410,7 +8535,7 @@ async function saveState(projectPath, state, stream, session) {
|
|
|
8410
8535
|
// src/state/learnings-content.ts
|
|
8411
8536
|
var fs11 = __toESM(require("fs"));
|
|
8412
8537
|
var path8 = __toESM(require("path"));
|
|
8413
|
-
var
|
|
8538
|
+
var crypto2 = __toESM(require("crypto"));
|
|
8414
8539
|
function parseFrontmatter2(line) {
|
|
8415
8540
|
const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
|
|
8416
8541
|
if (!match) return null;
|
|
@@ -8438,7 +8563,7 @@ function extractIndexEntry(entry) {
|
|
|
8438
8563
|
};
|
|
8439
8564
|
}
|
|
8440
8565
|
function computeEntryHash(text) {
|
|
8441
|
-
return
|
|
8566
|
+
return crypto2.createHash("sha256").update(text).digest("hex").slice(0, 8);
|
|
8442
8567
|
}
|
|
8443
8568
|
function normalizeLearningContent(text) {
|
|
8444
8569
|
let normalized = text;
|
|
@@ -8453,7 +8578,7 @@ function normalizeLearningContent(text) {
|
|
|
8453
8578
|
return normalized;
|
|
8454
8579
|
}
|
|
8455
8580
|
function computeContentHash(text) {
|
|
8456
|
-
return
|
|
8581
|
+
return crypto2.createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
8457
8582
|
}
|
|
8458
8583
|
function loadContentHashes(stateDir) {
|
|
8459
8584
|
const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
|
|
@@ -8593,7 +8718,7 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
|
8593
8718
|
// src/state/learnings.ts
|
|
8594
8719
|
var fs13 = __toESM(require("fs"));
|
|
8595
8720
|
var path10 = __toESM(require("path"));
|
|
8596
|
-
var
|
|
8721
|
+
var crypto3 = __toESM(require("crypto"));
|
|
8597
8722
|
async function appendLearning(projectPath, learning, skillName, outcome, stream, session) {
|
|
8598
8723
|
try {
|
|
8599
8724
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
@@ -8630,7 +8755,7 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
|
|
|
8630
8755
|
} else {
|
|
8631
8756
|
bulletLine = `- **${timestamp}:** ${learning}`;
|
|
8632
8757
|
}
|
|
8633
|
-
const hash =
|
|
8758
|
+
const hash = crypto3.createHash("sha256").update(bulletLine).digest("hex").slice(0, 8);
|
|
8634
8759
|
const tagsStr = fmTags.length > 0 ? ` tags:${fmTags.join(",")}` : "";
|
|
8635
8760
|
const frontmatter = `<!-- hash:${hash}${tagsStr} -->`;
|
|
8636
8761
|
const entry = `
|
|
@@ -9341,7 +9466,7 @@ async function updateSessionEntryStatus(projectPath, sessionSlug, section, entry
|
|
|
9341
9466
|
}
|
|
9342
9467
|
function generateEntryId() {
|
|
9343
9468
|
const timestamp = Date.now().toString(36);
|
|
9344
|
-
const random =
|
|
9469
|
+
const random = Buffer.from(crypto.getRandomValues(new Uint8Array(4))).toString("hex");
|
|
9345
9470
|
return `${timestamp}-${random}`;
|
|
9346
9471
|
}
|
|
9347
9472
|
|
|
@@ -9486,15 +9611,25 @@ async function loadEvents(projectPath, options) {
|
|
|
9486
9611
|
);
|
|
9487
9612
|
}
|
|
9488
9613
|
}
|
|
9614
|
+
function phaseTransitionFields(data) {
|
|
9615
|
+
return {
|
|
9616
|
+
from: data?.from ?? "?",
|
|
9617
|
+
to: data?.to ?? "?",
|
|
9618
|
+
suffix: data?.taskCount ? ` (${data.taskCount} tasks)` : ""
|
|
9619
|
+
};
|
|
9620
|
+
}
|
|
9489
9621
|
function formatPhaseTransition(event) {
|
|
9490
9622
|
const data = event.data;
|
|
9491
|
-
const
|
|
9492
|
-
return `phase: ${
|
|
9623
|
+
const { from, to, suffix } = phaseTransitionFields(data);
|
|
9624
|
+
return `phase: ${from} -> ${to}${suffix}`;
|
|
9625
|
+
}
|
|
9626
|
+
function formatGateChecks(checks) {
|
|
9627
|
+
return checks?.map((c) => `${c.name} ${c.passed ? "Y" : "N"}`).join(", ");
|
|
9493
9628
|
}
|
|
9494
9629
|
function formatGateResult(event) {
|
|
9495
9630
|
const data = event.data;
|
|
9496
9631
|
const status = data?.passed ? "passed" : "failed";
|
|
9497
|
-
const checks = data?.checks
|
|
9632
|
+
const checks = formatGateChecks(data?.checks);
|
|
9498
9633
|
return checks ? `gate: ${status} (${checks})` : `gate: ${status}`;
|
|
9499
9634
|
}
|
|
9500
9635
|
function formatHandoffDetail(event) {
|
|
@@ -9770,34 +9905,36 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
9770
9905
|
// src/security/stack-detector.ts
|
|
9771
9906
|
var fs22 = __toESM(require("fs"));
|
|
9772
9907
|
var path19 = __toESM(require("path"));
|
|
9773
|
-
function
|
|
9774
|
-
const
|
|
9908
|
+
function nodeSubStacks(allDeps) {
|
|
9909
|
+
const found = [];
|
|
9910
|
+
if (allDeps.react || allDeps["react-dom"]) found.push("react");
|
|
9911
|
+
if (allDeps.express) found.push("express");
|
|
9912
|
+
if (allDeps.koa) found.push("koa");
|
|
9913
|
+
if (allDeps.fastify) found.push("fastify");
|
|
9914
|
+
if (allDeps.next) found.push("next");
|
|
9915
|
+
if (allDeps.vue) found.push("vue");
|
|
9916
|
+
if (allDeps.angular || allDeps["@angular/core"]) found.push("angular");
|
|
9917
|
+
return found;
|
|
9918
|
+
}
|
|
9919
|
+
function detectNodeStacks(projectRoot) {
|
|
9775
9920
|
const pkgJsonPath = path19.join(projectRoot, "package.json");
|
|
9776
|
-
if (fs22.existsSync(pkgJsonPath))
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
};
|
|
9784
|
-
if (allDeps.react || allDeps["react-dom"]) stacks.push("react");
|
|
9785
|
-
if (allDeps.express) stacks.push("express");
|
|
9786
|
-
if (allDeps.koa) stacks.push("koa");
|
|
9787
|
-
if (allDeps.fastify) stacks.push("fastify");
|
|
9788
|
-
if (allDeps.next) stacks.push("next");
|
|
9789
|
-
if (allDeps.vue) stacks.push("vue");
|
|
9790
|
-
if (allDeps.angular || allDeps["@angular/core"]) stacks.push("angular");
|
|
9791
|
-
} catch {
|
|
9792
|
-
}
|
|
9921
|
+
if (!fs22.existsSync(pkgJsonPath)) return [];
|
|
9922
|
+
const stacks = ["node"];
|
|
9923
|
+
try {
|
|
9924
|
+
const pkgJson = JSON.parse(fs22.readFileSync(pkgJsonPath, "utf-8"));
|
|
9925
|
+
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
9926
|
+
stacks.push(...nodeSubStacks(allDeps));
|
|
9927
|
+
} catch {
|
|
9793
9928
|
}
|
|
9794
|
-
|
|
9795
|
-
|
|
9929
|
+
return stacks;
|
|
9930
|
+
}
|
|
9931
|
+
function detectStack(projectRoot) {
|
|
9932
|
+
const stacks = [...detectNodeStacks(projectRoot)];
|
|
9933
|
+
if (fs22.existsSync(path19.join(projectRoot, "go.mod"))) {
|
|
9796
9934
|
stacks.push("go");
|
|
9797
9935
|
}
|
|
9798
|
-
const
|
|
9799
|
-
|
|
9800
|
-
if (fs22.existsSync(requirementsPath) || fs22.existsSync(pyprojectPath)) {
|
|
9936
|
+
const hasPython = fs22.existsSync(path19.join(projectRoot, "requirements.txt")) || fs22.existsSync(path19.join(projectRoot, "pyproject.toml"));
|
|
9937
|
+
if (hasPython) {
|
|
9801
9938
|
stacks.push("python");
|
|
9802
9939
|
}
|
|
9803
9940
|
return stacks;
|
|
@@ -10641,6 +10778,56 @@ var SecurityScanner = class {
|
|
|
10641
10778
|
});
|
|
10642
10779
|
return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
|
|
10643
10780
|
}
|
|
10781
|
+
/** Build a finding for a suppression comment that is missing its justification. */
|
|
10782
|
+
buildSuppressionFinding(rule, filePath, lineNumber, line) {
|
|
10783
|
+
return {
|
|
10784
|
+
ruleId: rule.id,
|
|
10785
|
+
ruleName: rule.name,
|
|
10786
|
+
category: rule.category,
|
|
10787
|
+
severity: this.config.strict ? "error" : "warning",
|
|
10788
|
+
confidence: "high",
|
|
10789
|
+
file: filePath,
|
|
10790
|
+
line: lineNumber,
|
|
10791
|
+
match: line.trim(),
|
|
10792
|
+
context: line,
|
|
10793
|
+
message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
|
|
10794
|
+
remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
|
|
10795
|
+
};
|
|
10796
|
+
}
|
|
10797
|
+
/** Check one line against a rule's patterns; return a finding or null. */
|
|
10798
|
+
matchRuleLine(rule, resolved, filePath, lineNumber, line) {
|
|
10799
|
+
for (const pattern of rule.patterns) {
|
|
10800
|
+
pattern.lastIndex = 0;
|
|
10801
|
+
if (!pattern.test(line)) continue;
|
|
10802
|
+
return {
|
|
10803
|
+
ruleId: rule.id,
|
|
10804
|
+
ruleName: rule.name,
|
|
10805
|
+
category: rule.category,
|
|
10806
|
+
severity: resolved,
|
|
10807
|
+
confidence: rule.confidence,
|
|
10808
|
+
file: filePath,
|
|
10809
|
+
line: lineNumber,
|
|
10810
|
+
match: line.trim(),
|
|
10811
|
+
context: line,
|
|
10812
|
+
message: rule.message,
|
|
10813
|
+
remediation: rule.remediation,
|
|
10814
|
+
...rule.references ? { references: rule.references } : {}
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10817
|
+
return null;
|
|
10818
|
+
}
|
|
10819
|
+
/** Scan a single line against a resolved rule; push any findings into the array. */
|
|
10820
|
+
scanLineForRule(rule, resolved, line, lineNumber, filePath, findings) {
|
|
10821
|
+
const suppressionMatch = parseHarnessIgnore(line, rule.id);
|
|
10822
|
+
if (suppressionMatch) {
|
|
10823
|
+
if (!suppressionMatch.justification) {
|
|
10824
|
+
findings.push(this.buildSuppressionFinding(rule, filePath, lineNumber, line));
|
|
10825
|
+
}
|
|
10826
|
+
return;
|
|
10827
|
+
}
|
|
10828
|
+
const finding = this.matchRuleLine(rule, resolved, filePath, lineNumber, line);
|
|
10829
|
+
if (finding) findings.push(finding);
|
|
10830
|
+
}
|
|
10644
10831
|
/**
|
|
10645
10832
|
* Core scanning loop shared by scanContent and scanContentForFile.
|
|
10646
10833
|
* Evaluates each rule against each line, handling suppression (FP gate)
|
|
@@ -10657,46 +10844,7 @@ var SecurityScanner = class {
|
|
|
10657
10844
|
);
|
|
10658
10845
|
if (resolved === "off") continue;
|
|
10659
10846
|
for (let i = 0; i < lines.length; i++) {
|
|
10660
|
-
|
|
10661
|
-
const suppressionMatch = parseHarnessIgnore(line, rule.id);
|
|
10662
|
-
if (suppressionMatch) {
|
|
10663
|
-
if (!suppressionMatch.justification) {
|
|
10664
|
-
findings.push({
|
|
10665
|
-
ruleId: rule.id,
|
|
10666
|
-
ruleName: rule.name,
|
|
10667
|
-
category: rule.category,
|
|
10668
|
-
severity: this.config.strict ? "error" : "warning",
|
|
10669
|
-
confidence: "high",
|
|
10670
|
-
file: filePath,
|
|
10671
|
-
line: startLine + i,
|
|
10672
|
-
match: line.trim(),
|
|
10673
|
-
context: line,
|
|
10674
|
-
message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
|
|
10675
|
-
remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
|
|
10676
|
-
});
|
|
10677
|
-
}
|
|
10678
|
-
continue;
|
|
10679
|
-
}
|
|
10680
|
-
for (const pattern of rule.patterns) {
|
|
10681
|
-
pattern.lastIndex = 0;
|
|
10682
|
-
if (pattern.test(line)) {
|
|
10683
|
-
findings.push({
|
|
10684
|
-
ruleId: rule.id,
|
|
10685
|
-
ruleName: rule.name,
|
|
10686
|
-
category: rule.category,
|
|
10687
|
-
severity: resolved,
|
|
10688
|
-
confidence: rule.confidence,
|
|
10689
|
-
file: filePath,
|
|
10690
|
-
line: startLine + i,
|
|
10691
|
-
match: line.trim(),
|
|
10692
|
-
context: line,
|
|
10693
|
-
message: rule.message,
|
|
10694
|
-
remediation: rule.remediation,
|
|
10695
|
-
...rule.references ? { references: rule.references } : {}
|
|
10696
|
-
});
|
|
10697
|
-
break;
|
|
10698
|
-
}
|
|
10699
|
-
}
|
|
10847
|
+
this.scanLineForRule(rule, resolved, lines[i] ?? "", startLine + i, filePath, findings);
|
|
10700
10848
|
}
|
|
10701
10849
|
}
|
|
10702
10850
|
return findings;
|
|
@@ -12114,39 +12262,32 @@ function extractConventionRules(bundle) {
|
|
|
12114
12262
|
}
|
|
12115
12263
|
}
|
|
12116
12264
|
}
|
|
12117
|
-
return rules;
|
|
12118
|
-
}
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
for (
|
|
12122
|
-
const
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
break;
|
|
12137
|
-
}
|
|
12138
|
-
if (!hasJsDoc) {
|
|
12139
|
-
missing.push({
|
|
12140
|
-
file: cf.path,
|
|
12141
|
-
line: i + 1,
|
|
12142
|
-
exportName: exportMatch[1]
|
|
12143
|
-
});
|
|
12144
|
-
}
|
|
12145
|
-
}
|
|
12146
|
-
}
|
|
12147
|
-
}
|
|
12265
|
+
return rules;
|
|
12266
|
+
}
|
|
12267
|
+
var EXPORT_RE = /export\s+(?:async\s+)?(?:function|const|class|interface|type)\s+(\w+)/;
|
|
12268
|
+
function hasPrecedingJsDoc(lines, i) {
|
|
12269
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
12270
|
+
const prev = lines[j].trim();
|
|
12271
|
+
if (prev === "") continue;
|
|
12272
|
+
return prev.endsWith("*/");
|
|
12273
|
+
}
|
|
12274
|
+
return false;
|
|
12275
|
+
}
|
|
12276
|
+
function scanFileForMissingJsDoc(filePath, lines) {
|
|
12277
|
+
const missing = [];
|
|
12278
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12279
|
+
const exportMatch = lines[i].match(EXPORT_RE);
|
|
12280
|
+
if (exportMatch && !hasPrecedingJsDoc(lines, i)) {
|
|
12281
|
+
missing.push({ file: filePath, line: i + 1, exportName: exportMatch[1] });
|
|
12282
|
+
}
|
|
12283
|
+
}
|
|
12148
12284
|
return missing;
|
|
12149
12285
|
}
|
|
12286
|
+
function findMissingJsDoc(bundle) {
|
|
12287
|
+
return bundle.changedFiles.flatMap(
|
|
12288
|
+
(cf) => scanFileForMissingJsDoc(cf.path, cf.content.split("\n"))
|
|
12289
|
+
);
|
|
12290
|
+
}
|
|
12150
12291
|
function checkMissingJsDoc(bundle, rules) {
|
|
12151
12292
|
const jsDocRule = rules.find((r) => r.text.toLowerCase().includes("jsdoc"));
|
|
12152
12293
|
if (!jsDocRule) return [];
|
|
@@ -12211,29 +12352,27 @@ function checkChangeTypeSpecific(bundle) {
|
|
|
12211
12352
|
return [];
|
|
12212
12353
|
}
|
|
12213
12354
|
}
|
|
12355
|
+
function checkFileResultTypeConvention(cf, bundle, rule) {
|
|
12356
|
+
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
12357
|
+
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
12358
|
+
if (!hasTryCatch || usesResult) return null;
|
|
12359
|
+
return {
|
|
12360
|
+
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
12361
|
+
file: cf.path,
|
|
12362
|
+
lineRange: [1, cf.lines],
|
|
12363
|
+
domain: "compliance",
|
|
12364
|
+
severity: "suggestion",
|
|
12365
|
+
title: "Fallible operation uses try/catch instead of Result type",
|
|
12366
|
+
rationale: `Convention requires using Result type for fallible operations (from ${rule.source}).`,
|
|
12367
|
+
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
12368
|
+
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${rule.text}"`],
|
|
12369
|
+
validatedBy: "heuristic"
|
|
12370
|
+
};
|
|
12371
|
+
}
|
|
12214
12372
|
function checkResultTypeConvention(bundle, rules) {
|
|
12215
12373
|
const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
|
|
12216
12374
|
if (!resultTypeRule) return [];
|
|
12217
|
-
|
|
12218
|
-
for (const cf of bundle.changedFiles) {
|
|
12219
|
-
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
12220
|
-
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
12221
|
-
if (hasTryCatch && !usesResult) {
|
|
12222
|
-
findings.push({
|
|
12223
|
-
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
12224
|
-
file: cf.path,
|
|
12225
|
-
lineRange: [1, cf.lines],
|
|
12226
|
-
domain: "compliance",
|
|
12227
|
-
severity: "suggestion",
|
|
12228
|
-
title: "Fallible operation uses try/catch instead of Result type",
|
|
12229
|
-
rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
|
|
12230
|
-
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
12231
|
-
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${resultTypeRule.text}"`],
|
|
12232
|
-
validatedBy: "heuristic"
|
|
12233
|
-
});
|
|
12234
|
-
}
|
|
12235
|
-
}
|
|
12236
|
-
return findings;
|
|
12375
|
+
return bundle.changedFiles.map((cf) => checkFileResultTypeConvention(cf, bundle, resultTypeRule)).filter((f) => f !== null);
|
|
12237
12376
|
}
|
|
12238
12377
|
function runComplianceAgent(bundle) {
|
|
12239
12378
|
const rules = extractConventionRules(bundle);
|
|
@@ -12259,53 +12398,58 @@ var BUG_DETECTION_DESCRIPTOR = {
|
|
|
12259
12398
|
"Test coverage \u2014 tests for happy path, error paths, and edge cases"
|
|
12260
12399
|
]
|
|
12261
12400
|
};
|
|
12401
|
+
function hasPrecedingZeroCheck(lines, i) {
|
|
12402
|
+
const preceding = lines.slice(Math.max(0, i - 3), i).join("\n");
|
|
12403
|
+
return preceding.includes("=== 0") || preceding.includes("!== 0") || preceding.includes("== 0") || preceding.includes("!= 0");
|
|
12404
|
+
}
|
|
12262
12405
|
function detectDivisionByZero(bundle) {
|
|
12263
12406
|
const findings = [];
|
|
12264
12407
|
for (const cf of bundle.changedFiles) {
|
|
12265
12408
|
const lines = cf.content.split("\n");
|
|
12266
12409
|
for (let i = 0; i < lines.length; i++) {
|
|
12267
12410
|
const line = lines[i];
|
|
12268
|
-
if (line.match(/[^=!<>]\s*\/\s*[a-zA-Z_]\w*/)
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
});
|
|
12283
|
-
}
|
|
12284
|
-
}
|
|
12411
|
+
if (!line.match(/[^=!<>]\s*\/\s*[a-zA-Z_]\w*/) || line.includes("//")) continue;
|
|
12412
|
+
if (hasPrecedingZeroCheck(lines, i)) continue;
|
|
12413
|
+
findings.push({
|
|
12414
|
+
id: makeFindingId("bug", cf.path, i + 1, "division by zero"),
|
|
12415
|
+
file: cf.path,
|
|
12416
|
+
lineRange: [i + 1, i + 1],
|
|
12417
|
+
domain: "bug",
|
|
12418
|
+
severity: "important",
|
|
12419
|
+
title: "Potential division by zero without guard",
|
|
12420
|
+
rationale: "Division operation found without a preceding zero check on the divisor. This can cause Infinity or NaN at runtime.",
|
|
12421
|
+
suggestion: "Add a check for zero before dividing, or use a safe division utility.",
|
|
12422
|
+
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
12423
|
+
validatedBy: "heuristic"
|
|
12424
|
+
});
|
|
12285
12425
|
}
|
|
12286
12426
|
}
|
|
12287
12427
|
return findings;
|
|
12288
12428
|
}
|
|
12429
|
+
function isEmptyCatch(lines, i) {
|
|
12430
|
+
const line = lines[i];
|
|
12431
|
+
if (line.match(/catch\s*\([^)]*\)\s*\{\s*\}/)) return true;
|
|
12432
|
+
return line.match(/catch\s*\([^)]*\)\s*\{/) !== null && i + 1 < lines.length && lines[i + 1].trim() === "}";
|
|
12433
|
+
}
|
|
12289
12434
|
function detectEmptyCatch(bundle) {
|
|
12290
12435
|
const findings = [];
|
|
12291
12436
|
for (const cf of bundle.changedFiles) {
|
|
12292
12437
|
const lines = cf.content.split("\n");
|
|
12293
12438
|
for (let i = 0; i < lines.length; i++) {
|
|
12439
|
+
if (!isEmptyCatch(lines, i)) continue;
|
|
12294
12440
|
const line = lines[i];
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
});
|
|
12308
|
-
}
|
|
12441
|
+
findings.push({
|
|
12442
|
+
id: makeFindingId("bug", cf.path, i + 1, "empty catch block"),
|
|
12443
|
+
file: cf.path,
|
|
12444
|
+
lineRange: [i + 1, i + 2],
|
|
12445
|
+
domain: "bug",
|
|
12446
|
+
severity: "important",
|
|
12447
|
+
title: "Empty catch block silently swallows error",
|
|
12448
|
+
rationale: "Catching an error without handling, logging, or re-throwing it hides failures and makes debugging difficult.",
|
|
12449
|
+
suggestion: "Log the error, re-throw it, or handle it explicitly. If intentionally ignoring, add a comment explaining why.",
|
|
12450
|
+
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
12451
|
+
validatedBy: "heuristic"
|
|
12452
|
+
});
|
|
12309
12453
|
}
|
|
12310
12454
|
}
|
|
12311
12455
|
return findings;
|
|
@@ -12363,34 +12507,102 @@ var SECRET_PATTERNS = [
|
|
|
12363
12507
|
];
|
|
12364
12508
|
var SQL_CONCAT_PATTERN = /(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\s+.*?\+\s*\w+|`[^`]*\$\{[^}]*\}[^`]*(?:SELECT|INSERT|UPDATE|DELETE|WHERE)/i;
|
|
12365
12509
|
var SHELL_EXEC_PATTERN = /(?:exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/;
|
|
12510
|
+
function makeEvalFinding(file, lineNum, line) {
|
|
12511
|
+
return {
|
|
12512
|
+
id: makeFindingId("security", file, lineNum, "eval usage CWE-94"),
|
|
12513
|
+
file,
|
|
12514
|
+
lineRange: [lineNum, lineNum],
|
|
12515
|
+
domain: "security",
|
|
12516
|
+
severity: "critical",
|
|
12517
|
+
title: `Dangerous ${"eval"}() or new ${"Function"}() usage`,
|
|
12518
|
+
rationale: `${"eval"}() and new ${"Function"}() execute arbitrary code. If user input reaches these calls, it enables Remote Code Execution (CWE-94).`,
|
|
12519
|
+
suggestion: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
|
|
12520
|
+
evidence: [`Line ${lineNum}: ${line.trim()}`],
|
|
12521
|
+
validatedBy: "heuristic",
|
|
12522
|
+
cweId: "CWE-94",
|
|
12523
|
+
owaspCategory: "A03:2021 Injection",
|
|
12524
|
+
confidence: "high",
|
|
12525
|
+
remediation: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
|
|
12526
|
+
references: [
|
|
12527
|
+
"https://cwe.mitre.org/data/definitions/94.html",
|
|
12528
|
+
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
12529
|
+
]
|
|
12530
|
+
};
|
|
12531
|
+
}
|
|
12532
|
+
function makeSecretFinding(file, lineNum) {
|
|
12533
|
+
return {
|
|
12534
|
+
id: makeFindingId("security", file, lineNum, "hardcoded secret CWE-798"),
|
|
12535
|
+
file,
|
|
12536
|
+
lineRange: [lineNum, lineNum],
|
|
12537
|
+
domain: "security",
|
|
12538
|
+
severity: "critical",
|
|
12539
|
+
title: "Hardcoded secret or API key detected",
|
|
12540
|
+
rationale: "Hardcoded secrets in source code can be extracted from version history even after removal. Use environment variables or a secrets manager (CWE-798).",
|
|
12541
|
+
suggestion: "Move the secret to an environment variable and access it via process.env.",
|
|
12542
|
+
evidence: [`Line ${lineNum}: [secret detected \u2014 value redacted]`],
|
|
12543
|
+
validatedBy: "heuristic",
|
|
12544
|
+
cweId: "CWE-798",
|
|
12545
|
+
owaspCategory: "A07:2021 Identification and Authentication Failures",
|
|
12546
|
+
confidence: "high",
|
|
12547
|
+
remediation: "Move the secret to an environment variable and access it via process.env.",
|
|
12548
|
+
references: [
|
|
12549
|
+
"https://cwe.mitre.org/data/definitions/798.html",
|
|
12550
|
+
"https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
|
|
12551
|
+
]
|
|
12552
|
+
};
|
|
12553
|
+
}
|
|
12554
|
+
function makeSqlFinding(file, lineNum, line) {
|
|
12555
|
+
return {
|
|
12556
|
+
id: makeFindingId("security", file, lineNum, "SQL injection CWE-89"),
|
|
12557
|
+
file,
|
|
12558
|
+
lineRange: [lineNum, lineNum],
|
|
12559
|
+
domain: "security",
|
|
12560
|
+
severity: "critical",
|
|
12561
|
+
title: "Potential SQL injection via string concatenation",
|
|
12562
|
+
rationale: "Building SQL queries with string concatenation or template literals allows attackers to inject malicious SQL (CWE-89).",
|
|
12563
|
+
suggestion: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
12564
|
+
evidence: [`Line ${lineNum}: ${line.trim()}`],
|
|
12565
|
+
validatedBy: "heuristic",
|
|
12566
|
+
cweId: "CWE-89",
|
|
12567
|
+
owaspCategory: "A03:2021 Injection",
|
|
12568
|
+
confidence: "high",
|
|
12569
|
+
remediation: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
12570
|
+
references: [
|
|
12571
|
+
"https://cwe.mitre.org/data/definitions/89.html",
|
|
12572
|
+
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
12573
|
+
]
|
|
12574
|
+
};
|
|
12575
|
+
}
|
|
12576
|
+
function makeCommandFinding(file, lineNum, line) {
|
|
12577
|
+
return {
|
|
12578
|
+
id: makeFindingId("security", file, lineNum, "command injection CWE-78"),
|
|
12579
|
+
file,
|
|
12580
|
+
lineRange: [lineNum, lineNum],
|
|
12581
|
+
domain: "security",
|
|
12582
|
+
severity: "critical",
|
|
12583
|
+
title: "Potential command injection via shell exec with interpolation",
|
|
12584
|
+
rationale: "Using exec/spawn with template literal interpolation allows attackers to inject shell commands (CWE-78).",
|
|
12585
|
+
suggestion: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
12586
|
+
evidence: [`Line ${lineNum}: ${line.trim()}`],
|
|
12587
|
+
validatedBy: "heuristic",
|
|
12588
|
+
cweId: "CWE-78",
|
|
12589
|
+
owaspCategory: "A03:2021 Injection",
|
|
12590
|
+
confidence: "high",
|
|
12591
|
+
remediation: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
12592
|
+
references: [
|
|
12593
|
+
"https://cwe.mitre.org/data/definitions/78.html",
|
|
12594
|
+
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
12595
|
+
]
|
|
12596
|
+
};
|
|
12597
|
+
}
|
|
12366
12598
|
function detectEvalUsage(bundle) {
|
|
12367
12599
|
const findings = [];
|
|
12368
12600
|
for (const cf of bundle.changedFiles) {
|
|
12369
12601
|
const lines = cf.content.split("\n");
|
|
12370
12602
|
for (let i = 0; i < lines.length; i++) {
|
|
12371
12603
|
const line = lines[i];
|
|
12372
|
-
if (EVAL_PATTERN.test(line))
|
|
12373
|
-
|
|
12374
|
-
id: makeFindingId("security", cf.path, i + 1, "eval usage CWE-94"),
|
|
12375
|
-
file: cf.path,
|
|
12376
|
-
lineRange: [i + 1, i + 1],
|
|
12377
|
-
domain: "security",
|
|
12378
|
-
severity: "critical",
|
|
12379
|
-
title: `Dangerous ${"eval"}() or new ${"Function"}() usage`,
|
|
12380
|
-
rationale: `${"eval"}() and new ${"Function"}() execute arbitrary code. If user input reaches these calls, it enables Remote Code Execution (CWE-94).`,
|
|
12381
|
-
suggestion: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
|
|
12382
|
-
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
12383
|
-
validatedBy: "heuristic",
|
|
12384
|
-
cweId: "CWE-94",
|
|
12385
|
-
owaspCategory: "A03:2021 Injection",
|
|
12386
|
-
confidence: "high",
|
|
12387
|
-
remediation: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
|
|
12388
|
-
references: [
|
|
12389
|
-
"https://cwe.mitre.org/data/definitions/94.html",
|
|
12390
|
-
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
12391
|
-
]
|
|
12392
|
-
});
|
|
12393
|
-
}
|
|
12604
|
+
if (!EVAL_PATTERN.test(line)) continue;
|
|
12605
|
+
findings.push(makeEvalFinding(cf.path, i + 1, line));
|
|
12394
12606
|
}
|
|
12395
12607
|
}
|
|
12396
12608
|
return findings;
|
|
@@ -12402,31 +12614,9 @@ function detectHardcodedSecrets(bundle) {
|
|
|
12402
12614
|
for (let i = 0; i < lines.length; i++) {
|
|
12403
12615
|
const line = lines[i];
|
|
12404
12616
|
const codePart = line.includes("//") ? line.slice(0, line.indexOf("//")) : line;
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
id: makeFindingId("security", cf.path, i + 1, "hardcoded secret CWE-798"),
|
|
12409
|
-
file: cf.path,
|
|
12410
|
-
lineRange: [i + 1, i + 1],
|
|
12411
|
-
domain: "security",
|
|
12412
|
-
severity: "critical",
|
|
12413
|
-
title: "Hardcoded secret or API key detected",
|
|
12414
|
-
rationale: "Hardcoded secrets in source code can be extracted from version history even after removal. Use environment variables or a secrets manager (CWE-798).",
|
|
12415
|
-
suggestion: "Move the secret to an environment variable and access it via process.env.",
|
|
12416
|
-
evidence: [`Line ${i + 1}: [secret detected \u2014 value redacted]`],
|
|
12417
|
-
validatedBy: "heuristic",
|
|
12418
|
-
cweId: "CWE-798",
|
|
12419
|
-
owaspCategory: "A07:2021 Identification and Authentication Failures",
|
|
12420
|
-
confidence: "high",
|
|
12421
|
-
remediation: "Move the secret to an environment variable and access it via process.env.",
|
|
12422
|
-
references: [
|
|
12423
|
-
"https://cwe.mitre.org/data/definitions/798.html",
|
|
12424
|
-
"https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
|
|
12425
|
-
]
|
|
12426
|
-
});
|
|
12427
|
-
break;
|
|
12428
|
-
}
|
|
12429
|
-
}
|
|
12617
|
+
const matched = SECRET_PATTERNS.some((p) => p.test(codePart));
|
|
12618
|
+
if (!matched) continue;
|
|
12619
|
+
findings.push(makeSecretFinding(cf.path, i + 1));
|
|
12430
12620
|
}
|
|
12431
12621
|
}
|
|
12432
12622
|
return findings;
|
|
@@ -12437,28 +12627,8 @@ function detectSqlInjection(bundle) {
|
|
|
12437
12627
|
const lines = cf.content.split("\n");
|
|
12438
12628
|
for (let i = 0; i < lines.length; i++) {
|
|
12439
12629
|
const line = lines[i];
|
|
12440
|
-
if (SQL_CONCAT_PATTERN.test(line))
|
|
12441
|
-
|
|
12442
|
-
id: makeFindingId("security", cf.path, i + 1, "SQL injection CWE-89"),
|
|
12443
|
-
file: cf.path,
|
|
12444
|
-
lineRange: [i + 1, i + 1],
|
|
12445
|
-
domain: "security",
|
|
12446
|
-
severity: "critical",
|
|
12447
|
-
title: "Potential SQL injection via string concatenation",
|
|
12448
|
-
rationale: "Building SQL queries with string concatenation or template literals allows attackers to inject malicious SQL (CWE-89).",
|
|
12449
|
-
suggestion: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
12450
|
-
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
12451
|
-
validatedBy: "heuristic",
|
|
12452
|
-
cweId: "CWE-89",
|
|
12453
|
-
owaspCategory: "A03:2021 Injection",
|
|
12454
|
-
confidence: "high",
|
|
12455
|
-
remediation: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
12456
|
-
references: [
|
|
12457
|
-
"https://cwe.mitre.org/data/definitions/89.html",
|
|
12458
|
-
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
12459
|
-
]
|
|
12460
|
-
});
|
|
12461
|
-
}
|
|
12630
|
+
if (!SQL_CONCAT_PATTERN.test(line)) continue;
|
|
12631
|
+
findings.push(makeSqlFinding(cf.path, i + 1, line));
|
|
12462
12632
|
}
|
|
12463
12633
|
}
|
|
12464
12634
|
return findings;
|
|
@@ -12469,28 +12639,8 @@ function detectCommandInjection(bundle) {
|
|
|
12469
12639
|
const lines = cf.content.split("\n");
|
|
12470
12640
|
for (let i = 0; i < lines.length; i++) {
|
|
12471
12641
|
const line = lines[i];
|
|
12472
|
-
if (SHELL_EXEC_PATTERN.test(line))
|
|
12473
|
-
|
|
12474
|
-
id: makeFindingId("security", cf.path, i + 1, "command injection CWE-78"),
|
|
12475
|
-
file: cf.path,
|
|
12476
|
-
lineRange: [i + 1, i + 1],
|
|
12477
|
-
domain: "security",
|
|
12478
|
-
severity: "critical",
|
|
12479
|
-
title: "Potential command injection via shell exec with interpolation",
|
|
12480
|
-
rationale: "Using exec/spawn with template literal interpolation allows attackers to inject shell commands (CWE-78).",
|
|
12481
|
-
suggestion: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
12482
|
-
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
12483
|
-
validatedBy: "heuristic",
|
|
12484
|
-
cweId: "CWE-78",
|
|
12485
|
-
owaspCategory: "A03:2021 Injection",
|
|
12486
|
-
confidence: "high",
|
|
12487
|
-
remediation: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
12488
|
-
references: [
|
|
12489
|
-
"https://cwe.mitre.org/data/definitions/78.html",
|
|
12490
|
-
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
12491
|
-
]
|
|
12492
|
-
});
|
|
12493
|
-
}
|
|
12642
|
+
if (!SHELL_EXEC_PATTERN.test(line)) continue;
|
|
12643
|
+
findings.push(makeCommandFinding(cf.path, i + 1, line));
|
|
12494
12644
|
}
|
|
12495
12645
|
}
|
|
12496
12646
|
return findings;
|
|
@@ -12523,10 +12673,15 @@ function isViolationLine(line) {
|
|
|
12523
12673
|
const lower = line.toLowerCase();
|
|
12524
12674
|
return lower.includes("violation") || lower.includes("layer");
|
|
12525
12675
|
}
|
|
12526
|
-
|
|
12527
|
-
|
|
12676
|
+
var VIOLATION_FILE_RE = /(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/;
|
|
12677
|
+
function extractViolationLocation(line, fallbackPath) {
|
|
12678
|
+
const fileMatch = line.match(VIOLATION_FILE_RE);
|
|
12528
12679
|
const file = fileMatch?.[1] ?? fallbackPath;
|
|
12529
12680
|
const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
|
|
12681
|
+
return { file, lineNum };
|
|
12682
|
+
}
|
|
12683
|
+
function createLayerViolationFinding(line, fallbackPath) {
|
|
12684
|
+
const { file, lineNum } = extractViolationLocation(line, fallbackPath);
|
|
12530
12685
|
return {
|
|
12531
12686
|
id: makeFindingId("arch", file, lineNum, "layer violation"),
|
|
12532
12687
|
file,
|
|
@@ -12699,6 +12854,26 @@ function normalizePath(filePath, projectRoot) {
|
|
|
12699
12854
|
}
|
|
12700
12855
|
return normalized;
|
|
12701
12856
|
}
|
|
12857
|
+
function resolveImportPath3(currentFile, importPath) {
|
|
12858
|
+
const dir = path23.dirname(currentFile);
|
|
12859
|
+
let resolved = path23.join(dir, importPath).replace(/\\/g, "/");
|
|
12860
|
+
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
12861
|
+
resolved += ".ts";
|
|
12862
|
+
}
|
|
12863
|
+
return path23.normalize(resolved).replace(/\\/g, "/");
|
|
12864
|
+
}
|
|
12865
|
+
function enqueueImports(content, current, visited, queue, maxDepth) {
|
|
12866
|
+
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
12867
|
+
let match;
|
|
12868
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
12869
|
+
const importPath = match[1];
|
|
12870
|
+
if (!importPath.startsWith(".")) continue;
|
|
12871
|
+
const resolved = resolveImportPath3(current.file, importPath);
|
|
12872
|
+
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
12873
|
+
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
12874
|
+
}
|
|
12875
|
+
}
|
|
12876
|
+
}
|
|
12702
12877
|
function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
12703
12878
|
const visited = /* @__PURE__ */ new Set();
|
|
12704
12879
|
const queue = [{ file: fromFile, depth: 0 }];
|
|
@@ -12708,82 +12883,63 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
12708
12883
|
visited.add(current.file);
|
|
12709
12884
|
const content = fileContents.get(current.file);
|
|
12710
12885
|
if (!content) continue;
|
|
12711
|
-
|
|
12712
|
-
let match;
|
|
12713
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
12714
|
-
const importPath = match[1];
|
|
12715
|
-
if (!importPath.startsWith(".")) continue;
|
|
12716
|
-
const dir = path23.dirname(current.file);
|
|
12717
|
-
let resolved = path23.join(dir, importPath).replace(/\\/g, "/");
|
|
12718
|
-
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
12719
|
-
resolved += ".ts";
|
|
12720
|
-
}
|
|
12721
|
-
resolved = path23.normalize(resolved).replace(/\\/g, "/");
|
|
12722
|
-
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
12723
|
-
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
12724
|
-
}
|
|
12725
|
-
}
|
|
12886
|
+
enqueueImports(content, current, visited, queue, maxDepth);
|
|
12726
12887
|
}
|
|
12727
12888
|
visited.delete(fromFile);
|
|
12728
12889
|
return visited;
|
|
12729
12890
|
}
|
|
12891
|
+
function isMechanicallyExcluded(finding, exclusionSet, projectRoot) {
|
|
12892
|
+
const normalizedFile = normalizePath(finding.file, projectRoot);
|
|
12893
|
+
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange)) return true;
|
|
12894
|
+
if (exclusionSet.isExcluded(finding.file, finding.lineRange)) return true;
|
|
12895
|
+
const absoluteFile = path23.isAbsolute(finding.file) ? finding.file : path23.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
12896
|
+
return exclusionSet.isExcluded(absoluteFile, finding.lineRange);
|
|
12897
|
+
}
|
|
12898
|
+
async function validateWithGraph(crossFileRefs, graph) {
|
|
12899
|
+
try {
|
|
12900
|
+
for (const ref of crossFileRefs) {
|
|
12901
|
+
const reachable = await graph.isReachable(ref.from, ref.to);
|
|
12902
|
+
if (!reachable) return { result: "discard" };
|
|
12903
|
+
}
|
|
12904
|
+
return { result: "keep" };
|
|
12905
|
+
} catch {
|
|
12906
|
+
return { result: "fallback" };
|
|
12907
|
+
}
|
|
12908
|
+
}
|
|
12909
|
+
function validateWithHeuristic(finding, crossFileRefs, fileContents, projectRoot) {
|
|
12910
|
+
if (fileContents) {
|
|
12911
|
+
for (const ref of crossFileRefs) {
|
|
12912
|
+
const normalizedFrom = normalizePath(ref.from, projectRoot);
|
|
12913
|
+
const reachable = followImportChain(normalizedFrom, fileContents, 2);
|
|
12914
|
+
const normalizedTo = normalizePath(ref.to, projectRoot);
|
|
12915
|
+
if (reachable.has(normalizedTo)) {
|
|
12916
|
+
return { ...finding, validatedBy: "heuristic" };
|
|
12917
|
+
}
|
|
12918
|
+
}
|
|
12919
|
+
}
|
|
12920
|
+
return {
|
|
12921
|
+
...finding,
|
|
12922
|
+
severity: DOWNGRADE_MAP[finding.severity],
|
|
12923
|
+
validatedBy: "heuristic"
|
|
12924
|
+
};
|
|
12925
|
+
}
|
|
12926
|
+
async function processFinding(finding, exclusionSet, graph, projectRoot, fileContents) {
|
|
12927
|
+
if (isMechanicallyExcluded(finding, exclusionSet, projectRoot)) return null;
|
|
12928
|
+
const crossFileRefs = extractCrossFileRefs(finding);
|
|
12929
|
+
if (crossFileRefs.length === 0) return { ...finding };
|
|
12930
|
+
if (graph) {
|
|
12931
|
+
const { result } = await validateWithGraph(crossFileRefs, graph);
|
|
12932
|
+
if (result === "keep") return { ...finding, validatedBy: "graph" };
|
|
12933
|
+
if (result === "discard") return null;
|
|
12934
|
+
}
|
|
12935
|
+
return validateWithHeuristic(finding, crossFileRefs, fileContents, projectRoot);
|
|
12936
|
+
}
|
|
12730
12937
|
async function validateFindings(options) {
|
|
12731
12938
|
const { findings, exclusionSet, graph, projectRoot, fileContents } = options;
|
|
12732
12939
|
const validated = [];
|
|
12733
12940
|
for (const finding of findings) {
|
|
12734
|
-
const
|
|
12735
|
-
if (
|
|
12736
|
-
continue;
|
|
12737
|
-
}
|
|
12738
|
-
const absoluteFile = path23.isAbsolute(finding.file) ? finding.file : path23.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
12739
|
-
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
12740
|
-
continue;
|
|
12741
|
-
}
|
|
12742
|
-
const crossFileRefs = extractCrossFileRefs(finding);
|
|
12743
|
-
if (crossFileRefs.length === 0) {
|
|
12744
|
-
validated.push({ ...finding });
|
|
12745
|
-
continue;
|
|
12746
|
-
}
|
|
12747
|
-
if (graph) {
|
|
12748
|
-
try {
|
|
12749
|
-
let allReachable = true;
|
|
12750
|
-
for (const ref of crossFileRefs) {
|
|
12751
|
-
const reachable = await graph.isReachable(ref.from, ref.to);
|
|
12752
|
-
if (!reachable) {
|
|
12753
|
-
allReachable = false;
|
|
12754
|
-
break;
|
|
12755
|
-
}
|
|
12756
|
-
}
|
|
12757
|
-
if (allReachable) {
|
|
12758
|
-
validated.push({ ...finding, validatedBy: "graph" });
|
|
12759
|
-
}
|
|
12760
|
-
continue;
|
|
12761
|
-
} catch {
|
|
12762
|
-
}
|
|
12763
|
-
}
|
|
12764
|
-
{
|
|
12765
|
-
let chainValidated = false;
|
|
12766
|
-
if (fileContents) {
|
|
12767
|
-
for (const ref of crossFileRefs) {
|
|
12768
|
-
const normalizedFrom = normalizePath(ref.from, projectRoot);
|
|
12769
|
-
const reachable = followImportChain(normalizedFrom, fileContents, 2);
|
|
12770
|
-
const normalizedTo = normalizePath(ref.to, projectRoot);
|
|
12771
|
-
if (reachable.has(normalizedTo)) {
|
|
12772
|
-
chainValidated = true;
|
|
12773
|
-
break;
|
|
12774
|
-
}
|
|
12775
|
-
}
|
|
12776
|
-
}
|
|
12777
|
-
if (chainValidated) {
|
|
12778
|
-
validated.push({ ...finding, validatedBy: "heuristic" });
|
|
12779
|
-
} else {
|
|
12780
|
-
validated.push({
|
|
12781
|
-
...finding,
|
|
12782
|
-
severity: DOWNGRADE_MAP[finding.severity],
|
|
12783
|
-
validatedBy: "heuristic"
|
|
12784
|
-
});
|
|
12785
|
-
}
|
|
12786
|
-
}
|
|
12941
|
+
const result = await processFinding(finding, exclusionSet, graph, projectRoot, fileContents);
|
|
12942
|
+
if (result !== null) validated.push(result);
|
|
12787
12943
|
}
|
|
12788
12944
|
return validated;
|
|
12789
12945
|
}
|
|
@@ -13384,25 +13540,32 @@ function serializeRoadmap(roadmap) {
|
|
|
13384
13540
|
function serializeMilestoneHeading(milestone) {
|
|
13385
13541
|
return milestone.isBacklog ? "## Backlog" : `## ${milestone.name}`;
|
|
13386
13542
|
}
|
|
13543
|
+
function orDash(value) {
|
|
13544
|
+
return value ?? EM_DASH2;
|
|
13545
|
+
}
|
|
13546
|
+
function listOrDash(items) {
|
|
13547
|
+
return items.length > 0 ? items.join(", ") : EM_DASH2;
|
|
13548
|
+
}
|
|
13549
|
+
function serializeExtendedLines(feature) {
|
|
13550
|
+
const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
|
|
13551
|
+
if (!hasExtended) return [];
|
|
13552
|
+
return [
|
|
13553
|
+
`- **Assignee:** ${orDash(feature.assignee)}`,
|
|
13554
|
+
`- **Priority:** ${orDash(feature.priority)}`,
|
|
13555
|
+
`- **External-ID:** ${orDash(feature.externalId)}`
|
|
13556
|
+
];
|
|
13557
|
+
}
|
|
13387
13558
|
function serializeFeature(feature) {
|
|
13388
|
-
const spec = feature.spec ?? EM_DASH2;
|
|
13389
|
-
const plans = feature.plans.length > 0 ? feature.plans.join(", ") : EM_DASH2;
|
|
13390
|
-
const blockedBy = feature.blockedBy.length > 0 ? feature.blockedBy.join(", ") : EM_DASH2;
|
|
13391
13559
|
const lines = [
|
|
13392
13560
|
`### ${feature.name}`,
|
|
13393
13561
|
"",
|
|
13394
13562
|
`- **Status:** ${feature.status}`,
|
|
13395
|
-
`- **Spec:** ${spec}`,
|
|
13563
|
+
`- **Spec:** ${orDash(feature.spec)}`,
|
|
13396
13564
|
`- **Summary:** ${feature.summary}`,
|
|
13397
|
-
`- **Blockers:** ${blockedBy}`,
|
|
13398
|
-
`- **Plan:** ${plans}
|
|
13565
|
+
`- **Blockers:** ${listOrDash(feature.blockedBy)}`,
|
|
13566
|
+
`- **Plan:** ${listOrDash(feature.plans)}`,
|
|
13567
|
+
...serializeExtendedLines(feature)
|
|
13399
13568
|
];
|
|
13400
|
-
const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
|
|
13401
|
-
if (hasExtended) {
|
|
13402
|
-
lines.push(`- **Assignee:** ${feature.assignee ?? EM_DASH2}`);
|
|
13403
|
-
lines.push(`- **Priority:** ${feature.priority ?? EM_DASH2}`);
|
|
13404
|
-
lines.push(`- **External-ID:** ${feature.externalId ?? EM_DASH2}`);
|
|
13405
|
-
}
|
|
13406
13569
|
return lines;
|
|
13407
13570
|
}
|
|
13408
13571
|
function serializeAssignmentHistory(records) {
|
|
@@ -13436,6 +13599,26 @@ function isRegression(from, to) {
|
|
|
13436
13599
|
}
|
|
13437
13600
|
|
|
13438
13601
|
// src/roadmap/sync.ts
|
|
13602
|
+
function collectAutopilotStatuses(autopilotPath, featurePlans, allTaskStatuses) {
|
|
13603
|
+
try {
|
|
13604
|
+
const raw = fs24.readFileSync(autopilotPath, "utf-8");
|
|
13605
|
+
const autopilot = JSON.parse(raw);
|
|
13606
|
+
if (!autopilot.phases) return;
|
|
13607
|
+
const linkedPhases = autopilot.phases.filter(
|
|
13608
|
+
(phase) => phase.planPath ? featurePlans.some((p) => p === phase.planPath || phase.planPath.endsWith(p)) : false
|
|
13609
|
+
);
|
|
13610
|
+
for (const phase of linkedPhases) {
|
|
13611
|
+
if (phase.status === "complete") {
|
|
13612
|
+
allTaskStatuses.push("complete");
|
|
13613
|
+
} else if (phase.status === "pending") {
|
|
13614
|
+
allTaskStatuses.push("pending");
|
|
13615
|
+
} else {
|
|
13616
|
+
allTaskStatuses.push("in_progress");
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
13619
|
+
} catch {
|
|
13620
|
+
}
|
|
13621
|
+
}
|
|
13439
13622
|
function inferStatus(feature, projectPath, allFeatures) {
|
|
13440
13623
|
if (feature.blockedBy.length > 0) {
|
|
13441
13624
|
const blockerNotDone = feature.blockedBy.some((blockerName) => {
|
|
@@ -13471,26 +13654,7 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
13471
13654
|
if (!entry.isDirectory()) continue;
|
|
13472
13655
|
const autopilotPath = path24.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
13473
13656
|
if (!fs24.existsSync(autopilotPath)) continue;
|
|
13474
|
-
|
|
13475
|
-
const raw = fs24.readFileSync(autopilotPath, "utf-8");
|
|
13476
|
-
const autopilot = JSON.parse(raw);
|
|
13477
|
-
if (!autopilot.phases) continue;
|
|
13478
|
-
const linkedPhases = autopilot.phases.filter(
|
|
13479
|
-
(phase) => phase.planPath ? feature.plans.some((p) => p === phase.planPath || phase.planPath.endsWith(p)) : false
|
|
13480
|
-
);
|
|
13481
|
-
if (linkedPhases.length > 0) {
|
|
13482
|
-
for (const phase of linkedPhases) {
|
|
13483
|
-
if (phase.status === "complete") {
|
|
13484
|
-
allTaskStatuses.push("complete");
|
|
13485
|
-
} else if (phase.status === "pending") {
|
|
13486
|
-
allTaskStatuses.push("pending");
|
|
13487
|
-
} else {
|
|
13488
|
-
allTaskStatuses.push("in_progress");
|
|
13489
|
-
}
|
|
13490
|
-
}
|
|
13491
|
-
}
|
|
13492
|
-
} catch {
|
|
13493
|
-
}
|
|
13657
|
+
collectAutopilotStatuses(autopilotPath, feature.plans, allTaskStatuses);
|
|
13494
13658
|
}
|
|
13495
13659
|
} catch {
|
|
13496
13660
|
}
|
|
@@ -13807,23 +13971,36 @@ var GitHubIssuesSyncAdapter = class {
|
|
|
13807
13971
|
return (0, import_types25.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
13808
13972
|
}
|
|
13809
13973
|
}
|
|
13974
|
+
buildLabelsParam() {
|
|
13975
|
+
const filterLabels = this.config.labels ?? [];
|
|
13976
|
+
return filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
|
|
13977
|
+
}
|
|
13978
|
+
issueToTicketState(issue) {
|
|
13979
|
+
return {
|
|
13980
|
+
externalId: buildExternalId(this.owner, this.repo, issue.number),
|
|
13981
|
+
title: issue.title,
|
|
13982
|
+
status: issue.state,
|
|
13983
|
+
labels: issue.labels.map((l) => l.name),
|
|
13984
|
+
assignee: issue.assignee ? `@${issue.assignee.login}` : null
|
|
13985
|
+
};
|
|
13986
|
+
}
|
|
13987
|
+
async fetchIssuePage(page, labelsParam) {
|
|
13988
|
+
const perPage = 100;
|
|
13989
|
+
return fetchWithRetry(
|
|
13990
|
+
this.fetchFn,
|
|
13991
|
+
`${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
|
|
13992
|
+
{ method: "GET", headers: this.headers() },
|
|
13993
|
+
this.retryOpts
|
|
13994
|
+
);
|
|
13995
|
+
}
|
|
13810
13996
|
async fetchAllTickets() {
|
|
13811
13997
|
try {
|
|
13812
|
-
const
|
|
13813
|
-
const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
|
|
13998
|
+
const labelsParam = this.buildLabelsParam();
|
|
13814
13999
|
const tickets = [];
|
|
13815
14000
|
let page = 1;
|
|
13816
14001
|
const perPage = 100;
|
|
13817
14002
|
while (true) {
|
|
13818
|
-
const response = await
|
|
13819
|
-
this.fetchFn,
|
|
13820
|
-
`${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
|
|
13821
|
-
{
|
|
13822
|
-
method: "GET",
|
|
13823
|
-
headers: this.headers()
|
|
13824
|
-
},
|
|
13825
|
-
this.retryOpts
|
|
13826
|
-
);
|
|
14003
|
+
const response = await this.fetchIssuePage(page, labelsParam);
|
|
13827
14004
|
if (!response.ok) {
|
|
13828
14005
|
const text = await response.text();
|
|
13829
14006
|
return (0, import_types25.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
|
|
@@ -13831,13 +14008,7 @@ var GitHubIssuesSyncAdapter = class {
|
|
|
13831
14008
|
const data = await response.json();
|
|
13832
14009
|
const issues = data.filter((d) => !d.pull_request);
|
|
13833
14010
|
for (const issue of issues) {
|
|
13834
|
-
tickets.push(
|
|
13835
|
-
externalId: buildExternalId(this.owner, this.repo, issue.number),
|
|
13836
|
-
title: issue.title,
|
|
13837
|
-
status: issue.state,
|
|
13838
|
-
labels: issue.labels.map((l) => l.name),
|
|
13839
|
-
assignee: issue.assignee ? `@${issue.assignee.login}` : null
|
|
13840
|
-
});
|
|
14011
|
+
tickets.push(this.issueToTicketState(issue));
|
|
13841
14012
|
}
|
|
13842
14013
|
if (data.length < perPage) break;
|
|
13843
14014
|
page++;
|
|
@@ -13932,6 +14103,22 @@ async function syncToExternal(roadmap, adapter, config, prefetchedTickets) {
|
|
|
13932
14103
|
}
|
|
13933
14104
|
return result;
|
|
13934
14105
|
}
|
|
14106
|
+
function applyTicketToFeature(ticketState, feature, config, forceSync, result) {
|
|
14107
|
+
if (ticketState.assignee !== feature.assignee) {
|
|
14108
|
+
result.assignmentChanges.push({
|
|
14109
|
+
feature: feature.name,
|
|
14110
|
+
from: feature.assignee,
|
|
14111
|
+
to: ticketState.assignee
|
|
14112
|
+
});
|
|
14113
|
+
feature.assignee = ticketState.assignee;
|
|
14114
|
+
}
|
|
14115
|
+
const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
|
|
14116
|
+
if (!resolvedStatus || resolvedStatus === feature.status) return;
|
|
14117
|
+
const newStatus = resolvedStatus;
|
|
14118
|
+
if (!forceSync && isRegression(feature.status, newStatus)) return;
|
|
14119
|
+
if (!forceSync && feature.status === "blocked" && newStatus === "planned") return;
|
|
14120
|
+
feature.status = newStatus;
|
|
14121
|
+
}
|
|
13935
14122
|
async function syncFromExternal(roadmap, adapter, config, options, prefetchedTickets) {
|
|
13936
14123
|
const result = emptySyncResult();
|
|
13937
14124
|
const forceSync = options?.forceSync ?? false;
|
|
@@ -13958,25 +14145,7 @@ async function syncFromExternal(roadmap, adapter, config, options, prefetchedTic
|
|
|
13958
14145
|
for (const ticketState of tickets) {
|
|
13959
14146
|
const feature = featureByExternalId.get(ticketState.externalId);
|
|
13960
14147
|
if (!feature) continue;
|
|
13961
|
-
|
|
13962
|
-
result.assignmentChanges.push({
|
|
13963
|
-
feature: feature.name,
|
|
13964
|
-
from: feature.assignee,
|
|
13965
|
-
to: ticketState.assignee
|
|
13966
|
-
});
|
|
13967
|
-
feature.assignee = ticketState.assignee;
|
|
13968
|
-
}
|
|
13969
|
-
const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
|
|
13970
|
-
if (resolvedStatus && resolvedStatus !== feature.status) {
|
|
13971
|
-
const newStatus = resolvedStatus;
|
|
13972
|
-
if (!forceSync && isRegression(feature.status, newStatus)) {
|
|
13973
|
-
continue;
|
|
13974
|
-
}
|
|
13975
|
-
if (!forceSync && feature.status === "blocked" && newStatus === "planned") {
|
|
13976
|
-
continue;
|
|
13977
|
-
}
|
|
13978
|
-
feature.status = newStatus;
|
|
13979
|
-
}
|
|
14148
|
+
applyTicketToFeature(ticketState, feature, config, forceSync, result);
|
|
13980
14149
|
}
|
|
13981
14150
|
return result;
|
|
13982
14151
|
}
|
|
@@ -14024,6 +14193,24 @@ var PRIORITY_RANK = {
|
|
|
14024
14193
|
var POSITION_WEIGHT = 0.5;
|
|
14025
14194
|
var DEPENDENTS_WEIGHT = 0.3;
|
|
14026
14195
|
var AFFINITY_WEIGHT = 0.2;
|
|
14196
|
+
function isEligibleCandidate(feature, allFeatureNames, doneFeatures) {
|
|
14197
|
+
if (feature.status !== "planned" && feature.status !== "backlog") return false;
|
|
14198
|
+
const isBlocked = feature.blockedBy.some((blocker) => {
|
|
14199
|
+
const key = blocker.toLowerCase();
|
|
14200
|
+
return allFeatureNames.has(key) && !doneFeatures.has(key);
|
|
14201
|
+
});
|
|
14202
|
+
return !isBlocked;
|
|
14203
|
+
}
|
|
14204
|
+
function computeAffinityScore(feature, milestoneName, milestoneMap, userCompletedFeatures) {
|
|
14205
|
+
if (userCompletedFeatures.size === 0) return 0;
|
|
14206
|
+
const completedBlocker = feature.blockedBy.some(
|
|
14207
|
+
(b) => userCompletedFeatures.has(b.toLowerCase())
|
|
14208
|
+
);
|
|
14209
|
+
if (completedBlocker) return 1;
|
|
14210
|
+
const siblings = milestoneMap.get(milestoneName) ?? [];
|
|
14211
|
+
const completedSibling = siblings.some((s) => userCompletedFeatures.has(s));
|
|
14212
|
+
return completedSibling ? 0.5 : 0;
|
|
14213
|
+
}
|
|
14027
14214
|
function scoreRoadmapCandidates(roadmap, options) {
|
|
14028
14215
|
const allFeatures = roadmap.milestones.flatMap((m) => m.features);
|
|
14029
14216
|
const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
|
|
@@ -14062,33 +14249,18 @@ function scoreRoadmapCandidates(roadmap, options) {
|
|
|
14062
14249
|
const candidates = [];
|
|
14063
14250
|
let globalPosition = 0;
|
|
14064
14251
|
for (const ms of roadmap.milestones) {
|
|
14065
|
-
for (
|
|
14066
|
-
const feature = ms.features[featureIdx];
|
|
14252
|
+
for (const feature of ms.features) {
|
|
14067
14253
|
globalPosition++;
|
|
14068
|
-
if (feature
|
|
14069
|
-
const isBlocked = feature.blockedBy.some((blocker) => {
|
|
14070
|
-
const key = blocker.toLowerCase();
|
|
14071
|
-
return allFeatureNames.has(key) && !doneFeatures.has(key);
|
|
14072
|
-
});
|
|
14073
|
-
if (isBlocked) continue;
|
|
14254
|
+
if (!isEligibleCandidate(feature, allFeatureNames, doneFeatures)) continue;
|
|
14074
14255
|
const positionScore = 1 - (globalPosition - 1) / totalPositions;
|
|
14075
14256
|
const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
|
|
14076
14257
|
const dependentsScore = deps / maxDependents;
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
affinityScore = 1;
|
|
14084
|
-
} else {
|
|
14085
|
-
const siblings = milestoneMap.get(ms.name) ?? [];
|
|
14086
|
-
const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
|
|
14087
|
-
if (completedSiblings.length > 0) {
|
|
14088
|
-
affinityScore = 0.5;
|
|
14089
|
-
}
|
|
14090
|
-
}
|
|
14091
|
-
}
|
|
14258
|
+
const affinityScore = computeAffinityScore(
|
|
14259
|
+
feature,
|
|
14260
|
+
ms.name,
|
|
14261
|
+
milestoneMap,
|
|
14262
|
+
userCompletedFeatures
|
|
14263
|
+
);
|
|
14092
14264
|
const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
|
|
14093
14265
|
const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
|
|
14094
14266
|
candidates.push({
|
|
@@ -15150,6 +15322,27 @@ function aggregateByDay(records) {
|
|
|
15150
15322
|
// src/usage/jsonl-reader.ts
|
|
15151
15323
|
var fs30 = __toESM(require("fs"));
|
|
15152
15324
|
var path29 = __toESM(require("path"));
|
|
15325
|
+
function extractTokenUsage(entry, lineNumber) {
|
|
15326
|
+
const tokenUsage = entry.token_usage;
|
|
15327
|
+
if (!tokenUsage || typeof tokenUsage !== "object") {
|
|
15328
|
+
console.warn(
|
|
15329
|
+
`[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
|
|
15330
|
+
);
|
|
15331
|
+
return null;
|
|
15332
|
+
}
|
|
15333
|
+
return tokenUsage;
|
|
15334
|
+
}
|
|
15335
|
+
function applyOptionalFields2(record, entry) {
|
|
15336
|
+
if (entry.cache_creation_tokens != null) {
|
|
15337
|
+
record.cacheCreationTokens = entry.cache_creation_tokens;
|
|
15338
|
+
}
|
|
15339
|
+
if (entry.cache_read_tokens != null) {
|
|
15340
|
+
record.cacheReadTokens = entry.cache_read_tokens;
|
|
15341
|
+
}
|
|
15342
|
+
if (entry.model != null) {
|
|
15343
|
+
record.model = entry.model;
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15153
15346
|
function parseLine(line, lineNumber) {
|
|
15154
15347
|
let entry;
|
|
15155
15348
|
try {
|
|
@@ -15158,13 +15351,8 @@ function parseLine(line, lineNumber) {
|
|
|
15158
15351
|
console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
|
|
15159
15352
|
return null;
|
|
15160
15353
|
}
|
|
15161
|
-
const tokenUsage = entry
|
|
15162
|
-
if (!tokenUsage
|
|
15163
|
-
console.warn(
|
|
15164
|
-
`[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
|
|
15165
|
-
);
|
|
15166
|
-
return null;
|
|
15167
|
-
}
|
|
15354
|
+
const tokenUsage = extractTokenUsage(entry, lineNumber);
|
|
15355
|
+
if (!tokenUsage) return null;
|
|
15168
15356
|
const inputTokens = tokenUsage.input_tokens ?? 0;
|
|
15169
15357
|
const outputTokens = tokenUsage.output_tokens ?? 0;
|
|
15170
15358
|
const record = {
|
|
@@ -15176,15 +15364,7 @@ function parseLine(line, lineNumber) {
|
|
|
15176
15364
|
totalTokens: inputTokens + outputTokens
|
|
15177
15365
|
}
|
|
15178
15366
|
};
|
|
15179
|
-
|
|
15180
|
-
record.cacheCreationTokens = entry.cache_creation_tokens;
|
|
15181
|
-
}
|
|
15182
|
-
if (entry.cache_read_tokens != null) {
|
|
15183
|
-
record.cacheReadTokens = entry.cache_read_tokens;
|
|
15184
|
-
}
|
|
15185
|
-
if (entry.model != null) {
|
|
15186
|
-
record.model = entry.model;
|
|
15187
|
-
}
|
|
15367
|
+
applyOptionalFields2(record, entry);
|
|
15188
15368
|
return record;
|
|
15189
15369
|
}
|
|
15190
15370
|
function readCostRecords(projectRoot) {
|
|
@@ -15219,6 +15399,14 @@ function extractUsage(entry) {
|
|
|
15219
15399
|
const usage = message.usage;
|
|
15220
15400
|
return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
|
|
15221
15401
|
}
|
|
15402
|
+
function applyOptionalCCFields(record, message, usage) {
|
|
15403
|
+
const model = message.model;
|
|
15404
|
+
if (model) record.model = model;
|
|
15405
|
+
const cacheCreate = usage.cache_creation_input_tokens;
|
|
15406
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
15407
|
+
if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
|
|
15408
|
+
if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
|
|
15409
|
+
}
|
|
15222
15410
|
function buildRecord(entry, usage) {
|
|
15223
15411
|
const inputTokens = Number(usage.input_tokens) || 0;
|
|
15224
15412
|
const outputTokens = Number(usage.output_tokens) || 0;
|
|
@@ -15229,12 +15417,7 @@ function buildRecord(entry, usage) {
|
|
|
15229
15417
|
tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
15230
15418
|
_source: "claude-code"
|
|
15231
15419
|
};
|
|
15232
|
-
|
|
15233
|
-
if (model) record.model = model;
|
|
15234
|
-
const cacheCreate = usage.cache_creation_input_tokens;
|
|
15235
|
-
const cacheRead = usage.cache_read_input_tokens;
|
|
15236
|
-
if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
|
|
15237
|
-
if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
|
|
15420
|
+
applyOptionalCCFields(record, message, usage);
|
|
15238
15421
|
return record;
|
|
15239
15422
|
}
|
|
15240
15423
|
function parseCCLine(line, filePath, lineNumber) {
|
|
@@ -15302,7 +15485,7 @@ function parseCCRecords() {
|
|
|
15302
15485
|
}
|
|
15303
15486
|
|
|
15304
15487
|
// src/index.ts
|
|
15305
|
-
var VERSION = "0.
|
|
15488
|
+
var VERSION = "0.21.1";
|
|
15306
15489
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15307
15490
|
0 && (module.exports = {
|
|
15308
15491
|
AGENT_DESCRIPTORS,
|
|
@@ -15333,7 +15516,6 @@ var VERSION = "0.15.0";
|
|
|
15333
15516
|
ConfirmationSchema,
|
|
15334
15517
|
ConsoleSink,
|
|
15335
15518
|
ConstraintRuleSchema,
|
|
15336
|
-
ContentPipeline,
|
|
15337
15519
|
ContributingFeatureSchema,
|
|
15338
15520
|
ContributionsSchema,
|
|
15339
15521
|
CouplingCollector,
|