musubi-sdd 3.10.0 → 5.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.
Files changed (44) hide show
  1. package/README.md +24 -19
  2. package/package.json +1 -1
  3. package/src/agents/agent-loop.js +532 -0
  4. package/src/agents/agentic/code-generator.js +767 -0
  5. package/src/agents/agentic/code-reviewer.js +698 -0
  6. package/src/agents/agentic/index.js +43 -0
  7. package/src/agents/function-tool.js +432 -0
  8. package/src/agents/index.js +45 -0
  9. package/src/agents/schema-generator.js +514 -0
  10. package/src/analyzers/ast-extractor.js +870 -0
  11. package/src/analyzers/context-optimizer.js +681 -0
  12. package/src/analyzers/repository-map.js +692 -0
  13. package/src/integrations/index.js +7 -1
  14. package/src/integrations/mcp/index.js +175 -0
  15. package/src/integrations/mcp/mcp-context-provider.js +472 -0
  16. package/src/integrations/mcp/mcp-discovery.js +436 -0
  17. package/src/integrations/mcp/mcp-tool-registry.js +467 -0
  18. package/src/integrations/mcp-connector.js +818 -0
  19. package/src/integrations/tool-discovery.js +589 -0
  20. package/src/managers/index.js +7 -0
  21. package/src/managers/skill-tools.js +565 -0
  22. package/src/monitoring/cost-tracker.js +7 -0
  23. package/src/monitoring/incident-manager.js +10 -0
  24. package/src/monitoring/observability.js +10 -0
  25. package/src/monitoring/quality-dashboard.js +491 -0
  26. package/src/monitoring/release-manager.js +10 -0
  27. package/src/orchestration/agent-skill-binding.js +655 -0
  28. package/src/orchestration/error-handler.js +827 -0
  29. package/src/orchestration/index.js +235 -1
  30. package/src/orchestration/mcp-tool-adapters.js +896 -0
  31. package/src/orchestration/reasoning/index.js +58 -0
  32. package/src/orchestration/reasoning/planning-engine.js +831 -0
  33. package/src/orchestration/reasoning/reasoning-engine.js +710 -0
  34. package/src/orchestration/reasoning/self-correction.js +751 -0
  35. package/src/orchestration/skill-executor.js +665 -0
  36. package/src/orchestration/skill-registry.js +650 -0
  37. package/src/orchestration/workflow-examples.js +1072 -0
  38. package/src/orchestration/workflow-executor.js +779 -0
  39. package/src/phase4-integration.js +248 -0
  40. package/src/phase5-integration.js +402 -0
  41. package/src/steering/steering-auto-update.js +572 -0
  42. package/src/steering/steering-validator.js +547 -0
  43. package/src/templates/template-constraints.js +646 -0
  44. package/src/validators/advanced-validation.js +580 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Context Optimizer
3
+ *
4
+ * Optimizes context for LLM consumption by intelligently selecting
5
+ * and prioritizing relevant code and documentation.
6
+ *
7
+ * Part of MUSUBI v5.0.0 - Codebase Intelligence
8
+ *
9
+ * @module analyzers/context-optimizer
10
+ * @version 1.0.0
11
+ *
12
+ * @traceability
13
+ * - Requirement: REQ-P4-003 (Context Optimization)
14
+ * - Design: docs/design/tdd-musubi-v5.0.0.md#2.3
15
+ * - Test: tests/analyzers/context-optimizer.test.js
16
+ */
17
+
18
+ const { EventEmitter } = require('events');
19
+ const { RepositoryMap, createRepositoryMap } = require('./repository-map');
20
+ const { ASTExtractor, createASTExtractor } = require('./ast-extractor');
21
+
22
+ /**
23
+ * @typedef {Object} ContextRequest
24
+ * @property {string} query - User query or intent
25
+ * @property {string[]} [focusFiles] - Files to focus on
26
+ * @property {string[]} [focusSymbols] - Symbols to focus on
27
+ * @property {string} [task] - Task type (implement, debug, review, explain)
28
+ * @property {number} [maxTokens=8000] - Maximum token budget
29
+ * @property {boolean} [includeTests=false] - Include test files
30
+ * @property {boolean} [includeComments=true] - Include comments
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} ContextItem
35
+ * @property {string} type - Item type (file, symbol, import, doc)
36
+ * @property {string} path - File path
37
+ * @property {string} content - Content or summary
38
+ * @property {number} relevance - Relevance score (0-1)
39
+ * @property {number} tokens - Estimated token count
40
+ * @property {Object} metadata - Additional metadata
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} OptimizedContext
45
+ * @property {ContextItem[]} items - Context items in priority order
46
+ * @property {number} totalTokens - Total estimated tokens
47
+ * @property {string} formatted - Formatted context string
48
+ * @property {Object} stats - Context statistics
49
+ */
50
+
51
+ /**
52
+ * Token estimation constants
53
+ */
54
+ const CHARS_PER_TOKEN = 4; // Approximate
55
+ const TOKEN_OVERHEAD = {
56
+ fileHeader: 50,
57
+ symbolHeader: 20,
58
+ separator: 10
59
+ };
60
+
61
+ /**
62
+ * Task-specific weight configurations
63
+ */
64
+ const TASK_WEIGHTS = {
65
+ implement: {
66
+ entryPoints: 0.9,
67
+ relatedFiles: 0.8,
68
+ interfaces: 0.85,
69
+ tests: 0.3,
70
+ docs: 0.5
71
+ },
72
+ debug: {
73
+ errorLocation: 1.0,
74
+ callStack: 0.9,
75
+ relatedFiles: 0.7,
76
+ tests: 0.6,
77
+ docs: 0.4
78
+ },
79
+ review: {
80
+ changedFiles: 1.0,
81
+ relatedFiles: 0.7,
82
+ interfaces: 0.6,
83
+ tests: 0.8,
84
+ docs: 0.5
85
+ },
86
+ explain: {
87
+ targetFile: 1.0,
88
+ imports: 0.8,
89
+ relatedFiles: 0.6,
90
+ tests: 0.4,
91
+ docs: 0.7
92
+ },
93
+ refactor: {
94
+ targetFile: 1.0,
95
+ usages: 0.9,
96
+ interfaces: 0.8,
97
+ tests: 0.7,
98
+ docs: 0.5
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Context Optimizer class
104
+ * @extends EventEmitter
105
+ */
106
+ class ContextOptimizer extends EventEmitter {
107
+ /**
108
+ * Create context optimizer
109
+ * @param {Object} options - Configuration options
110
+ * @param {string} options.rootPath - Repository root path
111
+ * @param {number} [options.maxTokens=8000] - Default max tokens
112
+ * @param {number} [options.maxFiles=20] - Max files in context
113
+ * @param {boolean} [options.useAST=true] - Use AST for analysis
114
+ * @param {boolean} [options.cache=true] - Enable caching
115
+ */
116
+ constructor(options = {}) {
117
+ super();
118
+ this.rootPath = options.rootPath || process.cwd();
119
+ this.maxTokens = options.maxTokens ?? 8000;
120
+ this.maxFiles = options.maxFiles ?? 20;
121
+ this.useAST = options.useAST ?? true;
122
+ this.cacheEnabled = options.cache ?? true;
123
+
124
+ // Components
125
+ this.repoMap = null;
126
+ this.astExtractor = null;
127
+
128
+ // Caches
129
+ this.relevanceCache = new Map();
130
+ this.astCache = new Map();
131
+
132
+ // State
133
+ this.initialized = false;
134
+ }
135
+
136
+ /**
137
+ * Initialize optimizer with repository analysis
138
+ * @returns {Promise<void>}
139
+ */
140
+ async initialize() {
141
+ if (this.initialized) return;
142
+
143
+ this.emit('init:start');
144
+
145
+ // Create repository map
146
+ this.repoMap = createRepositoryMap({ rootPath: this.rootPath });
147
+ await this.repoMap.generate();
148
+
149
+ // Create AST extractor
150
+ if (this.useAST) {
151
+ this.astExtractor = createASTExtractor();
152
+ }
153
+
154
+ this.initialized = true;
155
+ this.emit('init:complete', {
156
+ files: this.repoMap.stats.totalFiles,
157
+ entryPoints: this.repoMap.entryPoints.length
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Optimize context for a query
163
+ * @param {ContextRequest} request - Context request
164
+ * @returns {Promise<OptimizedContext>}
165
+ */
166
+ async optimize(request) {
167
+ if (!this.initialized) {
168
+ await this.initialize();
169
+ }
170
+
171
+ this.emit('optimize:start', request);
172
+
173
+ const maxTokens = request.maxTokens ?? this.maxTokens;
174
+ const task = request.task || 'implement';
175
+ const weights = TASK_WEIGHTS[task] || TASK_WEIGHTS.implement;
176
+
177
+ // Step 1: Collect candidate files
178
+ const candidates = await this.collectCandidates(request);
179
+
180
+ // Step 2: Score candidates by relevance
181
+ const scored = await this.scoreRelevance(candidates, request, weights);
182
+
183
+ // Step 3: Sort by relevance
184
+ scored.sort((a, b) => b.relevance - a.relevance);
185
+
186
+ // Step 4: Select items within token budget
187
+ const selected = this.selectWithinBudget(scored, maxTokens);
188
+
189
+ // Step 5: Build formatted context
190
+ const formatted = this.formatContext(selected, request);
191
+
192
+ const result = {
193
+ items: selected,
194
+ totalTokens: selected.reduce((sum, item) => sum + item.tokens, 0),
195
+ formatted,
196
+ stats: {
197
+ candidateCount: candidates.length,
198
+ selectedCount: selected.length,
199
+ tokenBudget: maxTokens,
200
+ tokensUsed: selected.reduce((sum, item) => sum + item.tokens, 0)
201
+ }
202
+ };
203
+
204
+ this.emit('optimize:complete', result.stats);
205
+ return result;
206
+ }
207
+
208
+ /**
209
+ * Collect candidate files for context
210
+ * @param {ContextRequest} request - Context request
211
+ * @returns {Promise<ContextItem[]>}
212
+ * @private
213
+ */
214
+ async collectCandidates(request) {
215
+ const candidates = [];
216
+
217
+ // Add focus files with high priority
218
+ if (request.focusFiles?.length > 0) {
219
+ for (const pattern of request.focusFiles) {
220
+ const matches = this.repoMap.searchFiles(pattern);
221
+ for (const file of matches) {
222
+ candidates.push({
223
+ type: 'file',
224
+ path: file.path,
225
+ content: '',
226
+ relevance: 1.0,
227
+ tokens: this.estimateTokens(file.size),
228
+ metadata: { source: 'focus', file }
229
+ });
230
+ }
231
+ }
232
+ }
233
+
234
+ // Add entry points
235
+ for (const entry of this.repoMap.entryPoints.slice(0, 5)) {
236
+ const file = this.repoMap.files.find(f => f.path === entry);
237
+ if (file) {
238
+ candidates.push({
239
+ type: 'file',
240
+ path: file.path,
241
+ content: '',
242
+ relevance: 0.8,
243
+ tokens: this.estimateTokens(file.size),
244
+ metadata: { source: 'entryPoint', file }
245
+ });
246
+ }
247
+ }
248
+
249
+ // Add files matching query keywords
250
+ if (request.query) {
251
+ const keywords = this.extractKeywords(request.query);
252
+ for (const keyword of keywords) {
253
+ const matches = this.repoMap.searchFiles(keyword);
254
+ for (const file of matches.slice(0, 5)) {
255
+ if (!candidates.find(c => c.path === file.path)) {
256
+ candidates.push({
257
+ type: 'file',
258
+ path: file.path,
259
+ content: '',
260
+ relevance: 0.6,
261
+ tokens: this.estimateTokens(file.size),
262
+ metadata: { source: 'keyword', keyword, file }
263
+ });
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ // Add related files based on imports (if AST enabled)
270
+ if (this.useAST && candidates.length > 0) {
271
+ const imports = await this.collectImports(candidates.slice(0, 5));
272
+ for (const imp of imports.slice(0, 10)) {
273
+ if (!candidates.find(c => c.path === imp.path)) {
274
+ candidates.push({
275
+ type: 'file',
276
+ path: imp.path,
277
+ content: '',
278
+ relevance: 0.5,
279
+ tokens: this.estimateTokens(imp.size),
280
+ metadata: { source: 'import', file: imp }
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ // Add test files if requested
287
+ if (request.includeTests) {
288
+ const testFiles = this.repoMap.files.filter(f =>
289
+ f.path.includes('test') || f.path.includes('spec')
290
+ );
291
+ for (const file of testFiles.slice(0, 5)) {
292
+ if (!candidates.find(c => c.path === file.path)) {
293
+ candidates.push({
294
+ type: 'file',
295
+ path: file.path,
296
+ content: '',
297
+ relevance: 0.4,
298
+ tokens: this.estimateTokens(file.size),
299
+ metadata: { source: 'test', file }
300
+ });
301
+ }
302
+ }
303
+ }
304
+
305
+ return candidates;
306
+ }
307
+
308
+ /**
309
+ * Collect imports from candidate files
310
+ * @param {ContextItem[]} candidates - Candidate items
311
+ * @returns {Promise<Object[]>}
312
+ * @private
313
+ */
314
+ async collectImports(candidates) {
315
+ const imports = [];
316
+ const path = require('path');
317
+
318
+ for (const candidate of candidates) {
319
+ if (candidate.metadata.file?.language === 'unknown') continue;
320
+
321
+ try {
322
+ const filePath = path.join(this.rootPath, candidate.path);
323
+ const ast = await this.astExtractor.extractFromFile(filePath);
324
+
325
+ for (const imp of ast.imports) {
326
+ // Resolve relative imports
327
+ if (imp.source.startsWith('.')) {
328
+ const dir = path.dirname(candidate.path);
329
+ let resolved = path.join(dir, imp.source);
330
+
331
+ // Try common extensions
332
+ for (const ext of ['.js', '.ts', '.jsx', '.tsx', '/index.js', '/index.ts']) {
333
+ const withExt = resolved + ext;
334
+ const file = this.repoMap.files.find(f =>
335
+ f.path === withExt || f.path === resolved.replace(/\\/g, '/')
336
+ );
337
+ if (file) {
338
+ imports.push(file);
339
+ break;
340
+ }
341
+ }
342
+ }
343
+ }
344
+ } catch {
345
+ // Skip files that can't be parsed
346
+ }
347
+ }
348
+
349
+ return imports;
350
+ }
351
+
352
+ /**
353
+ * Score relevance of candidates
354
+ * @param {ContextItem[]} candidates - Candidate items
355
+ * @param {ContextRequest} request - Context request
356
+ * @param {Object} weights - Task weights
357
+ * @returns {Promise<ContextItem[]>}
358
+ * @private
359
+ */
360
+ async scoreRelevance(candidates, request, weights) {
361
+ for (const candidate of candidates) {
362
+ let score = candidate.relevance;
363
+
364
+ // Adjust by source
365
+ switch (candidate.metadata.source) {
366
+ case 'focus':
367
+ score *= 1.0;
368
+ break;
369
+ case 'entryPoint':
370
+ score *= weights.entryPoints;
371
+ break;
372
+ case 'keyword':
373
+ score *= weights.relatedFiles;
374
+ break;
375
+ case 'import':
376
+ score *= weights.relatedFiles * 0.8;
377
+ break;
378
+ case 'test':
379
+ score *= weights.tests;
380
+ break;
381
+ }
382
+
383
+ // Boost for focus symbols if present
384
+ if (request.focusSymbols?.length > 0 && this.useAST) {
385
+ try {
386
+ const path = require('path');
387
+ const filePath = path.join(this.rootPath, candidate.path);
388
+ const ast = await this.getOrExtractAST(filePath);
389
+
390
+ const hasSymbol = ast.symbols.some(s =>
391
+ request.focusSymbols.some(fs =>
392
+ s.name.toLowerCase().includes(fs.toLowerCase())
393
+ )
394
+ );
395
+
396
+ if (hasSymbol) {
397
+ score *= 1.5;
398
+ }
399
+ } catch {
400
+ // Skip
401
+ }
402
+ }
403
+
404
+ // Penalize very large files
405
+ if (candidate.tokens > 2000) {
406
+ score *= 0.7;
407
+ }
408
+
409
+ // Boost for exports (more important modules)
410
+ if (candidate.metadata.file?.exports?.length > 3) {
411
+ score *= 1.2;
412
+ }
413
+
414
+ candidate.relevance = Math.min(score, 1.0);
415
+ }
416
+
417
+ return candidates;
418
+ }
419
+
420
+ /**
421
+ * Get or extract AST with caching
422
+ * @param {string} filePath - File path
423
+ * @returns {Promise<Object>}
424
+ * @private
425
+ */
426
+ async getOrExtractAST(filePath) {
427
+ if (this.cacheEnabled && this.astCache.has(filePath)) {
428
+ return this.astCache.get(filePath);
429
+ }
430
+
431
+ const ast = await this.astExtractor.extractFromFile(filePath);
432
+
433
+ if (this.cacheEnabled) {
434
+ this.astCache.set(filePath, ast);
435
+ }
436
+
437
+ return ast;
438
+ }
439
+
440
+ /**
441
+ * Select items within token budget
442
+ * @param {ContextItem[]} scored - Scored items
443
+ * @param {number} maxTokens - Maximum tokens
444
+ * @returns {ContextItem[]}
445
+ * @private
446
+ */
447
+ selectWithinBudget(scored, maxTokens) {
448
+ const selected = [];
449
+ let tokensUsed = 0;
450
+
451
+ for (const item of scored) {
452
+ const itemTokens = item.tokens + TOKEN_OVERHEAD.fileHeader;
453
+
454
+ if (tokensUsed + itemTokens <= maxTokens) {
455
+ selected.push(item);
456
+ tokensUsed += itemTokens;
457
+ }
458
+
459
+ if (selected.length >= this.maxFiles) {
460
+ break;
461
+ }
462
+ }
463
+
464
+ return selected;
465
+ }
466
+
467
+ /**
468
+ * Format context for LLM consumption
469
+ * @param {ContextItem[]} items - Selected items
470
+ * @param {ContextRequest} request - Original request
471
+ * @returns {string}
472
+ * @private
473
+ */
474
+ formatContext(items, request) {
475
+ let context = `# Optimized Context\n\n`;
476
+ context += `Task: ${request.task || 'implementation'}\n`;
477
+ context += `Query: ${request.query || 'N/A'}\n`;
478
+ context += `Files: ${items.length}\n\n`;
479
+
480
+ context += `---\n\n`;
481
+
482
+ for (const item of items) {
483
+ context += `## ${item.path}\n\n`;
484
+ context += `- Type: ${item.type}\n`;
485
+ context += `- Relevance: ${(item.relevance * 100).toFixed(0)}%\n`;
486
+ context += `- Source: ${item.metadata.source}\n`;
487
+
488
+ if (item.metadata.file?.exports?.length > 0) {
489
+ context += `- Exports: ${item.metadata.file.exports.slice(0, 5).join(', ')}`;
490
+ if (item.metadata.file.exports.length > 5) {
491
+ context += ` (+${item.metadata.file.exports.length - 5} more)`;
492
+ }
493
+ context += '\n';
494
+ }
495
+
496
+ context += '\n';
497
+ }
498
+
499
+ // Add repository overview
500
+ if (this.repoMap) {
501
+ context += `---\n\n`;
502
+ context += `## Repository Overview\n\n`;
503
+ context += `- Total Files: ${this.repoMap.stats.totalFiles}\n`;
504
+ context += `- Languages: ${Object.keys(this.repoMap.stats.byLanguage).slice(0, 5).join(', ')}\n`;
505
+ context += `- Entry Points: ${this.repoMap.entryPoints.slice(0, 3).join(', ')}\n`;
506
+ }
507
+
508
+ return context;
509
+ }
510
+
511
+ /**
512
+ * Extract keywords from query
513
+ * @param {string} query - User query
514
+ * @returns {string[]}
515
+ * @private
516
+ */
517
+ extractKeywords(query) {
518
+ // Remove common words and extract meaningful terms
519
+ const stopWords = new Set([
520
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
521
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
522
+ 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
523
+ 'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by',
524
+ 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above',
525
+ 'below', 'between', 'under', 'again', 'further', 'then', 'once', 'here',
526
+ 'there', 'when', 'where', 'why', 'how', 'all', 'each', 'every', 'both',
527
+ 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not',
528
+ 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'and',
529
+ 'but', 'if', 'or', 'because', 'until', 'while', 'although', 'though',
530
+ 'this', 'that', 'these', 'those', 'what', 'which', 'who', 'whom',
531
+ 'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you',
532
+ 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself',
533
+ 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them',
534
+ 'their', 'theirs', 'themselves', 'create', 'add', 'fix', 'implement',
535
+ 'change', 'update', 'modify', 'file', 'code', 'function', 'class'
536
+ ]);
537
+
538
+ const words = query
539
+ .toLowerCase()
540
+ .replace(/[^a-z0-9\s-_]/g, ' ')
541
+ .split(/\s+/)
542
+ .filter(w => w.length > 2 && !stopWords.has(w));
543
+
544
+ // Also extract CamelCase and snake_case identifiers
545
+ const identifiers = query.match(/[A-Z][a-z]+|[a-z]+_[a-z]+/g) || [];
546
+
547
+ return [...new Set([...words, ...identifiers.map(i => i.toLowerCase())])];
548
+ }
549
+
550
+ /**
551
+ * Estimate tokens from bytes
552
+ * @param {number} bytes - File size in bytes
553
+ * @returns {number}
554
+ * @private
555
+ */
556
+ estimateTokens(bytes) {
557
+ return Math.ceil(bytes / CHARS_PER_TOKEN);
558
+ }
559
+
560
+ /**
561
+ * Build focused context for specific files
562
+ * @param {string[]} filePaths - File paths to include
563
+ * @param {Object} options - Options
564
+ * @returns {Promise<string>}
565
+ */
566
+ async buildFocusedContext(filePaths, options = {}) {
567
+ const fs = require('fs');
568
+ const path = require('path');
569
+ const { maxTokens = 4000, includeAST = true } = options;
570
+
571
+ let context = '';
572
+ let tokensUsed = 0;
573
+
574
+ for (const filePath of filePaths) {
575
+ const absPath = path.isAbsolute(filePath)
576
+ ? filePath
577
+ : path.join(this.rootPath, filePath);
578
+
579
+ try {
580
+ const content = await fs.promises.readFile(absPath, 'utf-8');
581
+ const tokens = this.estimateTokens(content.length);
582
+
583
+ if (tokensUsed + tokens > maxTokens) {
584
+ // Truncate to fit
585
+ const remaining = maxTokens - tokensUsed;
586
+ const chars = remaining * CHARS_PER_TOKEN;
587
+ context += `\n## ${filePath} (truncated)\n\n\`\`\`\n`;
588
+ context += content.slice(0, chars);
589
+ context += '\n...(truncated)\n\`\`\`\n';
590
+ break;
591
+ }
592
+
593
+ context += `\n## ${filePath}\n\n`;
594
+
595
+ // Add AST summary if enabled
596
+ if (includeAST && this.useAST) {
597
+ try {
598
+ const ast = await this.getOrExtractAST(absPath);
599
+ if (ast.symbols.length > 0) {
600
+ context += '**Symbols:**\n';
601
+ for (const sym of ast.symbols.slice(0, 10)) {
602
+ context += `- ${sym.type}: ${sym.name}\n`;
603
+ }
604
+ context += '\n';
605
+ }
606
+ } catch {
607
+ // Skip AST
608
+ }
609
+ }
610
+
611
+ context += '```\n' + content + '\n```\n';
612
+ tokensUsed += tokens;
613
+
614
+ } catch (error) {
615
+ context += `\n## ${filePath}\n\n*Error reading file: ${error.message}*\n`;
616
+ }
617
+ }
618
+
619
+ return context;
620
+ }
621
+
622
+ /**
623
+ * Get optimization statistics
624
+ * @returns {Object}
625
+ */
626
+ getStats() {
627
+ return {
628
+ initialized: this.initialized,
629
+ repoFiles: this.repoMap?.stats?.totalFiles || 0,
630
+ repoEntryPoints: this.repoMap?.entryPoints?.length || 0,
631
+ astCacheSize: this.astCache.size,
632
+ relevanceCacheSize: this.relevanceCache.size
633
+ };
634
+ }
635
+
636
+ /**
637
+ * Clear all caches
638
+ */
639
+ clearCaches() {
640
+ this.astCache.clear();
641
+ this.relevanceCache.clear();
642
+ }
643
+
644
+ /**
645
+ * Reset optimizer state
646
+ */
647
+ reset() {
648
+ this.clearCaches();
649
+ this.repoMap = null;
650
+ this.astExtractor = null;
651
+ this.initialized = false;
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Create context optimizer
657
+ * @param {Object} options - Options
658
+ * @returns {ContextOptimizer}
659
+ */
660
+ function createContextOptimizer(options = {}) {
661
+ return new ContextOptimizer(options);
662
+ }
663
+
664
+ /**
665
+ * Optimize context for query
666
+ * @param {string} rootPath - Repository root
667
+ * @param {ContextRequest} request - Context request
668
+ * @returns {Promise<OptimizedContext>}
669
+ */
670
+ async function optimizeContext(rootPath, request) {
671
+ const optimizer = createContextOptimizer({ rootPath });
672
+ return optimizer.optimize(request);
673
+ }
674
+
675
+ module.exports = {
676
+ ContextOptimizer,
677
+ createContextOptimizer,
678
+ optimizeContext,
679
+ TASK_WEIGHTS,
680
+ CHARS_PER_TOKEN
681
+ };