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/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