driftdetect-mcp 0.6.0 → 0.7.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/LICENSE +21 -0
- package/dist/bin/server.js +0 -0
- package/dist/enterprise-server.d.ts +1 -0
- package/dist/enterprise-server.d.ts.map +1 -1
- package/dist/enterprise-server.js +8 -1
- package/dist/enterprise-server.js.map +1 -1
- package/dist/tools/analysis/constants.d.ts +99 -0
- package/dist/tools/analysis/constants.d.ts.map +1 -0
- package/dist/tools/analysis/constants.js +421 -0
- package/dist/tools/analysis/constants.js.map +1 -0
- package/dist/tools/analysis/index.d.ts +1 -0
- package/dist/tools/analysis/index.d.ts.map +1 -1
- package/dist/tools/analysis/index.js +70 -0
- package/dist/tools/analysis/index.js.map +1 -1
- package/dist/tools/exploration/env.d.ts +53 -0
- package/dist/tools/exploration/env.d.ts.map +1 -0
- package/dist/tools/exploration/env.js +283 -0
- package/dist/tools/exploration/env.js.map +1 -0
- package/dist/tools/exploration/index.d.ts +2 -0
- package/dist/tools/exploration/index.d.ts.map +1 -1
- package/dist/tools/exploration/index.js +32 -0
- package/dist/tools/exploration/index.js.map +1 -1
- package/dist/tools/index.d.ts +6 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -5
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +10 -4
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/surgical/callers.d.ts +85 -0
- package/dist/tools/surgical/callers.d.ts.map +1 -0
- package/dist/tools/surgical/callers.js +239 -0
- package/dist/tools/surgical/callers.js.map +1 -0
- package/dist/tools/surgical/dependencies.d.ts +96 -0
- package/dist/tools/surgical/dependencies.d.ts.map +1 -0
- package/dist/tools/surgical/dependencies.js +433 -0
- package/dist/tools/surgical/dependencies.js.map +1 -0
- package/dist/tools/surgical/errors.d.ts +88 -0
- package/dist/tools/surgical/errors.d.ts.map +1 -0
- package/dist/tools/surgical/errors.js +275 -0
- package/dist/tools/surgical/errors.js.map +1 -0
- package/dist/tools/surgical/hooks.d.ts +69 -0
- package/dist/tools/surgical/hooks.d.ts.map +1 -0
- package/dist/tools/surgical/hooks.js +247 -0
- package/dist/tools/surgical/hooks.js.map +1 -0
- package/dist/tools/surgical/imports.d.ts +61 -0
- package/dist/tools/surgical/imports.d.ts.map +1 -0
- package/dist/tools/surgical/imports.js +211 -0
- package/dist/tools/surgical/imports.js.map +1 -0
- package/dist/tools/surgical/index.d.ts +42 -0
- package/dist/tools/surgical/index.d.ts.map +1 -0
- package/dist/tools/surgical/index.js +66 -0
- package/dist/tools/surgical/index.js.map +1 -0
- package/dist/tools/surgical/middleware.d.ts +69 -0
- package/dist/tools/surgical/middleware.d.ts.map +1 -0
- package/dist/tools/surgical/middleware.js +237 -0
- package/dist/tools/surgical/middleware.js.map +1 -0
- package/dist/tools/surgical/prevalidate.d.ts +76 -0
- package/dist/tools/surgical/prevalidate.d.ts.map +1 -0
- package/dist/tools/surgical/prevalidate.js +303 -0
- package/dist/tools/surgical/prevalidate.js.map +1 -0
- package/dist/tools/surgical/recent.d.ts +66 -0
- package/dist/tools/surgical/recent.d.ts.map +1 -0
- package/dist/tools/surgical/recent.js +238 -0
- package/dist/tools/surgical/recent.js.map +1 -0
- package/dist/tools/surgical/signature.d.ts +73 -0
- package/dist/tools/surgical/signature.d.ts.map +1 -0
- package/dist/tools/surgical/signature.js +190 -0
- package/dist/tools/surgical/signature.js.map +1 -0
- package/dist/tools/surgical/similar.d.ts +77 -0
- package/dist/tools/surgical/similar.d.ts.map +1 -0
- package/dist/tools/surgical/similar.js +285 -0
- package/dist/tools/surgical/similar.js.map +1 -0
- package/dist/tools/surgical/test-template.d.ts +70 -0
- package/dist/tools/surgical/test-template.d.ts.map +1 -0
- package/dist/tools/surgical/test-template.js +298 -0
- package/dist/tools/surgical/test-template.js.map +1 -0
- package/dist/tools/surgical/type.d.ts +69 -0
- package/dist/tools/surgical/type.d.ts.map +1 -0
- package/dist/tools/surgical/type.js +289 -0
- package/dist/tools/surgical/type.js.map +1 -0
- package/package.json +11 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"similar.d.ts","sourceRoot":"","sources":["../../../src/tools/surgical/similar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAgB,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAOnF,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;IAC9F,mCAAmC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,EAAE,kBAAkB,CAAC;CACjC;AA0BD,wBAAsB,aAAa,CACjC,cAAc,EAAE,cAAc,EAC9B,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CA2K7D;AAuFD;;GAEG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BjC,CAAC"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drift_similar - Find Semantically Similar Code
|
|
3
|
+
*
|
|
4
|
+
* Layer: Surgical
|
|
5
|
+
* Token Budget: 500 target, 1000 max
|
|
6
|
+
* Cache TTL: 5 minutes
|
|
7
|
+
* Invalidation Keys: patterns, callgraph
|
|
8
|
+
*
|
|
9
|
+
* Finds code semantically similar to what the AI is about to write.
|
|
10
|
+
* Solves: AI needs to see an example but there are 50 options. Which is most relevant?
|
|
11
|
+
*/
|
|
12
|
+
import { createResponseBuilder, Errors, metrics } from '../../infrastructure/index.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Intent to Pattern Category Mapping
|
|
15
|
+
// ============================================================================
|
|
16
|
+
const INTENT_CATEGORIES = {
|
|
17
|
+
api_endpoint: ['api', 'errors', 'auth'],
|
|
18
|
+
service: ['data-access', 'errors', 'logging'],
|
|
19
|
+
component: ['components', 'styling', 'accessibility'],
|
|
20
|
+
hook: ['components', 'data-access'],
|
|
21
|
+
utility: ['errors', 'logging'],
|
|
22
|
+
test: ['testing'],
|
|
23
|
+
middleware: ['api', 'auth', 'errors'],
|
|
24
|
+
};
|
|
25
|
+
const INTENT_DECORATORS = {
|
|
26
|
+
api_endpoint: ['@app.route', '@router', 'HttpGet', 'HttpPost', '@Get', '@Post', 'Route'],
|
|
27
|
+
middleware: ['@middleware', 'Middleware'],
|
|
28
|
+
test: ['@test', 'describe', 'it', 'test'],
|
|
29
|
+
};
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Handler
|
|
32
|
+
// ============================================================================
|
|
33
|
+
export async function handleSimilar(callGraphStore, patternStore, args) {
|
|
34
|
+
const startTime = Date.now();
|
|
35
|
+
const builder = createResponseBuilder();
|
|
36
|
+
// Validate input
|
|
37
|
+
if (!args.intent) {
|
|
38
|
+
throw Errors.missingParameter('intent');
|
|
39
|
+
}
|
|
40
|
+
if (!args.description || args.description.trim() === '') {
|
|
41
|
+
throw Errors.missingParameter('description');
|
|
42
|
+
}
|
|
43
|
+
const intent = args.intent;
|
|
44
|
+
const description = args.description.toLowerCase();
|
|
45
|
+
const scope = args.scope;
|
|
46
|
+
const limit = Math.min(args.limit ?? 3, 10);
|
|
47
|
+
// Load stores
|
|
48
|
+
await callGraphStore.initialize();
|
|
49
|
+
await patternStore.initialize();
|
|
50
|
+
const graph = callGraphStore.getGraph();
|
|
51
|
+
if (!graph) {
|
|
52
|
+
throw Errors.custom('CALLGRAPH_NOT_BUILT', 'Call graph has not been built. Run "drift callgraph build" first.', ['drift_status']);
|
|
53
|
+
}
|
|
54
|
+
// Get patterns for scoring
|
|
55
|
+
const allPatterns = patternStore.getAll();
|
|
56
|
+
const patternsByFile = new Map();
|
|
57
|
+
for (const pattern of allPatterns) {
|
|
58
|
+
for (const loc of pattern.locations) {
|
|
59
|
+
const existing = patternsByFile.get(loc.file) ?? [];
|
|
60
|
+
existing.push(pattern.name);
|
|
61
|
+
patternsByFile.set(loc.file, existing);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Score all functions
|
|
65
|
+
const scored = [];
|
|
66
|
+
const targetCategories = INTENT_CATEGORIES[intent] ?? [];
|
|
67
|
+
const targetDecorators = INTENT_DECORATORS[intent] ?? [];
|
|
68
|
+
for (const [, func] of graph.functions) {
|
|
69
|
+
// Skip if scope specified and doesn't match
|
|
70
|
+
if (scope && !func.file.includes(scope)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const reasons = [];
|
|
74
|
+
let score = 0;
|
|
75
|
+
// 1. Decorator match (strong signal)
|
|
76
|
+
for (const dec of func.decorators) {
|
|
77
|
+
if (targetDecorators.some(td => dec.includes(td))) {
|
|
78
|
+
score += 0.3;
|
|
79
|
+
reasons.push(`Has ${intent} decorator`);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 2. Pattern category match
|
|
84
|
+
const filePatterns = patternsByFile.get(func.file) ?? [];
|
|
85
|
+
const categoryMatches = filePatterns.filter(p => targetCategories.some(cat => p.toLowerCase().includes(cat)));
|
|
86
|
+
if (categoryMatches.length > 0) {
|
|
87
|
+
score += 0.2 * Math.min(categoryMatches.length, 3);
|
|
88
|
+
reasons.push(`Matches ${categoryMatches.length} ${intent} patterns`);
|
|
89
|
+
}
|
|
90
|
+
// 3. Name similarity (fuzzy match on description keywords)
|
|
91
|
+
const keywords = description.split(/\s+/).filter(w => w.length > 2);
|
|
92
|
+
const nameWords = func.name.toLowerCase().split(/(?=[A-Z])|_|-/).map(w => w.toLowerCase());
|
|
93
|
+
const qualifiedWords = func.qualifiedName.toLowerCase().split(/[._-]/);
|
|
94
|
+
let keywordMatches = 0;
|
|
95
|
+
for (const kw of keywords) {
|
|
96
|
+
if (nameWords.some(nw => nw.includes(kw) || kw.includes(nw))) {
|
|
97
|
+
keywordMatches++;
|
|
98
|
+
}
|
|
99
|
+
else if (qualifiedWords.some(qw => qw.includes(kw) || kw.includes(qw))) {
|
|
100
|
+
keywordMatches += 0.5;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (keywordMatches > 0 && keywords.length > 0) {
|
|
104
|
+
const keywordScore = (keywordMatches / keywords.length) * 0.3;
|
|
105
|
+
score += keywordScore;
|
|
106
|
+
reasons.push(`Name matches ${Math.round(keywordMatches)} keywords`);
|
|
107
|
+
}
|
|
108
|
+
// 4. Exported functions are more likely to be good examples
|
|
109
|
+
if (func.isExported) {
|
|
110
|
+
score += 0.1;
|
|
111
|
+
}
|
|
112
|
+
// 5. Has parameters (more complete example)
|
|
113
|
+
if (func.parameters.length > 0) {
|
|
114
|
+
score += 0.05;
|
|
115
|
+
}
|
|
116
|
+
// 6. Has return type (better documented)
|
|
117
|
+
if (func.returnType) {
|
|
118
|
+
score += 0.05;
|
|
119
|
+
}
|
|
120
|
+
if (score > 0.1) {
|
|
121
|
+
scored.push({ func, score, reasons });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Sort by score and take top N
|
|
125
|
+
scored.sort((a, b) => b.score - a.score);
|
|
126
|
+
const topMatches = scored.slice(0, limit);
|
|
127
|
+
// Build matches
|
|
128
|
+
const matches = topMatches.map(({ func, score, reasons }) => ({
|
|
129
|
+
file: func.file,
|
|
130
|
+
function: func.name,
|
|
131
|
+
class: func.className,
|
|
132
|
+
similarity: Math.round(score * 100) / 100,
|
|
133
|
+
reason: reasons.slice(0, 2).join('. '),
|
|
134
|
+
preview: buildPreview(func),
|
|
135
|
+
patterns: patternsByFile.get(func.file)?.slice(0, 3) ?? [],
|
|
136
|
+
}));
|
|
137
|
+
// Analyze conventions from matches
|
|
138
|
+
const conventions = analyzeConventions(topMatches.map(m => m.func));
|
|
139
|
+
const data = {
|
|
140
|
+
matches,
|
|
141
|
+
conventions,
|
|
142
|
+
};
|
|
143
|
+
// Build summary
|
|
144
|
+
let summary;
|
|
145
|
+
if (matches.length === 0) {
|
|
146
|
+
summary = `No similar ${intent} found matching "${args.description}"`;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
summary = `Found ${matches.length} similar ${intent} example${matches.length !== 1 ? 's' : ''}. Top: ${matches[0].file}`;
|
|
150
|
+
}
|
|
151
|
+
// Build hints
|
|
152
|
+
const hints = {
|
|
153
|
+
nextActions: matches.length > 0
|
|
154
|
+
? [
|
|
155
|
+
`Use drift_signature to get full signature for "${matches[0].function}"`,
|
|
156
|
+
`Use drift_imports to get correct imports for your new file`,
|
|
157
|
+
]
|
|
158
|
+
: [
|
|
159
|
+
'Try a different intent or broader description',
|
|
160
|
+
'Use drift_patterns_list to see available patterns',
|
|
161
|
+
],
|
|
162
|
+
relatedTools: ['drift_signature', 'drift_imports', 'drift_code_examples'],
|
|
163
|
+
};
|
|
164
|
+
if (matches.some(m => m.similarity < 0.3)) {
|
|
165
|
+
hints.warnings = ['Some matches have low similarity - review carefully'];
|
|
166
|
+
}
|
|
167
|
+
// Record metrics
|
|
168
|
+
metrics.recordRequest('drift_similar', Date.now() - startTime, true, false);
|
|
169
|
+
return builder
|
|
170
|
+
.withSummary(summary)
|
|
171
|
+
.withData(data)
|
|
172
|
+
.withHints(hints)
|
|
173
|
+
.buildContent();
|
|
174
|
+
}
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Helpers
|
|
177
|
+
// ============================================================================
|
|
178
|
+
/**
|
|
179
|
+
* Build a preview of the function signature
|
|
180
|
+
*/
|
|
181
|
+
function buildPreview(func) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
// Decorators (first one)
|
|
184
|
+
if (func.decorators.length > 0) {
|
|
185
|
+
parts.push(`@${func.decorators[0]}`);
|
|
186
|
+
}
|
|
187
|
+
// Export + async
|
|
188
|
+
if (func.isExported)
|
|
189
|
+
parts.push('export');
|
|
190
|
+
if (func.isAsync)
|
|
191
|
+
parts.push('async');
|
|
192
|
+
parts.push('function');
|
|
193
|
+
parts.push(func.name);
|
|
194
|
+
// Parameters (abbreviated)
|
|
195
|
+
const params = func.parameters.slice(0, 3).map(p => {
|
|
196
|
+
if (p.type)
|
|
197
|
+
return `${p.name}: ${p.type}`;
|
|
198
|
+
return p.name;
|
|
199
|
+
});
|
|
200
|
+
if (func.parameters.length > 3) {
|
|
201
|
+
params.push('...');
|
|
202
|
+
}
|
|
203
|
+
let preview = parts.join(' ') + `(${params.join(', ')})`;
|
|
204
|
+
if (func.returnType) {
|
|
205
|
+
preview += `: ${func.returnType}`;
|
|
206
|
+
}
|
|
207
|
+
return preview;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Analyze conventions from matched functions
|
|
211
|
+
*/
|
|
212
|
+
function analyzeConventions(funcs) {
|
|
213
|
+
if (funcs.length === 0) {
|
|
214
|
+
return {
|
|
215
|
+
naming: 'Unknown',
|
|
216
|
+
errorHandling: 'Unknown',
|
|
217
|
+
imports: 'Unknown',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Naming convention
|
|
221
|
+
const names = funcs.map(f => f.name);
|
|
222
|
+
const hasCamelCase = names.some(n => /^[a-z][a-zA-Z]*$/.test(n));
|
|
223
|
+
const hasSnakeCase = names.some(n => /^[a-z][a-z_]*$/.test(n));
|
|
224
|
+
const hasPascalCase = names.some(n => /^[A-Z][a-zA-Z]*$/.test(n));
|
|
225
|
+
let naming = 'mixed';
|
|
226
|
+
if (hasCamelCase && !hasSnakeCase)
|
|
227
|
+
naming = 'camelCase';
|
|
228
|
+
else if (hasSnakeCase && !hasCamelCase)
|
|
229
|
+
naming = 'snake_case';
|
|
230
|
+
else if (hasPascalCase)
|
|
231
|
+
naming = 'PascalCase';
|
|
232
|
+
// Error handling (check return types)
|
|
233
|
+
const returnTypes = funcs.map(f => f.returnType).filter(Boolean);
|
|
234
|
+
let errorHandling = 'Unknown';
|
|
235
|
+
if (returnTypes.some(rt => rt?.includes('Result'))) {
|
|
236
|
+
errorHandling = 'Result<T> pattern';
|
|
237
|
+
}
|
|
238
|
+
else if (returnTypes.some(rt => rt?.includes('Promise'))) {
|
|
239
|
+
errorHandling = 'Async with try/catch';
|
|
240
|
+
}
|
|
241
|
+
else if (funcs.some(f => f.isAsync)) {
|
|
242
|
+
errorHandling = 'Async functions';
|
|
243
|
+
}
|
|
244
|
+
// Import style (inferred from file paths)
|
|
245
|
+
const files = funcs.map(f => f.file);
|
|
246
|
+
let imports = 'relative paths';
|
|
247
|
+
if (files.some(f => f.includes('@/'))) {
|
|
248
|
+
imports = 'Path alias (@/)';
|
|
249
|
+
}
|
|
250
|
+
else if (files.some(f => f.includes('~/'))) {
|
|
251
|
+
imports = 'Path alias (~/)';
|
|
252
|
+
}
|
|
253
|
+
return { naming, errorHandling, imports };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Tool definition for MCP registration
|
|
257
|
+
*/
|
|
258
|
+
export const similarToolDefinition = {
|
|
259
|
+
name: 'drift_similar',
|
|
260
|
+
description: 'Find code semantically similar to what you\'re about to write. Returns relevant examples with patterns and conventions. Use before writing new code to find the right template.',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
intent: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
enum: ['api_endpoint', 'service', 'component', 'hook', 'utility', 'test', 'middleware'],
|
|
267
|
+
description: 'What kind of code are you writing?',
|
|
268
|
+
},
|
|
269
|
+
description: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'Natural language description (e.g., "user preferences CRUD")',
|
|
272
|
+
},
|
|
273
|
+
scope: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'Optional: limit to specific directory',
|
|
276
|
+
},
|
|
277
|
+
limit: {
|
|
278
|
+
type: 'number',
|
|
279
|
+
description: 'Max results (default: 3, max: 10)',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
required: ['intent', 'description'],
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
//# sourceMappingURL=similar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"similar.js","sourceRoot":"","sources":["../../../src/tools/surgical/similar.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drift_test_template - Generate Test Scaffolding
|
|
3
|
+
*
|
|
4
|
+
* Layer: Surgical
|
|
5
|
+
* Token Budget: 800 target, 1500 max
|
|
6
|
+
* Cache TTL: 5 minutes
|
|
7
|
+
* Invalidation Keys: test-topology, file:{targetFile}
|
|
8
|
+
*
|
|
9
|
+
* Generates test scaffolding based on existing test patterns.
|
|
10
|
+
* Solves: Tests are the most convention-heavy code. Every codebase is different.
|
|
11
|
+
*/
|
|
12
|
+
import type { CallGraphStore } from 'driftdetect-core';
|
|
13
|
+
export interface TestTemplateArgs {
|
|
14
|
+
/** File being tested */
|
|
15
|
+
targetFile: string;
|
|
16
|
+
/** Specific function to test (optional) */
|
|
17
|
+
function?: string;
|
|
18
|
+
/** Test type */
|
|
19
|
+
type?: 'unit' | 'integration' | 'e2e';
|
|
20
|
+
}
|
|
21
|
+
export interface TestConventions {
|
|
22
|
+
framework: string;
|
|
23
|
+
style: string;
|
|
24
|
+
mockStyle: string;
|
|
25
|
+
assertionStyle: string;
|
|
26
|
+
filePattern: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ExampleTest {
|
|
29
|
+
file: string;
|
|
30
|
+
preview: string;
|
|
31
|
+
}
|
|
32
|
+
export interface TestTemplateData {
|
|
33
|
+
testFile: string;
|
|
34
|
+
template: string;
|
|
35
|
+
conventions: TestConventions;
|
|
36
|
+
exampleTest?: ExampleTest | undefined;
|
|
37
|
+
}
|
|
38
|
+
export declare function handleTestTemplate(store: CallGraphStore, args: TestTemplateArgs, projectRoot: string): Promise<{
|
|
39
|
+
content: Array<{
|
|
40
|
+
type: string;
|
|
41
|
+
text: string;
|
|
42
|
+
}>;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Tool definition for MCP registration
|
|
46
|
+
*/
|
|
47
|
+
export declare const testTemplateToolDefinition: {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object";
|
|
52
|
+
properties: {
|
|
53
|
+
targetFile: {
|
|
54
|
+
type: string;
|
|
55
|
+
description: string;
|
|
56
|
+
};
|
|
57
|
+
function: {
|
|
58
|
+
type: string;
|
|
59
|
+
description: string;
|
|
60
|
+
};
|
|
61
|
+
type: {
|
|
62
|
+
type: string;
|
|
63
|
+
enum: string[];
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
required: string[];
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=test-template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-template.d.ts","sourceRoot":"","sources":["../../../src/tools/surgical/test-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,kBAAkB,CAAC;AASrE,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB;IAChB,IAAI,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,KAAK,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,eAAe,CAAC;IAC7B,WAAW,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CACvC;AAMD,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,gBAAgB,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CAsF7D;AAiOD;;GAEG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;CAsBtC,CAAC"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drift_test_template - Generate Test Scaffolding
|
|
3
|
+
*
|
|
4
|
+
* Layer: Surgical
|
|
5
|
+
* Token Budget: 800 target, 1500 max
|
|
6
|
+
* Cache TTL: 5 minutes
|
|
7
|
+
* Invalidation Keys: test-topology, file:{targetFile}
|
|
8
|
+
*
|
|
9
|
+
* Generates test scaffolding based on existing test patterns.
|
|
10
|
+
* Solves: Tests are the most convention-heavy code. Every codebase is different.
|
|
11
|
+
*/
|
|
12
|
+
import { createResponseBuilder, Errors, metrics } from '../../infrastructure/index.js';
|
|
13
|
+
import * as fs from 'node:fs/promises';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Handler
|
|
17
|
+
// ============================================================================
|
|
18
|
+
export async function handleTestTemplate(store, args, projectRoot) {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
const builder = createResponseBuilder();
|
|
21
|
+
// Validate input
|
|
22
|
+
if (!args.targetFile || args.targetFile.trim() === '') {
|
|
23
|
+
throw Errors.missingParameter('targetFile');
|
|
24
|
+
}
|
|
25
|
+
const targetFile = args.targetFile.trim();
|
|
26
|
+
const targetFunction = args.function?.trim();
|
|
27
|
+
const testType = args.type ?? 'unit';
|
|
28
|
+
// Load call graph
|
|
29
|
+
await store.initialize();
|
|
30
|
+
const graph = store.getGraph();
|
|
31
|
+
if (!graph) {
|
|
32
|
+
throw Errors.custom('CALLGRAPH_NOT_BUILT', 'Call graph has not been built. Run "drift callgraph build" first.', ['drift_status']);
|
|
33
|
+
}
|
|
34
|
+
// Find the target function if specified
|
|
35
|
+
let targetFunc;
|
|
36
|
+
if (targetFunction) {
|
|
37
|
+
for (const [, func] of graph.functions) {
|
|
38
|
+
if (func.file.endsWith(targetFile) && func.name === targetFunction) {
|
|
39
|
+
targetFunc = func;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Detect test conventions from existing tests
|
|
45
|
+
const detectedConventions = await detectTestConventions(projectRoot, targetFile);
|
|
46
|
+
// Find example test in same directory
|
|
47
|
+
const exampleTest = await findExampleTest(projectRoot, targetFile, detectedConventions);
|
|
48
|
+
// Generate test file path
|
|
49
|
+
const testFile = generateTestFilePath(targetFile, detectedConventions);
|
|
50
|
+
// Generate template
|
|
51
|
+
const template = generateTestTemplate(targetFile, targetFunc, detectedConventions, testType);
|
|
52
|
+
const data = {
|
|
53
|
+
testFile,
|
|
54
|
+
template,
|
|
55
|
+
conventions: detectedConventions,
|
|
56
|
+
exampleTest,
|
|
57
|
+
};
|
|
58
|
+
// Build summary
|
|
59
|
+
const funcName = targetFunc?.name ?? path.basename(targetFile, path.extname(targetFile));
|
|
60
|
+
const summary = `Generated ${testType} test template for "${funcName}" using ${detectedConventions.framework}/${detectedConventions.style}`;
|
|
61
|
+
// Build hints
|
|
62
|
+
const hints = {
|
|
63
|
+
nextActions: [
|
|
64
|
+
`Create ${testFile} with the template`,
|
|
65
|
+
'Fill in test cases based on function behavior',
|
|
66
|
+
exampleTest ? `Reference ${exampleTest.file} for patterns` : 'Add assertions matching codebase style',
|
|
67
|
+
],
|
|
68
|
+
relatedTools: ['drift_signature', 'drift_callers', 'drift_similar'],
|
|
69
|
+
};
|
|
70
|
+
if (!exampleTest) {
|
|
71
|
+
hints.warnings = ['No existing tests found in directory - conventions inferred from project'];
|
|
72
|
+
}
|
|
73
|
+
// Record metrics
|
|
74
|
+
metrics.recordRequest('drift_test_template', Date.now() - startTime, true, false);
|
|
75
|
+
return builder
|
|
76
|
+
.withSummary(summary)
|
|
77
|
+
.withData(data)
|
|
78
|
+
.withHints(hints)
|
|
79
|
+
.buildContent();
|
|
80
|
+
}
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Helpers
|
|
83
|
+
// ============================================================================
|
|
84
|
+
/**
|
|
85
|
+
* Detect test conventions from existing tests
|
|
86
|
+
*/
|
|
87
|
+
async function detectTestConventions(projectRoot, targetFile) {
|
|
88
|
+
const conventions = {
|
|
89
|
+
framework: 'vitest',
|
|
90
|
+
style: 'describe/it',
|
|
91
|
+
mockStyle: 'vi.mock',
|
|
92
|
+
assertionStyle: 'expect',
|
|
93
|
+
filePattern: '*.test.ts',
|
|
94
|
+
};
|
|
95
|
+
// Check package.json for test framework
|
|
96
|
+
try {
|
|
97
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
98
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf-8');
|
|
99
|
+
const pkg = JSON.parse(pkgContent);
|
|
100
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
101
|
+
if (deps['vitest']) {
|
|
102
|
+
conventions.framework = 'vitest';
|
|
103
|
+
conventions.mockStyle = 'vi.mock';
|
|
104
|
+
}
|
|
105
|
+
else if (deps['jest']) {
|
|
106
|
+
conventions.framework = 'jest';
|
|
107
|
+
conventions.mockStyle = 'jest.mock';
|
|
108
|
+
}
|
|
109
|
+
else if (deps['mocha']) {
|
|
110
|
+
conventions.framework = 'mocha';
|
|
111
|
+
conventions.mockStyle = 'sinon';
|
|
112
|
+
conventions.assertionStyle = 'chai';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Use defaults
|
|
117
|
+
}
|
|
118
|
+
// Check for test file patterns in the target directory
|
|
119
|
+
const targetDir = path.dirname(targetFile);
|
|
120
|
+
const fullTargetDir = path.join(projectRoot, targetDir);
|
|
121
|
+
try {
|
|
122
|
+
const files = await fs.readdir(fullTargetDir);
|
|
123
|
+
// Check for __tests__ directory
|
|
124
|
+
if (files.includes('__tests__')) {
|
|
125
|
+
conventions.filePattern = '__tests__/*.test.ts';
|
|
126
|
+
}
|
|
127
|
+
// Check for .spec.ts files
|
|
128
|
+
if (files.some(f => f.endsWith('.spec.ts'))) {
|
|
129
|
+
conventions.filePattern = '*.spec.ts';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Directory doesn't exist or can't be read
|
|
134
|
+
}
|
|
135
|
+
return conventions;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Find an example test in the same directory
|
|
139
|
+
*/
|
|
140
|
+
async function findExampleTest(projectRoot, targetFile, _conventions) {
|
|
141
|
+
const targetDir = path.dirname(targetFile);
|
|
142
|
+
// Possible test locations
|
|
143
|
+
const testDirs = [
|
|
144
|
+
path.join(projectRoot, targetDir, '__tests__'),
|
|
145
|
+
path.join(projectRoot, targetDir),
|
|
146
|
+
path.join(projectRoot, targetDir.replace('/src/', '/test/')),
|
|
147
|
+
];
|
|
148
|
+
for (const testDir of testDirs) {
|
|
149
|
+
try {
|
|
150
|
+
const files = await fs.readdir(testDir);
|
|
151
|
+
const testFiles = files.filter(f => f.endsWith('.test.ts') ||
|
|
152
|
+
f.endsWith('.spec.ts') ||
|
|
153
|
+
f.endsWith('.test.js') ||
|
|
154
|
+
f.endsWith('.spec.js'));
|
|
155
|
+
if (testFiles.length > 0) {
|
|
156
|
+
const testFile = testFiles[0];
|
|
157
|
+
const fullPath = path.join(testDir, testFile);
|
|
158
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
159
|
+
// Get first 20 lines as preview
|
|
160
|
+
const preview = content.split('\n').slice(0, 20).join('\n');
|
|
161
|
+
return {
|
|
162
|
+
file: path.relative(projectRoot, fullPath),
|
|
163
|
+
preview,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Directory doesn't exist
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Generate test file path based on conventions
|
|
175
|
+
*/
|
|
176
|
+
function generateTestFilePath(targetFile, conventions) {
|
|
177
|
+
const dir = path.dirname(targetFile);
|
|
178
|
+
const base = path.basename(targetFile, path.extname(targetFile));
|
|
179
|
+
const ext = path.extname(targetFile);
|
|
180
|
+
if (conventions.filePattern.includes('__tests__')) {
|
|
181
|
+
return path.join(dir, '__tests__', `${base}.test${ext}`);
|
|
182
|
+
}
|
|
183
|
+
if (conventions.filePattern.includes('.spec.')) {
|
|
184
|
+
return path.join(dir, `${base}.spec${ext}`);
|
|
185
|
+
}
|
|
186
|
+
return path.join(dir, `${base}.test${ext}`);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Generate test template
|
|
190
|
+
*/
|
|
191
|
+
function generateTestTemplate(targetFile, targetFunc, conventions, _testType) {
|
|
192
|
+
const moduleName = path.basename(targetFile, path.extname(targetFile));
|
|
193
|
+
const funcName = targetFunc?.name ?? moduleName;
|
|
194
|
+
// Import statement based on framework
|
|
195
|
+
let imports;
|
|
196
|
+
if (conventions.framework === 'vitest') {
|
|
197
|
+
imports = `import { describe, it, expect, beforeEach, vi } from 'vitest';`;
|
|
198
|
+
}
|
|
199
|
+
else if (conventions.framework === 'jest') {
|
|
200
|
+
imports = `// Jest globals are available automatically`;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
imports = `import { describe, it } from 'mocha';\nimport { expect } from 'chai';`;
|
|
204
|
+
}
|
|
205
|
+
// Import the module under test
|
|
206
|
+
const relativePath = './' + moduleName;
|
|
207
|
+
const moduleImport = targetFunc
|
|
208
|
+
? `import { ${funcName} } from '${relativePath}';`
|
|
209
|
+
: `import * as ${moduleName} from '${relativePath}';`;
|
|
210
|
+
// Mock setup based on framework
|
|
211
|
+
let mockSetup = '';
|
|
212
|
+
if (conventions.framework === 'vitest') {
|
|
213
|
+
mockSetup = `
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
vi.clearAllMocks();
|
|
216
|
+
});`;
|
|
217
|
+
}
|
|
218
|
+
else if (conventions.framework === 'jest') {
|
|
219
|
+
mockSetup = `
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
jest.clearAllMocks();
|
|
222
|
+
});`;
|
|
223
|
+
}
|
|
224
|
+
// Generate test cases
|
|
225
|
+
let testCases;
|
|
226
|
+
if (targetFunc) {
|
|
227
|
+
const params = targetFunc.parameters.map(p => p.name).join(', ');
|
|
228
|
+
const hasParams = targetFunc.parameters.length > 0;
|
|
229
|
+
testCases = `
|
|
230
|
+
it('should ${funcName} successfully', async () => {
|
|
231
|
+
// Arrange
|
|
232
|
+
${hasParams ? `const ${targetFunc.parameters[0]?.name ?? 'input'} = {}; // TODO: Add test data` : '// No input needed'}
|
|
233
|
+
|
|
234
|
+
// Act
|
|
235
|
+
const result = await ${funcName}(${params});
|
|
236
|
+
|
|
237
|
+
// Assert
|
|
238
|
+
expect(result).toBeDefined();
|
|
239
|
+
// TODO: Add specific assertions
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should handle errors gracefully', async () => {
|
|
243
|
+
// Arrange
|
|
244
|
+
// TODO: Set up error condition
|
|
245
|
+
|
|
246
|
+
// Act & Assert
|
|
247
|
+
// TODO: Add error handling test
|
|
248
|
+
});`;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
testCases = `
|
|
252
|
+
it('should work correctly', () => {
|
|
253
|
+
// Arrange
|
|
254
|
+
// TODO: Set up test data
|
|
255
|
+
|
|
256
|
+
// Act
|
|
257
|
+
// TODO: Call function under test
|
|
258
|
+
|
|
259
|
+
// Assert
|
|
260
|
+
// TODO: Add assertions
|
|
261
|
+
});`;
|
|
262
|
+
}
|
|
263
|
+
// Combine template
|
|
264
|
+
return `${imports}
|
|
265
|
+
${moduleImport}
|
|
266
|
+
|
|
267
|
+
describe('${funcName}', () => {${mockSetup}
|
|
268
|
+
${testCases}
|
|
269
|
+
});
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Tool definition for MCP registration
|
|
274
|
+
*/
|
|
275
|
+
export const testTemplateToolDefinition = {
|
|
276
|
+
name: 'drift_test_template',
|
|
277
|
+
description: 'Generate test scaffolding based on existing test patterns. Returns ready-to-use template matching codebase conventions (framework, mocking style, file location).',
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
targetFile: {
|
|
282
|
+
type: 'string',
|
|
283
|
+
description: 'File being tested (relative path)',
|
|
284
|
+
},
|
|
285
|
+
function: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
description: 'Optional: specific function to test',
|
|
288
|
+
},
|
|
289
|
+
type: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
enum: ['unit', 'integration', 'e2e'],
|
|
292
|
+
description: 'Test type (default: unit)',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ['targetFile'],
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
//# sourceMappingURL=test-template.js.map
|