mcp-adr-analysis-server 2.1.22 → 2.1.28
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 +63 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +370 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/tools/adr-aggregator-tools.d.ts +185 -0
- package/dist/src/tools/adr-aggregator-tools.d.ts.map +1 -0
- package/dist/src/tools/adr-aggregator-tools.js +872 -0
- package/dist/src/tools/adr-aggregator-tools.js.map +1 -0
- package/dist/src/tools/analyze-gaps-tool.d.ts +57 -0
- package/dist/src/tools/analyze-gaps-tool.d.ts.map +1 -0
- package/dist/src/tools/analyze-gaps-tool.js +727 -0
- package/dist/src/tools/analyze-gaps-tool.js.map +1 -0
- package/dist/src/tools/tool-catalog.d.ts +1 -1
- package/dist/src/tools/tool-catalog.d.ts.map +1 -1
- package/dist/src/tools/tool-catalog.js +382 -0
- package/dist/src/tools/tool-catalog.js.map +1 -1
- package/dist/src/tools/tool-dispatcher.d.ts.map +1 -1
- package/dist/src/tools/tool-dispatcher.js +4 -0
- package/dist/src/tools/tool-dispatcher.js.map +1 -1
- package/dist/src/types/adr-aggregator.d.ts +692 -0
- package/dist/src/types/adr-aggregator.d.ts.map +1 -0
- package/dist/src/types/adr-aggregator.js +8 -0
- package/dist/src/types/adr-aggregator.js.map +1 -0
- package/dist/src/types/index.d.ts +3 -2
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/memory-entities.d.ts +48 -48
- package/dist/src/utils/adr-aggregator-client.d.ts +177 -0
- package/dist/src/utils/adr-aggregator-client.d.ts.map +1 -0
- package/dist/src/utils/adr-aggregator-client.js +396 -0
- package/dist/src/utils/adr-aggregator-client.js.map +1 -0
- package/dist/src/utils/config.d.ts +3 -0
- package/dist/src/utils/config.d.ts.map +1 -1
- package/dist/src/utils/config.js +13 -0
- package/dist/src/utils/config.js.map +1 -1
- package/dist/src/utils/git-remote-detector.d.ts +67 -0
- package/dist/src/utils/git-remote-detector.d.ts.map +1 -0
- package/dist/src/utils/git-remote-detector.js +194 -0
- package/dist/src/utils/git-remote-detector.js.map +1 -0
- package/package.json +3 -3
|
@@ -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
|