@harness-engineering/core 0.21.0 → 0.21.2

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.
@@ -389,6 +389,28 @@ async function detectCircularDepsInFiles(files, parser, graphDependencyData) {
389
389
  }
390
390
 
391
391
  // src/architecture/collectors/circular-deps.ts
392
+ function makeStubParser() {
393
+ return {
394
+ name: "typescript",
395
+ extensions: [".ts", ".tsx"],
396
+ parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
397
+ extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
398
+ extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
399
+ health: async () => ({ ok: true, value: { available: true } })
400
+ };
401
+ }
402
+ function mapCycleViolations(cycles, rootDir, category) {
403
+ return cycles.map((cycle) => {
404
+ const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
405
+ const firstFile = relativePosix(rootDir, cycle.cycle[0]);
406
+ return {
407
+ id: violationId(firstFile, category, cyclePath),
408
+ file: firstFile,
409
+ detail: `Circular dependency: ${cyclePath}`,
410
+ severity: cycle.severity
411
+ };
412
+ });
413
+ }
392
414
  var CircularDepsCollector = class {
393
415
  category = "circular-deps";
394
416
  getRules(_config, _rootDir) {
@@ -404,21 +426,7 @@ var CircularDepsCollector = class {
404
426
  }
405
427
  async collect(_config, rootDir) {
406
428
  const files = await findFiles("**/*.ts", rootDir);
407
- const stubParser = {
408
- name: "typescript",
409
- extensions: [".ts", ".tsx"],
410
- parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
411
- extractImports: () => ({
412
- ok: false,
413
- error: { code: "EXTRACT_ERROR", message: "not needed" }
414
- }),
415
- extractExports: () => ({
416
- ok: false,
417
- error: { code: "EXTRACT_ERROR", message: "not needed" }
418
- }),
419
- health: async () => ({ ok: true, value: { available: true } })
420
- };
421
- const graphResult = await buildDependencyGraph(files, stubParser);
429
+ const graphResult = await buildDependencyGraph(files, makeStubParser());
422
430
  if (!graphResult.ok) {
423
431
  return [
424
432
  {
@@ -443,16 +451,7 @@ var CircularDepsCollector = class {
443
451
  ];
444
452
  }
445
453
  const { cycles, largestCycle } = result.value;
446
- const violations = cycles.map((cycle) => {
447
- const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
448
- const firstFile = relativePosix(rootDir, cycle.cycle[0]);
449
- return {
450
- id: violationId(firstFile, this.category, cyclePath),
451
- file: firstFile,
452
- detail: `Circular dependency: ${cyclePath}`,
453
- severity: cycle.severity
454
- };
455
- });
454
+ const violations = mapCycleViolations(cycles, rootDir, this.category);
456
455
  return [
457
456
  {
458
457
  category: this.category,
@@ -466,6 +465,30 @@ var CircularDepsCollector = class {
466
465
  };
467
466
 
468
467
  // src/architecture/collectors/layer-violations.ts
468
+ function makeLayerStubParser() {
469
+ return {
470
+ name: "typescript",
471
+ extensions: [".ts", ".tsx"],
472
+ parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
473
+ extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
474
+ extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
475
+ health: async () => ({ ok: true, value: { available: true } })
476
+ };
477
+ }
478
+ function mapLayerViolations(layerViolations, rootDir, category) {
479
+ return layerViolations.map((v) => {
480
+ const relFile = relativePosix(rootDir, v.file);
481
+ const relImport = relativePosix(rootDir, v.imports);
482
+ const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
483
+ return {
484
+ id: violationId(relFile, category ?? "", detail),
485
+ file: relFile,
486
+ category,
487
+ detail,
488
+ severity: "error"
489
+ };
490
+ });
491
+ }
469
492
  var LayerViolationCollector = class {
470
493
  category = "layer-violations";
471
494
  getRules(_config, _rootDir) {
@@ -480,18 +503,10 @@ var LayerViolationCollector = class {
480
503
  ];
481
504
  }
482
505
  async collect(_config, rootDir) {
483
- const stubParser = {
484
- name: "typescript",
485
- extensions: [".ts", ".tsx"],
486
- parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
487
- extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
488
- extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
489
- health: async () => ({ ok: true, value: { available: true } })
490
- };
491
506
  const result = await validateDependencies({
492
507
  layers: [],
493
508
  rootDir,
494
- parser: stubParser,
509
+ parser: makeLayerStubParser(),
495
510
  fallbackBehavior: "skip"
496
511
  });
497
512
  if (!result.ok) {
@@ -505,29 +520,12 @@ var LayerViolationCollector = class {
505
520
  }
506
521
  ];
507
522
  }
508
- const layerViolations = result.value.violations.filter(
509
- (v) => v.reason === "WRONG_LAYER"
523
+ const violations = mapLayerViolations(
524
+ result.value.violations.filter((v) => v.reason === "WRONG_LAYER"),
525
+ rootDir,
526
+ this.category
510
527
  );
511
- const violations = layerViolations.map((v) => {
512
- const relFile = relativePosix(rootDir, v.file);
513
- const relImport = relativePosix(rootDir, v.imports);
514
- const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
515
- return {
516
- id: violationId(relFile, this.category, detail),
517
- file: relFile,
518
- category: this.category,
519
- detail,
520
- severity: "error"
521
- };
522
- });
523
- return [
524
- {
525
- category: this.category,
526
- scope: "project",
527
- value: violations.length,
528
- violations
529
- }
530
- ];
528
+ return [{ category: this.category, scope: "project", value: violations.length, violations }];
531
529
  }
532
530
  };
533
531
 
@@ -651,39 +649,43 @@ function collectOrphanedBaselineViolations(baseline, visitedCategories) {
651
649
  }
652
650
  return resolved;
653
651
  }
652
+ function diffCategory(category, agg, baselineCategory, acc) {
653
+ const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
654
+ const baselineValue = baselineCategory?.value ?? 0;
655
+ const classified = classifyViolations(agg.violations, baselineViolationIds);
656
+ acc.newViolations.push(...classified.newViolations);
657
+ acc.preExisting.push(...classified.preExisting);
658
+ const currentViolationIds = new Set(agg.violations.map((v) => v.id));
659
+ acc.resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
660
+ if (baselineCategory && agg.value > baselineValue) {
661
+ acc.regressions.push({
662
+ category,
663
+ baselineValue,
664
+ currentValue: agg.value,
665
+ delta: agg.value - baselineValue
666
+ });
667
+ }
668
+ }
654
669
  function diff(current, baseline) {
655
670
  const aggregated = aggregateByCategory(current);
656
- const newViolations = [];
657
- const resolvedViolations = [];
658
- const preExisting = [];
659
- const regressions = [];
671
+ const acc = {
672
+ newViolations: [],
673
+ resolvedViolations: [],
674
+ preExisting: [],
675
+ regressions: []
676
+ };
660
677
  const visitedCategories = /* @__PURE__ */ new Set();
661
678
  for (const [category, agg] of aggregated) {
662
679
  visitedCategories.add(category);
663
- const baselineCategory = baseline.metrics[category];
664
- const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
665
- const baselineValue = baselineCategory?.value ?? 0;
666
- const classified = classifyViolations(agg.violations, baselineViolationIds);
667
- newViolations.push(...classified.newViolations);
668
- preExisting.push(...classified.preExisting);
669
- const currentViolationIds = new Set(agg.violations.map((v) => v.id));
670
- resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
671
- if (baselineCategory && agg.value > baselineValue) {
672
- regressions.push({
673
- category,
674
- baselineValue,
675
- currentValue: agg.value,
676
- delta: agg.value - baselineValue
677
- });
678
- }
680
+ diffCategory(category, agg, baseline.metrics[category], acc);
679
681
  }
680
- resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
682
+ acc.resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
681
683
  return {
682
- passed: newViolations.length === 0 && regressions.length === 0,
683
- newViolations,
684
- resolvedViolations,
685
- preExisting,
686
- regressions
684
+ passed: acc.newViolations.length === 0 && acc.regressions.length === 0,
685
+ newViolations: acc.newViolations,
686
+ resolvedViolations: acc.resolvedViolations,
687
+ preExisting: acc.preExisting,
688
+ regressions: acc.regressions
687
689
  };
688
690
  }
689
691
 
@@ -753,26 +755,28 @@ function findFunctionEnd(lines, startIdx) {
753
755
  }
754
756
  return lines.length - 1;
755
757
  }
756
- function computeCyclomaticComplexity(body) {
757
- let complexity = 1;
758
- const decisionPatterns = [
759
- /\bif\s*\(/g,
760
- /\belse\s+if\s*\(/g,
761
- /\bwhile\s*\(/g,
762
- /\bfor\s*\(/g,
763
- /\bcase\s+/g,
764
- /&&/g,
765
- /\|\|/g,
766
- /\?(?!=)/g,
767
- // Ternary ? but not ?. or ??
768
- /\bcatch\s*\(/g
769
- ];
770
- for (const pattern of decisionPatterns) {
758
+ var DECISION_PATTERNS = [
759
+ /\bif\s*\(/g,
760
+ /\belse\s+if\s*\(/g,
761
+ /\bwhile\s*\(/g,
762
+ /\bfor\s*\(/g,
763
+ /\bcase\s+/g,
764
+ /&&/g,
765
+ /\|\|/g,
766
+ /\?(?!=)/g,
767
+ // Ternary ? but not ?. or ??
768
+ /\bcatch\s*\(/g
769
+ ];
770
+ function countDecisionPoints(body) {
771
+ let count = 0;
772
+ for (const pattern of DECISION_PATTERNS) {
771
773
  const matches = body.match(pattern);
772
- if (matches) {
773
- complexity += matches.length;
774
- }
774
+ if (matches) count += matches.length;
775
775
  }
776
+ return count;
777
+ }
778
+ function computeCyclomaticComplexity(body) {
779
+ let complexity = 1 + countDecisionPoints(body);
776
780
  const elseIfMatches = body.match(/\belse\s+if\s*\(/g);
777
781
  if (elseIfMatches) {
778
782
  complexity -= elseIfMatches.length;
@@ -988,6 +992,42 @@ async function detectComplexityViolations(snapshot, config, graphData) {
988
992
  }
989
993
 
990
994
  // src/architecture/collectors/complexity.ts
995
+ function buildSnapshot(files, rootDir) {
996
+ return {
997
+ files: files.map((f) => ({
998
+ path: f,
999
+ ast: { type: "Program", body: null, language: "typescript" },
1000
+ imports: [],
1001
+ exports: [],
1002
+ internalSymbols: [],
1003
+ jsDocComments: []
1004
+ })),
1005
+ dependencyGraph: { nodes: [], edges: [] },
1006
+ exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
1007
+ docs: [],
1008
+ codeReferences: [],
1009
+ entryPoints: [],
1010
+ rootDir,
1011
+ config: { rootDir, analyze: {} },
1012
+ buildTime: 0
1013
+ };
1014
+ }
1015
+ function resolveMaxComplexity(config) {
1016
+ const threshold = config.thresholds.complexity;
1017
+ return typeof threshold === "number" ? threshold : threshold?.max ?? 15;
1018
+ }
1019
+ function mapComplexityViolations(complexityViolations, rootDir, category) {
1020
+ return complexityViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
1021
+ const relFile = relativePosix(rootDir, v.file);
1022
+ return {
1023
+ id: violationId(relFile, category ?? "", `${v.metric}:${v.function}`),
1024
+ file: relFile,
1025
+ category,
1026
+ detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
1027
+ severity: v.severity
1028
+ };
1029
+ });
1030
+ }
991
1031
  var ComplexityCollector = class {
992
1032
  category = "complexity";
993
1033
  getRules(_config, _rootDir) {
@@ -1003,32 +1043,11 @@ var ComplexityCollector = class {
1003
1043
  }
1004
1044
  async collect(_config, rootDir) {
1005
1045
  const files = await findFiles("**/*.ts", rootDir);
1006
- const snapshot = {
1007
- files: files.map((f) => ({
1008
- path: f,
1009
- ast: { type: "Program", body: null, language: "typescript" },
1010
- imports: [],
1011
- exports: [],
1012
- internalSymbols: [],
1013
- jsDocComments: []
1014
- })),
1015
- dependencyGraph: { nodes: [], edges: [] },
1016
- exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
1017
- docs: [],
1018
- codeReferences: [],
1019
- entryPoints: [],
1020
- rootDir,
1021
- config: { rootDir, analyze: {} },
1022
- buildTime: 0
1023
- };
1024
- const complexityThreshold = _config.thresholds.complexity;
1025
- const maxComplexity = typeof complexityThreshold === "number" ? complexityThreshold : complexityThreshold?.max ?? 15;
1046
+ const snapshot = buildSnapshot(files, rootDir);
1047
+ const maxComplexity = resolveMaxComplexity(_config);
1026
1048
  const complexityConfig = {
1027
1049
  thresholds: {
1028
- cyclomaticComplexity: {
1029
- error: maxComplexity,
1030
- warn: Math.floor(maxComplexity * 0.7)
1031
- }
1050
+ cyclomaticComplexity: { error: maxComplexity, warn: Math.floor(maxComplexity * 0.7) }
1032
1051
  }
1033
1052
  };
1034
1053
  const result = await detectComplexityViolations(snapshot, complexityConfig);
@@ -1044,20 +1063,7 @@ var ComplexityCollector = class {
1044
1063
  ];
1045
1064
  }
1046
1065
  const { violations: complexityViolations, stats } = result.value;
1047
- const filtered = complexityViolations.filter(
1048
- (v) => v.severity === "error" || v.severity === "warning"
1049
- );
1050
- const violations = filtered.map((v) => {
1051
- const relFile = relativePosix(rootDir, v.file);
1052
- const idDetail = `${v.metric}:${v.function}`;
1053
- return {
1054
- id: violationId(relFile, this.category, idDetail),
1055
- file: relFile,
1056
- category: this.category,
1057
- detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
1058
- severity: v.severity
1059
- };
1060
- });
1066
+ const violations = mapComplexityViolations(complexityViolations, rootDir, this.category);
1061
1067
  return [
1062
1068
  {
1063
1069
  category: this.category,
@@ -1136,8 +1142,8 @@ function resolveImportSource(source, fromFile, snapshot) {
1136
1142
  }
1137
1143
  return void 0;
1138
1144
  }
1139
- function checkViolations(metrics, config) {
1140
- const thresholds = {
1145
+ function resolveThresholds2(config) {
1146
+ return {
1141
1147
  fanOut: { ...DEFAULT_THRESHOLDS2.fanOut, ...config?.thresholds?.fanOut },
1142
1148
  fanIn: { ...DEFAULT_THRESHOLDS2.fanIn, ...config?.thresholds?.fanIn },
1143
1149
  couplingRatio: { ...DEFAULT_THRESHOLDS2.couplingRatio, ...config?.thresholds?.couplingRatio },
@@ -1146,53 +1152,70 @@ function checkViolations(metrics, config) {
1146
1152
  ...config?.thresholds?.transitiveDependencyDepth
1147
1153
  }
1148
1154
  };
1155
+ }
1156
+ function checkFanOut(m, threshold) {
1157
+ if (m.fanOut <= threshold) return null;
1158
+ return {
1159
+ file: m.file,
1160
+ metric: "fanOut",
1161
+ value: m.fanOut,
1162
+ threshold,
1163
+ tier: 2,
1164
+ severity: "warning",
1165
+ message: `File has ${m.fanOut} imports (threshold: ${threshold})`
1166
+ };
1167
+ }
1168
+ function checkFanIn(m, threshold) {
1169
+ if (m.fanIn <= threshold) return null;
1170
+ return {
1171
+ file: m.file,
1172
+ metric: "fanIn",
1173
+ value: m.fanIn,
1174
+ threshold,
1175
+ tier: 3,
1176
+ severity: "info",
1177
+ message: `File is imported by ${m.fanIn} files (threshold: ${threshold})`
1178
+ };
1179
+ }
1180
+ function checkCouplingRatio(m, threshold) {
1181
+ const totalConnections = m.fanIn + m.fanOut;
1182
+ if (totalConnections <= 5 || m.couplingRatio <= threshold) return null;
1183
+ return {
1184
+ file: m.file,
1185
+ metric: "couplingRatio",
1186
+ value: m.couplingRatio,
1187
+ threshold,
1188
+ tier: 2,
1189
+ severity: "warning",
1190
+ message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${threshold})`
1191
+ };
1192
+ }
1193
+ function checkTransitiveDepth(m, threshold) {
1194
+ if (m.transitiveDepth <= threshold) return null;
1195
+ return {
1196
+ file: m.file,
1197
+ metric: "transitiveDependencyDepth",
1198
+ value: m.transitiveDepth,
1199
+ threshold,
1200
+ tier: 3,
1201
+ severity: "info",
1202
+ message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${threshold})`
1203
+ };
1204
+ }
1205
+ function checkMetricViolations(m, thresholds) {
1206
+ const candidates = [
1207
+ checkFanOut(m, thresholds.fanOut.warn),
1208
+ checkFanIn(m, thresholds.fanIn.info),
1209
+ checkCouplingRatio(m, thresholds.couplingRatio.warn),
1210
+ checkTransitiveDepth(m, thresholds.transitiveDependencyDepth.info)
1211
+ ];
1212
+ return candidates.filter((v) => v !== null);
1213
+ }
1214
+ function checkViolations(metrics, config) {
1215
+ const thresholds = resolveThresholds2(config);
1149
1216
  const violations = [];
1150
1217
  for (const m of metrics) {
1151
- if (thresholds.fanOut.warn !== void 0 && m.fanOut > thresholds.fanOut.warn) {
1152
- violations.push({
1153
- file: m.file,
1154
- metric: "fanOut",
1155
- value: m.fanOut,
1156
- threshold: thresholds.fanOut.warn,
1157
- tier: 2,
1158
- severity: "warning",
1159
- message: `File has ${m.fanOut} imports (threshold: ${thresholds.fanOut.warn})`
1160
- });
1161
- }
1162
- if (thresholds.fanIn.info !== void 0 && m.fanIn > thresholds.fanIn.info) {
1163
- violations.push({
1164
- file: m.file,
1165
- metric: "fanIn",
1166
- value: m.fanIn,
1167
- threshold: thresholds.fanIn.info,
1168
- tier: 3,
1169
- severity: "info",
1170
- message: `File is imported by ${m.fanIn} files (threshold: ${thresholds.fanIn.info})`
1171
- });
1172
- }
1173
- const totalConnections = m.fanIn + m.fanOut;
1174
- if (totalConnections > 5 && thresholds.couplingRatio.warn !== void 0 && m.couplingRatio > thresholds.couplingRatio.warn) {
1175
- violations.push({
1176
- file: m.file,
1177
- metric: "couplingRatio",
1178
- value: m.couplingRatio,
1179
- threshold: thresholds.couplingRatio.warn,
1180
- tier: 2,
1181
- severity: "warning",
1182
- message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${thresholds.couplingRatio.warn})`
1183
- });
1184
- }
1185
- if (thresholds.transitiveDependencyDepth.info !== void 0 && m.transitiveDepth > thresholds.transitiveDependencyDepth.info) {
1186
- violations.push({
1187
- file: m.file,
1188
- metric: "transitiveDependencyDepth",
1189
- value: m.transitiveDepth,
1190
- threshold: thresholds.transitiveDependencyDepth.info,
1191
- tier: 3,
1192
- severity: "info",
1193
- message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${thresholds.transitiveDependencyDepth.info})`
1194
- });
1195
- }
1218
+ violations.push(...checkMetricViolations(m, thresholds));
1196
1219
  }
1197
1220
  return violations;
1198
1221
  }
@@ -1224,6 +1247,38 @@ async function detectCouplingViolations(snapshot, config, graphData) {
1224
1247
  }
1225
1248
 
1226
1249
  // src/architecture/collectors/coupling.ts
1250
+ function buildCouplingSnapshot(files, rootDir) {
1251
+ return {
1252
+ files: files.map((f) => ({
1253
+ path: f,
1254
+ ast: { type: "Program", body: null, language: "typescript" },
1255
+ imports: [],
1256
+ exports: [],
1257
+ internalSymbols: [],
1258
+ jsDocComments: []
1259
+ })),
1260
+ dependencyGraph: { nodes: [], edges: [] },
1261
+ exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
1262
+ docs: [],
1263
+ codeReferences: [],
1264
+ entryPoints: [],
1265
+ rootDir,
1266
+ config: { rootDir, analyze: {} },
1267
+ buildTime: 0
1268
+ };
1269
+ }
1270
+ function mapCouplingViolations(couplingViolations, rootDir, category) {
1271
+ return couplingViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
1272
+ const relFile = relativePosix(rootDir, v.file);
1273
+ return {
1274
+ id: violationId(relFile, category ?? "", v.metric),
1275
+ file: relFile,
1276
+ category,
1277
+ detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
1278
+ severity: v.severity
1279
+ };
1280
+ });
1281
+ }
1227
1282
  var CouplingCollector = class {
1228
1283
  category = "coupling";
1229
1284
  getRules(_config, _rootDir) {
@@ -1239,24 +1294,7 @@ var CouplingCollector = class {
1239
1294
  }
1240
1295
  async collect(_config, rootDir) {
1241
1296
  const files = await findFiles("**/*.ts", rootDir);
1242
- const snapshot = {
1243
- files: files.map((f) => ({
1244
- path: f,
1245
- ast: { type: "Program", body: null, language: "typescript" },
1246
- imports: [],
1247
- exports: [],
1248
- internalSymbols: [],
1249
- jsDocComments: []
1250
- })),
1251
- dependencyGraph: { nodes: [], edges: [] },
1252
- exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
1253
- docs: [],
1254
- codeReferences: [],
1255
- entryPoints: [],
1256
- rootDir,
1257
- config: { rootDir, analyze: {} },
1258
- buildTime: 0
1259
- };
1297
+ const snapshot = buildCouplingSnapshot(files, rootDir);
1260
1298
  const result = await detectCouplingViolations(snapshot);
1261
1299
  if (!result.ok) {
1262
1300
  return [
@@ -1270,20 +1308,7 @@ var CouplingCollector = class {
1270
1308
  ];
1271
1309
  }
1272
1310
  const { violations: couplingViolations, stats } = result.value;
1273
- const filtered = couplingViolations.filter(
1274
- (v) => v.severity === "error" || v.severity === "warning"
1275
- );
1276
- const violations = filtered.map((v) => {
1277
- const relFile = relativePosix(rootDir, v.file);
1278
- const idDetail = `${v.metric}`;
1279
- return {
1280
- id: violationId(relFile, this.category, idDetail),
1281
- file: relFile,
1282
- category: this.category,
1283
- detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
1284
- severity: v.severity
1285
- };
1286
- });
1311
+ const violations = mapCouplingViolations(couplingViolations, rootDir, this.category);
1287
1312
  return [
1288
1313
  {
1289
1314
  category: this.category,
@@ -1297,6 +1322,30 @@ var CouplingCollector = class {
1297
1322
  };
1298
1323
 
1299
1324
  // src/architecture/collectors/forbidden-imports.ts
1325
+ function makeForbiddenStubParser() {
1326
+ return {
1327
+ name: "typescript",
1328
+ extensions: [".ts", ".tsx"],
1329
+ parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
1330
+ extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
1331
+ extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
1332
+ health: async () => ({ ok: true, value: { available: true } })
1333
+ };
1334
+ }
1335
+ function mapForbiddenImportViolations(forbidden, rootDir, category) {
1336
+ return forbidden.map((v) => {
1337
+ const relFile = relativePosix(rootDir, v.file);
1338
+ const relImport = relativePosix(rootDir, v.imports);
1339
+ const detail = `forbidden import: ${relFile} -> ${relImport}`;
1340
+ return {
1341
+ id: violationId(relFile, category ?? "", detail),
1342
+ file: relFile,
1343
+ category,
1344
+ detail,
1345
+ severity: "error"
1346
+ };
1347
+ });
1348
+ }
1300
1349
  var ForbiddenImportCollector = class {
1301
1350
  category = "forbidden-imports";
1302
1351
  getRules(_config, _rootDir) {
@@ -1311,18 +1360,10 @@ var ForbiddenImportCollector = class {
1311
1360
  ];
1312
1361
  }
1313
1362
  async collect(_config, rootDir) {
1314
- const stubParser = {
1315
- name: "typescript",
1316
- extensions: [".ts", ".tsx"],
1317
- parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
1318
- extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
1319
- extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
1320
- health: async () => ({ ok: true, value: { available: true } })
1321
- };
1322
1363
  const result = await validateDependencies({
1323
1364
  layers: [],
1324
1365
  rootDir,
1325
- parser: stubParser,
1366
+ parser: makeForbiddenStubParser(),
1326
1367
  fallbackBehavior: "skip"
1327
1368
  });
1328
1369
  if (!result.ok) {
@@ -1336,91 +1377,94 @@ var ForbiddenImportCollector = class {
1336
1377
  }
1337
1378
  ];
1338
1379
  }
1339
- const forbidden = result.value.violations.filter(
1340
- (v) => v.reason === "FORBIDDEN_IMPORT"
1380
+ const violations = mapForbiddenImportViolations(
1381
+ result.value.violations.filter((v) => v.reason === "FORBIDDEN_IMPORT"),
1382
+ rootDir,
1383
+ this.category
1341
1384
  );
1342
- const violations = forbidden.map((v) => {
1343
- const relFile = relativePosix(rootDir, v.file);
1344
- const relImport = relativePosix(rootDir, v.imports);
1345
- const detail = `forbidden import: ${relFile} -> ${relImport}`;
1346
- return {
1347
- id: violationId(relFile, this.category, detail),
1348
- file: relFile,
1349
- category: this.category,
1350
- detail,
1351
- severity: "error"
1352
- };
1353
- });
1354
- return [
1355
- {
1356
- category: this.category,
1357
- scope: "project",
1358
- value: violations.length,
1359
- violations
1360
- }
1361
- ];
1385
+ return [{ category: this.category, scope: "project", value: violations.length, violations }];
1362
1386
  }
1363
1387
  };
1364
1388
 
1365
1389
  // src/architecture/collectors/module-size.ts
1366
1390
  import { readFile as readFile3, readdir } from "fs/promises";
1367
1391
  import { join as join2 } from "path";
1368
- async function discoverModules(rootDir) {
1369
- const modules = [];
1370
- async function scanDir(dir) {
1371
- let entries;
1372
- try {
1373
- entries = await readdir(dir, { withFileTypes: true });
1374
- } catch {
1375
- return;
1376
- }
1377
- const tsFiles = [];
1378
- const subdirs = [];
1379
- for (const entry of entries) {
1380
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") {
1381
- continue;
1382
- }
1383
- const fullPath = join2(dir, entry.name);
1384
- if (entry.isDirectory()) {
1385
- subdirs.push(fullPath);
1386
- } 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")) {
1387
- tsFiles.push(fullPath);
1388
- }
1389
- }
1390
- if (tsFiles.length > 0) {
1391
- let totalLoc = 0;
1392
- for (const f of tsFiles) {
1393
- try {
1394
- const content = await readFile3(f, "utf-8");
1395
- totalLoc += content.split("\n").filter((line) => line.trim().length > 0).length;
1396
- } catch {
1397
- }
1398
- }
1399
- modules.push({
1400
- modulePath: relativePosix(rootDir, dir),
1401
- fileCount: tsFiles.length,
1402
- totalLoc,
1403
- files: tsFiles.map((f) => relativePosix(rootDir, f))
1404
- });
1392
+ function isSkippedEntry(name) {
1393
+ return name.startsWith(".") || name === "node_modules" || name === "dist";
1394
+ }
1395
+ function isTsSourceFile(name) {
1396
+ if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
1397
+ if (name.endsWith(".test.ts") || name.endsWith(".test.tsx") || name.endsWith(".spec.ts"))
1398
+ return false;
1399
+ return true;
1400
+ }
1401
+ async function countLoc(filePath) {
1402
+ try {
1403
+ const content = await readFile3(filePath, "utf-8");
1404
+ return content.split("\n").filter((line) => line.trim().length > 0).length;
1405
+ } catch {
1406
+ return 0;
1407
+ }
1408
+ }
1409
+ async function buildModuleStats(rootDir, dir, tsFiles) {
1410
+ let totalLoc = 0;
1411
+ for (const f of tsFiles) {
1412
+ totalLoc += await countLoc(f);
1413
+ }
1414
+ return {
1415
+ modulePath: relativePosix(rootDir, dir),
1416
+ fileCount: tsFiles.length,
1417
+ totalLoc,
1418
+ files: tsFiles.map((f) => relativePosix(rootDir, f))
1419
+ };
1420
+ }
1421
+ async function scanDir(rootDir, dir, modules) {
1422
+ let entries;
1423
+ try {
1424
+ entries = await readdir(dir, { withFileTypes: true });
1425
+ } catch {
1426
+ return;
1427
+ }
1428
+ const tsFiles = [];
1429
+ const subdirs = [];
1430
+ for (const entry of entries) {
1431
+ if (isSkippedEntry(entry.name)) continue;
1432
+ const fullPath = join2(dir, entry.name);
1433
+ if (entry.isDirectory()) {
1434
+ subdirs.push(fullPath);
1435
+ continue;
1405
1436
  }
1406
- for (const sub of subdirs) {
1407
- await scanDir(sub);
1437
+ if (entry.isFile() && isTsSourceFile(entry.name)) {
1438
+ tsFiles.push(fullPath);
1408
1439
  }
1409
1440
  }
1410
- await scanDir(rootDir);
1441
+ if (tsFiles.length > 0) {
1442
+ modules.push(await buildModuleStats(rootDir, dir, tsFiles));
1443
+ }
1444
+ for (const sub of subdirs) {
1445
+ await scanDir(rootDir, sub, modules);
1446
+ }
1447
+ }
1448
+ async function discoverModules(rootDir) {
1449
+ const modules = [];
1450
+ await scanDir(rootDir, rootDir, modules);
1411
1451
  return modules;
1412
1452
  }
1453
+ function extractThresholds(config) {
1454
+ const thresholds = config.thresholds["module-size"];
1455
+ let maxLoc = Infinity;
1456
+ let maxFiles = Infinity;
1457
+ if (typeof thresholds === "object" && thresholds !== null) {
1458
+ const t = thresholds;
1459
+ if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
1460
+ if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
1461
+ }
1462
+ return { maxLoc, maxFiles };
1463
+ }
1413
1464
  var ModuleSizeCollector = class {
1414
1465
  category = "module-size";
1415
1466
  getRules(config, _rootDir) {
1416
- const thresholds = config.thresholds["module-size"];
1417
- let maxLoc = Infinity;
1418
- let maxFiles = Infinity;
1419
- if (typeof thresholds === "object" && thresholds !== null) {
1420
- const t = thresholds;
1421
- if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
1422
- if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
1423
- }
1467
+ const { maxLoc, maxFiles } = extractThresholds(config);
1424
1468
  const rules = [];
1425
1469
  if (maxLoc < Infinity) {
1426
1470
  const desc = `Module LOC must not exceed ${maxLoc}`;
@@ -1453,14 +1497,7 @@ var ModuleSizeCollector = class {
1453
1497
  }
1454
1498
  async collect(config, rootDir) {
1455
1499
  const modules = await discoverModules(rootDir);
1456
- const thresholds = config.thresholds["module-size"];
1457
- let maxLoc = Infinity;
1458
- let maxFiles = Infinity;
1459
- if (typeof thresholds === "object" && thresholds !== null) {
1460
- const t = thresholds;
1461
- if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
1462
- if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
1463
- }
1500
+ const { maxLoc, maxFiles } = extractThresholds(config);
1464
1501
  return modules.map((mod) => {
1465
1502
  const violations = [];
1466
1503
  if (mod.totalLoc > maxLoc) {
@@ -1510,27 +1547,33 @@ function extractImportSources(content, filePath) {
1510
1547
  }
1511
1548
  return sources;
1512
1549
  }
1513
- async function collectTsFiles(dir) {
1514
- const results = [];
1515
- async function scan(d) {
1516
- let entries;
1517
- try {
1518
- entries = await readdir2(d, { withFileTypes: true });
1519
- } catch {
1520
- return;
1521
- }
1522
- for (const entry of entries) {
1523
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
1524
- continue;
1525
- const fullPath = join3(d, entry.name);
1526
- if (entry.isDirectory()) {
1527
- await scan(fullPath);
1528
- } 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")) {
1529
- results.push(fullPath);
1530
- }
1550
+ function isSkippedEntry2(name) {
1551
+ return name.startsWith(".") || name === "node_modules" || name === "dist";
1552
+ }
1553
+ function isTsSourceFile2(name) {
1554
+ if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
1555
+ return !name.endsWith(".test.ts") && !name.endsWith(".test.tsx") && !name.endsWith(".spec.ts");
1556
+ }
1557
+ async function scanDir2(d, results) {
1558
+ let entries;
1559
+ try {
1560
+ entries = await readdir2(d, { withFileTypes: true });
1561
+ } catch {
1562
+ return;
1563
+ }
1564
+ for (const entry of entries) {
1565
+ if (isSkippedEntry2(entry.name)) continue;
1566
+ const fullPath = join3(d, entry.name);
1567
+ if (entry.isDirectory()) {
1568
+ await scanDir2(fullPath, results);
1569
+ } else if (entry.isFile() && isTsSourceFile2(entry.name)) {
1570
+ results.push(fullPath);
1531
1571
  }
1532
1572
  }
1533
- await scan(dir);
1573
+ }
1574
+ async function collectTsFiles(dir) {
1575
+ const results = [];
1576
+ await scanDir2(dir, results);
1534
1577
  return results;
1535
1578
  }
1536
1579
  function computeLongestChain(file, graph, visited, memo) {
@@ -1561,34 +1604,42 @@ var DepDepthCollector = class {
1561
1604
  }
1562
1605
  ];
1563
1606
  }
1564
- async collect(config, rootDir) {
1565
- const allFiles = await collectTsFiles(rootDir);
1607
+ async buildImportGraph(allFiles) {
1566
1608
  const graph = /* @__PURE__ */ new Map();
1567
1609
  const fileSet = new Set(allFiles);
1568
1610
  for (const file of allFiles) {
1569
1611
  try {
1570
1612
  const content = await readFile4(file, "utf-8");
1571
- const imports = extractImportSources(content, file).filter((imp) => fileSet.has(imp));
1572
- graph.set(file, imports);
1613
+ graph.set(
1614
+ file,
1615
+ extractImportSources(content, file).filter((imp) => fileSet.has(imp))
1616
+ );
1573
1617
  } catch {
1574
1618
  graph.set(file, []);
1575
1619
  }
1576
1620
  }
1621
+ return graph;
1622
+ }
1623
+ buildModuleMap(allFiles, rootDir) {
1577
1624
  const moduleMap = /* @__PURE__ */ new Map();
1578
1625
  for (const file of allFiles) {
1579
1626
  const relDir = relativePosix(rootDir, dirname3(file));
1580
1627
  if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
1581
1628
  moduleMap.get(relDir).push(file);
1582
1629
  }
1630
+ return moduleMap;
1631
+ }
1632
+ async collect(config, rootDir) {
1633
+ const allFiles = await collectTsFiles(rootDir);
1634
+ const graph = await this.buildImportGraph(allFiles);
1635
+ const moduleMap = this.buildModuleMap(allFiles, rootDir);
1583
1636
  const memo = /* @__PURE__ */ new Map();
1584
1637
  const threshold = typeof config.thresholds["dependency-depth"] === "number" ? config.thresholds["dependency-depth"] : Infinity;
1585
1638
  const results = [];
1586
1639
  for (const [modulePath, files] of moduleMap) {
1587
- let longestChain = 0;
1588
- for (const file of files) {
1589
- const depth = computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo);
1590
- if (depth > longestChain) longestChain = depth;
1591
- }
1640
+ const longestChain = files.reduce((max, file) => {
1641
+ return Math.max(max, computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo));
1642
+ }, 0);
1592
1643
  const violations = [];
1593
1644
  if (longestChain > threshold) {
1594
1645
  violations.push({