@harness-engineering/core 0.13.1 → 0.15.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/README.md +23 -0
- package/dist/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/architecture/matchers.js +240 -188
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-D6VFA6AS.mjs → chunk-BQUWXBGR.mjs} +240 -188
- package/dist/index.d.mts +444 -108
- package/dist/index.d.ts +444 -108
- package/dist/index.js +2835 -1263
- package/dist/index.mjs +2545 -1056
- 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 +7 -4
package/README.md
CHANGED
|
@@ -10,6 +10,29 @@ pnpm add @harness-engineering/core
|
|
|
10
10
|
|
|
11
11
|
## Modules
|
|
12
12
|
|
|
13
|
+
### Modules Overview
|
|
14
|
+
|
|
15
|
+
| Module | Description |
|
|
16
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| **validation** | Cross-cutting validation for file structure, configuration, and commit messages |
|
|
18
|
+
| **context** | AGENTS.md knowledge maps, documentation coverage analysis, token budgets, and context filtering |
|
|
19
|
+
| **constraints** | Architectural layer enforcement, circular dependency detection, and boundary validation |
|
|
20
|
+
| **entropy** | Documentation drift, dead code detection, pattern violations, and auto-fix |
|
|
21
|
+
| **architecture** | Metric collection via multiple collectors (circular deps, layer violations, complexity, coupling, forbidden imports, module size, dependency depth) |
|
|
22
|
+
| **review** | Code review pipeline with mechanical checks, fan-out agents (compliance, bugs, security, architecture), and deduplication |
|
|
23
|
+
| **feedback** | Agent feedback loops with self-review, peer review, telemetry, logging, and action tracking |
|
|
24
|
+
| **performance** | Benchmarking, baseline management, regression detection, and critical path analysis |
|
|
25
|
+
| **security** | Security scanning with secret detection, injection detection, XSS, crypto, path traversal, and stack-specific rules (Node.js, Express, React, Go) |
|
|
26
|
+
| **ci** | CI/CD check orchestration for running automated checks in continuous integration pipelines |
|
|
27
|
+
| **code-nav** | Code navigation: file parsing, symbol extraction/outlines, symbol search, and code range unfolding by language |
|
|
28
|
+
| **state** | State persistence including learnings, failures, handoffs, gates, streams, sessions, and event logging |
|
|
29
|
+
| **roadmap** | Roadmap parsing, serialization, and synchronization with codebase and issue tracking |
|
|
30
|
+
| **pipeline** | Skill pipeline orchestration for sequential and multi-turn skill execution |
|
|
31
|
+
| **workflow** | Workflow execution engine for structured multi-step workflows |
|
|
32
|
+
| **interaction** | Types and schemas for agent-to-human interactions (questions, confirmations, transitions) |
|
|
33
|
+
| **blueprint** | HTML documentation generation from module data using EJS templates |
|
|
34
|
+
| **shared** | Error types, Result pattern for error handling, file system utilities, and language parsers |
|
|
35
|
+
|
|
13
36
|
### Validation Module
|
|
14
37
|
|
|
15
38
|
Cross-cutting validation utilities used by all other modules.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { h as ArchHandle, j as ArchitectureOptions, s as archMatchers, t as archModule, u as architecture } from '../matchers-
|
|
1
|
+
export { h as ArchHandle, j as ArchitectureOptions, s as archMatchers, t as archModule, u as architecture } from '../matchers-Dj1t5vpg.mjs';
|
|
2
2
|
import 'zod';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { h as ArchHandle, j as ArchitectureOptions, s as archMatchers, t as archModule, u as architecture } from '../matchers-
|
|
1
|
+
export { h as ArchHandle, j as ArchitectureOptions, s as archMatchers, t as archModule, u as architecture } from '../matchers-Dj1t5vpg.js';
|
|
2
2
|
import 'zod';
|
|
@@ -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;
|
|
@@ -615,6 +621,31 @@ function aggregateByCategory(results) {
|
|
|
615
621
|
}
|
|
616
622
|
return map;
|
|
617
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
|
+
}
|
|
618
649
|
function diff(current, baseline) {
|
|
619
650
|
const aggregated = aggregateByCategory(current);
|
|
620
651
|
const newViolations = [];
|
|
@@ -627,21 +658,11 @@ function diff(current, baseline) {
|
|
|
627
658
|
const baselineCategory = baseline.metrics[category];
|
|
628
659
|
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
629
660
|
const baselineValue = baselineCategory?.value ?? 0;
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
} else {
|
|
634
|
-
newViolations.push(violation);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
661
|
+
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
662
|
+
newViolations.push(...classified.newViolations);
|
|
663
|
+
preExisting.push(...classified.preExisting);
|
|
637
664
|
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
638
|
-
|
|
639
|
-
for (const id of baselineCategory.violationIds) {
|
|
640
|
-
if (!currentViolationIds.has(id)) {
|
|
641
|
-
resolvedViolations.push(id);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
665
|
+
resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
645
666
|
if (baselineCategory && agg.value > baselineValue) {
|
|
646
667
|
regressions.push({
|
|
647
668
|
category,
|
|
@@ -651,16 +672,9 @@ function diff(current, baseline) {
|
|
|
651
672
|
});
|
|
652
673
|
}
|
|
653
674
|
}
|
|
654
|
-
|
|
655
|
-
if (!visitedCategories.has(category) && baselineCategory) {
|
|
656
|
-
for (const id of baselineCategory.violationIds) {
|
|
657
|
-
resolvedViolations.push(id);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
const passed = newViolations.length === 0 && regressions.length === 0;
|
|
675
|
+
resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
662
676
|
return {
|
|
663
|
-
passed,
|
|
677
|
+
passed: newViolations.length === 0 && regressions.length === 0,
|
|
664
678
|
newViolations,
|
|
665
679
|
resolvedViolations,
|
|
666
680
|
preExisting,
|
|
@@ -678,22 +692,22 @@ var DEFAULT_THRESHOLDS = {
|
|
|
678
692
|
fileLength: { info: 300 },
|
|
679
693
|
hotspotPercentile: { error: 95 }
|
|
680
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
|
+
];
|
|
681
705
|
function extractFunctions(content) {
|
|
682
706
|
const functions = [];
|
|
683
707
|
const lines = content.split("\n");
|
|
684
|
-
const patterns = [
|
|
685
|
-
// function declarations: function name(params) {
|
|
686
|
-
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
687
|
-
// method declarations: name(params) {
|
|
688
|
-
/^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
689
|
-
// arrow functions assigned to const/let/var: const name = (params) =>
|
|
690
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
691
|
-
// arrow functions assigned to const/let/var with single param: const name = param =>
|
|
692
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
|
|
693
|
-
];
|
|
694
708
|
for (let i = 0; i < lines.length; i++) {
|
|
695
709
|
const line = lines[i];
|
|
696
|
-
for (const pattern of
|
|
710
|
+
for (const pattern of FUNCTION_PATTERNS) {
|
|
697
711
|
const match = line.match(pattern);
|
|
698
712
|
if (match) {
|
|
699
713
|
const name = match[1] ?? "anonymous";
|
|
@@ -782,26 +796,155 @@ function computeNestingDepth(body) {
|
|
|
782
796
|
}
|
|
783
797
|
return maxDepth;
|
|
784
798
|
}
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
|
|
799
|
+
function resolveThresholds(config) {
|
|
800
|
+
const userThresholds = config?.thresholds;
|
|
801
|
+
if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
|
|
802
|
+
return {
|
|
788
803
|
cyclomaticComplexity: {
|
|
789
|
-
|
|
790
|
-
|
|
804
|
+
...DEFAULT_THRESHOLDS.cyclomaticComplexity,
|
|
805
|
+
...stripUndefined(userThresholds.cyclomaticComplexity)
|
|
791
806
|
},
|
|
792
807
|
nestingDepth: {
|
|
793
|
-
|
|
808
|
+
...DEFAULT_THRESHOLDS.nestingDepth,
|
|
809
|
+
...stripUndefined(userThresholds.nestingDepth)
|
|
794
810
|
},
|
|
795
811
|
functionLength: {
|
|
796
|
-
|
|
812
|
+
...DEFAULT_THRESHOLDS.functionLength,
|
|
813
|
+
...stripUndefined(userThresholds.functionLength)
|
|
797
814
|
},
|
|
798
815
|
parameterCount: {
|
|
799
|
-
|
|
816
|
+
...DEFAULT_THRESHOLDS.parameterCount,
|
|
817
|
+
...stripUndefined(userThresholds.parameterCount)
|
|
800
818
|
},
|
|
801
|
-
fileLength: {
|
|
802
|
-
info: config?.thresholds?.fileLength?.info ?? DEFAULT_THRESHOLDS.fileLength.info
|
|
803
|
-
}
|
|
819
|
+
fileLength: { ...DEFAULT_THRESHOLDS.fileLength, ...stripUndefined(userThresholds.fileLength) }
|
|
804
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);
|
|
805
948
|
let totalFunctions = 0;
|
|
806
949
|
for (const file of snapshot.files) {
|
|
807
950
|
let content;
|
|
@@ -811,107 +954,16 @@ async function detectComplexityViolations(snapshot, config, graphData) {
|
|
|
811
954
|
continue;
|
|
812
955
|
}
|
|
813
956
|
const lines = content.split("\n");
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
value: lines.length,
|
|
821
|
-
threshold: thresholds.fileLength.info,
|
|
822
|
-
tier: 3,
|
|
823
|
-
severity: "info",
|
|
824
|
-
message: `File has ${lines.length} lines (threshold: ${thresholds.fileLength.info})`
|
|
825
|
-
});
|
|
826
|
-
}
|
|
957
|
+
const fileLenViolation = checkFileLengthViolation(
|
|
958
|
+
file.path,
|
|
959
|
+
lines.length,
|
|
960
|
+
thresholds.fileLength.info
|
|
961
|
+
);
|
|
962
|
+
if (fileLenViolation) violations.push(fileLenViolation);
|
|
827
963
|
const functions = extractFunctions(content);
|
|
828
964
|
totalFunctions += functions.length;
|
|
829
965
|
for (const fn of functions) {
|
|
830
|
-
|
|
831
|
-
if (complexity > thresholds.cyclomaticComplexity.error) {
|
|
832
|
-
violations.push({
|
|
833
|
-
file: file.path,
|
|
834
|
-
function: fn.name,
|
|
835
|
-
line: fn.line,
|
|
836
|
-
metric: "cyclomaticComplexity",
|
|
837
|
-
value: complexity,
|
|
838
|
-
threshold: thresholds.cyclomaticComplexity.error,
|
|
839
|
-
tier: 1,
|
|
840
|
-
severity: "error",
|
|
841
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.cyclomaticComplexity.error})`
|
|
842
|
-
});
|
|
843
|
-
} else if (complexity > thresholds.cyclomaticComplexity.warn) {
|
|
844
|
-
violations.push({
|
|
845
|
-
file: file.path,
|
|
846
|
-
function: fn.name,
|
|
847
|
-
line: fn.line,
|
|
848
|
-
metric: "cyclomaticComplexity",
|
|
849
|
-
value: complexity,
|
|
850
|
-
threshold: thresholds.cyclomaticComplexity.warn,
|
|
851
|
-
tier: 2,
|
|
852
|
-
severity: "warning",
|
|
853
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.cyclomaticComplexity.warn})`
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
const nestingDepth = computeNestingDepth(fn.body);
|
|
857
|
-
if (nestingDepth > thresholds.nestingDepth.warn) {
|
|
858
|
-
violations.push({
|
|
859
|
-
file: file.path,
|
|
860
|
-
function: fn.name,
|
|
861
|
-
line: fn.line,
|
|
862
|
-
metric: "nestingDepth",
|
|
863
|
-
value: nestingDepth,
|
|
864
|
-
threshold: thresholds.nestingDepth.warn,
|
|
865
|
-
tier: 2,
|
|
866
|
-
severity: "warning",
|
|
867
|
-
message: `Function "${fn.name}" has nesting depth of ${nestingDepth} (threshold: ${thresholds.nestingDepth.warn})`
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
const fnLength = fn.endLine - fn.startLine + 1;
|
|
871
|
-
if (fnLength > thresholds.functionLength.warn) {
|
|
872
|
-
violations.push({
|
|
873
|
-
file: file.path,
|
|
874
|
-
function: fn.name,
|
|
875
|
-
line: fn.line,
|
|
876
|
-
metric: "functionLength",
|
|
877
|
-
value: fnLength,
|
|
878
|
-
threshold: thresholds.functionLength.warn,
|
|
879
|
-
tier: 2,
|
|
880
|
-
severity: "warning",
|
|
881
|
-
message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${thresholds.functionLength.warn})`
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
if (fn.params > thresholds.parameterCount.warn) {
|
|
885
|
-
violations.push({
|
|
886
|
-
file: file.path,
|
|
887
|
-
function: fn.name,
|
|
888
|
-
line: fn.line,
|
|
889
|
-
metric: "parameterCount",
|
|
890
|
-
value: fn.params,
|
|
891
|
-
threshold: thresholds.parameterCount.warn,
|
|
892
|
-
tier: 2,
|
|
893
|
-
severity: "warning",
|
|
894
|
-
message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${thresholds.parameterCount.warn})`
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
if (graphData) {
|
|
898
|
-
const hotspot = graphData.hotspots.find(
|
|
899
|
-
(h) => h.file === file.path && h.function === fn.name
|
|
900
|
-
);
|
|
901
|
-
if (hotspot && hotspot.hotspotScore > graphData.percentile95Score) {
|
|
902
|
-
violations.push({
|
|
903
|
-
file: file.path,
|
|
904
|
-
function: fn.name,
|
|
905
|
-
line: fn.line,
|
|
906
|
-
metric: "hotspotScore",
|
|
907
|
-
value: hotspot.hotspotScore,
|
|
908
|
-
threshold: graphData.percentile95Score,
|
|
909
|
-
tier: 1,
|
|
910
|
-
severity: "error",
|
|
911
|
-
message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
}
|
|
966
|
+
violations.push(...collectFunctionViolations(file.path, fn, thresholds, graphData));
|
|
915
967
|
}
|
|
916
968
|
}
|
|
917
969
|
const errorCount = violations.filter((v) => v.severity === "error").length;
|