@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.
@@ -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 path = relativePath.replace(/\\/g, "/");
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 = (0, import_path.relative)(rootDir, edge.from);
213
- const toRelative = (0, import_path.relative)(rootDir, edge.to);
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 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;
@@ -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) => (0, import_node_path.relative)(rootDir, f)).join(" -> ");
437
- const firstFile = (0, import_node_path.relative)(rootDir, cycle.cycle[0]);
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 = (0, import_node_path2.relative)(rootDir, v.file);
503
- const relImport = (0, import_node_path2.relative)(rootDir, v.imports);
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 import_node_path3 = require("path");
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, import_node_path3.join)(projectRoot, baselinePath) : (0, import_node_path3.join)(projectRoot, ".harness", "arch", "baselines.json");
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, import_node_path3.dirname)(this.baselinesPath);
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
- for (const violation of agg.violations) {
632
- if (baselineViolationIds.has(violation.id)) {
633
- preExisting.push(violation.id);
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
- if (baselineCategory) {
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
- for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
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 patterns) {
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
- async function detectComplexityViolations(snapshot, config, graphData) {
790
- const violations = [];
791
- const thresholds = {
799
+ function resolveThresholds(config) {
800
+ const userThresholds = config?.thresholds;
801
+ if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
802
+ return {
792
803
  cyclomaticComplexity: {
793
- error: config?.thresholds?.cyclomaticComplexity?.error ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.error,
794
- warn: config?.thresholds?.cyclomaticComplexity?.warn ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.warn
804
+ ...DEFAULT_THRESHOLDS.cyclomaticComplexity,
805
+ ...stripUndefined(userThresholds.cyclomaticComplexity)
795
806
  },
796
807
  nestingDepth: {
797
- warn: config?.thresholds?.nestingDepth?.warn ?? DEFAULT_THRESHOLDS.nestingDepth.warn
808
+ ...DEFAULT_THRESHOLDS.nestingDepth,
809
+ ...stripUndefined(userThresholds.nestingDepth)
798
810
  },
799
811
  functionLength: {
800
- warn: config?.thresholds?.functionLength?.warn ?? DEFAULT_THRESHOLDS.functionLength.warn
812
+ ...DEFAULT_THRESHOLDS.functionLength,
813
+ ...stripUndefined(userThresholds.functionLength)
801
814
  },
802
815
  parameterCount: {
803
- warn: config?.thresholds?.parameterCount?.warn ?? DEFAULT_THRESHOLDS.parameterCount.warn
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
- if (lines.length > thresholds.fileLength.info) {
819
- violations.push({
820
- file: file.path,
821
- function: "<file>",
822
- line: 1,
823
- metric: "fileLength",
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
- const complexity = computeCyclomaticComplexity(fn.body);
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 = (0, import_node_path4.relative)(rootDir, v.file);
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 = (0, import_node_path5.relative)(rootDir, v.file);
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 = (0, import_node_path6.relative)(rootDir, v.file);
1295
- const relImport = (0, import_node_path6.relative)(rootDir, v.imports);
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 import_node_path7 = require("path");
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, import_node_path7.join)(dir, entry.name);
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: (0, import_node_path7.relative)(rootDir, dir),
1395
+ modulePath: relativePosix(rootDir, dir),
1352
1396
  fileCount: tsFiles.length,
1353
1397
  totalLoc,
1354
- files: tsFiles.map((f) => (0, import_node_path7.relative)(rootDir, 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 import_node_path8 = require("path");
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, import_node_path8.dirname)(filePath);
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, import_node_path8.resolve)(dir, match[1]);
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, import_node_path8.join)(d, entry.name);
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 = (0, import_node_path8.relative)(rootDir, (0, import_node_path8.dirname)(file));
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
  }