mcp-adr-analysis-server 2.1.22 → 2.1.27

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 (40) hide show
  1. package/README.md +63 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/index.js +370 -0
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/src/tools/adr-aggregator-tools.d.ts +185 -0
  7. package/dist/src/tools/adr-aggregator-tools.d.ts.map +1 -0
  8. package/dist/src/tools/adr-aggregator-tools.js +872 -0
  9. package/dist/src/tools/adr-aggregator-tools.js.map +1 -0
  10. package/dist/src/tools/analyze-gaps-tool.d.ts +57 -0
  11. package/dist/src/tools/analyze-gaps-tool.d.ts.map +1 -0
  12. package/dist/src/tools/analyze-gaps-tool.js +727 -0
  13. package/dist/src/tools/analyze-gaps-tool.js.map +1 -0
  14. package/dist/src/tools/tool-catalog.d.ts +1 -1
  15. package/dist/src/tools/tool-catalog.d.ts.map +1 -1
  16. package/dist/src/tools/tool-catalog.js +382 -0
  17. package/dist/src/tools/tool-catalog.js.map +1 -1
  18. package/dist/src/tools/tool-dispatcher.d.ts.map +1 -1
  19. package/dist/src/tools/tool-dispatcher.js +4 -0
  20. package/dist/src/tools/tool-dispatcher.js.map +1 -1
  21. package/dist/src/types/adr-aggregator.d.ts +692 -0
  22. package/dist/src/types/adr-aggregator.d.ts.map +1 -0
  23. package/dist/src/types/adr-aggregator.js +8 -0
  24. package/dist/src/types/adr-aggregator.js.map +1 -0
  25. package/dist/src/types/index.d.ts +3 -2
  26. package/dist/src/types/index.d.ts.map +1 -1
  27. package/dist/src/types/memory-entities.d.ts +48 -48
  28. package/dist/src/utils/adr-aggregator-client.d.ts +177 -0
  29. package/dist/src/utils/adr-aggregator-client.d.ts.map +1 -0
  30. package/dist/src/utils/adr-aggregator-client.js +396 -0
  31. package/dist/src/utils/adr-aggregator-client.js.map +1 -0
  32. package/dist/src/utils/config.d.ts +3 -0
  33. package/dist/src/utils/config.d.ts.map +1 -1
  34. package/dist/src/utils/config.js +13 -0
  35. package/dist/src/utils/config.js.map +1 -1
  36. package/dist/src/utils/git-remote-detector.d.ts +67 -0
  37. package/dist/src/utils/git-remote-detector.d.ts.map +1 -0
  38. package/dist/src/utils/git-remote-detector.js +194 -0
  39. package/dist/src/utils/git-remote-detector.js.map +1 -0
  40. package/package.json +1 -1
@@ -0,0 +1,727 @@
1
+ /**
2
+ * Analyze Gaps Tool
3
+ * Scans local codebase and compares with ADRs to detect bi-directional gaps.
4
+ *
5
+ * Workflow:
6
+ * 1. Fetch ADRs (local or from aggregator)
7
+ * 2. Scan local files for:
8
+ * - File references in ADRs that don't exist
9
+ * - Technologies in package.json without ADR coverage
10
+ * - Architectural patterns (WebSocket, Redis, auth, etc.)
11
+ * 3. Report gaps to ADR Aggregator
12
+ */
13
+ import { promises as fs, existsSync } from 'fs';
14
+ import path from 'path';
15
+ import { McpAdrError } from '../types/index.js';
16
+ import { getAdrAggregatorClient, } from '../utils/adr-aggregator-client.js';
17
+ import { loadConfig } from '../utils/config.js';
18
+ import { isGitRepository } from '../utils/git-remote-detector.js';
19
+ // ============================================================================
20
+ // Technology Detection Patterns
21
+ // ============================================================================
22
+ /**
23
+ * Technology patterns to detect in package.json dependencies
24
+ */
25
+ const TECHNOLOGY_PATTERNS = {
26
+ redis: {
27
+ keywords: ['redis', 'ioredis', '@redis/client'],
28
+ suggestedAdrTitle: 'Caching Strategy with Redis',
29
+ },
30
+ postgresql: {
31
+ keywords: ['pg', 'postgres', 'postgresql', '@prisma/client', 'typeorm', 'knex'],
32
+ suggestedAdrTitle: 'Database Strategy with PostgreSQL',
33
+ },
34
+ mongodb: {
35
+ keywords: ['mongodb', 'mongoose', 'monk'],
36
+ suggestedAdrTitle: 'Database Strategy with MongoDB',
37
+ },
38
+ elasticsearch: {
39
+ keywords: ['elasticsearch', '@elastic/elasticsearch', 'opensearch'],
40
+ suggestedAdrTitle: 'Search Infrastructure with Elasticsearch',
41
+ },
42
+ rabbitmq: {
43
+ keywords: ['amqplib', 'amqp-connection-manager', 'rabbitmq'],
44
+ suggestedAdrTitle: 'Message Queue Strategy with RabbitMQ',
45
+ },
46
+ kafka: {
47
+ keywords: ['kafkajs', 'kafka-node', 'node-rdkafka'],
48
+ suggestedAdrTitle: 'Event Streaming with Kafka',
49
+ },
50
+ graphql: {
51
+ keywords: ['graphql', 'apollo-server', '@apollo/server', 'type-graphql', 'nexus'],
52
+ suggestedAdrTitle: 'API Strategy with GraphQL',
53
+ },
54
+ websocket: {
55
+ keywords: ['ws', 'socket.io', 'socket.io-client', '@socket.io/redis-adapter'],
56
+ suggestedAdrTitle: 'Real-time Communication with WebSockets',
57
+ },
58
+ grpc: {
59
+ keywords: ['grpc', '@grpc/grpc-js', '@grpc/proto-loader'],
60
+ suggestedAdrTitle: 'Service Communication with gRPC',
61
+ },
62
+ kubernetes: {
63
+ keywords: ['@kubernetes/client-node', 'kubernetes-client'],
64
+ suggestedAdrTitle: 'Container Orchestration with Kubernetes',
65
+ },
66
+ docker: {
67
+ keywords: ['dockerode', 'docker-compose'],
68
+ suggestedAdrTitle: 'Containerization Strategy with Docker',
69
+ },
70
+ aws: {
71
+ keywords: ['aws-sdk', '@aws-sdk/', 'aws-amplify'],
72
+ suggestedAdrTitle: 'Cloud Infrastructure with AWS',
73
+ },
74
+ gcp: {
75
+ keywords: ['@google-cloud/', 'firebase', 'firebase-admin', 'firebase-functions'],
76
+ suggestedAdrTitle: 'Cloud Infrastructure with Google Cloud',
77
+ },
78
+ azure: {
79
+ keywords: ['@azure/', 'azure-storage'],
80
+ suggestedAdrTitle: 'Cloud Infrastructure with Azure',
81
+ },
82
+ auth: {
83
+ keywords: ['passport', 'jsonwebtoken', 'bcrypt', 'oauth', '@auth0/', 'next-auth', 'clerk'],
84
+ suggestedAdrTitle: 'Authentication and Authorization Strategy',
85
+ },
86
+ testing: {
87
+ keywords: ['jest', 'mocha', 'vitest', 'cypress', 'playwright', '@testing-library/'],
88
+ suggestedAdrTitle: 'Testing Strategy and Framework Selection',
89
+ },
90
+ monitoring: {
91
+ keywords: ['prom-client', 'newrelic', '@sentry/', 'datadog', '@opentelemetry/'],
92
+ suggestedAdrTitle: 'Observability and Monitoring Strategy',
93
+ },
94
+ react: {
95
+ keywords: ['react', 'react-dom', 'next', '@remix-run/'],
96
+ suggestedAdrTitle: 'Frontend Framework Selection - React',
97
+ },
98
+ vue: {
99
+ keywords: ['vue', 'nuxt', '@vue/'],
100
+ suggestedAdrTitle: 'Frontend Framework Selection - Vue',
101
+ },
102
+ angular: {
103
+ keywords: ['@angular/core', '@angular/cli'],
104
+ suggestedAdrTitle: 'Frontend Framework Selection - Angular',
105
+ },
106
+ };
107
+ /**
108
+ * Architectural patterns to detect in source code
109
+ */
110
+ const ARCHITECTURAL_PATTERNS = {
111
+ microservices: {
112
+ filePatterns: [/docker-compose\.ya?ml$/i, /k8s\/.*\.ya?ml$/i, /\.?helm\//i],
113
+ codePatterns: [/service-discovery/i, /circuit-?breaker/i, /api-gateway/i],
114
+ suggestedAdrTitle: 'Microservices Architecture',
115
+ },
116
+ eventSourcing: {
117
+ filePatterns: [/events?\//i, /event-store/i],
118
+ codePatterns: [/event-?sourcing/i, /event-?store/i, /aggregate-?root/i],
119
+ suggestedAdrTitle: 'Event Sourcing Pattern',
120
+ },
121
+ cqrs: {
122
+ filePatterns: [/commands?\//i, /queries?\//i],
123
+ codePatterns: [/command-?handler/i, /query-?handler/i, /cqrs/i],
124
+ suggestedAdrTitle: 'CQRS Pattern Implementation',
125
+ },
126
+ domainDrivenDesign: {
127
+ filePatterns: [/domain\//i, /aggregates?\//i, /entities?\//i, /value-?objects?\//i],
128
+ codePatterns: [/bounded-?context/i, /aggregate/i, /domain-?event/i],
129
+ suggestedAdrTitle: 'Domain-Driven Design Adoption',
130
+ },
131
+ cleanArchitecture: {
132
+ filePatterns: [
133
+ /use-?cases?\//i,
134
+ /adapters?\//i,
135
+ /infrastructure\//i,
136
+ /presentation\//i,
137
+ /core\//i,
138
+ ],
139
+ codePatterns: [/use-?case/i, /presenter/i, /repository-?interface/i],
140
+ suggestedAdrTitle: 'Clean Architecture Implementation',
141
+ },
142
+ serverless: {
143
+ filePatterns: [/serverless\.ya?ml$/i, /netlify\.toml$/i, /vercel\.json$/i],
144
+ codePatterns: [/lambda-?handler/i, /cloud-?function/i],
145
+ suggestedAdrTitle: 'Serverless Architecture',
146
+ },
147
+ };
148
+ // ============================================================================
149
+ // Helper Functions
150
+ // ============================================================================
151
+ /**
152
+ * Discover ADR files in the project
153
+ */
154
+ async function discoverLocalAdrs(projectPath, adrDirectory) {
155
+ const adrPath = path.resolve(projectPath, adrDirectory);
156
+ const adrFiles = [];
157
+ try {
158
+ const files = await fs.readdir(adrPath);
159
+ for (const file of files) {
160
+ if (file.endsWith('.md')) {
161
+ adrFiles.push(path.join(adrPath, file));
162
+ }
163
+ }
164
+ }
165
+ catch {
166
+ // Directory doesn't exist or not readable
167
+ }
168
+ return adrFiles;
169
+ }
170
+ /**
171
+ * Normalize a file path and verify if it exists
172
+ *
173
+ * Handles various path formats:
174
+ * - ./src/file.ts → src/file.ts (strips leading ./)
175
+ * - ../shared/types.ts → ../shared/types.ts (preserves parent refs)
176
+ * - src/file.ts → src/file.ts (no change needed)
177
+ *
178
+ * @param referencedPath - The path as found in the ADR
179
+ * @param basePath - The base path to resolve relative paths against (usually repo root)
180
+ * @returns Object with exists flag and normalized path
181
+ */
182
+ function normalizeAndVerifyPath(referencedPath, basePath) {
183
+ // Normalize the path:
184
+ // - Strip leading ./ (current directory)
185
+ // - Preserve ../ (parent directory references)
186
+ // - Handle multiple leading ./
187
+ const normalized = referencedPath
188
+ .replace(/^(\.\/)+/, '') // Remove one or more leading ./
189
+ .trim();
190
+ // If path is empty after normalization, it's invalid
191
+ if (!normalized) {
192
+ return {
193
+ exists: false,
194
+ normalizedPath: referencedPath,
195
+ resolvedPath: '',
196
+ };
197
+ }
198
+ // Resolve the full path relative to basePath
199
+ const fullPath = path.resolve(basePath, normalized);
200
+ // Check if file exists
201
+ const exists = existsSync(fullPath);
202
+ return {
203
+ exists,
204
+ normalizedPath: normalized,
205
+ resolvedPath: fullPath,
206
+ };
207
+ }
208
+ /**
209
+ * Simple path normalization (for use in extraction)
210
+ * Strips leading ./ prefix only
211
+ */
212
+ function normalizeFilePath(ref) {
213
+ return ref.replace(/^(\.\/)+/, '').trim();
214
+ }
215
+ /**
216
+ * Extract file references from ADR content
217
+ * Looks for patterns like:
218
+ * - `src/file.ts` or `./src/file.ts`
219
+ * - [link](./path/to/file.ts)
220
+ * - References to: src/auth/oauth.ts
221
+ *
222
+ * Note: All paths are normalized by stripping leading ./ prefix
223
+ */
224
+ function extractFileReferences(content) {
225
+ const references = new Set();
226
+ // Match backtick code references: `src/file.ts` or `./src/file.ts`
227
+ const backtickPattern = /`([^`]+\.[a-zA-Z]{2,4})`/g;
228
+ let match;
229
+ while ((match = backtickPattern.exec(content)) !== null) {
230
+ const ref = match[1];
231
+ if (ref && !ref.includes(' ') && !ref.startsWith('http')) {
232
+ references.add(normalizeFilePath(ref));
233
+ }
234
+ }
235
+ // Match markdown links: [text](./path/file.ts)
236
+ const linkPattern = /\[([^\]]*)\]\(([^)]+\.[a-zA-Z]{2,4})\)/g;
237
+ while ((match = linkPattern.exec(content)) !== null) {
238
+ const ref = match[2];
239
+ if (ref && !ref.startsWith('http')) {
240
+ references.add(normalizeFilePath(ref));
241
+ }
242
+ }
243
+ // Match explicit file paths in common patterns (with or without ./ prefix)
244
+ const pathPattern = /(?:\.\/)?(?:src|lib|app|packages?)\/[\w\-/.]+\.[a-zA-Z]{2,4}(?=[\s,;:\])]|$)/g;
245
+ while ((match = pathPattern.exec(content)) !== null) {
246
+ references.add(normalizeFilePath(match[0]));
247
+ }
248
+ return Array.from(references);
249
+ }
250
+ /**
251
+ * Read and parse package.json
252
+ */
253
+ async function readPackageJson(projectPath) {
254
+ try {
255
+ const packageJsonPath = path.join(projectPath, 'package.json');
256
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
257
+ const pkg = JSON.parse(content);
258
+ return {
259
+ dependencies: pkg.dependencies || {},
260
+ devDependencies: pkg.devDependencies || {},
261
+ };
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }
267
+ /**
268
+ * Detect technologies from package.json
269
+ */
270
+ function detectTechnologies(dependencies, devDependencies) {
271
+ const allDeps = { ...dependencies, ...devDependencies };
272
+ const detected = [];
273
+ for (const [techName, config] of Object.entries(TECHNOLOGY_PATTERNS)) {
274
+ const matchingPackages = [];
275
+ for (const dep of Object.keys(allDeps)) {
276
+ for (const keyword of config.keywords) {
277
+ if (dep.toLowerCase().includes(keyword.toLowerCase())) {
278
+ matchingPackages.push(dep);
279
+ break;
280
+ }
281
+ }
282
+ }
283
+ if (matchingPackages.length > 0) {
284
+ detected.push({
285
+ name: techName,
286
+ packages: matchingPackages,
287
+ suggestedAdrTitle: config.suggestedAdrTitle,
288
+ });
289
+ }
290
+ }
291
+ return detected;
292
+ }
293
+ /**
294
+ * Scan directory recursively for files
295
+ */
296
+ async function scanDirectory(dirPath, includePatterns, excludePatterns, maxDepth = 5, currentDepth = 0) {
297
+ if (currentDepth >= maxDepth)
298
+ return [];
299
+ const files = [];
300
+ try {
301
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
302
+ for (const entry of entries) {
303
+ const fullPath = path.join(dirPath, entry.name);
304
+ const relativePath = fullPath;
305
+ // Skip excluded patterns
306
+ if (excludePatterns.some(pattern => pattern.test(relativePath))) {
307
+ continue;
308
+ }
309
+ if (entry.isDirectory()) {
310
+ // Skip common non-source directories
311
+ if (['node_modules', '.git', 'dist', 'build', 'coverage', '.next'].includes(entry.name)) {
312
+ continue;
313
+ }
314
+ const subFiles = await scanDirectory(fullPath, includePatterns, excludePatterns, maxDepth, currentDepth + 1);
315
+ files.push(...subFiles);
316
+ }
317
+ else if (entry.isFile()) {
318
+ if (includePatterns.length === 0 ||
319
+ includePatterns.some(pattern => pattern.test(entry.name))) {
320
+ files.push(fullPath);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ catch {
326
+ // Directory not accessible
327
+ }
328
+ return files;
329
+ }
330
+ /**
331
+ * Check if ADR content mentions a technology
332
+ */
333
+ function adrMentionsTechnology(adrContent, techName, keywords) {
334
+ const lowerContent = adrContent.toLowerCase();
335
+ if (lowerContent.includes(techName.toLowerCase())) {
336
+ return true;
337
+ }
338
+ for (const keyword of keywords) {
339
+ if (lowerContent.includes(keyword.toLowerCase())) {
340
+ return true;
341
+ }
342
+ }
343
+ return false;
344
+ }
345
+ /**
346
+ * Detect architectural patterns in the codebase
347
+ */
348
+ async function detectArchitecturalPatterns(projectPath, scannedFiles) {
349
+ const detected = [];
350
+ for (const [patternName, config] of Object.entries(ARCHITECTURAL_PATTERNS)) {
351
+ const evidence = [];
352
+ // Check file patterns
353
+ for (const file of scannedFiles) {
354
+ const relativePath = path.relative(projectPath, file);
355
+ for (const pattern of config.filePatterns) {
356
+ if (pattern.test(relativePath)) {
357
+ evidence.push(`File: ${relativePath}`);
358
+ break;
359
+ }
360
+ }
361
+ }
362
+ // For now, just use file-based detection
363
+ // Code pattern detection could be added by reading file contents
364
+ if (evidence.length > 0) {
365
+ detected.push({
366
+ name: patternName,
367
+ evidence,
368
+ suggestedAdrTitle: config.suggestedAdrTitle,
369
+ });
370
+ }
371
+ }
372
+ return detected;
373
+ }
374
+ /**
375
+ * Ensure client is configured
376
+ */
377
+ function ensureConfigured(client) {
378
+ if (!client.isConfigured()) {
379
+ throw new McpAdrError('ADR Aggregator API key not configured. Set ADR_AGGREGATOR_API_KEY environment variable.', 'NOT_CONFIGURED');
380
+ }
381
+ }
382
+ /**
383
+ * Ensure git repository
384
+ */
385
+ function ensureGitRepo(projectPath) {
386
+ if (!isGitRepository(projectPath)) {
387
+ throw new McpAdrError('Not a git repository. ADR Aggregator requires a git repository to auto-detect the repository name.', 'NOT_GIT_REPO');
388
+ }
389
+ }
390
+ // ============================================================================
391
+ // Main Tool Implementation
392
+ // ============================================================================
393
+ /**
394
+ * Analyze gaps between ADRs and codebase
395
+ *
396
+ * Detects:
397
+ * 1. ADR-to-code gaps: File references in ADRs that don't exist
398
+ * 2. Code-to-ADR gaps: Technologies/patterns in code without ADR coverage
399
+ */
400
+ export async function analyzeGaps(args, context) {
401
+ const { reportToAggregator = true, scanDirectories, includePatterns, excludePatterns } = args;
402
+ const config = loadConfig();
403
+ const resolvedProjectPath = args.projectPath || config.projectPath;
404
+ const client = getAdrAggregatorClient();
405
+ // Validate git repository if reporting to aggregator
406
+ if (reportToAggregator) {
407
+ ensureConfigured(client);
408
+ ensureGitRepo(resolvedProjectPath);
409
+ }
410
+ try {
411
+ context?.info('Starting gap analysis...');
412
+ context?.report_progress(5, 100);
413
+ const gaps = [];
414
+ let filesScanned = 0;
415
+ const technologiesDetected = [];
416
+ const patternsDetected = [];
417
+ // Step 1: Discover and analyze local ADRs
418
+ context?.info('Discovering ADRs...');
419
+ context?.report_progress(10, 100);
420
+ const adrFiles = await discoverLocalAdrs(resolvedProjectPath, config.adrDirectory);
421
+ const adrContents = [];
422
+ for (const adrFile of adrFiles) {
423
+ try {
424
+ const content = await fs.readFile(adrFile, 'utf-8');
425
+ adrContents.push({
426
+ path: path.relative(resolvedProjectPath, adrFile),
427
+ content,
428
+ });
429
+ }
430
+ catch {
431
+ // Skip unreadable files
432
+ }
433
+ }
434
+ context?.info(`Found ${adrContents.length} ADRs`);
435
+ context?.report_progress(20, 100);
436
+ // Step 2: Check file references in ADRs (ADR-to-code gaps)
437
+ context?.info('Checking file references in ADRs...');
438
+ context?.report_progress(30, 100);
439
+ for (const adr of adrContents) {
440
+ const fileRefs = extractFileReferences(adr.content);
441
+ for (const ref of fileRefs) {
442
+ // Normalize path and verify if file exists
443
+ const { exists, normalizedPath } = normalizeAndVerifyPath(ref, resolvedProjectPath);
444
+ // Only report as a gap if file truly doesn't exist
445
+ if (!exists) {
446
+ gaps.push({
447
+ gap_type: 'adr_to_code',
448
+ severity: 'error',
449
+ title: `Missing referenced file: ${normalizedPath}`,
450
+ adr_path: adr.path,
451
+ referenced_file: normalizedPath,
452
+ verified: true, // File existence was verified by MCP server
453
+ description: `ADR references file "${normalizedPath}" which does not exist in the codebase`,
454
+ });
455
+ }
456
+ }
457
+ }
458
+ context?.report_progress(40, 100);
459
+ // Step 3: Scan codebase for technologies
460
+ context?.info('Scanning package.json for technologies...');
461
+ context?.report_progress(50, 100);
462
+ const packageJson = await readPackageJson(resolvedProjectPath);
463
+ if (packageJson) {
464
+ const technologies = detectTechnologies(packageJson.dependencies, packageJson.devDependencies);
465
+ for (const tech of technologies) {
466
+ technologiesDetected.push(tech.name);
467
+ // Check if any ADR mentions this technology
468
+ const hasAdrCoverage = adrContents.some(adr => adrMentionsTechnology(adr.content, tech.name, TECHNOLOGY_PATTERNS[tech.name]?.keywords || []));
469
+ if (!hasAdrCoverage) {
470
+ // Determine severity based on technology importance
471
+ let severity = 'warning';
472
+ if (['auth', 'postgresql', 'mongodb', 'aws', 'gcp', 'azure'].includes(tech.name)) {
473
+ severity = 'error'; // Critical infrastructure decisions
474
+ }
475
+ gaps.push({
476
+ gap_type: 'code_to_adr',
477
+ severity,
478
+ title: `Undocumented technology: ${tech.name}`,
479
+ detected_pattern: `${tech.packages.join(', ')} usage`,
480
+ code_files: ['package.json', ...tech.packages.map(p => `node_modules/${p}`)],
481
+ suggested_adr_title: tech.suggestedAdrTitle,
482
+ description: `Technology "${tech.name}" is used (packages: ${tech.packages.join(', ')}) but has no ADR documenting the decision`,
483
+ });
484
+ }
485
+ }
486
+ }
487
+ context?.report_progress(60, 100);
488
+ // Step 4: Scan for architectural patterns
489
+ context?.info('Detecting architectural patterns...');
490
+ context?.report_progress(70, 100);
491
+ const dirsToScan = scanDirectories || ['src', 'lib', 'app', 'packages'];
492
+ const includeRegex = includePatterns?.map(p => new RegExp(p)) || [];
493
+ const excludeRegex = excludePatterns?.map(p => new RegExp(p)) || [/node_modules/, /\.git/];
494
+ const scannedFiles = [];
495
+ for (const dir of dirsToScan) {
496
+ const dirPath = path.join(resolvedProjectPath, dir);
497
+ try {
498
+ await fs.access(dirPath);
499
+ const files = await scanDirectory(dirPath, includeRegex, excludeRegex);
500
+ scannedFiles.push(...files);
501
+ }
502
+ catch {
503
+ // Directory doesn't exist
504
+ }
505
+ }
506
+ filesScanned = scannedFiles.length;
507
+ const architecturalPatterns = await detectArchitecturalPatterns(resolvedProjectPath, scannedFiles);
508
+ for (const pattern of architecturalPatterns) {
509
+ patternsDetected.push(pattern.name);
510
+ // Check if any ADR mentions this pattern
511
+ const hasAdrCoverage = adrContents.some(adr => adr.content.toLowerCase().includes(pattern.name.toLowerCase()) ||
512
+ adr.content.toLowerCase().includes(pattern.suggestedAdrTitle.toLowerCase()));
513
+ if (!hasAdrCoverage) {
514
+ gaps.push({
515
+ gap_type: 'code_to_adr',
516
+ severity: 'warning',
517
+ title: `Undocumented pattern: ${pattern.name}`,
518
+ detected_pattern: `${pattern.name} pattern detected`,
519
+ code_files: pattern.evidence.map(e => e.replace('File: ', '')),
520
+ suggested_adr_title: pattern.suggestedAdrTitle,
521
+ description: `Architectural pattern "${pattern.name}" detected but has no ADR documenting the decision`,
522
+ });
523
+ }
524
+ }
525
+ context?.report_progress(80, 100);
526
+ // Step 5: Report to ADR Aggregator if enabled
527
+ let gapsInserted;
528
+ let totalOpenGaps;
529
+ let repositoryName;
530
+ if (reportToAggregator && gaps.length > 0) {
531
+ context?.info('Reporting gaps to ADR Aggregator...');
532
+ context?.report_progress(90, 100);
533
+ try {
534
+ repositoryName = client.getRepositoryName(resolvedProjectPath);
535
+ const request = {
536
+ repository_name: repositoryName,
537
+ analyzed_at: new Date().toISOString(),
538
+ gaps,
539
+ analysis_summary: {
540
+ files_scanned: filesScanned,
541
+ adrs_checked: adrContents.length,
542
+ technologies_detected: technologiesDetected,
543
+ patterns_detected: patternsDetected,
544
+ },
545
+ };
546
+ const response = await client.reportCodeGaps(request);
547
+ gapsInserted = response.summary.gaps_inserted;
548
+ totalOpenGaps = response.summary.total_open_gaps;
549
+ context?.info(`Reported gaps: ${gapsInserted} new, ${response.summary.gaps_updated} updated, ${totalOpenGaps} total open`);
550
+ }
551
+ catch (error) {
552
+ // Log but don't fail if aggregator reporting fails
553
+ const errorMessage = error instanceof Error ? error.message : String(error);
554
+ context?.info(`Warning: Failed to report to aggregator: ${errorMessage}`);
555
+ }
556
+ }
557
+ context?.report_progress(100, 100);
558
+ // Build analysis summary
559
+ const summary = {
560
+ files_scanned: filesScanned,
561
+ adrs_checked: adrContents.length,
562
+ technologies_detected: technologiesDetected,
563
+ patterns_detected: patternsDetected,
564
+ };
565
+ // Format response
566
+ const adrToCodeGaps = gaps.filter(g => g.gap_type === 'adr_to_code');
567
+ const codeToAdrGaps = gaps.filter(g => g.gap_type === 'code_to_adr');
568
+ const errorGaps = gaps.filter(g => g.severity === 'error');
569
+ const warningGaps = gaps.filter(g => g.severity === 'warning');
570
+ return {
571
+ content: [
572
+ {
573
+ type: 'text',
574
+ text: `# Gap Analysis Report
575
+
576
+ ## Summary
577
+ - **Files Scanned:** ${summary.files_scanned}
578
+ - **ADRs Checked:** ${summary.adrs_checked}
579
+ - **Total Gaps Found:** ${gaps.length}
580
+ - Errors: ${errorGaps.length}
581
+ - Warnings: ${warningGaps.length}
582
+ - **Technologies Detected:** ${summary.technologies_detected.join(', ') || 'None'}
583
+ - **Patterns Detected:** ${summary.patterns_detected?.join(', ') || 'None'}
584
+ ${repositoryName ? `- **Repository:** ${repositoryName}` : ''}
585
+ ${gapsInserted !== undefined ? `- **Gaps Reported:** ${gapsInserted} new, ${totalOpenGaps} total open` : ''}
586
+
587
+ ---
588
+
589
+ ## ADR-to-Code Gaps (${adrToCodeGaps.length})
590
+ ${adrToCodeGaps.length > 0
591
+ ? adrToCodeGaps
592
+ .map(g => `
593
+ ### ${g.severity === 'error' ? '❌' : '⚠️'} ${g.title}
594
+ - **ADR:** ${g.adr_path}
595
+ - **Referenced File:** ${g.referenced_file}
596
+ - **Description:** ${g.description}
597
+ `)
598
+ .join('\n')
599
+ : '*No ADR-to-code gaps found*'}
600
+
601
+ ---
602
+
603
+ ## Code-to-ADR Gaps (${codeToAdrGaps.length})
604
+ ${codeToAdrGaps.length > 0
605
+ ? codeToAdrGaps
606
+ .map(g => `
607
+ ### ${g.severity === 'error' ? '❌' : '⚠️'} ${g.title}
608
+ - **Pattern Type:** ${g.detected_pattern}
609
+ - **Code Files:** ${g.code_files?.join(', ')}
610
+ - **Suggested ADR Title:** "${g.suggested_adr_title}"
611
+ - **Description:** ${g.description}
612
+ `)
613
+ .join('\n')
614
+ : '*No code-to-ADR gaps found*'}
615
+
616
+ ---
617
+
618
+ ## Recommendations
619
+
620
+ ${gaps.length === 0
621
+ ? '✅ **Great job!** Your ADRs are well-aligned with your codebase.'
622
+ : `
623
+ Based on the analysis, consider the following actions:
624
+
625
+ ${errorGaps.length > 0 ? '### High Priority (Errors)\n' + errorGaps.map(g => `- ${g.gap_type === 'adr_to_code' ? `Fix or remove reference to \`${g.referenced_file}\` in ${g.adr_path}` : `Create ADR: "${g.suggested_adr_title}"`}`).join('\n') : ''}
626
+
627
+ ${warningGaps.length > 0 ? '### Medium Priority (Warnings)\n' + warningGaps.map(g => `- ${g.gap_type === 'adr_to_code' ? `Fix or remove reference to \`${g.referenced_file}\` in ${g.adr_path}` : `Consider creating ADR: "${g.suggested_adr_title}"`}`).join('\n') : ''}
628
+ `}
629
+
630
+ ---
631
+
632
+ *Analysis performed at ${new Date().toISOString()}*
633
+ ${reportToAggregator ? `\nView in dashboard: ${client.getBaseUrl()}/mcp-dashboard` : ''}
634
+ `,
635
+ },
636
+ ],
637
+ };
638
+ }
639
+ catch (error) {
640
+ const errorMessage = error instanceof McpAdrError ? error.message : String(error);
641
+ return {
642
+ content: [
643
+ {
644
+ type: 'text',
645
+ text: `# Gap Analysis Failed
646
+
647
+ **Error:** ${errorMessage}
648
+
649
+ Please check:
650
+ 1. PROJECT_PATH is set correctly
651
+ 2. The project directory exists and is readable
652
+ 3. ADR_AGGREGATOR_API_KEY is set if reporting to aggregator`,
653
+ },
654
+ ],
655
+ isError: true,
656
+ };
657
+ }
658
+ }
659
+ /**
660
+ * Get current gaps from ADR Aggregator
661
+ */
662
+ export async function getGaps(args, context) {
663
+ const client = getAdrAggregatorClient();
664
+ ensureConfigured(client);
665
+ const config = loadConfig();
666
+ const resolvedProjectPath = args.projectPath || config.projectPath;
667
+ ensureGitRepo(resolvedProjectPath);
668
+ try {
669
+ const repositoryName = client.getRepositoryName(resolvedProjectPath);
670
+ context?.info(`Fetching gaps for ${repositoryName}...`);
671
+ context?.report_progress(30, 100);
672
+ const response = await client.getCodeGaps({
673
+ repository_name: repositoryName,
674
+ ...(args.includeDismissed !== undefined && { include_dismissed: args.includeDismissed }),
675
+ ...(args.includeResolved !== undefined && { include_resolved: args.includeResolved }),
676
+ });
677
+ context?.report_progress(100, 100);
678
+ return {
679
+ content: [
680
+ {
681
+ type: 'text',
682
+ text: `# Current Gaps for ${response.repository}
683
+
684
+ ## Summary
685
+ - **Total Gaps:** ${response.summary.total}
686
+ - **By Type:** ADR→Code: ${response.summary.by_type.adr_to_code || 0}, Code→ADR: ${response.summary.by_type.code_to_adr || 0}
687
+ - **By Severity:** Errors: ${response.summary.by_severity.error || 0}, Warnings: ${response.summary.by_severity.warning || 0}, Info: ${response.summary.by_severity.info || 0}
688
+ - **By Status:** Open: ${response.summary.by_status['open'] || 0}, Dismissed: ${response.summary.by_status['dismissed'] || 0}, Resolved: ${response.summary.by_status['resolved'] || 0}
689
+
690
+ ## Gaps
691
+ ${response.gaps.length > 0
692
+ ? response.gaps
693
+ .map(g => `
694
+ ### ${g.severity === 'error' ? '❌' : g.severity === 'warning' ? '⚠️' : 'ℹ️'} ${g.title}
695
+ - **ID:** ${g.id}
696
+ - **Type:** ${g.gap_type}
697
+ - **Status:** ${g.status}
698
+ - **First Detected:** ${g.first_detected}
699
+ - **Last Seen:** ${g.last_seen}
700
+ ${g.adr_path ? `- **ADR:** ${g.adr_path}` : ''}
701
+ ${g.referenced_file ? `- **Referenced File:** ${g.referenced_file}` : ''}
702
+ ${g.code_files ? `- **Code Files:** ${g.code_files.join(', ')}` : ''}
703
+ ${g.suggested_adr_title ? `- **Suggested ADR:** "${g.suggested_adr_title}"` : ''}
704
+ `)
705
+ .join('\n')
706
+ : '*No gaps found*'}
707
+ `,
708
+ },
709
+ ],
710
+ };
711
+ }
712
+ catch (error) {
713
+ const errorMessage = error instanceof McpAdrError ? error.message : String(error);
714
+ return {
715
+ content: [
716
+ {
717
+ type: 'text',
718
+ text: `# Failed to Get Gaps
719
+
720
+ **Error:** ${errorMessage}`,
721
+ },
722
+ ],
723
+ isError: true,
724
+ };
725
+ }
726
+ }
727
+ //# sourceMappingURL=analyze-gaps-tool.js.map