cmp-standards 3.1.2 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/cli/index.js +488 -1
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/db/migrations.d.ts +1 -1
  4. package/dist/db/migrations.d.ts.map +1 -1
  5. package/dist/db/migrations.js +102 -2
  6. package/dist/db/migrations.js.map +1 -1
  7. package/dist/eslint/ast-types.d.ts +235 -0
  8. package/dist/eslint/ast-types.d.ts.map +1 -0
  9. package/dist/eslint/ast-types.js +9 -0
  10. package/dist/eslint/ast-types.js.map +1 -0
  11. package/dist/eslint/rules/consistent-error-handling.d.ts.map +1 -1
  12. package/dist/eslint/rules/consistent-error-handling.js +2 -1
  13. package/dist/eslint/rules/consistent-error-handling.js.map +1 -1
  14. package/dist/eslint/rules/no-async-useeffect.js.map +1 -1
  15. package/dist/events/EventBus.js.map +1 -1
  16. package/dist/events/types.d.ts +174 -4
  17. package/dist/events/types.d.ts.map +1 -1
  18. package/dist/events/types.js +15 -0
  19. package/dist/events/types.js.map +1 -1
  20. package/dist/hooks/session-start.js +3 -3
  21. package/dist/hooks/session-start.js.map +1 -1
  22. package/dist/index.d.ts +11 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +21 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp/server.d.ts.map +1 -1
  27. package/dist/mcp/server.js +8 -4
  28. package/dist/mcp/server.js.map +1 -1
  29. package/dist/patterns/feedback-loop.d.ts +2 -2
  30. package/dist/patterns/lifecycle.d.ts.map +1 -1
  31. package/dist/patterns/lifecycle.js +11 -13
  32. package/dist/patterns/lifecycle.js.map +1 -1
  33. package/dist/patterns/registry.d.ts +2 -2
  34. package/dist/plugins/PluginManager.d.ts.map +1 -1
  35. package/dist/plugins/PluginManager.js +4 -1
  36. package/dist/plugins/PluginManager.js.map +1 -1
  37. package/dist/schema/codewiki-types.d.ts +1899 -0
  38. package/dist/schema/codewiki-types.d.ts.map +1 -0
  39. package/dist/schema/codewiki-types.js +585 -0
  40. package/dist/schema/codewiki-types.js.map +1 -0
  41. package/dist/schema/expert-types.d.ts +2 -2
  42. package/dist/schema/opportunity-types.d.ts +505 -0
  43. package/dist/schema/opportunity-types.d.ts.map +1 -0
  44. package/dist/schema/opportunity-types.js +255 -0
  45. package/dist/schema/opportunity-types.js.map +1 -0
  46. package/dist/services/AuditLog.d.ts.map +1 -1
  47. package/dist/services/AuditLog.js +4 -1
  48. package/dist/services/AuditLog.js.map +1 -1
  49. package/dist/services/CodeWikiIndexer.d.ts +145 -0
  50. package/dist/services/CodeWikiIndexer.d.ts.map +1 -0
  51. package/dist/services/CodeWikiIndexer.js +664 -0
  52. package/dist/services/CodeWikiIndexer.js.map +1 -0
  53. package/dist/services/OpportunityDiscovery.d.ts +84 -0
  54. package/dist/services/OpportunityDiscovery.d.ts.map +1 -0
  55. package/dist/services/OpportunityDiscovery.js +754 -0
  56. package/dist/services/OpportunityDiscovery.js.map +1 -0
  57. package/dist/services/ProjectScanner.d.ts.map +1 -1
  58. package/dist/services/ProjectScanner.js +1 -1
  59. package/dist/services/ProjectScanner.js.map +1 -1
  60. package/dist/services/index.d.ts +1 -0
  61. package/dist/services/index.d.ts.map +1 -1
  62. package/dist/services/index.js +2 -0
  63. package/dist/services/index.js.map +1 -1
  64. package/dist/utils/env.d.ts +149 -0
  65. package/dist/utils/env.d.ts.map +1 -0
  66. package/dist/utils/env.js +223 -0
  67. package/dist/utils/env.js.map +1 -0
  68. package/dist/utils/index.d.ts +3 -0
  69. package/dist/utils/index.d.ts.map +1 -1
  70. package/dist/utils/index.js +6 -0
  71. package/dist/utils/index.js.map +1 -1
  72. package/dist/utils/logger.d.ts +126 -0
  73. package/dist/utils/logger.d.ts.map +1 -0
  74. package/dist/utils/logger.js +231 -0
  75. package/dist/utils/logger.js.map +1 -0
  76. package/package.json +1 -1
@@ -0,0 +1,754 @@
1
+ /**
2
+ * @file OpportunityDiscovery Service - cmp-standards
3
+ * @description Multi-angle opportunity discovery system
4
+ *
5
+ * This service analyzes code from multiple perspectives to find:
6
+ * - New feature ideas
7
+ * - Edge cases and error handling gaps
8
+ * - Future feature possibilities
9
+ * - Technical debt
10
+ * - Performance improvements
11
+ * - Security vulnerabilities
12
+ * - DX improvements
13
+ */
14
+ import { ulid } from 'ulid';
15
+ import fs from 'fs/promises';
16
+ import path from 'path';
17
+ import { turso } from '../db/turso-client.js';
18
+ import { createOpportunity, } from '../schema/opportunity-types.js';
19
+ import { getExpertRouter } from '../experts/ExpertRouter.js';
20
+ const ANALYSIS_PATTERNS = [
21
+ // Edge Cases
22
+ {
23
+ name: 'Missing Error Handling',
24
+ category: 'edge_case',
25
+ patterns: [
26
+ /\.catch\s*\(\s*\)/,
27
+ /catch\s*\(\s*_?\s*\)\s*\{\s*\}/,
28
+ /catch\s*\{[\s\n]*\}/,
29
+ /\.then\([^)]+\)(?!\s*\.catch)/,
30
+ ],
31
+ impact: 'high',
32
+ effort: 'small',
33
+ description: 'Empty or missing error handlers can cause silent failures',
34
+ suggestedApproach: 'Add proper error handling with logging and user feedback',
35
+ },
36
+ {
37
+ name: 'Unchecked Array Access',
38
+ category: 'edge_case',
39
+ patterns: [
40
+ /\[\d+\](?!\s*\?\?)/,
41
+ /\.at\(\d+\)(?!\s*\?\?)/,
42
+ ],
43
+ impact: 'medium',
44
+ effort: 'trivial',
45
+ description: 'Array access without bounds checking can cause runtime errors',
46
+ suggestedApproach: 'Use optional chaining or array bounds validation',
47
+ },
48
+ {
49
+ name: 'Missing Null Checks',
50
+ category: 'edge_case',
51
+ patterns: [
52
+ /(\w+)\.\w+\s*&&\s*\1\./,
53
+ /if\s*\(\s*!\s*\w+\s*\)\s*return/,
54
+ ],
55
+ impact: 'medium',
56
+ effort: 'small',
57
+ description: 'Potential null/undefined access without proper guards',
58
+ suggestedApproach: 'Add null checks or use optional chaining',
59
+ },
60
+ // Technical Debt
61
+ {
62
+ name: 'TODO Comments',
63
+ category: 'technical_debt',
64
+ patterns: [
65
+ /\/\/\s*TODO/i,
66
+ /\/\/\s*FIXME/i,
67
+ /\/\/\s*HACK/i,
68
+ /\/\*\s*TODO/i,
69
+ ],
70
+ impact: 'low',
71
+ effort: 'medium',
72
+ description: 'Unaddressed TODO comments indicate incomplete work',
73
+ suggestedApproach: 'Review and either implement or create proper issues',
74
+ },
75
+ {
76
+ name: 'Any Type Usage',
77
+ category: 'technical_debt',
78
+ patterns: [
79
+ /:\s*any\b/,
80
+ /as\s+any\b/,
81
+ /<any>/,
82
+ ],
83
+ impact: 'medium',
84
+ effort: 'medium',
85
+ description: 'Using "any" type bypasses TypeScript safety',
86
+ suggestedApproach: 'Replace with proper types or use unknown with type guards',
87
+ },
88
+ {
89
+ name: 'Console Logs',
90
+ category: 'technical_debt',
91
+ patterns: [
92
+ /console\.log\(/,
93
+ /console\.warn\(/,
94
+ /console\.error\(/,
95
+ ],
96
+ impact: 'low',
97
+ effort: 'trivial',
98
+ description: 'Console statements should be replaced with proper logging',
99
+ suggestedApproach: 'Use structured logging service instead',
100
+ },
101
+ {
102
+ name: 'Magic Numbers',
103
+ category: 'technical_debt',
104
+ patterns: [
105
+ /(?<![\w.])(?:1000|3600|86400|60|24|365|100|1024)(?!\d)/,
106
+ ],
107
+ impact: 'low',
108
+ effort: 'trivial',
109
+ description: 'Magic numbers reduce code readability',
110
+ suggestedApproach: 'Extract to named constants with clear meaning',
111
+ },
112
+ // Performance
113
+ {
114
+ name: 'Potential N+1 Query',
115
+ category: 'performance',
116
+ patterns: [
117
+ /for\s*\([^)]+\)\s*\{[^}]*await\s+\w+\.(find|query|get|fetch)/i,
118
+ /\.map\([^)]*async/,
119
+ /\.forEach\([^)]*async/,
120
+ ],
121
+ impact: 'high',
122
+ effort: 'medium',
123
+ description: 'Async operations in loops can cause N+1 query problems',
124
+ suggestedApproach: 'Use Promise.all or batch queries',
125
+ },
126
+ {
127
+ name: 'Missing Memoization',
128
+ category: 'performance',
129
+ patterns: [
130
+ /useMemo\s*\(\s*\(\)\s*=>\s*\w+,\s*\[\]/,
131
+ /useCallback\s*\([^)]+,\s*\[\]/,
132
+ ],
133
+ impact: 'medium',
134
+ effort: 'small',
135
+ description: 'Empty dependency arrays may indicate missing memoization',
136
+ suggestedApproach: 'Review dependencies and add proper memo boundaries',
137
+ },
138
+ {
139
+ name: 'Unbounded Data Fetch',
140
+ category: 'performance',
141
+ patterns: [
142
+ /findMany\s*\(\s*\)/,
143
+ /find\s*\(\s*\{\s*\}\s*\)/,
144
+ /SELECT\s+\*\s+FROM/i,
145
+ ],
146
+ impact: 'high',
147
+ effort: 'small',
148
+ description: 'Fetching all records without limits can cause memory issues',
149
+ suggestedApproach: 'Add pagination or sensible limits',
150
+ },
151
+ // Security
152
+ {
153
+ name: 'Hardcoded Credentials',
154
+ category: 'security',
155
+ patterns: [
156
+ /password\s*[=:]\s*["'][^"']+["']/i,
157
+ /apiKey\s*[=:]\s*["'][^"']+["']/i,
158
+ /secret\s*[=:]\s*["'][^"']+["']/i,
159
+ /token\s*[=:]\s*["'][A-Za-z0-9]+["']/i,
160
+ ],
161
+ impact: 'high',
162
+ effort: 'small',
163
+ description: 'Hardcoded credentials are a security risk',
164
+ suggestedApproach: 'Move to environment variables or secrets manager',
165
+ },
166
+ {
167
+ name: 'SQL Injection Risk',
168
+ category: 'security',
169
+ patterns: [
170
+ /`SELECT.*\$\{/,
171
+ /`INSERT.*\$\{/,
172
+ /`UPDATE.*\$\{/,
173
+ /`DELETE.*\$\{/,
174
+ ],
175
+ impact: 'high',
176
+ effort: 'medium',
177
+ description: 'String interpolation in SQL queries risks injection',
178
+ suggestedApproach: 'Use parameterized queries or query builders',
179
+ },
180
+ {
181
+ name: 'Unsafe User Input',
182
+ category: 'security',
183
+ patterns: [
184
+ /innerHTML\s*=/,
185
+ /dangerouslySetInnerHTML/,
186
+ /eval\s*\(/,
187
+ /new\s+Function\s*\(/,
188
+ ],
189
+ impact: 'high',
190
+ effort: 'medium',
191
+ description: 'Unsafe handling of user input can lead to XSS',
192
+ suggestedApproach: 'Sanitize input and use safe alternatives',
193
+ },
194
+ // DX Improvements
195
+ {
196
+ name: 'Missing JSDoc',
197
+ category: 'dx_improvement',
198
+ patterns: [
199
+ /^export\s+(async\s+)?function\s+\w+/m,
200
+ /^export\s+const\s+\w+\s*=\s*\(/m,
201
+ ],
202
+ impact: 'low',
203
+ effort: 'small',
204
+ description: 'Exported functions should have documentation',
205
+ suggestedApproach: 'Add JSDoc comments with @param and @returns',
206
+ },
207
+ {
208
+ name: 'Complex Function',
209
+ category: 'dx_improvement',
210
+ patterns: [
211
+ /function\s+\w+[^{]*\{[\s\S]{500,}/,
212
+ ],
213
+ impact: 'medium',
214
+ effort: 'medium',
215
+ description: 'Large functions are hard to understand and test',
216
+ suggestedApproach: 'Break down into smaller, focused functions',
217
+ },
218
+ // Test Coverage
219
+ {
220
+ name: 'Missing Test File',
221
+ category: 'test_coverage',
222
+ patterns: [
223
+ /export\s+(async\s+)?function\s+\w+/,
224
+ /export\s+class\s+\w+/,
225
+ ],
226
+ impact: 'medium',
227
+ effort: 'medium',
228
+ description: 'Exported code should have corresponding tests',
229
+ suggestedApproach: 'Add unit tests for public API',
230
+ },
231
+ // Future Features
232
+ {
233
+ name: 'Feature Flag Candidate',
234
+ category: 'future_feature',
235
+ patterns: [
236
+ /if\s*\(\s*false\s*\)/,
237
+ /if\s*\(\s*true\s*\)/,
238
+ /\/\/\s*feature:/i,
239
+ /enabled:\s*false/,
240
+ ],
241
+ impact: 'medium',
242
+ effort: 'small',
243
+ description: 'Code suggests feature flags or conditional features',
244
+ suggestedApproach: 'Consider implementing proper feature flag system',
245
+ },
246
+ // Accessibility
247
+ {
248
+ name: 'Missing ARIA',
249
+ category: 'accessibility',
250
+ patterns: [
251
+ /<button[^>]*>(?![^<]*aria-)/i,
252
+ /<div[^>]*onClick/,
253
+ /<span[^>]*onClick/,
254
+ ],
255
+ impact: 'medium',
256
+ effort: 'small',
257
+ description: 'Interactive elements missing ARIA attributes',
258
+ suggestedApproach: 'Add appropriate aria-label and role attributes',
259
+ },
260
+ ];
261
+ const IDEA_PATTERNS = [
262
+ {
263
+ name: 'Caching Opportunity',
264
+ triggers: ['fetch', 'query', 'api', 'request'],
265
+ category: 'performance',
266
+ description: 'Repeated data fetching could benefit from caching',
267
+ impact: 'high',
268
+ effort: 'medium',
269
+ },
270
+ {
271
+ name: 'Retry Logic',
272
+ triggers: ['fetch', 'api', 'request', 'error'],
273
+ category: 'edge_case',
274
+ description: 'Network operations could benefit from retry logic',
275
+ impact: 'medium',
276
+ effort: 'small',
277
+ },
278
+ {
279
+ name: 'Rate Limiting',
280
+ triggers: ['api', 'endpoint', 'route', 'handler'],
281
+ category: 'security',
282
+ description: 'API endpoints could benefit from rate limiting',
283
+ impact: 'high',
284
+ effort: 'medium',
285
+ },
286
+ {
287
+ name: 'Input Validation',
288
+ triggers: ['body', 'params', 'query', 'input'],
289
+ category: 'security',
290
+ description: 'User input should be validated with Zod or similar',
291
+ impact: 'high',
292
+ effort: 'small',
293
+ },
294
+ {
295
+ name: 'Batch Operations',
296
+ triggers: ['create', 'insert', 'update', 'loop', 'forEach'],
297
+ category: 'performance',
298
+ description: 'Multiple operations could be batched for efficiency',
299
+ impact: 'medium',
300
+ effort: 'medium',
301
+ },
302
+ {
303
+ name: 'Event Sourcing',
304
+ triggers: ['state', 'history', 'audit', 'log', 'track'],
305
+ category: 'future_feature',
306
+ description: 'State changes could be captured for audit trail',
307
+ impact: 'medium',
308
+ effort: 'large',
309
+ },
310
+ {
311
+ name: 'Webhook Support',
312
+ triggers: ['event', 'notify', 'callback', 'integration'],
313
+ category: 'future_feature',
314
+ description: 'External integrations could benefit from webhooks',
315
+ impact: 'medium',
316
+ effort: 'medium',
317
+ },
318
+ {
319
+ name: 'Analytics Tracking',
320
+ triggers: ['click', 'view', 'user', 'action', 'event'],
321
+ category: 'future_feature',
322
+ description: 'User interactions could be tracked for analytics',
323
+ impact: 'low',
324
+ effort: 'medium',
325
+ },
326
+ ];
327
+ // =============================================================================
328
+ // Service Implementation
329
+ // =============================================================================
330
+ export class OpportunityDiscovery {
331
+ system;
332
+ constructor(system) {
333
+ this.system = system;
334
+ }
335
+ /**
336
+ * Discover opportunities across the codebase
337
+ */
338
+ async discover(request) {
339
+ const startTime = Date.now();
340
+ const runId = ulid();
341
+ const fullRequest = {
342
+ paths: request.paths ?? ['.'],
343
+ categories: request.categories ?? [],
344
+ minImpact: request.minImpact ?? 'low',
345
+ maxEffort: request.maxEffort ?? 'epic',
346
+ includeUncertain: request.includeUncertain ?? false,
347
+ depth: request.depth ?? 'standard',
348
+ system: request.system ?? this.system,
349
+ };
350
+ const opportunities = [];
351
+ const expertsUsed = new Set();
352
+ let filesAnalyzed = 0;
353
+ // Get files to analyze
354
+ const files = await this.collectFiles(fullRequest.paths, fullRequest.depth);
355
+ filesAnalyzed = files.length;
356
+ // Analyze each file
357
+ for (const file of files) {
358
+ try {
359
+ const content = await fs.readFile(file, 'utf-8');
360
+ const fileOpportunities = await this.analyzeFile(file, content, fullRequest);
361
+ opportunities.push(...fileOpportunities);
362
+ // Track which experts would review this
363
+ const router = getExpertRouter();
364
+ const routing = router.route({ files: [file], content });
365
+ routing.selectedExperts.forEach(e => expertsUsed.add(e.expertId));
366
+ }
367
+ catch {
368
+ // Skip files that can't be read
369
+ }
370
+ }
371
+ // Run higher-level analysis
372
+ const ideaOpportunities = await this.analyzeForIdeas(files, fullRequest);
373
+ opportunities.push(...ideaOpportunities);
374
+ // Filter by impact and effort
375
+ const filtered = this.filterOpportunities(opportunities, fullRequest);
376
+ // Deduplicate similar opportunities
377
+ const deduped = this.deduplicateOpportunities(filtered);
378
+ // Calculate summary
379
+ const summary = this.calculateSummary(deduped, filesAnalyzed, Date.now() - startTime);
380
+ // Store opportunities in database
381
+ await this.storeOpportunities(runId, deduped);
382
+ return {
383
+ runId,
384
+ timestamp: new Date().toISOString(),
385
+ request: fullRequest,
386
+ opportunities: deduped,
387
+ summary,
388
+ expertsUsed: Array.from(expertsUsed),
389
+ };
390
+ }
391
+ /**
392
+ * Collect files to analyze based on paths and depth
393
+ */
394
+ async collectFiles(paths, depth) {
395
+ const files = [];
396
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs'];
397
+ const maxFiles = depth === 'quick' ? 50 : depth === 'standard' ? 200 : 500;
398
+ for (const basePath of paths) {
399
+ try {
400
+ const stat = await fs.stat(basePath);
401
+ if (stat.isFile()) {
402
+ files.push(basePath);
403
+ }
404
+ else if (stat.isDirectory()) {
405
+ await this.walkDirectory(basePath, files, extensions, maxFiles);
406
+ }
407
+ }
408
+ catch {
409
+ // Skip invalid paths
410
+ }
411
+ if (files.length >= maxFiles)
412
+ break;
413
+ }
414
+ return files.slice(0, maxFiles);
415
+ }
416
+ /**
417
+ * Recursively walk directory to collect files
418
+ */
419
+ async walkDirectory(dir, files, extensions, maxFiles) {
420
+ if (files.length >= maxFiles)
421
+ return;
422
+ try {
423
+ const entries = await fs.readdir(dir, { withFileTypes: true });
424
+ for (const entry of entries) {
425
+ if (files.length >= maxFiles)
426
+ break;
427
+ const fullPath = path.join(dir, entry.name);
428
+ // Skip node_modules, .git, dist, etc.
429
+ if (entry.isDirectory()) {
430
+ if (['node_modules', '.git', 'dist', 'build', '.next', 'coverage'].includes(entry.name)) {
431
+ continue;
432
+ }
433
+ await this.walkDirectory(fullPath, files, extensions, maxFiles);
434
+ }
435
+ else if (entry.isFile()) {
436
+ const ext = path.extname(entry.name);
437
+ if (extensions.includes(ext)) {
438
+ files.push(fullPath);
439
+ }
440
+ }
441
+ }
442
+ }
443
+ catch {
444
+ // Skip directories that can't be read
445
+ }
446
+ }
447
+ /**
448
+ * Analyze a single file for opportunities
449
+ */
450
+ async analyzeFile(filePath, content, request) {
451
+ const opportunities = [];
452
+ const lines = content.split('\n');
453
+ for (const pattern of ANALYSIS_PATTERNS) {
454
+ // Skip if category is filtered out
455
+ if (request.categories.length > 0 && !request.categories.includes(pattern.category)) {
456
+ continue;
457
+ }
458
+ for (const regex of pattern.patterns) {
459
+ // Reset regex if global
460
+ regex.lastIndex = 0;
461
+ let match;
462
+ const fullContent = content;
463
+ const globalRegex = new RegExp(regex.source, regex.flags.includes('g') ? regex.flags : regex.flags + 'g');
464
+ while ((match = globalRegex.exec(fullContent)) !== null) {
465
+ // Find line number
466
+ const beforeMatch = fullContent.slice(0, match.index);
467
+ const lineNumber = beforeMatch.split('\n').length;
468
+ // Get code context (surrounding lines)
469
+ const startLine = Math.max(0, lineNumber - 2);
470
+ const endLine = Math.min(lines.length - 1, lineNumber + 2);
471
+ const codeContext = lines.slice(startLine, endLine + 1).join('\n');
472
+ const opportunity = createOpportunity(`${pattern.name} in ${path.basename(filePath)}`, pattern.description, pattern.category, {
473
+ impact: pattern.impact,
474
+ effort: pattern.effort,
475
+ source: 'code_analysis',
476
+ confidence: 0.8,
477
+ relatedFiles: [filePath],
478
+ suggestedApproach: pattern.suggestedApproach,
479
+ codeExamples: [{
480
+ file: filePath,
481
+ lineStart: startLine + 1,
482
+ lineEnd: endLine + 1,
483
+ code: codeContext,
484
+ note: `Match found at line ${lineNumber}`,
485
+ }],
486
+ tags: [pattern.category, pattern.name.toLowerCase().replace(/\s+/g, '-')],
487
+ });
488
+ opportunities.push(opportunity);
489
+ // Limit matches per pattern per file
490
+ if (opportunities.filter(o => o.title.includes(pattern.name)).length >= 3) {
491
+ break;
492
+ }
493
+ }
494
+ }
495
+ }
496
+ return opportunities;
497
+ }
498
+ /**
499
+ * Analyze files for higher-level idea opportunities
500
+ */
501
+ async analyzeForIdeas(files, request) {
502
+ const opportunities = [];
503
+ const triggerCounts = new Map();
504
+ // Count trigger occurrences across files
505
+ for (const file of files) {
506
+ try {
507
+ const content = await fs.readFile(file, 'utf-8');
508
+ const contentLower = content.toLowerCase();
509
+ for (const idea of IDEA_PATTERNS) {
510
+ for (const trigger of idea.triggers) {
511
+ if (contentLower.includes(trigger)) {
512
+ const existing = triggerCounts.get(idea.name) ?? { files: [], count: 0 };
513
+ if (!existing.files.includes(file)) {
514
+ existing.files.push(file);
515
+ }
516
+ existing.count++;
517
+ triggerCounts.set(idea.name, existing);
518
+ }
519
+ }
520
+ }
521
+ }
522
+ catch {
523
+ // Skip unreadable files
524
+ }
525
+ }
526
+ // Generate opportunities from patterns that appear frequently
527
+ for (const idea of IDEA_PATTERNS) {
528
+ const data = triggerCounts.get(idea.name);
529
+ if (data && data.files.length >= 2) {
530
+ // Skip if category is filtered out
531
+ if (request.categories.length > 0 && !request.categories.includes(idea.category)) {
532
+ continue;
533
+ }
534
+ const opportunity = createOpportunity(idea.name, idea.description, idea.category, {
535
+ impact: idea.impact,
536
+ effort: idea.effort,
537
+ source: 'pattern_detection',
538
+ confidence: Math.min(0.9, 0.5 + (data.files.length * 0.1)),
539
+ relatedFiles: data.files.slice(0, 5),
540
+ tags: [idea.category, 'pattern', idea.name.toLowerCase().replace(/\s+/g, '-')],
541
+ });
542
+ opportunities.push(opportunity);
543
+ }
544
+ }
545
+ return opportunities;
546
+ }
547
+ /**
548
+ * Filter opportunities by impact and effort
549
+ */
550
+ filterOpportunities(opportunities, request) {
551
+ const impactOrder = ['low', 'medium', 'high'];
552
+ const effortOrder = ['trivial', 'small', 'medium', 'large', 'epic'];
553
+ const minImpactIndex = impactOrder.indexOf(request.minImpact);
554
+ const maxEffortIndex = effortOrder.indexOf(request.maxEffort);
555
+ return opportunities.filter(opp => {
556
+ const impactIndex = impactOrder.indexOf(opp.impact);
557
+ const effortIndex = effortOrder.indexOf(opp.effort);
558
+ // Filter by impact (must be >= minImpact)
559
+ if (impactIndex < minImpactIndex)
560
+ return false;
561
+ // Filter by effort (must be <= maxEffort)
562
+ if (effortIndex > maxEffortIndex)
563
+ return false;
564
+ // Filter by confidence
565
+ if (!request.includeUncertain && opp.confidence < 0.5)
566
+ return false;
567
+ return true;
568
+ });
569
+ }
570
+ /**
571
+ * Deduplicate similar opportunities
572
+ */
573
+ deduplicateOpportunities(opportunities) {
574
+ const seen = new Map();
575
+ for (const opp of opportunities) {
576
+ // Create a key based on title and first related file
577
+ const key = `${opp.title}-${opp.relatedFiles[0] ?? ''}`;
578
+ const existing = seen.get(key);
579
+ if (!existing || opp.confidence > existing.confidence) {
580
+ seen.set(key, opp);
581
+ }
582
+ }
583
+ return Array.from(seen.values());
584
+ }
585
+ /**
586
+ * Calculate summary statistics
587
+ */
588
+ calculateSummary(opportunities, filesAnalyzed, analysisTimeMs) {
589
+ const byCategory = {};
590
+ const byImpact = {};
591
+ const byEffort = {};
592
+ const bySource = {};
593
+ const fileCount = {};
594
+ for (const opp of opportunities) {
595
+ byCategory[opp.category] = (byCategory[opp.category] ?? 0) + 1;
596
+ byImpact[opp.impact] = (byImpact[opp.impact] ?? 0) + 1;
597
+ byEffort[opp.effort] = (byEffort[opp.effort] ?? 0) + 1;
598
+ bySource[opp.source] = (bySource[opp.source] ?? 0) + 1;
599
+ for (const file of opp.relatedFiles) {
600
+ fileCount[file] = (fileCount[file] ?? 0) + 1;
601
+ }
602
+ }
603
+ const topFiles = Object.entries(fileCount)
604
+ .sort((a, b) => b[1] - a[1])
605
+ .slice(0, 10)
606
+ .map(([file, count]) => ({ file, count }));
607
+ return {
608
+ totalFound: opportunities.length,
609
+ byCategory,
610
+ byImpact,
611
+ byEffort,
612
+ bySource,
613
+ topFiles,
614
+ analysisTimeMs,
615
+ filesAnalyzed,
616
+ };
617
+ }
618
+ /**
619
+ * Store opportunities in Turso database
620
+ */
621
+ async storeOpportunities(runId, opportunities) {
622
+ for (const opp of opportunities) {
623
+ await turso.create('opportunity', this.system, {
624
+ ...opp,
625
+ runId,
626
+ }, 'active');
627
+ }
628
+ }
629
+ /**
630
+ * Get recent opportunities from Turso
631
+ */
632
+ async getRecent(limit = 20) {
633
+ const items = await turso.query({
634
+ system: this.system,
635
+ type: 'opportunity',
636
+ status: 'active',
637
+ limit,
638
+ orderBy: 'updated_at',
639
+ orderDir: 'DESC',
640
+ });
641
+ return items.map(item => item.content);
642
+ }
643
+ /**
644
+ * Get opportunities by category from Turso
645
+ */
646
+ async getByCategory(category, limit = 20) {
647
+ const items = await turso.query({
648
+ system: this.system,
649
+ type: 'opportunity',
650
+ status: 'active',
651
+ limit: limit * 2, // Fetch more to filter
652
+ });
653
+ return items
654
+ .map(item => item.content)
655
+ .filter(opp => opp.category === category)
656
+ .slice(0, limit);
657
+ }
658
+ /**
659
+ * Update opportunity status in Turso
660
+ */
661
+ async updateStatus(opportunityId, status) {
662
+ const items = await turso.query({
663
+ system: this.system,
664
+ type: 'opportunity',
665
+ limit: 100,
666
+ });
667
+ const item = items.find(i => i.content?.id === opportunityId);
668
+ if (item) {
669
+ const content = item.content;
670
+ content.status = status;
671
+ content.updatedAt = new Date().toISOString();
672
+ await turso.update(item.id, {
673
+ status: status === 'rejected' || status === 'implemented' ? 'done' : 'active',
674
+ content,
675
+ });
676
+ }
677
+ }
678
+ /**
679
+ * Export opportunities as markdown
680
+ */
681
+ async exportMarkdown(opportunities) {
682
+ let md = `# Opportunity Discovery Report\n\n`;
683
+ md += `*Generated: ${new Date().toISOString()}*\n\n`;
684
+ md += `Total opportunities found: **${opportunities.length}**\n\n`;
685
+ // Group by category
686
+ const byCategory = new Map();
687
+ for (const opp of opportunities) {
688
+ const list = byCategory.get(opp.category) ?? [];
689
+ list.push(opp);
690
+ byCategory.set(opp.category, list);
691
+ }
692
+ // Output by category
693
+ for (const [category, opps] of byCategory) {
694
+ md += `## ${this.formatCategory(category)} (${opps.length})\n\n`;
695
+ for (const opp of opps) {
696
+ md += `### ${opp.title}\n\n`;
697
+ md += `- **Impact**: ${opp.impact}\n`;
698
+ md += `- **Effort**: ${opp.effort}\n`;
699
+ md += `- **Confidence**: ${Math.round(opp.confidence * 100)}%\n`;
700
+ md += `- **Source**: ${opp.source}\n`;
701
+ if (opp.relatedFiles.length > 0) {
702
+ md += `- **Files**: ${opp.relatedFiles.join(', ')}\n`;
703
+ }
704
+ md += `\n${opp.description}\n\n`;
705
+ if (opp.suggestedApproach) {
706
+ md += `**Suggested Approach**: ${opp.suggestedApproach}\n\n`;
707
+ }
708
+ if (opp.codeExamples.length > 0) {
709
+ md += `**Code Context**:\n\`\`\`\n${opp.codeExamples[0].code}\n\`\`\`\n\n`;
710
+ }
711
+ md += `---\n\n`;
712
+ }
713
+ }
714
+ return md;
715
+ }
716
+ /**
717
+ * Format category name for display
718
+ */
719
+ formatCategory(category) {
720
+ const names = {
721
+ idea: 'Ideas',
722
+ edge_case: 'Edge Cases',
723
+ future_feature: 'Future Features',
724
+ technical_debt: 'Technical Debt',
725
+ performance: 'Performance',
726
+ security: 'Security',
727
+ dx_improvement: 'Developer Experience',
728
+ documentation: 'Documentation',
729
+ test_coverage: 'Test Coverage',
730
+ accessibility: 'Accessibility',
731
+ };
732
+ return names[category] ?? category;
733
+ }
734
+ }
735
+ // =============================================================================
736
+ // Factory Functions
737
+ // =============================================================================
738
+ let discoveryInstance = null;
739
+ /**
740
+ * Get or create OpportunityDiscovery instance
741
+ */
742
+ export function getOpportunityDiscovery(system) {
743
+ if (!discoveryInstance) {
744
+ discoveryInstance = new OpportunityDiscovery(system);
745
+ }
746
+ return discoveryInstance;
747
+ }
748
+ /**
749
+ * Create fresh OpportunityDiscovery instance
750
+ */
751
+ export function createOpportunityDiscovery(system) {
752
+ return new OpportunityDiscovery(system);
753
+ }
754
+ //# sourceMappingURL=OpportunityDiscovery.js.map