@barbozaa/archguard 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +750 -0
  3. package/bin/cli.js +3709 -0
  4. package/package.json +63 -0
package/bin/cli.js ADDED
@@ -0,0 +1,3709 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/cli.ts
4
+ import cac from "cac";
5
+ import pc10 from "picocolors";
6
+
7
+ // src/config/config-loader.ts
8
+ import { readFile } from "fs/promises";
9
+ import { resolve } from "path";
10
+
11
+ // src/config/config-schema.ts
12
+ import { z } from "zod";
13
+ var LayerRulesSchema = z.record(z.array(z.string())).optional();
14
+ var ForbiddenImportSchema = z.object({
15
+ pattern: z.string(),
16
+ from: z.string()
17
+ });
18
+ var ConfigSchema = z.object({
19
+ entryPoint: z.string().optional(),
20
+ srcDirectory: z.string().default("./src"),
21
+ tsConfigPath: z.string().optional(),
22
+ rules: z.object({
23
+ maxFileLines: z.number().default(500),
24
+ layerRules: LayerRulesSchema,
25
+ forbiddenImports: z.array(ForbiddenImportSchema).optional()
26
+ }).optional(),
27
+ ignore: z.array(z.string()).optional()
28
+ });
29
+ var defaultConfig = {
30
+ srcDirectory: "./src",
31
+ rules: {
32
+ maxFileLines: 500
33
+ },
34
+ ignore: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
35
+ };
36
+
37
+ // src/config/config-loader.ts
38
+ var ConfigLoader = class {
39
+ async load(configPath) {
40
+ if (configPath) {
41
+ return this.loadFromPath(configPath);
42
+ }
43
+ return this.loadFromDefaultPaths();
44
+ }
45
+ async loadFromPath(configPath) {
46
+ try {
47
+ const fullPath = resolve(process.cwd(), configPath);
48
+ const content = await readFile(fullPath, "utf-8");
49
+ return this.validate(JSON.parse(content));
50
+ } catch (error) {
51
+ throw new Error(`Failed to load config from ${configPath}: ${error}`);
52
+ }
53
+ }
54
+ async loadFromDefaultPaths() {
55
+ const defaultPaths = [
56
+ "./archguard.config.json",
57
+ "./.archguard.json"
58
+ ];
59
+ for (const path of defaultPaths) {
60
+ const config = await this.tryLoadFromPath(path);
61
+ if (config) return config;
62
+ }
63
+ return defaultConfig;
64
+ }
65
+ async tryLoadFromPath(path) {
66
+ try {
67
+ const fullPath = resolve(process.cwd(), path);
68
+ const content = await readFile(fullPath, "utf-8");
69
+ return this.validate(JSON.parse(content));
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ validate(data) {
75
+ try {
76
+ return ConfigSchema.parse(data);
77
+ } catch (error) {
78
+ throw new Error(`Invalid configuration: ${error}`);
79
+ }
80
+ }
81
+ };
82
+
83
+ // src/core/project-loader.ts
84
+ import { Project } from "ts-morph";
85
+ import { resolve as resolve2, relative } from "path";
86
+
87
+ // src/rules/utils/violation-utils.ts
88
+ function calculateSeverityByCount(count, thresholds) {
89
+ if (count >= thresholds.critical) return "critical";
90
+ if (count >= thresholds.warning) return "warning";
91
+ return "info";
92
+ }
93
+ function calculatePenaltyByThreshold(count, threshold, basePenalty) {
94
+ const excess = count - threshold;
95
+ if (excess <= 0) return 0;
96
+ const multiplier = Math.ceil(excess / threshold);
97
+ return basePenalty * multiplier;
98
+ }
99
+ function createViolation(params) {
100
+ const violation = {
101
+ rule: params.rule,
102
+ severity: params.severity,
103
+ message: params.message,
104
+ file: params.file,
105
+ line: params.line ?? 1,
106
+ impact: params.impact,
107
+ suggestedFix: params.suggestedFix,
108
+ penalty: params.penalty
109
+ };
110
+ if (params.relatedFile) {
111
+ violation.relatedFile = params.relatedFile;
112
+ }
113
+ return violation;
114
+ }
115
+ function getThresholdFromConfig(ruleConfig, key = "threshold") {
116
+ if (!ruleConfig || typeof ruleConfig !== "object") {
117
+ return void 0;
118
+ }
119
+ const config = ruleConfig;
120
+ const value = config[key];
121
+ return typeof value === "number" ? value : void 0;
122
+ }
123
+ function shouldSkipNodeModules(filePath) {
124
+ return filePath.includes("node_modules");
125
+ }
126
+ function isTestFile(filePath) {
127
+ return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(filePath);
128
+ }
129
+ function isDeclarationFile(filePath) {
130
+ return filePath.endsWith(".d.ts");
131
+ }
132
+ function getRelativePath(filePath, rootPath) {
133
+ if (rootPath === "/" || rootPath === "\\") {
134
+ return filePath.startsWith("/") || filePath.startsWith("\\") ? filePath.slice(1) : filePath;
135
+ }
136
+ return filePath.replace(rootPath + "/", "").replace(rootPath + "\\", "");
137
+ }
138
+ function createThresholdViolation(params) {
139
+ return createViolation({
140
+ rule: params.rule,
141
+ severity: params.severity,
142
+ message: params.message,
143
+ file: getRelativePath(params.file, params.rootPath),
144
+ line: params.line ?? 1,
145
+ impact: params.impact,
146
+ suggestedFix: params.suggestedFix,
147
+ penalty: params.penalty
148
+ });
149
+ }
150
+ function createArchitectureViolation(params) {
151
+ return createViolation({
152
+ rule: params.rule,
153
+ severity: params.severity,
154
+ message: params.message,
155
+ file: getRelativePath(params.file, params.rootPath),
156
+ relatedFile: getRelativePath(params.relatedFile, params.rootPath),
157
+ line: 1,
158
+ impact: params.impact,
159
+ suggestedFix: params.suggestedFix,
160
+ penalty: params.penalty
161
+ });
162
+ }
163
+
164
+ // src/rules/utils/rule-helpers.ts
165
+ function processSourceFiles(sourceFiles, rootPath, processor, options = {}) {
166
+ const {
167
+ skipNodeModules = true,
168
+ skipTests = false,
169
+ onlyTests = false,
170
+ skipDeclarations = true,
171
+ customSkipCheck
172
+ } = options;
173
+ for (const sourceFile of sourceFiles) {
174
+ const filePath = sourceFile.getFilePath();
175
+ if (shouldSkipSourceFile(filePath, { skipNodeModules, skipTests, onlyTests, skipDeclarations, customSkipCheck })) {
176
+ continue;
177
+ }
178
+ const relativePath = getRelativePath(filePath, rootPath);
179
+ processor(sourceFile, filePath, relativePath);
180
+ }
181
+ }
182
+ function shouldSkipSourceFile(filePath, options) {
183
+ const skipChecks = [
184
+ { condition: options.skipNodeModules, test: () => shouldSkipNodeModules(filePath) },
185
+ { condition: options.onlyTests, test: () => !isTestFile(filePath) },
186
+ { condition: options.skipTests, test: () => isTestFile(filePath) },
187
+ { condition: options.skipDeclarations, test: () => isDeclarationFile(filePath) },
188
+ { condition: !!options.customSkipCheck, test: () => options.customSkipCheck(filePath) }
189
+ ];
190
+ for (const { condition, test } of skipChecks) {
191
+ if (condition && test()) {
192
+ return true;
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+
198
+ // src/core/project-loader.ts
199
+ var ProjectLoader = class {
200
+ project = null;
201
+ async load(config) {
202
+ const rootPath = process.cwd();
203
+ const tsConfigPath = config.tsConfigPath || this.findTsConfig(rootPath);
204
+ this.project = new Project({
205
+ tsConfigFilePath: tsConfigPath,
206
+ skipAddingFilesFromTsConfig: false
207
+ });
208
+ let sourceFiles = this.project.getSourceFiles();
209
+ if (sourceFiles.length === 0) {
210
+ const srcDir = resolve2(rootPath, config.srcDirectory);
211
+ this.project.addSourceFilesAtPaths([
212
+ `${srcDir}/**/*.ts`,
213
+ `${srcDir}/**/*.tsx`
214
+ ]);
215
+ sourceFiles = this.project.getSourceFiles();
216
+ }
217
+ const filteredFiles = this.filterSourceFiles(sourceFiles, config);
218
+ if (filteredFiles.length === 0) {
219
+ console.warn(`
220
+ \u26A0\uFE0F Warning: No TypeScript files found in ${config.srcDirectory}`);
221
+ console.warn(` Check your srcDirectory setting or tsconfig.json
222
+ `);
223
+ }
224
+ const sourceFilePaths = filteredFiles.map(
225
+ (sf) => relative(rootPath, sf.getFilePath())
226
+ );
227
+ return {
228
+ rootPath,
229
+ sourceFiles: sourceFilePaths,
230
+ dependencies: /* @__PURE__ */ new Map(),
231
+ moduleCount: sourceFilePaths.length
232
+ };
233
+ }
234
+ getProject() {
235
+ if (!this.project) {
236
+ throw new Error("Project not loaded. Call load() first.");
237
+ }
238
+ return this.project;
239
+ }
240
+ findTsConfig(rootPath) {
241
+ const candidates = [
242
+ resolve2(rootPath, "tsconfig.json"),
243
+ resolve2(rootPath, "src", "tsconfig.json")
244
+ ];
245
+ for (const path of candidates) {
246
+ try {
247
+ return path;
248
+ } catch {
249
+ continue;
250
+ }
251
+ }
252
+ return resolve2(rootPath, "tsconfig.json");
253
+ }
254
+ filterSourceFiles(sourceFiles, config) {
255
+ const ignorePatterns = config.ignore || [];
256
+ return sourceFiles.filter((sf) => {
257
+ const filePath = sf.getFilePath();
258
+ if (shouldSkipNodeModules(filePath)) {
259
+ return false;
260
+ }
261
+ if (isTestFile(filePath)) {
262
+ return false;
263
+ }
264
+ for (const pattern of ignorePatterns) {
265
+ if (this.matchesPattern(filePath, pattern)) {
266
+ return false;
267
+ }
268
+ }
269
+ return true;
270
+ });
271
+ }
272
+ matchesPattern(filePath, pattern) {
273
+ const regex = new RegExp(
274
+ pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".")
275
+ );
276
+ return regex.test(filePath);
277
+ }
278
+ };
279
+
280
+ // src/core/graph-builder.ts
281
+ import { relative as relative2, dirname, resolve as resolve3 } from "path";
282
+ var GraphBuilder = class {
283
+ build(project, rootPath) {
284
+ const nodes = /* @__PURE__ */ new Map();
285
+ const sourceFiles = project.getSourceFiles();
286
+ for (const sourceFile of sourceFiles) {
287
+ if (sourceFile.getFilePath().includes("node_modules")) {
288
+ continue;
289
+ }
290
+ const filePath = this.normalizeFilePath(sourceFile, rootPath);
291
+ const dependencies = this.extractDependencies(sourceFile, rootPath);
292
+ nodes.set(filePath, {
293
+ file: filePath,
294
+ dependencies,
295
+ dependents: /* @__PURE__ */ new Set()
296
+ });
297
+ }
298
+ for (const [file, node] of nodes.entries()) {
299
+ for (const dep of node.dependencies) {
300
+ const depNode = nodes.get(dep);
301
+ if (depNode) {
302
+ depNode.dependents.add(file);
303
+ }
304
+ }
305
+ }
306
+ const cyclicGroups = this.detectCycles(nodes);
307
+ return {
308
+ nodes,
309
+ cyclicGroups
310
+ };
311
+ }
312
+ normalizeFilePath(sourceFile, rootPath) {
313
+ return relative2(rootPath, sourceFile.getFilePath());
314
+ }
315
+ extractDependencies(sourceFile, rootPath) {
316
+ const dependencies = /* @__PURE__ */ new Set();
317
+ const importDeclarations = sourceFile.getImportDeclarations();
318
+ for (const importDecl of importDeclarations) {
319
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
320
+ if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
321
+ continue;
322
+ }
323
+ const sourceFilePath = sourceFile.getFilePath();
324
+ const sourceFileDir = dirname(sourceFilePath);
325
+ try {
326
+ const resolvedPath = this.resolveImport(
327
+ moduleSpecifier,
328
+ sourceFileDir,
329
+ rootPath
330
+ );
331
+ if (resolvedPath) {
332
+ dependencies.add(resolvedPath);
333
+ }
334
+ } catch {
335
+ }
336
+ }
337
+ return dependencies;
338
+ }
339
+ resolveImport(moduleSpecifier, fromDir, rootPath) {
340
+ let resolved = resolve3(fromDir, moduleSpecifier);
341
+ const extensions = [".ts", ".tsx", ".js", "/index.ts", "/index.tsx"];
342
+ for (const ext of extensions) {
343
+ try {
344
+ const candidate = resolved + ext;
345
+ return relative2(rootPath, candidate);
346
+ } catch {
347
+ continue;
348
+ }
349
+ }
350
+ return relative2(rootPath, resolved);
351
+ }
352
+ detectCycles(nodes) {
353
+ const cycles = [];
354
+ const visited = /* @__PURE__ */ new Set();
355
+ const recursionStack = /* @__PURE__ */ new Set();
356
+ const currentPath = [];
357
+ const dfs = (file) => {
358
+ visited.add(file);
359
+ recursionStack.add(file);
360
+ currentPath.push(file);
361
+ const node = nodes.get(file);
362
+ if (!node) {
363
+ currentPath.pop();
364
+ recursionStack.delete(file);
365
+ return;
366
+ }
367
+ for (const dep of node.dependencies) {
368
+ if (!visited.has(dep)) {
369
+ dfs(dep);
370
+ } else if (recursionStack.has(dep)) {
371
+ const cycleStart = currentPath.indexOf(dep);
372
+ const cycle = currentPath.slice(cycleStart);
373
+ cycles.push([...cycle, dep]);
374
+ }
375
+ }
376
+ currentPath.pop();
377
+ recursionStack.delete(file);
378
+ };
379
+ for (const file of nodes.keys()) {
380
+ if (!visited.has(file)) {
381
+ dfs(file);
382
+ }
383
+ }
384
+ return cycles;
385
+ }
386
+ };
387
+
388
+ // src/output/penalty-calculator.ts
389
+ var PenaltyCalculator = class _PenaltyCalculator {
390
+ // Normalization configuration
391
+ static BASELINE_PROJECT_SIZE = 1e4;
392
+ static SMALL_PROJECT_THRESHOLD = 5e3;
393
+ static MEDIUM_PROJECT_THRESHOLD = 5e4;
394
+ static LARGE_PROJECT_THRESHOLD = 2e5;
395
+ static NORMALIZATION_POWER_SMALL = 0.3;
396
+ static NORMALIZATION_POWER_MEDIUM = 0.4;
397
+ static NORMALIZATION_POWER_LARGE = 0.5;
398
+ static MIN_LOC_FOR_NORMALIZATION = 1;
399
+ // Impact thresholds
400
+ static HIGH_IMPACT_THRESHOLD = 50;
401
+ static MEDIUM_IMPACT_THRESHOLD = 20;
402
+ static TOP_ISSUES_LIMIT = 5;
403
+ categoryMultipliers = {
404
+ structural: 1.2,
405
+ design: 1,
406
+ complexity: 0.8,
407
+ hygiene: 0.5
408
+ };
409
+ severityMultipliers = {
410
+ critical: 1,
411
+ warning: 0.6,
412
+ info: 0.3
413
+ };
414
+ ruleMetadata = /* @__PURE__ */ new Map([
415
+ // === CORE ARCHITECTURE RULES (Structural) ===
416
+ ["circular-deps", { name: "circular-deps", weight: 10, category: "structural" }],
417
+ ["layer-violation", { name: "layer-violation", weight: 9, category: "structural" }],
418
+ ["forbidden-imports", { name: "forbidden-imports", weight: 8, category: "structural" }],
419
+ // === COUPLING & COMPLEXITY ANALYSIS (Design) ===
420
+ ["too-many-imports", { name: "too-many-imports", weight: 7, category: "design" }],
421
+ ["shotgun-surgery", { name: "shotgun-surgery", weight: 7, category: "design" }],
422
+ ["data-clumps", { name: "data-clumps", weight: 6, category: "design" }],
423
+ ["long-parameter-list", { name: "long-parameter-list", weight: 5, category: "design" }],
424
+ // === COMPLEXITY (Cognitive Load) ===
425
+ ["cyclomatic-complexity", { name: "cyclomatic-complexity", weight: 5, category: "complexity" }],
426
+ ["deep-nesting", { name: "deep-nesting", weight: 4, category: "complexity" }],
427
+ ["large-function", { name: "large-function", weight: 4, category: "complexity" }],
428
+ ["max-file-lines", { name: "max-file-lines", weight: 3, category: "complexity" }],
429
+ // === CODE HEALTH (Hygiene) ===
430
+ ["duplicate-code", { name: "duplicate-code", weight: 6, category: "hygiene" }],
431
+ ["unused-exports", { name: "unused-exports", weight: 2, category: "hygiene" }],
432
+ ["dead-code", { name: "dead-code", weight: 3, category: "hygiene" }]
433
+ ]);
434
+ /**
435
+ * Calculate total penalty with category-specific weights and normalization
436
+ * @param violations Array of architectural violations
437
+ * @param totalLOC Total lines of code (must be positive)
438
+ * @throws {Error} If totalLOC is invalid
439
+ */
440
+ calculatePenalty(violations, totalLOC) {
441
+ if (totalLOC < _PenaltyCalculator.MIN_LOC_FOR_NORMALIZATION) {
442
+ throw new Error(`Invalid totalLOC: ${totalLOC}. Must be at least ${_PenaltyCalculator.MIN_LOC_FOR_NORMALIZATION}`);
443
+ }
444
+ const categorized = this.categorizeViolations(violations);
445
+ const structural = this.calculateCategoryPenalty(categorized.structural, "structural");
446
+ const design = this.calculateCategoryPenalty(categorized.design, "design");
447
+ const complexity = this.calculateCategoryPenalty(categorized.complexity, "complexity");
448
+ const hygiene = this.calculateCategoryPenalty(categorized.hygiene, "hygiene");
449
+ const totalPenalty = structural.penalty + design.penalty + complexity.penalty + hygiene.penalty;
450
+ const normalizedPenalty = this.normalizePenalty(totalPenalty, totalLOC);
451
+ return {
452
+ structural,
453
+ design,
454
+ complexity,
455
+ hygiene,
456
+ totalPenalty,
457
+ normalizedPenalty
458
+ };
459
+ }
460
+ /**
461
+ * Categorize violations by rule category
462
+ */
463
+ categorizeViolations(violations) {
464
+ const result = {
465
+ structural: [],
466
+ design: [],
467
+ complexity: [],
468
+ hygiene: []
469
+ };
470
+ for (const violation of violations) {
471
+ const metadata = this.getRuleMetadata(violation.rule);
472
+ const category = metadata?.category || "hygiene";
473
+ result[category].push(violation);
474
+ }
475
+ return result;
476
+ }
477
+ /**
478
+ * Calculate penalty for a specific category
479
+ */
480
+ calculateCategoryPenalty(violations, category) {
481
+ let penalty = 0;
482
+ for (const violation of violations) {
483
+ const metadata = this.getRuleMetadata(violation.rule);
484
+ const weight = metadata?.weight || 1;
485
+ const severityMultiplier = this.severityMultipliers[violation.severity];
486
+ const categoryMultiplier = this.categoryMultipliers[category];
487
+ penalty += weight * severityMultiplier * categoryMultiplier;
488
+ }
489
+ const topIssues = violations.sort((a, b) => {
490
+ const weightA = this.getRuleMetadata(a.rule)?.weight || 1;
491
+ const weightB = this.getRuleMetadata(b.rule)?.weight || 1;
492
+ return weightB - weightA;
493
+ }).slice(0, _PenaltyCalculator.TOP_ISSUES_LIMIT);
494
+ return {
495
+ violations: violations.length,
496
+ penalty: Math.round(penalty * 10) / 10,
497
+ weight: this.categoryMultipliers[category],
498
+ impact: this.getImpactLevel(penalty),
499
+ topIssues
500
+ };
501
+ }
502
+ /**
503
+ * Normalize penalty based on project size using power-law scaling
504
+ * Larger projects get increasingly favorable normalization to avoid unfair penalties
505
+ */
506
+ normalizePenalty(penalty, totalLOC) {
507
+ if (totalLOC <= _PenaltyCalculator.SMALL_PROJECT_THRESHOLD) {
508
+ return penalty;
509
+ }
510
+ const baselineSize = _PenaltyCalculator.BASELINE_PROJECT_SIZE;
511
+ let powerFactor;
512
+ if (totalLOC <= _PenaltyCalculator.MEDIUM_PROJECT_THRESHOLD) {
513
+ powerFactor = _PenaltyCalculator.NORMALIZATION_POWER_SMALL;
514
+ } else if (totalLOC <= _PenaltyCalculator.LARGE_PROJECT_THRESHOLD) {
515
+ powerFactor = _PenaltyCalculator.NORMALIZATION_POWER_MEDIUM;
516
+ } else {
517
+ powerFactor = _PenaltyCalculator.NORMALIZATION_POWER_LARGE;
518
+ }
519
+ const normalizationFactor = Math.pow(baselineSize / totalLOC, powerFactor);
520
+ return penalty * normalizationFactor;
521
+ }
522
+ /**
523
+ * Get rule metadata
524
+ */
525
+ getRuleMetadata(ruleName) {
526
+ const normalized = ruleName.toLowerCase().replace(/\s+/g, "-");
527
+ return this.ruleMetadata.get(normalized);
528
+ }
529
+ /**
530
+ * Determine impact level based on penalty threshold
531
+ */
532
+ getImpactLevel(penalty) {
533
+ if (penalty >= _PenaltyCalculator.HIGH_IMPACT_THRESHOLD) return "HIGH";
534
+ if (penalty >= _PenaltyCalculator.MEDIUM_IMPACT_THRESHOLD) return "MEDIUM";
535
+ return "LOW";
536
+ }
537
+ /**
538
+ * Get all rule metadata
539
+ */
540
+ getRuleMetadataMap() {
541
+ return new Map(this.ruleMetadata);
542
+ }
543
+ /**
544
+ * Get category multiplier
545
+ */
546
+ getCategoryMultiplier(category) {
547
+ return this.categoryMultipliers[category];
548
+ }
549
+ };
550
+
551
+ // src/output/score-calculator.ts
552
+ var ScoreCalculator = class _ScoreCalculator {
553
+ static STARTING_SCORE = 100;
554
+ static MIN_SCORE = 0;
555
+ static MAX_SCORE = 100;
556
+ penaltyCalculator = new PenaltyCalculator();
557
+ calculate(violations, totalModules, totalLOC) {
558
+ if (totalLOC && totalLOC > 0) {
559
+ const breakdown = this.penaltyCalculator.calculatePenalty(violations, totalLOC);
560
+ const rawScore = _ScoreCalculator.STARTING_SCORE - breakdown.normalizedPenalty;
561
+ const score = Math.max(
562
+ _ScoreCalculator.MIN_SCORE,
563
+ Math.min(_ScoreCalculator.MAX_SCORE, Math.round(rawScore))
564
+ );
565
+ const status = this.getStatus(score);
566
+ return { score, status, breakdown };
567
+ }
568
+ return this.calculateLegacy(violations, totalModules);
569
+ }
570
+ calculateLegacy(violations, totalModules) {
571
+ let totalPenalty = 0;
572
+ for (const violation of violations) {
573
+ totalPenalty += violation.penalty;
574
+ }
575
+ const scalingFactor = this.calculateScalingFactor(totalModules, violations.length);
576
+ const adjustedPenalty = totalPenalty / scalingFactor;
577
+ const score = Math.max(_ScoreCalculator.MIN_SCORE, Math.round(_ScoreCalculator.STARTING_SCORE - adjustedPenalty));
578
+ const status = this.getStatus(score);
579
+ return { score, status };
580
+ }
581
+ calculateScalingFactor(totalModules, violationCount) {
582
+ let baseScaling = 1;
583
+ if (totalModules <= 100) {
584
+ const violationRatio = violationCount / Math.max(1, totalModules);
585
+ baseScaling = 1 + Math.min(violationRatio * 2, 8);
586
+ } else if (totalModules <= 200) {
587
+ baseScaling = totalModules / 50;
588
+ } else {
589
+ baseScaling = 4 + (totalModules - 200) / 100;
590
+ }
591
+ return Math.max(1, baseScaling);
592
+ }
593
+ getStatus(score) {
594
+ if (score >= 90) return "Excellent";
595
+ if (score >= 75) return "Healthy";
596
+ if (score >= 60) return "Needs Attention";
597
+ return "Critical";
598
+ }
599
+ getGrade(score) {
600
+ if (score >= 90) return "EXCELLENT";
601
+ if (score >= 75) return "GOOD";
602
+ if (score >= 60) return "FAIR";
603
+ if (score >= 40) return "POOR";
604
+ return "CRITICAL";
605
+ }
606
+ };
607
+
608
+ // src/output/risk-ranker.ts
609
+ var RiskRanker = class {
610
+ rank(violations, topN = 5) {
611
+ const sorted = [...violations].sort((a, b) => {
612
+ const severityOrder = { critical: 3, warning: 2, info: 1 };
613
+ const severityDiff = severityOrder[b.severity] - severityOrder[a.severity];
614
+ if (severityDiff !== 0) {
615
+ return severityDiff;
616
+ }
617
+ return b.penalty - a.penalty;
618
+ });
619
+ return sorted.slice(0, topN);
620
+ }
621
+ countBySeverity(violations) {
622
+ return violations.reduce(
623
+ (acc, v) => {
624
+ acc[v.severity]++;
625
+ return acc;
626
+ },
627
+ { critical: 0, warning: 0, info: 0 }
628
+ );
629
+ }
630
+ };
631
+
632
+ // src/core/rule-context.ts
633
+ function createRuleContext(project, graph, config, rootPath) {
634
+ return { project, graph, config, rootPath };
635
+ }
636
+
637
+ // src/core/analyzer.ts
638
+ import { readFileSync, existsSync } from "fs";
639
+ import { join, basename } from "path";
640
+
641
+ // src/rules/circular-deps.rule.ts
642
+ var CircularDepsRule = class {
643
+ name = "circular-deps";
644
+ severity = "critical";
645
+ penalty = 5;
646
+ check(context) {
647
+ const { graph, rootPath } = context;
648
+ const violations = [];
649
+ const processedCycles = /* @__PURE__ */ new Set();
650
+ for (const cycle of graph.cyclicGroups) {
651
+ const signature = [...cycle].sort().join("->");
652
+ if (processedCycles.has(signature)) {
653
+ continue;
654
+ }
655
+ processedCycles.add(signature);
656
+ if (cycle.length < 2) {
657
+ continue;
658
+ }
659
+ const cycleDescription = cycle.slice(0, 3).join(" \u2192 ");
660
+ const mainFile = cycle[0];
661
+ const relatedFile = cycle[1];
662
+ violations.push(createArchitectureViolation({
663
+ rule: "Circular Dependency",
664
+ message: `Circular dependency detected: ${cycleDescription}${cycle.length > 3 ? "..." : ""}`,
665
+ file: mainFile,
666
+ relatedFile,
667
+ rootPath,
668
+ severity: this.severity,
669
+ impact: "Circular dependencies create tight coupling, make code harder to test, and can cause initialization issues. They prevent proper modularization and increase change risk.",
670
+ suggestedFix: `Break the cycle by:
671
+ 1. Introducing a shared abstraction/interface layer
672
+ 2. Using dependency injection
673
+ 3. Inverting the dependency (make one module depend on an abstraction)
674
+ 4. Extracting shared logic into a separate module`,
675
+ penalty: this.penalty
676
+ }));
677
+ }
678
+ return violations;
679
+ }
680
+ };
681
+
682
+ // src/rules/layer-violation.rule.ts
683
+ var LayerViolationRule = class {
684
+ name = "layer-violation";
685
+ severity = "critical";
686
+ penalty = 8;
687
+ check(context) {
688
+ const { project, graph, config, rootPath } = context;
689
+ const violations = [];
690
+ if (!config.rules?.layerRules) {
691
+ return violations;
692
+ }
693
+ const layerRules = config.rules.layerRules;
694
+ processSourceFiles(
695
+ project.getSourceFiles(),
696
+ rootPath,
697
+ (_, filePath, relativePath) => {
698
+ const fileLayer = this.getLayer(relativePath);
699
+ if (!fileLayer || !layerRules[fileLayer]) {
700
+ return;
701
+ }
702
+ const allowedLayers = layerRules[fileLayer];
703
+ const node = graph.nodes.get(relativePath);
704
+ if (!node) {
705
+ return;
706
+ }
707
+ for (const dependency of node.dependencies) {
708
+ const depLayer = this.getLayer(dependency);
709
+ if (!depLayer) {
710
+ continue;
711
+ }
712
+ if (!allowedLayers.includes(depLayer) && depLayer !== fileLayer) {
713
+ const depFilePath = `${rootPath}/${dependency}`;
714
+ violations.push(createArchitectureViolation({
715
+ rule: "Layer Violation",
716
+ severity: this.severity,
717
+ message: `${fileLayer} layer importing from ${depLayer} layer`,
718
+ file: filePath,
719
+ relatedFile: depFilePath,
720
+ rootPath,
721
+ impact: `Violates architectural boundaries. The ${fileLayer} layer should not depend on ${depLayer}. This breaks separation of concerns and creates unwanted coupling between layers.`,
722
+ suggestedFix: `Restructure dependencies to follow the layer hierarchy:
723
+ ${fileLayer} \u2192 [${allowedLayers.join(", ")}]
724
+
725
+ Consider:
726
+ 1. Moving shared logic to an allowed layer (${allowedLayers[0] || "domain"})
727
+ 2. Using dependency inversion (interfaces/abstractions)
728
+ 3. Re-evaluating if ${dependency} belongs in a different layer`,
729
+ penalty: this.penalty
730
+ }));
731
+ }
732
+ }
733
+ }
734
+ );
735
+ return violations;
736
+ }
737
+ getLayer(filePath) {
738
+ const parts = filePath.split("/");
739
+ const layers = ["ui", "application", "domain", "infra", "infrastructure"];
740
+ for (const part of parts) {
741
+ if (layers.includes(part.toLowerCase())) {
742
+ return part.toLowerCase();
743
+ }
744
+ }
745
+ return null;
746
+ }
747
+ };
748
+
749
+ // src/rules/forbidden-imports.rule.ts
750
+ var ForbiddenImportsRule = class {
751
+ name = "forbidden-imports";
752
+ severity = "warning";
753
+ penalty = 6;
754
+ check(context) {
755
+ const { project, config, rootPath } = context;
756
+ const violations = [];
757
+ if (!config.rules?.forbiddenImports) {
758
+ return violations;
759
+ }
760
+ const forbiddenRules = config.rules.forbiddenImports;
761
+ processSourceFiles(
762
+ project.getSourceFiles(),
763
+ rootPath,
764
+ (sourceFile, filePath, relativePath) => {
765
+ this.checkFileImports(sourceFile, filePath, relativePath, rootPath, forbiddenRules, violations);
766
+ },
767
+ { skipTests: false, skipDeclarations: false }
768
+ );
769
+ return violations;
770
+ }
771
+ checkFileImports(sourceFile, filePath, relativePath, rootPath, forbiddenRules, violations) {
772
+ const imports = sourceFile.getImportDeclarations();
773
+ for (const importDecl of imports) {
774
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
775
+ for (const rule of forbiddenRules) {
776
+ if (!this.isForbiddenImport(moduleSpecifier, relativePath, rule)) continue;
777
+ violations.push(createArchitectureViolation({
778
+ rule: "Forbidden Import",
779
+ severity: this.severity,
780
+ message: `Importing "${moduleSpecifier}" from "${relativePath}"`,
781
+ file: filePath,
782
+ relatedFile: moduleSpecifier,
783
+ rootPath,
784
+ impact: `This import violates project import rules. Forbidden imports can introduce unwanted dependencies, couple unrelated modules, or import test code into production.`,
785
+ suggestedFix: `Remove this import or restructure your code:
786
+ 1. If this is a test utility, move it to a shared test helper
787
+ 2. If this is production code, refactor to avoid the dependency
788
+ 3. Consider if the import rule needs updating`,
789
+ penalty: this.penalty
790
+ }));
791
+ }
792
+ }
793
+ }
794
+ isForbiddenImport(moduleSpecifier, filePath, rule) {
795
+ return this.matchesPattern(moduleSpecifier, rule.pattern) && this.matchesPattern(filePath, rule.from);
796
+ }
797
+ /**
798
+ * Matches a value against a glob pattern
799
+ * Supports: asterisk (any chars except /), double-asterisk (any chars including /)
800
+ *
801
+ * Examples:
802
+ * - "src/asterisk.ts" matches "src/file.ts" but not "src/dir/file.ts"
803
+ * - "src/double-asterisk" matches "src/file.ts" and "src/dir/file.ts"
804
+ * - "double-asterisk/asterisk.test.ts" matches any test.ts file in any directory
805
+ */
806
+ matchesPattern(value, pattern) {
807
+ let regexPattern = pattern.replace(/[.+^${}()|[\\\]]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*");
808
+ const regex = new RegExp(`^${regexPattern}$`);
809
+ return regex.test(value);
810
+ }
811
+ };
812
+
813
+ // src/rules/too-many-imports.rule.ts
814
+ var TooManyImportsRule = class {
815
+ name = "too-many-imports";
816
+ severity = "warning";
817
+ penalty = 5;
818
+ check(context) {
819
+ const { project, config, rootPath } = context;
820
+ const violations = [];
821
+ const threshold = config.rules?.["too-many-imports"]?.maxImports || 15;
822
+ processSourceFiles(
823
+ project.getSourceFiles(),
824
+ rootPath,
825
+ (sourceFile, _, __) => {
826
+ const importDeclarations = sourceFile.getImportDeclarations();
827
+ const importCount = importDeclarations.length;
828
+ if (importCount > threshold) {
829
+ const severity = calculateSeverityByCount(importCount, { critical: 25, warning: 15 });
830
+ violations.push(createThresholdViolation({
831
+ rule: "Too Many Imports",
832
+ severity,
833
+ message: `File has ${importCount} imports (max: ${threshold})`,
834
+ file: sourceFile.getFilePath(),
835
+ rootPath,
836
+ line: 1,
837
+ impact: "Increases coupling, reduces modularity, and violates Single Responsibility Principle",
838
+ suggestedFix: `Refactor file into smaller, focused modules. Remove unused imports. Consider facade pattern to reduce direct dependencies. Target: <${threshold} imports per file.`,
839
+ penalty: calculatePenaltyByThreshold(importCount, threshold, this.penalty)
840
+ }));
841
+ }
842
+ }
843
+ );
844
+ return violations;
845
+ }
846
+ };
847
+
848
+ // src/rules/cyclomatic-complexity.rule.ts
849
+ import { SyntaxKind as SyntaxKind2 } from "ts-morph";
850
+
851
+ // src/rules/base/function-analysis.rule.ts
852
+ import { SyntaxKind } from "ts-morph";
853
+ var FunctionAnalysisRule = class {
854
+ /**
855
+ * Main check method that orchestrates the analysis
856
+ * This implements the template method pattern
857
+ */
858
+ check(context) {
859
+ const { project, config, rootPath } = context;
860
+ const violations = [];
861
+ const sourceFiles = project.getSourceFiles();
862
+ const configValue = config.rules?.[this.name];
863
+ const threshold = configValue?.[this.getConfigKey()] || this.getDefaultThreshold();
864
+ for (const sourceFile of sourceFiles) {
865
+ const filePath = sourceFile.getFilePath();
866
+ if (shouldSkipNodeModules(filePath)) {
867
+ continue;
868
+ }
869
+ const checkContext = { filePath, rootPath, threshold, violations };
870
+ this.analyzeFunctions(sourceFile, checkContext);
871
+ this.analyzeMethods(sourceFile, checkContext);
872
+ this.analyzeArrowFunctions(sourceFile, checkContext);
873
+ }
874
+ return violations;
875
+ }
876
+ /**
877
+ * Analyze all function declarations in a source file
878
+ */
879
+ analyzeFunctions(sourceFile, context) {
880
+ const functions = sourceFile.getFunctions();
881
+ for (const func of functions) {
882
+ this.checkFunction(func, context);
883
+ }
884
+ }
885
+ /**
886
+ * Analyze all class methods in a source file
887
+ */
888
+ analyzeMethods(sourceFile, context) {
889
+ const classes = sourceFile.getClasses();
890
+ for (const cls of classes) {
891
+ const methods = cls.getMethods();
892
+ for (const method of methods) {
893
+ this.checkMethod(method, context);
894
+ }
895
+ }
896
+ }
897
+ /**
898
+ * Analyze all arrow functions in a source file
899
+ */
900
+ analyzeArrowFunctions(sourceFile, context) {
901
+ const variableStatements = sourceFile.getVariableStatements();
902
+ for (const stmt of variableStatements) {
903
+ const declarations = stmt.getDeclarations();
904
+ for (const decl of declarations) {
905
+ const initializer = decl.getInitializer();
906
+ if (initializer && initializer.getKind() === SyntaxKind.ArrowFunction) {
907
+ const arrowContext = {
908
+ ...context,
909
+ arrowFunc: initializer,
910
+ name: decl.getName()
911
+ };
912
+ this.checkArrowFunction(arrowContext);
913
+ }
914
+ }
915
+ }
916
+ }
917
+ /**
918
+ * Helper method to create a function violation with standardized structure
919
+ */
920
+ createFunctionViolation(params) {
921
+ return {
922
+ rule: params.rule,
923
+ severity: params.severity,
924
+ message: `Function '${params.functionName}' has ${params.metric} of ${params.metricValue} (max: ${params.threshold})`,
925
+ file: getRelativePath(params.filePath, params.rootPath),
926
+ line: params.line,
927
+ impact: params.impact,
928
+ suggestedFix: params.suggestedFix,
929
+ penalty: params.penalty
930
+ };
931
+ }
932
+ /**
933
+ * Helper method to create a method violation with standardized structure
934
+ */
935
+ createMethodViolation(params) {
936
+ return {
937
+ rule: params.rule,
938
+ severity: params.severity,
939
+ message: `Method '${params.className}.${params.methodName}' has ${params.metric} of ${params.metricValue} (max: ${params.threshold})`,
940
+ file: getRelativePath(params.filePath, params.rootPath),
941
+ line: params.line,
942
+ impact: params.impact,
943
+ suggestedFix: params.suggestedFix,
944
+ penalty: params.penalty
945
+ };
946
+ }
947
+ /**
948
+ * Helper method to create an arrow function violation with standardized structure
949
+ */
950
+ createArrowFunctionViolation(params) {
951
+ return {
952
+ rule: params.rule,
953
+ severity: params.severity,
954
+ message: `Arrow function '${params.functionName}' has ${params.metric} of ${params.metricValue} (max: ${params.threshold})`,
955
+ file: getRelativePath(params.filePath, params.rootPath),
956
+ line: params.line,
957
+ impact: params.impact,
958
+ suggestedFix: params.suggestedFix,
959
+ penalty: params.penalty
960
+ };
961
+ }
962
+ /**
963
+ * Helper to get class name from a method's parent
964
+ */
965
+ getClassName(method) {
966
+ const parent = method.getParent();
967
+ return parent && "getName" in parent && typeof parent.getName === "function" ? parent.getName() ?? "<unknown>" : "<unknown>";
968
+ }
969
+ /**
970
+ * Template method for generic function checking
971
+ * Extracts common violation creation logic
972
+ */
973
+ checkFunctionGeneric(params) {
974
+ const { body, getName, getLine, context, rule, metric, impact, suggestedFix, calculateMetric, calculateSeverity: getSeverity, calculatePenalty: getPenalty } = params;
975
+ if (!body) return;
976
+ const metricValue = calculateMetric(body);
977
+ if (metricValue > context.threshold) {
978
+ context.violations.push(
979
+ this.createFunctionViolation({
980
+ rule,
981
+ severity: getSeverity(metricValue),
982
+ functionName: getName() || "<anonymous>",
983
+ metric,
984
+ metricValue,
985
+ threshold: context.threshold,
986
+ line: getLine(),
987
+ filePath: context.filePath,
988
+ rootPath: context.rootPath,
989
+ impact,
990
+ suggestedFix,
991
+ penalty: getPenalty(metricValue)
992
+ })
993
+ );
994
+ }
995
+ }
996
+ /**
997
+ * Template method for generic method checking
998
+ */
999
+ checkMethodGeneric(params) {
1000
+ const { method, body, context, rule, metric, impact, suggestedFix, calculateMetric, calculateSeverity: getSeverity, calculatePenalty: getPenalty } = params;
1001
+ if (!body) return;
1002
+ const metricValue = calculateMetric(body);
1003
+ if (metricValue > context.threshold) {
1004
+ context.violations.push(
1005
+ this.createMethodViolation({
1006
+ rule,
1007
+ severity: getSeverity(metricValue),
1008
+ className: this.getClassName(method),
1009
+ methodName: method.getName(),
1010
+ metric,
1011
+ metricValue,
1012
+ threshold: context.threshold,
1013
+ line: method.getStartLineNumber(),
1014
+ filePath: context.filePath,
1015
+ rootPath: context.rootPath,
1016
+ impact,
1017
+ suggestedFix,
1018
+ penalty: getPenalty(metricValue)
1019
+ })
1020
+ );
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Template method for generic arrow function checking
1025
+ */
1026
+ checkArrowFunctionGeneric(params) {
1027
+ const { context, body, rule, metric, impact, suggestedFix, calculateMetric, calculateSeverity: getSeverity, calculatePenalty: getPenalty } = params;
1028
+ if (!body) return;
1029
+ const metricValue = calculateMetric(body);
1030
+ if (metricValue > context.threshold) {
1031
+ context.violations.push(
1032
+ this.createArrowFunctionViolation({
1033
+ rule,
1034
+ severity: getSeverity(metricValue),
1035
+ functionName: context.name,
1036
+ metric,
1037
+ metricValue,
1038
+ threshold: context.threshold,
1039
+ line: context.arrowFunc.getStartLineNumber(),
1040
+ filePath: context.filePath,
1041
+ rootPath: context.rootPath,
1042
+ impact,
1043
+ suggestedFix,
1044
+ penalty: getPenalty(metricValue)
1045
+ })
1046
+ );
1047
+ }
1048
+ }
1049
+ };
1050
+
1051
+ // src/rules/utils/severity-calculator.ts
1052
+ function calculateSeverity(value, thresholds) {
1053
+ if (value > thresholds.critical) return "critical";
1054
+ if (value > thresholds.warning) return "warning";
1055
+ return "info";
1056
+ }
1057
+ function calculatePenalty(value, threshold, thresholds, config) {
1058
+ const excess = value - threshold;
1059
+ if (value > thresholds.critical) {
1060
+ return config.criticalBase + excess * config.criticalMultiplier;
1061
+ } else if (value > thresholds.warning) {
1062
+ return config.warningBase + excess * config.warningMultiplier;
1063
+ } else {
1064
+ const multiplier = config.infoMultiplier ?? 1;
1065
+ return config.infoBase + excess * multiplier;
1066
+ }
1067
+ }
1068
+
1069
+ // src/rules/utils/function-analysis-config.ts
1070
+ var STANDARD_PENALTY_CONFIG = {
1071
+ criticalBase: 15,
1072
+ criticalMultiplier: 2,
1073
+ warningBase: 10,
1074
+ warningMultiplier: 1.5,
1075
+ infoBase: 5
1076
+ };
1077
+ var COMPLEXITY_IMPACT = "High complexity increases bug probability and makes code harder to understand and test";
1078
+ var NESTING_IMPACT = "Increases complexity and bug risk, makes code harder to understand and test";
1079
+ function createSeverityThresholds(critical, warning) {
1080
+ return { critical, warning };
1081
+ }
1082
+
1083
+ // src/rules/cyclomatic-complexity.rule.ts
1084
+ var SEVERITY_THRESHOLDS = createSeverityThresholds(20, 15);
1085
+ var PENALTY_CONFIG = {
1086
+ ...STANDARD_PENALTY_CONFIG,
1087
+ criticalBase: 20
1088
+ // Higher penalty for cyclomatic complexity
1089
+ };
1090
+ var CyclomaticComplexityRule = class extends FunctionAnalysisRule {
1091
+ name = "cyclomatic-complexity";
1092
+ severity = "info";
1093
+ penalty = 10;
1094
+ getConfigKey() {
1095
+ return "maxComplexity";
1096
+ }
1097
+ getDefaultThreshold() {
1098
+ return 10;
1099
+ }
1100
+ checkFunction(func, context) {
1101
+ this.checkFunctionGeneric({
1102
+ body: func.getBody(),
1103
+ getName: () => func.getName() || "anonymous",
1104
+ getLine: () => func.getStartLineNumber(),
1105
+ context,
1106
+ rule: "High Cyclomatic Complexity",
1107
+ metric: "cyclomatic complexity",
1108
+ impact: COMPLEXITY_IMPACT,
1109
+ suggestedFix: "Break down into smaller functions with single responsibilities. Extract complex conditionals into helper methods. Simplify logic by using early returns, strategy pattern, or lookup tables. Target complexity <10 for easier testing.",
1110
+ calculateMetric: (body) => this.calculateComplexity(body),
1111
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS),
1112
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS, PENALTY_CONFIG)
1113
+ });
1114
+ }
1115
+ checkMethod(method, context) {
1116
+ this.checkMethodGeneric({
1117
+ method,
1118
+ body: method.getBody(),
1119
+ context,
1120
+ rule: "High Cyclomatic Complexity",
1121
+ metric: "cyclomatic complexity",
1122
+ impact: COMPLEXITY_IMPACT,
1123
+ suggestedFix: "Extract complex logic into helper methods. Use polymorphism instead of conditionals. Simplify nested conditions with guard clauses. Consider state pattern for complex state management.",
1124
+ calculateMetric: (body) => this.calculateComplexity(body),
1125
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS),
1126
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS, PENALTY_CONFIG)
1127
+ });
1128
+ }
1129
+ checkArrowFunction(context) {
1130
+ this.checkArrowFunctionGeneric({
1131
+ context,
1132
+ body: context.arrowFunc.getBody(),
1133
+ rule: "High Cyclomatic Complexity",
1134
+ metric: "cyclomatic complexity",
1135
+ impact: COMPLEXITY_IMPACT,
1136
+ suggestedFix: "Convert to named function and break down logic. Extract decision points into separate functions. Use array methods (map, filter, reduce) to simplify iterations.",
1137
+ calculateMetric: (body) => this.calculateComplexity(body),
1138
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS),
1139
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS, PENALTY_CONFIG)
1140
+ });
1141
+ }
1142
+ /**
1143
+ * Calculate cyclomatic complexity by counting decision points
1144
+ * Complexity = 1 + number of decision points
1145
+ */
1146
+ calculateComplexity(node) {
1147
+ let complexity = 1;
1148
+ const DECISION_POINT_KINDS = /* @__PURE__ */ new Set([
1149
+ SyntaxKind2.IfStatement,
1150
+ SyntaxKind2.ConditionalExpression,
1151
+ SyntaxKind2.ForStatement,
1152
+ SyntaxKind2.ForInStatement,
1153
+ SyntaxKind2.ForOfStatement,
1154
+ SyntaxKind2.WhileStatement,
1155
+ SyntaxKind2.DoStatement,
1156
+ SyntaxKind2.CaseClause,
1157
+ SyntaxKind2.CatchClause
1158
+ ]);
1159
+ const LOGICAL_OPERATORS = /* @__PURE__ */ new Set([
1160
+ SyntaxKind2.AmpersandAmpersandToken,
1161
+ // &&
1162
+ SyntaxKind2.BarBarToken,
1163
+ // ||
1164
+ SyntaxKind2.QuestionQuestionToken
1165
+ // ??
1166
+ ]);
1167
+ const isLogicalBinaryExpression = (n) => {
1168
+ const binaryExpr = n;
1169
+ const operator = binaryExpr.getOperatorToken().getKind();
1170
+ return LOGICAL_OPERATORS.has(operator);
1171
+ };
1172
+ const countDecisionPoints = (n) => {
1173
+ const kind = n.getKind();
1174
+ if (DECISION_POINT_KINDS.has(kind)) {
1175
+ complexity++;
1176
+ } else if (kind === SyntaxKind2.BinaryExpression && isLogicalBinaryExpression(n)) {
1177
+ complexity++;
1178
+ }
1179
+ n.getChildren().forEach(countDecisionPoints);
1180
+ };
1181
+ countDecisionPoints(node);
1182
+ return complexity;
1183
+ }
1184
+ };
1185
+
1186
+ // src/rules/deep-nesting.rule.ts
1187
+ import { SyntaxKind as SyntaxKind3 } from "ts-morph";
1188
+ var SEVERITY_THRESHOLDS2 = createSeverityThresholds(5, 4);
1189
+ var DeepNestingRule = class extends FunctionAnalysisRule {
1190
+ name = "deep-nesting";
1191
+ severity = "info";
1192
+ penalty = 3;
1193
+ getConfigKey() {
1194
+ return "maxDepth";
1195
+ }
1196
+ getDefaultThreshold() {
1197
+ return 3;
1198
+ }
1199
+ checkFunction(func, context) {
1200
+ this.checkFunctionGeneric({
1201
+ body: func.getBody(),
1202
+ getName: () => func.getName() ?? "<anonymous>",
1203
+ getLine: () => func.getStartLineNumber(),
1204
+ context,
1205
+ rule: "Deep Nesting",
1206
+ metric: "nesting depth",
1207
+ impact: NESTING_IMPACT,
1208
+ suggestedFix: "Refactor nested blocks into smaller functions with descriptive names. Use early returns/guards to reduce nesting. Apply Extract Method pattern for complex conditional logic.",
1209
+ calculateMetric: (body) => this.calculateNestingDepth(body, 0),
1210
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS2),
1211
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS2, STANDARD_PENALTY_CONFIG)
1212
+ });
1213
+ }
1214
+ checkMethod(method, context) {
1215
+ this.checkMethodGeneric({
1216
+ method,
1217
+ body: method.getBody(),
1218
+ context,
1219
+ rule: "Deep Nesting",
1220
+ metric: "nesting depth",
1221
+ impact: NESTING_IMPACT,
1222
+ suggestedFix: "Break method into smaller helper methods. Use early returns to flatten conditional logic. Extract nested blocks into separate private methods.",
1223
+ calculateMetric: (body) => this.calculateNestingDepth(body, 0),
1224
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS2),
1225
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS2, STANDARD_PENALTY_CONFIG)
1226
+ });
1227
+ }
1228
+ checkArrowFunction(context) {
1229
+ this.checkArrowFunctionGeneric({
1230
+ context,
1231
+ body: context.arrowFunc.getBody(),
1232
+ rule: "Deep Nesting",
1233
+ metric: "nesting depth",
1234
+ impact: NESTING_IMPACT,
1235
+ suggestedFix: "Break method into smaller helper methods. Use early returns and guard clauses to reduce nesting depth.",
1236
+ calculateMetric: (body) => this.calculateNestingDepth(body, 0),
1237
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS2),
1238
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS2, STANDARD_PENALTY_CONFIG)
1239
+ });
1240
+ }
1241
+ /**
1242
+ * Recursively calculate maximum nesting depth
1243
+ * Counts if/for/while/try/switch/case statements
1244
+ */
1245
+ calculateNestingDepth(node, currentDepth) {
1246
+ let maxDepth = currentDepth;
1247
+ const children = node.getChildren();
1248
+ for (const child of children) {
1249
+ const childDepth = this.getChildDepth(child, currentDepth);
1250
+ const depth = this.calculateNestingDepth(child, childDepth);
1251
+ maxDepth = Math.max(maxDepth, depth);
1252
+ }
1253
+ return maxDepth;
1254
+ }
1255
+ getChildDepth(child, currentDepth) {
1256
+ const kind = child.getKind();
1257
+ if (this.isNestingStatement(kind)) {
1258
+ return currentDepth + 1;
1259
+ }
1260
+ return currentDepth;
1261
+ }
1262
+ isNestingStatement(kind) {
1263
+ const nestingKinds = [
1264
+ SyntaxKind3.IfStatement,
1265
+ SyntaxKind3.ForStatement,
1266
+ SyntaxKind3.ForInStatement,
1267
+ SyntaxKind3.ForOfStatement,
1268
+ SyntaxKind3.WhileStatement,
1269
+ SyntaxKind3.DoStatement,
1270
+ SyntaxKind3.SwitchStatement,
1271
+ SyntaxKind3.TryStatement,
1272
+ SyntaxKind3.CatchClause
1273
+ ];
1274
+ return nestingKinds.includes(kind);
1275
+ }
1276
+ };
1277
+
1278
+ // src/rules/large-function.rule.ts
1279
+ var SEVERITY_THRESHOLDS3 = createSeverityThresholds(100, 75);
1280
+ var PENALTY_CONFIG2 = {
1281
+ criticalBase: 15,
1282
+ criticalMultiplier: 0.1,
1283
+ warningBase: 10,
1284
+ warningMultiplier: 0.1,
1285
+ infoBase: 5,
1286
+ infoMultiplier: 0.05
1287
+ };
1288
+ var LargeFunctionRule = class extends FunctionAnalysisRule {
1289
+ name = "large-function";
1290
+ severity = "info";
1291
+ penalty = 5;
1292
+ getConfigKey() {
1293
+ return "maxLines";
1294
+ }
1295
+ getDefaultThreshold() {
1296
+ return 50;
1297
+ }
1298
+ checkFunction(func, context) {
1299
+ this.checkFunctionGeneric({
1300
+ body: func.getBody(),
1301
+ getName: () => func.getName() ?? "<anonymous>",
1302
+ getLine: () => func.getStartLineNumber(),
1303
+ context,
1304
+ rule: "Large Function",
1305
+ metric: "lines",
1306
+ impact: "Reduces testability, code comprehension, and maintainability",
1307
+ suggestedFix: "Split large function into smaller helper functions. Extract complex logic into well-named private functions or utility modules.",
1308
+ calculateMetric: (body) => this.getBodyLineCount(body.getText()),
1309
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS3),
1310
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS3, PENALTY_CONFIG2)
1311
+ });
1312
+ }
1313
+ checkMethod(method, context) {
1314
+ this.checkMethodGeneric({
1315
+ method,
1316
+ body: method.getBody(),
1317
+ context,
1318
+ rule: "Large Function",
1319
+ metric: "lines",
1320
+ impact: "Reduces testability, code comprehension, and maintainability",
1321
+ suggestedFix: "Split method into smaller helper methods. Extract complex logic into separate private methods or utility functions.",
1322
+ calculateMetric: (body) => this.getBodyLineCount(body.getText()),
1323
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS3),
1324
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS3, PENALTY_CONFIG2)
1325
+ });
1326
+ }
1327
+ checkArrowFunction(context) {
1328
+ this.checkArrowFunctionGeneric({
1329
+ context,
1330
+ body: context.arrowFunc.getBody(),
1331
+ rule: "Large Function",
1332
+ metric: "lines",
1333
+ impact: "Reduces testability, code comprehension, and maintainability",
1334
+ suggestedFix: "Convert to named function and break down logic. Extract complex operations into separate functions.",
1335
+ calculateMetric: (body) => this.getBodyLineCount(body.getText()),
1336
+ calculateSeverity: (value) => calculateSeverity(value, SEVERITY_THRESHOLDS3),
1337
+ calculatePenalty: (value) => calculatePenalty(value, context.threshold, SEVERITY_THRESHOLDS3, PENALTY_CONFIG2)
1338
+ });
1339
+ }
1340
+ getBodyLineCount(bodyText) {
1341
+ const cleaned = bodyText.trim();
1342
+ const withoutBraces = cleaned.slice(1, -1).trim();
1343
+ const lines = withoutBraces.split("\n");
1344
+ return lines.filter((line) => {
1345
+ const trimmed = line.trim();
1346
+ return trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*");
1347
+ }).length;
1348
+ }
1349
+ };
1350
+
1351
+ // src/rules/max-file-lines.rule.ts
1352
+ var MaxFileLinesRule = class _MaxFileLinesRule {
1353
+ name = "max-file-lines";
1354
+ severity = "warning";
1355
+ penalty = 3;
1356
+ static DEFAULT_MAX_LINES = 500;
1357
+ static CRITICAL_THRESHOLD = 1e3;
1358
+ static WARNING_THRESHOLD = 500;
1359
+ check(context) {
1360
+ const { project, config, rootPath } = context;
1361
+ const violations = [];
1362
+ const maxLines = config.rules?.maxFileLines || _MaxFileLinesRule.DEFAULT_MAX_LINES;
1363
+ processSourceFiles(
1364
+ project.getSourceFiles(),
1365
+ rootPath,
1366
+ (sourceFile) => {
1367
+ const lineCount = sourceFile.getEndLineNumber();
1368
+ if (lineCount > maxLines) {
1369
+ const severity = calculateSeverityByCount(lineCount, {
1370
+ critical: _MaxFileLinesRule.CRITICAL_THRESHOLD,
1371
+ warning: _MaxFileLinesRule.WARNING_THRESHOLD
1372
+ });
1373
+ violations.push(createThresholdViolation({
1374
+ rule: "Max File Lines",
1375
+ severity,
1376
+ message: `File has ${lineCount} lines (max: ${maxLines})`,
1377
+ file: sourceFile.getFilePath(),
1378
+ rootPath,
1379
+ line: 1,
1380
+ impact: "Large files are harder to maintain, test, and understand. They often indicate poor separation of concerns and violate Single Responsibility Principle.",
1381
+ suggestedFix: `Split this file into smaller, focused modules:
1382
+ 1. Group related functionality into separate files
1383
+ 2. Extract classes, interfaces, and utilities
1384
+ 3. Organize by responsibility (e.g., services, models, utils)
1385
+ 4. Consider using barrel exports (index.ts) for clean imports
1386
+
1387
+ Target: <${maxLines} lines per file`,
1388
+ penalty: this.calculatePenalty(lineCount, maxLines)
1389
+ }));
1390
+ }
1391
+ }
1392
+ );
1393
+ return violations;
1394
+ }
1395
+ calculatePenalty(lineCount, threshold) {
1396
+ const excess = lineCount - threshold;
1397
+ const excessFactor = excess / threshold;
1398
+ return Math.min(10, Math.round(this.penalty + excessFactor * 5));
1399
+ }
1400
+ };
1401
+
1402
+ // src/rules/base/parameter-analysis.rule.ts
1403
+ import { Node as Node3 } from "ts-morph";
1404
+ import { relative as relative3 } from "path";
1405
+ var ParameterAnalysisRule = class {
1406
+ /**
1407
+ * Main check method that iterates over source files
1408
+ * Uses template method pattern to process different function types
1409
+ */
1410
+ check(context) {
1411
+ const sourceFiles = context.project.getSourceFiles();
1412
+ for (const sourceFile of sourceFiles) {
1413
+ if (this.shouldSkipFile(sourceFile.getFilePath())) continue;
1414
+ const relativePath = relative3(context.rootPath, sourceFile.getFilePath());
1415
+ this.processFunctions(sourceFile, relativePath);
1416
+ this.processClasses(sourceFile, relativePath);
1417
+ this.processArrowFunctions(sourceFile, relativePath);
1418
+ }
1419
+ return this.getViolations();
1420
+ }
1421
+ /**
1422
+ * Determine if a file should be skipped during analysis
1423
+ * Override in subclasses for custom skip logic
1424
+ */
1425
+ shouldSkipFile(filePath) {
1426
+ return filePath.includes("node_modules") || filePath.endsWith(".d.ts");
1427
+ }
1428
+ /**
1429
+ * Process all standalone functions in a source file
1430
+ */
1431
+ processFunctions(sourceFile, relativePath) {
1432
+ const functions = sourceFile.getFunctions();
1433
+ for (const func of functions) {
1434
+ this.processFunction(func, relativePath);
1435
+ }
1436
+ }
1437
+ /**
1438
+ * Process all classes (methods and constructors) in a source file
1439
+ */
1440
+ processClasses(sourceFile, relativePath) {
1441
+ const classes = sourceFile.getClasses();
1442
+ for (const cls of classes) {
1443
+ const className = cls.getName() || "<anonymous>";
1444
+ for (const ctor of cls.getConstructors()) {
1445
+ this.processConstructor(ctor, className, relativePath);
1446
+ }
1447
+ for (const method of cls.getMethods()) {
1448
+ this.processMethod(method, className, relativePath);
1449
+ }
1450
+ for (const staticMethod of cls.getStaticMethods()) {
1451
+ this.processMethod(staticMethod, className, relativePath);
1452
+ }
1453
+ }
1454
+ }
1455
+ /**
1456
+ * Process all arrow functions in a source file
1457
+ */
1458
+ processArrowFunctions(sourceFile, relativePath) {
1459
+ const variableStatements = sourceFile.getVariableStatements();
1460
+ for (const stmt of variableStatements) {
1461
+ const declarations = stmt.getDeclarations();
1462
+ for (const decl of declarations) {
1463
+ const initializer = decl.getInitializer();
1464
+ if (initializer && Node3.isArrowFunction(initializer)) {
1465
+ const varName = decl.getName();
1466
+ this.processArrowFunction(initializer, varName, relativePath, decl);
1467
+ }
1468
+ }
1469
+ }
1470
+ }
1471
+ };
1472
+
1473
+ // src/rules/long-parameter-list.rule.ts
1474
+ var CRITICAL_PARAMETER_THRESHOLD = 6;
1475
+ var LongParameterListRule = class extends ParameterAnalysisRule {
1476
+ name = "long-parameter-list";
1477
+ severity = "warning";
1478
+ penalty = CRITICAL_PARAMETER_THRESHOLD;
1479
+ defaultThreshold = 4;
1480
+ threshold = this.defaultThreshold;
1481
+ violations = [];
1482
+ /**
1483
+ * Override check to set threshold from config before processing
1484
+ */
1485
+ check(context) {
1486
+ this.violations = [];
1487
+ const ruleConfig = context.config.rules?.[this.name];
1488
+ this.threshold = getThresholdFromConfig(ruleConfig, "maxParameters") ?? this.defaultThreshold;
1489
+ return super.check(context);
1490
+ }
1491
+ shouldSkipFile(filePath) {
1492
+ return shouldSkipNodeModules(filePath);
1493
+ }
1494
+ processFunction(func, relativePath) {
1495
+ const params = func.getParameters();
1496
+ if (params.length > this.threshold) {
1497
+ const functionName = func.getName() || "<anonymous>";
1498
+ this.violations.push(this.createViolation({
1499
+ name: functionName,
1500
+ count: params.length,
1501
+ threshold: this.threshold,
1502
+ file: relativePath,
1503
+ line: func.getStartLineNumber(),
1504
+ type: "function"
1505
+ }));
1506
+ }
1507
+ }
1508
+ processMethod(method, className, relativePath) {
1509
+ const params = method.getParameters();
1510
+ if (params.length > this.threshold) {
1511
+ const methodName = method.getName();
1512
+ this.violations.push(this.createViolation({
1513
+ name: `${className}.${methodName}`,
1514
+ count: params.length,
1515
+ threshold: this.threshold,
1516
+ file: relativePath,
1517
+ line: method.getStartLineNumber(),
1518
+ type: "method"
1519
+ }));
1520
+ }
1521
+ }
1522
+ processConstructor(constructor, className, relativePath) {
1523
+ const params = constructor.getParameters();
1524
+ if (params.length <= 6) return;
1525
+ if (params.length > this.threshold) {
1526
+ this.violations.push(this.createViolation({
1527
+ name: `${className}.constructor`,
1528
+ count: params.length,
1529
+ threshold: this.threshold,
1530
+ file: relativePath,
1531
+ line: constructor.getStartLineNumber(),
1532
+ type: "constructor"
1533
+ }));
1534
+ }
1535
+ }
1536
+ processArrowFunction(arrowFunc, varName, relativePath, declaration) {
1537
+ const params = arrowFunc.getParameters();
1538
+ if (params.length > this.threshold) {
1539
+ this.violations.push(this.createViolation({
1540
+ name: varName,
1541
+ count: params.length,
1542
+ threshold: this.threshold,
1543
+ file: relativePath,
1544
+ line: declaration.getStartLineNumber(),
1545
+ type: "arrow function"
1546
+ }));
1547
+ }
1548
+ }
1549
+ getViolations() {
1550
+ return this.violations;
1551
+ }
1552
+ createViolation(data) {
1553
+ const fixMessage = data.type === "constructor" ? "Use dependency injection container or grouping configuration objects." : `Reduce parameters using:
1554
+ 1. Parameter object pattern (group related params)
1555
+ 2. Builder pattern for complex construction
1556
+ 3. Extract to multiple focused functions
1557
+ 4. Store configuration in class properties`;
1558
+ return createViolation({
1559
+ rule: "Long Parameter List",
1560
+ severity: this.getSeverityByCount(data.count),
1561
+ message: `${data.type === "method" ? "Method" : data.type === "constructor" ? "Constructor" : "Function"} '${data.name}' has ${data.count} parameters (max: ${data.threshold})`,
1562
+ file: data.file,
1563
+ line: data.line,
1564
+ impact: "Reduces code readability, increases testing complexity, and makes refactoring harder",
1565
+ suggestedFix: fixMessage,
1566
+ penalty: this.calculatePenalty(data.count, data.threshold)
1567
+ });
1568
+ }
1569
+ getSeverityByCount(count) {
1570
+ if (count > CRITICAL_PARAMETER_THRESHOLD) return "critical";
1571
+ if (count >= 5) return "warning";
1572
+ return "info";
1573
+ }
1574
+ calculatePenalty(count, threshold) {
1575
+ return Math.min(30, (count - threshold) * 5);
1576
+ }
1577
+ };
1578
+
1579
+ // src/rules/data-clumps.rule.ts
1580
+ var DataClumpsRule = class extends ParameterAnalysisRule {
1581
+ name = "data-clumps";
1582
+ severity = "warning";
1583
+ penalty = 10;
1584
+ defaultMinOccurrences = 3;
1585
+ minOccurrences = this.defaultMinOccurrences;
1586
+ parameterGroups = /* @__PURE__ */ new Map();
1587
+ /**
1588
+ * Override check to set config and reset state before processing
1589
+ */
1590
+ check(context) {
1591
+ this.parameterGroups = /* @__PURE__ */ new Map();
1592
+ const ruleConfig = context.config.rules?.[this.name];
1593
+ this.minOccurrences = getThresholdFromConfig(ruleConfig, "minOccurrences") ?? this.defaultMinOccurrences;
1594
+ return super.check(context);
1595
+ }
1596
+ processFunction(func, relativePath) {
1597
+ const funcName = func.getName() || "anonymous";
1598
+ const context = {
1599
+ filePath: relativePath,
1600
+ functionName: funcName
1601
+ };
1602
+ this.processParameters(func, context);
1603
+ }
1604
+ processMethod(method, className, relativePath) {
1605
+ const methodName = method.getName();
1606
+ const context = {
1607
+ filePath: relativePath,
1608
+ functionName: methodName,
1609
+ className
1610
+ };
1611
+ this.processParameters(method, context);
1612
+ }
1613
+ processConstructor(constructor, className, relativePath) {
1614
+ const context = {
1615
+ filePath: relativePath,
1616
+ functionName: "constructor",
1617
+ className
1618
+ };
1619
+ this.processParameters(constructor, context);
1620
+ }
1621
+ processArrowFunction(arrowFunc, varName, relativePath, _) {
1622
+ const context = {
1623
+ filePath: relativePath,
1624
+ functionName: varName
1625
+ };
1626
+ this.processParameters(arrowFunc, context);
1627
+ }
1628
+ getViolations() {
1629
+ return this.generateViolations();
1630
+ }
1631
+ processParameters(node, context) {
1632
+ const parameters = node.getParameters();
1633
+ if (parameters.length < 3) return;
1634
+ const paramNames = parameters.map((p) => p.getName());
1635
+ const paramTypes = parameters.map((p) => p.getType().getText());
1636
+ const signature = paramNames.map((name, i) => `${name}:${paramTypes[i]}`).join(",");
1637
+ const methodName = context.className ? `${context.className}.${context.functionName}` : context.functionName;
1638
+ if (this.parameterGroups.has(signature)) {
1639
+ this.parameterGroups.get(signature)?.occurrences.push({
1640
+ file: context.filePath,
1641
+ method: methodName,
1642
+ line: node.getStartLineNumber()
1643
+ });
1644
+ } else {
1645
+ this.parameterGroups.set(signature, {
1646
+ parameters: paramNames,
1647
+ types: paramTypes,
1648
+ occurrences: [{
1649
+ file: context.filePath,
1650
+ method: methodName,
1651
+ line: node.getStartLineNumber()
1652
+ }]
1653
+ });
1654
+ }
1655
+ }
1656
+ generateViolations() {
1657
+ const violations = [];
1658
+ for (const group of this.parameterGroups.values()) {
1659
+ if (group.occurrences.length >= this.minOccurrences) {
1660
+ violations.push(createViolation({
1661
+ rule: "Data Clumps",
1662
+ severity: this.severity,
1663
+ message: `Found data clump with ${group.parameters.length} parameters (${group.parameters.join(", ")}) appearing in ${group.occurrences.length} locations.`,
1664
+ file: group.occurrences[0].file,
1665
+ line: group.occurrences[0].line,
1666
+ impact: "Repeating groups of parameters being passed around indicates missing abstraction.",
1667
+ suggestedFix: `Extract parameters (${group.parameters.join(", ")}) into a new class or interface.`,
1668
+ penalty: this.penalty
1669
+ }));
1670
+ }
1671
+ }
1672
+ return violations;
1673
+ }
1674
+ };
1675
+
1676
+ // src/rules/shotgun-surgery.rule.ts
1677
+ import { relative as relative4 } from "path";
1678
+ var ShotgunSurgeryRule = class {
1679
+ name = "shotgun-surgery";
1680
+ severity = "info";
1681
+ penalty = 6;
1682
+ defaultThreshold = 5;
1683
+ check(context) {
1684
+ const sourceFiles = context.project.getSourceFiles();
1685
+ const ruleConfig = context.config.rules?.[this.name];
1686
+ const threshold = getThresholdFromConfig(ruleConfig, "minFiles") ?? this.defaultThreshold;
1687
+ const symbolUsages = this.collectExportedSymbols(sourceFiles, context.rootPath);
1688
+ this.trackImports(sourceFiles, context.rootPath, symbolUsages);
1689
+ return this.generateViolations(symbolUsages, threshold);
1690
+ }
1691
+ collectExportedSymbols(sourceFiles, rootPath) {
1692
+ const symbolUsages = /* @__PURE__ */ new Map();
1693
+ processSourceFiles(
1694
+ sourceFiles,
1695
+ rootPath,
1696
+ (sourceFile, _, relativePath) => {
1697
+ const exportedDeclarations = sourceFile.getExportedDeclarations();
1698
+ for (const [name, declarations] of exportedDeclarations) {
1699
+ if (declarations.length === 0) continue;
1700
+ const mainDecl = declarations[0];
1701
+ const kindName = mainDecl.getKindName();
1702
+ if (["InterfaceDeclaration", "TypeAliasDeclaration", "EnumDeclaration"].includes(kindName)) {
1703
+ continue;
1704
+ }
1705
+ const key = `${relativePath}::${name}`;
1706
+ if (!symbolUsages.has(key)) {
1707
+ symbolUsages.set(key, {
1708
+ name,
1709
+ usedInFiles: /* @__PURE__ */ new Set(),
1710
+ usageCount: 0
1711
+ });
1712
+ }
1713
+ }
1714
+ }
1715
+ );
1716
+ return symbolUsages;
1717
+ }
1718
+ trackImports(sourceFiles, rootPath, symbolUsages) {
1719
+ processSourceFiles(
1720
+ sourceFiles,
1721
+ rootPath,
1722
+ (sourceFile, _, relativePath) => {
1723
+ const imports = sourceFile.getImportDeclarations();
1724
+ for (const importDecl of imports) {
1725
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1726
+ if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) continue;
1727
+ const importedFile = importDecl.getModuleSpecifierSourceFile();
1728
+ if (!importedFile) continue;
1729
+ const importedPath = relative4(rootPath, importedFile.getFilePath());
1730
+ this.trackNamedImports(importDecl, importedPath, relativePath, symbolUsages);
1731
+ this.trackDefaultImport(importDecl, importedPath, relativePath, symbolUsages);
1732
+ this.trackNamespaceImport(importDecl, importedPath, relativePath, symbolUsages);
1733
+ }
1734
+ }
1735
+ );
1736
+ }
1737
+ trackNamedImports(importDecl, importedPath, relativePath, symbolUsages) {
1738
+ const namedImports = importDecl.getNamedImports();
1739
+ for (const namedImport of namedImports) {
1740
+ const importName = namedImport.getName();
1741
+ const key = `${importedPath}::${importName}`;
1742
+ const usage = symbolUsages.get(key);
1743
+ if (usage) {
1744
+ usage.usedInFiles.add(relativePath);
1745
+ usage.usageCount++;
1746
+ }
1747
+ }
1748
+ }
1749
+ trackDefaultImport(importDecl, importedPath, relativePath, symbolUsages) {
1750
+ const defaultImport = importDecl.getDefaultImport();
1751
+ if (defaultImport) {
1752
+ const key = `${importedPath}::default`;
1753
+ const usage = symbolUsages.get(key);
1754
+ if (usage) {
1755
+ usage.usedInFiles.add(relativePath);
1756
+ usage.usageCount++;
1757
+ }
1758
+ }
1759
+ }
1760
+ /**
1761
+ * Track namespace imports (import * as name)
1762
+ * These indicate heavy coupling to a module's entire API
1763
+ */
1764
+ trackNamespaceImport(importDecl, importedPath, relativePath, symbolUsages) {
1765
+ const namespaceImport = importDecl.getNamespaceImport();
1766
+ if (namespaceImport) {
1767
+ const key = `${importedPath}::*`;
1768
+ if (!symbolUsages.has(key)) {
1769
+ symbolUsages.set(key, {
1770
+ name: "*",
1771
+ usedInFiles: /* @__PURE__ */ new Set(),
1772
+ usageCount: 0
1773
+ });
1774
+ }
1775
+ const usage = symbolUsages.get(key);
1776
+ usage.usedInFiles.add(relativePath);
1777
+ usage.usageCount++;
1778
+ }
1779
+ }
1780
+ generateViolations(symbolUsages, threshold) {
1781
+ const violations = [];
1782
+ for (const [key, usage] of symbolUsages.entries()) {
1783
+ const fileCount = usage.usedInFiles.size;
1784
+ if (fileCount >= threshold) {
1785
+ const [filePath, symbolName] = key.split("::");
1786
+ const fileList = Array.from(usage.usedInFiles).slice(0, 5).join(", ");
1787
+ violations.push(createViolation({
1788
+ rule: "Shotgun Surgery",
1789
+ severity: this.getSeverityByFileCount(fileCount),
1790
+ message: `Symbol '${symbolName}' from ${filePath} is used in ${fileCount} files: ${fileList}${fileCount > 5 ? "..." : ""}`,
1791
+ file: filePath,
1792
+ line: 1,
1793
+ impact: "Changes to this symbol require modifying many files, increasing risk and maintenance cost",
1794
+ suggestedFix: `Consider:
1795
+ 1. Introducing a facade or wrapper to reduce direct coupling
1796
+ 2. Using dependency injection to centralize usage
1797
+ 3. Extracting shared behavior into a base class or mixin
1798
+ 4. Evaluating if this is truly shared logic or duplicated code
1799
+ 5. Creating a higher-level abstraction that encapsulates this logic`,
1800
+ penalty: this.calculatePenalty(fileCount, threshold)
1801
+ }));
1802
+ }
1803
+ }
1804
+ return violations;
1805
+ }
1806
+ getSeverityByFileCount(count) {
1807
+ if (count >= 10) return "warning";
1808
+ return "info";
1809
+ }
1810
+ calculatePenalty(count, threshold) {
1811
+ return Math.min(10, 6 + Math.floor((count - threshold) / 2));
1812
+ }
1813
+ };
1814
+
1815
+ // src/rules/duplicate-code.rule.ts
1816
+ var CRITICAL_PENALTY = 2;
1817
+ var WARNING_PENALTY = 1;
1818
+ var INFO_PENALTY = 0.5;
1819
+ var MIN_DUPLICATE_LINES = 5;
1820
+ var CRITICAL_FILE_COUNT_THRESHOLD = 5;
1821
+ var WARNING_FILE_COUNT_THRESHOLD = 3;
1822
+ var DuplicateCodeRule = class {
1823
+ name = "duplicate-code";
1824
+ severity = "warning";
1825
+ penalty = INFO_PENALTY;
1826
+ minLines = MIN_DUPLICATE_LINES;
1827
+ check(context) {
1828
+ const sourceFiles = context.project.getSourceFiles();
1829
+ const ruleConfig = context.config.rules?.[this.name];
1830
+ let minLines = this.minLines;
1831
+ if (ruleConfig && typeof ruleConfig === "object" && "minLines" in ruleConfig) {
1832
+ const configMinLines = ruleConfig.minLines;
1833
+ if (typeof configMinLines === "number") {
1834
+ minLines = configMinLines;
1835
+ }
1836
+ }
1837
+ const codeBlocks = this.collectCodeBlocks(sourceFiles, context.rootPath, minLines);
1838
+ return this.generateViolations(codeBlocks);
1839
+ }
1840
+ collectCodeBlocks(sourceFiles, rootPath, minLines) {
1841
+ const codeBlocks = /* @__PURE__ */ new Map();
1842
+ processSourceFiles(
1843
+ sourceFiles,
1844
+ rootPath,
1845
+ (sourceFile, _, relativePath) => {
1846
+ const lines = sourceFile.getFullText().split("\n");
1847
+ for (let i = 0; i <= lines.length - minLines; i++) {
1848
+ const block = lines.slice(i, i + minLines);
1849
+ const normalized = this.normalizeCode(block);
1850
+ if (this.isInsignificant(normalized)) {
1851
+ continue;
1852
+ }
1853
+ const hash = this.hashCode(normalized);
1854
+ if (!codeBlocks.has(hash)) {
1855
+ codeBlocks.set(hash, []);
1856
+ }
1857
+ codeBlocks.get(hash).push({
1858
+ file: relativePath,
1859
+ line: i + 1,
1860
+ code: block.join("\n")
1861
+ });
1862
+ }
1863
+ },
1864
+ { skipTests: true }
1865
+ );
1866
+ return codeBlocks;
1867
+ }
1868
+ generateViolations(codeBlocks) {
1869
+ const violations = [];
1870
+ for (const [_hash, locations] of codeBlocks.entries()) {
1871
+ if (locations.length < 2) continue;
1872
+ const uniqueFiles = this.getUniqueFiles(locations);
1873
+ if (uniqueFiles.size < 2) continue;
1874
+ const violation = this.createDuplicateViolation(uniqueFiles);
1875
+ violations.push(violation);
1876
+ }
1877
+ return violations;
1878
+ }
1879
+ getUniqueFiles(locations) {
1880
+ const uniqueFiles = /* @__PURE__ */ new Map();
1881
+ for (const loc of locations) {
1882
+ if (!uniqueFiles.has(loc.file)) {
1883
+ uniqueFiles.set(loc.file, { line: loc.line, code: loc.code });
1884
+ }
1885
+ }
1886
+ return uniqueFiles;
1887
+ }
1888
+ createDuplicateViolation(uniqueFiles) {
1889
+ const files = Array.from(uniqueFiles.keys());
1890
+ const filesList = files.slice(0, 3).join(", ") + (files.length > 3 ? `, ... (${files.length} files)` : "");
1891
+ const { severity, penalty } = this.calculateSeverityAndPenalty(uniqueFiles.size);
1892
+ const firstFile = files[0];
1893
+ const firstOcc = uniqueFiles.get(firstFile);
1894
+ return createViolation({
1895
+ rule: "Duplicate Code",
1896
+ severity,
1897
+ message: `Code duplicated in ${uniqueFiles.size} files: ${filesList}`,
1898
+ file: firstFile,
1899
+ line: firstOcc.line,
1900
+ impact: "Increases maintenance burden and risk of inconsistencies",
1901
+ suggestedFix: "Extract common logic into a shared function, class, or component",
1902
+ penalty
1903
+ });
1904
+ }
1905
+ calculateSeverityAndPenalty(fileCount) {
1906
+ if (fileCount >= CRITICAL_FILE_COUNT_THRESHOLD) {
1907
+ return { severity: "critical", penalty: CRITICAL_PENALTY };
1908
+ }
1909
+ if (fileCount >= WARNING_FILE_COUNT_THRESHOLD) {
1910
+ return { severity: "warning", penalty: WARNING_PENALTY };
1911
+ }
1912
+ return { severity: "info", penalty: INFO_PENALTY };
1913
+ }
1914
+ normalizeCode(lines) {
1915
+ return lines.map((line) => line.trim()).filter((line) => line.length > 0).join("");
1916
+ }
1917
+ isInsignificant(normalized) {
1918
+ if (normalized.length < 10) return true;
1919
+ if (normalized.startsWith("import") || normalized.startsWith("export")) return true;
1920
+ if (normalized.startsWith("//") || normalized.startsWith("/*")) return true;
1921
+ if (/^[}\])]+$/.test(normalized)) return true;
1922
+ return false;
1923
+ }
1924
+ /**
1925
+ * Generate hash using DJB2 algorithm for better collision resistance
1926
+ * @param s String to hash
1927
+ * @returns Hexadecimal hash string
1928
+ */
1929
+ hashCode(s) {
1930
+ let hash = 5381;
1931
+ for (let i = 0; i < s.length; i++) {
1932
+ const char = s.charCodeAt(i);
1933
+ hash = (hash << 5) + hash + char;
1934
+ hash = hash >>> 0;
1935
+ }
1936
+ return hash.toString(16);
1937
+ }
1938
+ };
1939
+
1940
+ // src/rules/unused-exports.rule.ts
1941
+ var UnusedExportsRule = class {
1942
+ name = "unused-exports";
1943
+ severity = "info";
1944
+ penalty = 1;
1945
+ check(context) {
1946
+ const { project, config, rootPath } = context;
1947
+ const sourceFiles = project.getSourceFiles();
1948
+ const excludePatterns = config.rules?.[this.name]?.excludePatterns || [
1949
+ "index.ts",
1950
+ "index.tsx",
1951
+ "public-api.ts",
1952
+ "api.ts",
1953
+ ".d.ts"
1954
+ ];
1955
+ const exportMap = this.collectExports(sourceFiles, rootPath, excludePatterns);
1956
+ const importedNames = this.collectImports(sourceFiles);
1957
+ return this.findUnusedExports(exportMap, importedNames, sourceFiles, rootPath);
1958
+ }
1959
+ collectExports(sourceFiles, rootPath, excludePatterns) {
1960
+ const exportMap = /* @__PURE__ */ new Map();
1961
+ processSourceFiles(
1962
+ sourceFiles,
1963
+ rootPath,
1964
+ (sourceFile, filePath, relativePath) => {
1965
+ if (excludePatterns.some((pattern) => relativePath.includes(pattern))) return;
1966
+ const exports = /* @__PURE__ */ new Map();
1967
+ const exportedDeclarations = sourceFile.getExportedDeclarations();
1968
+ for (const [name, declarations] of exportedDeclarations) {
1969
+ if (declarations.length > 0) {
1970
+ const line = declarations[0].getStartLineNumber();
1971
+ exports.set(name, { line, filePath });
1972
+ }
1973
+ }
1974
+ if (exports.size > 0) {
1975
+ exportMap.set(relativePath, exports);
1976
+ }
1977
+ }
1978
+ );
1979
+ return exportMap;
1980
+ }
1981
+ collectImports(sourceFiles) {
1982
+ const importedNames = /* @__PURE__ */ new Map();
1983
+ for (const sourceFile of sourceFiles) {
1984
+ if (sourceFile.getFilePath().includes("node_modules")) continue;
1985
+ const imports = sourceFile.getImportDeclarations();
1986
+ for (const importDecl of imports) {
1987
+ const namedImports = importDecl.getNamedImports();
1988
+ for (const namedImport of namedImports) {
1989
+ const name = namedImport.getName();
1990
+ importedNames.set(name, (importedNames.get(name) || 0) + 1);
1991
+ }
1992
+ const defaultImport = importDecl.getDefaultImport();
1993
+ if (defaultImport) {
1994
+ const name = defaultImport.getText();
1995
+ importedNames.set(name, (importedNames.get(name) || 0) + 1);
1996
+ }
1997
+ }
1998
+ }
1999
+ return importedNames;
2000
+ }
2001
+ findUnusedExports(exportMap, importedNames, sourceFiles, rootPath) {
2002
+ const violations = [];
2003
+ for (const [relativePath, exports] of exportMap.entries()) {
2004
+ const sourceFile = sourceFiles.find((sf) => {
2005
+ const sfPath = sf.getFilePath().replace(rootPath + "/", "");
2006
+ return sfPath === relativePath;
2007
+ });
2008
+ for (const [exportName, { line }] of exports) {
2009
+ if (exportName === "default") continue;
2010
+ const importCount = importedNames.get(exportName) || 0;
2011
+ let usedLocally = false;
2012
+ if (sourceFile && importCount === 0) {
2013
+ usedLocally = this.checkLocalUsage(sourceFile, exportName);
2014
+ }
2015
+ if (importCount === 0 && !usedLocally) {
2016
+ violations.push(createViolation({
2017
+ rule: "Unused Export",
2018
+ severity: "info",
2019
+ message: `Export '${exportName}' is never imported`,
2020
+ file: relativePath,
2021
+ line,
2022
+ impact: "Dead code that increases maintenance burden and may confuse developers",
2023
+ suggestedFix: `Remove the export if truly unused. If it's part of a public API, document it. If it's used externally, add it to exclusion patterns in config.`,
2024
+ penalty: 4
2025
+ }));
2026
+ }
2027
+ }
2028
+ }
2029
+ return violations;
2030
+ }
2031
+ checkLocalUsage(sourceFile, exportName) {
2032
+ const declarations = sourceFile.getExportedDeclarations().get(exportName);
2033
+ if (!declarations || declarations.length === 0) return false;
2034
+ const declaration = declarations[0];
2035
+ if (declaration.getKindName() === "TypeAliasDeclaration" || declaration.getKindName() === "InterfaceDeclaration") {
2036
+ const fileText = sourceFile.getFullText();
2037
+ const typeUsagePattern = new RegExp(`(:|extends|implements|as)\\s*${exportName}\\b|<\\s*${exportName}\\s*>`, "g");
2038
+ const matches = fileText.match(typeUsagePattern);
2039
+ return matches ? matches.length > 0 : false;
2040
+ } else {
2041
+ const hasReferences = (node) => {
2042
+ return "findReferencesAsNodes" in node && typeof node.findReferencesAsNodes === "function";
2043
+ };
2044
+ if (hasReferences(declaration)) {
2045
+ const references = declaration.findReferencesAsNodes();
2046
+ return references.length > 1;
2047
+ }
2048
+ }
2049
+ return false;
2050
+ }
2051
+ };
2052
+
2053
+ // src/rules/dead-code.rule.ts
2054
+ import { Node as Node4, SyntaxKind as SyntaxKind4 } from "ts-morph";
2055
+ var DeadCodeRule = class {
2056
+ name = "dead-code";
2057
+ severity = "info";
2058
+ penalty = 4;
2059
+ check(context) {
2060
+ const { project, rootPath } = context;
2061
+ const violations = [];
2062
+ processSourceFiles(
2063
+ project.getSourceFiles(),
2064
+ rootPath,
2065
+ (sourceFile, filePath, _) => {
2066
+ this.checkUnreachableAfterReturn(sourceFile, filePath, violations);
2067
+ this.checkUnusedVariables(sourceFile, filePath, violations);
2068
+ }
2069
+ );
2070
+ return violations;
2071
+ }
2072
+ checkUnreachableAfterReturn(sourceFile, filePath, violations) {
2073
+ const functions = [
2074
+ ...sourceFile.getFunctions(),
2075
+ ...sourceFile.getClasses().flatMap((cls) => cls.getMethods())
2076
+ ];
2077
+ for (const func of functions) {
2078
+ const body = func.getBody();
2079
+ if (!body || !Node4.isBlock(body)) continue;
2080
+ const statements = body.getStatements();
2081
+ for (let i = 0; i < statements.length - 1; i++) {
2082
+ const statement = statements[i];
2083
+ if (this.isExitStatement(statement)) {
2084
+ const nextStatement = statements[i + 1];
2085
+ const functionName = "getName" in func ? func.getName() : "<anonymous>";
2086
+ violations.push(createViolation({
2087
+ rule: "Dead Code",
2088
+ severity: "info",
2089
+ message: `Unreachable code after ${statement.getKindName().toLowerCase()} in '${functionName}'`,
2090
+ file: filePath,
2091
+ line: nextStatement.getStartLineNumber(),
2092
+ impact: "Dead code increases maintenance burden and confuses developers",
2093
+ suggestedFix: "Remove unreachable code or restructure logic to make it reachable",
2094
+ penalty: 4
2095
+ }));
2096
+ break;
2097
+ }
2098
+ }
2099
+ }
2100
+ }
2101
+ checkUnusedVariables(sourceFile, filePath, violations) {
2102
+ const variableStatements = sourceFile.getVariableStatements();
2103
+ for (const stmt of variableStatements) {
2104
+ if (stmt.isExported()) continue;
2105
+ const declarations = stmt.getDeclarations();
2106
+ this.checkDeclarationsForUnusedVariables(declarations, filePath, sourceFile, violations);
2107
+ }
2108
+ }
2109
+ checkDeclarationsForUnusedVariables(declarations, filePath, sourceFile, violations) {
2110
+ for (const decl of declarations) {
2111
+ const name = decl.getName();
2112
+ if (this.shouldSkipVariable(name)) continue;
2113
+ const references = decl.findReferencesAsNodes();
2114
+ if (references.length !== 1) continue;
2115
+ const fileText = sourceFile.getFullText();
2116
+ if (this.isVariableUsed(name, fileText)) continue;
2117
+ violations.push(createViolation({
2118
+ rule: "Dead Code",
2119
+ severity: "info",
2120
+ message: `Variable '${name}' is declared but never used`,
2121
+ file: filePath,
2122
+ line: decl.getStartLineNumber(),
2123
+ impact: "Unused variables add clutter and may indicate incomplete refactoring",
2124
+ suggestedFix: `Remove unused variable '${name}' or use it in the code`,
2125
+ penalty: 2
2126
+ }));
2127
+ }
2128
+ }
2129
+ shouldSkipVariable(name) {
2130
+ return name.includes("{") || name.includes("[");
2131
+ }
2132
+ isVariableUsed(name, fileText) {
2133
+ const usagePattern = new RegExp(`\\b${name}\\b`, "g");
2134
+ const matches = fileText.match(usagePattern);
2135
+ if (matches && matches.length > 1) return true;
2136
+ const variableUsagePattern = new RegExp(`\\[\\s*${name}\\s*\\]|\\[\\s*['"\`]${name}['"\`]\\s*\\]|${name}\\s*\\[`, "g");
2137
+ if (variableUsagePattern.test(fileText)) return true;
2138
+ const contextPattern = new RegExp(`[:{,]\\s*${name}\\b|\\(${name}\\)|<${name}>`, "g");
2139
+ return contextPattern.test(fileText);
2140
+ }
2141
+ isExitStatement(statement) {
2142
+ const kind = statement.getKind();
2143
+ return kind === SyntaxKind4.ReturnStatement || kind === SyntaxKind4.ThrowStatement;
2144
+ }
2145
+ };
2146
+
2147
+ // src/core/analyzer.ts
2148
+ var Analyzer = class {
2149
+ rules = [
2150
+ // === CORE ARCHITECTURE RULES (Critical) ===
2151
+ new CircularDepsRule(),
2152
+ new LayerViolationRule(),
2153
+ new ForbiddenImportsRule(),
2154
+ // === COUPLING & COMPLEXITY ANALYSIS ===
2155
+ new TooManyImportsRule(),
2156
+ new CyclomaticComplexityRule(),
2157
+ new DeepNestingRule(),
2158
+ new LargeFunctionRule(),
2159
+ new MaxFileLinesRule(),
2160
+ // === DESIGN PATTERN & API QUALITY ===
2161
+ new LongParameterListRule(),
2162
+ new DataClumpsRule(),
2163
+ new ShotgunSurgeryRule(),
2164
+ // === CODE HEALTH ===
2165
+ new DuplicateCodeRule(),
2166
+ new UnusedExportsRule(),
2167
+ new DeadCodeRule()
2168
+ ];
2169
+ projectLoader = new ProjectLoader();
2170
+ graphBuilder = new GraphBuilder();
2171
+ scoreCalculator = new ScoreCalculator();
2172
+ riskRanker = new RiskRanker();
2173
+ async analyze(config) {
2174
+ const projectContext = await this.projectLoader.load(config);
2175
+ const project = this.projectLoader.getProject();
2176
+ const graph = this.graphBuilder.build(project, projectContext.rootPath);
2177
+ const { violations, errors } = this.runRules(project, graph, config, projectContext.rootPath);
2178
+ const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules"));
2179
+ const actualModuleCount = sourceFiles.length;
2180
+ const totalLOC = sourceFiles.reduce((sum, sf) => {
2181
+ return sum + sf.getEndLineNumber();
2182
+ }, 0);
2183
+ const counts = this.riskRanker.countBySeverity(violations);
2184
+ const { score, status, breakdown } = this.scoreCalculator.calculate(
2185
+ violations,
2186
+ actualModuleCount,
2187
+ totalLOC
2188
+ );
2189
+ const topRisks = this.riskRanker.rank(violations, 5);
2190
+ const violatedFiles = new Set(violations.map((v) => v.file));
2191
+ const healthyModuleCount = Math.max(0, actualModuleCount - violatedFiles.size);
2192
+ const projectName = this.getProjectName(projectContext.rootPath);
2193
+ const result = {
2194
+ violations,
2195
+ score,
2196
+ status,
2197
+ criticalCount: counts.critical,
2198
+ warningCount: counts.warning,
2199
+ infoCount: counts.info,
2200
+ healthyModuleCount,
2201
+ totalModules: actualModuleCount,
2202
+ topRisks,
2203
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2204
+ projectName,
2205
+ scoreBreakdown: breakdown,
2206
+ totalLOC
2207
+ };
2208
+ if (errors.length > 0) {
2209
+ result.ruleErrors = errors;
2210
+ }
2211
+ return result;
2212
+ }
2213
+ getProjectName(rootPath) {
2214
+ try {
2215
+ const packageJsonPath = join(rootPath, "package.json");
2216
+ if (existsSync(packageJsonPath)) {
2217
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2218
+ if (packageJson.name) {
2219
+ return packageJson.name;
2220
+ }
2221
+ }
2222
+ } catch (error) {
2223
+ }
2224
+ return basename(rootPath);
2225
+ }
2226
+ runRules(project, graph, config, rootPath) {
2227
+ const allViolations = [];
2228
+ const ruleErrors = [];
2229
+ const context = createRuleContext(project, graph, config, rootPath);
2230
+ for (const rule of this.rules) {
2231
+ try {
2232
+ const violations = rule.check(context);
2233
+ allViolations.push(...violations);
2234
+ } catch (error) {
2235
+ const ruleError = this.createRuleError(rule.name, error);
2236
+ ruleErrors.push(ruleError);
2237
+ this.logRuleError(rule.name, error);
2238
+ }
2239
+ }
2240
+ return { violations: allViolations, errors: ruleErrors };
2241
+ }
2242
+ createRuleError(ruleName, error) {
2243
+ return {
2244
+ ruleName,
2245
+ error: error instanceof Error ? error : new Error(String(error)),
2246
+ stack: error instanceof Error ? error.stack : void 0
2247
+ };
2248
+ }
2249
+ logRuleError(ruleName, error) {
2250
+ console.error(`
2251
+ \u26A0\uFE0F Error in rule "${ruleName}":`);
2252
+ console.error(error instanceof Error ? error.message : String(error));
2253
+ if (error instanceof Error && error.stack) {
2254
+ console.error("\nStack trace:");
2255
+ console.error(error.stack);
2256
+ }
2257
+ console.error("");
2258
+ }
2259
+ };
2260
+
2261
+ // src/output/terminal-reporter.ts
2262
+ import pc8 from "picocolors";
2263
+
2264
+ // src/output/utils/violation-utils.ts
2265
+ function groupViolationsByType(violations) {
2266
+ const grouped = {};
2267
+ for (const violation of violations) {
2268
+ if (!grouped[violation.rule]) {
2269
+ grouped[violation.rule] = [];
2270
+ }
2271
+ grouped[violation.rule].push(violation);
2272
+ }
2273
+ return grouped;
2274
+ }
2275
+ function getFileName(filePath) {
2276
+ return filePath.split("/").pop() || filePath;
2277
+ }
2278
+
2279
+ // src/output/formatters.ts
2280
+ import pc from "picocolors";
2281
+ function getSeverityIcon(severity) {
2282
+ switch (severity) {
2283
+ case "critical":
2284
+ return "\u{1F6A8}";
2285
+ case "warning":
2286
+ return "\u26A0\uFE0F";
2287
+ case "info":
2288
+ return "\u2139\uFE0F";
2289
+ default:
2290
+ return "\u2022";
2291
+ }
2292
+ }
2293
+ function getSeverityColor(severity) {
2294
+ switch (severity) {
2295
+ case "critical":
2296
+ return pc.red;
2297
+ case "warning":
2298
+ return pc.yellow;
2299
+ case "info":
2300
+ return pc.blue;
2301
+ default:
2302
+ return pc.white;
2303
+ }
2304
+ }
2305
+ function getStatusIcon(status) {
2306
+ switch (status) {
2307
+ case "Excellent":
2308
+ return "\u2728";
2309
+ case "Healthy":
2310
+ return "\u2705";
2311
+ case "Needs Attention":
2312
+ return "\u26A0\uFE0F";
2313
+ case "Critical":
2314
+ return "\u{1F6A8}";
2315
+ default:
2316
+ return "\u2022";
2317
+ }
2318
+ }
2319
+ function getStatusColor(status) {
2320
+ switch (status) {
2321
+ case "Excellent":
2322
+ return pc.green;
2323
+ case "Healthy":
2324
+ return pc.cyan;
2325
+ case "Needs Attention":
2326
+ return pc.yellow;
2327
+ case "Critical":
2328
+ return pc.red;
2329
+ default:
2330
+ return pc.white;
2331
+ }
2332
+ }
2333
+ function getScoreColor(score) {
2334
+ if (score >= 90) return pc.green;
2335
+ if (score >= 75) return pc.cyan;
2336
+ if (score >= 60) return pc.yellow;
2337
+ return pc.red;
2338
+ }
2339
+ function getScoreBar(score) {
2340
+ const filled = Math.floor(score / 10);
2341
+ const empty = 10 - filled;
2342
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
2343
+ if (score >= 90) return pc.green(bar);
2344
+ if (score >= 75) return pc.cyan(bar);
2345
+ if (score >= 60) return pc.yellow(bar);
2346
+ return pc.red(bar);
2347
+ }
2348
+ function formatPriority(priority) {
2349
+ switch (priority) {
2350
+ case "HIGH":
2351
+ return pc.red(pc.bold(priority));
2352
+ case "MEDIUM":
2353
+ return pc.yellow(pc.bold(priority));
2354
+ case "LOW":
2355
+ return pc.green(pc.bold(priority));
2356
+ default:
2357
+ return pc.dim(priority);
2358
+ }
2359
+ }
2360
+ function getRiskColor(level) {
2361
+ switch (level) {
2362
+ case "HIGH":
2363
+ return pc.red;
2364
+ case "MEDIUM":
2365
+ return pc.yellow;
2366
+ case "LOW":
2367
+ return pc.cyan;
2368
+ default:
2369
+ return pc.green;
2370
+ }
2371
+ }
2372
+ function wrapText(text, width) {
2373
+ const lines = text.split("\n");
2374
+ return lines.map((line) => {
2375
+ if (line.length <= width) return line;
2376
+ const words = line.split(" ");
2377
+ const wrapped = [];
2378
+ let currentLine = "";
2379
+ for (const word of words) {
2380
+ if ((currentLine + " " + word).trim().length <= width) {
2381
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
2382
+ } else {
2383
+ if (currentLine) wrapped.push(currentLine);
2384
+ currentLine = word;
2385
+ }
2386
+ }
2387
+ if (currentLine) wrapped.push(currentLine);
2388
+ return wrapped.join("\n");
2389
+ }).join("\n");
2390
+ }
2391
+ function getFileName2(filePath) {
2392
+ return filePath.split("/").pop() || filePath;
2393
+ }
2394
+
2395
+ // src/output/summaries/structure-violations.ts
2396
+ import pc2 from "picocolors";
2397
+ function printGodFileSummary(violations) {
2398
+ const sortedBySize = violations.map((v) => ({
2399
+ file: v.file,
2400
+ lines: parseInt(v.message.match(/(\d+) lines/)?.[1] || "0")
2401
+ })).sort((a, b) => b.lines - a.lines);
2402
+ console.log(pc2.dim(" Impact: ") + "Reduces code maintainability and increases cognitive load");
2403
+ console.log();
2404
+ console.log(pc2.dim(" Files:"));
2405
+ sortedBySize.forEach((item, idx) => {
2406
+ const fileName = item.file.split("/").pop() || item.file;
2407
+ console.log(` ${pc2.yellow(idx + 1 + ".")} ${fileName} ${pc2.dim(`\u2014 ${item.lines} lines`)}`);
2408
+ });
2409
+ console.log();
2410
+ console.log(pc2.bold(" \u{1F4A1} Suggested Fix:"));
2411
+ console.log(" Split large files into focused modules (classes, utilities, types).");
2412
+ console.log(" Target: Keep files under 500 lines for better maintainability.");
2413
+ }
2414
+ function printCircularDepSummary(violations) {
2415
+ console.log(pc2.dim(" Impact: ") + pc2.red("HIGH") + " \u2014 Prevents proper modularization, causes initialization issues");
2416
+ console.log();
2417
+ console.log(pc2.dim(" Detected cycles:"));
2418
+ violations.forEach((v, idx) => {
2419
+ const cycle = `${v.file} \u2194 ${v.relatedFile}`;
2420
+ console.log(` ${pc2.red(idx + 1 + ".")} ${pc2.dim(cycle)}`);
2421
+ });
2422
+ console.log();
2423
+ console.log(pc2.bold(" \u{1F4A1} Suggested Fix:"));
2424
+ console.log(" \u2022 Extract shared code into a new module");
2425
+ console.log(" \u2022 Use dependency injection");
2426
+ console.log(" \u2022 Introduce abstraction/interface layer");
2427
+ }
2428
+ function printLayerViolationSummary(violations) {
2429
+ console.log(pc2.dim(" Impact: ") + "Breaks architectural boundaries and separation of concerns");
2430
+ console.log();
2431
+ console.log(pc2.dim(" Architectural violations:"));
2432
+ violations.forEach((v, idx) => {
2433
+ const layerMatch = v.message.match(/(\w+) layer importing from (\w+) layer/);
2434
+ if (layerMatch) {
2435
+ const [, fromLayer, toLayer] = layerMatch;
2436
+ console.log(` ${pc2.yellow(idx + 1 + ".")} ${fromLayer} \u2192 ${toLayer} ${pc2.dim(`\u2014 ${v.file}`)}`);
2437
+ } else {
2438
+ console.log(` ${pc2.yellow(idx + 1 + ".")} ${v.file} \u2192 ${v.relatedFile || "unknown"}`);
2439
+ }
2440
+ });
2441
+ console.log();
2442
+ console.log(pc2.bold(" \u{1F4A1} Suggested Fix:"));
2443
+ console.log(" Respect layer hierarchy and use dependency inversion.");
2444
+ console.log(" Move shared logic to allowed layers or use interfaces.");
2445
+ }
2446
+ function printForbiddenImportSummary(violations) {
2447
+ console.log(pc2.dim(" Impact: ") + "Introduces unwanted dependencies and coupling");
2448
+ console.log();
2449
+ console.log(pc2.dim(" Forbidden imports detected:"));
2450
+ violations.forEach((v, idx) => {
2451
+ const importMatch = v.message.match(/Importing "([^"]+)"/);
2452
+ const importName = importMatch ? importMatch[1] : "unknown";
2453
+ console.log(` ${pc2.yellow(idx + 1 + ".")} ${importName} ${pc2.dim(`\u2014 in ${v.file}`)}`);
2454
+ });
2455
+ console.log();
2456
+ console.log(pc2.bold(" \u{1F4A1} Suggested Fix:"));
2457
+ console.log(" Remove forbidden imports or restructure code.");
2458
+ console.log(" Use dependency injection or refactor to avoid coupling.");
2459
+ }
2460
+
2461
+ // src/output/summaries/quality-violations.ts
2462
+ import pc4 from "picocolors";
2463
+
2464
+ // src/output/summaries/summary-helpers.ts
2465
+ import pc3 from "picocolors";
2466
+ function formatFileLocation(violation) {
2467
+ return violation.line ? `${violation.file}:${violation.line}` : violation.file;
2468
+ }
2469
+ function printImpact(impact) {
2470
+ console.log(pc3.dim(" Impact: ") + impact);
2471
+ console.log();
2472
+ }
2473
+ function printSuggestedFix(suggestions) {
2474
+ console.log();
2475
+ console.log(pc3.bold(" \u{1F4A1} Suggested Fix:"));
2476
+ suggestions.forEach((suggestion) => {
2477
+ console.log(` ${suggestion}`);
2478
+ });
2479
+ }
2480
+ function printNumberedList(title, items, color = "yellow") {
2481
+ console.log(pc3.dim(` ${title}:`));
2482
+ items.forEach((item, idx) => {
2483
+ const number = pc3[color](idx + 1 + ".");
2484
+ const line = item.secondary ? ` ${number} ${item.primary} ${pc3.dim(`\u2014 ${item.secondary}`)}` : ` ${number} ${item.primary}`;
2485
+ console.log(line);
2486
+ });
2487
+ }
2488
+ function printSummaryStats(label, value, details) {
2489
+ console.log(` ${pc3.yellow(label + ":")} ${value}${details ? ` ${pc3.dim(details)}` : ""}`);
2490
+ }
2491
+ function extractTotalFromMessages(violations, pattern) {
2492
+ return violations.reduce((sum, v) => {
2493
+ const match = v.message.match(pattern);
2494
+ return sum + (match ? parseInt(match[1]) : 0);
2495
+ }, 0);
2496
+ }
2497
+
2498
+ // src/output/summaries/quality-violations.ts
2499
+ function printMissingTestsSummary(violations) {
2500
+ printImpact("Reduces confidence in code changes and increases regression risk");
2501
+ const items = violations.map((v) => ({ primary: getFileName(v.file) }));
2502
+ printNumberedList("Untested files", items);
2503
+ printSuggestedFix([
2504
+ "Create corresponding .spec.ts files with unit tests.",
2505
+ "Target: at least 80% code coverage for critical logic."
2506
+ ]);
2507
+ }
2508
+ function printSkippedTestsSummary(violations) {
2509
+ const byFile = violations.reduce((acc, v) => {
2510
+ const fileName = v.file.split("/").pop() || v.file;
2511
+ if (!acc[fileName]) acc[fileName] = [];
2512
+ acc[fileName].push(v);
2513
+ return acc;
2514
+ }, {});
2515
+ printImpact("Reduces test coverage reliability, may hide broken functionality");
2516
+ const items = Object.keys(byFile).map((fileName) => {
2517
+ const count = byFile[fileName].length;
2518
+ return {
2519
+ primary: fileName,
2520
+ secondary: `${count} skipped test${count === 1 ? "" : "s"}`
2521
+ };
2522
+ });
2523
+ printNumberedList("Skipped tests by file", items);
2524
+ printSuggestedFix([
2525
+ "Unskip and fix failing tests. Remove obsolete tests.",
2526
+ "Add issue tracker references for deferred work."
2527
+ ]);
2528
+ }
2529
+ function printMissingTypeAnnotationsSummary(violations) {
2530
+ const totalMissing = extractTotalFromMessages(violations, /^(\d+) missing/);
2531
+ printImpact("Reduces type safety, IntelliSense quality, and code documentation");
2532
+ console.log(pc4.dim(" Missing type annotations:"));
2533
+ printSummaryStats("Total", `${totalMissing} annotations across ${violations.length} ${violations.length === 1 ? "file" : "files"}`);
2534
+ console.log();
2535
+ const sortedByCount = violations.map((v) => ({
2536
+ file: v.file,
2537
+ count: parseInt(v.message.match(/^(\d+) missing/)?.[1] || "0")
2538
+ })).sort((a, b) => b.count - a.count);
2539
+ const items = sortedByCount.map((item) => ({
2540
+ primary: getFileName(item.file),
2541
+ secondary: `${item.count} missing`
2542
+ }));
2543
+ printNumberedList("Files with most missing annotations", items, "cyan");
2544
+ printSuggestedFix([
2545
+ "Add explicit type annotations to parameters and return types.",
2546
+ "Use TypeScript strict mode. Enable noImplicitAny in tsconfig."
2547
+ ]);
2548
+ }
2549
+ function printUnusedExportsSummary(violations) {
2550
+ console.log(pc4.dim(" Impact: ") + "Dead code that increases maintenance burden and may confuse developers");
2551
+ console.log();
2552
+ console.log(pc4.dim(" Unused exports:"));
2553
+ violations.forEach((v, idx) => {
2554
+ const fileName = getFileName(v.file);
2555
+ const exportName = v.message.match(/Export '([^']+)'/)?.[1] || "unknown";
2556
+ console.log(` ${pc4.cyan(idx + 1 + ".")} ${exportName} ${pc4.dim(`in ${fileName}`)}`);
2557
+ });
2558
+ console.log();
2559
+ console.log(pc4.bold(" \u{1F4A1} Suggested Fix:"));
2560
+ console.log(" Remove unused exports or document if part of public API.");
2561
+ console.log(" Add to exclusion patterns if used externally.");
2562
+ }
2563
+ function printDeadCodeSummary(violations) {
2564
+ const unreachable = violations.filter((v) => v.message.includes("Unreachable"));
2565
+ const unused = violations.filter((v) => v.message.includes("never used"));
2566
+ console.log(pc4.dim(" Impact: ") + "Increases codebase size and confuses developers");
2567
+ console.log();
2568
+ if (unreachable.length > 0) {
2569
+ console.log(pc4.dim(" Unreachable code:"));
2570
+ unreachable.forEach((v, idx) => {
2571
+ const fileName = getFileName(v.file);
2572
+ const match = v.message.match(/in '([^']+)'/);
2573
+ const functionName = match ? match[1] : "unknown";
2574
+ console.log(` ${pc4.yellow(idx + 1 + ".")} ${functionName} ${pc4.dim(`in ${fileName}`)}`);
2575
+ });
2576
+ console.log();
2577
+ }
2578
+ if (unused.length > 0) {
2579
+ console.log(pc4.dim(" Unused variables:"));
2580
+ unused.forEach((v, idx) => {
2581
+ const fileName = getFileName(v.file);
2582
+ const match = v.message.match(/Variable '([^']+)'/);
2583
+ const varName = match ? match[1] : "unknown";
2584
+ console.log(` ${pc4.cyan(idx + 1 + ".")} ${varName} ${pc4.dim(`in ${fileName}`)}`);
2585
+ });
2586
+ console.log();
2587
+ }
2588
+ console.log(pc4.bold(" \u{1F4A1} Suggested Fix:"));
2589
+ console.log(" Remove dead code. Use IDE refactoring tools to safely delete.");
2590
+ console.log(" Enable strict TypeScript settings to catch unused code.");
2591
+ }
2592
+
2593
+ // src/output/summaries/complexity-violations.ts
2594
+ import pc5 from "picocolors";
2595
+ function printCyclomaticComplexitySummary(violations) {
2596
+ const sortedByComplexity = violations.map((v) => ({
2597
+ file: v.file,
2598
+ functionName: v.message.match(/(?:Function|Method|Arrow function) '([^']+)'/)?.[1] || "unknown",
2599
+ complexity: parseInt(v.message.match(/complexity of (\d+)/)?.[1] || "0")
2600
+ })).sort((a, b) => b.complexity - a.complexity);
2601
+ console.log(pc5.dim(" Impact: ") + "High complexity increases bug probability and makes code harder to test");
2602
+ console.log();
2603
+ console.log(pc5.dim(" Most complex functions:"));
2604
+ sortedByComplexity.forEach((item, idx) => {
2605
+ const fileName = item.file.split("/").pop() || item.file;
2606
+ console.log(` ${pc5.red(idx + 1 + ".")} ${item.functionName} ${pc5.dim(`in ${fileName} \u2014 complexity ${item.complexity}`)}`);
2607
+ });
2608
+ console.log();
2609
+ console.log(pc5.bold(" \u{1F4A1} Suggested Fix:"));
2610
+ console.log(" Break down into smaller functions. Extract conditionals.");
2611
+ console.log(" Use early returns, strategy pattern, or lookup tables.");
2612
+ }
2613
+ function printDeepNestingSummary(violations) {
2614
+ const sortedByDepth = violations.map((v) => ({
2615
+ file: v.file,
2616
+ functionName: v.message.match(/(?:Function|Method|Arrow function) '([^']+)'/)?.[1] || "unknown",
2617
+ depth: parseInt(v.message.match(/depth of (\d+) levels/)?.[1] || "0")
2618
+ })).sort((a, b) => b.depth - a.depth);
2619
+ console.log(pc5.dim(" Impact: ") + "Increases cyclomatic complexity, reduces readability and testability");
2620
+ console.log();
2621
+ console.log(pc5.dim(" Most deeply nested functions:"));
2622
+ sortedByDepth.forEach((item, idx) => {
2623
+ const fileName = item.file.split("/").pop() || item.file;
2624
+ console.log(` ${pc5.yellow(idx + 1 + ".")} ${item.functionName} ${pc5.dim(`in ${fileName} \u2014 ${item.depth} levels deep`)}`);
2625
+ });
2626
+ console.log();
2627
+ console.log(pc5.bold(" \u{1F4A1} Suggested Fix:"));
2628
+ console.log(" Use early returns/guard clauses to reduce nesting.");
2629
+ console.log(" Extract nested blocks into separate helper methods.");
2630
+ }
2631
+ function printLargeFunctionSummary(violations) {
2632
+ const sortedBySize = violations.map((v) => ({
2633
+ file: v.file,
2634
+ functionName: v.message.match(/(?:Function|Method|Arrow function) '([^']+)'/)?.[1] || "unknown",
2635
+ lines: parseInt(v.message.match(/(\d+) lines/)?.[1] || "0")
2636
+ })).sort((a, b) => b.lines - a.lines);
2637
+ console.log(pc5.dim(" Impact: ") + "Reduces testability, code comprehension, and maintainability");
2638
+ console.log();
2639
+ console.log(pc5.dim(" Largest functions:"));
2640
+ sortedBySize.forEach((item, idx) => {
2641
+ const fileName = item.file.split("/").pop() || item.file;
2642
+ console.log(` ${pc5.yellow(idx + 1 + ".")} ${item.functionName} ${pc5.dim(`in ${fileName} \u2014 ${item.lines} lines`)}`);
2643
+ });
2644
+ console.log();
2645
+ console.log(pc5.bold(" \u{1F4A1} Suggested Fix:"));
2646
+ console.log(" Split large functions into smaller helper functions.");
2647
+ console.log(" Extract complex logic into well-named private methods.");
2648
+ }
2649
+ function printLongParameterListSummary(violations) {
2650
+ const sortedByCount = violations.map((v) => ({
2651
+ file: v.file,
2652
+ functionName: v.message.match(/(?:Function|Method|Arrow function) '([^']+)'/)?.[1] || "unknown",
2653
+ count: parseInt(v.message.match(/has (\d+) parameters/)?.[1] || "0")
2654
+ })).sort((a, b) => b.count - a.count);
2655
+ console.log(pc5.dim(" Impact: ") + "Reduces readability, increases testing complexity");
2656
+ console.log();
2657
+ console.log(pc5.dim(" Functions with most parameters:"));
2658
+ sortedByCount.forEach((item, idx) => {
2659
+ const fileName = item.file.split("/").pop() || item.file;
2660
+ console.log(` ${pc5.yellow(idx + 1 + ".")} ${item.functionName} ${pc5.dim(`in ${fileName} \u2014 ${item.count} params`)}`);
2661
+ });
2662
+ console.log();
2663
+ console.log(pc5.bold(" \u{1F4A1} Suggested Fix:"));
2664
+ console.log(" Use parameter objects to group related parameters.");
2665
+ console.log(" Apply builder pattern for complex construction.");
2666
+ }
2667
+ function printTooManyImportsSummary(violations) {
2668
+ const sortedByCount = violations.map((v) => ({
2669
+ file: v.file,
2670
+ count: parseInt(v.message.match(/(\d+) imports/)?.[1] || "0")
2671
+ })).sort((a, b) => b.count - a.count);
2672
+ console.log(pc5.dim(" Impact: ") + "Increases coupling, reduces modularity, violates Single Responsibility");
2673
+ console.log();
2674
+ console.log(pc5.dim(" Files with excessive imports:"));
2675
+ sortedByCount.forEach((item, idx) => {
2676
+ const fileName = item.file.split("/").pop() || item.file;
2677
+ console.log(` ${pc5.yellow(idx + 1 + ".")} ${fileName} ${pc5.dim(`\u2014 ${item.count} imports`)}`);
2678
+ });
2679
+ console.log();
2680
+ console.log(pc5.bold(" \u{1F4A1} Suggested Fix:"));
2681
+ console.log(" Refactor into smaller, focused modules.");
2682
+ console.log(" Remove unused imports. Use facade pattern to reduce dependencies.");
2683
+ }
2684
+
2685
+ // src/output/summaries/code-smell-violations.ts
2686
+ import pc6 from "picocolors";
2687
+ function printFeatureEnvySummary(violations) {
2688
+ console.log(pc6.dim(" Impact: ") + "Violates encapsulation and creates tight coupling");
2689
+ console.log();
2690
+ console.log(pc6.dim(" Methods envying other classes:"));
2691
+ violations.forEach((v, idx) => {
2692
+ const fileName = v.file.split("/").pop() || v.file;
2693
+ const methodMatch = v.message.match(/Method '([^']+)'/);
2694
+ const objectMatch = v.message.match(/uses '([^']+)'/);
2695
+ const method = methodMatch ? methodMatch[1] : "unknown";
2696
+ const enviedObject = objectMatch ? objectMatch[1] : "unknown";
2697
+ console.log(` ${pc6.yellow(idx + 1 + ".")} ${method} ${pc6.dim(`envies ${enviedObject} \u2014 in ${fileName}`)}`);
2698
+ });
2699
+ console.log();
2700
+ console.log(pc6.bold(" \u{1F4A1} Suggested Fix:"));
2701
+ console.log(" Move method to the envied class or extract shared logic.");
2702
+ console.log(" Use Tell, Don't Ask principle to improve encapsulation.");
2703
+ }
2704
+ function printDataClumpsSummary(violations) {
2705
+ console.log(pc6.dim(" Impact: ") + "Indicates missing abstraction, makes refactoring error-prone");
2706
+ console.log();
2707
+ console.log(pc6.dim(" Parameter groups appearing together:"));
2708
+ violations.forEach((v, idx) => {
2709
+ const paramsMatch = v.message.match(/\[([^\]]+)\]/);
2710
+ const countMatch = v.message.match(/appears (\d+) times/);
2711
+ const params = paramsMatch ? paramsMatch[1] : "unknown";
2712
+ const count = countMatch ? countMatch[1] : "?";
2713
+ console.log(` ${pc6.yellow(idx + 1 + ".")} [${params}] ${pc6.dim(`\u2014 ${count} occurrences`)}`);
2714
+ });
2715
+ console.log();
2716
+ console.log(pc6.bold(" \u{1F4A1} Suggested Fix:"));
2717
+ console.log(" Extract parameter groups into interfaces or classes.");
2718
+ console.log(" Create cohesive types that represent domain concepts.");
2719
+ }
2720
+ function printShotgunSurgerySummary(violations) {
2721
+ const sortedByFileCount = violations.map((v) => ({
2722
+ file: v.file,
2723
+ symbol: v.message.match(/Symbol '([^']+)'/)?.[1] || "unknown",
2724
+ fileCount: parseInt(v.message.match(/used in (\d+) files/)?.[1] || "0")
2725
+ })).sort((a, b) => b.fileCount - a.fileCount);
2726
+ console.log(pc6.dim(" Impact: ") + "Changes require modifying many files, increasing risk");
2727
+ console.log();
2728
+ console.log(pc6.dim(" Symbols with widest usage:"));
2729
+ sortedByFileCount.forEach((item, idx) => {
2730
+ const fileName = item.file.split("/").pop() || item.file;
2731
+ console.log(` ${pc6.red(idx + 1 + ".")} ${item.symbol} ${pc6.dim(`from ${fileName} \u2014 ${item.fileCount} files`)}`);
2732
+ });
2733
+ console.log();
2734
+ console.log(pc6.bold(" \u{1F4A1} Suggested Fix:"));
2735
+ console.log(" Introduce facade or wrapper to reduce direct coupling.");
2736
+ console.log(" Use dependency injection to centralize usage.");
2737
+ }
2738
+ function printDuplicateCodeSummary(violations) {
2739
+ const groupedByFiles = /* @__PURE__ */ new Map();
2740
+ violations.forEach((v) => {
2741
+ const files = v.message.match(/files: (.+)$/)?.[1] || "unknown";
2742
+ if (!groupedByFiles.has(files)) {
2743
+ groupedByFiles.set(files, []);
2744
+ }
2745
+ groupedByFiles.get(files).push(v);
2746
+ });
2747
+ console.log(pc6.dim(" Impact: ") + "Increases maintenance burden and bug probability");
2748
+ console.log();
2749
+ console.log(pc6.dim(` ${groupedByFiles.size} unique duplicate patterns found:`));
2750
+ let idx = 1;
2751
+ for (const [files, dupes] of groupedByFiles.entries()) {
2752
+ const fileCount = dupes[0].message.match(/duplicated in (\d+) files/)?.[1] || "?";
2753
+ const blockCount = dupes.length;
2754
+ console.log(` ${pc6.yellow(idx + ".")} ${blockCount} block${blockCount > 1 ? "s" : ""} duplicated in ${fileCount} files ${pc6.dim(`\u2014 ${files}`)}`);
2755
+ idx++;
2756
+ }
2757
+ console.log();
2758
+ console.log(pc6.bold(" \u{1F4A1} Suggested Fix:"));
2759
+ console.log(" Extract into shared utility functions or classes.");
2760
+ console.log(" Use inheritance, composition, or Template Method pattern.");
2761
+ }
2762
+
2763
+ // src/output/summaries/style-violations.ts
2764
+ import pc7 from "picocolors";
2765
+ function printMagicNumbersSummary(violations) {
2766
+ const sortedByOccurrences = violations.map((v) => ({
2767
+ file: v.file,
2768
+ number: v.message.match(/Number '([^']+)'/)?.[1] || "unknown",
2769
+ count: parseInt(v.message.match(/appears (\d+) times/)?.[1] || "0")
2770
+ })).sort((a, b) => b.count - a.count);
2771
+ printImpact("Reduces code clarity and maintainability, makes changes error-prone");
2772
+ const items = sortedByOccurrences.map((item) => ({
2773
+ primary: `${item.number} ${pc7.dim(`in ${getFileName(item.file)}`)}`,
2774
+ secondary: `${item.count} occurrences`
2775
+ }));
2776
+ printNumberedList("Most repeated magic numbers", items);
2777
+ printSuggestedFix([
2778
+ "Extract into named constants with descriptive names.",
2779
+ "Use enums for related constants. Move to config for thresholds."
2780
+ ]);
2781
+ }
2782
+ function printWildcardImportsSummary(violations) {
2783
+ printImpact("Increases bundle size and reduces tree-shaking effectiveness");
2784
+ const items = violations.map((v) => {
2785
+ const fileName = getFileName(v.file);
2786
+ const match = v.message.match(/import \* as (\w+) from '([^']+)'/);
2787
+ const alias = match?.[1] || "unknown";
2788
+ const module = match?.[2] || "unknown";
2789
+ return {
2790
+ primary: fileName,
2791
+ secondary: `import * as ${alias} from '${module}'`
2792
+ };
2793
+ });
2794
+ printNumberedList("Wildcard imports detected", items, "cyan");
2795
+ printSuggestedFix([
2796
+ "Replace with named imports for only what you need.",
2797
+ "Enables better tree-shaking and makes dependencies explicit."
2798
+ ]);
2799
+ }
2800
+ function printTodoCommentsSummary(violations) {
2801
+ const totalMarkers = extractTotalFromMessages(violations, /^(\d+) technical/);
2802
+ const MARKER_TYPES = ["TODO", "FIXME", "HACK", "XXX"];
2803
+ const byType = violations.reduce((acc, v) => {
2804
+ const message = v.message;
2805
+ for (const type of MARKER_TYPES) {
2806
+ const pattern = new RegExp(`(\\d+) ${type}s?`);
2807
+ const match = message.match(pattern);
2808
+ if (match) {
2809
+ acc[type] = (acc[type] || 0) + parseInt(match[1]);
2810
+ }
2811
+ }
2812
+ return acc;
2813
+ }, {});
2814
+ printImpact("Indicates incomplete work, known issues, or deferred improvements");
2815
+ console.log(pc7.dim(" Technical debt markers:"));
2816
+ printSummaryStats("Total", `${totalMarkers} markers across ${violations.length} ${violations.length === 1 ? "file" : "files"}`);
2817
+ const breakdown = Object.entries(byType).map(([type, count]) => `${count} ${type}${count > 1 ? "s" : ""}`).join(", ");
2818
+ if (breakdown) {
2819
+ console.log(` ${pc7.dim("Breakdown:")} ${breakdown}`);
2820
+ }
2821
+ console.log();
2822
+ console.log(pc7.dim(" Files with most markers:"));
2823
+ const sortedByCount = violations.map((v) => ({
2824
+ file: v.file,
2825
+ count: parseInt(v.message.match(/^(\d+) technical/)?.[1] || "0")
2826
+ })).sort((a, b) => b.count - a.count);
2827
+ sortedByCount.forEach((item, idx) => {
2828
+ const fileName = getFileName(item.file);
2829
+ console.log(` ${pc7.yellow(idx + 1 + ".")} ${fileName} ${pc7.dim(`\u2014 ${item.count} markers`)}`);
2830
+ });
2831
+ console.log();
2832
+ console.log(pc7.bold(" \u{1F4A1} Suggested Fix:"));
2833
+ console.log(" Review markers: complete TODOs, fix FIXMEs, refactor HACKs.");
2834
+ console.log(" Create tracked issues for deferred work. Remove obsolete markers.");
2835
+ }
2836
+ function printGenericViolationSummary(violations) {
2837
+ console.log(pc7.dim(" Impact: ") + violations[0].impact);
2838
+ console.log();
2839
+ console.log(pc7.dim(" Violations:"));
2840
+ violations.forEach((v, idx) => {
2841
+ const fileLocation = formatFileLocation(v);
2842
+ const location = v.relatedFile ? `${fileLocation} \u2192 ${v.relatedFile}` : fileLocation;
2843
+ console.log(` ${idx + 1 + "."} ${pc7.dim(location)}`);
2844
+ });
2845
+ console.log();
2846
+ console.log(pc7.bold(" \u{1F4A1} Suggested Fix:"));
2847
+ console.log(" " + violations[0].suggestedFix.split("\n")[0]);
2848
+ }
2849
+
2850
+ // src/output/action-generators.ts
2851
+ function generateCircularDependencyActions(violations) {
2852
+ return violations.map((v) => {
2853
+ const files = v.relatedFile ? `${getFileName2(v.file)} \u2194 ${getFileName2(v.relatedFile)}` : getFileName2(v.file);
2854
+ return {
2855
+ description: `Resolve circular dependency: ${files}`,
2856
+ priority: "HIGH",
2857
+ effort: "2-4h",
2858
+ impact: "Improves testability and reduces coupling"
2859
+ };
2860
+ });
2861
+ }
2862
+ function generateLayerViolationActions(violations) {
2863
+ return violations.map((v) => {
2864
+ const files = v.relatedFile ? `${getFileName2(v.file)} \u2192 ${getFileName2(v.relatedFile)}` : getFileName2(v.file);
2865
+ return {
2866
+ description: `Fix layer violation: ${files}`,
2867
+ priority: "HIGH",
2868
+ effort: "1-2h",
2869
+ impact: "Maintains architectural boundaries"
2870
+ };
2871
+ });
2872
+ }
2873
+ function generateGodFileActions(violations) {
2874
+ const godFiles = violations.map((v) => ({
2875
+ violation: v,
2876
+ file: v.file,
2877
+ fileName: getFileName2(v.file),
2878
+ lines: parseInt(v.message.match(/(\d+) lines/)?.[1] || "0")
2879
+ })).sort((a, b) => b.lines - a.lines);
2880
+ return godFiles.map((item) => {
2881
+ let priority;
2882
+ let effort;
2883
+ let impact;
2884
+ if (item.lines > 1e3) {
2885
+ priority = "HIGH";
2886
+ effort = "4-8h";
2887
+ impact = "Significantly improves maintainability";
2888
+ } else if (item.lines > 750) {
2889
+ priority = "MEDIUM";
2890
+ effort = "2-4h";
2891
+ impact = "Improves modularity and code organization";
2892
+ } else {
2893
+ priority = "LOW";
2894
+ effort = "1-2h";
2895
+ impact = "Minor maintainability improvement";
2896
+ }
2897
+ return {
2898
+ description: `Refactor oversized file: ${item.fileName} (${item.lines} lines)`,
2899
+ priority,
2900
+ effort,
2901
+ impact,
2902
+ file: item.file,
2903
+ line: 1
2904
+ };
2905
+ });
2906
+ }
2907
+ function generateTooManyImportsActions(violations) {
2908
+ const topViolations = violations.map((v) => ({
2909
+ violation: v,
2910
+ file: v.file,
2911
+ fileName: getFileName2(v.file),
2912
+ count: parseInt(v.message.match(/(\d+) imports/)?.[1] || "0")
2913
+ })).sort((a, b) => b.count - a.count).slice(0, 3);
2914
+ return topViolations.map((item) => ({
2915
+ description: `Refactor high coupling: ${item.fileName} (${item.count} imports)`,
2916
+ priority: "HIGH",
2917
+ effort: "4-8h",
2918
+ impact: "Reduces coupling and improves modularity",
2919
+ file: item.file,
2920
+ line: 1
2921
+ }));
2922
+ }
2923
+ function generateLargeFunctionActions(violations) {
2924
+ const topFunctions = violations.map((v) => ({
2925
+ violation: v,
2926
+ file: v.file,
2927
+ name: v.message.match(/(?:Function|Method|Arrow function) '(.+?)'/)?.[1] || v.message.match(/'(.+?)'/)?.[1] || getFileName2(v.file),
2928
+ lines: parseInt(v.message.match(/(\d+) lines/)?.[1] || "0")
2929
+ })).sort((a, b) => b.lines - a.lines).slice(0, 3);
2930
+ return topFunctions.map((item) => {
2931
+ let priority;
2932
+ let effort;
2933
+ let impact;
2934
+ if (item.lines > 200) {
2935
+ priority = "HIGH";
2936
+ effort = "4-6h";
2937
+ impact = "Significantly improves testability and comprehension";
2938
+ } else if (item.lines > 100) {
2939
+ priority = "MEDIUM";
2940
+ effort = "2-4h";
2941
+ impact = "Improves maintainability and reduces complexity";
2942
+ } else {
2943
+ priority = "LOW";
2944
+ effort = "1-2h";
2945
+ impact = "Minor maintainability improvement";
2946
+ }
2947
+ return {
2948
+ description: `Refactor large function: ${item.name} in ${getFileName2(item.file)} (${item.lines} lines)`,
2949
+ priority,
2950
+ effort,
2951
+ impact,
2952
+ file: item.file,
2953
+ line: item.violation.line
2954
+ };
2955
+ });
2956
+ }
2957
+ function generateDeepNestingActions(violations) {
2958
+ const topViolations = violations.map((v) => ({
2959
+ violation: v,
2960
+ file: v.file,
2961
+ name: v.message.match(/(?:Function|Method|Arrow function) '(.+?)'/)?.[1] || v.message.match(/'(.+?)'/)?.[1] || getFileName2(v.file),
2962
+ depth: parseInt(v.message.match(/(\d+) levels/)?.[1] || "0")
2963
+ })).sort((a, b) => b.depth - a.depth).slice(0, 5);
2964
+ return topViolations.map((item) => {
2965
+ let priority;
2966
+ let effort;
2967
+ if (item.depth > 10) {
2968
+ priority = "HIGH";
2969
+ effort = "3-5h";
2970
+ } else if (item.depth > 6) {
2971
+ priority = "MEDIUM";
2972
+ effort = "2-3h";
2973
+ } else {
2974
+ priority = "LOW";
2975
+ effort = "1-2h";
2976
+ }
2977
+ return {
2978
+ description: `Reduce nesting in ${item.name} (${getFileName2(item.file)}) \u2014 ${item.depth} levels`,
2979
+ priority,
2980
+ effort,
2981
+ impact: "Reduces complexity and improves readability",
2982
+ file: item.file,
2983
+ line: item.violation.line
2984
+ };
2985
+ });
2986
+ }
2987
+ function createTestCoverageAction(violations) {
2988
+ const count = violations.length;
2989
+ return {
2990
+ description: `Add test coverage for ${count} untested files`,
2991
+ priority: "MEDIUM",
2992
+ effort: `${count}-${count * 3}h`,
2993
+ impact: "Reduces regression risk and increases code confidence"
2994
+ };
2995
+ }
2996
+ function createSkippedTestAction(violations) {
2997
+ const count = violations.length;
2998
+ return {
2999
+ description: `Unskip and fix ${count} skipped tests`,
3000
+ priority: "MEDIUM",
3001
+ effort: `${count}-${count * 2}h`,
3002
+ impact: "Improves test coverage and reliability"
3003
+ };
3004
+ }
3005
+ function createMagicNumberAction(violations) {
3006
+ const uniqueNumbers = new Set(violations.map((v) => v.message.match(/Number (\S+)/)?.[1])).size;
3007
+ return {
3008
+ description: `Extract ${uniqueNumbers} magic numbers into named constants`,
3009
+ priority: "LOW",
3010
+ effort: "4-8h",
3011
+ impact: "Improves code clarity and maintainability"
3012
+ };
3013
+ }
3014
+ function createTypeAnnotationAction(violations) {
3015
+ const count = violations.length;
3016
+ const fileCount = new Set(violations.map((v) => v.file)).size;
3017
+ return {
3018
+ description: `Add ${count} missing type annotations across ${fileCount} files`,
3019
+ priority: "MEDIUM",
3020
+ effort: "3-5h",
3021
+ impact: "Improves type safety and IDE support"
3022
+ };
3023
+ }
3024
+ function generateBulkActions(violations, type) {
3025
+ const count = violations.length;
3026
+ if (count === 0) return [];
3027
+ const actionFactories = {
3028
+ "Missing Test File": createTestCoverageAction,
3029
+ "Skipped Test": createSkippedTestAction,
3030
+ "Magic Number": createMagicNumberAction,
3031
+ "Missing Type Annotation": createTypeAnnotationAction
3032
+ };
3033
+ const simpleActions = {
3034
+ "Technical Debt Marker": {
3035
+ description: `Address ${count} technical debt marker(s) (TODO/FIXME/HACK)`,
3036
+ priority: "LOW",
3037
+ effort: "1-2h",
3038
+ impact: "Resolves deferred work and reduces technical debt"
3039
+ },
3040
+ "Unused Export": {
3041
+ description: `Clean up ${count} unused exports`,
3042
+ priority: "MEDIUM",
3043
+ effort: `${Math.ceil(count / 2)}-${count}h`,
3044
+ impact: "Reduces dead code and clarifies public API"
3045
+ },
3046
+ "Wildcard Import": {
3047
+ description: `Replace ${count} wildcard imports with explicit imports`,
3048
+ priority: "LOW",
3049
+ effort: "1-2h",
3050
+ impact: "Improves tree-shaking and reduces bundle size"
3051
+ }
3052
+ };
3053
+ const actionConfig = actionFactories[type]?.(violations) ?? simpleActions[type];
3054
+ if (!actionConfig) return [];
3055
+ return [{
3056
+ description: actionConfig.description,
3057
+ priority: actionConfig.priority,
3058
+ effort: actionConfig.effort,
3059
+ impact: actionConfig.impact
3060
+ }];
3061
+ }
3062
+
3063
+ // src/output/action-generator.ts
3064
+ function generateNextActions(result) {
3065
+ const actions = [];
3066
+ const grouped = groupViolationsByType(result.violations);
3067
+ if (grouped["Circular Dependency"]) {
3068
+ actions.push(...generateCircularDependencyActions(grouped["Circular Dependency"]));
3069
+ }
3070
+ if (grouped["Layer Violation"]) {
3071
+ actions.push(...generateLayerViolationActions(grouped["Layer Violation"]));
3072
+ }
3073
+ if (grouped["Too Many Imports"]) {
3074
+ actions.push(...generateTooManyImportsActions(grouped["Too Many Imports"]));
3075
+ }
3076
+ if (grouped["Large Function"]) {
3077
+ actions.push(...generateLargeFunctionActions(grouped["Large Function"]));
3078
+ }
3079
+ if (grouped["Deep Nesting"]) {
3080
+ actions.push(...generateDeepNestingActions(grouped["Deep Nesting"]));
3081
+ }
3082
+ if (grouped["God File"]) {
3083
+ actions.push(...generateGodFileActions(grouped["God File"]));
3084
+ }
3085
+ const bulkActionTypes = [
3086
+ "Missing Test File",
3087
+ "Skipped Test",
3088
+ "Technical Debt Marker",
3089
+ "Unused Export",
3090
+ "Missing Type Annotation",
3091
+ "Magic Number",
3092
+ "Wildcard Import"
3093
+ ];
3094
+ bulkActionTypes.forEach((type) => {
3095
+ if (grouped[type]) {
3096
+ actions.push(...generateBulkActions(grouped[type], type));
3097
+ }
3098
+ });
3099
+ return sortActionsByPriority(actions);
3100
+ }
3101
+ function sortActionsByPriority(actions) {
3102
+ if (actions.length === 0) {
3103
+ return [{
3104
+ description: "Continue maintaining current architecture standards",
3105
+ priority: "LOW",
3106
+ effort: "Ongoing"
3107
+ }];
3108
+ }
3109
+ const priorityOrder = { "HIGH": 1, "MEDIUM": 2, "LOW": 3 };
3110
+ return actions.sort((a, b) => {
3111
+ const aPriority = priorityOrder[a.priority] || 99;
3112
+ const bPriority = priorityOrder[b.priority] || 99;
3113
+ return aPriority - bPriority;
3114
+ });
3115
+ }
3116
+
3117
+ // src/output/terminal-reporter.ts
3118
+ var SEPARATOR_WIDTH = 54;
3119
+ var SUMMARY_PRINTERS = {
3120
+ "God File": printGodFileSummary,
3121
+ "Circular Dependency": printCircularDepSummary,
3122
+ "Missing Test File": printMissingTestsSummary,
3123
+ "Too Many Imports": printTooManyImportsSummary,
3124
+ "Large Function": printLargeFunctionSummary,
3125
+ "Deep Nesting": printDeepNestingSummary,
3126
+ "Skipped Test": printSkippedTestsSummary,
3127
+ "Magic Number": printMagicNumbersSummary,
3128
+ "Wildcard Import": printWildcardImportsSummary,
3129
+ "Technical Debt Marker": printTodoCommentsSummary,
3130
+ "Unused Export": printUnusedExportsSummary,
3131
+ "Missing Type Annotation": printMissingTypeAnnotationsSummary,
3132
+ "High Cyclomatic Complexity": printCyclomaticComplexitySummary,
3133
+ "Duplicate Code": printDuplicateCodeSummary,
3134
+ "Layer Violation": printLayerViolationSummary,
3135
+ "Forbidden Import": printForbiddenImportSummary,
3136
+ "Dead Code": printDeadCodeSummary,
3137
+ "Long Parameter List": printLongParameterListSummary,
3138
+ "Feature Envy": printFeatureEnvySummary,
3139
+ "Data Clump": printDataClumpsSummary,
3140
+ "Shotgun Surgery": printShotgunSurgerySummary
3141
+ };
3142
+ var TerminalReporter = class {
3143
+ report(result, verbose) {
3144
+ console.log();
3145
+ this.printHeader(result.projectName);
3146
+ console.log();
3147
+ this.printExecutiveSummary(result);
3148
+ console.log();
3149
+ this.printProjectStats(result);
3150
+ console.log();
3151
+ if (result.violations.length > 0) {
3152
+ this.printGroupedRisks(result);
3153
+ console.log();
3154
+ this.printNextActions(result);
3155
+ console.log();
3156
+ } else {
3157
+ this.printNoIssuesMessage();
3158
+ console.log();
3159
+ }
3160
+ if (verbose && result.violations.length > 0) {
3161
+ this.printDetailedViolations(result.violations);
3162
+ console.log();
3163
+ }
3164
+ }
3165
+ printHeader(projectName) {
3166
+ console.log(pc8.bold(pc8.cyan("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")));
3167
+ if (projectName) {
3168
+ console.log(pc8.bold(pc8.cyan(` ARCHGUARD \u2014 Analyzing ${pc8.white(projectName)}`)));
3169
+ } else {
3170
+ console.log(pc8.bold(pc8.cyan(" ARCHGUARD \u2014 Architecture Analysis Report")));
3171
+ }
3172
+ console.log(pc8.bold(pc8.cyan("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")));
3173
+ }
3174
+ printExecutiveSummary(result) {
3175
+ console.log(pc8.bold("\u{1F4CA} EXECUTIVE SUMMARY"));
3176
+ console.log(pc8.dim("\u2500".repeat(SEPARATOR_WIDTH)));
3177
+ console.log();
3178
+ const scoreColor = getScoreColor(result.score);
3179
+ const scoreBar = getScoreBar(result.score);
3180
+ console.log(` Architecture Score: ${scoreColor(pc8.bold(result.score.toString()))} / 100 ${scoreBar}`);
3181
+ const statusIcon = getStatusIcon(result.status);
3182
+ const statusColor = getStatusColor(result.status);
3183
+ console.log(` Health Status: ${statusColor(`${statusIcon} ${result.status}`)}`);
3184
+ const riskLevel = this.getRiskLevel(result);
3185
+ const riskColor = getRiskColor(riskLevel);
3186
+ console.log(` Risk Level: ${riskColor(riskLevel)}`);
3187
+ if (result.violations.length > 0) {
3188
+ const primaryConcern = this.getPrimaryConcern(result);
3189
+ console.log(` Primary Concern: ${pc8.yellow(primaryConcern)}`);
3190
+ } else {
3191
+ console.log(` Primary Concern: ${pc8.green("None \u2014 Excellent architecture!")}`);
3192
+ }
3193
+ if (result.scoreBreakdown) {
3194
+ console.log();
3195
+ this.printCategoryBreakdown(result);
3196
+ }
3197
+ }
3198
+ printCategoryBreakdown(result) {
3199
+ if (!result.scoreBreakdown) return;
3200
+ const { structural, design, complexity, hygiene } = result.scoreBreakdown;
3201
+ console.log(pc8.dim(" Category Breakdown:"));
3202
+ console.log();
3203
+ const structuralIcon = this.getCategoryIcon(structural.impact);
3204
+ console.log(` ${structuralIcon} Structural: ${this.formatCategoryLine(structural)}`);
3205
+ const designIcon = this.getCategoryIcon(design.impact);
3206
+ console.log(` ${designIcon} Design: ${this.formatCategoryLine(design)}`);
3207
+ const complexityIcon = this.getCategoryIcon(complexity.impact);
3208
+ console.log(` ${complexityIcon} Complexity: ${this.formatCategoryLine(complexity)}`);
3209
+ const hygieneIcon = this.getCategoryIcon(hygiene.impact);
3210
+ console.log(` ${hygieneIcon} Hygiene: ${this.formatCategoryLine(hygiene)}`);
3211
+ }
3212
+ getCategoryIcon(impact) {
3213
+ if (impact === "HIGH") return pc8.red("\u{1F534}");
3214
+ if (impact === "MEDIUM") return pc8.yellow("\u26A0\uFE0F ");
3215
+ return pc8.blue("\u2139\uFE0F ");
3216
+ }
3217
+ formatCategoryLine(category) {
3218
+ const count = pc8.dim(`${category.violations} issues`);
3219
+ const penalty = category.penalty > 0 ? pc8.red(`-${category.penalty.toFixed(1)} pts`) : pc8.green("0 pts");
3220
+ const impact = this.formatImpact(category.impact);
3221
+ return `${count.padEnd(20)} ${penalty.padEnd(20)} ${impact}`;
3222
+ }
3223
+ formatImpact(impact) {
3224
+ if (impact === "HIGH") return pc8.red("HIGH IMPACT");
3225
+ if (impact === "MEDIUM") return pc8.yellow("MEDIUM");
3226
+ return pc8.dim("LOW");
3227
+ }
3228
+ printProjectStats(result) {
3229
+ console.log(pc8.bold("\u{1F4C8} PROJECT STATISTICS"));
3230
+ console.log(pc8.dim("\u2500".repeat(SEPARATOR_WIDTH)));
3231
+ console.log();
3232
+ const grouped = groupViolationsByType(result.violations);
3233
+ this.printModuleStatistics(result);
3234
+ if (result.totalLOC) {
3235
+ console.log(` Total Lines of Code: ${pc8.cyan(result.totalLOC.toLocaleString())}`);
3236
+ }
3237
+ this.printArchitectureViolations(grouped);
3238
+ this.printGodFileDetails(grouped);
3239
+ }
3240
+ printModuleStatistics(result) {
3241
+ console.log(` Files Analyzed: ${pc8.bold(result.totalModules.toString())}`);
3242
+ const healthyPercent = Math.round(result.healthyModuleCount / result.totalModules * 100);
3243
+ console.log(` Healthy Modules: ${pc8.green(`${result.healthyModuleCount} / ${result.totalModules}`)} ${pc8.dim(`(${healthyPercent}%)`)}`);
3244
+ console.log();
3245
+ }
3246
+ printArchitectureViolations(grouped) {
3247
+ const circularCount = grouped["Circular Dependency"]?.length || 0;
3248
+ const layerCount = grouped["Layer Violation"]?.length || 0;
3249
+ const godFileCount = grouped["God File"]?.length || 0;
3250
+ const forbiddenCount = grouped["Forbidden Import"]?.length || 0;
3251
+ console.log(` Circular Dependencies: ${this.formatCountDisplay(circularCount)}`);
3252
+ console.log(` Layer Violations: ${this.formatCountDisplay(layerCount)}`);
3253
+ console.log(` God Files: ${godFileCount > 0 ? this.formatWarningDisplay(godFileCount) + " \u26A0\uFE0F" : pc8.green("0 \u2705")}`);
3254
+ console.log(` Forbidden Imports: ${this.formatWarningDisplay(forbiddenCount)}`);
3255
+ }
3256
+ printGodFileDetails(grouped) {
3257
+ const godFileCount = grouped["God File"]?.length || 0;
3258
+ if (godFileCount === 0) return;
3259
+ const largestFile = this.getLargestFile(grouped["God File"] || []);
3260
+ if (largestFile) {
3261
+ console.log(` Largest File: ${pc8.yellow(largestFile)}`);
3262
+ }
3263
+ const avgSize = this.getAverageFileSize(grouped["God File"] || []);
3264
+ if (avgSize) {
3265
+ console.log(` Average File Size: ${avgSize}`);
3266
+ }
3267
+ }
3268
+ formatCountDisplay(count) {
3269
+ if (count === 0) return pc8.green("0 \u2705");
3270
+ return pc8.red(count.toString());
3271
+ }
3272
+ formatWarningDisplay(count) {
3273
+ if (count === 0) return pc8.green("0 \u2705");
3274
+ return pc8.yellow(count.toString());
3275
+ }
3276
+ printNoIssuesMessage() {
3277
+ console.log(pc8.bold(pc8.green("\u2728 EXCELLENT!")));
3278
+ console.log(pc8.dim("\u2500".repeat(SEPARATOR_WIDTH)));
3279
+ console.log();
3280
+ console.log(pc8.green(" No architecture violations detected!"));
3281
+ console.log(pc8.dim(" Your codebase follows architectural best practices."));
3282
+ }
3283
+ printGroupedRisks(result) {
3284
+ console.log(pc8.bold("\u{1F3AF} RISK BREAKDOWN"));
3285
+ console.log(pc8.dim("\u2500".repeat(SEPARATOR_WIDTH)));
3286
+ console.log();
3287
+ const grouped = groupViolationsByType(result.violations);
3288
+ const sortedTypes = Object.keys(grouped).sort((a, b) => {
3289
+ const aWeight = this.getSeverityWeight(grouped[a][0].severity);
3290
+ const bWeight = this.getSeverityWeight(grouped[b][0].severity);
3291
+ return bWeight - aWeight;
3292
+ });
3293
+ for (const type of sortedTypes) {
3294
+ this.printGroupedViolationType(type, grouped[type]);
3295
+ }
3296
+ }
3297
+ printGroupedViolationType(type, violations) {
3298
+ const icon = getSeverityIcon(violations[0].severity);
3299
+ const color = getSeverityColor(violations[0].severity);
3300
+ console.log(color(` ${icon} ${type.toUpperCase()} (${violations.length})`));
3301
+ console.log(pc8.dim(" " + "\u2500".repeat(52)));
3302
+ const printer = SUMMARY_PRINTERS[type] || printGenericViolationSummary;
3303
+ printer(violations);
3304
+ console.log();
3305
+ }
3306
+ printNextActions(result) {
3307
+ console.log(pc8.bold("\u{1F527} RECOMMENDED ACTIONS"));
3308
+ console.log(pc8.dim("\u2500".repeat(SEPARATOR_WIDTH)));
3309
+ console.log();
3310
+ const actions = generateNextActions(result);
3311
+ actions.forEach((action, idx) => {
3312
+ console.log(` ${pc8.cyan(idx + 1 + ".")} ${action.description}`);
3313
+ console.log(` ${pc8.dim("Priority:")} ${formatPriority(action.priority)} ${pc8.dim("\u2502")} ${pc8.dim("Effort:")} ${action.effort}`);
3314
+ if (action.impact) {
3315
+ console.log(` ${pc8.dim("Impact:")} ${action.impact}`);
3316
+ }
3317
+ if (action.file) {
3318
+ const linkText = action.line ? `${action.file}:${action.line}` : action.file;
3319
+ console.log(` ${pc8.dim("File:")} ${pc8.blue(linkText)}`);
3320
+ }
3321
+ console.log();
3322
+ });
3323
+ console.log(pc8.dim(" \u{1F4A1} Tip: Address high-priority items first for maximum impact."));
3324
+ }
3325
+ printDetailedViolations(violations) {
3326
+ console.log(pc8.bold("\u{1F4CB} DETAILED VIOLATION LIST"));
3327
+ console.log(pc8.dim("\u2500".repeat(SEPARATOR_WIDTH)));
3328
+ console.log();
3329
+ for (const violation of violations) {
3330
+ this.printViolation(violation, true);
3331
+ console.log(pc8.dim("\u2500".repeat(SEPARATOR_WIDTH)));
3332
+ console.log();
3333
+ }
3334
+ }
3335
+ printViolation(violation, detailed) {
3336
+ const icon = getSeverityIcon(violation.severity);
3337
+ const color = getSeverityColor(violation.severity);
3338
+ console.log(color(`${icon} ${violation.rule.toUpperCase()}`));
3339
+ const fileLocation = violation.line ? `${violation.file}:${violation.line}` : violation.file;
3340
+ console.log(pc8.dim(fileLocation));
3341
+ if (violation.relatedFile) {
3342
+ console.log(pc8.dim(" \u2193 imports"));
3343
+ console.log(pc8.dim(violation.relatedFile));
3344
+ }
3345
+ console.log();
3346
+ console.log(pc8.dim(violation.message));
3347
+ if (detailed) {
3348
+ console.log();
3349
+ console.log(pc8.bold("Why this matters:"));
3350
+ console.log(wrapText(violation.impact, 50));
3351
+ console.log();
3352
+ console.log(pc8.bold("Suggested fix:"));
3353
+ console.log(pc8.green(wrapText(violation.suggestedFix, 50)));
3354
+ }
3355
+ }
3356
+ // Helper methods
3357
+ getRiskLevel(result) {
3358
+ if (result.criticalCount > 0) return "HIGH";
3359
+ if (result.warningCount > 5) return "MEDIUM";
3360
+ if (result.warningCount > 0) return "LOW";
3361
+ return "MINIMAL";
3362
+ }
3363
+ getPrimaryConcern(result) {
3364
+ const grouped = groupViolationsByType(result.violations);
3365
+ const primaryConcern = this.findPrimaryConcernByPriority(grouped);
3366
+ if (primaryConcern) return primaryConcern;
3367
+ return this.getMostCommonConcern(grouped);
3368
+ }
3369
+ findPrimaryConcernByPriority(grouped) {
3370
+ const concernConfigs = this.getConcernConfigurations();
3371
+ for (const config of concernConfigs) {
3372
+ const violations = grouped[config.type];
3373
+ if (violations && violations.length > 0) {
3374
+ return config.format(violations.length);
3375
+ }
3376
+ }
3377
+ return null;
3378
+ }
3379
+ getConcernConfigurations() {
3380
+ return [
3381
+ {
3382
+ type: "Circular Dependency",
3383
+ format: (count) => `Circular dependencies \u2014 ${count} cycle${count === 1 ? "" : "s"} detected`
3384
+ },
3385
+ {
3386
+ type: "Layer Violation",
3387
+ format: (count) => `Architectural boundaries \u2014 ${count} layer ${count === 1 ? "violation" : "violations"}`
3388
+ },
3389
+ {
3390
+ type: "Forbidden Import",
3391
+ format: (count) => `Import restrictions \u2014 ${count} forbidden ${count === 1 ? "import" : "imports"}`
3392
+ },
3393
+ {
3394
+ type: "God File",
3395
+ format: () => "File modularity \u2014 oversized source and test files"
3396
+ },
3397
+ {
3398
+ type: "Duplicate Code",
3399
+ format: (count) => `Code duplication \u2014 ${count} duplicate ${count === 1 ? "block" : "blocks"} found`
3400
+ },
3401
+ {
3402
+ type: "High Cyclomatic Complexity",
3403
+ format: (count) => `Code complexity \u2014 ${count} overly complex ${count === 1 ? "function" : "functions"}`
3404
+ },
3405
+ {
3406
+ type: "Large Function",
3407
+ format: (count) => `Function size \u2014 ${count} oversized ${count === 1 ? "function" : "functions"}`
3408
+ },
3409
+ {
3410
+ type: "Dead Code",
3411
+ format: (count) => `Dead code \u2014 ${count} unused ${count === 1 ? "element" : "elements"}`
3412
+ }
3413
+ ];
3414
+ }
3415
+ getMostCommonConcern(grouped) {
3416
+ const sortedTypes = Object.keys(grouped).sort(
3417
+ (a, b) => grouped[b].length - grouped[a].length
3418
+ );
3419
+ if (sortedTypes.length === 0) {
3420
+ return "None";
3421
+ }
3422
+ const primaryType = sortedTypes[0];
3423
+ const count = grouped[primaryType].length;
3424
+ return `${primaryType} \u2014 ${count} ${count === 1 ? "issue" : "issues"}`;
3425
+ }
3426
+ getLargestFile(violations) {
3427
+ if (violations.length === 0) return null;
3428
+ const largest = violations.map((v) => ({
3429
+ file: v.file,
3430
+ lines: parseInt(v.message.match(/(\d+) lines/)?.[1] || "0")
3431
+ })).sort((a, b) => b.lines - a.lines)[0];
3432
+ const fileName = largest.file.split("/").pop() || largest.file;
3433
+ return `${fileName} (${largest.lines} lines)`;
3434
+ }
3435
+ getAverageFileSize(violations) {
3436
+ if (violations.length === 0) return null;
3437
+ const sizes = violations.map(
3438
+ (v) => parseInt(v.message.match(/(\d+) lines/)?.[1] || "0")
3439
+ );
3440
+ const avg = Math.round(sizes.reduce((a, b) => a + b, 0) / sizes.length);
3441
+ const maxScale = 1500;
3442
+ const filled = Math.min(10, Math.floor(avg / maxScale * 10));
3443
+ const empty = 10 - filled;
3444
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
3445
+ return `${avg} lines ${pc8.dim(bar)}`;
3446
+ }
3447
+ getSeverityWeight(severity) {
3448
+ switch (severity) {
3449
+ case "critical":
3450
+ return 3;
3451
+ case "warning":
3452
+ return 2;
3453
+ case "info":
3454
+ return 1;
3455
+ default:
3456
+ return 0;
3457
+ }
3458
+ }
3459
+ };
3460
+
3461
+ // src/output/json-reporter.ts
3462
+ var JsonReporter = class {
3463
+ report(result, _verbose) {
3464
+ const output = {
3465
+ score: result.score,
3466
+ status: result.status,
3467
+ timestamp: result.timestamp,
3468
+ summary: {
3469
+ critical: result.criticalCount,
3470
+ warnings: result.warningCount,
3471
+ info: result.infoCount,
3472
+ healthyModules: result.healthyModuleCount,
3473
+ totalModules: result.totalModules
3474
+ },
3475
+ topRisks: result.topRisks.map((v) => ({
3476
+ rule: v.rule,
3477
+ severity: v.severity,
3478
+ file: v.file,
3479
+ relatedFile: v.relatedFile,
3480
+ line: v.line,
3481
+ message: v.message,
3482
+ impact: v.impact,
3483
+ suggestedFix: v.suggestedFix
3484
+ })),
3485
+ violations: result.violations.map((v) => ({
3486
+ rule: v.rule,
3487
+ severity: v.severity,
3488
+ file: v.file,
3489
+ relatedFile: v.relatedFile,
3490
+ line: v.line,
3491
+ message: v.message
3492
+ }))
3493
+ };
3494
+ console.log(JSON.stringify(output, null, 2));
3495
+ }
3496
+ };
3497
+
3498
+ // src/output/executive-reporter.ts
3499
+ import pc9 from "picocolors";
3500
+ var ExecutiveReporter = class _ExecutiveReporter {
3501
+ static REPORT_WIDTH = 78;
3502
+ static PADDING_WITH_BORDER = 92;
3503
+ static PADDING_WITH_ICONS = 90;
3504
+ static TOP_CRITICAL_LIMIT = 5;
3505
+ static TOP_WARNINGS_LIMIT = 3;
3506
+ static TOP_RULES_LIMIT = 3;
3507
+ static SCORE_IMPROVEMENT_TARGET = 15;
3508
+ static PENALTY_IMPROVEMENT_FACTOR = 0.6;
3509
+ scoreCalculator = new ScoreCalculator();
3510
+ report(result, _) {
3511
+ console.log();
3512
+ this.printExecutiveHeader(result);
3513
+ console.log();
3514
+ this.printArchitectureScore(result);
3515
+ console.log();
3516
+ if (result.violations.length > 0) {
3517
+ this.printCriticalIssues(result);
3518
+ console.log();
3519
+ this.printImmediateActions(result);
3520
+ } else {
3521
+ this.printExcellenceMessage();
3522
+ }
3523
+ console.log();
3524
+ }
3525
+ printExecutiveHeader(result) {
3526
+ const border = "\u2550".repeat(_ExecutiveReporter.REPORT_WIDTH);
3527
+ console.log(pc9.cyan(border));
3528
+ console.log(pc9.bold(pc9.cyan(" ARCHGUARD \u2014 EXECUTIVE ARCHITECTURE REPORT")));
3529
+ if (result.projectName) {
3530
+ console.log(pc9.cyan(` Project: ${pc9.white(result.projectName)}`));
3531
+ }
3532
+ const date = new Date(result.timestamp).toLocaleDateString("en-US", {
3533
+ year: "numeric",
3534
+ month: "long",
3535
+ day: "numeric"
3536
+ });
3537
+ console.log(pc9.dim(` Analysis Date: ${date}`));
3538
+ console.log(pc9.cyan(border));
3539
+ }
3540
+ printArchitectureScore(result) {
3541
+ const grade = this.scoreCalculator.getGrade(result.score);
3542
+ const scoreColor = getScoreColor(result.score);
3543
+ const statusIcon = getStatusIcon(result.status);
3544
+ const statusColor = getStatusColor(result.status);
3545
+ console.log(pc9.bold("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
3546
+ console.log(pc9.bold(`\u2502 ARCHITECTURE HEALTH SCORE: ${scoreColor(pc9.bold(result.score.toString()))} / 100 [${grade}]`).padEnd(_ExecutiveReporter.PADDING_WITH_BORDER) + pc9.bold("\u2502"));
3547
+ console.log(pc9.bold("\u2502 \u2502"));
3548
+ console.log(pc9.bold(`\u2502 Status: ${statusColor(`${statusIcon} ${result.status}`)}`.padEnd(_ExecutiveReporter.PADDING_WITH_BORDER)) + pc9.bold("\u2502"));
3549
+ if (result.scoreBreakdown) {
3550
+ console.log(pc9.bold("\u2502 \u2502"));
3551
+ console.log(pc9.bold("\u2502 Risk Breakdown: \u2502"));
3552
+ const { structural, design, complexity, hygiene } = result.scoreBreakdown;
3553
+ if (structural.violations > 0) {
3554
+ const impact = structural.impact === "HIGH" ? pc9.red("HIGH RISK") : structural.impact;
3555
+ console.log(pc9.bold(`\u2502 \u{1F534} Structural: ${structural.violations} issues -${structural.penalty.toFixed(1)} pts ${impact}`.padEnd(_ExecutiveReporter.PADDING_WITH_ICONS)) + pc9.bold("\u2502"));
3556
+ }
3557
+ if (design.violations > 0) {
3558
+ const impact = design.impact === "HIGH" ? pc9.yellow("MEDIUM RISK") : design.impact;
3559
+ console.log(pc9.bold(`\u2502 \u26A0\uFE0F Design: ${design.violations} issues -${design.penalty.toFixed(1)} pts ${impact}`.padEnd(_ExecutiveReporter.PADDING_WITH_ICONS)) + pc9.bold("\u2502"));
3560
+ }
3561
+ if (complexity.violations > 0) {
3562
+ console.log(pc9.bold(`\u2502 \u2139\uFE0F Complexity: ${complexity.violations} issues -${complexity.penalty.toFixed(1)} pts`.padEnd(_ExecutiveReporter.PADDING_WITH_ICONS)) + pc9.bold("\u2502"));
3563
+ }
3564
+ if (hygiene.violations > 0) {
3565
+ console.log(pc9.bold(`\u2502 \u{1F9F9} Hygiene: ${hygiene.violations} issues -${hygiene.penalty.toFixed(1)} pts`.padEnd(_ExecutiveReporter.PADDING_WITH_ICONS)) + pc9.bold("\u2502"));
3566
+ }
3567
+ }
3568
+ console.log(pc9.bold("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
3569
+ }
3570
+ printCriticalIssues(result) {
3571
+ const criticalViolations = result.violations.filter((v) => v.severity === "critical");
3572
+ if (criticalViolations.length === 0) {
3573
+ console.log(pc9.bold("\u{1F3AF} CRITICAL ISSUES"));
3574
+ console.log(pc9.dim("\u2500".repeat(_ExecutiveReporter.REPORT_WIDTH)));
3575
+ console.log();
3576
+ console.log(pc9.green(" \u2713 No critical architectural issues detected"));
3577
+ return;
3578
+ }
3579
+ console.log(pc9.bold(pc9.red("\u26D4 IMMEDIATE ACTION REQUIRED")));
3580
+ console.log(pc9.dim("\u2500".repeat(_ExecutiveReporter.REPORT_WIDTH)));
3581
+ console.log();
3582
+ console.log(pc9.red(` ${criticalViolations.length} CRITICAL architectural issues detected`));
3583
+ console.log();
3584
+ const topCritical = criticalViolations.slice(0, _ExecutiveReporter.TOP_CRITICAL_LIMIT);
3585
+ topCritical.forEach((violation, index) => {
3586
+ console.log(pc9.bold(` ${index + 1}. ${pc9.red(getSeverityIcon(violation.severity))} ${violation.rule}`));
3587
+ console.log(pc9.dim(` Location: ${violation.file}${violation.line ? `:${violation.line}` : ""}`));
3588
+ if (violation.relatedFile) {
3589
+ console.log(pc9.dim(` Related: ${violation.relatedFile}`));
3590
+ }
3591
+ console.log();
3592
+ console.log(pc9.yellow(` Impact: ${violation.impact}`));
3593
+ console.log();
3594
+ });
3595
+ if (criticalViolations.length > _ExecutiveReporter.TOP_CRITICAL_LIMIT) {
3596
+ console.log(pc9.dim(` ... and ${criticalViolations.length - _ExecutiveReporter.TOP_CRITICAL_LIMIT} more critical issues`));
3597
+ console.log();
3598
+ }
3599
+ }
3600
+ printImmediateActions(result) {
3601
+ console.log(pc9.bold("\u{1F3AF} IMMEDIATE ACTIONS"));
3602
+ console.log(pc9.dim("\u2500".repeat(_ExecutiveReporter.REPORT_WIDTH)));
3603
+ console.log();
3604
+ const criticalViolations = result.violations.filter((v) => v.severity === "critical");
3605
+ const highWarnings = result.violations.filter((v) => v.severity === "warning").sort((a, b) => b.penalty - a.penalty).slice(0, _ExecutiveReporter.TOP_WARNINGS_LIMIT);
3606
+ if (criticalViolations.length > 0) {
3607
+ console.log(pc9.bold(" THIS SPRINT (Critical):"));
3608
+ console.log();
3609
+ const grouped = this.groupByRule(criticalViolations);
3610
+ const topRules = Object.entries(grouped).sort(([, a], [, b]) => b.length - a.length).slice(0, _ExecutiveReporter.TOP_RULES_LIMIT);
3611
+ topRules.forEach(([rule, violations], index) => {
3612
+ const estimatedEffort = this.estimateEffort(violations.length);
3613
+ console.log(` ${index + 1}. ${pc9.red("\u2611")} Fix ${violations.length} ${rule} issue${violations.length > 1 ? "s" : ""}`);
3614
+ console.log(pc9.dim(` Estimated effort: ${estimatedEffort}`));
3615
+ console.log(pc9.dim(` Expected score impact: +${this.estimateScoreImprovement(violations)} points`));
3616
+ console.log();
3617
+ });
3618
+ }
3619
+ if (highWarnings.length > 0) {
3620
+ console.log(pc9.bold(" NEXT 2-4 WEEKS (High Priority):"));
3621
+ console.log();
3622
+ highWarnings.forEach((violation, index) => {
3623
+ console.log(` ${index + 1}. ${pc9.yellow("\u2611")} Address ${violation.rule}`);
3624
+ console.log(pc9.dim(` ${violation.file}${violation.line ? `:${violation.line}` : ""}`));
3625
+ console.log();
3626
+ });
3627
+ }
3628
+ const targetScore = Math.min(100, result.score + _ExecutiveReporter.SCORE_IMPROVEMENT_TARGET);
3629
+ console.log(pc9.dim(` Target for next analysis: ${targetScore}/100`));
3630
+ }
3631
+ printExcellenceMessage() {
3632
+ console.log(pc9.bold(pc9.green("\u2705 ARCHITECTURAL EXCELLENCE")));
3633
+ console.log(pc9.dim("\u2500".repeat(_ExecutiveReporter.REPORT_WIDTH)));
3634
+ console.log();
3635
+ console.log(pc9.green(" Your architecture is in excellent condition."));
3636
+ console.log(pc9.green(" No critical issues detected."));
3637
+ console.log();
3638
+ console.log(pc9.dim(" Continue maintaining this high standard through:"));
3639
+ console.log(pc9.dim(" \u2022 Regular architecture reviews"));
3640
+ console.log(pc9.dim(" \u2022 Continuous monitoring of metrics"));
3641
+ console.log(pc9.dim(" \u2022 Proactive refactoring of emerging issues"));
3642
+ console.log();
3643
+ }
3644
+ groupByRule(violations) {
3645
+ const grouped = {};
3646
+ for (const violation of violations) {
3647
+ if (!grouped[violation.rule]) {
3648
+ grouped[violation.rule] = [];
3649
+ }
3650
+ grouped[violation.rule].push(violation);
3651
+ }
3652
+ return grouped;
3653
+ }
3654
+ estimateEffort(count) {
3655
+ if (count === 1) return "2-4 hours";
3656
+ if (count <= 3) return "1-2 days";
3657
+ if (count <= 5) return "2-3 days";
3658
+ if (count <= 10) return "1 week";
3659
+ return "1-2 weeks";
3660
+ }
3661
+ estimateScoreImprovement(violations) {
3662
+ const totalPenalty = violations.reduce((sum, v) => sum + v.penalty, 0);
3663
+ return Math.round(totalPenalty * _ExecutiveReporter.PENALTY_IMPROVEMENT_FACTOR);
3664
+ }
3665
+ };
3666
+
3667
+ // src/cli/cli.ts
3668
+ var cli = cac("archguard");
3669
+ cli.command("[root]", "Analyze TypeScript project architecture").option("--config <path>", "Path to config file").option("--format <format>", "Output format (terminal | json | executive)", {
3670
+ default: "terminal"
3671
+ }).option("--fail-on-error", "Exit with code 1 if violations exist", {
3672
+ default: false
3673
+ }).option("--verbose", "Show detailed diagnostics", {
3674
+ default: false
3675
+ }).action(async (root, options) => {
3676
+ try {
3677
+ const targetDir = root || process.cwd();
3678
+ const originalDir = process.cwd();
3679
+ if (root) {
3680
+ process.chdir(targetDir);
3681
+ }
3682
+ const configLoader = new ConfigLoader();
3683
+ const config = await configLoader.load(options.config);
3684
+ const analyzer = new Analyzer();
3685
+ const result = await analyzer.analyze(config);
3686
+ if (root) {
3687
+ process.chdir(originalDir);
3688
+ }
3689
+ const reporter = options.format === "json" ? new JsonReporter() : options.format === "executive" ? new ExecutiveReporter() : new TerminalReporter();
3690
+ reporter.report(result, options.verbose);
3691
+ if (result.ruleErrors && result.ruleErrors.length > 0) {
3692
+ console.error(pc10.yellow(`
3693
+ \u26A0\uFE0F Warning: ${result.ruleErrors.length} rule(s) failed during analysis:`));
3694
+ for (const err of result.ruleErrors) {
3695
+ console.error(pc10.yellow(` - ${err.ruleName}`));
3696
+ }
3697
+ console.error(pc10.yellow(" Results may be incomplete. Use --verbose for details.\n"));
3698
+ }
3699
+ if (options.failOnError && result.violations.length > 0) {
3700
+ process.exit(1);
3701
+ }
3702
+ } catch (error) {
3703
+ console.error(pc10.red("Error:"), error instanceof Error ? error.message : error);
3704
+ process.exit(1);
3705
+ }
3706
+ });
3707
+ cli.help();
3708
+ cli.version("1.0.0");
3709
+ cli.parse();