driftdetect-mcp 0.4.0 โ 0.4.2
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/server.d.ts +12 -2
- package/dist/bin/server.d.ts.map +1 -1
- package/dist/bin/server.js +25 -5
- package/dist/bin/server.js.map +1 -1
- package/dist/enterprise-server.d.ts +78 -0
- package/dist/enterprise-server.d.ts.map +1 -0
- package/dist/enterprise-server.js +201 -0
- package/dist/enterprise-server.js.map +1 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/cache.d.ts +86 -0
- package/dist/infrastructure/cache.d.ts.map +1 -0
- package/dist/infrastructure/cache.js +271 -0
- package/dist/infrastructure/cache.js.map +1 -0
- package/dist/infrastructure/cursor-manager.d.ts +86 -0
- package/dist/infrastructure/cursor-manager.d.ts.map +1 -0
- package/dist/infrastructure/cursor-manager.js +175 -0
- package/dist/infrastructure/cursor-manager.js.map +1 -0
- package/dist/infrastructure/error-handler.d.ts +82 -0
- package/dist/infrastructure/error-handler.d.ts.map +1 -0
- package/dist/infrastructure/error-handler.js +226 -0
- package/dist/infrastructure/error-handler.js.map +1 -0
- package/dist/infrastructure/index.d.ts +19 -0
- package/dist/infrastructure/index.d.ts.map +1 -0
- package/dist/infrastructure/index.js +26 -0
- package/dist/infrastructure/index.js.map +1 -0
- package/dist/infrastructure/metrics.d.ts +104 -0
- package/dist/infrastructure/metrics.d.ts.map +1 -0
- package/dist/infrastructure/metrics.js +291 -0
- package/dist/infrastructure/metrics.js.map +1 -0
- package/dist/infrastructure/rate-limiter.d.ts +59 -0
- package/dist/infrastructure/rate-limiter.d.ts.map +1 -0
- package/dist/infrastructure/rate-limiter.js +132 -0
- package/dist/infrastructure/rate-limiter.js.map +1 -0
- package/dist/infrastructure/response-builder.d.ts +104 -0
- package/dist/infrastructure/response-builder.d.ts.map +1 -0
- package/dist/infrastructure/response-builder.js +207 -0
- package/dist/infrastructure/response-builder.js.map +1 -0
- package/dist/infrastructure/token-estimator.d.ts +48 -0
- package/dist/infrastructure/token-estimator.d.ts.map +1 -0
- package/dist/infrastructure/token-estimator.js +131 -0
- package/dist/infrastructure/token-estimator.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1074 -17
- package/dist/server.js.map +1 -1
- package/dist/tools/detail/code-examples.d.ts +33 -0
- package/dist/tools/detail/code-examples.d.ts.map +1 -0
- package/dist/tools/detail/code-examples.js +126 -0
- package/dist/tools/detail/code-examples.js.map +1 -0
- package/dist/tools/detail/dna-check.d.ts +32 -0
- package/dist/tools/detail/dna-check.d.ts.map +1 -0
- package/dist/tools/detail/dna-check.js +231 -0
- package/dist/tools/detail/dna-check.js.map +1 -0
- package/dist/tools/detail/dna-profile.d.ts +37 -0
- package/dist/tools/detail/dna-profile.d.ts.map +1 -0
- package/dist/tools/detail/dna-profile.js +101 -0
- package/dist/tools/detail/dna-profile.js.map +1 -0
- package/dist/tools/detail/file-patterns.d.ts +39 -0
- package/dist/tools/detail/file-patterns.d.ts.map +1 -0
- package/dist/tools/detail/file-patterns.js +103 -0
- package/dist/tools/detail/file-patterns.js.map +1 -0
- package/dist/tools/detail/files-list.d.ts +30 -0
- package/dist/tools/detail/files-list.d.ts.map +1 -0
- package/dist/tools/detail/files-list.js +99 -0
- package/dist/tools/detail/files-list.js.map +1 -0
- package/dist/tools/detail/impact-analysis.d.ts +53 -0
- package/dist/tools/detail/impact-analysis.d.ts.map +1 -0
- package/dist/tools/detail/impact-analysis.js +130 -0
- package/dist/tools/detail/impact-analysis.js.map +1 -0
- package/dist/tools/detail/index.d.ts +23 -0
- package/dist/tools/detail/index.d.ts.map +1 -0
- package/dist/tools/detail/index.js +200 -0
- package/dist/tools/detail/index.js.map +1 -0
- package/dist/tools/detail/pattern-get.d.ts +45 -0
- package/dist/tools/detail/pattern-get.d.ts.map +1 -0
- package/dist/tools/detail/pattern-get.js +87 -0
- package/dist/tools/detail/pattern-get.js.map +1 -0
- package/dist/tools/detail/reachability.d.ts +60 -0
- package/dist/tools/detail/reachability.d.ts.map +1 -0
- package/dist/tools/detail/reachability.js +168 -0
- package/dist/tools/detail/reachability.js.map +1 -0
- package/dist/tools/discovery/capabilities.d.ts +28 -0
- package/dist/tools/discovery/capabilities.d.ts.map +1 -0
- package/dist/tools/discovery/capabilities.js +112 -0
- package/dist/tools/discovery/capabilities.js.map +1 -0
- package/dist/tools/discovery/index.d.ts +13 -0
- package/dist/tools/discovery/index.d.ts.map +1 -0
- package/dist/tools/discovery/index.js +30 -0
- package/dist/tools/discovery/index.js.map +1 -0
- package/dist/tools/discovery/projects.d.ts +26 -0
- package/dist/tools/discovery/projects.d.ts.map +1 -0
- package/dist/tools/discovery/projects.js +210 -0
- package/dist/tools/discovery/projects.js.map +1 -0
- package/dist/tools/discovery/status.d.ts +42 -0
- package/dist/tools/discovery/status.d.ts.map +1 -0
- package/dist/tools/discovery/status.js +157 -0
- package/dist/tools/discovery/status.js.map +1 -0
- package/dist/tools/exploration/contracts-list.d.ts +35 -0
- package/dist/tools/exploration/contracts-list.d.ts.map +1 -0
- package/dist/tools/exploration/contracts-list.js +106 -0
- package/dist/tools/exploration/contracts-list.js.map +1 -0
- package/dist/tools/exploration/files-list.d.ts +29 -0
- package/dist/tools/exploration/files-list.d.ts.map +1 -0
- package/dist/tools/exploration/files-list.js +94 -0
- package/dist/tools/exploration/files-list.js.map +1 -0
- package/dist/tools/exploration/index.d.ts +17 -0
- package/dist/tools/exploration/index.d.ts.map +1 -0
- package/dist/tools/exploration/index.js +126 -0
- package/dist/tools/exploration/index.js.map +1 -0
- package/dist/tools/exploration/patterns-list.d.ts +40 -0
- package/dist/tools/exploration/patterns-list.d.ts.map +1 -0
- package/dist/tools/exploration/patterns-list.js +172 -0
- package/dist/tools/exploration/patterns-list.js.map +1 -0
- package/dist/tools/exploration/security-summary.d.ts +49 -0
- package/dist/tools/exploration/security-summary.d.ts.map +1 -0
- package/dist/tools/exploration/security-summary.js +111 -0
- package/dist/tools/exploration/security-summary.js.map +1 -0
- package/dist/tools/exploration/trends.d.ts +49 -0
- package/dist/tools/exploration/trends.d.ts.map +1 -0
- package/dist/tools/exploration/trends.js +147 -0
- package/dist/tools/exploration/trends.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +13 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/orchestration/context.d.ts +72 -0
- package/dist/tools/orchestration/context.d.ts.map +1 -0
- package/dist/tools/orchestration/context.js +499 -0
- package/dist/tools/orchestration/context.js.map +1 -0
- package/dist/tools/orchestration/index.d.ts +11 -0
- package/dist/tools/orchestration/index.d.ts.map +1 -0
- package/dist/tools/orchestration/index.js +56 -0
- package/dist/tools/orchestration/index.js.map +1 -0
- package/dist/tools/registry.d.ts +41 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/registry.js.map +1 -0
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
-
import { PatternStore, ManifestStore, HistoryStore, DNAStore, PlaybookGenerator, AIContextBuilder, GENE_IDS, BoundaryStore, } from 'driftdetect-core';
|
|
8
|
+
import { PatternStore, ManifestStore, HistoryStore, DNAStore, PlaybookGenerator, AIContextBuilder, GENE_IDS, BoundaryStore, createCallGraphAnalyzer, createBoundaryScanner, createSemanticDataAccessScanner, createImpactAnalyzer, createDeadCodeDetector, createCoverageAnalyzer, } from 'driftdetect-core';
|
|
9
9
|
import { PackManager } from './packs.js';
|
|
10
10
|
import { FeedbackManager } from './feedback.js';
|
|
11
|
+
import { handleProjects } from './tools/discovery/projects.js';
|
|
11
12
|
const PATTERN_CATEGORIES = [
|
|
12
13
|
'structural', 'components', 'styling', 'api', 'auth', 'errors',
|
|
13
14
|
'data-access', 'testing', 'logging', 'security', 'config',
|
|
@@ -38,6 +39,14 @@ const TOOLS = [
|
|
|
38
39
|
type: 'number',
|
|
39
40
|
description: 'Minimum confidence score (0.0-1.0)',
|
|
40
41
|
},
|
|
42
|
+
limit: {
|
|
43
|
+
type: 'number',
|
|
44
|
+
description: 'Maximum patterns to return (default: 30)',
|
|
45
|
+
},
|
|
46
|
+
includeExamples: {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
description: 'Include example locations (default: true, set false for compact output)',
|
|
49
|
+
},
|
|
41
50
|
},
|
|
42
51
|
required: [],
|
|
43
52
|
},
|
|
@@ -56,6 +65,18 @@ const TOOLS = [
|
|
|
56
65
|
type: 'string',
|
|
57
66
|
description: 'Filter by category',
|
|
58
67
|
},
|
|
68
|
+
limit: {
|
|
69
|
+
type: 'number',
|
|
70
|
+
description: 'Maximum patterns to return (default: 20)',
|
|
71
|
+
},
|
|
72
|
+
offset: {
|
|
73
|
+
type: 'number',
|
|
74
|
+
description: 'Skip first N patterns for pagination (default: 0)',
|
|
75
|
+
},
|
|
76
|
+
compact: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
description: 'Return summary only without full pattern details (default: false)',
|
|
79
|
+
},
|
|
59
80
|
},
|
|
60
81
|
required: ['path'],
|
|
61
82
|
},
|
|
@@ -74,6 +95,14 @@ const TOOLS = [
|
|
|
74
95
|
type: 'string',
|
|
75
96
|
description: 'Filter by category',
|
|
76
97
|
},
|
|
98
|
+
limit: {
|
|
99
|
+
type: 'number',
|
|
100
|
+
description: 'Maximum patterns to return (default: 10)',
|
|
101
|
+
},
|
|
102
|
+
maxLocations: {
|
|
103
|
+
type: 'number',
|
|
104
|
+
description: 'Maximum locations per pattern (default: 5)',
|
|
105
|
+
},
|
|
77
106
|
},
|
|
78
107
|
required: ['pattern'],
|
|
79
108
|
},
|
|
@@ -394,6 +423,83 @@ const TOOLS = [
|
|
|
394
423
|
type: 'boolean',
|
|
395
424
|
description: 'Include boundary violations in response (default: true)',
|
|
396
425
|
},
|
|
426
|
+
limit: {
|
|
427
|
+
type: 'number',
|
|
428
|
+
description: 'Maximum items per section (default: 10)',
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
required: [],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
// Call Graph Tool
|
|
435
|
+
{
|
|
436
|
+
name: 'drift_callgraph',
|
|
437
|
+
description: 'Build and query call graphs for code reachability analysis. Answers "What data can this code access?", "Who can reach this data?", "What breaks if I change this?", "What code is never called?", and "Which sensitive data paths are tested?" Use this to understand the impact of security findings or code changes.',
|
|
438
|
+
inputSchema: {
|
|
439
|
+
type: 'object',
|
|
440
|
+
properties: {
|
|
441
|
+
action: {
|
|
442
|
+
type: 'string',
|
|
443
|
+
enum: ['status', 'build', 'reach', 'inverse', 'function', 'security', 'impact', 'dead', 'coverage'],
|
|
444
|
+
description: 'Action to perform (default: status). "status" shows overview, "build" builds the call graph, "reach" finds what data a location can access, "inverse" finds who can access specific data, "function" shows function details, "security" shows security-prioritized view (P0-P4 tiers), "impact" analyzes what breaks if you change a file or function, "dead" finds functions that are never called, "coverage" analyzes test coverage for sensitive data access paths',
|
|
445
|
+
},
|
|
446
|
+
location: {
|
|
447
|
+
type: 'string',
|
|
448
|
+
description: 'Code location for "reach" action (file:line or function_name)',
|
|
449
|
+
},
|
|
450
|
+
target: {
|
|
451
|
+
type: 'string',
|
|
452
|
+
description: 'Data target for "inverse" action (table or table.field), or file/function for "impact" action',
|
|
453
|
+
},
|
|
454
|
+
functionName: {
|
|
455
|
+
type: 'string',
|
|
456
|
+
description: 'Function name for "function" action',
|
|
457
|
+
},
|
|
458
|
+
maxDepth: {
|
|
459
|
+
type: 'number',
|
|
460
|
+
description: 'Maximum traversal depth (default: 10)',
|
|
461
|
+
},
|
|
462
|
+
confidence: {
|
|
463
|
+
type: 'string',
|
|
464
|
+
enum: ['high', 'medium', 'low'],
|
|
465
|
+
description: 'Minimum confidence level for "dead" action (default: low)',
|
|
466
|
+
},
|
|
467
|
+
limit: {
|
|
468
|
+
type: 'number',
|
|
469
|
+
description: 'Maximum items per section in output (default: 10)',
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
required: [],
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
// Projects Tool
|
|
476
|
+
{
|
|
477
|
+
name: 'drift_projects',
|
|
478
|
+
description: 'List and manage registered drift projects. Enables working across multiple codebases. Use this to see all projects, switch between them, or get details about a specific project.',
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: 'object',
|
|
481
|
+
properties: {
|
|
482
|
+
action: {
|
|
483
|
+
type: 'string',
|
|
484
|
+
enum: ['list', 'info', 'switch', 'recent'],
|
|
485
|
+
description: 'Action to perform (default: list). "list" shows all projects, "info" shows details for a project, "switch" changes active project, "recent" shows recently used projects',
|
|
486
|
+
},
|
|
487
|
+
project: {
|
|
488
|
+
type: 'string',
|
|
489
|
+
description: 'Project name, ID, or path (for info/switch actions)',
|
|
490
|
+
},
|
|
491
|
+
language: {
|
|
492
|
+
type: 'string',
|
|
493
|
+
description: 'Filter by language (for list action)',
|
|
494
|
+
},
|
|
495
|
+
framework: {
|
|
496
|
+
type: 'string',
|
|
497
|
+
description: 'Filter by framework (for list action)',
|
|
498
|
+
},
|
|
499
|
+
limit: {
|
|
500
|
+
type: 'number',
|
|
501
|
+
description: 'Maximum projects to return (default: all)',
|
|
502
|
+
},
|
|
397
503
|
},
|
|
398
504
|
required: [],
|
|
399
505
|
},
|
|
@@ -449,6 +555,10 @@ export function createDriftMCPServer(config) {
|
|
|
449
555
|
return await handleDNACheck(config.projectRoot, dnaStore, args);
|
|
450
556
|
case 'drift_boundaries':
|
|
451
557
|
return await handleBoundaries(boundaryStore, args);
|
|
558
|
+
case 'drift_callgraph':
|
|
559
|
+
return await handleCallGraph(config.projectRoot, args);
|
|
560
|
+
case 'drift_projects':
|
|
561
|
+
return await handleProjects(args);
|
|
452
562
|
default:
|
|
453
563
|
return {
|
|
454
564
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -496,6 +606,11 @@ async function handlePatterns(store, args) {
|
|
|
496
606
|
if (args.minConfidence !== undefined) {
|
|
497
607
|
patterns = patterns.filter(p => p.confidence.score >= args.minConfidence);
|
|
498
608
|
}
|
|
609
|
+
const totalCount = patterns.length;
|
|
610
|
+
const limit = args.limit ?? 30;
|
|
611
|
+
const includeExamples = args.includeExamples ?? true;
|
|
612
|
+
// Apply limit
|
|
613
|
+
patterns = patterns.slice(0, limit);
|
|
499
614
|
// Format for AI consumption
|
|
500
615
|
const result = patterns.map(p => ({
|
|
501
616
|
id: p.id,
|
|
@@ -507,13 +622,22 @@ async function handlePatterns(store, args) {
|
|
|
507
622
|
confidenceLevel: p.confidence.level,
|
|
508
623
|
locationCount: p.locations.length,
|
|
509
624
|
outlierCount: p.outliers.length,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
625
|
+
...(includeExamples ? {
|
|
626
|
+
exampleLocations: p.locations.slice(0, 2).map(l => ({
|
|
627
|
+
file: l.file,
|
|
628
|
+
line: l.line,
|
|
629
|
+
})),
|
|
630
|
+
} : {}),
|
|
514
631
|
}));
|
|
632
|
+
// Add pagination info
|
|
633
|
+
const output = {
|
|
634
|
+
patterns: result,
|
|
635
|
+
total: totalCount,
|
|
636
|
+
showing: result.length,
|
|
637
|
+
...(totalCount > limit ? { truncated: true, hint: `Use limit parameter to see more (showing ${limit} of ${totalCount})` } : {}),
|
|
638
|
+
};
|
|
515
639
|
return {
|
|
516
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
640
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
517
641
|
};
|
|
518
642
|
}
|
|
519
643
|
async function handleFiles(store, args) {
|
|
@@ -530,6 +654,80 @@ async function handleFiles(store, args) {
|
|
|
530
654
|
content: [{ type: 'text', text: `No patterns found in "${args.path}"` }],
|
|
531
655
|
};
|
|
532
656
|
}
|
|
657
|
+
const limit = args.limit ?? 20;
|
|
658
|
+
const offset = args.offset ?? 0;
|
|
659
|
+
const compact = args.compact ?? false;
|
|
660
|
+
// Handle array results (from glob patterns)
|
|
661
|
+
if (Array.isArray(result)) {
|
|
662
|
+
const totalCount = result.length;
|
|
663
|
+
const paginatedResult = result.slice(offset, offset + limit);
|
|
664
|
+
if (compact) {
|
|
665
|
+
// Return summary only
|
|
666
|
+
const summary = {
|
|
667
|
+
totalFiles: totalCount,
|
|
668
|
+
showing: paginatedResult.length,
|
|
669
|
+
offset,
|
|
670
|
+
files: paginatedResult.map((r) => ({
|
|
671
|
+
file: r.file,
|
|
672
|
+
patternCount: r.patterns?.length ?? 0,
|
|
673
|
+
})),
|
|
674
|
+
...(totalCount > offset + limit ? {
|
|
675
|
+
truncated: true,
|
|
676
|
+
hint: `Use offset=${offset + limit} to see next page`
|
|
677
|
+
} : {}),
|
|
678
|
+
};
|
|
679
|
+
return {
|
|
680
|
+
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const output = {
|
|
684
|
+
totalFiles: totalCount,
|
|
685
|
+
showing: paginatedResult.length,
|
|
686
|
+
offset,
|
|
687
|
+
results: paginatedResult,
|
|
688
|
+
...(totalCount > offset + limit ? {
|
|
689
|
+
truncated: true,
|
|
690
|
+
hint: `Use offset=${offset + limit} to see next page`
|
|
691
|
+
} : {}),
|
|
692
|
+
};
|
|
693
|
+
return {
|
|
694
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
// Single file result - limit patterns within the file
|
|
698
|
+
if (result.patterns && Array.isArray(result.patterns)) {
|
|
699
|
+
const totalPatterns = result.patterns.length;
|
|
700
|
+
const paginatedPatterns = result.patterns.slice(offset, offset + limit);
|
|
701
|
+
if (compact) {
|
|
702
|
+
const summary = {
|
|
703
|
+
file: result.file,
|
|
704
|
+
totalPatterns,
|
|
705
|
+
showing: paginatedPatterns.length,
|
|
706
|
+
categories: [...new Set(result.patterns.map((p) => p.category))],
|
|
707
|
+
...(totalPatterns > offset + limit ? {
|
|
708
|
+
truncated: true,
|
|
709
|
+
hint: `Use offset=${offset + limit} to see next page`
|
|
710
|
+
} : {}),
|
|
711
|
+
};
|
|
712
|
+
return {
|
|
713
|
+
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const output = {
|
|
717
|
+
...result,
|
|
718
|
+
patterns: paginatedPatterns,
|
|
719
|
+
totalPatterns,
|
|
720
|
+
showing: paginatedPatterns.length,
|
|
721
|
+
offset,
|
|
722
|
+
...(totalPatterns > offset + limit ? {
|
|
723
|
+
truncated: true,
|
|
724
|
+
hint: `Use offset=${offset + limit} to see next page`
|
|
725
|
+
} : {}),
|
|
726
|
+
};
|
|
727
|
+
return {
|
|
728
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
729
|
+
};
|
|
730
|
+
}
|
|
533
731
|
return {
|
|
534
732
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
535
733
|
};
|
|
@@ -542,18 +740,36 @@ async function handleWhere(store, args) {
|
|
|
542
740
|
if (args.category) {
|
|
543
741
|
patterns = patterns.filter(p => p.category === args.category);
|
|
544
742
|
}
|
|
743
|
+
const limit = args.limit ?? 10;
|
|
744
|
+
const maxLocations = args.maxLocations ?? 5;
|
|
745
|
+
const totalPatterns = patterns.length;
|
|
746
|
+
// Apply pattern limit
|
|
747
|
+
patterns = patterns.slice(0, limit);
|
|
545
748
|
const result = patterns.map(p => ({
|
|
546
749
|
id: p.id,
|
|
547
750
|
name: p.name,
|
|
548
751
|
category: p.category,
|
|
549
|
-
|
|
752
|
+
totalLocations: p.locations.length,
|
|
753
|
+
locations: p.locations.slice(0, maxLocations).map(l => ({
|
|
550
754
|
file: l.file,
|
|
551
755
|
line: l.line,
|
|
552
756
|
column: l.column,
|
|
553
757
|
})),
|
|
758
|
+
...(p.locations.length > maxLocations ? {
|
|
759
|
+
locationsHint: `Showing ${maxLocations} of ${p.locations.length}. Use maxLocations parameter to see more.`
|
|
760
|
+
} : {}),
|
|
554
761
|
}));
|
|
762
|
+
const output = {
|
|
763
|
+
patterns: result,
|
|
764
|
+
total: totalPatterns,
|
|
765
|
+
showing: result.length,
|
|
766
|
+
...(totalPatterns > limit ? {
|
|
767
|
+
truncated: true,
|
|
768
|
+
hint: `Showing ${limit} of ${totalPatterns} patterns. Use limit parameter to see more.`
|
|
769
|
+
} : {}),
|
|
770
|
+
};
|
|
555
771
|
return {
|
|
556
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
772
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
557
773
|
};
|
|
558
774
|
}
|
|
559
775
|
async function handleExport(store, args) {
|
|
@@ -1829,6 +2045,7 @@ async function handleBoundaries(store, args) {
|
|
|
1829
2045
|
await store.initialize();
|
|
1830
2046
|
const action = args.action ?? 'overview';
|
|
1831
2047
|
const includeViolations = args.includeViolations ?? true;
|
|
2048
|
+
const limit = args.limit ?? 10;
|
|
1832
2049
|
switch (action) {
|
|
1833
2050
|
case 'overview': {
|
|
1834
2051
|
const accessMap = store.getAccessMap();
|
|
@@ -1846,12 +2063,16 @@ async function handleBoundaries(store, args) {
|
|
|
1846
2063
|
accessCount: info.accessedBy.length,
|
|
1847
2064
|
hasSensitive: info.sensitiveFields.length > 0,
|
|
1848
2065
|
}))
|
|
1849
|
-
.sort((a, b) => b.accessCount - a.accessCount)
|
|
1850
|
-
|
|
1851
|
-
|
|
2066
|
+
.sort((a, b) => b.accessCount - a.accessCount);
|
|
2067
|
+
const totalTables = tableEntries.length;
|
|
2068
|
+
const limitedTables = tableEntries.slice(0, limit);
|
|
2069
|
+
for (const table of limitedTables) {
|
|
1852
2070
|
const sensitive = table.hasSensitive ? ' โ ๏ธ' : '';
|
|
1853
2071
|
output += `- **${table.name}**${sensitive}: ${table.accessCount} access points\n`;
|
|
1854
2072
|
}
|
|
2073
|
+
if (totalTables > limit) {
|
|
2074
|
+
output += `\n*... and ${totalTables - limit} more tables. Use \`drift_boundaries action="table" table="<name>"\` for details.*\n`;
|
|
2075
|
+
}
|
|
1855
2076
|
output += '\n';
|
|
1856
2077
|
}
|
|
1857
2078
|
if (sensitiveFields.length > 0) {
|
|
@@ -1862,11 +2083,15 @@ async function handleBoundaries(store, args) {
|
|
|
1862
2083
|
fieldCounts.set(key, (fieldCounts.get(key) ?? 0) + 1);
|
|
1863
2084
|
}
|
|
1864
2085
|
const sortedFields = Array.from(fieldCounts.entries())
|
|
1865
|
-
.sort((a, b) => b[1] - a[1])
|
|
1866
|
-
|
|
1867
|
-
|
|
2086
|
+
.sort((a, b) => b[1] - a[1]);
|
|
2087
|
+
const totalFields = sortedFields.length;
|
|
2088
|
+
const limitedFields = sortedFields.slice(0, limit);
|
|
2089
|
+
for (const [fieldName, count] of limitedFields) {
|
|
1868
2090
|
output += `- **${fieldName}**: ${count} locations\n`;
|
|
1869
2091
|
}
|
|
2092
|
+
if (totalFields > limit) {
|
|
2093
|
+
output += `\n*... and ${totalFields - limit} more fields. Use \`drift_boundaries action="sensitive"\` for full list.*\n`;
|
|
2094
|
+
}
|
|
1870
2095
|
output += '\n';
|
|
1871
2096
|
}
|
|
1872
2097
|
if (includeViolations) {
|
|
@@ -1874,12 +2099,13 @@ async function handleBoundaries(store, args) {
|
|
|
1874
2099
|
if (rules) {
|
|
1875
2100
|
const violations = store.checkAllViolations();
|
|
1876
2101
|
if (violations.length > 0) {
|
|
2102
|
+
const limitedViolations = violations.slice(0, limit);
|
|
1877
2103
|
output += `## โ ๏ธ Violations (${violations.length})\n\n`;
|
|
1878
|
-
for (const v of
|
|
2104
|
+
for (const v of limitedViolations) {
|
|
1879
2105
|
output += `- **${v.file}:${v.line}** - ${v.message}\n`;
|
|
1880
2106
|
}
|
|
1881
|
-
if (violations.length >
|
|
1882
|
-
output +=
|
|
2107
|
+
if (violations.length > limit) {
|
|
2108
|
+
output += `\n*... and ${violations.length - limit} more. Use \`drift_boundaries action="check"\` for full list.*\n`;
|
|
1883
2109
|
}
|
|
1884
2110
|
}
|
|
1885
2111
|
else {
|
|
@@ -2117,4 +2343,835 @@ async function handleBoundaries(store, args) {
|
|
|
2117
2343
|
};
|
|
2118
2344
|
}
|
|
2119
2345
|
}
|
|
2346
|
+
// ============================================================================
|
|
2347
|
+
// Call Graph Handler Function
|
|
2348
|
+
// ============================================================================
|
|
2349
|
+
/**
|
|
2350
|
+
* Handle drift_callgraph tool - Call graph analysis
|
|
2351
|
+
*/
|
|
2352
|
+
async function handleCallGraph(projectRoot, args) {
|
|
2353
|
+
const action = args.action ?? 'status';
|
|
2354
|
+
const maxDepth = args.maxDepth ?? 10;
|
|
2355
|
+
const limit = args.limit ?? 10;
|
|
2356
|
+
const analyzer = createCallGraphAnalyzer({ rootDir: projectRoot });
|
|
2357
|
+
switch (action) {
|
|
2358
|
+
case 'build': {
|
|
2359
|
+
try {
|
|
2360
|
+
// File patterns to scan
|
|
2361
|
+
const filePatterns = [
|
|
2362
|
+
'**/*.ts',
|
|
2363
|
+
'**/*.tsx',
|
|
2364
|
+
'**/*.js',
|
|
2365
|
+
'**/*.jsx',
|
|
2366
|
+
'**/*.py',
|
|
2367
|
+
];
|
|
2368
|
+
// Step 1: Run semantic data access scanner (tree-sitter/TypeScript compiler based)
|
|
2369
|
+
const semanticScanner = createSemanticDataAccessScanner({ rootDir: projectRoot });
|
|
2370
|
+
const semanticResult = await semanticScanner.scanDirectory({ patterns: filePatterns });
|
|
2371
|
+
// Use semantic results as primary source
|
|
2372
|
+
let dataAccessPoints = semanticResult.accessPoints;
|
|
2373
|
+
// Step 2: Fall back to boundary scanner for additional coverage (regex-based)
|
|
2374
|
+
const boundaryScanner = createBoundaryScanner({ rootDir: projectRoot });
|
|
2375
|
+
await boundaryScanner.initialize();
|
|
2376
|
+
const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
|
|
2377
|
+
// Merge boundary results with semantic results (semantic takes precedence)
|
|
2378
|
+
for (const [, accessPoint] of Object.entries(boundaryResult.accessMap.accessPoints)) {
|
|
2379
|
+
const existing = dataAccessPoints.get(accessPoint.file) ?? [];
|
|
2380
|
+
// Only add if not already detected by semantic scanner
|
|
2381
|
+
const isDuplicate = existing.some(ap => ap.line === accessPoint.line && ap.table === accessPoint.table);
|
|
2382
|
+
if (!isDuplicate) {
|
|
2383
|
+
existing.push(accessPoint);
|
|
2384
|
+
dataAccessPoints.set(accessPoint.file, existing);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
// Step 3: Build call graph with data access points
|
|
2388
|
+
await analyzer.initialize();
|
|
2389
|
+
const graph = await analyzer.scan(filePatterns, dataAccessPoints);
|
|
2390
|
+
let output = '# Call Graph Built\n\n';
|
|
2391
|
+
output += `- **Functions:** ${graph.stats.totalFunctions}\n`;
|
|
2392
|
+
output += `- **Call Sites:** ${graph.stats.totalCallSites}\n`;
|
|
2393
|
+
output += `- **Resolved:** ${graph.stats.resolvedCallSites} (${Math.round(graph.stats.resolvedCallSites / Math.max(1, graph.stats.totalCallSites) * 100)}%)\n`;
|
|
2394
|
+
output += `- **Entry Points:** ${graph.entryPoints.length}\n`;
|
|
2395
|
+
output += `- **Data Accessors:** ${graph.dataAccessors.length}\n\n`;
|
|
2396
|
+
// Semantic scanner stats (primary)
|
|
2397
|
+
if (semanticResult.stats.accessPointsFound > 0) {
|
|
2398
|
+
output += '## Data Access Detection (Semantic)\n\n';
|
|
2399
|
+
output += `- **Files Scanned:** ${semanticResult.stats.filesScanned}\n`;
|
|
2400
|
+
output += `- **Access Points:** ${semanticResult.stats.accessPointsFound}\n`;
|
|
2401
|
+
if (Object.keys(semanticResult.stats.byOrm).length > 0) {
|
|
2402
|
+
const orms = Object.entries(semanticResult.stats.byOrm)
|
|
2403
|
+
.sort((a, b) => b[1] - a[1])
|
|
2404
|
+
.map(([orm, count]) => `${orm}: ${count}`)
|
|
2405
|
+
.join(', ');
|
|
2406
|
+
output += `- **By ORM:** ${orms}\n`;
|
|
2407
|
+
}
|
|
2408
|
+
output += '\n';
|
|
2409
|
+
}
|
|
2410
|
+
// Boundary stats (fallback)
|
|
2411
|
+
if (boundaryResult.stats.accessPointsFound > 0) {
|
|
2412
|
+
output += '## Data Access Detection (Regex Fallback)\n\n';
|
|
2413
|
+
output += `- **Tables Found:** ${boundaryResult.stats.tablesFound}\n`;
|
|
2414
|
+
output += `- **Access Points:** ${boundaryResult.stats.accessPointsFound}\n`;
|
|
2415
|
+
output += `- **Sensitive Fields:** ${boundaryResult.stats.sensitiveFieldsFound}\n\n`;
|
|
2416
|
+
}
|
|
2417
|
+
const languages = Object.entries(graph.stats.byLanguage)
|
|
2418
|
+
.filter(([, count]) => count > 0)
|
|
2419
|
+
.sort((a, b) => b[1] - a[1]);
|
|
2420
|
+
if (languages.length > 0) {
|
|
2421
|
+
output += '## By Language\n\n';
|
|
2422
|
+
for (const [lang, count] of languages) {
|
|
2423
|
+
output += `- **${lang}:** ${count} functions\n`;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2427
|
+
}
|
|
2428
|
+
catch (error) {
|
|
2429
|
+
return {
|
|
2430
|
+
content: [{ type: 'text', text: `Error building call graph: ${error}` }],
|
|
2431
|
+
isError: true,
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
case 'status': {
|
|
2436
|
+
try {
|
|
2437
|
+
await analyzer.initialize();
|
|
2438
|
+
const graph = analyzer.getGraph();
|
|
2439
|
+
if (!graph) {
|
|
2440
|
+
return {
|
|
2441
|
+
content: [{
|
|
2442
|
+
type: 'text',
|
|
2443
|
+
text: '# No Call Graph Found\n\n' +
|
|
2444
|
+
'Run `drift_callgraph action="build"` to build the call graph first.\n\n' +
|
|
2445
|
+
'The call graph enables:\n' +
|
|
2446
|
+
'- Forward reachability: "What data can this code access?"\n' +
|
|
2447
|
+
'- Inverse reachability: "Who can access this data?"\n' +
|
|
2448
|
+
'- Security impact analysis\n',
|
|
2449
|
+
}],
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
let output = '# Call Graph Status\n\n';
|
|
2453
|
+
output += `- **Functions:** ${graph.stats.totalFunctions}\n`;
|
|
2454
|
+
output += `- **Call Sites:** ${graph.stats.totalCallSites} (${graph.stats.resolvedCallSites} resolved)\n`;
|
|
2455
|
+
output += `- **Entry Points:** ${graph.entryPoints.length}\n`;
|
|
2456
|
+
output += `- **Data Accessors:** ${graph.dataAccessors.length}\n\n`;
|
|
2457
|
+
if (graph.entryPoints.length > 0) {
|
|
2458
|
+
output += '## Entry Points\n\n';
|
|
2459
|
+
const limitedEntryPoints = graph.entryPoints.slice(0, limit);
|
|
2460
|
+
for (const id of limitedEntryPoints) {
|
|
2461
|
+
const func = graph.functions.get(id);
|
|
2462
|
+
if (func) {
|
|
2463
|
+
output += `- **${func.qualifiedName}** @ ${func.file}:${func.startLine}\n`;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (graph.entryPoints.length > limit) {
|
|
2467
|
+
output += `\n*... and ${graph.entryPoints.length - limit} more entry points*\n`;
|
|
2468
|
+
}
|
|
2469
|
+
output += '\n';
|
|
2470
|
+
}
|
|
2471
|
+
if (graph.dataAccessors.length > 0) {
|
|
2472
|
+
output += '## Data Accessors\n\n';
|
|
2473
|
+
const limitedAccessors = graph.dataAccessors.slice(0, limit);
|
|
2474
|
+
for (const id of limitedAccessors) {
|
|
2475
|
+
const func = graph.functions.get(id);
|
|
2476
|
+
if (func) {
|
|
2477
|
+
const tables = [...new Set(func.dataAccess.map(d => d.table))];
|
|
2478
|
+
output += `- **${func.qualifiedName}** โ [${tables.join(', ')}]\n`;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
if (graph.dataAccessors.length > limit) {
|
|
2482
|
+
output += `\n*... and ${graph.dataAccessors.length - limit} more data accessors*\n`;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2486
|
+
}
|
|
2487
|
+
catch (error) {
|
|
2488
|
+
return {
|
|
2489
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
2490
|
+
isError: true,
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
case 'reach': {
|
|
2495
|
+
if (!args.location) {
|
|
2496
|
+
return {
|
|
2497
|
+
content: [{
|
|
2498
|
+
type: 'text',
|
|
2499
|
+
text: 'Error: location parameter required for "reach" action.\n\n' +
|
|
2500
|
+
'Examples:\n' +
|
|
2501
|
+
'- `drift_callgraph action="reach" location="src/auth.py:45"`\n' +
|
|
2502
|
+
'- `drift_callgraph action="reach" location="login_user"`',
|
|
2503
|
+
}],
|
|
2504
|
+
isError: true,
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
try {
|
|
2508
|
+
await analyzer.initialize();
|
|
2509
|
+
const graph = analyzer.getGraph();
|
|
2510
|
+
if (!graph) {
|
|
2511
|
+
return {
|
|
2512
|
+
content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
|
|
2513
|
+
isError: true,
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
// Parse location
|
|
2517
|
+
let result;
|
|
2518
|
+
if (args.location.includes(':')) {
|
|
2519
|
+
const parts = args.location.split(':');
|
|
2520
|
+
const file = parts[0];
|
|
2521
|
+
const lineStr = parts[1];
|
|
2522
|
+
if (file && lineStr) {
|
|
2523
|
+
const line = parseInt(lineStr, 10);
|
|
2524
|
+
if (!isNaN(line)) {
|
|
2525
|
+
result = analyzer.getReachableData(file, line, { maxDepth });
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
if (!result) {
|
|
2530
|
+
// Try as function name
|
|
2531
|
+
let funcId;
|
|
2532
|
+
for (const [id, func] of graph.functions) {
|
|
2533
|
+
if (func.name === args.location || func.qualifiedName === args.location) {
|
|
2534
|
+
funcId = id;
|
|
2535
|
+
break;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
if (funcId) {
|
|
2539
|
+
result = analyzer.getReachableDataFromFunction(funcId, { maxDepth });
|
|
2540
|
+
}
|
|
2541
|
+
else {
|
|
2542
|
+
return {
|
|
2543
|
+
content: [{ type: 'text', text: `Function or location '${args.location}' not found.` }],
|
|
2544
|
+
isError: true,
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
let output = '# Reachability Analysis\n\n';
|
|
2549
|
+
output += `**Origin:** ${args.location}\n`;
|
|
2550
|
+
output += `**Tables Reachable:** ${result.tables.join(', ') || 'none'}\n`;
|
|
2551
|
+
output += `**Functions Traversed:** ${result.functionsTraversed}\n`;
|
|
2552
|
+
output += `**Max Depth:** ${result.maxDepth}\n\n`;
|
|
2553
|
+
if (result.sensitiveFields.length > 0) {
|
|
2554
|
+
output += '## โ ๏ธ Sensitive Fields Accessible\n\n';
|
|
2555
|
+
for (const sf of result.sensitiveFields) {
|
|
2556
|
+
output += `- **${sf.field.table}.${sf.field.field}** (${sf.field.sensitivityType})\n`;
|
|
2557
|
+
output += ` - ${sf.accessCount} access point(s), ${sf.paths.length} path(s)\n`;
|
|
2558
|
+
}
|
|
2559
|
+
output += '\n';
|
|
2560
|
+
}
|
|
2561
|
+
if (result.reachableAccess.length > 0) {
|
|
2562
|
+
output += '## Data Access Points\n\n';
|
|
2563
|
+
for (const ra of result.reachableAccess.slice(0, 15)) {
|
|
2564
|
+
output += `- **${ra.access.operation}** ${ra.access.table}.${ra.access.fields.join(', ')}\n`;
|
|
2565
|
+
output += ` - Path: ${ra.path.map(p => p.functionName).join(' โ ')}\n`;
|
|
2566
|
+
}
|
|
2567
|
+
if (result.reachableAccess.length > 15) {
|
|
2568
|
+
output += `- ... and ${result.reachableAccess.length - 15} more\n`;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2572
|
+
}
|
|
2573
|
+
catch (error) {
|
|
2574
|
+
return {
|
|
2575
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
2576
|
+
isError: true,
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
case 'inverse': {
|
|
2581
|
+
if (!args.target) {
|
|
2582
|
+
return {
|
|
2583
|
+
content: [{
|
|
2584
|
+
type: 'text',
|
|
2585
|
+
text: 'Error: target parameter required for "inverse" action.\n\n' +
|
|
2586
|
+
'Examples:\n' +
|
|
2587
|
+
'- `drift_callgraph action="inverse" target="users"`\n' +
|
|
2588
|
+
'- `drift_callgraph action="inverse" target="users.password_hash"`',
|
|
2589
|
+
}],
|
|
2590
|
+
isError: true,
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
try {
|
|
2594
|
+
await analyzer.initialize();
|
|
2595
|
+
const graph = analyzer.getGraph();
|
|
2596
|
+
if (!graph) {
|
|
2597
|
+
return {
|
|
2598
|
+
content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
|
|
2599
|
+
isError: true,
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
const parts = args.target.split('.');
|
|
2603
|
+
const table = parts[0] ?? '';
|
|
2604
|
+
const field = parts.length > 1 ? parts.slice(1).join('.') : undefined;
|
|
2605
|
+
const result = analyzer.getCodePathsToData(field ? { table, field, maxDepth } : { table, maxDepth });
|
|
2606
|
+
let output = '# Inverse Reachability\n\n';
|
|
2607
|
+
output += `**Target:** ${args.target}\n`;
|
|
2608
|
+
output += `**Direct Accessors:** ${result.totalAccessors}\n`;
|
|
2609
|
+
output += `**Entry Points That Can Reach:** ${result.entryPoints.length}\n\n`;
|
|
2610
|
+
if (result.accessPaths.length > 0) {
|
|
2611
|
+
output += '## Access Paths\n\n';
|
|
2612
|
+
for (const ap of result.accessPaths.slice(0, 10)) {
|
|
2613
|
+
const entryFunc = graph.functions.get(ap.entryPoint);
|
|
2614
|
+
if (entryFunc) {
|
|
2615
|
+
output += `### ${entryFunc.qualifiedName}\n`;
|
|
2616
|
+
output += `- Path: ${ap.path.map(p => p.functionName).join(' โ ')}\n\n`;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (result.accessPaths.length > 10) {
|
|
2620
|
+
output += `... and ${result.accessPaths.length - 10} more paths\n`;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
else {
|
|
2624
|
+
output += 'No entry points can reach this data.\n';
|
|
2625
|
+
}
|
|
2626
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2627
|
+
}
|
|
2628
|
+
catch (error) {
|
|
2629
|
+
return {
|
|
2630
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
2631
|
+
isError: true,
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
case 'function': {
|
|
2636
|
+
if (!args.functionName) {
|
|
2637
|
+
return {
|
|
2638
|
+
content: [{
|
|
2639
|
+
type: 'text',
|
|
2640
|
+
text: 'Error: functionName parameter required for "function" action.',
|
|
2641
|
+
}],
|
|
2642
|
+
isError: true,
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
try {
|
|
2646
|
+
await analyzer.initialize();
|
|
2647
|
+
const graph = analyzer.getGraph();
|
|
2648
|
+
if (!graph) {
|
|
2649
|
+
return {
|
|
2650
|
+
content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
|
|
2651
|
+
isError: true,
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
let func;
|
|
2655
|
+
for (const [, f] of graph.functions) {
|
|
2656
|
+
if (f.name === args.functionName || f.qualifiedName === args.functionName) {
|
|
2657
|
+
func = f;
|
|
2658
|
+
break;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
if (!func) {
|
|
2662
|
+
return {
|
|
2663
|
+
content: [{ type: 'text', text: `Function '${args.functionName}' not found.` }],
|
|
2664
|
+
isError: true,
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
let output = `# Function: ${func.qualifiedName}\n\n`;
|
|
2668
|
+
output += `- **File:** ${func.file}:${func.startLine}\n`;
|
|
2669
|
+
output += `- **Language:** ${func.language}\n`;
|
|
2670
|
+
if (func.className)
|
|
2671
|
+
output += `- **Class:** ${func.className}\n`;
|
|
2672
|
+
output += `- **Exported:** ${func.isExported ? 'yes' : 'no'}\n`;
|
|
2673
|
+
output += `- **Async:** ${func.isAsync ? 'yes' : 'no'}\n\n`;
|
|
2674
|
+
if (func.parameters.length > 0) {
|
|
2675
|
+
output += '## Parameters\n\n';
|
|
2676
|
+
for (const p of func.parameters) {
|
|
2677
|
+
output += `- **${p.name}**${p.type ? `: ${p.type}` : ''}\n`;
|
|
2678
|
+
}
|
|
2679
|
+
output += '\n';
|
|
2680
|
+
}
|
|
2681
|
+
if (func.calls.length > 0) {
|
|
2682
|
+
output += `## Calls (${func.calls.length})\n\n`;
|
|
2683
|
+
for (const c of func.calls.slice(0, 10)) {
|
|
2684
|
+
const status = c.resolved ? 'โ' : '?';
|
|
2685
|
+
output += `- ${status} **${c.calleeName}** (line ${c.line})\n`;
|
|
2686
|
+
}
|
|
2687
|
+
if (func.calls.length > 10) {
|
|
2688
|
+
output += `- ... and ${func.calls.length - 10} more\n`;
|
|
2689
|
+
}
|
|
2690
|
+
output += '\n';
|
|
2691
|
+
}
|
|
2692
|
+
if (func.calledBy.length > 0) {
|
|
2693
|
+
output += `## Called By (${func.calledBy.length})\n\n`;
|
|
2694
|
+
for (const c of func.calledBy.slice(0, 10)) {
|
|
2695
|
+
const caller = graph.functions.get(c.callerId);
|
|
2696
|
+
output += `- **${caller?.qualifiedName ?? c.callerId}**\n`;
|
|
2697
|
+
}
|
|
2698
|
+
if (func.calledBy.length > 10) {
|
|
2699
|
+
output += `- ... and ${func.calledBy.length - 10} more\n`;
|
|
2700
|
+
}
|
|
2701
|
+
output += '\n';
|
|
2702
|
+
}
|
|
2703
|
+
if (func.dataAccess.length > 0) {
|
|
2704
|
+
output += '## Data Access\n\n';
|
|
2705
|
+
for (const d of func.dataAccess) {
|
|
2706
|
+
output += `- **${d.operation}** ${d.table}.${d.fields.join(', ')}\n`;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2710
|
+
}
|
|
2711
|
+
catch (error) {
|
|
2712
|
+
return {
|
|
2713
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
2714
|
+
isError: true,
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
case 'security': {
|
|
2719
|
+
try {
|
|
2720
|
+
// Import security prioritizer dynamically to avoid circular deps
|
|
2721
|
+
const { createSecurityPrioritizer } = await import('driftdetect-core');
|
|
2722
|
+
// Run boundary scan
|
|
2723
|
+
const boundaryScanner = createBoundaryScanner({ rootDir: projectRoot });
|
|
2724
|
+
await boundaryScanner.initialize();
|
|
2725
|
+
const filePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py'];
|
|
2726
|
+
const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
|
|
2727
|
+
// Prioritize by security
|
|
2728
|
+
const prioritizer = createSecurityPrioritizer();
|
|
2729
|
+
const prioritized = prioritizer.prioritize(boundaryResult.accessMap);
|
|
2730
|
+
const { summary } = prioritized;
|
|
2731
|
+
let output = '# ๐ Security-Prioritized Data Access\n\n';
|
|
2732
|
+
output += '## Summary\n\n';
|
|
2733
|
+
output += `- **Total Access Points:** ${summary.totalAccessPoints}\n`;
|
|
2734
|
+
output += `- **๐ด Critical (P0/P1):** ${summary.criticalCount}\n`;
|
|
2735
|
+
output += `- **๐ก High (P2):** ${summary.highCount}\n`;
|
|
2736
|
+
output += `- **โช Low (P3/P4):** ${summary.lowCount}\n`;
|
|
2737
|
+
output += `- **๐ฆ Noise (filtered):** ${summary.noiseCount}\n\n`;
|
|
2738
|
+
// Regulations
|
|
2739
|
+
if (summary.regulations.length > 0) {
|
|
2740
|
+
output += '## Regulatory Implications\n\n';
|
|
2741
|
+
output += summary.regulations.map(r => `**${r.toUpperCase()}**`).join(', ') + '\n\n';
|
|
2742
|
+
}
|
|
2743
|
+
// Critical items - use limit parameter
|
|
2744
|
+
if (prioritized.critical.length > 0) {
|
|
2745
|
+
const limitedCritical = prioritized.critical.slice(0, limit);
|
|
2746
|
+
output += '## ๐จ Critical Security Items (P0/P1)\n\n';
|
|
2747
|
+
output += 'These require immediate attention:\n\n';
|
|
2748
|
+
for (const p of limitedCritical) {
|
|
2749
|
+
const sensitivityIcon = p.security.maxSensitivity === 'credentials' ? '๐' :
|
|
2750
|
+
p.security.maxSensitivity === 'financial' ? '๐ฐ' :
|
|
2751
|
+
p.security.maxSensitivity === 'health' ? '๐ฅ' :
|
|
2752
|
+
p.security.maxSensitivity === 'pii' ? '๐ค' : 'โ';
|
|
2753
|
+
output += `### ${p.security.tier} ${sensitivityIcon} ${p.accessPoint.table}\n`;
|
|
2754
|
+
output += `- **Operation:** ${p.accessPoint.operation}\n`;
|
|
2755
|
+
output += `- **Fields:** ${p.accessPoint.fields.join(', ') || '*'}\n`;
|
|
2756
|
+
output += `- **Location:** ${p.accessPoint.file}:${p.accessPoint.line}\n`;
|
|
2757
|
+
output += `- **Risk Score:** ${p.security.riskScore}/100\n`;
|
|
2758
|
+
output += `- **Rationale:** ${p.security.rationale}\n`;
|
|
2759
|
+
if (p.security.regulations.length > 0) {
|
|
2760
|
+
output += `- **Regulations:** ${p.security.regulations.join(', ')}\n`;
|
|
2761
|
+
}
|
|
2762
|
+
output += '\n';
|
|
2763
|
+
}
|
|
2764
|
+
if (prioritized.critical.length > limit) {
|
|
2765
|
+
output += `*... and ${prioritized.critical.length - limit} more critical items. Use \`limit=${limit + 10}\` to see more.*\n\n`;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
// High priority items - use limit parameter
|
|
2769
|
+
if (prioritized.high.length > 0) {
|
|
2770
|
+
const highLimit = Math.max(5, Math.floor(limit / 2));
|
|
2771
|
+
const limitedHigh = prioritized.high.slice(0, highLimit);
|
|
2772
|
+
output += '## โ ๏ธ High Priority Items (P2)\n\n';
|
|
2773
|
+
for (const p of limitedHigh) {
|
|
2774
|
+
output += `- **${p.accessPoint.table}**.${p.accessPoint.fields.join(', ') || '*'} (${p.accessPoint.operation})\n`;
|
|
2775
|
+
output += ` - ${p.accessPoint.file}:${p.accessPoint.line}\n`;
|
|
2776
|
+
}
|
|
2777
|
+
if (prioritized.high.length > highLimit) {
|
|
2778
|
+
output += `\n*... and ${prioritized.high.length - highLimit} more high priority items*\n`;
|
|
2779
|
+
}
|
|
2780
|
+
output += '\n';
|
|
2781
|
+
}
|
|
2782
|
+
// Sensitivity breakdown
|
|
2783
|
+
output += '## By Sensitivity Type\n\n';
|
|
2784
|
+
if (summary.bySensitivity.credentials > 0) {
|
|
2785
|
+
output += `- **๐ Credentials:** ${summary.bySensitivity.credentials}\n`;
|
|
2786
|
+
}
|
|
2787
|
+
if (summary.bySensitivity.financial > 0) {
|
|
2788
|
+
output += `- **๐ฐ Financial:** ${summary.bySensitivity.financial}\n`;
|
|
2789
|
+
}
|
|
2790
|
+
if (summary.bySensitivity.health > 0) {
|
|
2791
|
+
output += `- **๐ฅ Health:** ${summary.bySensitivity.health}\n`;
|
|
2792
|
+
}
|
|
2793
|
+
if (summary.bySensitivity.pii > 0) {
|
|
2794
|
+
output += `- **๐ค PII:** ${summary.bySensitivity.pii}\n`;
|
|
2795
|
+
}
|
|
2796
|
+
output += `- **โ Unknown:** ${summary.bySensitivity.unknown}\n`;
|
|
2797
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2798
|
+
}
|
|
2799
|
+
catch (error) {
|
|
2800
|
+
return {
|
|
2801
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
2802
|
+
isError: true,
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
case 'impact': {
|
|
2807
|
+
if (!args.target) {
|
|
2808
|
+
return {
|
|
2809
|
+
content: [{
|
|
2810
|
+
type: 'text',
|
|
2811
|
+
text: 'Error: target parameter required for "impact" action.\n\n' +
|
|
2812
|
+
'Examples:\n' +
|
|
2813
|
+
'- `drift_callgraph action="impact" target="src/auth.py"` (analyze file)\n' +
|
|
2814
|
+
'- `drift_callgraph action="impact" target="login_user"` (analyze function)',
|
|
2815
|
+
}],
|
|
2816
|
+
isError: true,
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
try {
|
|
2820
|
+
await analyzer.initialize();
|
|
2821
|
+
const graph = analyzer.getGraph();
|
|
2822
|
+
if (!graph) {
|
|
2823
|
+
return {
|
|
2824
|
+
content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
|
|
2825
|
+
isError: true,
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
const impactAnalyzer = createImpactAnalyzer(graph);
|
|
2829
|
+
let result;
|
|
2830
|
+
// Determine if target is a file or function
|
|
2831
|
+
if (args.target.includes('/') || args.target.includes('.py') || args.target.includes('.ts') || args.target.includes('.js')) {
|
|
2832
|
+
result = impactAnalyzer.analyzeFile(args.target);
|
|
2833
|
+
}
|
|
2834
|
+
else {
|
|
2835
|
+
result = impactAnalyzer.analyzeFunctionByName(args.target);
|
|
2836
|
+
}
|
|
2837
|
+
let output = '# ๐ฅ Impact Analysis\n\n';
|
|
2838
|
+
// Target info
|
|
2839
|
+
if (result.target.type === 'file') {
|
|
2840
|
+
output += `**Target:** ${result.target.file} (${result.changedFunctions.length} functions)\n\n`;
|
|
2841
|
+
}
|
|
2842
|
+
else {
|
|
2843
|
+
output += `**Target:** ${result.target.functionName ?? result.target.functionId ?? 'unknown'}\n\n`;
|
|
2844
|
+
}
|
|
2845
|
+
// Risk assessment
|
|
2846
|
+
const riskEmoji = result.risk === 'critical' ? '๐ด' :
|
|
2847
|
+
result.risk === 'high' ? '๐ ' :
|
|
2848
|
+
result.risk === 'medium' ? '๐ก' : '๐ข';
|
|
2849
|
+
output += `**Risk Level:** ${riskEmoji} **${result.risk.toUpperCase()}** (score: ${result.riskScore}/100)\n\n`;
|
|
2850
|
+
// Summary
|
|
2851
|
+
output += '## Summary\n\n';
|
|
2852
|
+
output += `- **Direct Callers:** ${result.summary.directCallers}\n`;
|
|
2853
|
+
output += `- **Transitive Callers:** ${result.summary.transitiveCallers}\n`;
|
|
2854
|
+
output += `- **Affected Entry Points:** ${result.summary.affectedEntryPoints}\n`;
|
|
2855
|
+
output += `- **Sensitive Data Paths:** ${result.summary.affectedDataPaths}\n`;
|
|
2856
|
+
output += `- **Max Call Depth:** ${result.summary.maxDepth}\n\n`;
|
|
2857
|
+
// Entry points affected - use limit
|
|
2858
|
+
if (result.entryPoints.length > 0) {
|
|
2859
|
+
const limitedEntryPoints = result.entryPoints.slice(0, limit);
|
|
2860
|
+
output += '## ๐ช Affected Entry Points (User-Facing Impact)\n\n';
|
|
2861
|
+
for (const ep of limitedEntryPoints) {
|
|
2862
|
+
output += `### ${ep.qualifiedName}\n`;
|
|
2863
|
+
output += `- **Location:** ${ep.file}:${ep.line}\n`;
|
|
2864
|
+
output += `- **Path:** ${ep.pathToChange.map(p => p.functionName).join(' โ ')}\n\n`;
|
|
2865
|
+
}
|
|
2866
|
+
if (result.entryPoints.length > limit) {
|
|
2867
|
+
output += `*... and ${result.entryPoints.length - limit} more entry points. Use \`limit=${limit + 10}\` to see more.*\n\n`;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
// Sensitive data paths - use limit
|
|
2871
|
+
if (result.sensitiveDataPaths.length > 0) {
|
|
2872
|
+
const limitedPaths = result.sensitiveDataPaths.slice(0, limit);
|
|
2873
|
+
output += '## ๐ Sensitive Data Paths Affected\n\n';
|
|
2874
|
+
for (const dp of limitedPaths) {
|
|
2875
|
+
const sensitivityIcon = dp.sensitivity === 'credentials' ? '๐' :
|
|
2876
|
+
dp.sensitivity === 'financial' ? '๐ฐ' :
|
|
2877
|
+
dp.sensitivity === 'health' ? '๐ฅ' : '๐ค';
|
|
2878
|
+
output += `### ${sensitivityIcon} ${dp.sensitivity}: ${dp.table}.${dp.fields.join(', ')}\n`;
|
|
2879
|
+
output += `- **Operation:** ${dp.operation}\n`;
|
|
2880
|
+
output += `- **Entry Point:** ${dp.entryPoint}\n`;
|
|
2881
|
+
output += `- **Path:** ${dp.fullPath.map(n => n.functionName).join(' โ ')}\n\n`;
|
|
2882
|
+
}
|
|
2883
|
+
if (result.sensitiveDataPaths.length > limit) {
|
|
2884
|
+
output += `*... and ${result.sensitiveDataPaths.length - limit} more sensitive paths*\n\n`;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
// Direct callers - use limit
|
|
2888
|
+
const directCallers = result.affected.filter(a => a.depth === 1);
|
|
2889
|
+
if (directCallers.length > 0) {
|
|
2890
|
+
const limitedDirect = directCallers.slice(0, limit);
|
|
2891
|
+
output += '## ๐ Direct Callers (Immediate Impact)\n\n';
|
|
2892
|
+
for (const caller of limitedDirect) {
|
|
2893
|
+
const icon = caller.accessesSensitiveData ? '๐ด' : 'โช';
|
|
2894
|
+
output += `- ${icon} **${caller.qualifiedName}** @ ${caller.file}:${caller.line}\n`;
|
|
2895
|
+
}
|
|
2896
|
+
if (directCallers.length > limit) {
|
|
2897
|
+
output += `\n*... and ${directCallers.length - limit} more direct callers*\n`;
|
|
2898
|
+
}
|
|
2899
|
+
output += '\n';
|
|
2900
|
+
}
|
|
2901
|
+
// Transitive callers - use smaller limit
|
|
2902
|
+
const transitiveCallers = result.affected.filter(a => a.depth > 1);
|
|
2903
|
+
if (transitiveCallers.length > 0) {
|
|
2904
|
+
const transitiveLimit = Math.max(5, Math.floor(limit / 2));
|
|
2905
|
+
const limitedTransitive = transitiveCallers.slice(0, transitiveLimit);
|
|
2906
|
+
output += '## ๐ Transitive Callers (Ripple Effect)\n\n';
|
|
2907
|
+
for (const caller of limitedTransitive) {
|
|
2908
|
+
output += `- [depth ${caller.depth}] **${caller.qualifiedName}**\n`;
|
|
2909
|
+
}
|
|
2910
|
+
if (transitiveCallers.length > transitiveLimit) {
|
|
2911
|
+
output += `\n*... and ${transitiveCallers.length - transitiveLimit} more transitive callers*\n`;
|
|
2912
|
+
}
|
|
2913
|
+
output += '\n';
|
|
2914
|
+
}
|
|
2915
|
+
// Recommendations
|
|
2916
|
+
if (result.risk === 'critical' || result.risk === 'high') {
|
|
2917
|
+
output += '## โ ๏ธ Recommendations\n\n';
|
|
2918
|
+
if (result.sensitiveDataPaths.length > 0) {
|
|
2919
|
+
output += '- Review all sensitive data paths before merging\n';
|
|
2920
|
+
}
|
|
2921
|
+
if (result.entryPoints.length > 5) {
|
|
2922
|
+
output += '- Consider incremental rollout - many entry points affected\n';
|
|
2923
|
+
}
|
|
2924
|
+
if (result.summary.maxDepth > 5) {
|
|
2925
|
+
output += '- Deep call chain - test thoroughly for regressions\n';
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return { content: [{ type: 'text', text: output }] };
|
|
2929
|
+
}
|
|
2930
|
+
catch (error) {
|
|
2931
|
+
return {
|
|
2932
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
2933
|
+
isError: true,
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
case 'dead': {
|
|
2938
|
+
try {
|
|
2939
|
+
await analyzer.initialize();
|
|
2940
|
+
const graph = analyzer.getGraph();
|
|
2941
|
+
if (!graph) {
|
|
2942
|
+
return {
|
|
2943
|
+
content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
|
|
2944
|
+
isError: true,
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
const detector = createDeadCodeDetector(graph);
|
|
2948
|
+
const minConfidence = args.confidence ?? 'low';
|
|
2949
|
+
const result = detector.detect({ minConfidence });
|
|
2950
|
+
let output = '# ๐ Dead Code Analysis\n\n';
|
|
2951
|
+
// Summary
|
|
2952
|
+
output += '## Summary\n\n';
|
|
2953
|
+
output += `- **Total Functions:** ${result.summary.totalFunctions}\n`;
|
|
2954
|
+
output += `- **Dead Candidates:** ${result.summary.deadCandidates}\n`;
|
|
2955
|
+
output += `- **High Confidence:** ${result.summary.highConfidence}\n`;
|
|
2956
|
+
output += `- **Medium Confidence:** ${result.summary.mediumConfidence}\n`;
|
|
2957
|
+
output += `- **Low Confidence:** ${result.summary.lowConfidence}\n`;
|
|
2958
|
+
output += `- **Estimated Dead Lines:** ~${result.summary.estimatedDeadLines.toLocaleString()}\n\n`;
|
|
2959
|
+
// Excluded
|
|
2960
|
+
output += '## Excluded from Analysis\n\n';
|
|
2961
|
+
output += `- Entry Points: ${result.excluded.entryPoints}\n`;
|
|
2962
|
+
output += `- Functions with Callers: ${result.excluded.withCallers}\n`;
|
|
2963
|
+
output += `- Framework Hooks: ${result.excluded.frameworkHooks}\n\n`;
|
|
2964
|
+
// By language
|
|
2965
|
+
if (Object.keys(result.summary.byLanguage).length > 0) {
|
|
2966
|
+
output += '## By Language\n\n';
|
|
2967
|
+
for (const [lang, count] of Object.entries(result.summary.byLanguage)) {
|
|
2968
|
+
output += `- **${lang}:** ${count}\n`;
|
|
2969
|
+
}
|
|
2970
|
+
output += '\n';
|
|
2971
|
+
}
|
|
2972
|
+
// High confidence candidates - use limit
|
|
2973
|
+
const highConf = result.candidates.filter(c => c.confidence === 'high');
|
|
2974
|
+
if (highConf.length > 0) {
|
|
2975
|
+
const limitedHigh = highConf.slice(0, limit);
|
|
2976
|
+
output += '## ๐ด High Confidence Dead Code\n\n';
|
|
2977
|
+
output += 'These functions are very likely unused:\n\n';
|
|
2978
|
+
for (const c of limitedHigh) {
|
|
2979
|
+
output += `### ${c.qualifiedName}\n`;
|
|
2980
|
+
output += `- **File:** ${c.file}:${c.line}\n`;
|
|
2981
|
+
output += `- **Lines:** ${c.linesOfCode}\n`;
|
|
2982
|
+
if (c.hasDataAccess) {
|
|
2983
|
+
output += `- โ ๏ธ Has data access\n`;
|
|
2984
|
+
}
|
|
2985
|
+
output += '\n';
|
|
2986
|
+
}
|
|
2987
|
+
if (highConf.length > limit) {
|
|
2988
|
+
output += `*... and ${highConf.length - limit} more high-confidence candidates. Use \`limit=${limit + 10}\` to see more.*\n\n`;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
// Medium confidence candidates - use smaller limit
|
|
2992
|
+
const medConf = result.candidates.filter(c => c.confidence === 'medium');
|
|
2993
|
+
if (medConf.length > 0) {
|
|
2994
|
+
const medLimit = Math.max(5, Math.floor(limit / 2));
|
|
2995
|
+
const limitedMed = medConf.slice(0, medLimit);
|
|
2996
|
+
output += '## ๐ก Medium Confidence Dead Code\n\n';
|
|
2997
|
+
output += 'These might be unused but have some indicators they could be called:\n\n';
|
|
2998
|
+
for (const c of limitedMed) {
|
|
2999
|
+
output += `- **${c.qualifiedName}** @ ${c.file}:${c.line}`;
|
|
3000
|
+
if (c.possibleFalsePositives.length > 0) {
|
|
3001
|
+
output += ` (${c.possibleFalsePositives.join(', ')})`;
|
|
3002
|
+
}
|
|
3003
|
+
output += '\n';
|
|
3004
|
+
}
|
|
3005
|
+
if (medConf.length > medLimit) {
|
|
3006
|
+
output += `\n*... and ${medConf.length - medLimit} more medium-confidence candidates*\n`;
|
|
3007
|
+
}
|
|
3008
|
+
output += '\n';
|
|
3009
|
+
}
|
|
3010
|
+
// Files with most dead code - use limit
|
|
3011
|
+
if (result.summary.byFile.length > 0) {
|
|
3012
|
+
const limitedFiles = result.summary.byFile.slice(0, limit);
|
|
3013
|
+
output += '## Files with Most Dead Code\n\n';
|
|
3014
|
+
for (const f of limitedFiles) {
|
|
3015
|
+
output += `- **${f.file}**: ${f.count} functions (~${f.lines} lines)\n`;
|
|
3016
|
+
}
|
|
3017
|
+
if (result.summary.byFile.length > limit) {
|
|
3018
|
+
output += `\n*... and ${result.summary.byFile.length - limit} more files*\n`;
|
|
3019
|
+
}
|
|
3020
|
+
output += '\n';
|
|
3021
|
+
}
|
|
3022
|
+
// Recommendations
|
|
3023
|
+
if (result.summary.highConfidence > 0) {
|
|
3024
|
+
output += '## ๐ก Recommendations\n\n';
|
|
3025
|
+
output += '1. Start with high-confidence candidates - they\'re safest to remove\n';
|
|
3026
|
+
output += '2. Search for dynamic calls (getattr, reflection) before removing\n';
|
|
3027
|
+
output += '3. Check if functions are called from tests\n';
|
|
3028
|
+
output += '4. Consider adding `# pragma: no cover` for intentionally unused code\n';
|
|
3029
|
+
}
|
|
3030
|
+
return { content: [{ type: 'text', text: output }] };
|
|
3031
|
+
}
|
|
3032
|
+
catch (error) {
|
|
3033
|
+
return {
|
|
3034
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
3035
|
+
isError: true,
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
case 'coverage': {
|
|
3040
|
+
try {
|
|
3041
|
+
await analyzer.initialize();
|
|
3042
|
+
const graph = analyzer.getGraph();
|
|
3043
|
+
if (!graph) {
|
|
3044
|
+
return {
|
|
3045
|
+
content: [{ type: 'text', text: 'No call graph found. Run build first.' }],
|
|
3046
|
+
isError: true,
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
const coverageAnalyzer = createCoverageAnalyzer(graph);
|
|
3050
|
+
const result = coverageAnalyzer.analyze();
|
|
3051
|
+
let output = '# ๐งช Sensitive Data Test Coverage\n\n';
|
|
3052
|
+
// Summary
|
|
3053
|
+
output += '## Summary\n\n';
|
|
3054
|
+
output += `- **Sensitive Fields:** ${result.summary.totalSensitiveFields}\n`;
|
|
3055
|
+
output += `- **Access Paths:** ${result.summary.totalAccessPaths}\n`;
|
|
3056
|
+
output += `- **Tested Paths:** ${result.summary.testedAccessPaths}\n`;
|
|
3057
|
+
output += `- **Coverage:** ${result.summary.coveragePercent}%\n`;
|
|
3058
|
+
output += `- **Test Files:** ${result.testFiles.length}\n`;
|
|
3059
|
+
output += `- **Test Functions:** ${result.testFunctions}\n\n`;
|
|
3060
|
+
// By sensitivity
|
|
3061
|
+
output += '## Coverage by Sensitivity\n\n';
|
|
3062
|
+
const sensOrder = ['credentials', 'financial', 'health', 'pii'];
|
|
3063
|
+
for (const sens of sensOrder) {
|
|
3064
|
+
const s = result.summary.bySensitivity[sens];
|
|
3065
|
+
if (s.fields > 0) {
|
|
3066
|
+
const icon = sens === 'credentials' ? '๐' :
|
|
3067
|
+
sens === 'financial' ? '๐ฐ' :
|
|
3068
|
+
sens === 'health' ? '๐ฅ' : '๐ค';
|
|
3069
|
+
const status = s.coveragePercent >= 80 ? 'โ' : s.coveragePercent >= 50 ? 'โ' : 'โ';
|
|
3070
|
+
output += `- ${icon} **${sens}**: ${status} ${s.coveragePercent}% (${s.testedPaths}/${s.paths} paths)\n`;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
output += '\n';
|
|
3074
|
+
// Field coverage - use limit
|
|
3075
|
+
if (result.fields.length > 0) {
|
|
3076
|
+
const limitedFields = result.fields.slice(0, limit);
|
|
3077
|
+
output += '## Field Coverage\n\n';
|
|
3078
|
+
for (const f of limitedFields) {
|
|
3079
|
+
const statusIcon = f.status === 'covered' ? 'โ' :
|
|
3080
|
+
f.status === 'partial' ? 'โ' : 'โ';
|
|
3081
|
+
const sensIcon = f.sensitivity === 'credentials' ? '๐' :
|
|
3082
|
+
f.sensitivity === 'financial' ? '๐ฐ' :
|
|
3083
|
+
f.sensitivity === 'health' ? '๐ฅ' : '๐ค';
|
|
3084
|
+
output += `- ${statusIcon} ${sensIcon} **${f.fullName}**: ${f.testedPaths}/${f.totalPaths} paths tested (${f.coveragePercent}%)\n`;
|
|
3085
|
+
}
|
|
3086
|
+
if (result.fields.length > limit) {
|
|
3087
|
+
output += `\n*... and ${result.fields.length - limit} more fields. Use \`limit=${limit + 10}\` to see more.*\n`;
|
|
3088
|
+
}
|
|
3089
|
+
output += '\n';
|
|
3090
|
+
}
|
|
3091
|
+
// Uncovered paths by priority - use smaller limits for each sensitivity type
|
|
3092
|
+
const pathLimit = Math.max(3, Math.floor(limit / 3));
|
|
3093
|
+
const uncoveredByCredentials = result.uncoveredPaths.filter((p) => p.sensitivity === 'credentials');
|
|
3094
|
+
const uncoveredByFinancial = result.uncoveredPaths.filter((p) => p.sensitivity === 'financial');
|
|
3095
|
+
const uncoveredByHealth = result.uncoveredPaths.filter((p) => p.sensitivity === 'health');
|
|
3096
|
+
const uncoveredByPii = result.uncoveredPaths.filter((p) => p.sensitivity === 'pii');
|
|
3097
|
+
if (uncoveredByCredentials.length > 0) {
|
|
3098
|
+
const limitedCreds = uncoveredByCredentials.slice(0, pathLimit);
|
|
3099
|
+
output += '## ๐ Untested Credential Access Paths\n\n';
|
|
3100
|
+
output += '**CRITICAL: These paths access credentials without test coverage**\n\n';
|
|
3101
|
+
for (const p of limitedCreds) {
|
|
3102
|
+
output += `### ${p.table}.${p.field}\n`;
|
|
3103
|
+
output += `- **Entry Point:** ${p.entryPoint.name} @ ${p.entryPoint.file}:${p.entryPoint.line}\n`;
|
|
3104
|
+
output += `- **Accessor:** ${p.accessor.name} @ ${p.accessor.file}:${p.accessor.line}\n`;
|
|
3105
|
+
output += `- **Depth:** ${p.depth}\n\n`;
|
|
3106
|
+
}
|
|
3107
|
+
if (uncoveredByCredentials.length > pathLimit) {
|
|
3108
|
+
output += `*... and ${uncoveredByCredentials.length - pathLimit} more credential paths*\n\n`;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
if (uncoveredByFinancial.length > 0) {
|
|
3112
|
+
const limitedFinancial = uncoveredByFinancial.slice(0, pathLimit);
|
|
3113
|
+
output += '## ๐ฐ Untested Financial Data Paths\n\n';
|
|
3114
|
+
for (const p of limitedFinancial) {
|
|
3115
|
+
output += `- **${p.table}.${p.field}**: ${p.entryPoint.name} โ ${p.accessor.name}\n`;
|
|
3116
|
+
}
|
|
3117
|
+
if (uncoveredByFinancial.length > pathLimit) {
|
|
3118
|
+
output += `\n*... and ${uncoveredByFinancial.length - pathLimit} more*\n`;
|
|
3119
|
+
}
|
|
3120
|
+
output += '\n';
|
|
3121
|
+
}
|
|
3122
|
+
if (uncoveredByHealth.length > 0) {
|
|
3123
|
+
const limitedHealth = uncoveredByHealth.slice(0, pathLimit);
|
|
3124
|
+
output += '## ๐ฅ Untested Health Data Paths\n\n';
|
|
3125
|
+
for (const p of limitedHealth) {
|
|
3126
|
+
output += `- **${p.table}.${p.field}**: ${p.entryPoint.name} โ ${p.accessor.name}\n`;
|
|
3127
|
+
}
|
|
3128
|
+
if (uncoveredByHealth.length > pathLimit) {
|
|
3129
|
+
output += `\n*... and ${uncoveredByHealth.length - pathLimit} more*\n`;
|
|
3130
|
+
}
|
|
3131
|
+
output += '\n';
|
|
3132
|
+
}
|
|
3133
|
+
if (uncoveredByPii.length > 0) {
|
|
3134
|
+
const limitedPii = uncoveredByPii.slice(0, pathLimit);
|
|
3135
|
+
output += '## ๐ค Untested PII Access Paths\n\n';
|
|
3136
|
+
for (const p of limitedPii) {
|
|
3137
|
+
output += `- **${p.table}.${p.field}**: ${p.entryPoint.name} โ ${p.accessor.name}\n`;
|
|
3138
|
+
}
|
|
3139
|
+
if (uncoveredByPii.length > pathLimit) {
|
|
3140
|
+
output += `\n*... and ${uncoveredByPii.length - pathLimit} more*\n`;
|
|
3141
|
+
}
|
|
3142
|
+
output += '\n';
|
|
3143
|
+
}
|
|
3144
|
+
// Recommendations
|
|
3145
|
+
if (result.uncoveredPaths.length > 0) {
|
|
3146
|
+
output += '## ๐ก Recommendations\n\n';
|
|
3147
|
+
if (uncoveredByCredentials.length > 0) {
|
|
3148
|
+
output += `1. **CRITICAL:** ${uncoveredByCredentials.length} credential access paths need tests\n`;
|
|
3149
|
+
}
|
|
3150
|
+
if (uncoveredByFinancial.length > 0) {
|
|
3151
|
+
output += `2. ${uncoveredByFinancial.length} financial data paths need tests\n`;
|
|
3152
|
+
}
|
|
3153
|
+
if (result.summary.coveragePercent < 50) {
|
|
3154
|
+
output += `3. Overall coverage is ${result.summary.coveragePercent}% - consider adding integration tests\n`;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
else {
|
|
3158
|
+
output += '## โ All Sensitive Data Paths Tested\n\n';
|
|
3159
|
+
output += 'Great job! All sensitive data access paths are covered by tests.\n';
|
|
3160
|
+
}
|
|
3161
|
+
return { content: [{ type: 'text', text: output }] };
|
|
3162
|
+
}
|
|
3163
|
+
catch (error) {
|
|
3164
|
+
return {
|
|
3165
|
+
content: [{ type: 'text', text: `Error: ${error}` }],
|
|
3166
|
+
isError: true,
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
default:
|
|
3171
|
+
return {
|
|
3172
|
+
content: [{ type: 'text', text: `Unknown action: ${action}` }],
|
|
3173
|
+
isError: true,
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
2120
3177
|
//# sourceMappingURL=server.js.map
|