@harness-engineering/core 0.21.1 → 0.21.3

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