code-graph-context 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -2
- package/dist/constants.js +167 -0
- package/dist/core/config/fairsquare-framework-schema.js +9 -7
- package/dist/core/config/schema.js +41 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +166 -110
- package/dist/core/parsers/typescript-parser.js +1039 -742
- package/dist/core/parsers/workspace-parser.js +175 -193
- package/dist/core/utils/code-normalizer.js +299 -0
- package/dist/core/utils/file-change-detection.js +17 -2
- package/dist/core/utils/file-utils.js +40 -5
- package/dist/core/utils/graph-factory.js +161 -0
- package/dist/core/utils/shared-utils.js +79 -0
- package/dist/core/workspace/workspace-detector.js +59 -5
- package/dist/mcp/constants.js +141 -8
- package/dist/mcp/handlers/graph-generator.handler.js +1 -0
- package/dist/mcp/handlers/incremental-parse.handler.js +3 -6
- package/dist/mcp/handlers/parallel-import.handler.js +136 -0
- package/dist/mcp/handlers/streaming-import.handler.js +14 -59
- package/dist/mcp/mcp.server.js +1 -1
- package/dist/mcp/services/job-manager.js +5 -8
- package/dist/mcp/services/watch-manager.js +7 -18
- package/dist/mcp/tools/detect-dead-code.tool.js +413 -0
- package/dist/mcp/tools/detect-duplicate-code.tool.js +450 -0
- package/dist/mcp/tools/impact-analysis.tool.js +20 -4
- package/dist/mcp/tools/index.js +4 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +15 -14
- package/dist/mcp/workers/chunk-worker-pool.js +196 -0
- package/dist/mcp/workers/chunk-worker.types.js +4 -0
- package/dist/mcp/workers/chunk.worker.js +89 -0
- package/dist/mcp/workers/parse-coordinator.js +183 -0
- package/dist/mcp/workers/worker.pool.js +54 -0
- package/dist/storage/neo4j/neo4j.service.js +190 -10
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Tracks background parsing jobs for async mode
|
|
4
4
|
*/
|
|
5
5
|
import { randomBytes } from 'crypto';
|
|
6
|
+
import { JOBS } from '../constants.js';
|
|
6
7
|
const generateJobId = () => {
|
|
7
8
|
return `job_${randomBytes(8).toString('hex')}`;
|
|
8
9
|
};
|
|
@@ -15,10 +16,6 @@ const createInitialProgress = () => ({
|
|
|
15
16
|
currentChunk: 0,
|
|
16
17
|
totalChunks: 0,
|
|
17
18
|
});
|
|
18
|
-
// Cleanup interval: 5 minutes
|
|
19
|
-
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
20
|
-
// Maximum concurrent jobs to prevent memory exhaustion
|
|
21
|
-
const MAX_JOBS = 100;
|
|
22
19
|
class JobManager {
|
|
23
20
|
jobs = new Map();
|
|
24
21
|
cleanupInterval = null;
|
|
@@ -38,7 +35,7 @@ class JobManager {
|
|
|
38
35
|
if (cleaned > 0) {
|
|
39
36
|
console.log(`[JobManager] Cleaned up ${cleaned} old jobs`);
|
|
40
37
|
}
|
|
41
|
-
},
|
|
38
|
+
}, JOBS.cleanupIntervalMs);
|
|
42
39
|
// Don't prevent Node.js from exiting if this is the only timer
|
|
43
40
|
this.cleanupInterval.unref();
|
|
44
41
|
}
|
|
@@ -57,11 +54,11 @@ class JobManager {
|
|
|
57
54
|
*/
|
|
58
55
|
createJob(projectPath, projectId) {
|
|
59
56
|
// SECURITY: Enforce maximum job limit to prevent memory exhaustion
|
|
60
|
-
if (this.jobs.size >=
|
|
57
|
+
if (this.jobs.size >= JOBS.maxJobs) {
|
|
61
58
|
// Try to cleanup old jobs first
|
|
62
59
|
const cleaned = this.cleanupOldJobs(0); // Remove all completed/failed jobs
|
|
63
|
-
if (this.jobs.size >=
|
|
64
|
-
throw new Error(`Maximum job limit (${
|
|
60
|
+
if (this.jobs.size >= JOBS.maxJobs) {
|
|
61
|
+
throw new Error(`Maximum job limit (${JOBS.maxJobs}) reached. ` +
|
|
65
62
|
`${this.listJobs('running').length} jobs are currently running. ` +
|
|
66
63
|
`Please wait for jobs to complete or cancel existing jobs.`);
|
|
67
64
|
}
|
|
@@ -4,19 +4,8 @@
|
|
|
4
4
|
* Uses @parcel/watcher for high-performance file watching
|
|
5
5
|
*/
|
|
6
6
|
import * as watcher from '@parcel/watcher';
|
|
7
|
+
import { WATCH } from '../constants.js';
|
|
7
8
|
import { debugLog } from '../utils.js';
|
|
8
|
-
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
9
|
-
'**/node_modules/**',
|
|
10
|
-
'**/dist/**',
|
|
11
|
-
'**/build/**',
|
|
12
|
-
'**/.git/**',
|
|
13
|
-
'**/*.d.ts',
|
|
14
|
-
'**/*.js.map',
|
|
15
|
-
'**/*.js',
|
|
16
|
-
];
|
|
17
|
-
const DEFAULT_DEBOUNCE_MS = 1000;
|
|
18
|
-
const MAX_WATCHERS = 10;
|
|
19
|
-
const MAX_PENDING_EVENTS = 1000;
|
|
20
9
|
class WatchManager {
|
|
21
10
|
watchers = new Map();
|
|
22
11
|
mcpServer = null;
|
|
@@ -62,15 +51,15 @@ class WatchManager {
|
|
|
62
51
|
return this.getWatcherInfoFromState(existing);
|
|
63
52
|
}
|
|
64
53
|
// Enforce maximum watcher limit
|
|
65
|
-
if (this.watchers.size >=
|
|
66
|
-
throw new Error(`Maximum watcher limit (${
|
|
54
|
+
if (this.watchers.size >= WATCH.maxWatchers) {
|
|
55
|
+
throw new Error(`Maximum watcher limit (${WATCH.maxWatchers}) reached. ` + `Stop an existing watcher before starting a new one.`);
|
|
67
56
|
}
|
|
68
57
|
const fullConfig = {
|
|
69
58
|
projectPath: config.projectPath,
|
|
70
59
|
projectId: config.projectId,
|
|
71
60
|
tsconfigPath: config.tsconfigPath,
|
|
72
|
-
debounceMs: config.debounceMs ??
|
|
73
|
-
excludePatterns: config.excludePatterns ??
|
|
61
|
+
debounceMs: config.debounceMs ?? WATCH.defaultDebounceMs,
|
|
62
|
+
excludePatterns: config.excludePatterns ?? [...WATCH.excludePatterns],
|
|
74
63
|
};
|
|
75
64
|
await debugLog('Creating @parcel/watcher subscription', {
|
|
76
65
|
watchPath: fullConfig.projectPath,
|
|
@@ -154,9 +143,9 @@ class WatchManager {
|
|
|
154
143
|
timestamp: Date.now(),
|
|
155
144
|
};
|
|
156
145
|
// Prevent unbounded event accumulation - drop oldest events if buffer is full
|
|
157
|
-
if (state.pendingEvents.length >=
|
|
146
|
+
if (state.pendingEvents.length >= WATCH.maxPendingEvents) {
|
|
158
147
|
debugLog('Event buffer full, dropping oldest events', { projectId: state.projectId });
|
|
159
|
-
state.pendingEvents = state.pendingEvents.slice(-Math.floor(
|
|
148
|
+
state.pendingEvents = state.pendingEvents.slice(-Math.floor(WATCH.maxPendingEvents / 2));
|
|
160
149
|
}
|
|
161
150
|
state.pendingEvents.push(event);
|
|
162
151
|
debugLog('Event added to pending', { pendingCount: state.pendingEvents.length });
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect Dead Code Tool
|
|
3
|
+
* Identifies potentially unused code in the codebase
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { toNumber, isUIComponent, isPackageExport, isExcludedByPattern, getShortPath, } from '../../core/utils/shared-utils.js';
|
|
7
|
+
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
8
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
9
|
+
import { createErrorResponse, createSuccessResponse, debugLog, resolveProjectIdOrError } from '../utils.js';
|
|
10
|
+
// Default file patterns to exclude
|
|
11
|
+
const DEFAULT_ENTRY_POINT_FILE_PATTERNS = [
|
|
12
|
+
// Common entry points
|
|
13
|
+
'main.ts',
|
|
14
|
+
'app.ts',
|
|
15
|
+
'index.ts',
|
|
16
|
+
// NestJS
|
|
17
|
+
'*.module.ts',
|
|
18
|
+
'*.controller.ts',
|
|
19
|
+
// Fastify / Express
|
|
20
|
+
'*.routes.ts',
|
|
21
|
+
'*.router.ts',
|
|
22
|
+
'*.handler.ts',
|
|
23
|
+
'server.ts',
|
|
24
|
+
// Next.js / React frameworks (file-based routing)
|
|
25
|
+
'page.tsx',
|
|
26
|
+
'page.ts',
|
|
27
|
+
'layout.tsx',
|
|
28
|
+
'layout.ts',
|
|
29
|
+
'route.tsx',
|
|
30
|
+
'route.ts',
|
|
31
|
+
'loading.tsx',
|
|
32
|
+
'error.tsx',
|
|
33
|
+
'not-found.tsx',
|
|
34
|
+
'template.tsx',
|
|
35
|
+
'default.tsx',
|
|
36
|
+
// Remix
|
|
37
|
+
'root.tsx',
|
|
38
|
+
// Astro
|
|
39
|
+
'*.astro',
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Determine confidence level based on code characteristics.
|
|
43
|
+
* Returns both the level and a human-readable explanation.
|
|
44
|
+
*/
|
|
45
|
+
const determineConfidence = (item) => {
|
|
46
|
+
// HIGH: Exported but definitively never imported
|
|
47
|
+
if (item.isExported && item.reason.includes('never imported')) {
|
|
48
|
+
return { level: 'HIGH', reason: 'Exported but never imported anywhere' };
|
|
49
|
+
}
|
|
50
|
+
// MEDIUM: Private with no internal calls
|
|
51
|
+
if (item.visibility === 'private') {
|
|
52
|
+
return { level: 'MEDIUM', reason: 'Private method with no internal callers' };
|
|
53
|
+
}
|
|
54
|
+
// LOW: Could be used dynamically
|
|
55
|
+
return { level: 'LOW', reason: 'Could be used via dynamic references' };
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Calculate risk level based on dead code count.
|
|
59
|
+
*/
|
|
60
|
+
const getRiskLevel = (totalCount, highCount) => {
|
|
61
|
+
if (highCount >= 20 || totalCount >= 50)
|
|
62
|
+
return 'CRITICAL';
|
|
63
|
+
if (highCount >= 10 || totalCount >= 25)
|
|
64
|
+
return 'HIGH';
|
|
65
|
+
if (highCount >= 5 || totalCount >= 10)
|
|
66
|
+
return 'MEDIUM';
|
|
67
|
+
return 'LOW';
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Check if confidence meets minimum threshold.
|
|
71
|
+
*/
|
|
72
|
+
const shouldInclude = (confidence, minConfidence) => {
|
|
73
|
+
const levels = ['LOW', 'MEDIUM', 'HIGH'];
|
|
74
|
+
return levels.indexOf(confidence) >= levels.indexOf(minConfidence);
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Check if semantic type is excluded.
|
|
78
|
+
*/
|
|
79
|
+
const isExcludedBySemanticType = (semanticType, excludeTypes) => {
|
|
80
|
+
return semanticType != null && excludeTypes.includes(semanticType);
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Determine category of dead code item based on file path.
|
|
84
|
+
*/
|
|
85
|
+
const determineCategory = (filePath) => {
|
|
86
|
+
if (isUIComponent(filePath))
|
|
87
|
+
return 'ui-component';
|
|
88
|
+
if (isPackageExport(filePath))
|
|
89
|
+
return 'library-export';
|
|
90
|
+
return 'internal-unused';
|
|
91
|
+
};
|
|
92
|
+
export const createDetectDeadCodeTool = (server) => {
|
|
93
|
+
server.registerTool(TOOL_NAMES.detectDeadCode, {
|
|
94
|
+
title: TOOL_METADATA[TOOL_NAMES.detectDeadCode].title,
|
|
95
|
+
description: TOOL_METADATA[TOOL_NAMES.detectDeadCode].description,
|
|
96
|
+
inputSchema: {
|
|
97
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
98
|
+
excludePatterns: z
|
|
99
|
+
.array(z.string())
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Additional file patterns to exclude as entry points (e.g., ["*.config.ts", "*.seed.ts"])'),
|
|
102
|
+
excludeSemanticTypes: z
|
|
103
|
+
.array(z.string())
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Additional semantic types to exclude (e.g., ["EntityClass", "DTOClass"])'),
|
|
106
|
+
includeEntryPoints: z
|
|
107
|
+
.boolean()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Include excluded entry points in a separate audit section for review (default: true). ' +
|
|
110
|
+
'Entry points are always excluded from main results.')
|
|
111
|
+
.default(true),
|
|
112
|
+
minConfidence: z
|
|
113
|
+
.enum(['LOW', 'MEDIUM', 'HIGH'])
|
|
114
|
+
.optional()
|
|
115
|
+
.describe('Minimum confidence level to include in results (default: LOW)')
|
|
116
|
+
.default('LOW'),
|
|
117
|
+
summaryOnly: z
|
|
118
|
+
.boolean()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe('Return only summary statistics without full dead code list (default: false)')
|
|
121
|
+
.default(false),
|
|
122
|
+
limit: z
|
|
123
|
+
.number()
|
|
124
|
+
.int()
|
|
125
|
+
.min(1)
|
|
126
|
+
.max(500)
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Maximum number of dead code items to return per page (default: 100)')
|
|
129
|
+
.default(100),
|
|
130
|
+
offset: z
|
|
131
|
+
.number()
|
|
132
|
+
.int()
|
|
133
|
+
.min(0)
|
|
134
|
+
.optional()
|
|
135
|
+
.describe('Number of items to skip for pagination (default: 0)')
|
|
136
|
+
.default(0),
|
|
137
|
+
filterCategory: z
|
|
138
|
+
.enum(['library-export', 'ui-component', 'internal-unused', 'all'])
|
|
139
|
+
.optional()
|
|
140
|
+
.describe('Filter by category: library-export, ui-component, internal-unused, or all (default: all)')
|
|
141
|
+
.default('all'),
|
|
142
|
+
excludeLibraryExports: z
|
|
143
|
+
.boolean()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe('Exclude all items from packages/* directories (default: false)')
|
|
146
|
+
.default(false),
|
|
147
|
+
excludeCoreTypes: z
|
|
148
|
+
.array(z.string())
|
|
149
|
+
.optional()
|
|
150
|
+
.describe('Exclude specific core types from results (e.g., ["InterfaceDeclaration", "TypeAliasDeclaration"] to skip type definitions)')
|
|
151
|
+
.default([]),
|
|
152
|
+
},
|
|
153
|
+
}, async ({ projectId, excludePatterns = [], excludeSemanticTypes = [], includeEntryPoints = true, minConfidence = 'LOW', summaryOnly = false, limit = 100, offset = 0, filterCategory = 'all', excludeLibraryExports = false, excludeCoreTypes = [], }) => {
|
|
154
|
+
const neo4jService = new Neo4jService();
|
|
155
|
+
try {
|
|
156
|
+
// Resolve project ID
|
|
157
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
158
|
+
if (!projectResult.success)
|
|
159
|
+
return projectResult.error;
|
|
160
|
+
const resolvedProjectId = projectResult.projectId;
|
|
161
|
+
await debugLog('Dead code detection started', {
|
|
162
|
+
projectId: resolvedProjectId,
|
|
163
|
+
excludePatterns,
|
|
164
|
+
excludeSemanticTypes,
|
|
165
|
+
minConfidence,
|
|
166
|
+
});
|
|
167
|
+
// Query project's actual semantic types (data-driven, per-project detection)
|
|
168
|
+
const semanticTypesResult = (await neo4jService.run(QUERIES.GET_PROJECT_SEMANTIC_TYPES, {
|
|
169
|
+
projectId: resolvedProjectId,
|
|
170
|
+
}));
|
|
171
|
+
const projectSemanticTypes = semanticTypesResult.map((r) => r.semanticType);
|
|
172
|
+
// Combine project semantic types with user-provided exclusions
|
|
173
|
+
const allExcludeSemanticTypes = [...projectSemanticTypes, ...excludeSemanticTypes];
|
|
174
|
+
const allExcludePatterns = [...DEFAULT_ENTRY_POINT_FILE_PATTERNS, ...excludePatterns];
|
|
175
|
+
// Run all queries in parallel for better performance
|
|
176
|
+
const [unreferencedExports, uncalledPrivateMethods, unreferencedInterfaces, entryPointsResult] = await Promise.all([
|
|
177
|
+
// 1. Find unreferenced exports
|
|
178
|
+
neo4jService.run(QUERIES.FIND_UNREFERENCED_EXPORTS, {
|
|
179
|
+
projectId: resolvedProjectId,
|
|
180
|
+
}),
|
|
181
|
+
// 2. Find uncalled private methods
|
|
182
|
+
neo4jService.run(QUERIES.FIND_UNCALLED_PRIVATE_METHODS, {
|
|
183
|
+
projectId: resolvedProjectId,
|
|
184
|
+
}),
|
|
185
|
+
// 3. Find unreferenced interfaces
|
|
186
|
+
neo4jService.run(QUERIES.FIND_UNREFERENCED_INTERFACES, {
|
|
187
|
+
projectId: resolvedProjectId,
|
|
188
|
+
}),
|
|
189
|
+
// 4. Get framework entry points for exclusion/audit (using project's semantic types)
|
|
190
|
+
neo4jService.run(QUERIES.GET_FRAMEWORK_ENTRY_POINTS, {
|
|
191
|
+
projectId: resolvedProjectId,
|
|
192
|
+
semanticTypes: allExcludeSemanticTypes,
|
|
193
|
+
}),
|
|
194
|
+
]);
|
|
195
|
+
// Create set of entry point IDs for filtering
|
|
196
|
+
const entryPointIds = new Set(entryPointsResult.map((r) => r.nodeId));
|
|
197
|
+
// Process and filter results
|
|
198
|
+
const deadCodeItems = [];
|
|
199
|
+
// Process unreferenced exports
|
|
200
|
+
for (const item of unreferencedExports) {
|
|
201
|
+
if (entryPointIds.has(item.nodeId))
|
|
202
|
+
continue;
|
|
203
|
+
if (isExcludedByPattern(item.filePath, allExcludePatterns))
|
|
204
|
+
continue;
|
|
205
|
+
if (isExcludedBySemanticType(item.semanticType, allExcludeSemanticTypes))
|
|
206
|
+
continue;
|
|
207
|
+
const confidence = determineConfidence({
|
|
208
|
+
isExported: true,
|
|
209
|
+
coreType: item.coreType,
|
|
210
|
+
reason: item.reason,
|
|
211
|
+
});
|
|
212
|
+
if (shouldInclude(confidence.level, minConfidence)) {
|
|
213
|
+
const category = determineCategory(item.filePath);
|
|
214
|
+
deadCodeItems.push({
|
|
215
|
+
nodeId: item.nodeId,
|
|
216
|
+
name: item.name,
|
|
217
|
+
type: item.coreType,
|
|
218
|
+
coreType: item.coreType,
|
|
219
|
+
semanticType: item.semanticType ?? null,
|
|
220
|
+
filePath: item.filePath,
|
|
221
|
+
lineNumber: toNumber(item.lineNumber),
|
|
222
|
+
confidence: confidence.level,
|
|
223
|
+
confidenceReason: confidence.reason,
|
|
224
|
+
category,
|
|
225
|
+
reason: item.reason,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Process uncalled private methods
|
|
230
|
+
for (const item of uncalledPrivateMethods) {
|
|
231
|
+
// Apply same exclusion checks as other dead code types
|
|
232
|
+
if (entryPointIds.has(item.nodeId))
|
|
233
|
+
continue;
|
|
234
|
+
if (isExcludedByPattern(item.filePath, allExcludePatterns))
|
|
235
|
+
continue;
|
|
236
|
+
if (isExcludedBySemanticType(item.semanticType, allExcludeSemanticTypes))
|
|
237
|
+
continue;
|
|
238
|
+
const confidence = determineConfidence({
|
|
239
|
+
isExported: false,
|
|
240
|
+
visibility: 'private',
|
|
241
|
+
coreType: item.coreType,
|
|
242
|
+
reason: item.reason,
|
|
243
|
+
});
|
|
244
|
+
if (shouldInclude(confidence.level, minConfidence)) {
|
|
245
|
+
const category = determineCategory(item.filePath);
|
|
246
|
+
deadCodeItems.push({
|
|
247
|
+
nodeId: item.nodeId,
|
|
248
|
+
name: item.name,
|
|
249
|
+
type: item.coreType,
|
|
250
|
+
coreType: item.coreType,
|
|
251
|
+
semanticType: item.semanticType ?? null,
|
|
252
|
+
filePath: item.filePath,
|
|
253
|
+
lineNumber: toNumber(item.lineNumber),
|
|
254
|
+
confidence: confidence.level,
|
|
255
|
+
confidenceReason: confidence.reason,
|
|
256
|
+
category,
|
|
257
|
+
reason: item.reason,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Process unreferenced interfaces
|
|
262
|
+
for (const item of unreferencedInterfaces) {
|
|
263
|
+
if (entryPointIds.has(item.nodeId))
|
|
264
|
+
continue;
|
|
265
|
+
if (isExcludedByPattern(item.filePath, allExcludePatterns))
|
|
266
|
+
continue;
|
|
267
|
+
const confidence = determineConfidence({
|
|
268
|
+
isExported: true,
|
|
269
|
+
coreType: item.coreType,
|
|
270
|
+
reason: item.reason,
|
|
271
|
+
});
|
|
272
|
+
if (shouldInclude(confidence.level, minConfidence)) {
|
|
273
|
+
const category = determineCategory(item.filePath);
|
|
274
|
+
deadCodeItems.push({
|
|
275
|
+
nodeId: item.nodeId,
|
|
276
|
+
name: item.name,
|
|
277
|
+
type: item.coreType,
|
|
278
|
+
coreType: item.coreType,
|
|
279
|
+
semanticType: item.semanticType ?? null,
|
|
280
|
+
filePath: item.filePath,
|
|
281
|
+
lineNumber: toNumber(item.lineNumber),
|
|
282
|
+
confidence: confidence.level,
|
|
283
|
+
confidenceReason: confidence.reason,
|
|
284
|
+
category,
|
|
285
|
+
reason: item.reason,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Apply exclusion filters
|
|
290
|
+
let filteredItems = deadCodeItems;
|
|
291
|
+
// Exclude library exports if requested
|
|
292
|
+
if (excludeLibraryExports) {
|
|
293
|
+
filteredItems = filteredItems.filter((i) => i.category !== 'library-export');
|
|
294
|
+
}
|
|
295
|
+
// Exclude specific core types if requested
|
|
296
|
+
if (excludeCoreTypes.length > 0) {
|
|
297
|
+
filteredItems = filteredItems.filter((i) => !excludeCoreTypes.includes(i.coreType));
|
|
298
|
+
}
|
|
299
|
+
// Apply category filter if specified
|
|
300
|
+
if (filterCategory !== 'all') {
|
|
301
|
+
filteredItems = filteredItems.filter((i) => i.category === filterCategory);
|
|
302
|
+
}
|
|
303
|
+
// Calculate statistics on ALL items (before filtering)
|
|
304
|
+
const byConfidence = {
|
|
305
|
+
HIGH: deadCodeItems.filter((i) => i.confidence === 'HIGH').length,
|
|
306
|
+
MEDIUM: deadCodeItems.filter((i) => i.confidence === 'MEDIUM').length,
|
|
307
|
+
LOW: deadCodeItems.filter((i) => i.confidence === 'LOW').length,
|
|
308
|
+
};
|
|
309
|
+
const byCategory = {
|
|
310
|
+
'library-export': deadCodeItems.filter((i) => i.category === 'library-export').length,
|
|
311
|
+
'ui-component': deadCodeItems.filter((i) => i.category === 'ui-component').length,
|
|
312
|
+
'internal-unused': deadCodeItems.filter((i) => i.category === 'internal-unused').length,
|
|
313
|
+
};
|
|
314
|
+
const byType = {};
|
|
315
|
+
for (const item of deadCodeItems) {
|
|
316
|
+
byType[item.type] = (byType[item.type] ?? 0) + 1;
|
|
317
|
+
}
|
|
318
|
+
// Use filtered items for affected files and output
|
|
319
|
+
const affectedFiles = [...new Set(filteredItems.map((i) => i.filePath))].sort();
|
|
320
|
+
const riskLevel = getRiskLevel(filteredItems.length, byConfidence.HIGH);
|
|
321
|
+
// Build entry points list for audit
|
|
322
|
+
const excludedEntryPoints = includeEntryPoints
|
|
323
|
+
? entryPointsResult.map((r) => ({
|
|
324
|
+
nodeId: r.nodeId,
|
|
325
|
+
name: r.name,
|
|
326
|
+
type: r.coreType ?? 'Unknown',
|
|
327
|
+
semanticType: r.semanticType ?? null,
|
|
328
|
+
filePath: r.filePath,
|
|
329
|
+
}))
|
|
330
|
+
: [];
|
|
331
|
+
// Build summary based on filter
|
|
332
|
+
const filterSuffix = filterCategory !== 'all' ? ` (filtered to ${filterCategory})` : '';
|
|
333
|
+
const summary = filteredItems.length === 0
|
|
334
|
+
? 'No potentially dead code found' + filterSuffix
|
|
335
|
+
: `Found ${filteredItems.length} potentially dead code items across ${affectedFiles.length} files` +
|
|
336
|
+
filterSuffix;
|
|
337
|
+
// Count entry points (always available)
|
|
338
|
+
const excludedEntryPointsCount = entryPointsResult.length;
|
|
339
|
+
// Compute top files by dead code count (used in both modes)
|
|
340
|
+
const fileDeadCodeCounts = {};
|
|
341
|
+
for (const item of filteredItems) {
|
|
342
|
+
const shortPath = getShortPath(item.filePath);
|
|
343
|
+
fileDeadCodeCounts[shortPath] = (fileDeadCodeCounts[shortPath] ?? 0) + 1;
|
|
344
|
+
}
|
|
345
|
+
const topFilesByDeadCode = Object.entries(fileDeadCodeCounts)
|
|
346
|
+
.sort((a, b) => b[1] - a[1])
|
|
347
|
+
.slice(0, 20)
|
|
348
|
+
.map(([file, count]) => ({ file, count }));
|
|
349
|
+
// Build result based on summaryOnly flag
|
|
350
|
+
let result;
|
|
351
|
+
if (summaryOnly) {
|
|
352
|
+
// Summary mode: statistics only, no full arrays
|
|
353
|
+
result = {
|
|
354
|
+
summary,
|
|
355
|
+
riskLevel,
|
|
356
|
+
totalCount: filteredItems.length,
|
|
357
|
+
totalBeforeFilter: deadCodeItems.length,
|
|
358
|
+
byConfidence,
|
|
359
|
+
byCategory,
|
|
360
|
+
byType,
|
|
361
|
+
affectedFiles,
|
|
362
|
+
topFilesByDeadCode,
|
|
363
|
+
excludedEntryPointsCount,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// Paginated mode: apply limit/offset
|
|
368
|
+
const paginatedItems = filteredItems.slice(offset, offset + limit);
|
|
369
|
+
const hasMore = offset + limit < filteredItems.length;
|
|
370
|
+
result = {
|
|
371
|
+
summary,
|
|
372
|
+
riskLevel,
|
|
373
|
+
totalCount: filteredItems.length,
|
|
374
|
+
totalBeforeFilter: deadCodeItems.length,
|
|
375
|
+
byConfidence,
|
|
376
|
+
byCategory,
|
|
377
|
+
byType,
|
|
378
|
+
topFilesByDeadCode,
|
|
379
|
+
deadCode: paginatedItems,
|
|
380
|
+
pagination: {
|
|
381
|
+
offset,
|
|
382
|
+
limit,
|
|
383
|
+
returned: paginatedItems.length,
|
|
384
|
+
hasMore,
|
|
385
|
+
},
|
|
386
|
+
excludedEntryPointsCount,
|
|
387
|
+
// Only include full entry points array on first page
|
|
388
|
+
...(offset === 0 && includeEntryPoints ? { excludedEntryPoints } : {}),
|
|
389
|
+
affectedFiles,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
await debugLog('Dead code detection complete', {
|
|
393
|
+
projectId: resolvedProjectId,
|
|
394
|
+
totalCount: deadCodeItems.length,
|
|
395
|
+
filteredCount: filteredItems.length,
|
|
396
|
+
filterCategory,
|
|
397
|
+
riskLevel,
|
|
398
|
+
summaryOnly,
|
|
399
|
+
offset,
|
|
400
|
+
limit,
|
|
401
|
+
});
|
|
402
|
+
return createSuccessResponse(JSON.stringify(result, null, 2));
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
console.error('Dead code detection error:', error);
|
|
406
|
+
await debugLog('Dead code detection error', { projectId, error });
|
|
407
|
+
return createErrorResponse(error);
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
await neo4jService.close();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
};
|