minovative-mind-cli 2.14.1 → 2.14.3
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 +40 -59
- package/dist/services/agent/slashCommands.js +144 -13
- package/dist/services/agent/toolLoop.js +11 -2
- package/dist/services/agent/types.d.ts +9 -2
- package/dist/services/agent-tools.d.ts +20 -1
- package/dist/services/agent-tools.js +272 -47
- package/dist/services/agent.js +43 -18
- package/dist/services/ai.d.ts +9 -0
- package/dist/services/ai.js +71 -21
- package/dist/services/chatHistoryService.d.ts +5 -0
- package/dist/services/contextAgent.d.ts +14 -6
- package/dist/services/contextAgent.js +36 -10
- package/dist/services/orchestration/investigationAgent.js +31 -7
- package/dist/services/orchestration/investigationCache.js +19 -9
- package/dist/services/orchestration/readCache.d.ts +1 -0
- package/dist/services/orchestration/readCache.js +5 -2
- package/dist/services/orchestration/scopedTools.js +37 -10
- package/dist/services/orchestration/subAgent.js +16 -2
- package/dist/services/proxyClient.d.ts +6 -0
- package/dist/services/proxyClient.js +24 -10
- package/dist/services/sessionSettings.d.ts +66 -0
- package/dist/services/sessionSettings.js +126 -0
- package/dist/services/userProfileService.d.ts +14 -0
- package/dist/services/userProfileService.js +105 -3
- package/dist/services/verificationService.js +24 -2
- package/dist/utils/analysisRunner.d.ts +120 -8
- package/dist/utils/analysisRunner.js +946 -125
- package/dist/utils/antiCheatingGuard.d.ts +21 -0
- package/dist/utils/antiCheatingGuard.js +554 -0
- package/dist/utils/contextPrompts.d.ts +39 -0
- package/dist/utils/contextPrompts.js +81 -9
- package/dist/utils/contextRanker.d.ts +216 -0
- package/dist/utils/contextRanker.js +603 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +4 -1
- package/dist/utils/dependencyTracer/modules/graph.js +11 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +10 -0
- package/dist/utils/dependencyTracer.d.ts +40 -2
- package/dist/utils/dependencyTracer.js +95 -3
- package/dist/utils/fileReadCache.d.ts +58 -0
- package/dist/utils/fileReadCache.js +162 -0
- package/dist/utils/projectStorage.js +2 -1
- package/dist/utils/symbolExtractor.d.ts +12 -0
- package/dist/utils/symbolExtractor.js +111 -15
- package/dist/utils/systemPrompts.d.ts +4 -3
- package/dist/utils/systemPrompts.js +66 -8
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/utils/contextRanker.ts
|
|
3
|
+
* @description Classical BM25 / TF-IDF information retrieval engine with dependency graph
|
|
4
|
+
* centrality scoring and deterministic 3-tier token budgeting.
|
|
5
|
+
*
|
|
6
|
+
* Partitions candidate workspace files into:
|
|
7
|
+
* - Tier 1: Full raw source code for high-relevance edit targets and primary files.
|
|
8
|
+
* - Tier 2: Compact AST declaration skeletons for peripheral hubs and dependent modules.
|
|
9
|
+
* - Tier 3: Omitted overflow files to strictly respect token budget boundaries.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { extractDeclarationsOutline } from './symbolExtractor.js';
|
|
13
|
+
import { CONTEXT_BUDGET_CONFIG } from './contextPrompts.js';
|
|
14
|
+
// ─── Stop Words ──────────────────────────────────────────────────────
|
|
15
|
+
const COMMON_STOP_WORDS = new Set([
|
|
16
|
+
'a',
|
|
17
|
+
'about',
|
|
18
|
+
'above',
|
|
19
|
+
'after',
|
|
20
|
+
'again',
|
|
21
|
+
'against',
|
|
22
|
+
'all',
|
|
23
|
+
'am',
|
|
24
|
+
'an',
|
|
25
|
+
'and',
|
|
26
|
+
'any',
|
|
27
|
+
'are',
|
|
28
|
+
'as',
|
|
29
|
+
'at',
|
|
30
|
+
'be',
|
|
31
|
+
'because',
|
|
32
|
+
'been',
|
|
33
|
+
'before',
|
|
34
|
+
'being',
|
|
35
|
+
'below',
|
|
36
|
+
'between',
|
|
37
|
+
'both',
|
|
38
|
+
'but',
|
|
39
|
+
'by',
|
|
40
|
+
'can',
|
|
41
|
+
'could',
|
|
42
|
+
'did',
|
|
43
|
+
'do',
|
|
44
|
+
'does',
|
|
45
|
+
'doing',
|
|
46
|
+
'down',
|
|
47
|
+
'during',
|
|
48
|
+
'each',
|
|
49
|
+
'few',
|
|
50
|
+
'for',
|
|
51
|
+
'from',
|
|
52
|
+
'further',
|
|
53
|
+
'had',
|
|
54
|
+
'has',
|
|
55
|
+
'have',
|
|
56
|
+
'having',
|
|
57
|
+
'he',
|
|
58
|
+
'her',
|
|
59
|
+
'here',
|
|
60
|
+
'hers',
|
|
61
|
+
'herself',
|
|
62
|
+
'him',
|
|
63
|
+
'himself',
|
|
64
|
+
'his',
|
|
65
|
+
'how',
|
|
66
|
+
'i',
|
|
67
|
+
'if',
|
|
68
|
+
'in',
|
|
69
|
+
'into',
|
|
70
|
+
'is',
|
|
71
|
+
'it',
|
|
72
|
+
'its',
|
|
73
|
+
'itself',
|
|
74
|
+
'just',
|
|
75
|
+
'me',
|
|
76
|
+
'more',
|
|
77
|
+
'most',
|
|
78
|
+
'my',
|
|
79
|
+
'myself',
|
|
80
|
+
'no',
|
|
81
|
+
'nor',
|
|
82
|
+
'not',
|
|
83
|
+
'now',
|
|
84
|
+
'of',
|
|
85
|
+
'off',
|
|
86
|
+
'on',
|
|
87
|
+
'once',
|
|
88
|
+
'only',
|
|
89
|
+
'or',
|
|
90
|
+
'other',
|
|
91
|
+
'our',
|
|
92
|
+
'ours',
|
|
93
|
+
'ourselves',
|
|
94
|
+
'out',
|
|
95
|
+
'over',
|
|
96
|
+
'own',
|
|
97
|
+
'same',
|
|
98
|
+
'should',
|
|
99
|
+
'so',
|
|
100
|
+
'some',
|
|
101
|
+
'such',
|
|
102
|
+
'than',
|
|
103
|
+
'that',
|
|
104
|
+
'the',
|
|
105
|
+
'their',
|
|
106
|
+
'theirs',
|
|
107
|
+
'them',
|
|
108
|
+
'themselves',
|
|
109
|
+
'then',
|
|
110
|
+
'there',
|
|
111
|
+
'these',
|
|
112
|
+
'they',
|
|
113
|
+
'this',
|
|
114
|
+
'those',
|
|
115
|
+
'through',
|
|
116
|
+
'to',
|
|
117
|
+
'too',
|
|
118
|
+
'under',
|
|
119
|
+
'until',
|
|
120
|
+
'up',
|
|
121
|
+
'very',
|
|
122
|
+
'was',
|
|
123
|
+
'we',
|
|
124
|
+
'were',
|
|
125
|
+
'what',
|
|
126
|
+
'when',
|
|
127
|
+
'where',
|
|
128
|
+
'which',
|
|
129
|
+
'while',
|
|
130
|
+
'who',
|
|
131
|
+
'whom',
|
|
132
|
+
'why',
|
|
133
|
+
'will',
|
|
134
|
+
'with',
|
|
135
|
+
'would',
|
|
136
|
+
'you',
|
|
137
|
+
'your',
|
|
138
|
+
'yours',
|
|
139
|
+
'yourself',
|
|
140
|
+
'yourselves',
|
|
141
|
+
]);
|
|
142
|
+
// ─── Tokenizer & Query Preprocessor ──────────────────────────────────
|
|
143
|
+
/**
|
|
144
|
+
* Tokenizes code and text into normalized keywords.
|
|
145
|
+
* Handles camelCase, PascalCase, snake_case, kebab-case, and dot/slash delimiters.
|
|
146
|
+
*
|
|
147
|
+
* @param text The input string to tokenize
|
|
148
|
+
* @param filterStopWords Whether to filter common English stop words (default: true)
|
|
149
|
+
* @returns Array of lowercase alphanumeric tokens
|
|
150
|
+
*/
|
|
151
|
+
export function tokenize(text, filterStopWords = true) {
|
|
152
|
+
if (!text)
|
|
153
|
+
return [];
|
|
154
|
+
// Split on camelCase boundaries (e.g. "contextRanker" -> "context Ranker")
|
|
155
|
+
const splitCamel = text.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
156
|
+
// Split by non-alphanumeric characters (punctuation, symbols, whitespace)
|
|
157
|
+
const rawTokens = splitCamel.toLowerCase().split(/[^a-z0-9_]+/);
|
|
158
|
+
const tokens = [];
|
|
159
|
+
for (const raw of rawTokens) {
|
|
160
|
+
// Also split any remaining snake_case tokens
|
|
161
|
+
const subTokens = raw.split('_');
|
|
162
|
+
for (const sub of subTokens) {
|
|
163
|
+
const trimmed = sub.trim();
|
|
164
|
+
if (trimmed.length < 2)
|
|
165
|
+
continue;
|
|
166
|
+
if (filterStopWords && COMMON_STOP_WORDS.has(trimmed))
|
|
167
|
+
continue;
|
|
168
|
+
tokens.push(trimmed);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return tokens;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Extracts unique search terms from a user prompt or query.
|
|
175
|
+
*/
|
|
176
|
+
export function extractQueryTerms(prompt) {
|
|
177
|
+
const tokens = tokenize(prompt, true);
|
|
178
|
+
return Array.from(new Set(tokens));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* In-memory BM25 index and scoring implementation.
|
|
182
|
+
* Delivers deterministic <5ms search ranking across candidate files.
|
|
183
|
+
*/
|
|
184
|
+
export class BM25Engine {
|
|
185
|
+
avgDocLength = 0;
|
|
186
|
+
b;
|
|
187
|
+
docFrequency = new Map();
|
|
188
|
+
documents = [];
|
|
189
|
+
k1;
|
|
190
|
+
totalDocs = 0;
|
|
191
|
+
constructor(options) {
|
|
192
|
+
this.k1 = options?.k1 ?? 1.2;
|
|
193
|
+
this.b = options?.b ?? 0.75;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Adds a document to the BM25 index.
|
|
197
|
+
* Path tokens can be repeated to apply a structural path boost.
|
|
198
|
+
*/
|
|
199
|
+
addDocument(id, content, filePath, pathBoostFactor = 3) {
|
|
200
|
+
const contentTokens = tokenize(content, true);
|
|
201
|
+
const pathTokens = tokenize(filePath, true);
|
|
202
|
+
// Apply path boost factor by repeating path tokens
|
|
203
|
+
const boostedTokens = [...contentTokens];
|
|
204
|
+
for (let i = 0; i < pathBoostFactor; i++) {
|
|
205
|
+
boostedTokens.push(...pathTokens);
|
|
206
|
+
}
|
|
207
|
+
const termFreqs = new Map();
|
|
208
|
+
const uniqueTermsInDoc = new Set();
|
|
209
|
+
for (const token of boostedTokens) {
|
|
210
|
+
termFreqs.set(token, (termFreqs.get(token) || 0) + 1);
|
|
211
|
+
uniqueTermsInDoc.add(token);
|
|
212
|
+
}
|
|
213
|
+
for (const term of uniqueTermsInDoc) {
|
|
214
|
+
this.docFrequency.set(term, (this.docFrequency.get(term) || 0) + 1);
|
|
215
|
+
}
|
|
216
|
+
this.documents.push({
|
|
217
|
+
id,
|
|
218
|
+
tokens: boostedTokens,
|
|
219
|
+
termFreqs,
|
|
220
|
+
length: boostedTokens.length,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Finalizes document statistics (average document length).
|
|
225
|
+
*/
|
|
226
|
+
build() {
|
|
227
|
+
this.totalDocs = this.documents.length;
|
|
228
|
+
if (this.totalDocs === 0) {
|
|
229
|
+
this.avgDocLength = 0;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const totalLength = this.documents.reduce((acc, doc) => acc + doc.length, 0);
|
|
233
|
+
this.avgDocLength = totalLength / this.totalDocs;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Scores all indexed documents against query terms using standard BM25 formula with Robertson-Spärck Jones IDF.
|
|
237
|
+
*
|
|
238
|
+
* @param queryTerms Array of query terms
|
|
239
|
+
* @returns Map of document ID to raw BM25 score
|
|
240
|
+
*/
|
|
241
|
+
score(queryTerms) {
|
|
242
|
+
const scores = new Map();
|
|
243
|
+
if (this.totalDocs === 0 || queryTerms.length === 0) {
|
|
244
|
+
for (const doc of this.documents) {
|
|
245
|
+
scores.set(doc.id, 0);
|
|
246
|
+
}
|
|
247
|
+
return scores;
|
|
248
|
+
}
|
|
249
|
+
for (const doc of this.documents) {
|
|
250
|
+
let docScore = 0;
|
|
251
|
+
for (const term of queryTerms) {
|
|
252
|
+
const tf = doc.termFreqs.get(term) || 0;
|
|
253
|
+
if (tf === 0)
|
|
254
|
+
continue;
|
|
255
|
+
const df = this.docFrequency.get(term) || 0;
|
|
256
|
+
// Smoothed BM25 IDF: ln((N - n + 0.5) / (n + 0.5) + 1)
|
|
257
|
+
const idf = Math.log((this.totalDocs - df + 0.5) / (df + 0.5) + 1);
|
|
258
|
+
// BM25 term frequency saturation
|
|
259
|
+
const numerator = tf * (this.k1 + 1);
|
|
260
|
+
const denominator = this.avgDocLength > 0
|
|
261
|
+
? tf + this.k1 * (1 - this.b + this.b * (doc.length / this.avgDocLength))
|
|
262
|
+
: tf + this.k1;
|
|
263
|
+
docScore += idf * (numerator / denominator);
|
|
264
|
+
}
|
|
265
|
+
scores.set(doc.id, docScore);
|
|
266
|
+
}
|
|
267
|
+
return scores;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Computes BM25 scores for a collection of file candidates.
|
|
272
|
+
*
|
|
273
|
+
* @param candidates List of file candidates
|
|
274
|
+
* @param queryTerms Query terms from prompt
|
|
275
|
+
* @param options BM25 parameters
|
|
276
|
+
* @returns Map of filePath to normalized BM25 score in [0.0, 1.0]
|
|
277
|
+
*/
|
|
278
|
+
export function computeBM25Scores(candidates, queryTerms, options) {
|
|
279
|
+
const engine = new BM25Engine(options);
|
|
280
|
+
for (const candidate of candidates) {
|
|
281
|
+
engine.addDocument(candidate.filePath, candidate.content, candidate.filePath);
|
|
282
|
+
}
|
|
283
|
+
engine.build();
|
|
284
|
+
const rawScores = engine.score(queryTerms);
|
|
285
|
+
let maxScore = 0;
|
|
286
|
+
for (const score of rawScores.values()) {
|
|
287
|
+
if (score > maxScore)
|
|
288
|
+
maxScore = score;
|
|
289
|
+
}
|
|
290
|
+
const result = new Map();
|
|
291
|
+
for (const [filePath, raw] of rawScores.entries()) {
|
|
292
|
+
const normalized = maxScore > 0 ? raw / maxScore : 0;
|
|
293
|
+
result.set(filePath, { rawScore: raw, normalizedScore: normalized });
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Computes TF-IDF scores for a collection of candidates as a lightweight alternative.
|
|
299
|
+
*/
|
|
300
|
+
export function computeTfIdfScores(candidates, queryTerms) {
|
|
301
|
+
const totalDocs = candidates.length;
|
|
302
|
+
const docTokensMap = new Map();
|
|
303
|
+
const docFreq = new Map();
|
|
304
|
+
for (const candidate of candidates) {
|
|
305
|
+
const tokens = tokenize(`${candidate.filePath} ${candidate.content}`, true);
|
|
306
|
+
docTokensMap.set(candidate.filePath, tokens);
|
|
307
|
+
const unique = new Set(tokens);
|
|
308
|
+
for (const term of unique) {
|
|
309
|
+
docFreq.set(term, (docFreq.get(term) || 0) + 1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const rawScores = new Map();
|
|
313
|
+
let maxScore = 0;
|
|
314
|
+
for (const candidate of candidates) {
|
|
315
|
+
const tokens = docTokensMap.get(candidate.filePath) || [];
|
|
316
|
+
const docLen = tokens.length || 1;
|
|
317
|
+
const tfMap = new Map();
|
|
318
|
+
for (const t of tokens) {
|
|
319
|
+
tfMap.set(t, (tfMap.get(t) || 0) + 1);
|
|
320
|
+
}
|
|
321
|
+
let score = 0;
|
|
322
|
+
for (const query of queryTerms) {
|
|
323
|
+
const tf = (tfMap.get(query) || 0) / docLen;
|
|
324
|
+
const df = docFreq.get(query) || 0;
|
|
325
|
+
if (df > 0) {
|
|
326
|
+
const idf = Math.log(1 + totalDocs / df);
|
|
327
|
+
score += tf * idf;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
rawScores.set(candidate.filePath, score);
|
|
331
|
+
if (score > maxScore)
|
|
332
|
+
maxScore = score;
|
|
333
|
+
}
|
|
334
|
+
const result = new Map();
|
|
335
|
+
for (const [filePath, raw] of rawScores.entries()) {
|
|
336
|
+
const normalized = maxScore > 0 ? raw / maxScore : 0;
|
|
337
|
+
result.set(filePath, { rawScore: raw, normalizedScore: normalized });
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
// ─── Path & Filename Match Scoring ───────────────────────────────────
|
|
342
|
+
/**
|
|
343
|
+
* Calculates a relevance score for a file path based on matching query terms against
|
|
344
|
+
* the file's basename, directory segments, and file extension.
|
|
345
|
+
*
|
|
346
|
+
* @param filePath File path
|
|
347
|
+
* @param queryTerms Extracted query terms
|
|
348
|
+
* @returns Path match score between 0.0 and 1.0
|
|
349
|
+
*/
|
|
350
|
+
export function calculatePathMatchScore(filePath, queryTerms) {
|
|
351
|
+
if (!filePath || queryTerms.length === 0)
|
|
352
|
+
return 0;
|
|
353
|
+
const basename = path.basename(filePath);
|
|
354
|
+
const basenameNoExt = path.parse(filePath).name;
|
|
355
|
+
const baseTokens = new Set(tokenize(basenameNoExt, false));
|
|
356
|
+
const fullPathTokens = new Set(tokenize(filePath, false));
|
|
357
|
+
let matchedBaseTerms = 0;
|
|
358
|
+
let matchedPathTerms = 0;
|
|
359
|
+
for (const term of queryTerms) {
|
|
360
|
+
const termLower = term.toLowerCase();
|
|
361
|
+
// Exact basename match (highest boost)
|
|
362
|
+
if (basenameNoExt.toLowerCase() === termLower || basename.toLowerCase() === termLower) {
|
|
363
|
+
return 1.0;
|
|
364
|
+
}
|
|
365
|
+
if (baseTokens.has(termLower)) {
|
|
366
|
+
matchedBaseTerms++;
|
|
367
|
+
}
|
|
368
|
+
else if (fullPathTokens.has(termLower)) {
|
|
369
|
+
matchedPathTerms++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const totalTerms = queryTerms.length;
|
|
373
|
+
const baseScore = (matchedBaseTerms / totalTerms) * 0.8;
|
|
374
|
+
const pathScore = (matchedPathTerms / totalTerms) * 0.4;
|
|
375
|
+
return Math.min(1.0, baseScore + pathScore);
|
|
376
|
+
}
|
|
377
|
+
// ─── Centrality Extraction & Normalization ───────────────────────────
|
|
378
|
+
/**
|
|
379
|
+
* Extracts and normalizes centrality metrics for a list of candidate files.
|
|
380
|
+
*
|
|
381
|
+
* @param candidates List of candidate files
|
|
382
|
+
* @param graph Optional dependency graph
|
|
383
|
+
* @param customCentrality Optional pre-computed centrality map
|
|
384
|
+
* @returns Map of filePath to normalized centrality score in [0.0, 1.0]
|
|
385
|
+
*/
|
|
386
|
+
export function computeCandidateCentralityScores(candidates, graph, customCentrality) {
|
|
387
|
+
const result = new Map();
|
|
388
|
+
let maxRawScore = 0;
|
|
389
|
+
for (const candidate of candidates) {
|
|
390
|
+
let metrics = { inDegree: 0, outDegree: 0, totalDegree: 0, score: 0 };
|
|
391
|
+
if (customCentrality?.has(candidate.filePath)) {
|
|
392
|
+
metrics = customCentrality.get(candidate.filePath);
|
|
393
|
+
}
|
|
394
|
+
else if (graph) {
|
|
395
|
+
metrics = graph.getCentrality(candidate.filePath);
|
|
396
|
+
}
|
|
397
|
+
if (metrics.score > maxRawScore) {
|
|
398
|
+
maxRawScore = metrics.score;
|
|
399
|
+
}
|
|
400
|
+
result.set(candidate.filePath, { metrics, normalizedScore: 0 });
|
|
401
|
+
}
|
|
402
|
+
for (const [filePath, entry] of result.entries()) {
|
|
403
|
+
const normalized = maxRawScore > 0 ? entry.metrics.score / maxRawScore : 0;
|
|
404
|
+
result.set(filePath, { metrics: entry.metrics, normalizedScore: normalized });
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
// ─── Token Estimator ─────────────────────────────────────────────────
|
|
409
|
+
/**
|
|
410
|
+
* Estimates token count from string length using a characters-per-token heuristic.
|
|
411
|
+
*/
|
|
412
|
+
export function estimateTokens(text, charsPerToken = CONTEXT_BUDGET_CONFIG.ESTIMATED_CHARS_PER_TOKEN) {
|
|
413
|
+
if (!text)
|
|
414
|
+
return 0;
|
|
415
|
+
return Math.ceil(text.length / charsPerToken);
|
|
416
|
+
}
|
|
417
|
+
// ─── Main Ranking & 3-Tier Budget Allocation Engine ───────────────────
|
|
418
|
+
/**
|
|
419
|
+
* Core Context Ranker:
|
|
420
|
+
* Ranks candidate files via hybrid BM25 + Graph Centrality + Path Match scoring,
|
|
421
|
+
* then partitions them into Tier 1 (full source), Tier 2 (AST outline skeleton),
|
|
422
|
+
* and Tier 3 (omitted) within strict token budgets.
|
|
423
|
+
*
|
|
424
|
+
* @param files Array of candidate file objects or map of filePath -> content
|
|
425
|
+
* @param prompt User prompt or instruction
|
|
426
|
+
* @param options Ranking and budgeting options
|
|
427
|
+
* @returns Partitioned 3-Tier result with complete metadata and statistics
|
|
428
|
+
*/
|
|
429
|
+
export function rankFilesForBudget(files, prompt, options) {
|
|
430
|
+
// 1. Normalize input candidates
|
|
431
|
+
let candidates = [];
|
|
432
|
+
if (Array.isArray(files)) {
|
|
433
|
+
candidates = files;
|
|
434
|
+
}
|
|
435
|
+
else if (files instanceof Map) {
|
|
436
|
+
for (const [filePath, content] of files.entries()) {
|
|
437
|
+
candidates.push({ filePath, content });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (typeof files === 'object' && files !== null) {
|
|
441
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
442
|
+
candidates.push({ filePath, content });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const charsPerToken = options?.charsPerToken ?? CONTEXT_BUDGET_CONFIG.ESTIMATED_CHARS_PER_TOKEN;
|
|
446
|
+
// Determine budgets
|
|
447
|
+
const defaultMaxChars = CONTEXT_BUDGET_CONFIG.DEFAULT_CONTEXT_MAX_CHARS;
|
|
448
|
+
const charBudget = typeof options?.charBudget === 'number' && options.charBudget > 0
|
|
449
|
+
? options.charBudget
|
|
450
|
+
: typeof options?.tokenBudget === 'number' && options.tokenBudget > 0
|
|
451
|
+
? options.tokenBudget * charsPerToken
|
|
452
|
+
: defaultMaxChars;
|
|
453
|
+
const tokenBudget = Math.floor(charBudget / charsPerToken);
|
|
454
|
+
const tier1Ratio = options?.tier1BudgetRatio ?? 0.7;
|
|
455
|
+
const tier2Ratio = options?.tier2BudgetRatio ?? 0.3;
|
|
456
|
+
const tier1CharBudget = Math.floor(charBudget * tier1Ratio);
|
|
457
|
+
const tier2CharBudget = Math.floor(charBudget * tier2Ratio);
|
|
458
|
+
const weights = {
|
|
459
|
+
bm25: options?.weights?.bm25 ?? 0.6,
|
|
460
|
+
centrality: options?.weights?.centrality ?? 0.25,
|
|
461
|
+
pathMatch: options?.weights?.pathMatch ?? 0.15,
|
|
462
|
+
};
|
|
463
|
+
// Normalize weight sum
|
|
464
|
+
const weightSum = weights.bm25 + weights.centrality + weights.pathMatch;
|
|
465
|
+
const normWeights = {
|
|
466
|
+
bm25: weightSum > 0 ? weights.bm25 / weightSum : 0.6,
|
|
467
|
+
centrality: weightSum > 0 ? weights.centrality / weightSum : 0.25,
|
|
468
|
+
pathMatch: weightSum > 0 ? weights.pathMatch / weightSum : 0.15,
|
|
469
|
+
};
|
|
470
|
+
if (candidates.length === 0) {
|
|
471
|
+
return {
|
|
472
|
+
tier1: [],
|
|
473
|
+
tier2: [],
|
|
474
|
+
tier3: [],
|
|
475
|
+
rankedFiles: [],
|
|
476
|
+
totalTokensUsed: 0,
|
|
477
|
+
totalCharsUsed: 0,
|
|
478
|
+
stats: {
|
|
479
|
+
totalCandidates: 0,
|
|
480
|
+
tier1Count: 0,
|
|
481
|
+
tier2Count: 0,
|
|
482
|
+
tier3Count: 0,
|
|
483
|
+
tokenBudget,
|
|
484
|
+
tokensUsed: 0,
|
|
485
|
+
charBudget,
|
|
486
|
+
charsUsed: 0,
|
|
487
|
+
budgetUtilization: 0,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
// 2. Extract query terms from user prompt
|
|
492
|
+
const queryTerms = extractQueryTerms(prompt);
|
|
493
|
+
// 3. Compute BM25 scores
|
|
494
|
+
const bm25Map = computeBM25Scores(candidates, queryTerms, {
|
|
495
|
+
k1: options?.bm25K1,
|
|
496
|
+
b: options?.bm25B,
|
|
497
|
+
});
|
|
498
|
+
// 4. Compute Centrality scores
|
|
499
|
+
const centralityMap = computeCandidateCentralityScores(candidates, options?.graph, options?.centralityMetrics);
|
|
500
|
+
// 5. Precompute AST outlines & composite scores
|
|
501
|
+
const scoredFiles = candidates.map((candidate) => {
|
|
502
|
+
const bm25Info = bm25Map.get(candidate.filePath) || { rawScore: 0, normalizedScore: 0 };
|
|
503
|
+
const centralityInfo = centralityMap.get(candidate.filePath) || {
|
|
504
|
+
metrics: { inDegree: 0, outDegree: 0, totalDegree: 0, score: 0 },
|
|
505
|
+
normalizedScore: 0,
|
|
506
|
+
};
|
|
507
|
+
const pathMatchScore = calculatePathMatchScore(candidate.filePath, queryTerms);
|
|
508
|
+
// Compute AST outline skeleton
|
|
509
|
+
const outline = candidate.outline || extractDeclarationsOutline(candidate.content, candidate.filePath) || '';
|
|
510
|
+
let compositeScore = normWeights.bm25 * bm25Info.normalizedScore +
|
|
511
|
+
normWeights.centrality * centralityInfo.normalizedScore +
|
|
512
|
+
normWeights.pathMatch * pathMatchScore;
|
|
513
|
+
// Explicit targets receive top priority boost
|
|
514
|
+
if (candidate.isExplicitTarget) {
|
|
515
|
+
compositeScore += 10.0;
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
filePath: candidate.filePath,
|
|
519
|
+
content: candidate.content,
|
|
520
|
+
outline,
|
|
521
|
+
tier: 3, // Default to tier 3, will be assigned during budget partitioning
|
|
522
|
+
score: compositeScore,
|
|
523
|
+
bm25Score: bm25Info.normalizedScore,
|
|
524
|
+
centralityScore: centralityInfo.normalizedScore,
|
|
525
|
+
pathMatchScore,
|
|
526
|
+
estimatedTokens: estimateTokens(candidate.content, charsPerToken),
|
|
527
|
+
estimatedChars: candidate.content.length,
|
|
528
|
+
isExplicitTarget: candidate.isExplicitTarget,
|
|
529
|
+
metadata: candidate.metadata,
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
// 6. Sort candidate files descending by composite relevance score
|
|
533
|
+
scoredFiles.sort((a, b) => b.score - a.score);
|
|
534
|
+
// 7. Partition into Tier 1 (Full Content), Tier 2 (AST Skeleton), and Tier 3 (Omitted)
|
|
535
|
+
const tier1 = [];
|
|
536
|
+
const tier2 = [];
|
|
537
|
+
const tier3 = [];
|
|
538
|
+
let currentTotalChars = 0;
|
|
539
|
+
let currentTier1Chars = 0;
|
|
540
|
+
let currentTier2Chars = 0;
|
|
541
|
+
for (const file of scoredFiles) {
|
|
542
|
+
const fullContentChars = file.content.length;
|
|
543
|
+
const outlineChars = file.outline.length || Math.min(fullContentChars, 200);
|
|
544
|
+
// Check if score is below minimal cutoff threshold (unless explicit target)
|
|
545
|
+
if (!file.isExplicitTarget && options?.minScoreThreshold && file.score < options.minScoreThreshold) {
|
|
546
|
+
file.tier = 3;
|
|
547
|
+
file.estimatedChars = 0;
|
|
548
|
+
file.estimatedTokens = 0;
|
|
549
|
+
tier3.push(file);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
// Attempt Tier 1: Full content
|
|
553
|
+
// Criteria: Explicit targets or fits within remaining Tier 1 budget and overall budget
|
|
554
|
+
const fitsTier1 = file.isExplicitTarget ||
|
|
555
|
+
(currentTier1Chars + fullContentChars <= tier1CharBudget && currentTotalChars + fullContentChars <= charBudget);
|
|
556
|
+
if (fitsTier1 && currentTotalChars + fullContentChars <= charBudget) {
|
|
557
|
+
file.tier = 1;
|
|
558
|
+
file.estimatedChars = fullContentChars;
|
|
559
|
+
file.estimatedTokens = estimateTokens(file.content, charsPerToken);
|
|
560
|
+
tier1.push(file);
|
|
561
|
+
currentTier1Chars += fullContentChars;
|
|
562
|
+
currentTotalChars += fullContentChars;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
// Attempt Tier 2: AST Outline Skeleton
|
|
566
|
+
// Criteria: Fits within Tier 2 budget and overall budget
|
|
567
|
+
const fitsTier2 = currentTier2Chars + outlineChars <= tier2CharBudget && currentTotalChars + outlineChars <= charBudget;
|
|
568
|
+
if (fitsTier2) {
|
|
569
|
+
file.tier = 2;
|
|
570
|
+
file.estimatedChars = outlineChars;
|
|
571
|
+
file.estimatedTokens = estimateTokens(file.outline, charsPerToken);
|
|
572
|
+
tier2.push(file);
|
|
573
|
+
currentTier2Chars += outlineChars;
|
|
574
|
+
currentTotalChars += outlineChars;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
// If it doesn't fit in Tier 1 or Tier 2, it is assigned to Tier 3 (Omitted)
|
|
578
|
+
file.tier = 3;
|
|
579
|
+
file.estimatedChars = 0;
|
|
580
|
+
file.estimatedTokens = 0;
|
|
581
|
+
tier3.push(file);
|
|
582
|
+
}
|
|
583
|
+
const totalTokensUsed = Math.ceil(currentTotalChars / charsPerToken);
|
|
584
|
+
return {
|
|
585
|
+
tier1,
|
|
586
|
+
tier2,
|
|
587
|
+
tier3,
|
|
588
|
+
rankedFiles: scoredFiles,
|
|
589
|
+
totalTokensUsed,
|
|
590
|
+
totalCharsUsed: currentTotalChars,
|
|
591
|
+
stats: {
|
|
592
|
+
totalCandidates: scoredFiles.length,
|
|
593
|
+
tier1Count: tier1.length,
|
|
594
|
+
tier2Count: tier2.length,
|
|
595
|
+
tier3Count: tier3.length,
|
|
596
|
+
tokenBudget,
|
|
597
|
+
tokensUsed: totalTokensUsed,
|
|
598
|
+
charBudget,
|
|
599
|
+
charsUsed: currentTotalChars,
|
|
600
|
+
budgetUtilization: charBudget > 0 ? Math.min(1.0, currentTotalChars / charBudget) : 0,
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { DependencyNode } from './types.js';
|
|
1
|
+
import { DependencyNode, CentralityMetrics } from './types.js';
|
|
2
2
|
export interface DependencyGraph {
|
|
3
3
|
getImports(filePath: string): string[];
|
|
4
4
|
getImportedBy(filePath: string): string[];
|
|
5
5
|
getReverseDependencyTree(filePath: string, maxDepth?: number): string[];
|
|
6
6
|
getForwardDependencyTree(filePath: string, maxDepth?: number): string[];
|
|
7
|
+
getCentrality(filePath: string): CentralityMetrics;
|
|
8
|
+
getAllCentrality(): Map<string, CentralityMetrics>;
|
|
7
9
|
readonly nodes: ReadonlyMap<string, DependencyNode>;
|
|
8
10
|
}
|
|
11
|
+
export declare function computeGraphCentrality(nodes: ReadonlyMap<string, DependencyNode> | Map<string, DependencyNode>): Map<string, CentralityMetrics>;
|
|
9
12
|
export declare function bfsTraverse(nodes: Map<string, DependencyNode>, startFile: string, direction: 'imports' | 'importedBy', maxDepth: number): string[];
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export function computeGraphCentrality(nodes) {
|
|
2
|
+
const result = new Map();
|
|
3
|
+
for (const [file, node] of nodes.entries()) {
|
|
4
|
+
const inDegree = node.importedBy.size;
|
|
5
|
+
const outDegree = node.imports.size;
|
|
6
|
+
const totalDegree = inDegree + outDegree;
|
|
7
|
+
const score = inDegree * 1.5 + outDegree * 0.5;
|
|
8
|
+
result.set(file, { inDegree, outDegree, totalDegree, score });
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
1
12
|
export function bfsTraverse(nodes, startFile, direction, maxDepth) {
|
|
2
13
|
const visited = new Set();
|
|
3
14
|
visited.add(startFile);
|
|
@@ -2,3 +2,13 @@ export interface DependencyNode {
|
|
|
2
2
|
imports: Set<string>;
|
|
3
3
|
importedBy: Set<string>;
|
|
4
4
|
}
|
|
5
|
+
export interface CentralityMetrics {
|
|
6
|
+
/** Number of files that directly import this file (hub dependency) */
|
|
7
|
+
inDegree: number;
|
|
8
|
+
/** Number of files this file directly imports */
|
|
9
|
+
outDegree: number;
|
|
10
|
+
/** Total direct connections (inDegree + outDegree) */
|
|
11
|
+
totalDegree: number;
|
|
12
|
+
/** Centrality score (weighted: inDegree * 1.5 + outDegree * 0.5) */
|
|
13
|
+
score: number;
|
|
14
|
+
}
|