driftdetect 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1376 @@
1
+ /**
2
+ * Call Graph Command - drift callgraph
3
+ *
4
+ * Build and query call graphs to understand code reachability.
5
+ * Answers: "What data can this code access?" and "Who can reach this data?"
6
+ *
7
+ * @requirements Call Graph Feature
8
+ */
9
+ import { Command } from 'commander';
10
+ import * as fs from 'node:fs/promises';
11
+ import * as path from 'node:path';
12
+ import chalk from 'chalk';
13
+ import { createCallGraphAnalyzer, createBoundaryScanner, createSecurityPrioritizer, createImpactAnalyzer, createDeadCodeDetector, createCoverageAnalyzer, createSemanticDataAccessScanner, detectProjectStack, } from 'driftdetect-core';
14
+ import { createSpinner } from '../ui/spinner.js';
15
+ /** Directory name for drift configuration */
16
+ const DRIFT_DIR = '.drift';
17
+ /** Directory name for call graph data */
18
+ const CALLGRAPH_DIR = 'callgraph';
19
+ /**
20
+ * Check if call graph data exists
21
+ */
22
+ async function callGraphExists(rootDir) {
23
+ try {
24
+ await fs.access(path.join(rootDir, DRIFT_DIR, CALLGRAPH_DIR, 'graph.json'));
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /**
32
+ * Show helpful message when call graph not built
33
+ */
34
+ function showNotBuiltMessage() {
35
+ console.log();
36
+ console.log(chalk.yellow('⚠️ No call graph built yet.'));
37
+ console.log();
38
+ console.log(chalk.gray('Build a call graph to analyze code reachability:'));
39
+ console.log();
40
+ console.log(chalk.cyan(' drift callgraph build'));
41
+ console.log();
42
+ }
43
+ /**
44
+ * Format detected stack for display
45
+ */
46
+ function formatDetectedStack(stack) {
47
+ console.log();
48
+ console.log(chalk.bold('🔍 Detected Project Stack'));
49
+ console.log(chalk.gray('─'.repeat(50)));
50
+ if (stack.languages.length > 0) {
51
+ const langIcons = {
52
+ 'typescript': '🟦',
53
+ 'javascript': '🟨',
54
+ 'python': '🐍',
55
+ 'csharp': '🟣',
56
+ 'java': '☕',
57
+ 'php': '🐘',
58
+ };
59
+ const langs = stack.languages.map(l => `${langIcons[l] ?? '📄'} ${l}`).join(' ');
60
+ console.log(` Languages: ${langs}`);
61
+ }
62
+ if (stack.orms.length > 0) {
63
+ const ormColors = {
64
+ 'supabase': chalk.green,
65
+ 'prisma': chalk.cyan,
66
+ 'django': chalk.green,
67
+ 'sqlalchemy': chalk.yellow,
68
+ 'ef-core': chalk.magenta,
69
+ 'dapper': chalk.blue,
70
+ 'spring-data-jpa': chalk.green,
71
+ 'hibernate': chalk.yellow,
72
+ 'eloquent': chalk.red,
73
+ 'doctrine': chalk.blue,
74
+ };
75
+ const orms = stack.orms.map(o => {
76
+ const color = ormColors[o] ?? chalk.gray;
77
+ return color(o);
78
+ }).join(', ');
79
+ console.log(` ORMs/Data: ${orms}`);
80
+ }
81
+ if (stack.frameworks.length > 0) {
82
+ console.log(` Frameworks: ${chalk.cyan(stack.frameworks.join(', '))}`);
83
+ }
84
+ console.log();
85
+ }
86
+ /**
87
+ * Show tree-sitter availability warnings
88
+ */
89
+ function showParserWarnings(detectedLanguages) {
90
+ // Languages with tree-sitter support
91
+ const treeSitterSupported = ['typescript', 'javascript', 'python', 'csharp', 'java', 'php'];
92
+ // Check for unsupported languages
93
+ const unsupported = detectedLanguages.filter(l => !treeSitterSupported.includes(l));
94
+ if (unsupported.length > 0) {
95
+ console.log(chalk.yellow('⚠️ Parser Warnings:'));
96
+ for (const lang of unsupported) {
97
+ console.log(chalk.yellow(` • ${lang}: No tree-sitter parser available, using regex fallback`));
98
+ }
99
+ console.log();
100
+ }
101
+ }
102
+ /**
103
+ * Build subcommand - build the call graph
104
+ */
105
+ async function buildAction(options) {
106
+ const rootDir = process.cwd();
107
+ const format = options.format ?? 'text';
108
+ const isTextFormat = format === 'text';
109
+ try {
110
+ // Step 0: Detect project stack first
111
+ if (isTextFormat) {
112
+ console.log();
113
+ console.log(chalk.bold('🚀 Building Call Graph'));
114
+ console.log(chalk.gray('═'.repeat(50)));
115
+ }
116
+ const detectedStack = await detectProjectStack(rootDir);
117
+ if (isTextFormat && (detectedStack.languages.length > 0 || detectedStack.orms.length > 0)) {
118
+ formatDetectedStack(detectedStack);
119
+ showParserWarnings(detectedStack.languages);
120
+ }
121
+ const spinner = isTextFormat ? createSpinner('Initializing...') : null;
122
+ spinner?.start();
123
+ // File patterns to scan - include all supported languages
124
+ const filePatterns = [
125
+ '**/*.ts',
126
+ '**/*.tsx',
127
+ '**/*.js',
128
+ '**/*.jsx',
129
+ '**/*.py',
130
+ '**/*.cs',
131
+ '**/*.java',
132
+ '**/*.php',
133
+ ];
134
+ // Step 1: Run semantic data access scanner (tree-sitter based)
135
+ spinner?.text('🌳 Scanning with tree-sitter (semantic analysis)...');
136
+ const semanticScanner = createSemanticDataAccessScanner({
137
+ rootDir,
138
+ verbose: options.verbose ?? false,
139
+ autoDetect: true,
140
+ });
141
+ const semanticResult = await semanticScanner.scanDirectory({ patterns: filePatterns });
142
+ // Use semantic results as primary source
143
+ const dataAccessPoints = semanticResult.accessPoints;
144
+ const semanticStats = semanticResult.stats;
145
+ // Step 2: Fall back to boundary scanner for additional coverage (regex-based)
146
+ spinner?.text('🔍 Scanning for additional patterns (regex fallback)...');
147
+ const boundaryScanner = createBoundaryScanner({ rootDir, verbose: options.verbose ?? false });
148
+ await boundaryScanner.initialize();
149
+ const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
150
+ // Merge boundary results with semantic results (semantic takes precedence)
151
+ let regexAdditions = 0;
152
+ for (const [, accessPoint] of Object.entries(boundaryResult.accessMap.accessPoints)) {
153
+ const existing = dataAccessPoints.get(accessPoint.file) ?? [];
154
+ // Only add if not already detected by semantic scanner
155
+ const isDuplicate = existing.some(ap => ap.line === accessPoint.line && ap.table === accessPoint.table);
156
+ if (!isDuplicate) {
157
+ existing.push(accessPoint);
158
+ dataAccessPoints.set(accessPoint.file, existing);
159
+ regexAdditions++;
160
+ }
161
+ }
162
+ // Step 3: Build call graph with data access points
163
+ spinner?.text('📊 Building call graph...');
164
+ const analyzer = createCallGraphAnalyzer({ rootDir });
165
+ await analyzer.initialize();
166
+ const graph = await analyzer.scan(filePatterns, dataAccessPoints);
167
+ // Ensure directory exists
168
+ const graphDir = path.join(rootDir, DRIFT_DIR, CALLGRAPH_DIR);
169
+ await fs.mkdir(graphDir, { recursive: true });
170
+ // Save graph
171
+ spinner?.text('💾 Saving call graph...');
172
+ const graphPath = path.join(graphDir, 'graph.json');
173
+ await fs.writeFile(graphPath, JSON.stringify({
174
+ version: graph.version,
175
+ generatedAt: graph.generatedAt,
176
+ projectRoot: graph.projectRoot,
177
+ stats: graph.stats,
178
+ entryPoints: graph.entryPoints,
179
+ dataAccessors: graph.dataAccessors,
180
+ functions: Object.fromEntries(graph.functions),
181
+ }, null, 2));
182
+ spinner?.stop();
183
+ // JSON output
184
+ if (format === 'json') {
185
+ console.log(JSON.stringify({
186
+ success: true,
187
+ detectedStack,
188
+ stats: graph.stats,
189
+ entryPoints: graph.entryPoints.length,
190
+ dataAccessors: graph.dataAccessors.length,
191
+ semanticStats: semanticStats,
192
+ boundaryStats: boundaryResult.stats,
193
+ regexAdditions,
194
+ }, null, 2));
195
+ return;
196
+ }
197
+ // Text output
198
+ console.log();
199
+ console.log(chalk.green.bold('✓ Call graph built successfully'));
200
+ console.log();
201
+ // Main statistics box
202
+ console.log(chalk.bold('📊 Graph Statistics'));
203
+ console.log(chalk.gray('─'.repeat(50)));
204
+ console.log(` Functions: ${chalk.cyan.bold(graph.stats.totalFunctions.toLocaleString())}`);
205
+ console.log(` Call Sites: ${chalk.cyan(graph.stats.totalCallSites.toLocaleString())} (${chalk.green(Math.round(graph.stats.resolvedCallSites / Math.max(1, graph.stats.totalCallSites) * 100) + '%')} resolved)`);
206
+ console.log(` Entry Points: ${chalk.magenta.bold(graph.entryPoints.length.toLocaleString())} ${chalk.gray('(API routes, exports)')}`);
207
+ console.log(` Data Accessors: ${chalk.yellow.bold(graph.dataAccessors.length.toLocaleString())} ${chalk.gray('(functions with DB access)')}`);
208
+ console.log();
209
+ // Data access detection summary
210
+ const totalAccessPoints = semanticStats.accessPointsFound + regexAdditions;
211
+ if (totalAccessPoints > 0) {
212
+ console.log(chalk.bold('💾 Data Access Detection'));
213
+ console.log(chalk.gray('─'.repeat(50)));
214
+ console.log(` ${chalk.green('🌳 Tree-sitter:')} ${chalk.cyan(semanticStats.accessPointsFound)} access points ${chalk.gray(`(${semanticStats.filesScanned} files)`)}`);
215
+ if (regexAdditions > 0) {
216
+ console.log(` ${chalk.yellow('🔍 Regex fallback:')} ${chalk.cyan(regexAdditions)} additional points`);
217
+ }
218
+ // ORM breakdown
219
+ if (Object.keys(semanticStats.byOrm).length > 0) {
220
+ console.log();
221
+ console.log(chalk.gray(' By ORM/Framework:'));
222
+ const sortedOrms = Object.entries(semanticStats.byOrm)
223
+ .sort((a, b) => b[1] - a[1]);
224
+ for (const [orm, count] of sortedOrms) {
225
+ const bar = '█'.repeat(Math.min(20, Math.ceil(count / Math.max(...sortedOrms.map(([, c]) => c)) * 20)));
226
+ console.log(` ${chalk.gray(orm.padEnd(15))} ${chalk.cyan(bar)} ${count}`);
227
+ }
228
+ }
229
+ console.log();
230
+ }
231
+ // Language breakdown
232
+ const languages = Object.entries(graph.stats.byLanguage)
233
+ .filter(([, count]) => count > 0)
234
+ .sort((a, b) => b[1] - a[1]);
235
+ if (languages.length > 0) {
236
+ console.log(chalk.bold('📝 By Language'));
237
+ console.log(chalk.gray('─'.repeat(50)));
238
+ const langIcons = {
239
+ 'typescript': '🟦',
240
+ 'javascript': '🟨',
241
+ 'python': '🐍',
242
+ 'csharp': '🟣',
243
+ 'java': '☕',
244
+ 'php': '🐘',
245
+ };
246
+ for (const [lang, count] of languages) {
247
+ const icon = langIcons[lang] ?? '📄';
248
+ const bar = '█'.repeat(Math.min(20, Math.ceil(count / Math.max(...languages.map(([, c]) => c)) * 20)));
249
+ console.log(` ${icon} ${lang.padEnd(12)} ${chalk.cyan(bar)} ${count.toLocaleString()} functions`);
250
+ }
251
+ console.log();
252
+ }
253
+ // Errors summary
254
+ if (semanticStats.errors > 0) {
255
+ console.log(chalk.yellow(`⚠️ ${semanticStats.errors} file(s) had parsing errors (use --verbose for details)`));
256
+ console.log();
257
+ }
258
+ // Next steps
259
+ console.log(chalk.gray('─'.repeat(50)));
260
+ console.log(chalk.bold('📌 Next Steps:'));
261
+ console.log(chalk.gray(` • drift callgraph status ${chalk.white('View entry points & data accessors')}`));
262
+ console.log(chalk.gray(` • drift callgraph status -s ${chalk.white('Security-prioritized view (P0-P4)')}`));
263
+ console.log(chalk.gray(` • drift callgraph reach <fn> ${chalk.white('What data can this code access?')}`));
264
+ console.log(chalk.gray(` • drift callgraph coverage ${chalk.white('Test coverage for sensitive data')}`));
265
+ console.log();
266
+ }
267
+ catch (error) {
268
+ if (format === 'json') {
269
+ console.log(JSON.stringify({ error: String(error) }));
270
+ }
271
+ else {
272
+ console.log(chalk.red(`\n❌ Error: ${error}`));
273
+ if (options.verbose && error instanceof Error && error.stack) {
274
+ console.log(chalk.gray(error.stack));
275
+ }
276
+ }
277
+ }
278
+ }
279
+ /**
280
+ * Status subcommand - show call graph overview
281
+ */
282
+ async function statusAction(options) {
283
+ const rootDir = process.cwd();
284
+ const format = options.format ?? 'text';
285
+ const showSecurity = options.security ?? false;
286
+ if (!(await callGraphExists(rootDir))) {
287
+ if (format === 'json') {
288
+ console.log(JSON.stringify({ error: 'No call graph found' }));
289
+ }
290
+ else {
291
+ showNotBuiltMessage();
292
+ }
293
+ return;
294
+ }
295
+ const analyzer = createCallGraphAnalyzer({ rootDir });
296
+ await analyzer.initialize();
297
+ const graph = analyzer.getGraph();
298
+ if (!graph) {
299
+ if (format === 'json') {
300
+ console.log(JSON.stringify({ error: 'Failed to load call graph' }));
301
+ }
302
+ else {
303
+ console.log(chalk.red('Failed to load call graph'));
304
+ }
305
+ return;
306
+ }
307
+ // If security flag is set, run boundary scan and prioritize
308
+ if (showSecurity) {
309
+ await showSecurityPrioritizedStatus(rootDir, graph, format);
310
+ return;
311
+ }
312
+ // JSON output
313
+ if (format === 'json') {
314
+ console.log(JSON.stringify({
315
+ stats: graph.stats,
316
+ entryPoints: graph.entryPoints.map(id => {
317
+ const func = graph.functions.get(id);
318
+ return func ? { id, name: func.qualifiedName, file: func.file, line: func.startLine } : { id };
319
+ }),
320
+ dataAccessors: graph.dataAccessors.map(id => {
321
+ const func = graph.functions.get(id);
322
+ return func ? {
323
+ id,
324
+ name: func.qualifiedName,
325
+ file: func.file,
326
+ line: func.startLine,
327
+ tables: [...new Set(func.dataAccess.map(d => d.table))],
328
+ } : { id };
329
+ }),
330
+ }, null, 2));
331
+ return;
332
+ }
333
+ // Text output
334
+ console.log();
335
+ console.log(chalk.bold('📊 Call Graph Status'));
336
+ console.log(chalk.gray('─'.repeat(60)));
337
+ console.log();
338
+ console.log(`Functions: ${chalk.cyan(graph.stats.totalFunctions)}`);
339
+ console.log(`Call Sites: ${chalk.cyan(graph.stats.totalCallSites)} (${chalk.green(graph.stats.resolvedCallSites)} resolved)`);
340
+ console.log(`Entry Points: ${chalk.cyan(graph.entryPoints.length)}`);
341
+ console.log(`Data Accessors: ${chalk.cyan(graph.dataAccessors.length)}`);
342
+ console.log();
343
+ // Entry points
344
+ if (graph.entryPoints.length > 0) {
345
+ console.log(chalk.bold('Entry Points (API Routes, Exports):'));
346
+ for (const id of graph.entryPoints.slice(0, 10)) {
347
+ const func = graph.functions.get(id);
348
+ if (func) {
349
+ console.log(` ${chalk.magenta('🚪')} ${chalk.white(func.qualifiedName)}`);
350
+ console.log(chalk.gray(` ${func.file}:${func.startLine}`));
351
+ }
352
+ }
353
+ if (graph.entryPoints.length > 10) {
354
+ console.log(chalk.gray(` ... and ${graph.entryPoints.length - 10} more`));
355
+ }
356
+ console.log();
357
+ }
358
+ // Data accessors
359
+ if (graph.dataAccessors.length > 0) {
360
+ console.log(chalk.bold('Data Accessors (Functions with DB access):'));
361
+ for (const id of graph.dataAccessors.slice(0, 10)) {
362
+ const func = graph.functions.get(id);
363
+ if (func) {
364
+ const tables = [...new Set(func.dataAccess.map(d => d.table))];
365
+ console.log(` ${chalk.blue('💾')} ${chalk.white(func.qualifiedName)} → [${tables.join(', ')}]`);
366
+ console.log(chalk.gray(` ${func.file}:${func.startLine}`));
367
+ }
368
+ }
369
+ if (graph.dataAccessors.length > 10) {
370
+ console.log(chalk.gray(` ... and ${graph.dataAccessors.length - 10} more`));
371
+ }
372
+ console.log();
373
+ }
374
+ console.log(chalk.gray("Tip: Use 'drift callgraph status --security' to see security-prioritized view"));
375
+ console.log();
376
+ }
377
+ /**
378
+ * Show security-prioritized status view
379
+ */
380
+ async function showSecurityPrioritizedStatus(rootDir, _graph, format) {
381
+ // Only show spinner for text format
382
+ const isTextFormat = format === 'text';
383
+ const spinner = isTextFormat ? createSpinner('Analyzing security priorities...') : null;
384
+ spinner?.start();
385
+ // Run boundary scan
386
+ const boundaryScanner = createBoundaryScanner({ rootDir, verbose: false });
387
+ await boundaryScanner.initialize();
388
+ const filePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py'];
389
+ const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
390
+ // Prioritize by security
391
+ const prioritizer = createSecurityPrioritizer();
392
+ const prioritized = prioritizer.prioritize(boundaryResult.accessMap);
393
+ spinner?.stop();
394
+ // JSON output
395
+ if (!isTextFormat) {
396
+ console.log(JSON.stringify({
397
+ summary: prioritized.summary,
398
+ critical: prioritized.critical.map(p => ({
399
+ table: p.accessPoint.table,
400
+ fields: p.accessPoint.fields,
401
+ operation: p.accessPoint.operation,
402
+ file: p.accessPoint.file,
403
+ line: p.accessPoint.line,
404
+ tier: p.security.tier,
405
+ riskScore: p.security.riskScore,
406
+ sensitivity: p.security.maxSensitivity,
407
+ regulations: p.security.regulations,
408
+ rationale: p.security.rationale,
409
+ })),
410
+ high: prioritized.high.slice(0, 20).map(p => ({
411
+ table: p.accessPoint.table,
412
+ fields: p.accessPoint.fields,
413
+ operation: p.accessPoint.operation,
414
+ file: p.accessPoint.file,
415
+ line: p.accessPoint.line,
416
+ tier: p.security.tier,
417
+ riskScore: p.security.riskScore,
418
+ sensitivity: p.security.maxSensitivity,
419
+ })),
420
+ }, null, 2));
421
+ return;
422
+ }
423
+ // Text output
424
+ console.log();
425
+ console.log(chalk.bold('🔒 Security-Prioritized Data Access'));
426
+ console.log(chalk.gray('─'.repeat(60)));
427
+ console.log();
428
+ // Summary
429
+ const { summary } = prioritized;
430
+ console.log(chalk.bold('Summary:'));
431
+ console.log(` Total Access Points: ${chalk.cyan(summary.totalAccessPoints)}`);
432
+ console.log(` ${chalk.red('🔴 Critical (P0/P1):')} ${chalk.red(summary.criticalCount)}`);
433
+ console.log(` ${chalk.yellow('🟡 High (P2):')} ${chalk.yellow(summary.highCount)}`);
434
+ console.log(` ${chalk.gray('⚪ Low (P3/P4):')} ${chalk.gray(summary.lowCount)}`);
435
+ console.log(` ${chalk.gray('📦 Noise (filtered):')} ${chalk.gray(summary.noiseCount)}`);
436
+ console.log();
437
+ // Regulations
438
+ if (summary.regulations.length > 0) {
439
+ console.log(chalk.bold('Regulatory Implications:'));
440
+ console.log(` ${summary.regulations.map(r => chalk.magenta(r.toUpperCase())).join(', ')}`);
441
+ console.log();
442
+ }
443
+ // Critical items (P0/P1)
444
+ if (prioritized.critical.length > 0) {
445
+ console.log(chalk.bold.red('🚨 Critical Security Items (P0/P1):'));
446
+ for (const p of prioritized.critical.slice(0, 15)) {
447
+ const tierColor = p.security.tier === 'P0' ? chalk.bgRed.white : chalk.red;
448
+ const sensitivityIcon = getSensitivityIcon(p.security.maxSensitivity);
449
+ const opColor = p.accessPoint.operation === 'write' ? chalk.yellow :
450
+ p.accessPoint.operation === 'delete' ? chalk.red : chalk.gray;
451
+ console.log(` ${tierColor(` ${p.security.tier} `)} ${sensitivityIcon} ${chalk.white(p.accessPoint.table)}`);
452
+ console.log(` ${opColor(p.accessPoint.operation)} ${p.accessPoint.fields.join(', ') || '*'}`);
453
+ console.log(chalk.gray(` ${p.accessPoint.file}:${p.accessPoint.line}`));
454
+ console.log(chalk.gray(` ${p.security.rationale}`));
455
+ if (p.security.regulations.length > 0) {
456
+ console.log(chalk.magenta(` Regulations: ${p.security.regulations.join(', ')}`));
457
+ }
458
+ }
459
+ if (prioritized.critical.length > 15) {
460
+ console.log(chalk.gray(` ... and ${prioritized.critical.length - 15} more critical items`));
461
+ }
462
+ console.log();
463
+ }
464
+ // High priority items (P2)
465
+ if (prioritized.high.length > 0) {
466
+ console.log(chalk.bold.yellow('⚠️ High Priority Items (P2):'));
467
+ for (const p of prioritized.high.slice(0, 10)) {
468
+ const sensitivityIcon = getSensitivityIcon(p.security.maxSensitivity);
469
+ console.log(` ${chalk.yellow('P2')} ${sensitivityIcon} ${chalk.white(p.accessPoint.table)}.${p.accessPoint.fields.join(', ') || '*'}`);
470
+ console.log(chalk.gray(` ${p.accessPoint.file}:${p.accessPoint.line}`));
471
+ }
472
+ if (prioritized.high.length > 10) {
473
+ console.log(chalk.gray(` ... and ${prioritized.high.length - 10} more high priority items`));
474
+ }
475
+ console.log();
476
+ }
477
+ // Sensitivity breakdown
478
+ console.log(chalk.bold('By Sensitivity Type:'));
479
+ if (summary.bySensitivity.credentials > 0) {
480
+ console.log(` ${chalk.red('🔑 Credentials:')} ${summary.bySensitivity.credentials}`);
481
+ }
482
+ if (summary.bySensitivity.financial > 0) {
483
+ console.log(` ${chalk.magenta('💰 Financial:')} ${summary.bySensitivity.financial}`);
484
+ }
485
+ if (summary.bySensitivity.health > 0) {
486
+ console.log(` ${chalk.blue('🏥 Health:')} ${summary.bySensitivity.health}`);
487
+ }
488
+ if (summary.bySensitivity.pii > 0) {
489
+ console.log(` ${chalk.yellow('👤 PII:')} ${summary.bySensitivity.pii}`);
490
+ }
491
+ console.log(` ${chalk.gray('❓ Unknown:')} ${summary.bySensitivity.unknown}`);
492
+ console.log();
493
+ }
494
+ /**
495
+ * Get icon for sensitivity type
496
+ */
497
+ function getSensitivityIcon(sensitivity) {
498
+ switch (sensitivity) {
499
+ case 'credentials': return chalk.red('🔑');
500
+ case 'financial': return chalk.magenta('💰');
501
+ case 'health': return chalk.blue('🏥');
502
+ case 'pii': return chalk.yellow('👤');
503
+ default: return chalk.gray('❓');
504
+ }
505
+ }
506
+ /**
507
+ * Reach subcommand - what data can this code reach?
508
+ */
509
+ async function reachAction(location, options) {
510
+ const rootDir = process.cwd();
511
+ const format = options.format ?? 'text';
512
+ const maxDepth = options.maxDepth ?? 10;
513
+ if (!(await callGraphExists(rootDir))) {
514
+ if (format === 'json') {
515
+ console.log(JSON.stringify({ error: 'No call graph found' }));
516
+ }
517
+ else {
518
+ showNotBuiltMessage();
519
+ }
520
+ return;
521
+ }
522
+ // Parse location: file:line or function name
523
+ let file;
524
+ let line;
525
+ let functionName;
526
+ if (location.includes(':')) {
527
+ const parts = location.split(':');
528
+ const filePart = parts[0];
529
+ const linePart = parts[1];
530
+ if (filePart && linePart) {
531
+ const parsedLine = parseInt(linePart, 10);
532
+ if (!isNaN(parsedLine)) {
533
+ file = filePart;
534
+ line = parsedLine;
535
+ }
536
+ else {
537
+ functionName = location;
538
+ }
539
+ }
540
+ else {
541
+ functionName = location;
542
+ }
543
+ }
544
+ else {
545
+ functionName = location;
546
+ }
547
+ const analyzer = createCallGraphAnalyzer({ rootDir });
548
+ await analyzer.initialize();
549
+ let result;
550
+ if (file !== undefined && line !== undefined) {
551
+ result = analyzer.getReachableData(file, line, { maxDepth });
552
+ }
553
+ else if (functionName) {
554
+ // Find function by name
555
+ const graph = analyzer.getGraph();
556
+ if (!graph) {
557
+ console.log(format === 'json'
558
+ ? JSON.stringify({ error: 'Failed to load call graph' })
559
+ : chalk.red('Failed to load call graph'));
560
+ return;
561
+ }
562
+ let funcId;
563
+ for (const [id, func] of graph.functions) {
564
+ if (func.name === functionName || func.qualifiedName === functionName) {
565
+ funcId = id;
566
+ break;
567
+ }
568
+ }
569
+ if (!funcId) {
570
+ console.log(format === 'json'
571
+ ? JSON.stringify({ error: `Function '${functionName}' not found` })
572
+ : chalk.red(`Function '${functionName}' not found`));
573
+ return;
574
+ }
575
+ result = analyzer.getReachableDataFromFunction(funcId, { maxDepth });
576
+ }
577
+ else {
578
+ console.log(format === 'json'
579
+ ? JSON.stringify({ error: 'Invalid location format. Use file:line or function_name' })
580
+ : chalk.red('Invalid location format. Use file:line or function_name'));
581
+ return;
582
+ }
583
+ // JSON output
584
+ if (format === 'json') {
585
+ console.log(JSON.stringify({
586
+ origin: result.origin,
587
+ tables: result.tables,
588
+ sensitiveFields: result.sensitiveFields.map(sf => ({
589
+ field: `${sf.field.table}.${sf.field.field}`,
590
+ type: sf.field.sensitivityType,
591
+ accessCount: sf.accessCount,
592
+ })),
593
+ maxDepth: result.maxDepth,
594
+ functionsTraversed: result.functionsTraversed,
595
+ accessPoints: result.reachableAccess.map(ra => ({
596
+ table: ra.access.table,
597
+ fields: ra.access.fields,
598
+ operation: ra.access.operation,
599
+ depth: ra.depth,
600
+ path: ra.path.map(p => p.functionName),
601
+ })),
602
+ }, null, 2));
603
+ return;
604
+ }
605
+ // Text output
606
+ console.log();
607
+ console.log(chalk.bold('🔎 Reachability Analysis'));
608
+ console.log(chalk.gray('─'.repeat(60)));
609
+ console.log();
610
+ console.log(`Origin: ${chalk.cyan(file ? `${file}:${line}` : functionName)}`);
611
+ console.log(`Tables Reachable: ${chalk.yellow(result.tables.join(', ') || 'none')}`);
612
+ console.log(`Functions Traversed: ${chalk.cyan(result.functionsTraversed)}`);
613
+ console.log(`Max Depth: ${chalk.cyan(result.maxDepth)}`);
614
+ console.log();
615
+ // Sensitive fields
616
+ if (result.sensitiveFields.length > 0) {
617
+ console.log(chalk.bold.yellow('⚠️ Sensitive Fields Accessible:'));
618
+ for (const sf of result.sensitiveFields) {
619
+ const typeColor = sf.field.sensitivityType === 'credentials' ? chalk.red :
620
+ sf.field.sensitivityType === 'pii' ? chalk.yellow :
621
+ sf.field.sensitivityType === 'financial' ? chalk.magenta : chalk.gray;
622
+ console.log(` ${typeColor('●')} ${sf.field.table}.${sf.field.field} (${sf.field.sensitivityType})`);
623
+ console.log(chalk.gray(` ${sf.accessCount} access point(s), ${sf.paths.length} path(s)`));
624
+ }
625
+ console.log();
626
+ }
627
+ // Access points
628
+ if (result.reachableAccess.length > 0) {
629
+ console.log(chalk.bold('Data Access Points:'));
630
+ for (const ra of result.reachableAccess.slice(0, 15)) {
631
+ const opColor = ra.access.operation === 'write' ? chalk.yellow :
632
+ ra.access.operation === 'delete' ? chalk.red : chalk.gray;
633
+ console.log(` ${opColor(ra.access.operation)} ${chalk.white(ra.access.table)}.${ra.access.fields.join(', ')}`);
634
+ console.log(chalk.gray(` Path: ${ra.path.map(p => p.functionName).join(' → ')}`));
635
+ }
636
+ if (result.reachableAccess.length > 15) {
637
+ console.log(chalk.gray(` ... and ${result.reachableAccess.length - 15} more`));
638
+ }
639
+ console.log();
640
+ }
641
+ else {
642
+ console.log(chalk.gray('No data access points found from this location.'));
643
+ console.log();
644
+ }
645
+ }
646
+ /**
647
+ * Inverse subcommand - who can reach this data?
648
+ */
649
+ async function inverseAction(target, options) {
650
+ const rootDir = process.cwd();
651
+ const format = options.format ?? 'text';
652
+ const maxDepth = options.maxDepth ?? 10;
653
+ if (!(await callGraphExists(rootDir))) {
654
+ if (format === 'json') {
655
+ console.log(JSON.stringify({ error: 'No call graph found' }));
656
+ }
657
+ else {
658
+ showNotBuiltMessage();
659
+ }
660
+ return;
661
+ }
662
+ // Parse target: table or table.field
663
+ const parts = target.split('.');
664
+ const table = parts[0] ?? '';
665
+ const field = parts.length > 1 ? parts.slice(1).join('.') : undefined;
666
+ const analyzer = createCallGraphAnalyzer({ rootDir });
667
+ await analyzer.initialize();
668
+ const result = analyzer.getCodePathsToData(field ? { table, field, maxDepth } : { table, maxDepth });
669
+ // JSON output
670
+ if (format === 'json') {
671
+ console.log(JSON.stringify({
672
+ target: result.target,
673
+ totalAccessors: result.totalAccessors,
674
+ entryPoints: result.entryPoints,
675
+ accessPaths: result.accessPaths.map(ap => ({
676
+ entryPoint: ap.entryPoint,
677
+ path: ap.path.map(p => p.functionName),
678
+ accessPoint: ap.accessPoint ? {
679
+ table: ap.accessPoint.table,
680
+ fields: ap.accessPoint.fields,
681
+ operation: ap.accessPoint.operation,
682
+ } : null,
683
+ })),
684
+ }, null, 2));
685
+ return;
686
+ }
687
+ // Text output
688
+ console.log();
689
+ console.log(chalk.bold('🔄 Inverse Reachability'));
690
+ console.log(chalk.gray('─'.repeat(60)));
691
+ console.log();
692
+ console.log(`Target: ${chalk.cyan(field ? `${table}.${field}` : table)}`);
693
+ console.log(`Direct Accessors: ${chalk.cyan(result.totalAccessors)}`);
694
+ console.log(`Entry Points That Can Reach: ${chalk.cyan(result.entryPoints.length)}`);
695
+ console.log();
696
+ if (result.accessPaths.length > 0) {
697
+ console.log(chalk.bold('Access Paths:'));
698
+ const graph = analyzer.getGraph();
699
+ for (const ap of result.accessPaths.slice(0, 10)) {
700
+ const entryFunc = graph?.functions.get(ap.entryPoint);
701
+ if (entryFunc) {
702
+ console.log(` ${chalk.magenta('🚪')} ${chalk.white(entryFunc.qualifiedName)}`);
703
+ console.log(chalk.gray(` Path: ${ap.path.map(p => p.functionName).join(' → ')}`));
704
+ }
705
+ }
706
+ if (result.accessPaths.length > 10) {
707
+ console.log(chalk.gray(` ... and ${result.accessPaths.length - 10} more paths`));
708
+ }
709
+ console.log();
710
+ }
711
+ else {
712
+ console.log(chalk.gray('No entry points can reach this data.'));
713
+ console.log();
714
+ }
715
+ }
716
+ /**
717
+ * Function subcommand - show details about a function
718
+ */
719
+ async function functionAction(name, options) {
720
+ const rootDir = process.cwd();
721
+ const format = options.format ?? 'text';
722
+ if (!(await callGraphExists(rootDir))) {
723
+ if (format === 'json') {
724
+ console.log(JSON.stringify({ error: 'No call graph found' }));
725
+ }
726
+ else {
727
+ showNotBuiltMessage();
728
+ }
729
+ return;
730
+ }
731
+ const analyzer = createCallGraphAnalyzer({ rootDir });
732
+ await analyzer.initialize();
733
+ const graph = analyzer.getGraph();
734
+ if (!graph) {
735
+ console.log(format === 'json'
736
+ ? JSON.stringify({ error: 'Failed to load call graph' })
737
+ : chalk.red('Failed to load call graph'));
738
+ return;
739
+ }
740
+ // Find function by name
741
+ let func;
742
+ for (const [, f] of graph.functions) {
743
+ if (f.name === name || f.qualifiedName === name || f.id.includes(name)) {
744
+ func = f;
745
+ break;
746
+ }
747
+ }
748
+ if (!func) {
749
+ console.log(format === 'json'
750
+ ? JSON.stringify({ error: `Function '${name}' not found` })
751
+ : chalk.red(`Function '${name}' not found`));
752
+ return;
753
+ }
754
+ // JSON output
755
+ if (format === 'json') {
756
+ console.log(JSON.stringify({
757
+ id: func.id,
758
+ name: func.name,
759
+ qualifiedName: func.qualifiedName,
760
+ file: func.file,
761
+ line: func.startLine,
762
+ language: func.language,
763
+ className: func.className,
764
+ isExported: func.isExported,
765
+ isAsync: func.isAsync,
766
+ parameters: func.parameters,
767
+ returnType: func.returnType,
768
+ calls: func.calls.map(c => ({
769
+ callee: c.calleeName,
770
+ resolved: c.resolved,
771
+ line: c.line,
772
+ })),
773
+ calledBy: func.calledBy.map(c => ({
774
+ caller: c.callerId,
775
+ line: c.line,
776
+ })),
777
+ dataAccess: func.dataAccess.map(d => ({
778
+ table: d.table,
779
+ fields: d.fields,
780
+ operation: d.operation,
781
+ })),
782
+ }, null, 2));
783
+ return;
784
+ }
785
+ // Text output
786
+ console.log();
787
+ console.log(chalk.bold(`📋 Function: ${func.qualifiedName}`));
788
+ console.log(chalk.gray('─'.repeat(60)));
789
+ console.log();
790
+ console.log(`File: ${chalk.cyan(func.file)}:${func.startLine}`);
791
+ console.log(`Language: ${chalk.cyan(func.language)}`);
792
+ if (func.className)
793
+ console.log(`Class: ${chalk.cyan(func.className)}`);
794
+ console.log(`Exported: ${func.isExported ? chalk.green('yes') : chalk.gray('no')}`);
795
+ console.log(`Async: ${func.isAsync ? chalk.green('yes') : chalk.gray('no')}`);
796
+ console.log();
797
+ // Parameters
798
+ if (func.parameters.length > 0) {
799
+ console.log(chalk.bold('Parameters:'));
800
+ for (const p of func.parameters) {
801
+ console.log(` ${chalk.white(p.name)}${p.type ? `: ${chalk.gray(p.type)}` : ''}`);
802
+ }
803
+ console.log();
804
+ }
805
+ // Calls
806
+ if (func.calls.length > 0) {
807
+ console.log(chalk.bold(`Calls (${func.calls.length}):`));
808
+ for (const c of func.calls.slice(0, 10)) {
809
+ const status = c.resolved ? chalk.green('✓') : chalk.gray('?');
810
+ console.log(` ${status} ${chalk.white(c.calleeName)} ${chalk.gray(`line ${c.line}`)}`);
811
+ }
812
+ if (func.calls.length > 10) {
813
+ console.log(chalk.gray(` ... and ${func.calls.length - 10} more`));
814
+ }
815
+ console.log();
816
+ }
817
+ // Called by
818
+ if (func.calledBy.length > 0) {
819
+ console.log(chalk.bold(`Called By (${func.calledBy.length}):`));
820
+ for (const c of func.calledBy.slice(0, 10)) {
821
+ const caller = graph.functions.get(c.callerId);
822
+ console.log(` ${chalk.white(caller?.qualifiedName ?? c.callerId)}`);
823
+ }
824
+ if (func.calledBy.length > 10) {
825
+ console.log(chalk.gray(` ... and ${func.calledBy.length - 10} more`));
826
+ }
827
+ console.log();
828
+ }
829
+ // Data access
830
+ if (func.dataAccess.length > 0) {
831
+ console.log(chalk.bold('Data Access:'));
832
+ for (const d of func.dataAccess) {
833
+ const opColor = d.operation === 'write' ? chalk.yellow :
834
+ d.operation === 'delete' ? chalk.red : chalk.gray;
835
+ console.log(` ${opColor(d.operation)} ${chalk.white(d.table)}.${d.fields.join(', ')}`);
836
+ }
837
+ console.log();
838
+ }
839
+ }
840
+ /**
841
+ * Impact subcommand - what breaks if I change this?
842
+ */
843
+ async function impactAction(target, options) {
844
+ const rootDir = process.cwd();
845
+ const format = options.format ?? 'text';
846
+ if (!(await callGraphExists(rootDir))) {
847
+ if (format === 'json') {
848
+ console.log(JSON.stringify({ error: 'No call graph found' }));
849
+ }
850
+ else {
851
+ showNotBuiltMessage();
852
+ }
853
+ return;
854
+ }
855
+ const spinner = format === 'text' ? createSpinner('Analyzing impact...') : null;
856
+ spinner?.start();
857
+ const analyzer = createCallGraphAnalyzer({ rootDir });
858
+ await analyzer.initialize();
859
+ const graph = analyzer.getGraph();
860
+ if (!graph) {
861
+ spinner?.stop();
862
+ console.log(format === 'json'
863
+ ? JSON.stringify({ error: 'Failed to load call graph' })
864
+ : chalk.red('Failed to load call graph'));
865
+ return;
866
+ }
867
+ const impactAnalyzer = createImpactAnalyzer(graph);
868
+ let result;
869
+ // Determine if target is a file or function
870
+ if (target.includes('/') || target.includes('.py') || target.includes('.ts') || target.includes('.js')) {
871
+ // It's a file path
872
+ result = impactAnalyzer.analyzeFile(target);
873
+ }
874
+ else {
875
+ // It's a function name
876
+ result = impactAnalyzer.analyzeFunctionByName(target);
877
+ }
878
+ spinner?.stop();
879
+ // JSON output
880
+ if (format === 'json') {
881
+ console.log(JSON.stringify({
882
+ target: result.target,
883
+ risk: result.risk,
884
+ riskScore: result.riskScore,
885
+ summary: result.summary,
886
+ affected: result.affected.slice(0, 50).map(a => ({
887
+ name: a.qualifiedName,
888
+ file: a.file,
889
+ line: a.line,
890
+ depth: a.depth,
891
+ isEntryPoint: a.isEntryPoint,
892
+ accessesSensitiveData: a.accessesSensitiveData,
893
+ path: a.pathToChange.map(p => p.functionName),
894
+ })),
895
+ entryPoints: result.entryPoints.map(e => ({
896
+ name: e.qualifiedName,
897
+ file: e.file,
898
+ line: e.line,
899
+ path: e.pathToChange.map(p => p.functionName),
900
+ })),
901
+ sensitiveDataPaths: result.sensitiveDataPaths.map(p => ({
902
+ table: p.table,
903
+ fields: p.fields,
904
+ operation: p.operation,
905
+ sensitivity: p.sensitivity,
906
+ entryPoint: p.entryPoint,
907
+ path: p.fullPath.map(n => n.functionName),
908
+ })),
909
+ }, null, 2));
910
+ return;
911
+ }
912
+ // Text output
913
+ console.log();
914
+ console.log(chalk.bold('💥 Impact Analysis'));
915
+ console.log(chalk.gray('─'.repeat(60)));
916
+ console.log();
917
+ // Target info
918
+ if (result.target.type === 'file') {
919
+ console.log(`Target: ${chalk.cyan(result.target.file)} (${result.changedFunctions.length} functions)`);
920
+ }
921
+ else {
922
+ console.log(`Target: ${chalk.cyan(result.target.functionName ?? result.target.functionId ?? 'unknown')}`);
923
+ }
924
+ console.log();
925
+ // Risk assessment
926
+ const riskColor = result.risk === 'critical' ? chalk.bgRed.white :
927
+ result.risk === 'high' ? chalk.red :
928
+ result.risk === 'medium' ? chalk.yellow : chalk.green;
929
+ console.log(`Risk Level: ${riskColor(` ${result.risk.toUpperCase()} `)} (score: ${result.riskScore}/100)`);
930
+ console.log();
931
+ // Summary
932
+ console.log(chalk.bold('Summary:'));
933
+ console.log(` Direct Callers: ${chalk.cyan(result.summary.directCallers)}`);
934
+ console.log(` Transitive Callers: ${chalk.cyan(result.summary.transitiveCallers)}`);
935
+ console.log(` Affected Entry Points: ${chalk.yellow(result.summary.affectedEntryPoints)}`);
936
+ console.log(` Sensitive Data Paths: ${chalk.red(result.summary.affectedDataPaths)}`);
937
+ console.log(` Max Call Depth: ${chalk.gray(result.summary.maxDepth)}`);
938
+ console.log();
939
+ // Entry points affected
940
+ if (result.entryPoints.length > 0) {
941
+ console.log(chalk.bold.yellow('🚪 Affected Entry Points (User-Facing Impact):'));
942
+ for (const ep of result.entryPoints.slice(0, 10)) {
943
+ console.log(` ${chalk.magenta('●')} ${chalk.white(ep.qualifiedName)}`);
944
+ console.log(chalk.gray(` ${ep.file}:${ep.line}`));
945
+ console.log(chalk.gray(` Path: ${ep.pathToChange.map(p => p.functionName).join(' → ')}`));
946
+ }
947
+ if (result.entryPoints.length > 10) {
948
+ console.log(chalk.gray(` ... and ${result.entryPoints.length - 10} more entry points`));
949
+ }
950
+ console.log();
951
+ }
952
+ // Sensitive data paths
953
+ if (result.sensitiveDataPaths.length > 0) {
954
+ console.log(chalk.bold.red('🔒 Sensitive Data Paths Affected:'));
955
+ for (const dp of result.sensitiveDataPaths.slice(0, 10)) {
956
+ const sensitivityIcon = dp.sensitivity === 'credentials' ? '🔑' :
957
+ dp.sensitivity === 'financial' ? '💰' :
958
+ dp.sensitivity === 'health' ? '🏥' : '👤';
959
+ const sensitivityColor = dp.sensitivity === 'credentials' ? chalk.red :
960
+ dp.sensitivity === 'financial' ? chalk.magenta :
961
+ dp.sensitivity === 'health' ? chalk.blue : chalk.yellow;
962
+ console.log(` ${sensitivityIcon} ${sensitivityColor(dp.sensitivity)} ${chalk.white(dp.table)}.${dp.fields.join(', ')}`);
963
+ console.log(chalk.gray(` Entry: ${dp.entryPoint}`));
964
+ console.log(chalk.gray(` Path: ${dp.fullPath.map(n => n.functionName).join(' → ')}`));
965
+ }
966
+ if (result.sensitiveDataPaths.length > 10) {
967
+ console.log(chalk.gray(` ... and ${result.sensitiveDataPaths.length - 10} more sensitive paths`));
968
+ }
969
+ console.log();
970
+ }
971
+ // Direct callers
972
+ const directCallers = result.affected.filter(a => a.depth === 1);
973
+ if (directCallers.length > 0) {
974
+ console.log(chalk.bold('📞 Direct Callers (Immediate Impact):'));
975
+ for (const caller of directCallers.slice(0, 10)) {
976
+ const icon = caller.accessesSensitiveData ? chalk.red('●') : chalk.gray('○');
977
+ console.log(` ${icon} ${chalk.white(caller.qualifiedName)}`);
978
+ console.log(chalk.gray(` ${caller.file}:${caller.line}`));
979
+ }
980
+ if (directCallers.length > 10) {
981
+ console.log(chalk.gray(` ... and ${directCallers.length - 10} more direct callers`));
982
+ }
983
+ console.log();
984
+ }
985
+ // Transitive callers (depth > 1)
986
+ const transitiveCallers = result.affected.filter(a => a.depth > 1);
987
+ if (transitiveCallers.length > 0) {
988
+ console.log(chalk.bold('🔗 Transitive Callers (Ripple Effect):'));
989
+ for (const caller of transitiveCallers.slice(0, 8)) {
990
+ const depthIndicator = chalk.gray(`[depth ${caller.depth}]`);
991
+ console.log(` ${depthIndicator} ${chalk.white(caller.qualifiedName)}`);
992
+ }
993
+ if (transitiveCallers.length > 8) {
994
+ console.log(chalk.gray(` ... and ${transitiveCallers.length - 8} more transitive callers`));
995
+ }
996
+ console.log();
997
+ }
998
+ // Recommendations
999
+ if (result.risk === 'critical' || result.risk === 'high') {
1000
+ console.log(chalk.bold('⚠️ Recommendations:'));
1001
+ if (result.sensitiveDataPaths.length > 0) {
1002
+ console.log(chalk.yellow(' • Review all sensitive data paths before merging'));
1003
+ }
1004
+ if (result.entryPoints.length > 5) {
1005
+ console.log(chalk.yellow(' • Consider incremental rollout - many entry points affected'));
1006
+ }
1007
+ if (result.summary.maxDepth > 5) {
1008
+ console.log(chalk.yellow(' • Deep call chain - test thoroughly for regressions'));
1009
+ }
1010
+ console.log();
1011
+ }
1012
+ }
1013
+ /**
1014
+ * Dead code subcommand - find unused functions
1015
+ */
1016
+ async function deadCodeAction(options) {
1017
+ const rootDir = process.cwd();
1018
+ const format = options.format ?? 'text';
1019
+ const minConfidence = (options.confidence ?? 'low');
1020
+ const includeExported = options.includeExported ?? false;
1021
+ const includeTests = options.includeTests ?? false;
1022
+ if (!(await callGraphExists(rootDir))) {
1023
+ if (format === 'json') {
1024
+ console.log(JSON.stringify({ error: 'No call graph found' }));
1025
+ }
1026
+ else {
1027
+ showNotBuiltMessage();
1028
+ }
1029
+ return;
1030
+ }
1031
+ const spinner = format === 'text' ? createSpinner('Detecting dead code...') : null;
1032
+ spinner?.start();
1033
+ const analyzer = createCallGraphAnalyzer({ rootDir });
1034
+ await analyzer.initialize();
1035
+ const graph = analyzer.getGraph();
1036
+ if (!graph) {
1037
+ spinner?.stop();
1038
+ console.log(format === 'json'
1039
+ ? JSON.stringify({ error: 'Failed to load call graph' })
1040
+ : chalk.red('Failed to load call graph'));
1041
+ return;
1042
+ }
1043
+ const detector = createDeadCodeDetector(graph);
1044
+ const result = detector.detect({
1045
+ minConfidence,
1046
+ includeExported,
1047
+ includeTests,
1048
+ });
1049
+ spinner?.stop();
1050
+ // JSON output
1051
+ if (format === 'json') {
1052
+ console.log(JSON.stringify({
1053
+ summary: result.summary,
1054
+ candidates: result.candidates.slice(0, 100).map(c => ({
1055
+ name: c.qualifiedName,
1056
+ file: c.file,
1057
+ line: c.line,
1058
+ confidence: c.confidence,
1059
+ linesOfCode: c.linesOfCode,
1060
+ possibleFalsePositives: c.possibleFalsePositives,
1061
+ hasDataAccess: c.hasDataAccess,
1062
+ })),
1063
+ excluded: result.excluded,
1064
+ }, null, 2));
1065
+ return;
1066
+ }
1067
+ // Text output
1068
+ console.log();
1069
+ console.log(chalk.bold('🗑️ Dead Code Detection'));
1070
+ console.log(chalk.gray('─'.repeat(60)));
1071
+ console.log();
1072
+ // Summary
1073
+ const { summary } = result;
1074
+ console.log(chalk.bold('Summary:'));
1075
+ console.log(` Total Functions: ${chalk.cyan(summary.totalFunctions)}`);
1076
+ console.log(` Dead Code Candidates: ${chalk.yellow(summary.deadCandidates)}`);
1077
+ console.log(` Estimated Dead Lines: ${chalk.red(summary.estimatedDeadLines)}`);
1078
+ console.log();
1079
+ // By confidence
1080
+ console.log(chalk.bold('By Confidence:'));
1081
+ console.log(` ${chalk.red('🔴 High:')} ${summary.highConfidence} (safe to remove)`);
1082
+ console.log(` ${chalk.yellow('🟡 Medium:')} ${summary.mediumConfidence} (review first)`);
1083
+ console.log(` ${chalk.gray('⚪ Low:')} ${summary.lowConfidence} (might be false positive)`);
1084
+ console.log();
1085
+ // Excluded
1086
+ console.log(chalk.bold('Excluded from Analysis:'));
1087
+ console.log(` Entry Points: ${chalk.gray(result.excluded.entryPoints)}`);
1088
+ console.log(` Functions with Callers: ${chalk.gray(result.excluded.withCallers)}`);
1089
+ console.log(` Framework Hooks: ${chalk.gray(result.excluded.frameworkHooks)}`);
1090
+ console.log();
1091
+ // High confidence candidates
1092
+ const highConf = result.candidates.filter(c => c.confidence === 'high');
1093
+ if (highConf.length > 0) {
1094
+ console.log(chalk.bold.red('🔴 High Confidence (Safe to Remove):'));
1095
+ for (const c of highConf.slice(0, 15)) {
1096
+ console.log(` ${chalk.white(c.qualifiedName)} ${chalk.gray(`(${c.linesOfCode} lines)`)}`);
1097
+ console.log(chalk.gray(` ${c.file}:${c.line}`));
1098
+ }
1099
+ if (highConf.length > 15) {
1100
+ console.log(chalk.gray(` ... and ${highConf.length - 15} more`));
1101
+ }
1102
+ console.log();
1103
+ }
1104
+ // Medium confidence candidates
1105
+ const medConf = result.candidates.filter(c => c.confidence === 'medium');
1106
+ if (medConf.length > 0) {
1107
+ console.log(chalk.bold.yellow('🟡 Medium Confidence (Review First):'));
1108
+ for (const c of medConf.slice(0, 10)) {
1109
+ const reasons = c.possibleFalsePositives.slice(0, 2).join(', ');
1110
+ console.log(` ${chalk.white(c.qualifiedName)} ${chalk.gray(`(${c.linesOfCode} lines)`)}`);
1111
+ console.log(chalk.gray(` ${c.file}:${c.line}`));
1112
+ if (reasons) {
1113
+ console.log(chalk.gray(` Might be: ${reasons}`));
1114
+ }
1115
+ }
1116
+ if (medConf.length > 10) {
1117
+ console.log(chalk.gray(` ... and ${medConf.length - 10} more`));
1118
+ }
1119
+ console.log();
1120
+ }
1121
+ // Files with most dead code
1122
+ if (summary.byFile.length > 0) {
1123
+ console.log(chalk.bold('Files with Most Dead Code:'));
1124
+ for (const f of summary.byFile.slice(0, 10)) {
1125
+ console.log(` ${chalk.cyan(f.count)} functions (${f.lines} lines): ${chalk.white(f.file)}`);
1126
+ }
1127
+ console.log();
1128
+ }
1129
+ // Recommendations
1130
+ if (summary.highConfidence > 0) {
1131
+ console.log(chalk.bold('💡 Recommendations:'));
1132
+ console.log(chalk.green(` • ${summary.highConfidence} functions can likely be safely removed`));
1133
+ console.log(chalk.green(` • This would remove ~${summary.estimatedDeadLines} lines of code`));
1134
+ if (summary.mediumConfidence > 0) {
1135
+ console.log(chalk.yellow(` • Review ${summary.mediumConfidence} medium-confidence candidates before removing`));
1136
+ }
1137
+ console.log();
1138
+ }
1139
+ }
1140
+ /**
1141
+ * Coverage subcommand - analyze test coverage for sensitive data access
1142
+ */
1143
+ async function coverageAction(options) {
1144
+ const rootDir = process.cwd();
1145
+ const format = options.format ?? 'text';
1146
+ const showSensitive = options.sensitive ?? true;
1147
+ if (!(await callGraphExists(rootDir))) {
1148
+ if (format === 'json') {
1149
+ console.log(JSON.stringify({ error: 'No call graph found' }));
1150
+ }
1151
+ else {
1152
+ showNotBuiltMessage();
1153
+ }
1154
+ return;
1155
+ }
1156
+ const spinner = format === 'text' ? createSpinner('Analyzing test coverage for sensitive data...') : null;
1157
+ spinner?.start();
1158
+ const analyzer = createCallGraphAnalyzer({ rootDir });
1159
+ await analyzer.initialize();
1160
+ const graph = analyzer.getGraph();
1161
+ if (!graph) {
1162
+ spinner?.stop();
1163
+ console.log(format === 'json'
1164
+ ? JSON.stringify({ error: 'Failed to load call graph' })
1165
+ : chalk.red('Failed to load call graph'));
1166
+ return;
1167
+ }
1168
+ const coverageAnalyzer = createCoverageAnalyzer(graph);
1169
+ const result = coverageAnalyzer.analyze();
1170
+ spinner?.stop();
1171
+ // JSON output
1172
+ if (format === 'json') {
1173
+ console.log(JSON.stringify({
1174
+ summary: result.summary,
1175
+ fields: result.fields.map(f => ({
1176
+ field: f.fullName,
1177
+ sensitivity: f.sensitivity,
1178
+ totalPaths: f.totalPaths,
1179
+ testedPaths: f.testedPaths,
1180
+ coveragePercent: f.coveragePercent,
1181
+ status: f.status,
1182
+ })),
1183
+ uncoveredPaths: result.uncoveredPaths.slice(0, 50).map(p => ({
1184
+ field: `${p.table}.${p.field}`,
1185
+ sensitivity: p.sensitivity,
1186
+ entryPoint: p.entryPoint.name,
1187
+ accessor: p.accessor.name,
1188
+ depth: p.depth,
1189
+ })),
1190
+ testFiles: result.testFiles,
1191
+ testFunctions: result.testFunctions,
1192
+ }, null, 2));
1193
+ return;
1194
+ }
1195
+ // Text output
1196
+ console.log();
1197
+ console.log(chalk.bold('🧪 Sensitive Data Test Coverage'));
1198
+ console.log(chalk.gray('─'.repeat(60)));
1199
+ console.log();
1200
+ // Summary
1201
+ const { summary } = result;
1202
+ console.log(chalk.bold('Summary:'));
1203
+ console.log(` Sensitive Fields: ${chalk.cyan(summary.totalSensitiveFields)}`);
1204
+ console.log(` Access Paths: ${chalk.cyan(summary.totalAccessPaths)}`);
1205
+ console.log(` Tested Paths: ${chalk.green(summary.testedAccessPaths)}`);
1206
+ console.log(` Coverage: ${getCoverageColor(summary.coveragePercent)(`${summary.coveragePercent}%`)}`);
1207
+ console.log(` Test Files: ${chalk.gray(result.testFiles.length)}`);
1208
+ console.log(` Test Functions: ${chalk.gray(result.testFunctions)}`);
1209
+ console.log();
1210
+ // By sensitivity
1211
+ console.log(chalk.bold('Coverage by Sensitivity:'));
1212
+ const sensOrder = ['credentials', 'financial', 'health', 'pii'];
1213
+ for (const sens of sensOrder) {
1214
+ const s = summary.bySensitivity[sens];
1215
+ if (s.fields > 0) {
1216
+ const icon = sens === 'credentials' ? '🔑' :
1217
+ sens === 'financial' ? '💰' :
1218
+ sens === 'health' ? '🏥' : '👤';
1219
+ const color = getCoverageColor(s.coveragePercent);
1220
+ console.log(` ${icon} ${chalk.white(sens)}: ${color(`${s.coveragePercent}%`)} (${s.testedPaths}/${s.paths} paths)`);
1221
+ }
1222
+ }
1223
+ console.log();
1224
+ // Field coverage
1225
+ if (result.fields.length > 0 && showSensitive) {
1226
+ console.log(chalk.bold('Field Coverage:'));
1227
+ for (const f of result.fields.slice(0, 20)) {
1228
+ const statusIcon = f.status === 'covered' ? chalk.green('✓') :
1229
+ f.status === 'partial' ? chalk.yellow('◐') :
1230
+ chalk.red('✗');
1231
+ const coverageColor = getCoverageColor(f.coveragePercent);
1232
+ const sensIcon = f.sensitivity === 'credentials' ? '🔑' :
1233
+ f.sensitivity === 'financial' ? '💰' :
1234
+ f.sensitivity === 'health' ? '🏥' : '👤';
1235
+ console.log(` ${statusIcon} ${sensIcon} ${chalk.white(f.fullName)}: ${coverageColor(`${f.testedPaths}/${f.totalPaths}`)} paths tested`);
1236
+ }
1237
+ if (result.fields.length > 20) {
1238
+ console.log(chalk.gray(` ... and ${result.fields.length - 20} more fields`));
1239
+ }
1240
+ console.log();
1241
+ }
1242
+ // Uncovered paths (highest priority)
1243
+ const uncoveredByCredentials = result.uncoveredPaths.filter(p => p.sensitivity === 'credentials');
1244
+ const uncoveredByFinancial = result.uncoveredPaths.filter(p => p.sensitivity === 'financial');
1245
+ const uncoveredByHealth = result.uncoveredPaths.filter(p => p.sensitivity === 'health');
1246
+ const uncoveredByPii = result.uncoveredPaths.filter(p => p.sensitivity === 'pii');
1247
+ if (uncoveredByCredentials.length > 0) {
1248
+ console.log(chalk.bold.red('🔑 Untested Credential Access Paths:'));
1249
+ for (const p of uncoveredByCredentials.slice(0, 5)) {
1250
+ console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
1251
+ console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
1252
+ console.log(chalk.gray(` ${p.entryPoint.file}:${p.entryPoint.line}`));
1253
+ }
1254
+ if (uncoveredByCredentials.length > 5) {
1255
+ console.log(chalk.gray(` ... and ${uncoveredByCredentials.length - 5} more`));
1256
+ }
1257
+ console.log();
1258
+ }
1259
+ if (uncoveredByFinancial.length > 0) {
1260
+ console.log(chalk.bold.magenta('💰 Untested Financial Data Paths:'));
1261
+ for (const p of uncoveredByFinancial.slice(0, 5)) {
1262
+ console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
1263
+ console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
1264
+ }
1265
+ if (uncoveredByFinancial.length > 5) {
1266
+ console.log(chalk.gray(` ... and ${uncoveredByFinancial.length - 5} more`));
1267
+ }
1268
+ console.log();
1269
+ }
1270
+ if (uncoveredByHealth.length > 0) {
1271
+ console.log(chalk.bold.blue('🏥 Untested Health Data Paths:'));
1272
+ for (const p of uncoveredByHealth.slice(0, 3)) {
1273
+ console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
1274
+ console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
1275
+ }
1276
+ if (uncoveredByHealth.length > 3) {
1277
+ console.log(chalk.gray(` ... and ${uncoveredByHealth.length - 3} more`));
1278
+ }
1279
+ console.log();
1280
+ }
1281
+ if (uncoveredByPii.length > 0) {
1282
+ console.log(chalk.bold.yellow('👤 Untested PII Access Paths:'));
1283
+ for (const p of uncoveredByPii.slice(0, 3)) {
1284
+ console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
1285
+ console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
1286
+ }
1287
+ if (uncoveredByPii.length > 3) {
1288
+ console.log(chalk.gray(` ... and ${uncoveredByPii.length - 3} more`));
1289
+ }
1290
+ console.log();
1291
+ }
1292
+ // Recommendations
1293
+ if (result.uncoveredPaths.length > 0) {
1294
+ console.log(chalk.bold('💡 Recommendations:'));
1295
+ if (uncoveredByCredentials.length > 0) {
1296
+ console.log(chalk.red(` • ${uncoveredByCredentials.length} credential access paths need tests (highest priority)`));
1297
+ }
1298
+ if (uncoveredByFinancial.length > 0) {
1299
+ console.log(chalk.magenta(` • ${uncoveredByFinancial.length} financial data paths need tests`));
1300
+ }
1301
+ if (summary.coveragePercent < 50) {
1302
+ console.log(chalk.yellow(` • Overall coverage is ${summary.coveragePercent}% - consider adding integration tests`));
1303
+ }
1304
+ console.log();
1305
+ }
1306
+ else {
1307
+ console.log(chalk.green('✓ All sensitive data access paths are covered by tests!'));
1308
+ console.log();
1309
+ }
1310
+ }
1311
+ /**
1312
+ * Get color function based on coverage percentage
1313
+ */
1314
+ function getCoverageColor(percent) {
1315
+ if (percent >= 80)
1316
+ return chalk.green;
1317
+ if (percent >= 50)
1318
+ return chalk.yellow;
1319
+ return chalk.red;
1320
+ }
1321
+ /**
1322
+ * Create the callgraph command with subcommands
1323
+ */
1324
+ export const callgraphCommand = new Command('callgraph')
1325
+ .description('Build and query call graphs for code reachability analysis')
1326
+ .option('--verbose', 'Enable verbose output')
1327
+ .action(statusAction);
1328
+ // Subcommands
1329
+ callgraphCommand
1330
+ .command('build')
1331
+ .description('Build the call graph from source files')
1332
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1333
+ .action(buildAction);
1334
+ callgraphCommand
1335
+ .command('status')
1336
+ .description('Show call graph overview and statistics')
1337
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1338
+ .option('-s, --security', 'Show security-prioritized view (P0-P4 tiers)')
1339
+ .action(statusAction);
1340
+ callgraphCommand
1341
+ .command('reach <location>')
1342
+ .description('What data can this code reach? (file:line or function_name)')
1343
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1344
+ .option('-d, --max-depth <depth>', 'Maximum traversal depth', '10')
1345
+ .action((location, opts) => reachAction(location, { ...opts, maxDepth: parseInt(opts.maxDepth, 10) }));
1346
+ callgraphCommand
1347
+ .command('inverse <target>')
1348
+ .description('Who can reach this data? (table or table.field)')
1349
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1350
+ .option('-d, --max-depth <depth>', 'Maximum traversal depth', '10')
1351
+ .action((target, opts) => inverseAction(target, { ...opts, maxDepth: parseInt(opts.maxDepth, 10) }));
1352
+ callgraphCommand
1353
+ .command('function <name>')
1354
+ .description('Show details about a specific function')
1355
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1356
+ .action(functionAction);
1357
+ callgraphCommand
1358
+ .command('impact <target>')
1359
+ .description('What breaks if I change this? (file path or function name)')
1360
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1361
+ .action(impactAction);
1362
+ callgraphCommand
1363
+ .command('dead')
1364
+ .description('Find dead code (functions never called)')
1365
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1366
+ .option('-c, --confidence <level>', 'Minimum confidence (high, medium, low)', 'low')
1367
+ .option('--include-exported', 'Include exported functions (might be used externally)')
1368
+ .option('--include-tests', 'Include test files')
1369
+ .action(deadCodeAction);
1370
+ callgraphCommand
1371
+ .command('coverage')
1372
+ .description('Analyze test coverage for sensitive data access paths')
1373
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
1374
+ .option('--sensitive', 'Show sensitive field details (default: true)')
1375
+ .action(coverageAction);
1376
+ //# sourceMappingURL=callgraph.js.map