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.
- package/dist/bin/drift.js +37 -1
- package/dist/bin/drift.js.map +1 -1
- package/dist/commands/approve.d.ts +20 -0
- package/dist/commands/approve.d.ts.map +1 -1
- package/dist/commands/approve.js +38 -72
- package/dist/commands/approve.js.map +1 -1
- package/dist/commands/check.d.ts +41 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +21 -11
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/constraints.d.ts +17 -0
- package/dist/commands/constraints.d.ts.map +1 -0
- package/dist/commands/constraints.js +686 -0
- package/dist/commands/constraints.js.map +1 -0
- package/dist/commands/coupling.d.ts +17 -0
- package/dist/commands/coupling.d.ts.map +1 -0
- package/dist/commands/coupling.js +726 -0
- package/dist/commands/coupling.js.map +1 -0
- package/dist/commands/decisions.d.ts +19 -0
- package/dist/commands/decisions.d.ts.map +1 -0
- package/dist/commands/decisions.js +771 -0
- package/dist/commands/decisions.js.map +1 -0
- package/dist/commands/error-handling.d.ts +15 -0
- package/dist/commands/error-handling.d.ts.map +1 -0
- package/dist/commands/error-handling.js +608 -0
- package/dist/commands/error-handling.js.map +1 -0
- package/dist/commands/export.d.ts +16 -0
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +46 -50
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/files.d.ts +15 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +27 -48
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/go.d.ts +21 -0
- package/dist/commands/go.d.ts.map +1 -0
- package/dist/commands/go.js +530 -0
- package/dist/commands/go.js.map +1 -0
- package/dist/commands/ignore.d.ts +20 -0
- package/dist/commands/ignore.d.ts.map +1 -1
- package/dist/commands/ignore.js +25 -48
- package/dist/commands/ignore.js.map +1 -1
- package/dist/commands/index.d.ts +11 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +15 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/migrate-storage.d.ts +23 -0
- package/dist/commands/migrate-storage.d.ts.map +1 -0
- package/dist/commands/migrate-storage.js +337 -0
- package/dist/commands/migrate-storage.js.map +1 -0
- package/dist/commands/report.d.ts +22 -0
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +19 -10
- package/dist/commands/report.js.map +1 -1
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +134 -3
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/simulate.d.ts +17 -0
- package/dist/commands/simulate.d.ts.map +1 -0
- package/dist/commands/simulate.js +253 -0
- package/dist/commands/simulate.js.map +1 -0
- package/dist/commands/skills.d.ts +16 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +409 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.d.ts +20 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +74 -72
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test-topology.d.ts +15 -0
- package/dist/commands/test-topology.d.ts.map +1 -0
- package/dist/commands/test-topology.js +589 -0
- package/dist/commands/test-topology.js.map +1 -0
- package/dist/commands/where.d.ts +15 -0
- package/dist/commands/where.d.ts.map +1 -1
- package/dist/commands/where.js +41 -88
- package/dist/commands/where.js.map +1 -1
- package/dist/commands/wpf.d.ts +21 -0
- package/dist/commands/wpf.d.ts.map +1 -0
- package/dist/commands/wpf.js +632 -0
- package/dist/commands/wpf.js.map +1 -0
- package/dist/commands/wrappers.d.ts +16 -0
- package/dist/commands/wrappers.d.ts.map +1 -0
- package/dist/commands/wrappers.js +181 -0
- package/dist/commands/wrappers.js.map +1 -0
- package/dist/services/pattern-service-factory.d.ts +37 -0
- package/dist/services/pattern-service-factory.d.ts.map +1 -0
- package/dist/services/pattern-service-factory.js +41 -0
- package/dist/services/pattern-service-factory.js.map +1 -0
- package/package.json +35 -23
- 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
|