@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.
@@ -82,14 +82,10 @@ var ConstraintRuleSchema = z.object({
82
82
  // forward-compat for governs edges
83
83
  });
84
84
 
85
- // src/architecture/collectors/circular-deps.ts
86
- import { relative as relative2 } from "path";
87
-
88
85
  // src/architecture/collectors/hash.ts
89
86
  import { createHash } from "crypto";
90
87
  function violationId(relativePath, category, normalizedDetail) {
91
- const path = relativePath.replace(/\\/g, "/");
92
- const input = `${path}:${category}:${normalizedDetail}`;
88
+ const input = `${relativePath}:${category}:${normalizedDetail}`;
93
89
  return createHash("sha256").update(input).digest("hex");
94
90
  }
95
91
  function constraintRuleId(category, scope, description) {
@@ -131,6 +127,7 @@ function resolveFileToLayer(file, layers) {
131
127
  // src/shared/fs-utils.ts
132
128
  import { access, constants, readFile } from "fs";
133
129
  import { promisify } from "util";
130
+ import { relative } from "path";
134
131
  import { glob } from "glob";
135
132
  var accessAsync = promisify(access);
136
133
  var readFileAsync = promisify(readFile);
@@ -153,9 +150,12 @@ async function readFileContent(path) {
153
150
  async function findFiles(pattern, cwd = process.cwd()) {
154
151
  return glob(pattern, { cwd, absolute: true });
155
152
  }
153
+ function relativePosix(from, to) {
154
+ return relative(from, to).replaceAll("\\", "/");
155
+ }
156
156
 
157
157
  // src/constraints/dependencies.ts
158
- import { dirname, resolve, relative } from "path";
158
+ import { dirname, resolve } from "path";
159
159
  function resolveImportPath(importSource, fromFile, _rootDir) {
160
160
  if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
161
161
  return null;
@@ -207,8 +207,8 @@ async function buildDependencyGraph(files, parser, graphDependencyData) {
207
207
  function checkLayerViolations(graph, layers, rootDir) {
208
208
  const violations = [];
209
209
  for (const edge of graph.edges) {
210
- const fromRelative = relative(rootDir, edge.from);
211
- const toRelative = relative(rootDir, edge.to);
210
+ const fromRelative = relativePosix(rootDir, edge.from);
211
+ const toRelative = relativePosix(rootDir, edge.to);
212
212
  const fromLayer = resolveFileToLayer(fromRelative, layers);
213
213
  const toLayer = resolveFileToLayer(toRelative, layers);
214
214
  if (!fromLayer || !toLayer) continue;
@@ -292,65 +292,71 @@ async function validateDependencies(config) {
292
292
  }
293
293
 
294
294
  // src/constraints/circular-deps.ts
295
- function tarjanSCC(graph) {
296
- const nodeMap = /* @__PURE__ */ new Map();
297
- const stack = [];
298
- const sccs = [];
299
- let index = 0;
295
+ function buildAdjacencyList(graph) {
300
296
  const adjacency = /* @__PURE__ */ new Map();
297
+ const nodeSet = new Set(graph.nodes);
301
298
  for (const node of graph.nodes) {
302
299
  adjacency.set(node, []);
303
300
  }
304
301
  for (const edge of graph.edges) {
305
302
  const neighbors = adjacency.get(edge.from);
306
- if (neighbors && graph.nodes.includes(edge.to)) {
303
+ if (neighbors && nodeSet.has(edge.to)) {
307
304
  neighbors.push(edge.to);
308
305
  }
309
306
  }
310
- function strongConnect(node) {
311
- nodeMap.set(node, {
312
- index,
313
- lowlink: index,
314
- onStack: true
315
- });
316
- index++;
317
- stack.push(node);
318
- const neighbors = adjacency.get(node) ?? [];
319
- for (const neighbor of neighbors) {
320
- const neighborData = nodeMap.get(neighbor);
321
- if (!neighborData) {
322
- strongConnect(neighbor);
323
- const nodeData2 = nodeMap.get(node);
324
- const updatedNeighborData = nodeMap.get(neighbor);
325
- nodeData2.lowlink = Math.min(nodeData2.lowlink, updatedNeighborData.lowlink);
326
- } else if (neighborData.onStack) {
327
- const nodeData2 = nodeMap.get(node);
328
- nodeData2.lowlink = Math.min(nodeData2.lowlink, neighborData.index);
329
- }
307
+ return adjacency;
308
+ }
309
+ function isCyclicSCC(scc, adjacency) {
310
+ if (scc.length > 1) return true;
311
+ if (scc.length === 1) {
312
+ const selfNode = scc[0];
313
+ const selfNeighbors = adjacency.get(selfNode) ?? [];
314
+ return selfNeighbors.includes(selfNode);
315
+ }
316
+ return false;
317
+ }
318
+ function processNeighbors(node, neighbors, nodeMap, stack, adjacency, sccs, indexRef) {
319
+ for (const neighbor of neighbors) {
320
+ const neighborData = nodeMap.get(neighbor);
321
+ if (!neighborData) {
322
+ strongConnectImpl(neighbor, nodeMap, stack, adjacency, sccs, indexRef);
323
+ const nodeData = nodeMap.get(node);
324
+ const updatedNeighborData = nodeMap.get(neighbor);
325
+ nodeData.lowlink = Math.min(nodeData.lowlink, updatedNeighborData.lowlink);
326
+ } else if (neighborData.onStack) {
327
+ const nodeData = nodeMap.get(node);
328
+ nodeData.lowlink = Math.min(nodeData.lowlink, neighborData.index);
330
329
  }
331
- const nodeData = nodeMap.get(node);
332
- if (nodeData.lowlink === nodeData.index) {
333
- const scc = [];
334
- let w;
335
- do {
336
- w = stack.pop();
337
- nodeMap.get(w).onStack = false;
338
- scc.push(w);
339
- } while (w !== node);
340
- if (scc.length > 1) {
341
- sccs.push(scc);
342
- } else if (scc.length === 1) {
343
- const selfNode = scc[0];
344
- const selfNeighbors = adjacency.get(selfNode) ?? [];
345
- if (selfNeighbors.includes(selfNode)) {
346
- sccs.push(scc);
347
- }
348
- }
330
+ }
331
+ }
332
+ function strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef) {
333
+ nodeMap.set(node, { index: indexRef.value, lowlink: indexRef.value, onStack: true });
334
+ indexRef.value++;
335
+ stack.push(node);
336
+ processNeighbors(node, adjacency.get(node) ?? [], nodeMap, stack, adjacency, sccs, indexRef);
337
+ const nodeData = nodeMap.get(node);
338
+ if (nodeData.lowlink === nodeData.index) {
339
+ const scc = [];
340
+ let w;
341
+ do {
342
+ w = stack.pop();
343
+ nodeMap.get(w).onStack = false;
344
+ scc.push(w);
345
+ } while (w !== node);
346
+ if (isCyclicSCC(scc, adjacency)) {
347
+ sccs.push(scc);
349
348
  }
350
349
  }
350
+ }
351
+ function tarjanSCC(graph) {
352
+ const nodeMap = /* @__PURE__ */ new Map();
353
+ const stack = [];
354
+ const sccs = [];
355
+ const indexRef = { value: 0 };
356
+ const adjacency = buildAdjacencyList(graph);
351
357
  for (const node of graph.nodes) {
352
358
  if (!nodeMap.has(node)) {
353
- strongConnect(node);
359
+ strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef);
354
360
  }
355
361
  }
356
362
  return sccs;
@@ -438,8 +444,8 @@ var CircularDepsCollector = class {
438
444
  }
439
445
  const { cycles, largestCycle } = result.value;
440
446
  const violations = cycles.map((cycle) => {
441
- const cyclePath = cycle.cycle.map((f) => relative2(rootDir, f)).join(" -> ");
442
- const firstFile = relative2(rootDir, cycle.cycle[0]);
447
+ const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
448
+ const firstFile = relativePosix(rootDir, cycle.cycle[0]);
443
449
  return {
444
450
  id: violationId(firstFile, this.category, cyclePath),
445
451
  file: firstFile,
@@ -460,7 +466,6 @@ var CircularDepsCollector = class {
460
466
  };
461
467
 
462
468
  // src/architecture/collectors/layer-violations.ts
463
- import { relative as relative3 } from "path";
464
469
  var LayerViolationCollector = class {
465
470
  category = "layer-violations";
466
471
  getRules(_config, _rootDir) {
@@ -504,8 +509,8 @@ var LayerViolationCollector = class {
504
509
  (v) => v.reason === "WRONG_LAYER"
505
510
  );
506
511
  const violations = layerViolations.map((v) => {
507
- const relFile = relative3(rootDir, v.file);
508
- const relImport = relative3(rootDir, v.imports);
512
+ const relFile = relativePosix(rootDir, v.file);
513
+ const relImport = relativePosix(rootDir, v.imports);
509
514
  const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
510
515
  return {
511
516
  id: violationId(relFile, this.category, detail),
@@ -621,6 +626,31 @@ function aggregateByCategory(results) {
621
626
  }
622
627
  return map;
623
628
  }
629
+ function classifyViolations(violations, baselineViolationIds) {
630
+ const newViolations = [];
631
+ const preExisting = [];
632
+ for (const violation of violations) {
633
+ if (baselineViolationIds.has(violation.id)) {
634
+ preExisting.push(violation.id);
635
+ } else {
636
+ newViolations.push(violation);
637
+ }
638
+ }
639
+ return { newViolations, preExisting };
640
+ }
641
+ function findResolvedViolations(baselineCategory, currentViolationIds) {
642
+ if (!baselineCategory) return [];
643
+ return baselineCategory.violationIds.filter((id) => !currentViolationIds.has(id));
644
+ }
645
+ function collectOrphanedBaselineViolations(baseline, visitedCategories) {
646
+ const resolved = [];
647
+ for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
648
+ if (!visitedCategories.has(category) && baselineCategory) {
649
+ resolved.push(...baselineCategory.violationIds);
650
+ }
651
+ }
652
+ return resolved;
653
+ }
624
654
  function diff(current, baseline) {
625
655
  const aggregated = aggregateByCategory(current);
626
656
  const newViolations = [];
@@ -633,21 +663,11 @@ function diff(current, baseline) {
633
663
  const baselineCategory = baseline.metrics[category];
634
664
  const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
635
665
  const baselineValue = baselineCategory?.value ?? 0;
636
- for (const violation of agg.violations) {
637
- if (baselineViolationIds.has(violation.id)) {
638
- preExisting.push(violation.id);
639
- } else {
640
- newViolations.push(violation);
641
- }
642
- }
666
+ const classified = classifyViolations(agg.violations, baselineViolationIds);
667
+ newViolations.push(...classified.newViolations);
668
+ preExisting.push(...classified.preExisting);
643
669
  const currentViolationIds = new Set(agg.violations.map((v) => v.id));
644
- if (baselineCategory) {
645
- for (const id of baselineCategory.violationIds) {
646
- if (!currentViolationIds.has(id)) {
647
- resolvedViolations.push(id);
648
- }
649
- }
650
- }
670
+ resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
651
671
  if (baselineCategory && agg.value > baselineValue) {
652
672
  regressions.push({
653
673
  category,
@@ -657,16 +677,9 @@ function diff(current, baseline) {
657
677
  });
658
678
  }
659
679
  }
660
- for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
661
- if (!visitedCategories.has(category) && baselineCategory) {
662
- for (const id of baselineCategory.violationIds) {
663
- resolvedViolations.push(id);
664
- }
665
- }
666
- }
667
- const passed = newViolations.length === 0 && regressions.length === 0;
680
+ resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
668
681
  return {
669
- passed,
682
+ passed: newViolations.length === 0 && regressions.length === 0,
670
683
  newViolations,
671
684
  resolvedViolations,
672
685
  preExisting,
@@ -674,9 +687,6 @@ function diff(current, baseline) {
674
687
  };
675
688
  }
676
689
 
677
- // src/architecture/collectors/complexity.ts
678
- import { relative as relative4 } from "path";
679
-
680
690
  // src/entropy/detectors/complexity.ts
681
691
  import { readFile as readFile2 } from "fs/promises";
682
692
  var DEFAULT_THRESHOLDS = {
@@ -687,22 +697,22 @@ var DEFAULT_THRESHOLDS = {
687
697
  fileLength: { info: 300 },
688
698
  hotspotPercentile: { error: 95 }
689
699
  };
700
+ var FUNCTION_PATTERNS = [
701
+ // function declarations: function name(params) {
702
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
703
+ // method declarations: name(params) {
704
+ /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
705
+ // arrow functions assigned to const/let/var: const name = (params) =>
706
+ /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
707
+ // arrow functions assigned to const/let/var with single param: const name = param =>
708
+ /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
709
+ ];
690
710
  function extractFunctions(content) {
691
711
  const functions = [];
692
712
  const lines = content.split("\n");
693
- const patterns = [
694
- // function declarations: function name(params) {
695
- /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
696
- // method declarations: name(params) {
697
- /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
698
- // arrow functions assigned to const/let/var: const name = (params) =>
699
- /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
700
- // arrow functions assigned to const/let/var with single param: const name = param =>
701
- /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
702
- ];
703
713
  for (let i = 0; i < lines.length; i++) {
704
714
  const line = lines[i];
705
- for (const pattern of patterns) {
715
+ for (const pattern of FUNCTION_PATTERNS) {
706
716
  const match = line.match(pattern);
707
717
  if (match) {
708
718
  const name = match[1] ?? "anonymous";
@@ -791,26 +801,155 @@ function computeNestingDepth(body) {
791
801
  }
792
802
  return maxDepth;
793
803
  }
794
- async function detectComplexityViolations(snapshot, config, graphData) {
795
- const violations = [];
796
- const thresholds = {
804
+ function resolveThresholds(config) {
805
+ const userThresholds = config?.thresholds;
806
+ if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
807
+ return {
797
808
  cyclomaticComplexity: {
798
- error: config?.thresholds?.cyclomaticComplexity?.error ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.error,
799
- warn: config?.thresholds?.cyclomaticComplexity?.warn ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.warn
809
+ ...DEFAULT_THRESHOLDS.cyclomaticComplexity,
810
+ ...stripUndefined(userThresholds.cyclomaticComplexity)
800
811
  },
801
812
  nestingDepth: {
802
- warn: config?.thresholds?.nestingDepth?.warn ?? DEFAULT_THRESHOLDS.nestingDepth.warn
813
+ ...DEFAULT_THRESHOLDS.nestingDepth,
814
+ ...stripUndefined(userThresholds.nestingDepth)
803
815
  },
804
816
  functionLength: {
805
- warn: config?.thresholds?.functionLength?.warn ?? DEFAULT_THRESHOLDS.functionLength.warn
817
+ ...DEFAULT_THRESHOLDS.functionLength,
818
+ ...stripUndefined(userThresholds.functionLength)
806
819
  },
807
820
  parameterCount: {
808
- warn: config?.thresholds?.parameterCount?.warn ?? DEFAULT_THRESHOLDS.parameterCount.warn
821
+ ...DEFAULT_THRESHOLDS.parameterCount,
822
+ ...stripUndefined(userThresholds.parameterCount)
809
823
  },
810
- fileLength: {
811
- info: config?.thresholds?.fileLength?.info ?? DEFAULT_THRESHOLDS.fileLength.info
812
- }
824
+ fileLength: { ...DEFAULT_THRESHOLDS.fileLength, ...stripUndefined(userThresholds.fileLength) }
825
+ };
826
+ }
827
+ function stripUndefined(obj) {
828
+ if (!obj) return {};
829
+ const result = {};
830
+ for (const [key, val] of Object.entries(obj)) {
831
+ if (val !== void 0) result[key] = val;
832
+ }
833
+ return result;
834
+ }
835
+ function checkFileLengthViolation(filePath, lineCount, threshold) {
836
+ if (lineCount <= threshold) return null;
837
+ return {
838
+ file: filePath,
839
+ function: "<file>",
840
+ line: 1,
841
+ metric: "fileLength",
842
+ value: lineCount,
843
+ threshold,
844
+ tier: 3,
845
+ severity: "info",
846
+ message: `File has ${lineCount} lines (threshold: ${threshold})`
847
+ };
848
+ }
849
+ function checkCyclomaticComplexity(filePath, fn, thresholds) {
850
+ const complexity = computeCyclomaticComplexity(fn.body);
851
+ if (complexity > thresholds.error) {
852
+ return {
853
+ file: filePath,
854
+ function: fn.name,
855
+ line: fn.line,
856
+ metric: "cyclomaticComplexity",
857
+ value: complexity,
858
+ threshold: thresholds.error,
859
+ tier: 1,
860
+ severity: "error",
861
+ message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.error})`
862
+ };
863
+ }
864
+ if (complexity > thresholds.warn) {
865
+ return {
866
+ file: filePath,
867
+ function: fn.name,
868
+ line: fn.line,
869
+ metric: "cyclomaticComplexity",
870
+ value: complexity,
871
+ threshold: thresholds.warn,
872
+ tier: 2,
873
+ severity: "warning",
874
+ message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.warn})`
875
+ };
876
+ }
877
+ return null;
878
+ }
879
+ function checkNestingDepth(filePath, fn, threshold) {
880
+ const depth = computeNestingDepth(fn.body);
881
+ if (depth <= threshold) return null;
882
+ return {
883
+ file: filePath,
884
+ function: fn.name,
885
+ line: fn.line,
886
+ metric: "nestingDepth",
887
+ value: depth,
888
+ threshold,
889
+ tier: 2,
890
+ severity: "warning",
891
+ message: `Function "${fn.name}" has nesting depth of ${depth} (threshold: ${threshold})`
813
892
  };
893
+ }
894
+ function checkFunctionLength(filePath, fn, threshold) {
895
+ const fnLength = fn.endLine - fn.startLine + 1;
896
+ if (fnLength <= threshold) return null;
897
+ return {
898
+ file: filePath,
899
+ function: fn.name,
900
+ line: fn.line,
901
+ metric: "functionLength",
902
+ value: fnLength,
903
+ threshold,
904
+ tier: 2,
905
+ severity: "warning",
906
+ message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${threshold})`
907
+ };
908
+ }
909
+ function checkParameterCount(filePath, fn, threshold) {
910
+ if (fn.params <= threshold) return null;
911
+ return {
912
+ file: filePath,
913
+ function: fn.name,
914
+ line: fn.line,
915
+ metric: "parameterCount",
916
+ value: fn.params,
917
+ threshold,
918
+ tier: 2,
919
+ severity: "warning",
920
+ message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${threshold})`
921
+ };
922
+ }
923
+ function checkHotspot(filePath, fn, graphData) {
924
+ const hotspot = graphData.hotspots.find((h) => h.file === filePath && h.function === fn.name);
925
+ if (!hotspot || hotspot.hotspotScore <= graphData.percentile95Score) return null;
926
+ return {
927
+ file: filePath,
928
+ function: fn.name,
929
+ line: fn.line,
930
+ metric: "hotspotScore",
931
+ value: hotspot.hotspotScore,
932
+ threshold: graphData.percentile95Score,
933
+ tier: 1,
934
+ severity: "error",
935
+ message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
936
+ };
937
+ }
938
+ function collectFunctionViolations(filePath, fn, thresholds, graphData) {
939
+ const checks = [
940
+ checkCyclomaticComplexity(filePath, fn, thresholds.cyclomaticComplexity),
941
+ checkNestingDepth(filePath, fn, thresholds.nestingDepth.warn),
942
+ checkFunctionLength(filePath, fn, thresholds.functionLength.warn),
943
+ checkParameterCount(filePath, fn, thresholds.parameterCount.warn)
944
+ ];
945
+ if (graphData) {
946
+ checks.push(checkHotspot(filePath, fn, graphData));
947
+ }
948
+ return checks.filter((v) => v !== null);
949
+ }
950
+ async function detectComplexityViolations(snapshot, config, graphData) {
951
+ const violations = [];
952
+ const thresholds = resolveThresholds(config);
814
953
  let totalFunctions = 0;
815
954
  for (const file of snapshot.files) {
816
955
  let content;
@@ -820,107 +959,16 @@ async function detectComplexityViolations(snapshot, config, graphData) {
820
959
  continue;
821
960
  }
822
961
  const lines = content.split("\n");
823
- if (lines.length > thresholds.fileLength.info) {
824
- violations.push({
825
- file: file.path,
826
- function: "<file>",
827
- line: 1,
828
- metric: "fileLength",
829
- value: lines.length,
830
- threshold: thresholds.fileLength.info,
831
- tier: 3,
832
- severity: "info",
833
- message: `File has ${lines.length} lines (threshold: ${thresholds.fileLength.info})`
834
- });
835
- }
962
+ const fileLenViolation = checkFileLengthViolation(
963
+ file.path,
964
+ lines.length,
965
+ thresholds.fileLength.info
966
+ );
967
+ if (fileLenViolation) violations.push(fileLenViolation);
836
968
  const functions = extractFunctions(content);
837
969
  totalFunctions += functions.length;
838
970
  for (const fn of functions) {
839
- const complexity = computeCyclomaticComplexity(fn.body);
840
- if (complexity > thresholds.cyclomaticComplexity.error) {
841
- violations.push({
842
- file: file.path,
843
- function: fn.name,
844
- line: fn.line,
845
- metric: "cyclomaticComplexity",
846
- value: complexity,
847
- threshold: thresholds.cyclomaticComplexity.error,
848
- tier: 1,
849
- severity: "error",
850
- message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.cyclomaticComplexity.error})`
851
- });
852
- } else if (complexity > thresholds.cyclomaticComplexity.warn) {
853
- violations.push({
854
- file: file.path,
855
- function: fn.name,
856
- line: fn.line,
857
- metric: "cyclomaticComplexity",
858
- value: complexity,
859
- threshold: thresholds.cyclomaticComplexity.warn,
860
- tier: 2,
861
- severity: "warning",
862
- message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.cyclomaticComplexity.warn})`
863
- });
864
- }
865
- const nestingDepth = computeNestingDepth(fn.body);
866
- if (nestingDepth > thresholds.nestingDepth.warn) {
867
- violations.push({
868
- file: file.path,
869
- function: fn.name,
870
- line: fn.line,
871
- metric: "nestingDepth",
872
- value: nestingDepth,
873
- threshold: thresholds.nestingDepth.warn,
874
- tier: 2,
875
- severity: "warning",
876
- message: `Function "${fn.name}" has nesting depth of ${nestingDepth} (threshold: ${thresholds.nestingDepth.warn})`
877
- });
878
- }
879
- const fnLength = fn.endLine - fn.startLine + 1;
880
- if (fnLength > thresholds.functionLength.warn) {
881
- violations.push({
882
- file: file.path,
883
- function: fn.name,
884
- line: fn.line,
885
- metric: "functionLength",
886
- value: fnLength,
887
- threshold: thresholds.functionLength.warn,
888
- tier: 2,
889
- severity: "warning",
890
- message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${thresholds.functionLength.warn})`
891
- });
892
- }
893
- if (fn.params > thresholds.parameterCount.warn) {
894
- violations.push({
895
- file: file.path,
896
- function: fn.name,
897
- line: fn.line,
898
- metric: "parameterCount",
899
- value: fn.params,
900
- threshold: thresholds.parameterCount.warn,
901
- tier: 2,
902
- severity: "warning",
903
- message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${thresholds.parameterCount.warn})`
904
- });
905
- }
906
- if (graphData) {
907
- const hotspot = graphData.hotspots.find(
908
- (h) => h.file === file.path && h.function === fn.name
909
- );
910
- if (hotspot && hotspot.hotspotScore > graphData.percentile95Score) {
911
- violations.push({
912
- file: file.path,
913
- function: fn.name,
914
- line: fn.line,
915
- metric: "hotspotScore",
916
- value: hotspot.hotspotScore,
917
- threshold: graphData.percentile95Score,
918
- tier: 1,
919
- severity: "error",
920
- message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
921
- });
922
- }
923
- }
971
+ violations.push(...collectFunctionViolations(file.path, fn, thresholds, graphData));
924
972
  }
925
973
  }
926
974
  const errorCount = violations.filter((v) => v.severity === "error").length;
@@ -1000,7 +1048,7 @@ var ComplexityCollector = class {
1000
1048
  (v) => v.severity === "error" || v.severity === "warning"
1001
1049
  );
1002
1050
  const violations = filtered.map((v) => {
1003
- const relFile = relative4(rootDir, v.file);
1051
+ const relFile = relativePosix(rootDir, v.file);
1004
1052
  const idDetail = `${v.metric}:${v.function}`;
1005
1053
  return {
1006
1054
  id: violationId(relFile, this.category, idDetail),
@@ -1025,9 +1073,6 @@ var ComplexityCollector = class {
1025
1073
  }
1026
1074
  };
1027
1075
 
1028
- // src/architecture/collectors/coupling.ts
1029
- import { relative as relative5 } from "path";
1030
-
1031
1076
  // src/entropy/detectors/coupling.ts
1032
1077
  var DEFAULT_THRESHOLDS2 = {
1033
1078
  fanOut: { warn: 15 },
@@ -1229,7 +1274,7 @@ var CouplingCollector = class {
1229
1274
  (v) => v.severity === "error" || v.severity === "warning"
1230
1275
  );
1231
1276
  const violations = filtered.map((v) => {
1232
- const relFile = relative5(rootDir, v.file);
1277
+ const relFile = relativePosix(rootDir, v.file);
1233
1278
  const idDetail = `${v.metric}`;
1234
1279
  return {
1235
1280
  id: violationId(relFile, this.category, idDetail),
@@ -1252,7 +1297,6 @@ var CouplingCollector = class {
1252
1297
  };
1253
1298
 
1254
1299
  // src/architecture/collectors/forbidden-imports.ts
1255
- import { relative as relative6 } from "path";
1256
1300
  var ForbiddenImportCollector = class {
1257
1301
  category = "forbidden-imports";
1258
1302
  getRules(_config, _rootDir) {
@@ -1296,8 +1340,8 @@ var ForbiddenImportCollector = class {
1296
1340
  (v) => v.reason === "FORBIDDEN_IMPORT"
1297
1341
  );
1298
1342
  const violations = forbidden.map((v) => {
1299
- const relFile = relative6(rootDir, v.file);
1300
- const relImport = relative6(rootDir, v.imports);
1343
+ const relFile = relativePosix(rootDir, v.file);
1344
+ const relImport = relativePosix(rootDir, v.imports);
1301
1345
  const detail = `forbidden import: ${relFile} -> ${relImport}`;
1302
1346
  return {
1303
1347
  id: violationId(relFile, this.category, detail),
@@ -1320,7 +1364,7 @@ var ForbiddenImportCollector = class {
1320
1364
 
1321
1365
  // src/architecture/collectors/module-size.ts
1322
1366
  import { readFile as readFile3, readdir } from "fs/promises";
1323
- import { join as join2, relative as relative7 } from "path";
1367
+ import { join as join2 } from "path";
1324
1368
  async function discoverModules(rootDir) {
1325
1369
  const modules = [];
1326
1370
  async function scanDir(dir) {
@@ -1353,10 +1397,10 @@ async function discoverModules(rootDir) {
1353
1397
  }
1354
1398
  }
1355
1399
  modules.push({
1356
- modulePath: relative7(rootDir, dir),
1400
+ modulePath: relativePosix(rootDir, dir),
1357
1401
  fileCount: tsFiles.length,
1358
1402
  totalLoc,
1359
- files: tsFiles.map((f) => relative7(rootDir, f))
1403
+ files: tsFiles.map((f) => relativePosix(rootDir, f))
1360
1404
  });
1361
1405
  }
1362
1406
  for (const sub of subdirs) {
@@ -1448,7 +1492,7 @@ var ModuleSizeCollector = class {
1448
1492
 
1449
1493
  // src/architecture/collectors/dep-depth.ts
1450
1494
  import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
1451
- import { join as join3, relative as relative8, dirname as dirname3, resolve as resolve2 } from "path";
1495
+ import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
1452
1496
  function extractImportSources(content, filePath) {
1453
1497
  const importRegex = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
1454
1498
  const dynamicRegex = /import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
@@ -1532,7 +1576,7 @@ var DepDepthCollector = class {
1532
1576
  }
1533
1577
  const moduleMap = /* @__PURE__ */ new Map();
1534
1578
  for (const file of allFiles) {
1535
- const relDir = relative8(rootDir, dirname3(file));
1579
+ const relDir = relativePosix(rootDir, dirname3(file));
1536
1580
  if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
1537
1581
  moduleMap.get(relDir).push(file);
1538
1582
  }
@@ -1790,6 +1834,7 @@ export {
1790
1834
  fileExists,
1791
1835
  readFileContent,
1792
1836
  findFiles,
1837
+ relativePosix,
1793
1838
  defineLayer,
1794
1839
  resolveFileToLayer,
1795
1840
  buildDependencyGraph,