@harness-engineering/core 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/architecture/matchers.js +267 -223
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-ZHGBWFYD.mjs → chunk-BQUWXBGR.mjs} +262 -217
- package/dist/index.d.mts +177 -107
- package/dist/index.d.ts +177 -107
- package/dist/index.js +1905 -1451
- package/dist/index.mjs +1608 -1208
- package/dist/{matchers-Dj1t5vpg.d.mts → matchers-D20x48U9.d.mts} +46 -46
- package/dist/{matchers-Dj1t5vpg.d.ts → matchers-D20x48U9.d.ts} +46 -46
- package/package.json +3 -3
|
@@ -110,14 +110,10 @@ var ConstraintRuleSchema = import_zod.z.object({
|
|
|
110
110
|
// forward-compat for governs edges
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
// src/architecture/collectors/circular-deps.ts
|
|
114
|
-
var import_node_path = require("path");
|
|
115
|
-
|
|
116
113
|
// src/architecture/collectors/hash.ts
|
|
117
114
|
var import_node_crypto = require("crypto");
|
|
118
115
|
function violationId(relativePath, category, normalizedDetail) {
|
|
119
|
-
const
|
|
120
|
-
const input = `${path}:${category}:${normalizedDetail}`;
|
|
116
|
+
const input = `${relativePath}:${category}:${normalizedDetail}`;
|
|
121
117
|
return (0, import_node_crypto.createHash)("sha256").update(input).digest("hex");
|
|
122
118
|
}
|
|
123
119
|
function constraintRuleId(category, scope, description) {
|
|
@@ -149,12 +145,16 @@ function resolveFileToLayer(file, layers) {
|
|
|
149
145
|
// src/shared/fs-utils.ts
|
|
150
146
|
var import_fs = require("fs");
|
|
151
147
|
var import_util = require("util");
|
|
148
|
+
var import_node_path = require("path");
|
|
152
149
|
var import_glob = require("glob");
|
|
153
150
|
var accessAsync = (0, import_util.promisify)(import_fs.access);
|
|
154
151
|
var readFileAsync = (0, import_util.promisify)(import_fs.readFile);
|
|
155
152
|
async function findFiles(pattern, cwd = process.cwd()) {
|
|
156
153
|
return (0, import_glob.glob)(pattern, { cwd, absolute: true });
|
|
157
154
|
}
|
|
155
|
+
function relativePosix(from, to) {
|
|
156
|
+
return (0, import_node_path.relative)(from, to).replaceAll("\\", "/");
|
|
157
|
+
}
|
|
158
158
|
|
|
159
159
|
// src/constraints/dependencies.ts
|
|
160
160
|
var import_path = require("path");
|
|
@@ -209,8 +209,8 @@ async function buildDependencyGraph(files, parser, graphDependencyData) {
|
|
|
209
209
|
function checkLayerViolations(graph, layers, rootDir) {
|
|
210
210
|
const violations = [];
|
|
211
211
|
for (const edge of graph.edges) {
|
|
212
|
-
const fromRelative = (
|
|
213
|
-
const toRelative = (
|
|
212
|
+
const fromRelative = relativePosix(rootDir, edge.from);
|
|
213
|
+
const toRelative = relativePosix(rootDir, edge.to);
|
|
214
214
|
const fromLayer = resolveFileToLayer(fromRelative, layers);
|
|
215
215
|
const toLayer = resolveFileToLayer(toRelative, layers);
|
|
216
216
|
if (!fromLayer || !toLayer) continue;
|
|
@@ -294,65 +294,71 @@ async function validateDependencies(config) {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
// src/constraints/circular-deps.ts
|
|
297
|
-
function
|
|
298
|
-
const nodeMap = /* @__PURE__ */ new Map();
|
|
299
|
-
const stack = [];
|
|
300
|
-
const sccs = [];
|
|
301
|
-
let index = 0;
|
|
297
|
+
function buildAdjacencyList(graph) {
|
|
302
298
|
const adjacency = /* @__PURE__ */ new Map();
|
|
299
|
+
const nodeSet = new Set(graph.nodes);
|
|
303
300
|
for (const node of graph.nodes) {
|
|
304
301
|
adjacency.set(node, []);
|
|
305
302
|
}
|
|
306
303
|
for (const edge of graph.edges) {
|
|
307
304
|
const neighbors = adjacency.get(edge.from);
|
|
308
|
-
if (neighbors &&
|
|
305
|
+
if (neighbors && nodeSet.has(edge.to)) {
|
|
309
306
|
neighbors.push(edge.to);
|
|
310
307
|
}
|
|
311
308
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
309
|
+
return adjacency;
|
|
310
|
+
}
|
|
311
|
+
function isCyclicSCC(scc, adjacency) {
|
|
312
|
+
if (scc.length > 1) return true;
|
|
313
|
+
if (scc.length === 1) {
|
|
314
|
+
const selfNode = scc[0];
|
|
315
|
+
const selfNeighbors = adjacency.get(selfNode) ?? [];
|
|
316
|
+
return selfNeighbors.includes(selfNode);
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
function processNeighbors(node, neighbors, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
321
|
+
for (const neighbor of neighbors) {
|
|
322
|
+
const neighborData = nodeMap.get(neighbor);
|
|
323
|
+
if (!neighborData) {
|
|
324
|
+
strongConnectImpl(neighbor, nodeMap, stack, adjacency, sccs, indexRef);
|
|
325
|
+
const nodeData = nodeMap.get(node);
|
|
326
|
+
const updatedNeighborData = nodeMap.get(neighbor);
|
|
327
|
+
nodeData.lowlink = Math.min(nodeData.lowlink, updatedNeighborData.lowlink);
|
|
328
|
+
} else if (neighborData.onStack) {
|
|
329
|
+
const nodeData = nodeMap.get(node);
|
|
330
|
+
nodeData.lowlink = Math.min(nodeData.lowlink, neighborData.index);
|
|
332
331
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
335
|
+
nodeMap.set(node, { index: indexRef.value, lowlink: indexRef.value, onStack: true });
|
|
336
|
+
indexRef.value++;
|
|
337
|
+
stack.push(node);
|
|
338
|
+
processNeighbors(node, adjacency.get(node) ?? [], nodeMap, stack, adjacency, sccs, indexRef);
|
|
339
|
+
const nodeData = nodeMap.get(node);
|
|
340
|
+
if (nodeData.lowlink === nodeData.index) {
|
|
341
|
+
const scc = [];
|
|
342
|
+
let w;
|
|
343
|
+
do {
|
|
344
|
+
w = stack.pop();
|
|
345
|
+
nodeMap.get(w).onStack = false;
|
|
346
|
+
scc.push(w);
|
|
347
|
+
} while (w !== node);
|
|
348
|
+
if (isCyclicSCC(scc, adjacency)) {
|
|
349
|
+
sccs.push(scc);
|
|
351
350
|
}
|
|
352
351
|
}
|
|
352
|
+
}
|
|
353
|
+
function tarjanSCC(graph) {
|
|
354
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
355
|
+
const stack = [];
|
|
356
|
+
const sccs = [];
|
|
357
|
+
const indexRef = { value: 0 };
|
|
358
|
+
const adjacency = buildAdjacencyList(graph);
|
|
353
359
|
for (const node of graph.nodes) {
|
|
354
360
|
if (!nodeMap.has(node)) {
|
|
355
|
-
|
|
361
|
+
strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef);
|
|
356
362
|
}
|
|
357
363
|
}
|
|
358
364
|
return sccs;
|
|
@@ -433,8 +439,8 @@ var CircularDepsCollector = class {
|
|
|
433
439
|
}
|
|
434
440
|
const { cycles, largestCycle } = result.value;
|
|
435
441
|
const violations = cycles.map((cycle) => {
|
|
436
|
-
const cyclePath = cycle.cycle.map((f) => (
|
|
437
|
-
const firstFile = (
|
|
442
|
+
const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
|
|
443
|
+
const firstFile = relativePosix(rootDir, cycle.cycle[0]);
|
|
438
444
|
return {
|
|
439
445
|
id: violationId(firstFile, this.category, cyclePath),
|
|
440
446
|
file: firstFile,
|
|
@@ -455,7 +461,6 @@ var CircularDepsCollector = class {
|
|
|
455
461
|
};
|
|
456
462
|
|
|
457
463
|
// src/architecture/collectors/layer-violations.ts
|
|
458
|
-
var import_node_path2 = require("path");
|
|
459
464
|
var LayerViolationCollector = class {
|
|
460
465
|
category = "layer-violations";
|
|
461
466
|
getRules(_config, _rootDir) {
|
|
@@ -499,8 +504,8 @@ var LayerViolationCollector = class {
|
|
|
499
504
|
(v) => v.reason === "WRONG_LAYER"
|
|
500
505
|
);
|
|
501
506
|
const violations = layerViolations.map((v) => {
|
|
502
|
-
const relFile = (
|
|
503
|
-
const relImport = (
|
|
507
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
508
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
504
509
|
const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
|
|
505
510
|
return {
|
|
506
511
|
id: violationId(relFile, this.category, detail),
|
|
@@ -524,11 +529,11 @@ var LayerViolationCollector = class {
|
|
|
524
529
|
// src/architecture/baseline-manager.ts
|
|
525
530
|
var import_node_fs = require("fs");
|
|
526
531
|
var import_node_crypto2 = require("crypto");
|
|
527
|
-
var
|
|
532
|
+
var import_node_path2 = require("path");
|
|
528
533
|
var ArchBaselineManager = class {
|
|
529
534
|
baselinesPath;
|
|
530
535
|
constructor(projectRoot, baselinePath) {
|
|
531
|
-
this.baselinesPath = baselinePath ? (0,
|
|
536
|
+
this.baselinesPath = baselinePath ? (0, import_node_path2.join)(projectRoot, baselinePath) : (0, import_node_path2.join)(projectRoot, ".harness", "arch", "baselines.json");
|
|
532
537
|
}
|
|
533
538
|
/**
|
|
534
539
|
* Snapshot the current metric results into an ArchBaseline.
|
|
@@ -589,7 +594,7 @@ var ArchBaselineManager = class {
|
|
|
589
594
|
* Uses atomic write (write to temp file, then rename) to prevent corruption.
|
|
590
595
|
*/
|
|
591
596
|
save(baseline) {
|
|
592
|
-
const dir = (0,
|
|
597
|
+
const dir = (0, import_node_path2.dirname)(this.baselinesPath);
|
|
593
598
|
if (!(0, import_node_fs.existsSync)(dir)) {
|
|
594
599
|
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
595
600
|
}
|
|
@@ -616,6 +621,31 @@ function aggregateByCategory(results) {
|
|
|
616
621
|
}
|
|
617
622
|
return map;
|
|
618
623
|
}
|
|
624
|
+
function classifyViolations(violations, baselineViolationIds) {
|
|
625
|
+
const newViolations = [];
|
|
626
|
+
const preExisting = [];
|
|
627
|
+
for (const violation of violations) {
|
|
628
|
+
if (baselineViolationIds.has(violation.id)) {
|
|
629
|
+
preExisting.push(violation.id);
|
|
630
|
+
} else {
|
|
631
|
+
newViolations.push(violation);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return { newViolations, preExisting };
|
|
635
|
+
}
|
|
636
|
+
function findResolvedViolations(baselineCategory, currentViolationIds) {
|
|
637
|
+
if (!baselineCategory) return [];
|
|
638
|
+
return baselineCategory.violationIds.filter((id) => !currentViolationIds.has(id));
|
|
639
|
+
}
|
|
640
|
+
function collectOrphanedBaselineViolations(baseline, visitedCategories) {
|
|
641
|
+
const resolved = [];
|
|
642
|
+
for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
|
|
643
|
+
if (!visitedCategories.has(category) && baselineCategory) {
|
|
644
|
+
resolved.push(...baselineCategory.violationIds);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return resolved;
|
|
648
|
+
}
|
|
619
649
|
function diff(current, baseline) {
|
|
620
650
|
const aggregated = aggregateByCategory(current);
|
|
621
651
|
const newViolations = [];
|
|
@@ -628,21 +658,11 @@ function diff(current, baseline) {
|
|
|
628
658
|
const baselineCategory = baseline.metrics[category];
|
|
629
659
|
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
630
660
|
const baselineValue = baselineCategory?.value ?? 0;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
} else {
|
|
635
|
-
newViolations.push(violation);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
661
|
+
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
662
|
+
newViolations.push(...classified.newViolations);
|
|
663
|
+
preExisting.push(...classified.preExisting);
|
|
638
664
|
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
639
|
-
|
|
640
|
-
for (const id of baselineCategory.violationIds) {
|
|
641
|
-
if (!currentViolationIds.has(id)) {
|
|
642
|
-
resolvedViolations.push(id);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
665
|
+
resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
646
666
|
if (baselineCategory && agg.value > baselineValue) {
|
|
647
667
|
regressions.push({
|
|
648
668
|
category,
|
|
@@ -652,16 +672,9 @@ function diff(current, baseline) {
|
|
|
652
672
|
});
|
|
653
673
|
}
|
|
654
674
|
}
|
|
655
|
-
|
|
656
|
-
if (!visitedCategories.has(category) && baselineCategory) {
|
|
657
|
-
for (const id of baselineCategory.violationIds) {
|
|
658
|
-
resolvedViolations.push(id);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
const passed = newViolations.length === 0 && regressions.length === 0;
|
|
675
|
+
resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
663
676
|
return {
|
|
664
|
-
passed,
|
|
677
|
+
passed: newViolations.length === 0 && regressions.length === 0,
|
|
665
678
|
newViolations,
|
|
666
679
|
resolvedViolations,
|
|
667
680
|
preExisting,
|
|
@@ -669,9 +682,6 @@ function diff(current, baseline) {
|
|
|
669
682
|
};
|
|
670
683
|
}
|
|
671
684
|
|
|
672
|
-
// src/architecture/collectors/complexity.ts
|
|
673
|
-
var import_node_path4 = require("path");
|
|
674
|
-
|
|
675
685
|
// src/entropy/detectors/complexity.ts
|
|
676
686
|
var import_promises = require("fs/promises");
|
|
677
687
|
var DEFAULT_THRESHOLDS = {
|
|
@@ -682,22 +692,22 @@ var DEFAULT_THRESHOLDS = {
|
|
|
682
692
|
fileLength: { info: 300 },
|
|
683
693
|
hotspotPercentile: { error: 95 }
|
|
684
694
|
};
|
|
695
|
+
var FUNCTION_PATTERNS = [
|
|
696
|
+
// function declarations: function name(params) {
|
|
697
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
698
|
+
// method declarations: name(params) {
|
|
699
|
+
/^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
700
|
+
// arrow functions assigned to const/let/var: const name = (params) =>
|
|
701
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
702
|
+
// arrow functions assigned to const/let/var with single param: const name = param =>
|
|
703
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
|
|
704
|
+
];
|
|
685
705
|
function extractFunctions(content) {
|
|
686
706
|
const functions = [];
|
|
687
707
|
const lines = content.split("\n");
|
|
688
|
-
const patterns = [
|
|
689
|
-
// function declarations: function name(params) {
|
|
690
|
-
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
691
|
-
// method declarations: name(params) {
|
|
692
|
-
/^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
693
|
-
// arrow functions assigned to const/let/var: const name = (params) =>
|
|
694
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
695
|
-
// arrow functions assigned to const/let/var with single param: const name = param =>
|
|
696
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
|
|
697
|
-
];
|
|
698
708
|
for (let i = 0; i < lines.length; i++) {
|
|
699
709
|
const line = lines[i];
|
|
700
|
-
for (const pattern of
|
|
710
|
+
for (const pattern of FUNCTION_PATTERNS) {
|
|
701
711
|
const match = line.match(pattern);
|
|
702
712
|
if (match) {
|
|
703
713
|
const name = match[1] ?? "anonymous";
|
|
@@ -786,26 +796,155 @@ function computeNestingDepth(body) {
|
|
|
786
796
|
}
|
|
787
797
|
return maxDepth;
|
|
788
798
|
}
|
|
789
|
-
|
|
790
|
-
const
|
|
791
|
-
|
|
799
|
+
function resolveThresholds(config) {
|
|
800
|
+
const userThresholds = config?.thresholds;
|
|
801
|
+
if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
|
|
802
|
+
return {
|
|
792
803
|
cyclomaticComplexity: {
|
|
793
|
-
|
|
794
|
-
|
|
804
|
+
...DEFAULT_THRESHOLDS.cyclomaticComplexity,
|
|
805
|
+
...stripUndefined(userThresholds.cyclomaticComplexity)
|
|
795
806
|
},
|
|
796
807
|
nestingDepth: {
|
|
797
|
-
|
|
808
|
+
...DEFAULT_THRESHOLDS.nestingDepth,
|
|
809
|
+
...stripUndefined(userThresholds.nestingDepth)
|
|
798
810
|
},
|
|
799
811
|
functionLength: {
|
|
800
|
-
|
|
812
|
+
...DEFAULT_THRESHOLDS.functionLength,
|
|
813
|
+
...stripUndefined(userThresholds.functionLength)
|
|
801
814
|
},
|
|
802
815
|
parameterCount: {
|
|
803
|
-
|
|
816
|
+
...DEFAULT_THRESHOLDS.parameterCount,
|
|
817
|
+
...stripUndefined(userThresholds.parameterCount)
|
|
804
818
|
},
|
|
805
|
-
fileLength: {
|
|
806
|
-
info: config?.thresholds?.fileLength?.info ?? DEFAULT_THRESHOLDS.fileLength.info
|
|
807
|
-
}
|
|
819
|
+
fileLength: { ...DEFAULT_THRESHOLDS.fileLength, ...stripUndefined(userThresholds.fileLength) }
|
|
808
820
|
};
|
|
821
|
+
}
|
|
822
|
+
function stripUndefined(obj) {
|
|
823
|
+
if (!obj) return {};
|
|
824
|
+
const result = {};
|
|
825
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
826
|
+
if (val !== void 0) result[key] = val;
|
|
827
|
+
}
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
830
|
+
function checkFileLengthViolation(filePath, lineCount, threshold) {
|
|
831
|
+
if (lineCount <= threshold) return null;
|
|
832
|
+
return {
|
|
833
|
+
file: filePath,
|
|
834
|
+
function: "<file>",
|
|
835
|
+
line: 1,
|
|
836
|
+
metric: "fileLength",
|
|
837
|
+
value: lineCount,
|
|
838
|
+
threshold,
|
|
839
|
+
tier: 3,
|
|
840
|
+
severity: "info",
|
|
841
|
+
message: `File has ${lineCount} lines (threshold: ${threshold})`
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function checkCyclomaticComplexity(filePath, fn, thresholds) {
|
|
845
|
+
const complexity = computeCyclomaticComplexity(fn.body);
|
|
846
|
+
if (complexity > thresholds.error) {
|
|
847
|
+
return {
|
|
848
|
+
file: filePath,
|
|
849
|
+
function: fn.name,
|
|
850
|
+
line: fn.line,
|
|
851
|
+
metric: "cyclomaticComplexity",
|
|
852
|
+
value: complexity,
|
|
853
|
+
threshold: thresholds.error,
|
|
854
|
+
tier: 1,
|
|
855
|
+
severity: "error",
|
|
856
|
+
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.error})`
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (complexity > thresholds.warn) {
|
|
860
|
+
return {
|
|
861
|
+
file: filePath,
|
|
862
|
+
function: fn.name,
|
|
863
|
+
line: fn.line,
|
|
864
|
+
metric: "cyclomaticComplexity",
|
|
865
|
+
value: complexity,
|
|
866
|
+
threshold: thresholds.warn,
|
|
867
|
+
tier: 2,
|
|
868
|
+
severity: "warning",
|
|
869
|
+
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.warn})`
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
function checkNestingDepth(filePath, fn, threshold) {
|
|
875
|
+
const depth = computeNestingDepth(fn.body);
|
|
876
|
+
if (depth <= threshold) return null;
|
|
877
|
+
return {
|
|
878
|
+
file: filePath,
|
|
879
|
+
function: fn.name,
|
|
880
|
+
line: fn.line,
|
|
881
|
+
metric: "nestingDepth",
|
|
882
|
+
value: depth,
|
|
883
|
+
threshold,
|
|
884
|
+
tier: 2,
|
|
885
|
+
severity: "warning",
|
|
886
|
+
message: `Function "${fn.name}" has nesting depth of ${depth} (threshold: ${threshold})`
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function checkFunctionLength(filePath, fn, threshold) {
|
|
890
|
+
const fnLength = fn.endLine - fn.startLine + 1;
|
|
891
|
+
if (fnLength <= threshold) return null;
|
|
892
|
+
return {
|
|
893
|
+
file: filePath,
|
|
894
|
+
function: fn.name,
|
|
895
|
+
line: fn.line,
|
|
896
|
+
metric: "functionLength",
|
|
897
|
+
value: fnLength,
|
|
898
|
+
threshold,
|
|
899
|
+
tier: 2,
|
|
900
|
+
severity: "warning",
|
|
901
|
+
message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${threshold})`
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function checkParameterCount(filePath, fn, threshold) {
|
|
905
|
+
if (fn.params <= threshold) return null;
|
|
906
|
+
return {
|
|
907
|
+
file: filePath,
|
|
908
|
+
function: fn.name,
|
|
909
|
+
line: fn.line,
|
|
910
|
+
metric: "parameterCount",
|
|
911
|
+
value: fn.params,
|
|
912
|
+
threshold,
|
|
913
|
+
tier: 2,
|
|
914
|
+
severity: "warning",
|
|
915
|
+
message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${threshold})`
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function checkHotspot(filePath, fn, graphData) {
|
|
919
|
+
const hotspot = graphData.hotspots.find((h) => h.file === filePath && h.function === fn.name);
|
|
920
|
+
if (!hotspot || hotspot.hotspotScore <= graphData.percentile95Score) return null;
|
|
921
|
+
return {
|
|
922
|
+
file: filePath,
|
|
923
|
+
function: fn.name,
|
|
924
|
+
line: fn.line,
|
|
925
|
+
metric: "hotspotScore",
|
|
926
|
+
value: hotspot.hotspotScore,
|
|
927
|
+
threshold: graphData.percentile95Score,
|
|
928
|
+
tier: 1,
|
|
929
|
+
severity: "error",
|
|
930
|
+
message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function collectFunctionViolations(filePath, fn, thresholds, graphData) {
|
|
934
|
+
const checks = [
|
|
935
|
+
checkCyclomaticComplexity(filePath, fn, thresholds.cyclomaticComplexity),
|
|
936
|
+
checkNestingDepth(filePath, fn, thresholds.nestingDepth.warn),
|
|
937
|
+
checkFunctionLength(filePath, fn, thresholds.functionLength.warn),
|
|
938
|
+
checkParameterCount(filePath, fn, thresholds.parameterCount.warn)
|
|
939
|
+
];
|
|
940
|
+
if (graphData) {
|
|
941
|
+
checks.push(checkHotspot(filePath, fn, graphData));
|
|
942
|
+
}
|
|
943
|
+
return checks.filter((v) => v !== null);
|
|
944
|
+
}
|
|
945
|
+
async function detectComplexityViolations(snapshot, config, graphData) {
|
|
946
|
+
const violations = [];
|
|
947
|
+
const thresholds = resolveThresholds(config);
|
|
809
948
|
let totalFunctions = 0;
|
|
810
949
|
for (const file of snapshot.files) {
|
|
811
950
|
let content;
|
|
@@ -815,107 +954,16 @@ async function detectComplexityViolations(snapshot, config, graphData) {
|
|
|
815
954
|
continue;
|
|
816
955
|
}
|
|
817
956
|
const lines = content.split("\n");
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
value: lines.length,
|
|
825
|
-
threshold: thresholds.fileLength.info,
|
|
826
|
-
tier: 3,
|
|
827
|
-
severity: "info",
|
|
828
|
-
message: `File has ${lines.length} lines (threshold: ${thresholds.fileLength.info})`
|
|
829
|
-
});
|
|
830
|
-
}
|
|
957
|
+
const fileLenViolation = checkFileLengthViolation(
|
|
958
|
+
file.path,
|
|
959
|
+
lines.length,
|
|
960
|
+
thresholds.fileLength.info
|
|
961
|
+
);
|
|
962
|
+
if (fileLenViolation) violations.push(fileLenViolation);
|
|
831
963
|
const functions = extractFunctions(content);
|
|
832
964
|
totalFunctions += functions.length;
|
|
833
965
|
for (const fn of functions) {
|
|
834
|
-
|
|
835
|
-
if (complexity > thresholds.cyclomaticComplexity.error) {
|
|
836
|
-
violations.push({
|
|
837
|
-
file: file.path,
|
|
838
|
-
function: fn.name,
|
|
839
|
-
line: fn.line,
|
|
840
|
-
metric: "cyclomaticComplexity",
|
|
841
|
-
value: complexity,
|
|
842
|
-
threshold: thresholds.cyclomaticComplexity.error,
|
|
843
|
-
tier: 1,
|
|
844
|
-
severity: "error",
|
|
845
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.cyclomaticComplexity.error})`
|
|
846
|
-
});
|
|
847
|
-
} else if (complexity > thresholds.cyclomaticComplexity.warn) {
|
|
848
|
-
violations.push({
|
|
849
|
-
file: file.path,
|
|
850
|
-
function: fn.name,
|
|
851
|
-
line: fn.line,
|
|
852
|
-
metric: "cyclomaticComplexity",
|
|
853
|
-
value: complexity,
|
|
854
|
-
threshold: thresholds.cyclomaticComplexity.warn,
|
|
855
|
-
tier: 2,
|
|
856
|
-
severity: "warning",
|
|
857
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.cyclomaticComplexity.warn})`
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
const nestingDepth = computeNestingDepth(fn.body);
|
|
861
|
-
if (nestingDepth > thresholds.nestingDepth.warn) {
|
|
862
|
-
violations.push({
|
|
863
|
-
file: file.path,
|
|
864
|
-
function: fn.name,
|
|
865
|
-
line: fn.line,
|
|
866
|
-
metric: "nestingDepth",
|
|
867
|
-
value: nestingDepth,
|
|
868
|
-
threshold: thresholds.nestingDepth.warn,
|
|
869
|
-
tier: 2,
|
|
870
|
-
severity: "warning",
|
|
871
|
-
message: `Function "${fn.name}" has nesting depth of ${nestingDepth} (threshold: ${thresholds.nestingDepth.warn})`
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
const fnLength = fn.endLine - fn.startLine + 1;
|
|
875
|
-
if (fnLength > thresholds.functionLength.warn) {
|
|
876
|
-
violations.push({
|
|
877
|
-
file: file.path,
|
|
878
|
-
function: fn.name,
|
|
879
|
-
line: fn.line,
|
|
880
|
-
metric: "functionLength",
|
|
881
|
-
value: fnLength,
|
|
882
|
-
threshold: thresholds.functionLength.warn,
|
|
883
|
-
tier: 2,
|
|
884
|
-
severity: "warning",
|
|
885
|
-
message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${thresholds.functionLength.warn})`
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
if (fn.params > thresholds.parameterCount.warn) {
|
|
889
|
-
violations.push({
|
|
890
|
-
file: file.path,
|
|
891
|
-
function: fn.name,
|
|
892
|
-
line: fn.line,
|
|
893
|
-
metric: "parameterCount",
|
|
894
|
-
value: fn.params,
|
|
895
|
-
threshold: thresholds.parameterCount.warn,
|
|
896
|
-
tier: 2,
|
|
897
|
-
severity: "warning",
|
|
898
|
-
message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${thresholds.parameterCount.warn})`
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
if (graphData) {
|
|
902
|
-
const hotspot = graphData.hotspots.find(
|
|
903
|
-
(h) => h.file === file.path && h.function === fn.name
|
|
904
|
-
);
|
|
905
|
-
if (hotspot && hotspot.hotspotScore > graphData.percentile95Score) {
|
|
906
|
-
violations.push({
|
|
907
|
-
file: file.path,
|
|
908
|
-
function: fn.name,
|
|
909
|
-
line: fn.line,
|
|
910
|
-
metric: "hotspotScore",
|
|
911
|
-
value: hotspot.hotspotScore,
|
|
912
|
-
threshold: graphData.percentile95Score,
|
|
913
|
-
tier: 1,
|
|
914
|
-
severity: "error",
|
|
915
|
-
message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
}
|
|
966
|
+
violations.push(...collectFunctionViolations(file.path, fn, thresholds, graphData));
|
|
919
967
|
}
|
|
920
968
|
}
|
|
921
969
|
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
@@ -995,7 +1043,7 @@ var ComplexityCollector = class {
|
|
|
995
1043
|
(v) => v.severity === "error" || v.severity === "warning"
|
|
996
1044
|
);
|
|
997
1045
|
const violations = filtered.map((v) => {
|
|
998
|
-
const relFile = (
|
|
1046
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
999
1047
|
const idDetail = `${v.metric}:${v.function}`;
|
|
1000
1048
|
return {
|
|
1001
1049
|
id: violationId(relFile, this.category, idDetail),
|
|
@@ -1020,9 +1068,6 @@ var ComplexityCollector = class {
|
|
|
1020
1068
|
}
|
|
1021
1069
|
};
|
|
1022
1070
|
|
|
1023
|
-
// src/architecture/collectors/coupling.ts
|
|
1024
|
-
var import_node_path5 = require("path");
|
|
1025
|
-
|
|
1026
1071
|
// src/entropy/detectors/coupling.ts
|
|
1027
1072
|
var DEFAULT_THRESHOLDS2 = {
|
|
1028
1073
|
fanOut: { warn: 15 },
|
|
@@ -1224,7 +1269,7 @@ var CouplingCollector = class {
|
|
|
1224
1269
|
(v) => v.severity === "error" || v.severity === "warning"
|
|
1225
1270
|
);
|
|
1226
1271
|
const violations = filtered.map((v) => {
|
|
1227
|
-
const relFile = (
|
|
1272
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1228
1273
|
const idDetail = `${v.metric}`;
|
|
1229
1274
|
return {
|
|
1230
1275
|
id: violationId(relFile, this.category, idDetail),
|
|
@@ -1247,7 +1292,6 @@ var CouplingCollector = class {
|
|
|
1247
1292
|
};
|
|
1248
1293
|
|
|
1249
1294
|
// src/architecture/collectors/forbidden-imports.ts
|
|
1250
|
-
var import_node_path6 = require("path");
|
|
1251
1295
|
var ForbiddenImportCollector = class {
|
|
1252
1296
|
category = "forbidden-imports";
|
|
1253
1297
|
getRules(_config, _rootDir) {
|
|
@@ -1291,8 +1335,8 @@ var ForbiddenImportCollector = class {
|
|
|
1291
1335
|
(v) => v.reason === "FORBIDDEN_IMPORT"
|
|
1292
1336
|
);
|
|
1293
1337
|
const violations = forbidden.map((v) => {
|
|
1294
|
-
const relFile = (
|
|
1295
|
-
const relImport = (
|
|
1338
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1339
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
1296
1340
|
const detail = `forbidden import: ${relFile} -> ${relImport}`;
|
|
1297
1341
|
return {
|
|
1298
1342
|
id: violationId(relFile, this.category, detail),
|
|
@@ -1315,7 +1359,7 @@ var ForbiddenImportCollector = class {
|
|
|
1315
1359
|
|
|
1316
1360
|
// src/architecture/collectors/module-size.ts
|
|
1317
1361
|
var import_promises2 = require("fs/promises");
|
|
1318
|
-
var
|
|
1362
|
+
var import_node_path3 = require("path");
|
|
1319
1363
|
async function discoverModules(rootDir) {
|
|
1320
1364
|
const modules = [];
|
|
1321
1365
|
async function scanDir(dir) {
|
|
@@ -1331,7 +1375,7 @@ async function discoverModules(rootDir) {
|
|
|
1331
1375
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") {
|
|
1332
1376
|
continue;
|
|
1333
1377
|
}
|
|
1334
|
-
const fullPath = (0,
|
|
1378
|
+
const fullPath = (0, import_node_path3.join)(dir, entry.name);
|
|
1335
1379
|
if (entry.isDirectory()) {
|
|
1336
1380
|
subdirs.push(fullPath);
|
|
1337
1381
|
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".test.tsx") && !entry.name.endsWith(".spec.ts")) {
|
|
@@ -1348,10 +1392,10 @@ async function discoverModules(rootDir) {
|
|
|
1348
1392
|
}
|
|
1349
1393
|
}
|
|
1350
1394
|
modules.push({
|
|
1351
|
-
modulePath: (
|
|
1395
|
+
modulePath: relativePosix(rootDir, dir),
|
|
1352
1396
|
fileCount: tsFiles.length,
|
|
1353
1397
|
totalLoc,
|
|
1354
|
-
files: tsFiles.map((f) => (
|
|
1398
|
+
files: tsFiles.map((f) => relativePosix(rootDir, f))
|
|
1355
1399
|
});
|
|
1356
1400
|
}
|
|
1357
1401
|
for (const sub of subdirs) {
|
|
@@ -1443,16 +1487,16 @@ var ModuleSizeCollector = class {
|
|
|
1443
1487
|
|
|
1444
1488
|
// src/architecture/collectors/dep-depth.ts
|
|
1445
1489
|
var import_promises3 = require("fs/promises");
|
|
1446
|
-
var
|
|
1490
|
+
var import_node_path4 = require("path");
|
|
1447
1491
|
function extractImportSources(content, filePath) {
|
|
1448
1492
|
const importRegex = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
|
|
1449
1493
|
const dynamicRegex = /import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
1450
1494
|
const sources = [];
|
|
1451
|
-
const dir = (0,
|
|
1495
|
+
const dir = (0, import_node_path4.dirname)(filePath);
|
|
1452
1496
|
for (const regex of [importRegex, dynamicRegex]) {
|
|
1453
1497
|
let match;
|
|
1454
1498
|
while ((match = regex.exec(content)) !== null) {
|
|
1455
|
-
let resolved = (0,
|
|
1499
|
+
let resolved = (0, import_node_path4.resolve)(dir, match[1]);
|
|
1456
1500
|
if (!resolved.endsWith(".ts") && !resolved.endsWith(".tsx")) {
|
|
1457
1501
|
resolved += ".ts";
|
|
1458
1502
|
}
|
|
@@ -1473,7 +1517,7 @@ async function collectTsFiles(dir) {
|
|
|
1473
1517
|
for (const entry of entries) {
|
|
1474
1518
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
1475
1519
|
continue;
|
|
1476
|
-
const fullPath = (0,
|
|
1520
|
+
const fullPath = (0, import_node_path4.join)(d, entry.name);
|
|
1477
1521
|
if (entry.isDirectory()) {
|
|
1478
1522
|
await scan(fullPath);
|
|
1479
1523
|
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".test.tsx") && !entry.name.endsWith(".spec.ts")) {
|
|
@@ -1527,7 +1571,7 @@ var DepDepthCollector = class {
|
|
|
1527
1571
|
}
|
|
1528
1572
|
const moduleMap = /* @__PURE__ */ new Map();
|
|
1529
1573
|
for (const file of allFiles) {
|
|
1530
|
-
const relDir = (
|
|
1574
|
+
const relDir = relativePosix(rootDir, (0, import_node_path4.dirname)(file));
|
|
1531
1575
|
if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
|
|
1532
1576
|
moduleMap.get(relDir).push(file);
|
|
1533
1577
|
}
|