@harness-engineering/core 0.13.1 → 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.
@@ -294,65 +294,71 @@ async function validateDependencies(config) {
294
294
  }
295
295
 
296
296
  // src/constraints/circular-deps.ts
297
- function tarjanSCC(graph) {
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 && graph.nodes.includes(edge.to)) {
305
+ if (neighbors && nodeSet.has(edge.to)) {
309
306
  neighbors.push(edge.to);
310
307
  }
311
308
  }
312
- function strongConnect(node) {
313
- nodeMap.set(node, {
314
- index,
315
- lowlink: index,
316
- onStack: true
317
- });
318
- index++;
319
- stack.push(node);
320
- const neighbors = adjacency.get(node) ?? [];
321
- for (const neighbor of neighbors) {
322
- const neighborData = nodeMap.get(neighbor);
323
- if (!neighborData) {
324
- strongConnect(neighbor);
325
- const nodeData2 = nodeMap.get(node);
326
- const updatedNeighborData = nodeMap.get(neighbor);
327
- nodeData2.lowlink = Math.min(nodeData2.lowlink, updatedNeighborData.lowlink);
328
- } else if (neighborData.onStack) {
329
- const nodeData2 = nodeMap.get(node);
330
- nodeData2.lowlink = Math.min(nodeData2.lowlink, neighborData.index);
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
- const nodeData = nodeMap.get(node);
334
- if (nodeData.lowlink === nodeData.index) {
335
- const scc = [];
336
- let w;
337
- do {
338
- w = stack.pop();
339
- nodeMap.get(w).onStack = false;
340
- scc.push(w);
341
- } while (w !== node);
342
- if (scc.length > 1) {
343
- sccs.push(scc);
344
- } else if (scc.length === 1) {
345
- const selfNode = scc[0];
346
- const selfNeighbors = adjacency.get(selfNode) ?? [];
347
- if (selfNeighbors.includes(selfNode)) {
348
- sccs.push(scc);
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
- strongConnect(node);
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
- for (const violation of agg.violations) {
631
- if (baselineViolationIds.has(violation.id)) {
632
- preExisting.push(violation.id);
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
- if (baselineCategory) {
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
- for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
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 patterns) {
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
- async function detectComplexityViolations(snapshot, config, graphData) {
786
- const violations = [];
787
- const thresholds = {
799
+ function resolveThresholds(config) {
800
+ const userThresholds = config?.thresholds;
801
+ if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
802
+ return {
788
803
  cyclomaticComplexity: {
789
- error: config?.thresholds?.cyclomaticComplexity?.error ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.error,
790
- warn: config?.thresholds?.cyclomaticComplexity?.warn ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.warn
804
+ ...DEFAULT_THRESHOLDS.cyclomaticComplexity,
805
+ ...stripUndefined(userThresholds.cyclomaticComplexity)
791
806
  },
792
807
  nestingDepth: {
793
- warn: config?.thresholds?.nestingDepth?.warn ?? DEFAULT_THRESHOLDS.nestingDepth.warn
808
+ ...DEFAULT_THRESHOLDS.nestingDepth,
809
+ ...stripUndefined(userThresholds.nestingDepth)
794
810
  },
795
811
  functionLength: {
796
- warn: config?.thresholds?.functionLength?.warn ?? DEFAULT_THRESHOLDS.functionLength.warn
812
+ ...DEFAULT_THRESHOLDS.functionLength,
813
+ ...stripUndefined(userThresholds.functionLength)
797
814
  },
798
815
  parameterCount: {
799
- warn: config?.thresholds?.parameterCount?.warn ?? DEFAULT_THRESHOLDS.parameterCount.warn
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
- if (lines.length > thresholds.fileLength.info) {
815
- violations.push({
816
- file: file.path,
817
- function: "<file>",
818
- line: 1,
819
- metric: "fileLength",
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
- const complexity = computeCyclomaticComplexity(fn.body);
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;
@@ -2,7 +2,7 @@ import {
2
2
  archMatchers,
3
3
  archModule,
4
4
  architecture
5
- } from "../chunk-D6VFA6AS.mjs";
5
+ } from "../chunk-BQUWXBGR.mjs";
6
6
  export {
7
7
  archMatchers,
8
8
  archModule,