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.
- package/README.md +24 -19
- package/package.json +1 -1
- package/src/agents/agent-loop.js +532 -0
- package/src/agents/agentic/code-generator.js +767 -0
- package/src/agents/agentic/code-reviewer.js +698 -0
- package/src/agents/agentic/index.js +43 -0
- package/src/agents/function-tool.js +432 -0
- package/src/agents/index.js +45 -0
- package/src/agents/schema-generator.js +514 -0
- package/src/analyzers/ast-extractor.js +870 -0
- package/src/analyzers/context-optimizer.js +681 -0
- package/src/analyzers/repository-map.js +692 -0
- package/src/integrations/index.js +7 -1
- package/src/integrations/mcp/index.js +175 -0
- package/src/integrations/mcp/mcp-context-provider.js +472 -0
- package/src/integrations/mcp/mcp-discovery.js +436 -0
- package/src/integrations/mcp/mcp-tool-registry.js +467 -0
- package/src/integrations/mcp-connector.js +818 -0
- package/src/integrations/tool-discovery.js +589 -0
- package/src/managers/index.js +7 -0
- package/src/managers/skill-tools.js +565 -0
- package/src/monitoring/cost-tracker.js +7 -0
- package/src/monitoring/incident-manager.js +10 -0
- package/src/monitoring/observability.js +10 -0
- package/src/monitoring/quality-dashboard.js +491 -0
- package/src/monitoring/release-manager.js +10 -0
- package/src/orchestration/agent-skill-binding.js +655 -0
- package/src/orchestration/error-handler.js +827 -0
- package/src/orchestration/index.js +235 -1
- package/src/orchestration/mcp-tool-adapters.js +896 -0
- package/src/orchestration/reasoning/index.js +58 -0
- package/src/orchestration/reasoning/planning-engine.js +831 -0
- package/src/orchestration/reasoning/reasoning-engine.js +710 -0
- package/src/orchestration/reasoning/self-correction.js +751 -0
- package/src/orchestration/skill-executor.js +665 -0
- package/src/orchestration/skill-registry.js +650 -0
- package/src/orchestration/workflow-examples.js +1072 -0
- package/src/orchestration/workflow-executor.js +779 -0
- package/src/phase4-integration.js +248 -0
- package/src/phase5-integration.js +402 -0
- package/src/steering/steering-auto-update.js +572 -0
- package/src/steering/steering-validator.js +547 -0
- package/src/templates/template-constraints.js +646 -0
- 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
|
+
};
|