@ngommans/codefocus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ngommans/codefocus might be problematic. Click here for more details.
- package/README.md +124 -0
- package/dist/benchmark-43DOYNYR.js +465 -0
- package/dist/benchmark-43DOYNYR.js.map +1 -0
- package/dist/chunk-6XH2ZLP6.js +127 -0
- package/dist/chunk-6XH2ZLP6.js.map +1 -0
- package/dist/chunk-7RYHZOYF.js +27 -0
- package/dist/chunk-7RYHZOYF.js.map +1 -0
- package/dist/chunk-ITVAEU6K.js +250 -0
- package/dist/chunk-ITVAEU6K.js.map +1 -0
- package/dist/chunk-Q6DOBQ4F.js +231 -0
- package/dist/chunk-Q6DOBQ4F.js.map +1 -0
- package/dist/chunk-X7DRJUEX.js +543 -0
- package/dist/chunk-X7DRJUEX.js.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-ICBN54MT.js +64 -0
- package/dist/commands-ICBN54MT.js.map +1 -0
- package/dist/config-OCBWYENF.js +12 -0
- package/dist/config-OCBWYENF.js.map +1 -0
- package/dist/extended-benchmark-5RUXDG3D.js +323 -0
- package/dist/extended-benchmark-5RUXDG3D.js.map +1 -0
- package/dist/find-W5UDE4US.js +63 -0
- package/dist/find-W5UDE4US.js.map +1 -0
- package/dist/graph-DZNBEATA.js +189 -0
- package/dist/graph-DZNBEATA.js.map +1 -0
- package/dist/map-6WOMDLCP.js +131 -0
- package/dist/map-6WOMDLCP.js.map +1 -0
- package/dist/mcp-7WYTXIQS.js +354 -0
- package/dist/mcp-7WYTXIQS.js.map +1 -0
- package/dist/mcp-server.js +369 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/query-DJNWYYJD.js +427 -0
- package/dist/query-DJNWYYJD.js.map +1 -0
- package/dist/query-PS6QVPXP.js +538 -0
- package/dist/query-PS6QVPXP.js.map +1 -0
- package/dist/root-ODTOXM2J.js +10 -0
- package/dist/root-ODTOXM2J.js.map +1 -0
- package/dist/watcher-LFBZAM5E.js +73 -0
- package/dist/watcher-LFBZAM5E.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SCORING_CONFIG,
|
|
4
|
+
loadScoringConfig
|
|
5
|
+
} from "./chunk-6XH2ZLP6.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveRoot
|
|
8
|
+
} from "./chunk-7RYHZOYF.js";
|
|
9
|
+
import {
|
|
10
|
+
IndexDatabase
|
|
11
|
+
} from "./chunk-Q6DOBQ4F.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/query.ts
|
|
14
|
+
import { createRequire } from "module";
|
|
15
|
+
import { resolve } from "path";
|
|
16
|
+
import { existsSync, readFileSync } from "fs";
|
|
17
|
+
var require2 = createRequire(import.meta.url);
|
|
18
|
+
var { DirectedGraph } = require2("graphology");
|
|
19
|
+
var pagerank = require2("graphology-metrics/centrality/pagerank");
|
|
20
|
+
var { getEncoding } = require2("js-tiktoken");
|
|
21
|
+
function computeFilePagerank(db) {
|
|
22
|
+
const graph = new DirectedGraph();
|
|
23
|
+
for (const file of db.getAllFiles()) {
|
|
24
|
+
graph.addNode(file.path);
|
|
25
|
+
}
|
|
26
|
+
for (const edge of db.getFileImportEdges()) {
|
|
27
|
+
const key = `${edge.source_file}->${edge.target_file}`;
|
|
28
|
+
if (!graph.hasEdge(key)) {
|
|
29
|
+
graph.addEdgeWithKey(key, edge.source_file, edge.target_file);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const ranks = graph.order > 0 ? pagerank(graph, { getEdgeWeight: null }) : {};
|
|
33
|
+
return new Map(Object.entries(ranks));
|
|
34
|
+
}
|
|
35
|
+
function ftsSeededSymbols(db, term, rootDir, config = DEFAULT_SCORING_CONFIG) {
|
|
36
|
+
const ftsResults = db.searchContent(term);
|
|
37
|
+
if (ftsResults.length === 0) return [];
|
|
38
|
+
const results = [];
|
|
39
|
+
const maxRank = Math.max(...ftsResults.map((r) => r.rank));
|
|
40
|
+
for (const fts of ftsResults) {
|
|
41
|
+
let normalizedScore = maxRank > 0 ? fts.rank / maxRank * 0.4 : 0.2;
|
|
42
|
+
const absPath = resolve(rootDir, fts.file_path);
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = readFileSync(absPath, "utf-8");
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const lines = content.split("\n");
|
|
50
|
+
const termLower = term.toLowerCase();
|
|
51
|
+
const matchingLines = [];
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
if (lines[i].toLowerCase().includes(termLower)) {
|
|
54
|
+
matchingLines.push(i + 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (matchingLines.length === 0) continue;
|
|
58
|
+
const tfBoost = Math.log2(1 + matchingLines.length);
|
|
59
|
+
normalizedScore *= tfBoost;
|
|
60
|
+
const fileSymbols = db.getSymbolsByFile(fts.file_path);
|
|
61
|
+
const hasSymbolNameMatch = fileSymbols.some(
|
|
62
|
+
(s) => s.name.toLowerCase().includes(termLower) || (s.signature?.toLowerCase().includes(termLower) ?? false)
|
|
63
|
+
);
|
|
64
|
+
if (hasSymbolNameMatch) {
|
|
65
|
+
normalizedScore *= config.symbolProximityBoost;
|
|
66
|
+
}
|
|
67
|
+
for (const sym of fileSymbols) {
|
|
68
|
+
if (!sym.id) continue;
|
|
69
|
+
const containsMatch = matchingLines.some(
|
|
70
|
+
(line) => line >= sym.start_line && line <= sym.end_line
|
|
71
|
+
);
|
|
72
|
+
if (containsMatch) {
|
|
73
|
+
results.push({
|
|
74
|
+
symbol: sym,
|
|
75
|
+
score: normalizedScore,
|
|
76
|
+
matchType: "contains",
|
|
77
|
+
depth: 0
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
function searchAndExpand(db, term, maxDepth, rootDir, config = DEFAULT_SCORING_CONFIG) {
|
|
85
|
+
const scored = /* @__PURE__ */ new Map();
|
|
86
|
+
const directMatches = db.findSymbols(term);
|
|
87
|
+
for (const sym of directMatches) {
|
|
88
|
+
if (!sym.id) continue;
|
|
89
|
+
const isExact = sym.name.toLowerCase() === term.toLowerCase();
|
|
90
|
+
scored.set(sym.id, {
|
|
91
|
+
symbol: sym,
|
|
92
|
+
score: isExact ? 1 : 0.5,
|
|
93
|
+
matchType: isExact ? "exact" : "contains",
|
|
94
|
+
depth: 0
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (scored.size === 0) {
|
|
98
|
+
const ftsSymbols = ftsSeededSymbols(db, term, rootDir, config);
|
|
99
|
+
for (const ss of ftsSymbols) {
|
|
100
|
+
if (ss.symbol.id && !scored.has(ss.symbol.id)) {
|
|
101
|
+
scored.set(ss.symbol.id, ss);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (maxDepth > 0) {
|
|
106
|
+
const fileDegree = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const edge of db.getFileImportEdges()) {
|
|
108
|
+
fileDegree.set(
|
|
109
|
+
edge.source_file,
|
|
110
|
+
(fileDegree.get(edge.source_file) ?? 0) + 1
|
|
111
|
+
);
|
|
112
|
+
fileDegree.set(
|
|
113
|
+
edge.target_file,
|
|
114
|
+
(fileDegree.get(edge.target_file) ?? 0) + 1
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
let frontier = new Set(scored.keys());
|
|
118
|
+
for (let hop = 1; hop <= maxDepth && frontier.size > 0; hop++) {
|
|
119
|
+
const nextFrontier = /* @__PURE__ */ new Set();
|
|
120
|
+
for (const symbolId of frontier) {
|
|
121
|
+
const outRefs = db.getOutgoingReferences(symbolId);
|
|
122
|
+
for (const ref of outRefs) {
|
|
123
|
+
if (!scored.has(ref.target_id)) {
|
|
124
|
+
const sym = db.getSymbolById(ref.target_id);
|
|
125
|
+
if (sym) {
|
|
126
|
+
const baseWeight = ref.ref_type === "import" ? config.importEdgeWeight : config.typeRefWeight;
|
|
127
|
+
const deg = fileDegree.get(ref.target_file) ?? 0;
|
|
128
|
+
const hubDampening = deg > 1 ? 1 / Math.sqrt(deg) : 1;
|
|
129
|
+
const graphScore = baseWeight / hop * hubDampening;
|
|
130
|
+
scored.set(ref.target_id, {
|
|
131
|
+
symbol: sym,
|
|
132
|
+
score: graphScore,
|
|
133
|
+
matchType: "graph",
|
|
134
|
+
depth: hop
|
|
135
|
+
});
|
|
136
|
+
nextFrontier.add(ref.target_id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const inRefs = db.getIncomingReferences(symbolId);
|
|
141
|
+
for (const ref of inRefs) {
|
|
142
|
+
if (!scored.has(ref.source_id)) {
|
|
143
|
+
const sym = db.getSymbolById(ref.source_id);
|
|
144
|
+
if (sym) {
|
|
145
|
+
const baseWeight = ref.ref_type === "import" ? config.importEdgeWeight : config.typeRefWeight;
|
|
146
|
+
const deg = fileDegree.get(ref.source_file) ?? 0;
|
|
147
|
+
const hubDampening = deg > 1 ? 1 / Math.sqrt(deg) : 1;
|
|
148
|
+
const graphScore = baseWeight / hop * hubDampening;
|
|
149
|
+
scored.set(ref.source_id, {
|
|
150
|
+
symbol: sym,
|
|
151
|
+
score: graphScore,
|
|
152
|
+
matchType: "graph",
|
|
153
|
+
depth: hop
|
|
154
|
+
});
|
|
155
|
+
nextFrontier.add(ref.source_id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
frontier = nextFrontier;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return Array.from(scored.values());
|
|
164
|
+
}
|
|
165
|
+
function groupByFile(scoredSymbols, pagerankScores) {
|
|
166
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const ss of scoredSymbols) {
|
|
168
|
+
const fp = ss.symbol.file_path;
|
|
169
|
+
if (!fileMap.has(fp)) fileMap.set(fp, []);
|
|
170
|
+
fileMap.get(fp).push(ss);
|
|
171
|
+
}
|
|
172
|
+
const sections = [];
|
|
173
|
+
for (const [filePath, symbols] of fileMap) {
|
|
174
|
+
symbols.sort((a, b) => a.symbol.start_line - b.symbol.start_line);
|
|
175
|
+
const maxSymScore = Math.max(...symbols.map((s) => s.score));
|
|
176
|
+
const hasDirectMatch = symbols.some((s) => s.matchType !== "graph");
|
|
177
|
+
const prScore = hasDirectMatch ? pagerankScores.get(filePath) ?? 0 : 0;
|
|
178
|
+
const fileScore = maxSymScore + prScore;
|
|
179
|
+
const startLine = Math.min(...symbols.map((s) => s.symbol.start_line));
|
|
180
|
+
const endLine = Math.max(...symbols.map((s) => s.symbol.end_line));
|
|
181
|
+
sections.push({
|
|
182
|
+
filePath,
|
|
183
|
+
symbols,
|
|
184
|
+
score: fileScore,
|
|
185
|
+
startLine,
|
|
186
|
+
endLine
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
sections.sort(
|
|
190
|
+
(a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath)
|
|
191
|
+
);
|
|
192
|
+
return sections;
|
|
193
|
+
}
|
|
194
|
+
function applyScoreFloor(sections, config = DEFAULT_SCORING_CONFIG) {
|
|
195
|
+
if (sections.length === 0) return sections;
|
|
196
|
+
const maxScore = sections[0].score;
|
|
197
|
+
const floor = config.scoreFloorRatio * maxScore;
|
|
198
|
+
return sections.filter((s) => s.score >= floor);
|
|
199
|
+
}
|
|
200
|
+
function findElbow(scores, config = DEFAULT_SCORING_CONFIG) {
|
|
201
|
+
if (scores.length <= 1) return null;
|
|
202
|
+
const maxScore = scores[0];
|
|
203
|
+
for (let i = 0; i < scores.length - 1; i++) {
|
|
204
|
+
const drop = (scores[i] - scores[i + 1]) / scores[i];
|
|
205
|
+
if (drop > config.elbowDropRatio && scores[i + 1] < 0.3 * maxScore) {
|
|
206
|
+
return i;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function applyElbow(sections, config = DEFAULT_SCORING_CONFIG) {
|
|
212
|
+
if (sections.length < 3) return sections;
|
|
213
|
+
const scores = sections.map((s) => s.score);
|
|
214
|
+
const elbowIdx = findElbow(scores, config);
|
|
215
|
+
if (elbowIdx !== null) {
|
|
216
|
+
return sections.slice(0, elbowIdx + 1);
|
|
217
|
+
}
|
|
218
|
+
return sections;
|
|
219
|
+
}
|
|
220
|
+
function readSourceLines(rootDir, filePath, startLine, endLine) {
|
|
221
|
+
const absPath = resolve(rootDir, filePath);
|
|
222
|
+
try {
|
|
223
|
+
const content = readFileSync(absPath, "utf-8");
|
|
224
|
+
const lines = content.split("\n");
|
|
225
|
+
const start = Math.max(0, startLine - 1);
|
|
226
|
+
const end = Math.min(lines.length, endLine);
|
|
227
|
+
return lines.slice(start, end).join("\n");
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function renderSection(rootDir, section) {
|
|
233
|
+
const symbolNames = section.symbols.map((s) => s.symbol.name).filter((name, i, arr) => arr.indexOf(name) === i).slice(0, 5);
|
|
234
|
+
const nameList = symbolNames.join(", ");
|
|
235
|
+
const header = `\u2500\u2500 ${section.filePath}:${section.startLine}-${section.endLine} (${nameList}) \u2500\u2500`;
|
|
236
|
+
const source = readSourceLines(
|
|
237
|
+
rootDir,
|
|
238
|
+
section.filePath,
|
|
239
|
+
section.startLine,
|
|
240
|
+
section.endLine
|
|
241
|
+
);
|
|
242
|
+
if (source === null) {
|
|
243
|
+
return `${header}
|
|
244
|
+
[file not readable]`;
|
|
245
|
+
}
|
|
246
|
+
return `${header}
|
|
247
|
+
${source}`;
|
|
248
|
+
}
|
|
249
|
+
function runQueryCore(root, term, budget, depth, options) {
|
|
250
|
+
const dbPath = resolve(root, ".codefocus", "index.db");
|
|
251
|
+
if (!existsSync(dbPath)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`no index found at ${dbPath}
|
|
254
|
+
Run 'codefocus index --root ${root}' first.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const config = options?.config ?? loadScoringConfig(root);
|
|
258
|
+
const db = new IndexDatabase(dbPath);
|
|
259
|
+
try {
|
|
260
|
+
const scoredSymbols = searchAndExpand(db, term, depth, root, config);
|
|
261
|
+
if (scoredSymbols.length === 0) {
|
|
262
|
+
return `No results found for "${term}"`;
|
|
263
|
+
}
|
|
264
|
+
const pagerankScores = computeFilePagerank(db);
|
|
265
|
+
const allSections = groupByFile(scoredSymbols, pagerankScores);
|
|
266
|
+
const afterFloor = applyScoreFloor(allSections, config);
|
|
267
|
+
const sections = applyElbow(afterFloor, config);
|
|
268
|
+
const enc = getEncoding("cl100k_base");
|
|
269
|
+
const blocks = [];
|
|
270
|
+
let tokenCount = 0;
|
|
271
|
+
let totalSymbols = 0;
|
|
272
|
+
let truncated = false;
|
|
273
|
+
for (const section of sections) {
|
|
274
|
+
const block = renderSection(root, section);
|
|
275
|
+
const blockTokens = enc.encode(block).length;
|
|
276
|
+
if (blocks.length > 0 && blockTokens > 0) {
|
|
277
|
+
const marginalValue = section.score / blockTokens;
|
|
278
|
+
if (marginalValue < config.minMarginalValue) {
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (tokenCount + blockTokens <= budget) {
|
|
283
|
+
blocks.push(block);
|
|
284
|
+
tokenCount += blockTokens;
|
|
285
|
+
totalSymbols += section.symbols.length;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const lines = block.split("\n");
|
|
289
|
+
let partial = lines[0];
|
|
290
|
+
let partialTokens = enc.encode(partial).length;
|
|
291
|
+
if (tokenCount + partialTokens > budget) {
|
|
292
|
+
truncated = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
for (let i = 1; i < lines.length; i++) {
|
|
296
|
+
const candidate = partial + "\n" + lines[i];
|
|
297
|
+
const candidateTokens = enc.encode(candidate).length;
|
|
298
|
+
if (tokenCount + candidateTokens > budget) break;
|
|
299
|
+
partial = candidate;
|
|
300
|
+
partialTokens = candidateTokens;
|
|
301
|
+
}
|
|
302
|
+
blocks.push(partial);
|
|
303
|
+
tokenCount += partialTokens;
|
|
304
|
+
totalSymbols += section.symbols.length;
|
|
305
|
+
truncated = true;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
const allScores = sections.map((s) => s.score).sort((a, b) => b - a);
|
|
309
|
+
const topScore = allScores[0] ?? 0;
|
|
310
|
+
const medianScore = allScores.length > 0 ? allScores[Math.floor(allScores.length / 2)] : 0;
|
|
311
|
+
const hasExact = sections.some(
|
|
312
|
+
(s) => s.symbols.some((ss) => ss.matchType === "exact")
|
|
313
|
+
);
|
|
314
|
+
const scoreRange = allScores.length > 1 ? (allScores[0] - allScores[allScores.length - 1]) / allScores[0] : 0;
|
|
315
|
+
const confidence = hasExact && (medianScore >= 0.3 || sections.length <= 2) ? "high" : !hasExact || medianScore < 0.1 ? scoreRange < 0.3 ? "medium" : "low" : "medium";
|
|
316
|
+
const output = [];
|
|
317
|
+
const shown = blocks.length;
|
|
318
|
+
const frontMatter = [
|
|
319
|
+
"---",
|
|
320
|
+
`query: ${term}`,
|
|
321
|
+
`tokens: ${tokenCount}`,
|
|
322
|
+
`files: ${shown}`,
|
|
323
|
+
`symbols: ${totalSymbols}`,
|
|
324
|
+
`truncated: ${truncated}`,
|
|
325
|
+
`confidence: ${confidence}`,
|
|
326
|
+
`top_score: ${topScore.toFixed(2)}`,
|
|
327
|
+
`median_score: ${medianScore.toFixed(2)}`,
|
|
328
|
+
"---"
|
|
329
|
+
].join("\n");
|
|
330
|
+
output.push(frontMatter);
|
|
331
|
+
output.push("");
|
|
332
|
+
output.push(blocks.join("\n\n"));
|
|
333
|
+
if (options?.stats) {
|
|
334
|
+
output.push("");
|
|
335
|
+
const totalCandidates = allSections.length;
|
|
336
|
+
const afterFloorCount = afterFloor.length;
|
|
337
|
+
const afterElbowCount = sections.length;
|
|
338
|
+
if (totalCandidates > afterElbowCount) {
|
|
339
|
+
output.push(
|
|
340
|
+
`[stats] filtering: ${totalCandidates} candidates \u2192 ${afterFloorCount} after B1 floor \u2192 ${afterElbowCount} after B3 elbow`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
for (const section of sections.slice(0, shown)) {
|
|
344
|
+
const topSym = section.symbols.reduce(
|
|
345
|
+
(best, s) => s.score > best.score ? s : best,
|
|
346
|
+
section.symbols[0]
|
|
347
|
+
);
|
|
348
|
+
const block = renderSection(root, section);
|
|
349
|
+
const blockTokens = enc.encode(block).length;
|
|
350
|
+
const marginal = blockTokens > 0 ? (section.score / blockTokens).toFixed(6) : "N/A";
|
|
351
|
+
output.push(
|
|
352
|
+
`[stats] ${section.filePath.padEnd(40)} score=${section.score.toFixed(2)} match=${topSym.matchType.padEnd(8)} depth=${topSym.depth} value/tok=${marginal}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const p25 = allScores.length > 0 ? allScores[Math.floor(allScores.length * 0.75)] : 0;
|
|
356
|
+
const minScore = allScores[allScores.length - 1] ?? 0;
|
|
357
|
+
output.push(
|
|
358
|
+
`[stats] score distribution: max=${topScore.toFixed(2)} median=${medianScore.toFixed(2)} p25=${p25.toFixed(2)} min=${minScore.toFixed(2)}`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const total = sections.length;
|
|
362
|
+
const parts = [
|
|
363
|
+
`[query] ${shown} files, ${totalSymbols} symbols, ~${tokenCount} tokens`
|
|
364
|
+
];
|
|
365
|
+
if (truncated) {
|
|
366
|
+
parts.push(`(budget: ${budget}, truncated)`);
|
|
367
|
+
}
|
|
368
|
+
if (total > shown) {
|
|
369
|
+
parts.push(`(${total - shown} more files not shown)`);
|
|
370
|
+
}
|
|
371
|
+
output.push(`
|
|
372
|
+
${parts.join(" ")}`);
|
|
373
|
+
return output.join("\n");
|
|
374
|
+
} finally {
|
|
375
|
+
db.close();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function runQuery(positional, flags) {
|
|
379
|
+
if (flags.help) {
|
|
380
|
+
console.log(`codefocus query \u2014 Search and return ranked code context
|
|
381
|
+
|
|
382
|
+
Usage: codefocus query <search-term> [options]
|
|
383
|
+
|
|
384
|
+
Options:
|
|
385
|
+
--budget <tokens> Token budget for output (default: 8000)
|
|
386
|
+
--depth <n> Max graph traversal depth (default: 2)
|
|
387
|
+
--root <path> Root directory of indexed project (default: auto-detect)
|
|
388
|
+
--stats Show verbose scoring detail per section
|
|
389
|
+
--help Show this help message
|
|
390
|
+
|
|
391
|
+
Examples:
|
|
392
|
+
codefocus query handleSync
|
|
393
|
+
codefocus query Calculator --budget 4000
|
|
394
|
+
codefocus query "runIndex" --depth 3`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const term = positional[0];
|
|
398
|
+
if (!term) {
|
|
399
|
+
console.error("Error: query requires a search term");
|
|
400
|
+
process.exitCode = 2;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const root = resolveRoot(flags.root);
|
|
404
|
+
const config = loadScoringConfig(root);
|
|
405
|
+
const budget = parseInt(String(flags.budget || String(config.defaultBudget)), 10);
|
|
406
|
+
if (isNaN(budget) || budget <= 0) {
|
|
407
|
+
console.error("Error: --budget must be a positive integer");
|
|
408
|
+
process.exitCode = 2;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const depth = parseInt(String(flags.depth || String(config.defaultDepth)), 10);
|
|
412
|
+
if (isNaN(depth) || depth < 0) {
|
|
413
|
+
console.error("Error: --depth must be a non-negative integer");
|
|
414
|
+
process.exitCode = 2;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const result = runQueryCore(root, term, budget, depth, {
|
|
418
|
+
stats: !!flags.stats,
|
|
419
|
+
config
|
|
420
|
+
});
|
|
421
|
+
console.log(result);
|
|
422
|
+
}
|
|
423
|
+
export {
|
|
424
|
+
runQuery,
|
|
425
|
+
runQueryCore
|
|
426
|
+
};
|
|
427
|
+
//# sourceMappingURL=query-DJNWYYJD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/query.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { resolve } from \"node:path\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { IndexDatabase, type SymbolRow } from \"../db.js\";\nimport { resolveRoot } from \"../root.js\";\nimport {\n loadScoringConfig,\n DEFAULT_SCORING_CONFIG,\n type ScoringConfig,\n} from \"../config.js\";\n\nconst require = createRequire(import.meta.url);\nconst { DirectedGraph } = require(\"graphology\");\nconst pagerank = require(\"graphology-metrics/centrality/pagerank\");\nconst { getEncoding } = require(\"js-tiktoken\");\n\n// ── types ───────────────────────────────────────────────────────────────\n\ninterface ScoredSymbol {\n symbol: SymbolRow;\n score: number;\n matchType: \"exact\" | \"contains\" | \"graph\";\n depth: number;\n}\n\ninterface FileSection {\n filePath: string;\n symbols: ScoredSymbol[];\n score: number;\n startLine: number;\n endLine: number;\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────\n\n/**\n * Compute PageRank scores for all files in the index.\n */\nfunction computeFilePagerank(db: IndexDatabase): Map<string, number> {\n const graph = new DirectedGraph();\n\n for (const file of db.getAllFiles()) {\n graph.addNode(file.path);\n }\n\n for (const edge of db.getFileImportEdges()) {\n const key = `${edge.source_file}->${edge.target_file}`;\n if (!graph.hasEdge(key)) {\n graph.addEdgeWithKey(key, edge.source_file, edge.target_file);\n }\n }\n\n const ranks: Record<string, number> =\n graph.order > 0 ? pagerank(graph, { getEdgeWeight: null }) : {};\n\n return new Map(Object.entries(ranks));\n}\n\n/**\n * Use FTS5 to find symbols whose containing file matches the search term.\n * Scores FTS5 hits by proximity to symbol boundaries: a match inside a\n * function body promotes that function, not the entire file.\n *\n * Spike 8 A1: Weighted term frequency — files where the term appears more\n * often (relative to file length) get a log-scaled boost.\n * Spike 8 A2: Symbol-name proximity — files containing a symbol whose name\n * includes the search term get a 1.5x boost.\n */\nfunction ftsSeededSymbols(\n db: IndexDatabase,\n term: string,\n rootDir: string,\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): ScoredSymbol[] {\n const ftsResults = db.searchContent(term);\n if (ftsResults.length === 0) return [];\n\n const results: ScoredSymbol[] = [];\n const maxRank = Math.max(...ftsResults.map((r) => r.rank));\n\n for (const fts of ftsResults) {\n // Normalize FTS5 BM25 score to 0-0.4 range (below exact match, above graph)\n let normalizedScore = maxRank > 0 ? (fts.rank / maxRank) * 0.4 : 0.2;\n\n // Get file content to find which symbols contain the search term\n const absPath = resolve(rootDir, fts.file_path);\n let content: string;\n try {\n content = readFileSync(absPath, \"utf-8\");\n } catch {\n continue;\n }\n\n const lines = content.split(\"\\n\");\n const termLower = term.toLowerCase();\n\n // Find line numbers where the term appears\n const matchingLines: number[] = [];\n for (let i = 0; i < lines.length; i++) {\n if (lines[i].toLowerCase().includes(termLower)) {\n matchingLines.push(i + 1); // 1-indexed\n }\n }\n\n if (matchingLines.length === 0) continue;\n\n // A1: Weighted term frequency — boost files where the term is\n // structurally important (many mentions) vs. incidental (1-2 mentions).\n // Uses absolute count: log2(1 + count). Large files with many mentions\n // score higher than small files with few, unlike density-based metrics\n // which penalise important large files.\n const tfBoost = Math.log2(1 + matchingLines.length);\n normalizedScore *= tfBoost;\n\n // A2: Symbol-name proximity boost — if any symbol in this file has a\n // name or signature that includes the search term, the file is more\n // relevant than one where the term only appears in code comments.\n const fileSymbols = db.getSymbolsByFile(fts.file_path);\n const hasSymbolNameMatch = fileSymbols.some(\n (s) =>\n s.name.toLowerCase().includes(termLower) ||\n (s.signature?.toLowerCase().includes(termLower) ?? false),\n );\n if (hasSymbolNameMatch) {\n normalizedScore *= config.symbolProximityBoost;\n }\n\n // Get symbols in this file and score by proximity\n for (const sym of fileSymbols) {\n if (!sym.id) continue;\n\n // Check if any match is within this symbol's line range\n const containsMatch = matchingLines.some(\n (line) => line >= sym.start_line && line <= sym.end_line,\n );\n\n if (containsMatch) {\n // Match is inside this symbol's body — high relevance\n results.push({\n symbol: sym,\n score: normalizedScore,\n matchType: \"contains\" as const,\n depth: 0,\n });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Search for symbols matching the term, then expand via graph traversal\n * up to `maxDepth` hops.\n */\nfunction searchAndExpand(\n db: IndexDatabase,\n term: string,\n maxDepth: number,\n rootDir: string,\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): ScoredSymbol[] {\n const scored = new Map<number, ScoredSymbol>();\n\n // Phase 1: Direct symbol name matches\n const directMatches = db.findSymbols(term);\n\n for (const sym of directMatches) {\n if (!sym.id) continue;\n const isExact = sym.name.toLowerCase() === term.toLowerCase();\n scored.set(sym.id, {\n symbol: sym,\n score: isExact ? 1.0 : 0.5,\n matchType: isExact ? \"exact\" : \"contains\",\n depth: 0,\n });\n }\n\n // Phase 1b: FTS5 full-text content search (secondary seed)\n // Use only when symbol-name matching returns no results\n if (scored.size === 0) {\n const ftsSymbols = ftsSeededSymbols(db, term, rootDir, config);\n for (const ss of ftsSymbols) {\n if (ss.symbol.id && !scored.has(ss.symbol.id)) {\n scored.set(ss.symbol.id, ss);\n }\n }\n }\n\n // Phase 2: Graph expansion (BFS) with edge-weight scoring\n // Weight by reference type: import (0.4) > type_ref (0.2)\n // Dampen high-degree hub nodes: multiply score by 1/sqrt(fileDegree)\n // Uses total degree (in + out) to catch both importers (cli.ts) and\n // importees (db.ts) that act as hubs\n if (maxDepth > 0) {\n // Precompute file-level total degree (in + out) from import graph\n const fileDegree = new Map<string, number>();\n for (const edge of db.getFileImportEdges()) {\n fileDegree.set(\n edge.source_file,\n (fileDegree.get(edge.source_file) ?? 0) + 1,\n );\n fileDegree.set(\n edge.target_file,\n (fileDegree.get(edge.target_file) ?? 0) + 1,\n );\n }\n\n let frontier = new Set(scored.keys());\n\n for (let hop = 1; hop <= maxDepth && frontier.size > 0; hop++) {\n const nextFrontier = new Set<number>();\n\n for (const symbolId of frontier) {\n // Walk outgoing references (what this symbol depends on)\n const outRefs = db.getOutgoingReferences(symbolId);\n for (const ref of outRefs) {\n if (!scored.has(ref.target_id)) {\n const sym = db.getSymbolById(ref.target_id);\n if (sym) {\n const baseWeight =\n ref.ref_type === \"import\" ? config.importEdgeWeight : config.typeRefWeight;\n const deg = fileDegree.get(ref.target_file) ?? 0;\n const hubDampening =\n deg > 1 ? 1 / Math.sqrt(deg) : 1;\n const graphScore = (baseWeight / hop) * hubDampening;\n\n scored.set(ref.target_id, {\n symbol: sym,\n score: graphScore,\n matchType: \"graph\",\n depth: hop,\n });\n nextFrontier.add(ref.target_id);\n }\n }\n }\n\n // Walk incoming references (what depends on this symbol)\n const inRefs = db.getIncomingReferences(symbolId);\n for (const ref of inRefs) {\n if (!scored.has(ref.source_id)) {\n const sym = db.getSymbolById(ref.source_id);\n if (sym) {\n const baseWeight =\n ref.ref_type === \"import\" ? config.importEdgeWeight : config.typeRefWeight;\n const deg = fileDegree.get(ref.source_file) ?? 0;\n const hubDampening =\n deg > 1 ? 1 / Math.sqrt(deg) : 1;\n const graphScore = (baseWeight / hop) * hubDampening;\n\n scored.set(ref.source_id, {\n symbol: sym,\n score: graphScore,\n matchType: \"graph\",\n depth: hop,\n });\n nextFrontier.add(ref.source_id);\n }\n }\n }\n }\n\n frontier = nextFrontier;\n }\n }\n\n return Array.from(scored.values());\n}\n\n/**\n * Group scored symbols by file and compute file-level sections.\n */\nfunction groupByFile(\n scoredSymbols: ScoredSymbol[],\n pagerankScores: Map<string, number>,\n): FileSection[] {\n const fileMap = new Map<string, ScoredSymbol[]>();\n\n for (const ss of scoredSymbols) {\n const fp = ss.symbol.file_path;\n if (!fileMap.has(fp)) fileMap.set(fp, []);\n fileMap.get(fp)!.push(ss);\n }\n\n const sections: FileSection[] = [];\n\n for (const [filePath, symbols] of fileMap) {\n symbols.sort((a, b) => a.symbol.start_line - b.symbol.start_line);\n\n // File score: max symbol score, with pagerank boost only for direct matches\n // Graph-expanded files should not get pagerank boost (it undoes hub dampening)\n const maxSymScore = Math.max(...symbols.map((s) => s.score));\n const hasDirectMatch = symbols.some((s) => s.matchType !== \"graph\");\n const prScore = hasDirectMatch ? (pagerankScores.get(filePath) ?? 0) : 0;\n const fileScore = maxSymScore + prScore;\n\n // Combined line range (union of all symbol ranges)\n const startLine = Math.min(...symbols.map((s) => s.symbol.start_line));\n const endLine = Math.max(...symbols.map((s) => s.symbol.end_line));\n\n sections.push({\n filePath,\n symbols,\n score: fileScore,\n startLine,\n endLine,\n });\n }\n\n // Sort by score descending, then alphabetically\n sections.sort(\n (a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath),\n );\n\n return sections;\n}\n\n/**\n * B1: Apply relevance threshold cutoff.\n * Drop sections whose score falls below scoreFloorRatio * maxScore.\n */\nfunction applyScoreFloor(\n sections: FileSection[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): FileSection[] {\n if (sections.length === 0) return sections;\n const maxScore = sections[0].score;\n const floor = config.scoreFloorRatio * maxScore;\n return sections.filter((s) => s.score >= floor);\n}\n\n/**\n * B3: Detect the \"elbow\" in the score distribution.\n * Returns the index of the last section to keep (inclusive), or null\n * if no significant elbow is found.\n *\n * Looks for the first gap where the relative drop between consecutive\n * sorted scores exceeds elbowDropRatio. Only triggers when the score\n * below the gap is less than 30% of the max score, preventing cuts\n * between clustered high-relevance results.\n */\nfunction findElbow(\n scores: number[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): number | null {\n if (scores.length <= 1) return null;\n const maxScore = scores[0];\n for (let i = 0; i < scores.length - 1; i++) {\n const drop = (scores[i] - scores[i + 1]) / scores[i];\n if (drop > config.elbowDropRatio && scores[i + 1] < 0.3 * maxScore) {\n return i;\n }\n }\n return null;\n}\n\n/**\n * B3: Apply elbow detection to trim sections.\n * Only applies when there are 3+ sections to avoid over-trimming small\n * result sets where graph expansion may be genuinely useful.\n */\nfunction applyElbow(\n sections: FileSection[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): FileSection[] {\n if (sections.length < 3) return sections;\n const scores = sections.map((s) => s.score);\n const elbowIdx = findElbow(scores, config);\n if (elbowIdx !== null) {\n return sections.slice(0, elbowIdx + 1);\n }\n return sections;\n}\n\n/**\n * Read source lines from a file, returning the specified range.\n */\nfunction readSourceLines(\n rootDir: string,\n filePath: string,\n startLine: number,\n endLine: number,\n): string | null {\n const absPath = resolve(rootDir, filePath);\n try {\n const content = readFileSync(absPath, \"utf-8\");\n const lines = content.split(\"\\n\");\n // Lines are 1-indexed in the database\n const start = Math.max(0, startLine - 1);\n const end = Math.min(lines.length, endLine);\n return lines.slice(start, end).join(\"\\n\");\n } catch {\n return null;\n }\n}\n\n/**\n * Render a file section with header and source code.\n */\nfunction renderSection(rootDir: string, section: FileSection): string {\n const symbolNames = section.symbols\n .map((s) => s.symbol.name)\n .filter((name, i, arr) => arr.indexOf(name) === i)\n .slice(0, 5);\n\n const nameList = symbolNames.join(\", \");\n const header = `── ${section.filePath}:${section.startLine}-${section.endLine} (${nameList}) ──`;\n\n const source = readSourceLines(\n rootDir,\n section.filePath,\n section.startLine,\n section.endLine,\n );\n\n if (source === null) {\n return `${header}\\n[file not readable]`;\n }\n\n return `${header}\\n${source}`;\n}\n\n// ── core query logic (shared by CLI and MCP server) ───────────────────\n\nexport function runQueryCore(\n root: string,\n term: string,\n budget: number,\n depth: number,\n options?: { stats?: boolean; config?: ScoringConfig },\n): string {\n const dbPath = resolve(root, \".codefocus\", \"index.db\");\n\n if (!existsSync(dbPath)) {\n throw new Error(\n `no index found at ${dbPath}\\nRun 'codefocus index --root ${root}' first.`,\n );\n }\n\n const config = options?.config ?? loadScoringConfig(root);\n\n const db = new IndexDatabase(dbPath);\n try {\n // 1. Search + graph expansion\n const scoredSymbols = searchAndExpand(db, term, depth, root, config);\n\n if (scoredSymbols.length === 0) {\n return `No results found for \"${term}\"`;\n }\n\n // 2. Compute file PageRank\n const pagerankScores = computeFilePagerank(db);\n\n // 3. Group by file and rank\n const allSections = groupByFile(scoredSymbols, pagerankScores);\n\n // 4. Phase B: Apply relevance thresholds\n // B1: Score floor — drop sections below scoreFloorRatio * maxScore\n const afterFloor = applyScoreFloor(allSections, config);\n // B3: Elbow detection — find natural gap in score distribution\n const sections = applyElbow(afterFloor, config);\n\n // 5. Render with token budget enforcement + B2 marginal value\n const enc = getEncoding(\"cl100k_base\");\n const blocks: string[] = [];\n let tokenCount = 0;\n let totalSymbols = 0;\n let truncated = false;\n\n for (const section of sections) {\n const block = renderSection(root, section);\n const blockTokens = enc.encode(block).length;\n\n // B2: Marginal value per token — stop when value density is too low.\n // Only apply after the first block (always include the top result).\n if (blocks.length > 0 && blockTokens > 0) {\n const marginalValue = section.score / blockTokens;\n if (marginalValue < config.minMarginalValue) {\n break;\n }\n }\n\n if (tokenCount + blockTokens <= budget) {\n blocks.push(block);\n tokenCount += blockTokens;\n totalSymbols += section.symbols.length;\n continue;\n }\n\n // Try partial fit — include header + as many source lines as budget allows\n const lines = block.split(\"\\n\");\n let partial = lines[0];\n let partialTokens = enc.encode(partial).length;\n\n if (tokenCount + partialTokens > budget) {\n truncated = true;\n break;\n }\n\n for (let i = 1; i < lines.length; i++) {\n const candidate = partial + \"\\n\" + lines[i];\n const candidateTokens = enc.encode(candidate).length;\n if (tokenCount + candidateTokens > budget) break;\n partial = candidate;\n partialTokens = candidateTokens;\n }\n\n blocks.push(partial);\n tokenCount += partialTokens;\n totalSymbols += section.symbols.length;\n truncated = true;\n break;\n }\n\n // Compute confidence from score distribution (B3 enhancement)\n // Use the post-threshold sections for confidence calculation\n const allScores = sections.map((s) => s.score).sort((a, b) => b - a);\n const topScore = allScores[0] ?? 0;\n const medianScore =\n allScores.length > 0\n ? allScores[Math.floor(allScores.length / 2)]\n : 0;\n const hasExact = sections.some((s) =>\n s.symbols.some((ss) => ss.matchType === \"exact\"),\n );\n // B3: Enhanced confidence — factor in score concentration\n // High: exact match and scores are tightly clustered (elbow removed noise)\n // Low: no exact matches or scores are very spread out\n const scoreRange = allScores.length > 1\n ? (allScores[0] - allScores[allScores.length - 1]) / allScores[0]\n : 0;\n const confidence: \"high\" | \"medium\" | \"low\" =\n hasExact && (medianScore >= 0.3 || sections.length <= 2)\n ? \"high\"\n : !hasExact || medianScore < 0.1\n ? scoreRange < 0.3 ? \"medium\" : \"low\"\n : \"medium\";\n\n // Build output string\n const output: string[] = [];\n\n // YAML front matter\n const shown = blocks.length;\n const frontMatter = [\n \"---\",\n `query: ${term}`,\n `tokens: ${tokenCount}`,\n `files: ${shown}`,\n `symbols: ${totalSymbols}`,\n `truncated: ${truncated}`,\n `confidence: ${confidence}`,\n `top_score: ${topScore.toFixed(2)}`,\n `median_score: ${medianScore.toFixed(2)}`,\n \"---\",\n ].join(\"\\n\");\n\n output.push(frontMatter);\n output.push(\"\");\n output.push(blocks.join(\"\\n\\n\"));\n\n // --stats: verbose scoring detail\n if (options?.stats) {\n output.push(\"\");\n const totalCandidates = allSections.length;\n const afterFloorCount = afterFloor.length;\n const afterElbowCount = sections.length;\n if (totalCandidates > afterElbowCount) {\n output.push(\n `[stats] filtering: ${totalCandidates} candidates → ${afterFloorCount} after B1 floor → ${afterElbowCount} after B3 elbow`,\n );\n }\n for (const section of sections.slice(0, shown)) {\n const topSym = section.symbols.reduce(\n (best, s) => (s.score > best.score ? s : best),\n section.symbols[0],\n );\n const block = renderSection(root, section);\n const blockTokens = enc.encode(block).length;\n const marginal = blockTokens > 0\n ? (section.score / blockTokens).toFixed(6)\n : \"N/A\";\n output.push(\n `[stats] ${section.filePath.padEnd(40)} score=${section.score.toFixed(2)} match=${topSym.matchType.padEnd(8)} depth=${topSym.depth} value/tok=${marginal}`,\n );\n }\n const p25 =\n allScores.length > 0\n ? allScores[Math.floor(allScores.length * 0.75)]\n : 0;\n const minScore = allScores[allScores.length - 1] ?? 0;\n output.push(\n `[stats] score distribution: max=${topScore.toFixed(2)} median=${medianScore.toFixed(2)} p25=${p25.toFixed(2)} min=${minScore.toFixed(2)}`,\n );\n }\n\n // Compact footer for backward compatibility with benchmark parsing\n const total = sections.length;\n const parts = [\n `[query] ${shown} files, ${totalSymbols} symbols, ~${tokenCount} tokens`,\n ];\n if (truncated) {\n parts.push(`(budget: ${budget}, truncated)`);\n }\n if (total > shown) {\n parts.push(`(${total - shown} more files not shown)`);\n }\n output.push(`\\n${parts.join(\" \")}`);\n\n return output.join(\"\\n\");\n } finally {\n db.close();\n }\n}\n\n// ── command entry point ────────────────────────────────────────────────\n\nexport async function runQuery(\n positional: string[],\n flags: Record<string, string | boolean>,\n): Promise<void> {\n if (flags.help) {\n console.log(`codefocus query — Search and return ranked code context\n\nUsage: codefocus query <search-term> [options]\n\nOptions:\n --budget <tokens> Token budget for output (default: 8000)\n --depth <n> Max graph traversal depth (default: 2)\n --root <path> Root directory of indexed project (default: auto-detect)\n --stats Show verbose scoring detail per section\n --help Show this help message\n\nExamples:\n codefocus query handleSync\n codefocus query Calculator --budget 4000\n codefocus query \"runIndex\" --depth 3`);\n return;\n }\n\n const term = positional[0];\n if (!term) {\n console.error(\"Error: query requires a search term\");\n process.exitCode = 2;\n return;\n }\n\n const root = resolveRoot(flags.root);\n const config = loadScoringConfig(root);\n\n const budget = parseInt(String(flags.budget || String(config.defaultBudget)), 10);\n if (isNaN(budget) || budget <= 0) {\n console.error(\"Error: --budget must be a positive integer\");\n process.exitCode = 2;\n return;\n }\n\n const depth = parseInt(String(flags.depth || String(config.defaultDepth)), 10);\n if (isNaN(depth) || depth < 0) {\n console.error(\"Error: --depth must be a non-negative integer\");\n process.exitCode = 2;\n return;\n }\n\n const result = runQueryCore(root, term, budget, depth, {\n stats: !!flags.stats,\n config,\n });\n console.log(result);\n}\n"],"mappings":"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","names":["require"]}
|