driftdetect-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/dist/bin/server.d.ts +20 -0
- package/dist/bin/server.d.ts.map +1 -0
- package/dist/bin/server.js +40 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +197 -0
- package/dist/feedback.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/packs.d.ts +86 -0
- package/dist/packs.d.ts.map +1 -0
- package/dist/packs.js +654 -0
- package/dist/packs.js.map +1 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +976 -0
- package/dist/server.js.map +1 -0
- package/package.json +50 -0
package/dist/packs.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Packs - Pre-defined bundles of patterns for common tasks
|
|
3
|
+
*
|
|
4
|
+
* Provides cached, task-oriented pattern context for AI agents.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import * as fs from 'node:fs/promises';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { PatternStore } from 'driftdetect-core';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// File Filtering - Exclude noisy files from examples
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Files to exclude from pattern examples (documentation, config, etc.)
|
|
15
|
+
* These files often contain keywords but aren't useful as code examples.
|
|
16
|
+
*/
|
|
17
|
+
const EXAMPLE_EXCLUDE_PATTERNS = [
|
|
18
|
+
// Documentation
|
|
19
|
+
/README/i,
|
|
20
|
+
/CHANGELOG/i,
|
|
21
|
+
/CONTRIBUTING/i,
|
|
22
|
+
/LICENSE/i,
|
|
23
|
+
/\.md$/i,
|
|
24
|
+
// CI/CD and config
|
|
25
|
+
/\.github\//,
|
|
26
|
+
/\.gitlab\//,
|
|
27
|
+
/\.ya?ml$/i,
|
|
28
|
+
/\.toml$/i,
|
|
29
|
+
/Dockerfile/i,
|
|
30
|
+
/docker-compose/i,
|
|
31
|
+
// Package manifests (not useful as code examples)
|
|
32
|
+
/package\.json$/i,
|
|
33
|
+
/package-lock\.json$/i,
|
|
34
|
+
/pnpm-lock\.yaml$/i,
|
|
35
|
+
/yarn\.lock$/i,
|
|
36
|
+
/requirements\.txt$/i,
|
|
37
|
+
/pyproject\.toml$/i,
|
|
38
|
+
/Cargo\.toml$/i,
|
|
39
|
+
/go\.mod$/i,
|
|
40
|
+
// Environment and secrets
|
|
41
|
+
/\.env/i,
|
|
42
|
+
/\.example$/i,
|
|
43
|
+
// Generated/build files
|
|
44
|
+
/dist\//,
|
|
45
|
+
/build\//,
|
|
46
|
+
/node_modules\//,
|
|
47
|
+
/\.min\./,
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Deprecation markers that indicate legacy/deprecated code
|
|
51
|
+
*/
|
|
52
|
+
const DEPRECATION_MARKERS = [
|
|
53
|
+
/DEPRECATED/i,
|
|
54
|
+
/LEGACY/i,
|
|
55
|
+
/@deprecated/i,
|
|
56
|
+
/TODO:\s*remove/i,
|
|
57
|
+
/REMOVAL:\s*planned/i,
|
|
58
|
+
/backward.?compat/i,
|
|
59
|
+
/will be removed/i,
|
|
60
|
+
/no longer (used|supported|maintained)/i,
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Check if a file should be excluded from examples
|
|
64
|
+
*/
|
|
65
|
+
function shouldExcludeFile(filePath) {
|
|
66
|
+
return EXAMPLE_EXCLUDE_PATTERNS.some(pattern => pattern.test(filePath));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if content contains deprecation markers
|
|
70
|
+
*/
|
|
71
|
+
function isDeprecatedContent(content) {
|
|
72
|
+
// Check first 500 chars (usually where deprecation notices are)
|
|
73
|
+
const header = content.slice(0, 500);
|
|
74
|
+
return DEPRECATION_MARKERS.some(pattern => pattern.test(header));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Score a location for example quality (higher = better)
|
|
78
|
+
*/
|
|
79
|
+
function scoreLocation(_loc, filePath) {
|
|
80
|
+
let score = 1.0;
|
|
81
|
+
// Penalize documentation files
|
|
82
|
+
if (/\.md$/i.test(filePath))
|
|
83
|
+
score *= 0.1;
|
|
84
|
+
if (/README/i.test(filePath))
|
|
85
|
+
score *= 0.1;
|
|
86
|
+
// Penalize config files
|
|
87
|
+
if (/\.ya?ml$/i.test(filePath))
|
|
88
|
+
score *= 0.2;
|
|
89
|
+
if (/\.json$/i.test(filePath))
|
|
90
|
+
score *= 0.3;
|
|
91
|
+
// Boost source code files
|
|
92
|
+
if (/\.(ts|tsx|js|jsx)$/i.test(filePath))
|
|
93
|
+
score *= 1.5;
|
|
94
|
+
if (/\.(py|rb|go|rs|java)$/i.test(filePath))
|
|
95
|
+
score *= 1.5;
|
|
96
|
+
// Boost files in src/ directories
|
|
97
|
+
if (/\/src\//i.test(filePath))
|
|
98
|
+
score *= 1.3;
|
|
99
|
+
if (/\/lib\//i.test(filePath))
|
|
100
|
+
score *= 1.2;
|
|
101
|
+
// Penalize test files slightly (still useful but prefer production code)
|
|
102
|
+
if (/\.(test|spec)\./i.test(filePath))
|
|
103
|
+
score *= 0.7;
|
|
104
|
+
if (/\/__tests__\//i.test(filePath))
|
|
105
|
+
score *= 0.7;
|
|
106
|
+
return score;
|
|
107
|
+
}
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Default Pack Definitions
|
|
110
|
+
// ============================================================================
|
|
111
|
+
export const DEFAULT_PACKS = [
|
|
112
|
+
{
|
|
113
|
+
name: 'backend_route',
|
|
114
|
+
description: 'Everything needed to build a new API endpoint',
|
|
115
|
+
categories: ['api', 'auth', 'security', 'errors'],
|
|
116
|
+
patterns: ['middleware', 'rate-limit', 'response', 'token', 'validation'],
|
|
117
|
+
maxExamples: 2,
|
|
118
|
+
contextLines: 12,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'react_component',
|
|
122
|
+
description: 'Patterns for new React components',
|
|
123
|
+
categories: ['components', 'styling', 'accessibility', 'types'],
|
|
124
|
+
patterns: ['props', 'hooks', 'error-boundary', 'aria'],
|
|
125
|
+
maxExamples: 2,
|
|
126
|
+
contextLines: 15,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'data_layer',
|
|
130
|
+
description: 'Database access and service patterns',
|
|
131
|
+
categories: ['data-access', 'errors', 'types', 'logging'],
|
|
132
|
+
patterns: ['repository', 'dto', 'validation', 'transaction'],
|
|
133
|
+
maxExamples: 2,
|
|
134
|
+
contextLines: 12,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'testing',
|
|
138
|
+
description: 'Test structure and mocking patterns',
|
|
139
|
+
categories: ['testing'],
|
|
140
|
+
maxExamples: 3,
|
|
141
|
+
contextLines: 20,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'security_audit',
|
|
145
|
+
description: 'Security patterns for code review',
|
|
146
|
+
categories: ['security', 'auth'],
|
|
147
|
+
patterns: ['injection', 'xss', 'csrf', 'sanitization', 'secret'],
|
|
148
|
+
maxExamples: 2,
|
|
149
|
+
contextLines: 15,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Pack Manager
|
|
154
|
+
// ============================================================================
|
|
155
|
+
export class PackManager {
|
|
156
|
+
projectRoot;
|
|
157
|
+
store;
|
|
158
|
+
packsDir;
|
|
159
|
+
cacheDir;
|
|
160
|
+
customPacks = [];
|
|
161
|
+
constructor(projectRoot, store) {
|
|
162
|
+
this.projectRoot = projectRoot;
|
|
163
|
+
this.store = store;
|
|
164
|
+
this.packsDir = path.join(projectRoot, '.drift', 'packs');
|
|
165
|
+
this.cacheDir = path.join(projectRoot, '.drift', 'cache', 'packs');
|
|
166
|
+
}
|
|
167
|
+
async initialize() {
|
|
168
|
+
await this.store.initialize();
|
|
169
|
+
await this.ensureDirectories();
|
|
170
|
+
await this.loadCustomPacks();
|
|
171
|
+
}
|
|
172
|
+
async ensureDirectories() {
|
|
173
|
+
await fs.mkdir(this.packsDir, { recursive: true });
|
|
174
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
async loadCustomPacks() {
|
|
177
|
+
const customPacksPath = path.join(this.packsDir, 'packs.json');
|
|
178
|
+
try {
|
|
179
|
+
const content = await fs.readFile(customPacksPath, 'utf-8');
|
|
180
|
+
this.customPacks = JSON.parse(content);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// No custom packs defined - that's fine
|
|
184
|
+
this.customPacks = [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
getAllPacks() {
|
|
188
|
+
// Custom packs override defaults with same name
|
|
189
|
+
const packMap = new Map();
|
|
190
|
+
for (const pack of DEFAULT_PACKS) {
|
|
191
|
+
packMap.set(pack.name, pack);
|
|
192
|
+
}
|
|
193
|
+
for (const pack of this.customPacks) {
|
|
194
|
+
packMap.set(pack.name, pack);
|
|
195
|
+
}
|
|
196
|
+
return Array.from(packMap.values());
|
|
197
|
+
}
|
|
198
|
+
getPack(name) {
|
|
199
|
+
return this.getAllPacks().find(p => p.name === name);
|
|
200
|
+
}
|
|
201
|
+
async getPackContent(name, options = {}) {
|
|
202
|
+
const packDef = this.getPack(name);
|
|
203
|
+
if (!packDef) {
|
|
204
|
+
throw new Error(`Unknown pack: ${name}. Available: ${this.getAllPacks().map(p => p.name).join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
const cachePath = path.join(this.cacheDir, `${name}.md`);
|
|
207
|
+
const metaPath = path.join(this.cacheDir, `${name}.meta.json`);
|
|
208
|
+
// Check if we need to regenerate
|
|
209
|
+
if (!options.refresh) {
|
|
210
|
+
const staleCheck = await this.checkStaleness(packDef, metaPath);
|
|
211
|
+
if (!staleCheck.isStale) {
|
|
212
|
+
try {
|
|
213
|
+
const content = await fs.readFile(cachePath, 'utf-8');
|
|
214
|
+
const meta = JSON.parse(await fs.readFile(metaPath, 'utf-8'));
|
|
215
|
+
return {
|
|
216
|
+
content,
|
|
217
|
+
fromCache: true,
|
|
218
|
+
generatedAt: meta.generatedAt,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Cache read failed - regenerate
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Will regenerate - include reason
|
|
227
|
+
const result = await this.generatePack(packDef);
|
|
228
|
+
const packResult = {
|
|
229
|
+
content: result.content,
|
|
230
|
+
fromCache: result.fromCache,
|
|
231
|
+
generatedAt: result.generatedAt,
|
|
232
|
+
};
|
|
233
|
+
if (staleCheck.reason) {
|
|
234
|
+
packResult.staleReason = staleCheck.reason;
|
|
235
|
+
}
|
|
236
|
+
return packResult;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return this.generatePack(packDef);
|
|
240
|
+
}
|
|
241
|
+
async checkStaleness(packDef, metaPath) {
|
|
242
|
+
try {
|
|
243
|
+
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
244
|
+
const meta = JSON.parse(metaContent);
|
|
245
|
+
// Check 1: Pack definition changed
|
|
246
|
+
const currentDefHash = this.hashPackDef(packDef);
|
|
247
|
+
if (meta.packDefHash !== currentDefHash) {
|
|
248
|
+
return { isStale: true, reason: 'Pack definition changed' };
|
|
249
|
+
}
|
|
250
|
+
// Check 2: Pattern content changed
|
|
251
|
+
const currentPatternHash = await this.computePatternHash(packDef);
|
|
252
|
+
if (meta.patternHash !== currentPatternHash) {
|
|
253
|
+
return { isStale: true, reason: 'Patterns updated' };
|
|
254
|
+
}
|
|
255
|
+
// Check 3: Source files modified
|
|
256
|
+
const cacheTime = new Date(meta.generatedAt).getTime();
|
|
257
|
+
for (const file of meta.sourceFiles) {
|
|
258
|
+
try {
|
|
259
|
+
const filePath = path.join(this.projectRoot, file);
|
|
260
|
+
const stat = await fs.stat(filePath);
|
|
261
|
+
if (stat.mtimeMs > cacheTime) {
|
|
262
|
+
return { isStale: true, reason: `Source file modified: ${file}` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// File doesn't exist anymore - stale
|
|
267
|
+
return { isStale: true, reason: `Source file removed: ${file}` };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { isStale: false };
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// No meta file - needs generation
|
|
274
|
+
return { isStale: true, reason: 'No cache exists' };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async generatePack(packDef) {
|
|
278
|
+
const maxExamples = packDef.maxExamples ?? 2;
|
|
279
|
+
const contextLines = packDef.contextLines ?? 12;
|
|
280
|
+
const includeDeprecated = packDef.includeDeprecated ?? false;
|
|
281
|
+
const minConfidence = packDef.minConfidence ?? 0.5;
|
|
282
|
+
// Get patterns matching the pack definition
|
|
283
|
+
let patterns = this.store.getAll();
|
|
284
|
+
// Filter by categories
|
|
285
|
+
const cats = new Set(packDef.categories);
|
|
286
|
+
patterns = patterns.filter(p => cats.has(p.category));
|
|
287
|
+
// Filter by minimum confidence
|
|
288
|
+
patterns = patterns.filter(p => p.confidence.score >= minConfidence);
|
|
289
|
+
// Filter by pattern names if specified
|
|
290
|
+
if (packDef.patterns && packDef.patterns.length > 0) {
|
|
291
|
+
const patternFilters = packDef.patterns.map(p => p.toLowerCase());
|
|
292
|
+
patterns = patterns.filter(p => patternFilters.some(f => p.name.toLowerCase().includes(f) ||
|
|
293
|
+
p.subcategory.toLowerCase().includes(f) ||
|
|
294
|
+
p.id.toLowerCase().includes(f)));
|
|
295
|
+
}
|
|
296
|
+
// Deduplicate by subcategory
|
|
297
|
+
const uniquePatterns = new Map();
|
|
298
|
+
for (const p of patterns) {
|
|
299
|
+
const key = `${p.category}/${p.subcategory}`;
|
|
300
|
+
if (!uniquePatterns.has(key) || p.locations.length > uniquePatterns.get(key).locations.length) {
|
|
301
|
+
uniquePatterns.set(key, p);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Limit to 25 patterns max
|
|
305
|
+
const limitedPatterns = Array.from(uniquePatterns.entries()).slice(0, 25);
|
|
306
|
+
// Read code snippets
|
|
307
|
+
const fileCache = new Map();
|
|
308
|
+
const fileContentCache = new Map();
|
|
309
|
+
const sourceFiles = new Set();
|
|
310
|
+
let excludedCount = 0;
|
|
311
|
+
let deprecatedCount = 0;
|
|
312
|
+
const getFileLines = async (filePath) => {
|
|
313
|
+
if (fileCache.has(filePath)) {
|
|
314
|
+
return fileCache.get(filePath);
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
const fullPath = path.join(this.projectRoot, filePath);
|
|
318
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
319
|
+
const lines = content.split('\n');
|
|
320
|
+
fileCache.set(filePath, lines);
|
|
321
|
+
fileContentCache.set(filePath, content);
|
|
322
|
+
sourceFiles.add(filePath);
|
|
323
|
+
return lines;
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const extractSnippet = (lines, startLine, endLine) => {
|
|
330
|
+
const start = Math.max(0, startLine - contextLines - 1);
|
|
331
|
+
const end = Math.min(lines.length, (endLine ?? startLine) + contextLines);
|
|
332
|
+
return lines.slice(start, end).join('\n');
|
|
333
|
+
};
|
|
334
|
+
// Build output
|
|
335
|
+
let output = `# Pattern Pack: ${packDef.name}\n\n`;
|
|
336
|
+
output += `${packDef.description}\n\n`;
|
|
337
|
+
output += `Generated: ${new Date().toISOString()}\n\n`;
|
|
338
|
+
output += `---\n\n`;
|
|
339
|
+
// Group by category
|
|
340
|
+
const grouped = new Map();
|
|
341
|
+
for (const [, pattern] of limitedPatterns) {
|
|
342
|
+
const examples = [];
|
|
343
|
+
const seenFiles = new Set();
|
|
344
|
+
// Sort locations by quality score (best examples first)
|
|
345
|
+
const scoredLocations = pattern.locations
|
|
346
|
+
.map(loc => ({ loc, score: scoreLocation(loc, loc.file) }))
|
|
347
|
+
.filter(({ loc }) => !shouldExcludeFile(loc.file))
|
|
348
|
+
.sort((a, b) => b.score - a.score);
|
|
349
|
+
// Track excluded files
|
|
350
|
+
const excludedFromPattern = pattern.locations.length - scoredLocations.length;
|
|
351
|
+
excludedCount += excludedFromPattern;
|
|
352
|
+
for (const { loc } of scoredLocations) {
|
|
353
|
+
if (seenFiles.has(loc.file))
|
|
354
|
+
continue;
|
|
355
|
+
if (examples.length >= maxExamples)
|
|
356
|
+
break;
|
|
357
|
+
const lines = await getFileLines(loc.file);
|
|
358
|
+
if (lines.length === 0)
|
|
359
|
+
continue;
|
|
360
|
+
// Check for deprecation markers
|
|
361
|
+
const content = fileContentCache.get(loc.file) || '';
|
|
362
|
+
if (!includeDeprecated && isDeprecatedContent(content)) {
|
|
363
|
+
deprecatedCount++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const snippet = extractSnippet(lines, loc.line, loc.endLine);
|
|
367
|
+
if (snippet.trim()) {
|
|
368
|
+
examples.push({ file: loc.file, line: loc.line, code: snippet });
|
|
369
|
+
seenFiles.add(loc.file);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (examples.length > 0) {
|
|
373
|
+
if (!grouped.has(pattern.category)) {
|
|
374
|
+
grouped.set(pattern.category, []);
|
|
375
|
+
}
|
|
376
|
+
grouped.get(pattern.category).push({ pattern, examples });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Format output
|
|
380
|
+
for (const [category, items] of grouped) {
|
|
381
|
+
output += `## ${category.toUpperCase()}\n\n`;
|
|
382
|
+
for (const { pattern, examples } of items) {
|
|
383
|
+
output += `### ${pattern.subcategory}\n`;
|
|
384
|
+
output += `**${pattern.name}** (${(pattern.confidence.score * 100).toFixed(0)}% confidence)\n`;
|
|
385
|
+
if (pattern.description) {
|
|
386
|
+
output += `${pattern.description}\n`;
|
|
387
|
+
}
|
|
388
|
+
output += '\n';
|
|
389
|
+
for (const ex of examples) {
|
|
390
|
+
output += `**${ex.file}:${ex.line}**\n`;
|
|
391
|
+
output += '```\n';
|
|
392
|
+
output += ex.code;
|
|
393
|
+
output += '\n```\n\n';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Add filtering stats at the end
|
|
398
|
+
if (excludedCount > 0 || deprecatedCount > 0) {
|
|
399
|
+
output += `---\n\n`;
|
|
400
|
+
output += `*Filtering: ${excludedCount} non-source files excluded`;
|
|
401
|
+
if (deprecatedCount > 0) {
|
|
402
|
+
output += `, ${deprecatedCount} deprecated files skipped`;
|
|
403
|
+
}
|
|
404
|
+
output += `*\n`;
|
|
405
|
+
}
|
|
406
|
+
// Save cache
|
|
407
|
+
const generatedAt = new Date().toISOString();
|
|
408
|
+
const cachePath = path.join(this.cacheDir, `${packDef.name}.md`);
|
|
409
|
+
const metaPath = path.join(this.cacheDir, `${packDef.name}.meta.json`);
|
|
410
|
+
const meta = {
|
|
411
|
+
name: packDef.name,
|
|
412
|
+
generatedAt,
|
|
413
|
+
patternHash: await this.computePatternHash(packDef),
|
|
414
|
+
sourceFiles: Array.from(sourceFiles),
|
|
415
|
+
packDefHash: this.hashPackDef(packDef),
|
|
416
|
+
};
|
|
417
|
+
await fs.writeFile(cachePath, output, 'utf-8');
|
|
418
|
+
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
419
|
+
return {
|
|
420
|
+
content: output,
|
|
421
|
+
fromCache: false,
|
|
422
|
+
generatedAt,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
hashPackDef(packDef) {
|
|
426
|
+
const str = JSON.stringify({
|
|
427
|
+
categories: packDef.categories,
|
|
428
|
+
patterns: packDef.patterns,
|
|
429
|
+
maxExamples: packDef.maxExamples,
|
|
430
|
+
contextLines: packDef.contextLines,
|
|
431
|
+
});
|
|
432
|
+
return createHash('md5').update(str).digest('hex').slice(0, 12);
|
|
433
|
+
}
|
|
434
|
+
async computePatternHash(packDef) {
|
|
435
|
+
let patterns = this.store.getAll();
|
|
436
|
+
const cats = new Set(packDef.categories);
|
|
437
|
+
patterns = patterns.filter(p => cats.has(p.category));
|
|
438
|
+
// Hash pattern IDs and location counts
|
|
439
|
+
const data = patterns.map(p => `${p.id}:${p.locations.length}`).sort().join('|');
|
|
440
|
+
return createHash('md5').update(data).digest('hex').slice(0, 12);
|
|
441
|
+
}
|
|
442
|
+
async refreshAllPacks() {
|
|
443
|
+
const results = new Map();
|
|
444
|
+
for (const pack of this.getAllPacks()) {
|
|
445
|
+
const result = await this.getPackContent(pack.name, { refresh: true });
|
|
446
|
+
results.set(pack.name, result);
|
|
447
|
+
}
|
|
448
|
+
return results;
|
|
449
|
+
}
|
|
450
|
+
// ==========================================================================
|
|
451
|
+
// Usage Tracking & Pack Learning
|
|
452
|
+
// ==========================================================================
|
|
453
|
+
/**
|
|
454
|
+
* Track pack/category usage for learning
|
|
455
|
+
*/
|
|
456
|
+
async trackUsage(usage) {
|
|
457
|
+
const usageFile = path.join(this.packsDir, 'usage.json');
|
|
458
|
+
let usageHistory = [];
|
|
459
|
+
try {
|
|
460
|
+
const content = await fs.readFile(usageFile, 'utf-8');
|
|
461
|
+
usageHistory = JSON.parse(content);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// No existing usage file
|
|
465
|
+
}
|
|
466
|
+
// Add new usage
|
|
467
|
+
usageHistory.push({
|
|
468
|
+
...usage,
|
|
469
|
+
timestamp: usage.timestamp || new Date().toISOString(),
|
|
470
|
+
});
|
|
471
|
+
// Keep last 1000 entries
|
|
472
|
+
if (usageHistory.length > 1000) {
|
|
473
|
+
usageHistory = usageHistory.slice(-1000);
|
|
474
|
+
}
|
|
475
|
+
await fs.writeFile(usageFile, JSON.stringify(usageHistory, null, 2), 'utf-8');
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Suggest packs based on usage patterns
|
|
479
|
+
*/
|
|
480
|
+
async suggestPacks() {
|
|
481
|
+
const usageFile = path.join(this.packsDir, 'usage.json');
|
|
482
|
+
let usageHistory = [];
|
|
483
|
+
try {
|
|
484
|
+
const content = await fs.readFile(usageFile, 'utf-8');
|
|
485
|
+
usageHistory = JSON.parse(content);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return []; // No usage data
|
|
489
|
+
}
|
|
490
|
+
// Group by category combination
|
|
491
|
+
const comboCounts = new Map();
|
|
492
|
+
for (const usage of usageHistory) {
|
|
493
|
+
const key = usage.categories.sort().join(',');
|
|
494
|
+
const existing = comboCounts.get(key);
|
|
495
|
+
if (existing) {
|
|
496
|
+
existing.count++;
|
|
497
|
+
existing.lastUsed = usage.timestamp;
|
|
498
|
+
// Merge patterns
|
|
499
|
+
if (usage.patterns) {
|
|
500
|
+
for (const p of usage.patterns) {
|
|
501
|
+
if (!existing.patterns.includes(p)) {
|
|
502
|
+
existing.patterns.push(p);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
comboCounts.set(key, {
|
|
509
|
+
categories: usage.categories,
|
|
510
|
+
patterns: usage.patterns || [],
|
|
511
|
+
count: 1,
|
|
512
|
+
lastUsed: usage.timestamp,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Filter out existing packs and sort by usage
|
|
517
|
+
const existingPackKeys = new Set(this.getAllPacks().map(p => p.categories.sort().join(',')));
|
|
518
|
+
const suggestions = [];
|
|
519
|
+
for (const [key, data] of comboCounts) {
|
|
520
|
+
// Skip if already a pack
|
|
521
|
+
if (existingPackKeys.has(key))
|
|
522
|
+
continue;
|
|
523
|
+
// Only suggest if used at least 3 times
|
|
524
|
+
if (data.count < 3)
|
|
525
|
+
continue;
|
|
526
|
+
// Generate a name from categories
|
|
527
|
+
const name = `custom_${data.categories.slice(0, 2).join('_')}`;
|
|
528
|
+
suggestions.push({
|
|
529
|
+
name,
|
|
530
|
+
description: `Auto-suggested pack based on ${data.count} uses`,
|
|
531
|
+
categories: data.categories,
|
|
532
|
+
patterns: data.patterns.length > 0 ? data.patterns : undefined,
|
|
533
|
+
usageCount: data.count,
|
|
534
|
+
lastUsed: data.lastUsed,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
// Sort by usage count descending
|
|
538
|
+
suggestions.sort((a, b) => b.usageCount - a.usageCount);
|
|
539
|
+
return suggestions.slice(0, 5); // Top 5 suggestions
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Create a custom pack from suggestion or manual definition
|
|
543
|
+
*/
|
|
544
|
+
async createCustomPack(pack) {
|
|
545
|
+
const customPacksPath = path.join(this.packsDir, 'packs.json');
|
|
546
|
+
let customPacks = [];
|
|
547
|
+
try {
|
|
548
|
+
const content = await fs.readFile(customPacksPath, 'utf-8');
|
|
549
|
+
customPacks = JSON.parse(content);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// No existing custom packs
|
|
553
|
+
}
|
|
554
|
+
// Check for duplicate name
|
|
555
|
+
const existingIndex = customPacks.findIndex(p => p.name === pack.name);
|
|
556
|
+
if (existingIndex >= 0) {
|
|
557
|
+
// Update existing
|
|
558
|
+
customPacks[existingIndex] = pack;
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
customPacks.push(pack);
|
|
562
|
+
}
|
|
563
|
+
await fs.writeFile(customPacksPath, JSON.stringify(customPacks, null, 2), 'utf-8');
|
|
564
|
+
// Reload custom packs
|
|
565
|
+
this.customPacks = customPacks;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Delete a custom pack
|
|
569
|
+
*/
|
|
570
|
+
async deleteCustomPack(name) {
|
|
571
|
+
const customPacksPath = path.join(this.packsDir, 'packs.json');
|
|
572
|
+
let customPacks = [];
|
|
573
|
+
try {
|
|
574
|
+
const content = await fs.readFile(customPacksPath, 'utf-8');
|
|
575
|
+
customPacks = JSON.parse(content);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const initialLength = customPacks.length;
|
|
581
|
+
customPacks = customPacks.filter(p => p.name !== name);
|
|
582
|
+
if (customPacks.length === initialLength) {
|
|
583
|
+
return false; // Not found
|
|
584
|
+
}
|
|
585
|
+
await fs.writeFile(customPacksPath, JSON.stringify(customPacks, null, 2), 'utf-8');
|
|
586
|
+
this.customPacks = customPacks;
|
|
587
|
+
// Also delete cache
|
|
588
|
+
try {
|
|
589
|
+
await fs.unlink(path.join(this.cacheDir, `${name}.md`));
|
|
590
|
+
await fs.unlink(path.join(this.cacheDir, `${name}.meta.json`));
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// Cache files may not exist
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Infer packs from codebase structure (co-occurring patterns)
|
|
599
|
+
*/
|
|
600
|
+
async inferPacksFromStructure() {
|
|
601
|
+
const patterns = this.store.getAll();
|
|
602
|
+
// Track which categories appear together in files
|
|
603
|
+
const fileCategories = new Map();
|
|
604
|
+
for (const p of patterns) {
|
|
605
|
+
for (const loc of p.locations) {
|
|
606
|
+
if (!fileCategories.has(loc.file)) {
|
|
607
|
+
fileCategories.set(loc.file, new Set());
|
|
608
|
+
}
|
|
609
|
+
fileCategories.get(loc.file).add(p.category);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Count category co-occurrences
|
|
613
|
+
const coOccurrence = new Map();
|
|
614
|
+
for (const categories of fileCategories.values()) {
|
|
615
|
+
if (categories.size < 2)
|
|
616
|
+
continue;
|
|
617
|
+
const catArray = Array.from(categories).sort();
|
|
618
|
+
// Generate pairs and triples
|
|
619
|
+
for (let i = 0; i < catArray.length; i++) {
|
|
620
|
+
for (let j = i + 1; j < catArray.length; j++) {
|
|
621
|
+
const pair = `${catArray[i]},${catArray[j]}`;
|
|
622
|
+
coOccurrence.set(pair, (coOccurrence.get(pair) || 0) + 1);
|
|
623
|
+
// Triples
|
|
624
|
+
for (let k = j + 1; k < catArray.length; k++) {
|
|
625
|
+
const triple = `${catArray[i]},${catArray[j]},${catArray[k]}`;
|
|
626
|
+
coOccurrence.set(triple, (coOccurrence.get(triple) || 0) + 1);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Filter to significant co-occurrences (at least 5 files)
|
|
632
|
+
const suggestions = [];
|
|
633
|
+
const existingPackKeys = new Set(this.getAllPacks().map(p => p.categories.sort().join(',')));
|
|
634
|
+
for (const [key, count] of coOccurrence) {
|
|
635
|
+
if (count < 5)
|
|
636
|
+
continue;
|
|
637
|
+
if (existingPackKeys.has(key))
|
|
638
|
+
continue;
|
|
639
|
+
const categories = key.split(',');
|
|
640
|
+
const name = `inferred_${categories.slice(0, 2).join('_')}`;
|
|
641
|
+
suggestions.push({
|
|
642
|
+
name,
|
|
643
|
+
description: `Inferred from ${count} files with co-occurring patterns`,
|
|
644
|
+
categories,
|
|
645
|
+
usageCount: count,
|
|
646
|
+
lastUsed: new Date().toISOString(),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
// Sort by count and return top suggestions
|
|
650
|
+
suggestions.sort((a, b) => b.usageCount - a.usageCount);
|
|
651
|
+
return suggestions.slice(0, 5);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=packs.js.map
|