driftdetect 0.4.7 → 0.6.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 (92) hide show
  1. package/dist/bin/drift.js +37 -1
  2. package/dist/bin/drift.js.map +1 -1
  3. package/dist/commands/approve.d.ts +20 -0
  4. package/dist/commands/approve.d.ts.map +1 -1
  5. package/dist/commands/approve.js +38 -72
  6. package/dist/commands/approve.js.map +1 -1
  7. package/dist/commands/check.d.ts +41 -0
  8. package/dist/commands/check.d.ts.map +1 -1
  9. package/dist/commands/check.js +21 -11
  10. package/dist/commands/check.js.map +1 -1
  11. package/dist/commands/constraints.d.ts +17 -0
  12. package/dist/commands/constraints.d.ts.map +1 -0
  13. package/dist/commands/constraints.js +686 -0
  14. package/dist/commands/constraints.js.map +1 -0
  15. package/dist/commands/coupling.d.ts +17 -0
  16. package/dist/commands/coupling.d.ts.map +1 -0
  17. package/dist/commands/coupling.js +726 -0
  18. package/dist/commands/coupling.js.map +1 -0
  19. package/dist/commands/decisions.d.ts +19 -0
  20. package/dist/commands/decisions.d.ts.map +1 -0
  21. package/dist/commands/decisions.js +771 -0
  22. package/dist/commands/decisions.js.map +1 -0
  23. package/dist/commands/error-handling.d.ts +15 -0
  24. package/dist/commands/error-handling.d.ts.map +1 -0
  25. package/dist/commands/error-handling.js +608 -0
  26. package/dist/commands/error-handling.js.map +1 -0
  27. package/dist/commands/export.d.ts +16 -0
  28. package/dist/commands/export.d.ts.map +1 -1
  29. package/dist/commands/export.js +46 -50
  30. package/dist/commands/export.js.map +1 -1
  31. package/dist/commands/files.d.ts +15 -0
  32. package/dist/commands/files.d.ts.map +1 -1
  33. package/dist/commands/files.js +27 -48
  34. package/dist/commands/files.js.map +1 -1
  35. package/dist/commands/go.d.ts +21 -0
  36. package/dist/commands/go.d.ts.map +1 -0
  37. package/dist/commands/go.js +530 -0
  38. package/dist/commands/go.js.map +1 -0
  39. package/dist/commands/ignore.d.ts +20 -0
  40. package/dist/commands/ignore.d.ts.map +1 -1
  41. package/dist/commands/ignore.js +25 -48
  42. package/dist/commands/ignore.js.map +1 -1
  43. package/dist/commands/index.d.ts +11 -0
  44. package/dist/commands/index.d.ts.map +1 -1
  45. package/dist/commands/index.js +15 -0
  46. package/dist/commands/index.js.map +1 -1
  47. package/dist/commands/migrate-storage.d.ts +23 -0
  48. package/dist/commands/migrate-storage.d.ts.map +1 -0
  49. package/dist/commands/migrate-storage.js +337 -0
  50. package/dist/commands/migrate-storage.js.map +1 -0
  51. package/dist/commands/report.d.ts +22 -0
  52. package/dist/commands/report.d.ts.map +1 -1
  53. package/dist/commands/report.js +19 -10
  54. package/dist/commands/report.js.map +1 -1
  55. package/dist/commands/scan.d.ts +2 -0
  56. package/dist/commands/scan.d.ts.map +1 -1
  57. package/dist/commands/scan.js +134 -3
  58. package/dist/commands/scan.js.map +1 -1
  59. package/dist/commands/simulate.d.ts +17 -0
  60. package/dist/commands/simulate.d.ts.map +1 -0
  61. package/dist/commands/simulate.js +253 -0
  62. package/dist/commands/simulate.js.map +1 -0
  63. package/dist/commands/skills.d.ts +16 -0
  64. package/dist/commands/skills.d.ts.map +1 -0
  65. package/dist/commands/skills.js +409 -0
  66. package/dist/commands/skills.js.map +1 -0
  67. package/dist/commands/status.d.ts +20 -0
  68. package/dist/commands/status.d.ts.map +1 -1
  69. package/dist/commands/status.js +74 -72
  70. package/dist/commands/status.js.map +1 -1
  71. package/dist/commands/test-topology.d.ts +15 -0
  72. package/dist/commands/test-topology.d.ts.map +1 -0
  73. package/dist/commands/test-topology.js +589 -0
  74. package/dist/commands/test-topology.js.map +1 -0
  75. package/dist/commands/where.d.ts +15 -0
  76. package/dist/commands/where.d.ts.map +1 -1
  77. package/dist/commands/where.js +41 -88
  78. package/dist/commands/where.js.map +1 -1
  79. package/dist/commands/wpf.d.ts +21 -0
  80. package/dist/commands/wpf.d.ts.map +1 -0
  81. package/dist/commands/wpf.js +632 -0
  82. package/dist/commands/wpf.js.map +1 -0
  83. package/dist/commands/wrappers.d.ts +16 -0
  84. package/dist/commands/wrappers.d.ts.map +1 -0
  85. package/dist/commands/wrappers.js +181 -0
  86. package/dist/commands/wrappers.js.map +1 -0
  87. package/dist/services/pattern-service-factory.d.ts +37 -0
  88. package/dist/services/pattern-service-factory.d.ts.map +1 -0
  89. package/dist/services/pattern-service-factory.js +41 -0
  90. package/dist/services/pattern-service-factory.js.map +1 -0
  91. package/package.json +35 -23
  92. package/LICENSE +0 -21
@@ -0,0 +1,726 @@
1
+ /**
2
+ * Module Coupling Command - drift coupling
3
+ *
4
+ * Analyze module dependencies, detect cycles, and calculate coupling metrics.
5
+ * Based on Robert C. Martin's coupling metrics (Ca, Ce, Instability, Abstractness, Distance).
6
+ */
7
+ import { Command } from 'commander';
8
+ import * as fs from 'node:fs/promises';
9
+ import * as path from 'node:path';
10
+ import chalk from 'chalk';
11
+ import { createModuleCouplingAnalyzer, createCallGraphAnalyzer, } from 'driftdetect-core';
12
+ import { createSpinner } from '../ui/spinner.js';
13
+ const DRIFT_DIR = '.drift';
14
+ const COUPLING_DIR = 'module-coupling';
15
+ /**
16
+ * Check if coupling data exists
17
+ */
18
+ async function couplingExists(rootDir) {
19
+ try {
20
+ await fs.access(path.join(rootDir, DRIFT_DIR, COUPLING_DIR, 'graph.json'));
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /**
28
+ * Show helpful message when coupling not built
29
+ */
30
+ function showNotBuiltMessage() {
31
+ console.log();
32
+ console.log(chalk.yellow('⚠️ No module coupling graph built yet.'));
33
+ console.log();
34
+ console.log(chalk.gray('Build coupling graph to analyze dependencies:'));
35
+ console.log();
36
+ console.log(chalk.cyan(' drift coupling build'));
37
+ console.log();
38
+ }
39
+ /**
40
+ * Build subcommand - analyze modules and build coupling graph
41
+ */
42
+ async function buildAction(options) {
43
+ const rootDir = process.cwd();
44
+ const format = options.format ?? 'text';
45
+ const isTextFormat = format === 'text';
46
+ try {
47
+ if (isTextFormat) {
48
+ console.log();
49
+ console.log(chalk.bold('🔗 Building Module Coupling Graph'));
50
+ console.log(chalk.gray('═'.repeat(50)));
51
+ }
52
+ const spinner = isTextFormat ? createSpinner('Initializing...') : null;
53
+ spinner?.start();
54
+ // Load call graph (required)
55
+ spinner?.text('Loading call graph...');
56
+ const callGraphAnalyzer = createCallGraphAnalyzer({ rootDir });
57
+ await callGraphAnalyzer.initialize();
58
+ const callGraph = callGraphAnalyzer.getGraph();
59
+ if (!callGraph) {
60
+ spinner?.stop();
61
+ if (format === 'json') {
62
+ console.log(JSON.stringify({ error: 'Call graph required. Run: drift callgraph build' }));
63
+ }
64
+ else {
65
+ console.log(chalk.yellow('\n⚠️ Call graph required. Run: drift callgraph build'));
66
+ }
67
+ return;
68
+ }
69
+ // Initialize coupling analyzer
70
+ spinner?.text('Analyzing module dependencies...');
71
+ const analyzer = createModuleCouplingAnalyzer({ rootDir });
72
+ analyzer.setCallGraph(callGraph);
73
+ // Build the graph
74
+ spinner?.text('Building coupling graph...');
75
+ const graph = analyzer.build();
76
+ // Save results
77
+ spinner?.text('Saving results...');
78
+ const couplingDir = path.join(rootDir, DRIFT_DIR, COUPLING_DIR);
79
+ await fs.mkdir(couplingDir, { recursive: true });
80
+ // Serialize the graph (convert Map to object)
81
+ const serializedGraph = {
82
+ modules: Object.fromEntries(graph.modules),
83
+ edges: graph.edges,
84
+ cycles: graph.cycles,
85
+ metrics: graph.metrics,
86
+ generatedAt: graph.generatedAt,
87
+ projectRoot: graph.projectRoot,
88
+ };
89
+ await fs.writeFile(path.join(couplingDir, 'graph.json'), JSON.stringify(serializedGraph, null, 2));
90
+ spinner?.stop();
91
+ // Output
92
+ if (format === 'json') {
93
+ console.log(JSON.stringify({
94
+ success: true,
95
+ modules: graph.modules.size,
96
+ edges: graph.edges.length,
97
+ cycles: graph.cycles.length,
98
+ metrics: graph.metrics,
99
+ }, null, 2));
100
+ return;
101
+ }
102
+ // Text output
103
+ console.log();
104
+ console.log(chalk.green.bold('✓ Module coupling graph built successfully'));
105
+ console.log();
106
+ formatMetrics(graph.metrics);
107
+ formatCycleSummary(graph.cycles);
108
+ console.log(chalk.gray('─'.repeat(50)));
109
+ console.log(chalk.bold('📌 Next Steps:'));
110
+ console.log(chalk.gray(` • drift coupling status ${chalk.white('View coupling overview')}`));
111
+ console.log(chalk.gray(` • drift coupling cycles ${chalk.white('List dependency cycles')}`));
112
+ console.log(chalk.gray(` • drift coupling hotspots ${chalk.white('Find highly coupled modules')}`));
113
+ console.log(chalk.gray(` • drift coupling analyze <m> ${chalk.white('Analyze specific module')}`));
114
+ console.log();
115
+ }
116
+ catch (error) {
117
+ if (format === 'json') {
118
+ console.log(JSON.stringify({ error: String(error) }));
119
+ }
120
+ else {
121
+ console.log(chalk.red(`\n❌ Error: ${error}`));
122
+ }
123
+ }
124
+ }
125
+ /**
126
+ * Status subcommand - show coupling overview
127
+ */
128
+ async function statusAction(options) {
129
+ const rootDir = process.cwd();
130
+ const format = options.format ?? 'text';
131
+ if (!(await couplingExists(rootDir))) {
132
+ if (format === 'json') {
133
+ console.log(JSON.stringify({ error: 'No coupling graph found' }));
134
+ }
135
+ else {
136
+ showNotBuiltMessage();
137
+ }
138
+ return;
139
+ }
140
+ try {
141
+ const data = JSON.parse(await fs.readFile(path.join(rootDir, DRIFT_DIR, COUPLING_DIR, 'graph.json'), 'utf-8'));
142
+ if (format === 'json') {
143
+ console.log(JSON.stringify(data.metrics, null, 2));
144
+ return;
145
+ }
146
+ console.log();
147
+ console.log(chalk.bold('🔗 Module Coupling Status'));
148
+ console.log(chalk.gray('─'.repeat(60)));
149
+ console.log();
150
+ formatMetrics(data.metrics);
151
+ formatCycleSummary(data.cycles);
152
+ }
153
+ catch (error) {
154
+ if (format === 'json') {
155
+ console.log(JSON.stringify({ error: String(error) }));
156
+ }
157
+ else {
158
+ console.log(chalk.red(`Error: ${error}`));
159
+ }
160
+ }
161
+ }
162
+ /**
163
+ * Cycles subcommand - list dependency cycles
164
+ */
165
+ async function cyclesAction(options) {
166
+ const rootDir = process.cwd();
167
+ const format = options.format ?? 'text';
168
+ const maxLength = options.maxCycleLength ?? 10;
169
+ const minSeverity = options.minSeverity ?? 'info';
170
+ if (!(await couplingExists(rootDir))) {
171
+ if (format === 'json') {
172
+ console.log(JSON.stringify({ error: 'No coupling graph found' }));
173
+ }
174
+ else {
175
+ showNotBuiltMessage();
176
+ }
177
+ return;
178
+ }
179
+ try {
180
+ const data = JSON.parse(await fs.readFile(path.join(rootDir, DRIFT_DIR, COUPLING_DIR, 'graph.json'), 'utf-8'));
181
+ let cycles = data.cycles;
182
+ // Filter by length
183
+ cycles = cycles.filter(c => c.length <= maxLength);
184
+ // Filter by severity
185
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
186
+ const minOrder = severityOrder[minSeverity];
187
+ cycles = cycles.filter(c => severityOrder[c.severity] <= minOrder);
188
+ if (format === 'json') {
189
+ console.log(JSON.stringify({ cycles, total: cycles.length }, null, 2));
190
+ return;
191
+ }
192
+ console.log();
193
+ console.log(chalk.bold('🔄 Dependency Cycles'));
194
+ console.log(chalk.gray('─'.repeat(60)));
195
+ console.log();
196
+ if (cycles.length === 0) {
197
+ console.log(chalk.green('✓ No dependency cycles found!'));
198
+ console.log();
199
+ return;
200
+ }
201
+ formatCycles(cycles);
202
+ }
203
+ catch (error) {
204
+ if (format === 'json') {
205
+ console.log(JSON.stringify({ error: String(error) }));
206
+ }
207
+ else {
208
+ console.log(chalk.red(`Error: ${error}`));
209
+ }
210
+ }
211
+ }
212
+ /**
213
+ * Hotspots subcommand - find highly coupled modules
214
+ */
215
+ async function hotspotsAction(options) {
216
+ const rootDir = process.cwd();
217
+ const format = options.format ?? 'text';
218
+ const limit = options.limit ?? 15;
219
+ const minCoupling = options.minCoupling ?? 3;
220
+ if (!(await couplingExists(rootDir))) {
221
+ if (format === 'json') {
222
+ console.log(JSON.stringify({ error: 'No coupling graph found' }));
223
+ }
224
+ else {
225
+ showNotBuiltMessage();
226
+ }
227
+ return;
228
+ }
229
+ try {
230
+ const data = JSON.parse(await fs.readFile(path.join(rootDir, DRIFT_DIR, COUPLING_DIR, 'graph.json'), 'utf-8'));
231
+ const modules = Object.entries(data.modules);
232
+ const hotspots = modules
233
+ .map(([path, mod]) => ({
234
+ path,
235
+ coupling: mod.metrics.Ca + mod.metrics.Ce,
236
+ metrics: mod.metrics,
237
+ }))
238
+ .filter(h => h.coupling >= minCoupling)
239
+ .sort((a, b) => b.coupling - a.coupling)
240
+ .slice(0, limit);
241
+ if (format === 'json') {
242
+ console.log(JSON.stringify({ hotspots }, null, 2));
243
+ return;
244
+ }
245
+ console.log();
246
+ console.log(chalk.bold('🔥 Coupling Hotspots'));
247
+ console.log(chalk.gray('─'.repeat(60)));
248
+ console.log();
249
+ if (hotspots.length === 0) {
250
+ console.log(chalk.green('✓ No highly coupled modules found!'));
251
+ console.log();
252
+ return;
253
+ }
254
+ formatHotspots(hotspots);
255
+ }
256
+ catch (error) {
257
+ if (format === 'json') {
258
+ console.log(JSON.stringify({ error: String(error) }));
259
+ }
260
+ else {
261
+ console.log(chalk.red(`Error: ${error}`));
262
+ }
263
+ }
264
+ }
265
+ /**
266
+ * Analyze subcommand - analyze specific module
267
+ */
268
+ async function analyzeAction(modulePath, options) {
269
+ const rootDir = process.cwd();
270
+ const format = options.format ?? 'text';
271
+ const spinner = format === 'text' ? createSpinner('Analyzing module...') : null;
272
+ spinner?.start();
273
+ try {
274
+ // Load call graph and rebuild analyzer
275
+ const callGraphAnalyzer = createCallGraphAnalyzer({ rootDir });
276
+ await callGraphAnalyzer.initialize();
277
+ const callGraph = callGraphAnalyzer.getGraph();
278
+ if (!callGraph) {
279
+ spinner?.stop();
280
+ if (format === 'json') {
281
+ console.log(JSON.stringify({ error: 'Call graph required' }));
282
+ }
283
+ else {
284
+ console.log(chalk.yellow('\n⚠️ Call graph required. Run: drift callgraph build'));
285
+ }
286
+ return;
287
+ }
288
+ const analyzer = createModuleCouplingAnalyzer({ rootDir });
289
+ analyzer.setCallGraph(callGraph);
290
+ analyzer.build();
291
+ const analysis = analyzer.analyzeModule(modulePath);
292
+ spinner?.stop();
293
+ if (!analysis) {
294
+ if (format === 'json') {
295
+ console.log(JSON.stringify({ error: `Module not found: ${modulePath}` }));
296
+ }
297
+ else {
298
+ console.log(chalk.yellow(`\n⚠️ Module not found: ${modulePath}`));
299
+ }
300
+ return;
301
+ }
302
+ if (format === 'json') {
303
+ console.log(JSON.stringify(analysis, null, 2));
304
+ return;
305
+ }
306
+ console.log();
307
+ console.log(chalk.bold(`📦 Module Analysis: ${modulePath}`));
308
+ console.log(chalk.gray('─'.repeat(60)));
309
+ console.log();
310
+ formatModuleAnalysis(analysis);
311
+ }
312
+ catch (error) {
313
+ spinner?.stop();
314
+ if (format === 'json') {
315
+ console.log(JSON.stringify({ error: String(error) }));
316
+ }
317
+ else {
318
+ console.log(chalk.red(`Error: ${error}`));
319
+ }
320
+ }
321
+ }
322
+ /**
323
+ * Refactor-impact subcommand - analyze impact of refactoring a module
324
+ */
325
+ async function refactorImpactAction(modulePath, options) {
326
+ const rootDir = process.cwd();
327
+ const format = options.format ?? 'text';
328
+ const spinner = format === 'text' ? createSpinner('Analyzing refactor impact...') : null;
329
+ spinner?.start();
330
+ try {
331
+ const callGraphAnalyzer = createCallGraphAnalyzer({ rootDir });
332
+ await callGraphAnalyzer.initialize();
333
+ const callGraph = callGraphAnalyzer.getGraph();
334
+ if (!callGraph) {
335
+ spinner?.stop();
336
+ if (format === 'json') {
337
+ console.log(JSON.stringify({ error: 'Call graph required' }));
338
+ }
339
+ else {
340
+ console.log(chalk.yellow('\n⚠️ Call graph required. Run: drift callgraph build'));
341
+ }
342
+ return;
343
+ }
344
+ const analyzer = createModuleCouplingAnalyzer({ rootDir });
345
+ analyzer.setCallGraph(callGraph);
346
+ analyzer.build();
347
+ const impact = analyzer.analyzeRefactorImpact(modulePath);
348
+ spinner?.stop();
349
+ if (!impact) {
350
+ if (format === 'json') {
351
+ console.log(JSON.stringify({ error: `Module not found: ${modulePath}` }));
352
+ }
353
+ else {
354
+ console.log(chalk.yellow(`\n⚠️ Module not found: ${modulePath}`));
355
+ }
356
+ return;
357
+ }
358
+ if (format === 'json') {
359
+ console.log(JSON.stringify(impact, null, 2));
360
+ return;
361
+ }
362
+ console.log();
363
+ console.log(chalk.bold(`🔧 Refactor Impact: ${modulePath}`));
364
+ console.log(chalk.gray('─'.repeat(60)));
365
+ console.log();
366
+ formatRefactorImpact(impact);
367
+ }
368
+ catch (error) {
369
+ spinner?.stop();
370
+ if (format === 'json') {
371
+ console.log(JSON.stringify({ error: String(error) }));
372
+ }
373
+ else {
374
+ console.log(chalk.red(`Error: ${error}`));
375
+ }
376
+ }
377
+ }
378
+ /**
379
+ * Unused-exports subcommand - find unused exports
380
+ */
381
+ async function unusedExportsAction(options) {
382
+ const rootDir = process.cwd();
383
+ const format = options.format ?? 'text';
384
+ const limit = options.limit ?? 20;
385
+ const spinner = format === 'text' ? createSpinner('Finding unused exports...') : null;
386
+ spinner?.start();
387
+ try {
388
+ const callGraphAnalyzer = createCallGraphAnalyzer({ rootDir });
389
+ await callGraphAnalyzer.initialize();
390
+ const callGraph = callGraphAnalyzer.getGraph();
391
+ if (!callGraph) {
392
+ spinner?.stop();
393
+ if (format === 'json') {
394
+ console.log(JSON.stringify({ error: 'Call graph required' }));
395
+ }
396
+ else {
397
+ console.log(chalk.yellow('\n⚠️ Call graph required. Run: drift callgraph build'));
398
+ }
399
+ return;
400
+ }
401
+ const analyzer = createModuleCouplingAnalyzer({ rootDir });
402
+ analyzer.setCallGraph(callGraph);
403
+ analyzer.build();
404
+ const unused = analyzer.getUnusedExports().slice(0, limit);
405
+ spinner?.stop();
406
+ if (format === 'json') {
407
+ console.log(JSON.stringify({ unused, total: unused.length }, null, 2));
408
+ return;
409
+ }
410
+ console.log();
411
+ console.log(chalk.bold('📤 Unused Exports'));
412
+ console.log(chalk.gray('─'.repeat(60)));
413
+ console.log();
414
+ if (unused.length === 0) {
415
+ console.log(chalk.green('✓ No unused exports found!'));
416
+ console.log();
417
+ return;
418
+ }
419
+ formatUnusedExports(unused);
420
+ }
421
+ catch (error) {
422
+ spinner?.stop();
423
+ if (format === 'json') {
424
+ console.log(JSON.stringify({ error: String(error) }));
425
+ }
426
+ else {
427
+ console.log(chalk.red(`Error: ${error}`));
428
+ }
429
+ }
430
+ }
431
+ // ============================================================================
432
+ // Formatters
433
+ // ============================================================================
434
+ function formatMetrics(metrics) {
435
+ console.log(chalk.bold('📊 Overview'));
436
+ console.log(chalk.gray('─'.repeat(50)));
437
+ console.log(` Modules: ${chalk.cyan.bold(metrics.totalModules)}`);
438
+ console.log(` Dependencies: ${chalk.cyan.bold(metrics.totalEdges)}`);
439
+ console.log(` Cycles: ${getCycleColor(metrics.cycleCount)}`);
440
+ console.log();
441
+ console.log(chalk.bold('📈 Metrics'));
442
+ console.log(chalk.gray('─'.repeat(50)));
443
+ console.log(` Avg Instability: ${getInstabilityColor(metrics.avgInstability)}`);
444
+ console.log(` Avg Distance: ${getDistanceColor(metrics.avgDistance)}`);
445
+ console.log();
446
+ if (metrics.zoneOfPain.length > 0) {
447
+ console.log(chalk.bold.red('⚠️ Zone of Pain (stable + concrete)'));
448
+ console.log(chalk.gray('─'.repeat(50)));
449
+ for (const mod of metrics.zoneOfPain.slice(0, 5)) {
450
+ console.log(` ${chalk.red('●')} ${mod}`);
451
+ }
452
+ if (metrics.zoneOfPain.length > 5) {
453
+ console.log(chalk.gray(` ... and ${metrics.zoneOfPain.length - 5} more`));
454
+ }
455
+ console.log();
456
+ }
457
+ if (metrics.zoneOfUselessness.length > 0) {
458
+ console.log(chalk.bold.yellow('⚠️ Zone of Uselessness (unstable + abstract)'));
459
+ console.log(chalk.gray('─'.repeat(50)));
460
+ for (const mod of metrics.zoneOfUselessness.slice(0, 5)) {
461
+ console.log(` ${chalk.yellow('●')} ${mod}`);
462
+ }
463
+ if (metrics.zoneOfUselessness.length > 5) {
464
+ console.log(chalk.gray(` ... and ${metrics.zoneOfUselessness.length - 5} more`));
465
+ }
466
+ console.log();
467
+ }
468
+ }
469
+ function formatCycleSummary(cycles) {
470
+ if (cycles.length === 0) {
471
+ console.log(chalk.green('✓ No dependency cycles detected'));
472
+ console.log();
473
+ return;
474
+ }
475
+ const critical = cycles.filter(c => c.severity === 'critical').length;
476
+ const warning = cycles.filter(c => c.severity === 'warning').length;
477
+ const info = cycles.filter(c => c.severity === 'info').length;
478
+ console.log(chalk.bold('🔄 Cycle Summary'));
479
+ console.log(chalk.gray('─'.repeat(50)));
480
+ if (critical > 0)
481
+ console.log(` ${chalk.red('●')} Critical: ${chalk.red.bold(critical)}`);
482
+ if (warning > 0)
483
+ console.log(` ${chalk.yellow('●')} Warning: ${chalk.yellow.bold(warning)}`);
484
+ if (info > 0)
485
+ console.log(` ${chalk.gray('●')} Info: ${chalk.gray(info)}`);
486
+ console.log();
487
+ }
488
+ function formatCycles(cycles) {
489
+ for (const cycle of cycles) {
490
+ const severityIcon = cycle.severity === 'critical' ? chalk.red('🔴') :
491
+ cycle.severity === 'warning' ? chalk.yellow('🟡') : chalk.gray('⚪');
492
+ console.log(`${severityIcon} ${chalk.bold(`Cycle ${cycle.id}`)} (${cycle.length} modules)`);
493
+ console.log(chalk.gray(' Path:'));
494
+ for (let i = 0; i < cycle.path.length; i++) {
495
+ const mod = cycle.path[i];
496
+ const arrow = i < cycle.path.length - 1 ? '→' : '↩';
497
+ console.log(` ${mod} ${chalk.gray(arrow)}`);
498
+ }
499
+ if (cycle.breakPoints.length > 0) {
500
+ const best = cycle.breakPoints[0];
501
+ console.log(chalk.gray(' Suggested break:'));
502
+ console.log(` ${chalk.cyan(best.edge.from)} → ${chalk.cyan(best.edge.to)}`);
503
+ console.log(chalk.gray(` ${best.rationale}`));
504
+ console.log(chalk.gray(` Approach: ${best.approach}`));
505
+ }
506
+ console.log();
507
+ }
508
+ }
509
+ function formatHotspots(hotspots) {
510
+ const maxCoupling = hotspots[0]?.coupling ?? 1;
511
+ for (const { path: modPath, coupling, metrics } of hotspots) {
512
+ const barLength = Math.ceil((coupling / maxCoupling) * 20);
513
+ const bar = '█'.repeat(barLength);
514
+ const couplingColor = coupling > 15 ? chalk.red : coupling > 8 ? chalk.yellow : chalk.green;
515
+ console.log(`${couplingColor(bar)} ${chalk.white(modPath)}`);
516
+ console.log(chalk.gray(` Ca: ${metrics.Ca} (dependents) | Ce: ${metrics.Ce} (dependencies) | I: ${metrics.instability}`));
517
+ console.log();
518
+ }
519
+ }
520
+ function formatModuleAnalysis(analysis) {
521
+ const { module, directDependencies, directDependents, cyclesInvolved, health } = analysis;
522
+ // Module info
523
+ console.log(chalk.bold('Module Info'));
524
+ console.log(chalk.gray('─'.repeat(50)));
525
+ console.log(` Role: ${getRoleIcon(module.role)} ${module.role}`);
526
+ console.log(` Exports: ${module.exports.length}`);
527
+ console.log(` Entry Point: ${module.isEntryPoint ? chalk.green('Yes') : 'No'}`);
528
+ console.log(` Leaf: ${module.isLeaf ? chalk.green('Yes') : 'No'}`);
529
+ console.log();
530
+ // Metrics
531
+ console.log(chalk.bold('Coupling Metrics'));
532
+ console.log(chalk.gray('─'.repeat(50)));
533
+ console.log(` Ca (Afferent): ${module.metrics.Ca} modules depend on this`);
534
+ console.log(` Ce (Efferent): ${module.metrics.Ce} dependencies`);
535
+ console.log(` Instability: ${getInstabilityColor(module.metrics.instability)}`);
536
+ console.log(` Abstractness: ${module.metrics.abstractness}`);
537
+ console.log(` Distance: ${getDistanceColor(module.metrics.distance)}`);
538
+ console.log();
539
+ // Dependencies
540
+ if (directDependencies.length > 0) {
541
+ console.log(chalk.bold('Dependencies'));
542
+ console.log(chalk.gray('─'.repeat(50)));
543
+ for (const dep of directDependencies.slice(0, 10)) {
544
+ console.log(` → ${dep.path}`);
545
+ if (dep.symbols.length > 0) {
546
+ console.log(chalk.gray(` Imports: ${dep.symbols.slice(0, 5).join(', ')}${dep.symbols.length > 5 ? '...' : ''}`));
547
+ }
548
+ }
549
+ if (directDependencies.length > 10) {
550
+ console.log(chalk.gray(` ... and ${directDependencies.length - 10} more`));
551
+ }
552
+ console.log();
553
+ }
554
+ // Dependents
555
+ if (directDependents.length > 0) {
556
+ console.log(chalk.bold('Dependents'));
557
+ console.log(chalk.gray('─'.repeat(50)));
558
+ for (const dep of directDependents.slice(0, 10)) {
559
+ console.log(` ← ${dep.path}`);
560
+ }
561
+ if (directDependents.length > 10) {
562
+ console.log(chalk.gray(` ... and ${directDependents.length - 10} more`));
563
+ }
564
+ console.log();
565
+ }
566
+ // Cycles
567
+ if (cyclesInvolved.length > 0) {
568
+ console.log(chalk.bold.yellow('⚠️ Involved in Cycles'));
569
+ console.log(chalk.gray('─'.repeat(50)));
570
+ for (const cycle of cyclesInvolved) {
571
+ console.log(` ${cycle.id}: ${cycle.path.join(' → ')}`);
572
+ }
573
+ console.log();
574
+ }
575
+ // Health
576
+ console.log(chalk.bold('Health Assessment'));
577
+ console.log(chalk.gray('─'.repeat(50)));
578
+ console.log(` Score: ${getHealthColor(health.score)}`);
579
+ if (health.issues.length > 0) {
580
+ console.log(chalk.gray(' Issues:'));
581
+ for (const issue of health.issues) {
582
+ console.log(` ${chalk.yellow('•')} ${issue}`);
583
+ }
584
+ }
585
+ if (health.suggestions.length > 0) {
586
+ console.log(chalk.gray(' Suggestions:'));
587
+ for (const suggestion of health.suggestions) {
588
+ console.log(` ${chalk.cyan('→')} ${suggestion}`);
589
+ }
590
+ }
591
+ console.log();
592
+ }
593
+ function formatRefactorImpact(impact) {
594
+ const riskColor = impact.risk === 'critical' ? chalk.red :
595
+ impact.risk === 'high' ? chalk.yellow :
596
+ impact.risk === 'medium' ? chalk.cyan : chalk.green;
597
+ console.log(chalk.bold('Impact Summary'));
598
+ console.log(chalk.gray('─'.repeat(50)));
599
+ console.log(` Affected Modules: ${chalk.cyan.bold(impact.totalAffected)}`);
600
+ console.log(` Risk Level: ${riskColor(impact.risk.toUpperCase())}`);
601
+ console.log();
602
+ if (impact.affectedModules.length > 0) {
603
+ console.log(chalk.bold('Affected Modules'));
604
+ console.log(chalk.gray('─'.repeat(50)));
605
+ for (const mod of impact.affectedModules.slice(0, 15)) {
606
+ const effortIcon = mod.effort === 'high' ? chalk.red('●') :
607
+ mod.effort === 'medium' ? chalk.yellow('●') : chalk.green('●');
608
+ console.log(` ${effortIcon} ${mod.path}`);
609
+ console.log(chalk.gray(` ${mod.reason}`));
610
+ }
611
+ if (impact.affectedModules.length > 15) {
612
+ console.log(chalk.gray(` ... and ${impact.affectedModules.length - 15} more`));
613
+ }
614
+ console.log();
615
+ }
616
+ if (impact.suggestions.length > 0) {
617
+ console.log(chalk.bold('Suggestions'));
618
+ console.log(chalk.gray('─'.repeat(50)));
619
+ for (const suggestion of impact.suggestions) {
620
+ console.log(` ${chalk.cyan('→')} ${suggestion}`);
621
+ }
622
+ console.log();
623
+ }
624
+ }
625
+ function formatUnusedExports(unused) {
626
+ for (const { module, unusedExports, possibleReasons } of unused) {
627
+ console.log(`${chalk.yellow('●')} ${module}`);
628
+ for (const exp of unusedExports.slice(0, 5)) {
629
+ console.log(chalk.gray(` • ${exp.name} (${exp.kind})`));
630
+ }
631
+ if (unusedExports.length > 5) {
632
+ console.log(chalk.gray(` ... and ${unusedExports.length - 5} more`));
633
+ }
634
+ if (possibleReasons.length > 0) {
635
+ console.log(chalk.gray(` Possible: ${possibleReasons.join(', ')}`));
636
+ }
637
+ console.log();
638
+ }
639
+ }
640
+ // ============================================================================
641
+ // Helpers
642
+ // ============================================================================
643
+ function getCycleColor(count) {
644
+ if (count === 0)
645
+ return chalk.green.bold('0');
646
+ if (count <= 3)
647
+ return chalk.yellow.bold(String(count));
648
+ return chalk.red.bold(String(count));
649
+ }
650
+ function getInstabilityColor(value) {
651
+ // Instability near 0.5 is ideal (balanced)
652
+ const distance = Math.abs(value - 0.5);
653
+ if (distance <= 0.2)
654
+ return chalk.green(value.toFixed(2));
655
+ if (distance <= 0.35)
656
+ return chalk.yellow(value.toFixed(2));
657
+ return chalk.red(value.toFixed(2));
658
+ }
659
+ function getDistanceColor(value) {
660
+ // Distance near 0 is ideal
661
+ if (value <= 0.2)
662
+ return chalk.green(value.toFixed(2));
663
+ if (value <= 0.4)
664
+ return chalk.yellow(value.toFixed(2));
665
+ return chalk.red(value.toFixed(2));
666
+ }
667
+ function getHealthColor(score) {
668
+ if (score >= 70)
669
+ return chalk.green(`${score}/100`);
670
+ if (score >= 50)
671
+ return chalk.yellow(`${score}/100`);
672
+ return chalk.red(`${score}/100`);
673
+ }
674
+ function getRoleIcon(role) {
675
+ switch (role) {
676
+ case 'hub': return '🎯';
677
+ case 'authority': return '📚';
678
+ case 'balanced': return '⚖️';
679
+ case 'isolated': return '🏝️';
680
+ default: return '📦';
681
+ }
682
+ }
683
+ // ============================================================================
684
+ // Command Registration
685
+ // ============================================================================
686
+ export function createCouplingCommand() {
687
+ const cmd = new Command('coupling')
688
+ .description('Analyze module dependencies and coupling metrics')
689
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
690
+ .option('-v, --verbose', 'Enable verbose output');
691
+ cmd
692
+ .command('build')
693
+ .description('Build module coupling graph')
694
+ .action(() => buildAction(cmd.opts()));
695
+ cmd
696
+ .command('status')
697
+ .description('Show coupling overview')
698
+ .action(() => statusAction(cmd.opts()));
699
+ cmd
700
+ .command('cycles')
701
+ .description('List dependency cycles')
702
+ .option('-l, --max-cycle-length <number>', 'Maximum cycle length', '10')
703
+ .option('-s, --min-severity <level>', 'Minimum severity (info, warning, critical)', 'info')
704
+ .action((opts) => cyclesAction({ ...cmd.opts(), ...opts }));
705
+ cmd
706
+ .command('hotspots')
707
+ .description('Find highly coupled modules')
708
+ .option('-l, --limit <number>', 'Maximum results', '15')
709
+ .option('-m, --min-coupling <number>', 'Minimum coupling threshold', '3')
710
+ .action((opts) => hotspotsAction({ ...cmd.opts(), ...opts }));
711
+ cmd
712
+ .command('analyze <module>')
713
+ .description('Analyze specific module coupling')
714
+ .action((module) => analyzeAction(module, cmd.opts()));
715
+ cmd
716
+ .command('refactor-impact <module>')
717
+ .description('Analyze impact of refactoring a module')
718
+ .action((module) => refactorImpactAction(module, cmd.opts()));
719
+ cmd
720
+ .command('unused-exports')
721
+ .description('Find unused exports')
722
+ .option('-l, --limit <number>', 'Maximum results', '20')
723
+ .action((opts) => unusedExportsAction({ ...cmd.opts(), ...opts }));
724
+ return cmd;
725
+ }
726
+ //# sourceMappingURL=coupling.js.map