code-graph-context 2.0.0 → 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.
Files changed (34) hide show
  1. package/README.md +156 -2
  2. package/dist/constants.js +167 -0
  3. package/dist/core/config/fairsquare-framework-schema.js +9 -7
  4. package/dist/core/config/nestjs-framework-schema.js +60 -43
  5. package/dist/core/config/schema.js +41 -2
  6. package/dist/core/embeddings/natural-language-to-cypher.service.js +166 -110
  7. package/dist/core/parsers/typescript-parser.js +1043 -747
  8. package/dist/core/parsers/workspace-parser.js +177 -194
  9. package/dist/core/utils/code-normalizer.js +299 -0
  10. package/dist/core/utils/file-change-detection.js +17 -2
  11. package/dist/core/utils/file-utils.js +40 -5
  12. package/dist/core/utils/graph-factory.js +161 -0
  13. package/dist/core/utils/shared-utils.js +79 -0
  14. package/dist/core/workspace/workspace-detector.js +59 -5
  15. package/dist/mcp/constants.js +141 -8
  16. package/dist/mcp/handlers/graph-generator.handler.js +1 -0
  17. package/dist/mcp/handlers/incremental-parse.handler.js +3 -6
  18. package/dist/mcp/handlers/parallel-import.handler.js +136 -0
  19. package/dist/mcp/handlers/streaming-import.handler.js +14 -59
  20. package/dist/mcp/mcp.server.js +1 -1
  21. package/dist/mcp/services/job-manager.js +5 -8
  22. package/dist/mcp/services/watch-manager.js +7 -18
  23. package/dist/mcp/tools/detect-dead-code.tool.js +413 -0
  24. package/dist/mcp/tools/detect-duplicate-code.tool.js +450 -0
  25. package/dist/mcp/tools/impact-analysis.tool.js +20 -4
  26. package/dist/mcp/tools/index.js +4 -0
  27. package/dist/mcp/tools/parse-typescript-project.tool.js +15 -14
  28. package/dist/mcp/workers/chunk-worker-pool.js +196 -0
  29. package/dist/mcp/workers/chunk-worker.types.js +4 -0
  30. package/dist/mcp/workers/chunk.worker.js +89 -0
  31. package/dist/mcp/workers/parse-coordinator.js +183 -0
  32. package/dist/mcp/workers/worker.pool.js +54 -0
  33. package/dist/storage/neo4j/neo4j.service.js +190 -10
  34. 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
- }, CLEANUP_INTERVAL_MS);
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 >= MAX_JOBS) {
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 >= MAX_JOBS) {
64
- throw new Error(`Maximum job limit (${MAX_JOBS}) reached. ` +
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 >= MAX_WATCHERS) {
66
- throw new Error(`Maximum watcher limit (${MAX_WATCHERS}) reached. ` + `Stop an existing watcher before starting a new one.`);
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 ?? DEFAULT_DEBOUNCE_MS,
73
- excludePatterns: config.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS,
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 >= MAX_PENDING_EVENTS) {
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(MAX_PENDING_EVENTS / 2));
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
+ };