@vohongtho.infotech/code-intel 1.0.1 → 1.0.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 +34 -4
- package/dist/cli/hook.js +451 -0
- package/dist/cli/hook.js.map +1 -0
- package/dist/cli/main.js +3254 -904
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/router.js +35 -0
- package/dist/cli/router.js.map +1 -0
- package/dist/cli/search.js +244 -0
- package/dist/cli/search.js.map +1 -0
- package/dist/index.d.ts +29 -2
- package/dist/index.js +675 -232
- package/dist/index.js.map +1 -1
- package/dist/web/assets/es-DOPp2DTx.js +10 -0
- package/dist/web/assets/index-BPmJG_ti.css +2 -0
- package/dist/web/assets/index-gqp6A4LM.js +354 -0
- package/dist/web/index.html +2 -2
- package/package.json +8 -6
- package/dist/web/assets/es-Bu8iwdFw.js +0 -10
- package/dist/web/assets/index-C9M6YLlS.css +0 -2
- package/dist/web/assets/index-CKc3HEpe.js +0 -354
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
|
|
6
|
+
// src/cli/router.ts
|
|
7
|
+
var __dir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
var SLIM_DISPATCH = {
|
|
9
|
+
search: join(__dir, "search.js")
|
|
10
|
+
};
|
|
11
|
+
var cmd = process.argv[2];
|
|
12
|
+
var slimBin = cmd ? SLIM_DISPATCH[cmd] : void 0;
|
|
13
|
+
if (slimBin) {
|
|
14
|
+
const child = spawn(
|
|
15
|
+
process.execPath,
|
|
16
|
+
[slimBin, ...process.argv.slice(3)],
|
|
17
|
+
{ stdio: "inherit", env: process.env }
|
|
18
|
+
);
|
|
19
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
20
|
+
child.on("error", () => {
|
|
21
|
+
const main = join(__dir, "main.js");
|
|
22
|
+
const fb = spawn(process.execPath, [main, ...process.argv.slice(2)], { stdio: "inherit", env: process.env });
|
|
23
|
+
fb.on("close", (c) => process.exit(c ?? 0));
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
const main = join(__dir, "main.js");
|
|
27
|
+
const child = spawn(process.execPath, [main, ...process.argv.slice(2)], { stdio: "inherit", env: process.env });
|
|
28
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
29
|
+
child.on("error", (err) => {
|
|
30
|
+
console.error(err.message);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=router.js.map
|
|
35
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/router.ts"],"names":[],"mappings":";;;;;AAcA,IAAM,KAAA,GAAQ,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AAOpD,IAAM,aAAA,GAAkD;AAAA,EACtD,MAAA,EAAQ,IAAA,CAAK,KAAA,EAAO,WAAW;AACjC,CAAA;AAEA,IAAM,GAAA,GAAM,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA;AAC1B,IAAM,OAAA,GAAU,GAAA,GAAM,aAAA,CAAc,GAAG,CAAA,GAAI,MAAA;AAE3C,IAAI,OAAA,EAAS;AAGX,EAAA,MAAM,KAAA,GAAQ,KAAA;AAAA,IACZ,OAAA,CAAQ,QAAA;AAAA,IACR,CAAC,OAAA,EAAS,GAAG,QAAQ,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,IAClC,EAAE,KAAA,EAAO,SAAA,EAAW,GAAA,EAAK,QAAQ,GAAA;AAAI,GACvC;AACA,EAAA,KAAA,CAAM,EAAA,CAAG,SAAS,CAAC,IAAA,KAAS,QAAQ,IAAA,CAAK,IAAA,IAAQ,CAAC,CAAC,CAAA;AACnD,EAAA,KAAA,CAAM,EAAA,CAAG,SAAS,MAAM;AAEtB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,EAAO,SAAS,CAAA;AAClC,IAAA,MAAM,KAAK,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAC,IAAA,EAAM,GAAG,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAC,GAAG,EAAE,KAAA,EAAO,WAAW,GAAA,EAAK,OAAA,CAAQ,KAAK,CAAA;AAC3G,IAAA,EAAA,CAAG,EAAA,CAAG,SAAS,CAAC,CAAA,KAAM,QAAQ,IAAA,CAAK,CAAA,IAAK,CAAC,CAAC,CAAA;AAAA,EAC5C,CAAC,CAAA;AACH,CAAA,MAAO;AAEL,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,EAAO,SAAS,CAAA;AAClC,EAAA,MAAM,QAAQ,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAC,IAAA,EAAM,GAAG,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAC,GAAG,EAAE,KAAA,EAAO,WAAW,GAAA,EAAK,OAAA,CAAQ,KAAK,CAAA;AAC9G,EAAA,KAAA,CAAM,EAAA,CAAG,SAAS,CAAC,IAAA,KAAS,QAAQ,IAAA,CAAK,IAAA,IAAQ,CAAC,CAAC,CAAA;AACnD,EAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAQ;AAAE,IAAA,OAAA,CAAQ,KAAA,CAAM,IAAI,OAAO,CAAA;AAAG,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAAG,CAAC,CAAA;AAC7E","file":"router.js","sourcesContent":["/**\n * router.ts — Tiny entry point for the `code-intel` binary.\n *\n * Startup cost of THIS file: ~50 ms (no heavy imports).\n *\n * Fast commands (have a dedicated slim bundle) are dispatched by spawning\n * node with the slim binary — avoiding parsing the 800 KB main.js.\n *\n * All other commands fall through to main.js via dynamic import.\n */\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\nimport { spawn } from 'node:child_process';\n\nconst __dir = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Commands that have a dedicated slim bundle.\n * Key = argv[2] (sub-command name).\n * Value = path to the slim dist file relative to this router's directory.\n */\nconst SLIM_DISPATCH: Readonly<Record<string, string>> = {\n search: join(__dir, 'search.js'),\n};\n\nconst cmd = process.argv[2];\nconst slimBin = cmd ? SLIM_DISPATCH[cmd] : undefined;\n\nif (slimBin) {\n // Dispatch to slim binary.\n // Strip the sub-command token (argv[2]) — slim binary parses from argv[2] onward.\n const child = spawn(\n process.execPath,\n [slimBin, ...process.argv.slice(3)],\n { stdio: 'inherit', env: process.env },\n );\n child.on('close', (code) => process.exit(code ?? 0));\n child.on('error', () => {\n // Slim binary failed to launch — fall through to main.js\n const main = join(__dir, 'main.js');\n const fb = spawn(process.execPath, [main, ...process.argv.slice(2)], { stdio: 'inherit', env: process.env });\n fb.on('close', (c) => process.exit(c ?? 0));\n });\n} else {\n // All other commands: spawn main.js (dynamic import would cause esbuild to bundle it).\n const main = join(__dir, 'main.js');\n const child = spawn(process.execPath, [main, ...process.argv.slice(2)], { stdio: 'inherit', env: process.env });\n child.on('close', (code) => process.exit(code ?? 0));\n child.on('error', (err) => { console.error(err.message); process.exit(1); });\n}\n"]}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
|
|
7
|
+
// src/cli/search.ts
|
|
8
|
+
var K1 = 1.2;
|
|
9
|
+
var B = 0.75;
|
|
10
|
+
function tokenize(text) {
|
|
11
|
+
return text.toLowerCase().split(/[\s\-_./\\:(){}[\]<>,"'`~!@#$%^&*+=|;?]+/).filter((t) => t.length >= 2 && t.length <= 64);
|
|
12
|
+
}
|
|
13
|
+
function heapTopK(scores, k) {
|
|
14
|
+
if (k <= 0) return [];
|
|
15
|
+
const heap = [];
|
|
16
|
+
function heapifyUp(i) {
|
|
17
|
+
while (i > 0) {
|
|
18
|
+
const parent = i - 1 >> 1;
|
|
19
|
+
if (heap[parent][1] > heap[i][1]) {
|
|
20
|
+
[heap[parent], heap[i]] = [heap[i], heap[parent]];
|
|
21
|
+
i = parent;
|
|
22
|
+
} else break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function heapifyDown(i) {
|
|
26
|
+
const n = heap.length;
|
|
27
|
+
while (true) {
|
|
28
|
+
let s = i;
|
|
29
|
+
const l = 2 * i + 1, r = 2 * i + 2;
|
|
30
|
+
if (l < n && heap[l][1] < heap[s][1]) s = l;
|
|
31
|
+
if (r < n && heap[r][1] < heap[s][1]) s = r;
|
|
32
|
+
if (s === i) break;
|
|
33
|
+
[heap[s], heap[i]] = [heap[i], heap[s]];
|
|
34
|
+
i = s;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const [nodeId, score] of scores) {
|
|
38
|
+
if (heap.length < k) {
|
|
39
|
+
heap.push([nodeId, score]);
|
|
40
|
+
heapifyUp(heap.length - 1);
|
|
41
|
+
} else if (score > heap[0][1]) {
|
|
42
|
+
heap[0] = [nodeId, score];
|
|
43
|
+
heapifyDown(0);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return heap.sort((a, b) => b[1] - a[1]);
|
|
47
|
+
}
|
|
48
|
+
function getBm25DbPath(workspaceRoot) {
|
|
49
|
+
return path.join(workspaceRoot, ".code-intel", "bm25.db");
|
|
50
|
+
}
|
|
51
|
+
var Bm25Searcher = class {
|
|
52
|
+
constructor(dbPath) {
|
|
53
|
+
this.dbPath = dbPath;
|
|
54
|
+
}
|
|
55
|
+
dbPath;
|
|
56
|
+
invertedIndex = /* @__PURE__ */ new Map();
|
|
57
|
+
docLengths = /* @__PURE__ */ new Map();
|
|
58
|
+
nodeMeta = /* @__PURE__ */ new Map();
|
|
59
|
+
avgdl = 1;
|
|
60
|
+
docCount = 0;
|
|
61
|
+
_loaded = false;
|
|
62
|
+
get isLoaded() {
|
|
63
|
+
return this._loaded;
|
|
64
|
+
}
|
|
65
|
+
load() {
|
|
66
|
+
if (!fs.existsSync(this.dbPath)) return;
|
|
67
|
+
const db = new Database(this.dbPath, { readonly: true });
|
|
68
|
+
try {
|
|
69
|
+
const getMeta = db.prepare("SELECT value FROM bm25_meta WHERE key = ?");
|
|
70
|
+
this.avgdl = parseFloat(getMeta.get("avgdl")?.value ?? "1");
|
|
71
|
+
this.docCount = parseInt(getMeta.get("docCount")?.value ?? "0", 10);
|
|
72
|
+
const postingRows = db.prepare("SELECT term, postings FROM bm25_index").all();
|
|
73
|
+
for (const row of postingRows) {
|
|
74
|
+
this.invertedIndex.set(row.term, JSON.parse(row.postings));
|
|
75
|
+
}
|
|
76
|
+
const dlRows = db.prepare("SELECT node_id, doclen FROM bm25_doclen").all();
|
|
77
|
+
for (const row of dlRows) {
|
|
78
|
+
this.docLengths.set(row.node_id, row.doclen);
|
|
79
|
+
}
|
|
80
|
+
const metaRows = db.prepare("SELECT node_id, name, kind, file_path, snippet FROM bm25_nodemeta").all();
|
|
81
|
+
for (const row of metaRows) {
|
|
82
|
+
this.nodeMeta.set(row.node_id, {
|
|
83
|
+
name: row.name,
|
|
84
|
+
kind: row.kind,
|
|
85
|
+
filePath: row.file_path,
|
|
86
|
+
snippet: row.snippet ?? void 0
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
this._loaded = true;
|
|
90
|
+
} finally {
|
|
91
|
+
db.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
search(query, limit) {
|
|
95
|
+
if (!this._loaded || this.invertedIndex.size === 0) return [];
|
|
96
|
+
const queryTerms = [...new Set(tokenize(query))];
|
|
97
|
+
if (queryTerms.length === 0) return [];
|
|
98
|
+
const scores = /* @__PURE__ */ new Map();
|
|
99
|
+
const N = this.docCount;
|
|
100
|
+
const avgdl = this.avgdl;
|
|
101
|
+
for (const term of queryTerms) {
|
|
102
|
+
const postings = this.invertedIndex.get(term);
|
|
103
|
+
if (!postings) continue;
|
|
104
|
+
const df = postings.length;
|
|
105
|
+
if (N > 100 && df / N > 0.6) continue;
|
|
106
|
+
const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
|
|
107
|
+
for (const { nodeId, tf } of postings) {
|
|
108
|
+
const dl = this.docLengths.get(nodeId) ?? avgdl;
|
|
109
|
+
const score = idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (dl / avgdl)));
|
|
110
|
+
scores.set(nodeId, (scores.get(nodeId) ?? 0) + score);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (scores.size === 0) return [];
|
|
114
|
+
return heapTopK(scores, limit).map(([nodeId, score]) => {
|
|
115
|
+
const meta = this.nodeMeta.get(nodeId);
|
|
116
|
+
return {
|
|
117
|
+
nodeId,
|
|
118
|
+
name: meta?.name ?? nodeId,
|
|
119
|
+
kind: meta?.kind ?? "unknown",
|
|
120
|
+
filePath: meta?.filePath ?? "",
|
|
121
|
+
score,
|
|
122
|
+
snippet: meta?.snippet
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/search/reranker.ts
|
|
129
|
+
function tokenizeForRerank(text) {
|
|
130
|
+
return text.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s\-_./:(){}[\]<>,"'`~!@#$%^&*+=|;?\\]+/).filter((t) => t.length >= 2);
|
|
131
|
+
}
|
|
132
|
+
var DEFAULT_KIND_WEIGHTS = {
|
|
133
|
+
class: 1.2,
|
|
134
|
+
interface: 1.15,
|
|
135
|
+
function: 1.1,
|
|
136
|
+
method: 1.08,
|
|
137
|
+
type_alias: 1.03,
|
|
138
|
+
enum: 1.02,
|
|
139
|
+
constant: 0.98,
|
|
140
|
+
variable: 0.9,
|
|
141
|
+
file: 0.85
|
|
142
|
+
};
|
|
143
|
+
var PATH_MULTIPLIER_TEST = 0.4;
|
|
144
|
+
var PATH_MULTIPLIER_DIST = 0.25;
|
|
145
|
+
function rerank(query, results, options = {}) {
|
|
146
|
+
if (results.length === 0) return results;
|
|
147
|
+
const {
|
|
148
|
+
nameWeight = 0.4,
|
|
149
|
+
snippetWeight = 0.25,
|
|
150
|
+
kindWeights = {}
|
|
151
|
+
} = options;
|
|
152
|
+
const effectiveKindWeights = {
|
|
153
|
+
...DEFAULT_KIND_WEIGHTS,
|
|
154
|
+
...kindWeights
|
|
155
|
+
};
|
|
156
|
+
const queryTerms = [...new Set(tokenizeForRerank(query))];
|
|
157
|
+
if (queryTerms.length === 0) return results.slice();
|
|
158
|
+
const queryLower = query.toLowerCase();
|
|
159
|
+
const scored = results.map((r) => {
|
|
160
|
+
let bonus = 0;
|
|
161
|
+
const nameLower = r.name.toLowerCase();
|
|
162
|
+
const nameTerms = tokenizeForRerank(r.name);
|
|
163
|
+
if (nameLower === queryLower) {
|
|
164
|
+
bonus += nameWeight;
|
|
165
|
+
} else if (nameLower.startsWith(queryLower)) {
|
|
166
|
+
bonus += nameWeight * 0.75;
|
|
167
|
+
} else if (queryLower.includes(nameLower) && nameLower.length >= 3) {
|
|
168
|
+
bonus += nameWeight * 0.45;
|
|
169
|
+
} else {
|
|
170
|
+
const matchCount = queryTerms.filter((t) => nameTerms.includes(t)).length;
|
|
171
|
+
if (matchCount > 0) {
|
|
172
|
+
const overlap = matchCount / queryTerms.length;
|
|
173
|
+
bonus += nameWeight * overlap * 0.6;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (r.snippet && r.snippet.length > 0) {
|
|
177
|
+
const snippetLower = r.snippet.toLowerCase();
|
|
178
|
+
const hitCount = queryTerms.filter((t) => snippetLower.includes(t)).length;
|
|
179
|
+
bonus += snippetWeight * (hitCount / queryTerms.length);
|
|
180
|
+
}
|
|
181
|
+
const kw = effectiveKindWeights[r.kind] ?? 1;
|
|
182
|
+
const fp = r.filePath;
|
|
183
|
+
const fpNorm = "/" + fp;
|
|
184
|
+
const isTestPath = fpNorm.includes("/test/") || fpNorm.includes("/tests/") || fpNorm.includes("/spec/") || fpNorm.includes("/__tests__/") || fp.includes(".test.") || fp.includes(".spec.");
|
|
185
|
+
const isDistPath = fpNorm.includes("/dist/") || fpNorm.includes("/build/") || fp.endsWith(".d.ts") || fpNorm.includes("/node_modules/");
|
|
186
|
+
const pathMul = isDistPath ? PATH_MULTIPLIER_DIST : isTestPath ? PATH_MULTIPLIER_TEST : 1;
|
|
187
|
+
const clampedBonus = Math.max(0, Math.min(nameWeight + snippetWeight, bonus));
|
|
188
|
+
const finalScore = r.score * (1 + clampedBonus) * kw * pathMul;
|
|
189
|
+
return { result: r, finalScore };
|
|
190
|
+
});
|
|
191
|
+
return scored.sort((a, b) => b.finalScore - a.finalScore).map(({ result, finalScore }) => ({ ...result, score: finalScore }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/cli/search.ts
|
|
195
|
+
var program = new Command();
|
|
196
|
+
program.name("code-intel search").description("Search the knowledge graph for symbols matching a query").argument("<query>", "Search query (name, kind, or partial match)").option("-l, --limit <n>", "Maximum number of results", "20").option("-p, --path <path>", "Path to the repository (default: current directory)", ".").option("--no-rerank", "Disable post-retrieval re-ranking (show raw BM25 order)").addHelpText("after", `
|
|
197
|
+
Runs BM25 text search across all indexed symbols \u2014 functions, classes,
|
|
198
|
+
files, routes, interfaces, and more. Results are re-ranked by default
|
|
199
|
+
using name-affinity, snippet coverage, symbol kind, and path quality.
|
|
200
|
+
|
|
201
|
+
Examples:
|
|
202
|
+
$ code-intel search "handleRequest"
|
|
203
|
+
$ code-intel search "auth" --limit 10
|
|
204
|
+
$ code-intel search "UserService" --path ./backend
|
|
205
|
+
$ code-intel search "auth" --no-rerank # raw BM25 order for comparison
|
|
206
|
+
`).action((query, opts) => {
|
|
207
|
+
const limitN = parseInt(opts.limit, 10);
|
|
208
|
+
const workspaceRoot = path.resolve(opts.path);
|
|
209
|
+
const rerankDisabled = opts.rerank === false;
|
|
210
|
+
const bm25DbPath = getBm25DbPath(workspaceRoot);
|
|
211
|
+
if (!fs.existsSync(bm25DbPath)) {
|
|
212
|
+
console.error(`
|
|
213
|
+
No search index found. Run: code-intel analyze
|
|
214
|
+
`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
const idx = new Bm25Searcher(bm25DbPath);
|
|
218
|
+
idx.load();
|
|
219
|
+
if (!idx.isLoaded) {
|
|
220
|
+
console.error(`
|
|
221
|
+
Search index could not be loaded. Run: code-intel analyze
|
|
222
|
+
`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const candidates = idx.search(query, limitN * 3);
|
|
226
|
+
const results = rerankDisabled ? candidates.slice(0, limitN) : rerank(query, candidates).slice(0, limitN);
|
|
227
|
+
if (results.length === 0) {
|
|
228
|
+
console.log(`
|
|
229
|
+
No results found for "${query}".
|
|
230
|
+
`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const label = rerankDisabled ? "bm25 (re-ranking off)" : "bm25 (re-ranked)";
|
|
234
|
+
console.log(`
|
|
235
|
+
${results.length} result(s) for "${query}" [${label}]:
|
|
236
|
+
`);
|
|
237
|
+
for (const r of results) {
|
|
238
|
+
console.log(` ${r.kind.padEnd(14)} ${r.name.padEnd(32)} ${r.filePath}`);
|
|
239
|
+
}
|
|
240
|
+
console.log("");
|
|
241
|
+
});
|
|
242
|
+
program.parse();
|
|
243
|
+
//# sourceMappingURL=search.js.map
|
|
244
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/search/bm25-searcher.ts","../../src/search/reranker.ts","../../src/cli/search.ts"],"names":["path","fs"],"mappings":"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","file":"search.js","sourcesContent":["/**\n * bm25-searcher.ts — Minimal read-only BM25 search for the slim CLI binary.\n *\n * Intentionally has NO dependency on Logger, winston, or OTel.\n * Used ONLY by the `search.ts` slim entry; all other code uses the full\n * `Bm25Index` class from bm25-index.ts which includes build/load/update and logging.\n *\n * Dependencies: better-sqlite3, node:fs, node:path (all fast to load).\n */\n\nimport Database from 'better-sqlite3';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport type { SearchResult } from './text-search.js';\n\n// ── BM25 hyperparameters ──────────────────────────────────────────────────────\nconst K1 = 1.2;\nconst B = 0.75;\n\n// ── Internal types ─────────────────────────────────────────────────────────────\ninterface PostingEntry { nodeId: string; tf: number }\ninterface NodeMeta { name: string; kind: string; filePath: string; snippet?: string }\n\n// ── Tokenizer ─────────────────────────────────────────────────────────────────\nfunction tokenize(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[\\s\\-_./\\\\:(){}[\\]<>,\"'`~!@#$%^&*+=|;?]+/)\n .filter((t) => t.length >= 2 && t.length <= 64);\n}\n\n// ── Min-heap top-K ─────────────────────────────────────────────────────────────\nfunction heapTopK(scores: Map<string, number>, k: number): [string, number][] {\n if (k <= 0) return [];\n const heap: [string, number][] = [];\n\n function heapifyUp(i: number) {\n while (i > 0) {\n const parent = (i - 1) >> 1;\n if (heap[parent]![1] > heap[i]![1]) {\n [heap[parent], heap[i]] = [heap[i]!, heap[parent]!];\n i = parent;\n } else break;\n }\n }\n function heapifyDown(i: number) {\n const n = heap.length;\n while (true) {\n let s = i;\n const l = 2 * i + 1, r = 2 * i + 2;\n if (l < n && heap[l]![1] < heap[s]![1]) s = l;\n if (r < n && heap[r]![1] < heap[s]![1]) s = r;\n if (s === i) break;\n [heap[s], heap[i]] = [heap[i]!, heap[s]!];\n i = s;\n }\n }\n\n for (const [nodeId, score] of scores) {\n if (heap.length < k) {\n heap.push([nodeId, score]);\n heapifyUp(heap.length - 1);\n } else if (score > heap[0]![1]) {\n heap[0] = [nodeId, score];\n heapifyDown(0);\n }\n }\n return heap.sort((a, b) => b[1] - a[1]);\n}\n\n// ── Path helper ────────────────────────────────────────────────────────────────\nexport function getBm25DbPath(workspaceRoot: string): string {\n return path.join(workspaceRoot, '.code-intel', 'bm25.db');\n}\n\n// ── Slim searcher ──────────────────────────────────────────────────────────────\n\nexport class Bm25Searcher {\n private readonly invertedIndex = new Map<string, PostingEntry[]>();\n private readonly docLengths = new Map<string, number>();\n private readonly nodeMeta = new Map<string, NodeMeta>();\n private avgdl = 1;\n private docCount = 0;\n private _loaded = false;\n\n constructor(private readonly dbPath: string) {}\n\n get isLoaded(): boolean { return this._loaded; }\n\n load(): void {\n if (!fs.existsSync(this.dbPath)) return;\n\n const db = new Database(this.dbPath, { readonly: true });\n try {\n const getMeta = db.prepare('SELECT value FROM bm25_meta WHERE key = ?');\n this.avgdl = parseFloat((getMeta.get('avgdl') as { value: string } | undefined)?.value ?? '1');\n this.docCount = parseInt ((getMeta.get('docCount') as { value: string } | undefined)?.value ?? '0', 10);\n\n const postingRows = db.prepare('SELECT term, postings FROM bm25_index').all() as { term: string; postings: string }[];\n for (const row of postingRows) {\n this.invertedIndex.set(row.term, JSON.parse(row.postings) as PostingEntry[]);\n }\n\n const dlRows = db.prepare('SELECT node_id, doclen FROM bm25_doclen').all() as { node_id: string; doclen: number }[];\n for (const row of dlRows) {\n this.docLengths.set(row.node_id, row.doclen);\n }\n\n const metaRows = db.prepare('SELECT node_id, name, kind, file_path, snippet FROM bm25_nodemeta').all() as {\n node_id: string; name: string; kind: string; file_path: string; snippet: string | null;\n }[];\n for (const row of metaRows) {\n this.nodeMeta.set(row.node_id, {\n name: row.name,\n kind: row.kind,\n filePath: row.file_path,\n snippet: row.snippet ?? undefined,\n });\n }\n\n this._loaded = true;\n } finally {\n db.close();\n }\n }\n\n search(query: string, limit: number): SearchResult[] {\n if (!this._loaded || this.invertedIndex.size === 0) return [];\n\n const queryTerms = [...new Set(tokenize(query))];\n if (queryTerms.length === 0) return [];\n\n const scores = new Map<string, number>();\n const N = this.docCount;\n const avgdl = this.avgdl;\n\n for (const term of queryTerms) {\n const postings = this.invertedIndex.get(term);\n if (!postings) continue;\n\n const df = postings.length;\n if (N > 100 && df / N > 0.6) continue;\n\n const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);\n\n for (const { nodeId, tf } of postings) {\n const dl = this.docLengths.get(nodeId) ?? avgdl;\n const score = idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (dl / avgdl)));\n scores.set(nodeId, (scores.get(nodeId) ?? 0) + score);\n }\n }\n\n if (scores.size === 0) return [];\n\n return heapTopK(scores, limit).map(([nodeId, score]) => {\n const meta = this.nodeMeta.get(nodeId);\n return {\n nodeId,\n name: meta?.name ?? nodeId,\n kind: meta?.kind ?? 'unknown',\n filePath: meta?.filePath ?? '',\n score,\n snippet: meta?.snippet,\n };\n });\n }\n}\n","/**\n * reranker.ts — Lightweight feature-based re-ranker for code search results.\n *\n * Applied AFTER initial retrieval (BM25 / vector / hybrid) on the top-K candidate set.\n * Uses signals that are too expensive to compute across all N nodes but cheap on top-K.\n *\n * Signals:\n * 1. Name-query affinity — exact / prefix / camelCase token overlap\n * 2. Snippet term coverage — how many query terms appear in the code snippet\n * 3. Kind preference — classes/functions ranked above constants/variables\n * 4. Path quality — test/dist/build paths penalised via a hard multiplier\n *\n * Final score = score × nameKindSnippetMultiplier × pathMultiplier\n *\n * The path multiplier is a HARD factor (not additive bonus) so that test/dist\n * files never beat source files regardless of their raw BM25 score.\n */\n\n// ── Tokenizer ─────────────────────────────────────────────────────────────────\n\n/**\n * Tokenize a string with camelCase and snake_case awareness.\n *\n * \"UserService\" → [\"user\", \"service\"]\n * \"hashPassword\" → [\"hash\", \"password\"]\n * \"user_service\" → [\"user\", \"service\"]\n * \"XMLParser\" → [\"xml\", \"parser\"]\n */\nexport function tokenizeForRerank(text: string): string[] {\n return text\n // Split uppercase acronyms before a capitalized word: \"XMLParser\" → \"XML Parser\"\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n // Split camelCase: \"camelCase\" → \"camel Case\"\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .split(/[\\s\\-_./:(){}[\\]<>,\"'`~!@#$%^&*+=|;?\\\\]+/)\n .filter((t) => t.length >= 2);\n}\n\n// ── Kind weights ──────────────────────────────────────────────────────────────\n\n/**\n * Per-kind preference multiplier.\n * Applied as: score × kindWeight (after name/snippet bonuses).\n * Values > 1.0 = boost; < 1.0 = suppress.\n */\nexport const DEFAULT_KIND_WEIGHTS: Readonly<Record<string, number>> = {\n class: 1.20,\n interface: 1.15,\n function: 1.10,\n method: 1.08,\n type_alias: 1.03,\n enum: 1.02,\n constant: 0.98,\n variable: 0.90,\n file: 0.85,\n};\n\n// ── Path quality multipliers ──────────────────────────────────────────────────\n\n/** Hard multipliers applied to path category — these override bonuses from other signals. */\nconst PATH_MULTIPLIER_TEST = 0.40; // test/spec files always rank well below source\nconst PATH_MULTIPLIER_DIST = 0.25; // dist/build/.d.ts files nearly always irrelevant\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport interface RerankOptions {\n /**\n * Weight applied to the name-query affinity signal (additive bonus in [0, nameWeight]).\n * Higher = name similarity matters more. Default: 0.4\n */\n nameWeight?: number;\n /**\n * Weight applied to snippet term coverage (additive bonus in [0, snippetWeight]).\n * Higher = results with more query terms in their snippet are boosted more. Default: 0.25\n */\n snippetWeight?: number;\n /**\n * Per-kind multiplier overrides. Merged with DEFAULT_KIND_WEIGHTS.\n * Pass an empty object to use defaults unchanged.\n */\n kindWeights?: Partial<Record<string, number>>;\n}\n\n/** Minimum shape required for re-ranking — a strict subset of HybridSearchResult. */\nexport interface RerankableResult {\n nodeId: string;\n name: string;\n kind: string;\n filePath: string;\n score: number;\n snippet?: string;\n}\n\n/**\n * Re-rank a set of search results using lightweight feature signals.\n *\n * @param query Original search query string.\n * @param results Candidates from BM25/vector/hybrid retrieval, sorted by retrieval score.\n * @param options Tuning knobs — all optional, safe defaults are provided.\n * @returns New array sorted by re-rank score (descending). Input array is NOT mutated.\n */\nexport function rerank<T extends RerankableResult>(\n query: string,\n results: T[],\n options: RerankOptions = {},\n): T[] {\n if (results.length === 0) return results;\n\n const {\n nameWeight = 0.4,\n snippetWeight = 0.25,\n kindWeights = {},\n } = options;\n\n const effectiveKindWeights: Record<string, number> = {\n ...(DEFAULT_KIND_WEIGHTS as Record<string, number>),\n ...(kindWeights as Record<string, number>),\n };\n\n const queryTerms = [...new Set(tokenizeForRerank(query))];\n // If query tokenizes to nothing (e.g. single character), return as-is\n if (queryTerms.length === 0) return results.slice();\n\n const queryLower = query.toLowerCase();\n\n const scored: { result: T; finalScore: number }[] = results.map((r) => {\n let bonus = 0;\n\n // ── Signal 1: Name-query affinity ──────────────────────────────────────\n const nameLower = r.name.toLowerCase();\n const nameTerms = tokenizeForRerank(r.name);\n\n if (nameLower === queryLower) {\n // Exact match: maximum name bonus\n bonus += nameWeight;\n } else if (nameLower.startsWith(queryLower)) {\n // Symbol name starts with the query (e.g. query \"auth\" → \"authenticate\")\n bonus += nameWeight * 0.75;\n } else if (queryLower.includes(nameLower) && nameLower.length >= 3) {\n // Query contains the symbol name (e.g. query \"user service\" → \"user\")\n bonus += nameWeight * 0.45;\n } else {\n // Token-level overlap — handles camelCase / multi-word queries.\n // e.g. query \"user service\" → terms [\"user\",\"service\"] match \"UserService\"\n const matchCount = queryTerms.filter((t) => nameTerms.includes(t)).length;\n if (matchCount > 0) {\n const overlap = matchCount / queryTerms.length;\n bonus += nameWeight * overlap * 0.6;\n }\n }\n\n // ── Signal 2: Snippet term coverage ────────────────────────────────────\n if (r.snippet && r.snippet.length > 0) {\n const snippetLower = r.snippet.toLowerCase();\n const hitCount = queryTerms.filter((t) => snippetLower.includes(t)).length;\n bonus += snippetWeight * (hitCount / queryTerms.length);\n }\n\n // ── Signal 3: Kind preference (multiplicative) ───────────────────────\n const kw = effectiveKindWeights[r.kind] ?? 1.0;\n\n // ── Signal 4: Path quality (hard multiplier) ─────────────────────────\n // Normalise path so both 'tests/foo' and '/tests/foo' match the same patterns.\n const fp = r.filePath;\n const fpNorm = '/' + fp;\n const isTestPath =\n fpNorm.includes('/test/') || fpNorm.includes('/tests/') ||\n fpNorm.includes('/spec/') || fpNorm.includes('/__tests__/') ||\n fp.includes('.test.') || fp.includes('.spec.');\n const isDistPath =\n fpNorm.includes('/dist/') || fpNorm.includes('/build/') ||\n fp.endsWith('.d.ts') || fpNorm.includes('/node_modules/');\n\n const pathMul = isDistPath ? PATH_MULTIPLIER_DIST\n : isTestPath ? PATH_MULTIPLIER_TEST\n : 1.0;\n\n // Clamp additive bonus to [0, nameWeight + snippetWeight]\n const clampedBonus = Math.max(0, Math.min(nameWeight + snippetWeight, bonus));\n\n const finalScore = r.score * (1 + clampedBonus) * kw * pathMul;\n\n return { result: r, finalScore };\n });\n\n return scored\n .sort((a, b) => b.finalScore - a.finalScore)\n .map(({ result, finalScore }) => ({ ...result, score: finalScore }));\n}\n","/**\n * search.ts — Slim standalone entry for `code-intel search`.\n *\n * Bundle target: ~30 KB vs 800 KB for main.js → fast startup.\n *\n * Dependencies: commander, better-sqlite3, bm25-searcher, reranker.\n * No winston, no OTel, no graph, no pipeline, no HTTP, no auth.\n *\n * This file is invoked by dist/cli/router.js with argv starting at the query:\n * node search.js <query> [--limit N] [--path P] [--no-rerank]\n * (\"search\" has already been consumed by the router.)\n */\n\nimport { Command } from 'commander';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { Bm25Searcher, getBm25DbPath } from '../search/bm25-searcher.js';\nimport { rerank } from '../search/reranker.js';\nimport type { SearchResult } from '../search/text-search.js';\n\nconst program = new Command();\n\nprogram\n .name('code-intel search')\n .description('Search the knowledge graph for symbols matching a query')\n .argument('<query>', 'Search query (name, kind, or partial match)')\n .option('-l, --limit <n>', 'Maximum number of results', '20')\n .option('-p, --path <path>', 'Path to the repository (default: current directory)', '.')\n .option('--no-rerank', 'Disable post-retrieval re-ranking (show raw BM25 order)')\n .addHelpText('after', `\n Runs BM25 text search across all indexed symbols — functions, classes,\n files, routes, interfaces, and more. Results are re-ranked by default\n using name-affinity, snippet coverage, symbol kind, and path quality.\n\n Examples:\n $ code-intel search \"handleRequest\"\n $ code-intel search \"auth\" --limit 10\n $ code-intel search \"UserService\" --path ./backend\n $ code-intel search \"auth\" --no-rerank # raw BM25 order for comparison\n`)\n .action((query: string, opts: { limit: string; path: string; rerank: boolean }) => {\n const limitN = parseInt(opts.limit, 10);\n const workspaceRoot = path.resolve(opts.path);\n const rerankDisabled = opts.rerank === false;\n\n const bm25DbPath = getBm25DbPath(workspaceRoot);\n\n if (!fs.existsSync(bm25DbPath)) {\n console.error(`\\n No search index found. Run: code-intel analyze\\n`);\n process.exit(1);\n }\n\n const idx = new Bm25Searcher(bm25DbPath);\n idx.load();\n\n if (!idx.isLoaded) {\n console.error(`\\n Search index could not be loaded. Run: code-intel analyze\\n`);\n process.exit(1);\n }\n\n // Fetch 3× limit so re-ranker has room to reorder\n const candidates: SearchResult[] = idx.search(query, limitN * 3);\n const results = rerankDisabled\n ? candidates.slice(0, limitN)\n : rerank(query, candidates).slice(0, limitN);\n\n if (results.length === 0) {\n console.log(`\\n No results found for \"${query}\".\\n`);\n return;\n }\n\n const label = rerankDisabled ? 'bm25 (re-ranking off)' : 'bm25 (re-ranked)';\n console.log(`\\n ${results.length} result(s) for \"${query}\" [${label}]:\\n`);\n for (const r of results) {\n console.log(` ${r.kind.padEnd(14)} ${r.name.padEnd(32)} ${r.filePath}`);\n }\n console.log('');\n });\n\n// argv starts at the query argument (router already consumed \"search\")\nprogram.parse();\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -202,11 +202,36 @@ declare function detectOverrides(graph: KnowledgeGraph): CodeEdge[];
|
|
|
202
202
|
|
|
203
203
|
interface LLMConfig {
|
|
204
204
|
/** Which provider to use. Default: 'ollama'. */
|
|
205
|
-
provider?: 'openai' | 'anthropic' | 'ollama';
|
|
205
|
+
provider?: 'openai' | 'anthropic' | 'ollama' | 'custom';
|
|
206
206
|
/** Model name / ID passed to the provider. Each provider has its own default. */
|
|
207
207
|
model?: string;
|
|
208
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* For 'custom' provider: the base URL of the OpenAI-compatible API.
|
|
210
|
+
* e.g. 'http://localhost:1234/v1' (LM Studio), 'https://api.groq.com/openai/v1', etc.
|
|
211
|
+
*/
|
|
212
|
+
baseUrl?: string;
|
|
213
|
+
/**
|
|
214
|
+
* API key / token for the provider.
|
|
215
|
+
* For 'custom': passed as Bearer token. For 'openai': falls back to $OPENAI_API_KEY.
|
|
216
|
+
* For 'ollama': not needed.
|
|
217
|
+
*/
|
|
218
|
+
apiKey?: string;
|
|
219
|
+
/**
|
|
220
|
+
* Request mode for the summarize phase.
|
|
221
|
+
* - 'per-node' (default): one API request per symbol — works with all providers.
|
|
222
|
+
* - 'batch': bundle all symbols in a batch into ONE API request and parse the
|
|
223
|
+
* JSON array response. Use this with premium-per-request providers (e.g.
|
|
224
|
+
* copilot-api) to minimise the number of API calls.
|
|
225
|
+
*/
|
|
226
|
+
requestMode?: 'per-node' | 'batch';
|
|
227
|
+
/** Max concurrent LLM calls per batch (only used in 'per-node' mode). Default: 5. */
|
|
209
228
|
batchSize?: number;
|
|
229
|
+
/**
|
|
230
|
+
* Context window size (tokens) of the model.
|
|
231
|
+
* Used to calculate how many symbols can be packed into a single batch request.
|
|
232
|
+
* Default: 8192 (conservative for local models).
|
|
233
|
+
*/
|
|
234
|
+
contextWindow?: number;
|
|
210
235
|
/**
|
|
211
236
|
* Cost guard: stop after summarising this many nodes per run.
|
|
212
237
|
* Undefined = no limit.
|
|
@@ -228,6 +253,8 @@ interface PipelineContext {
|
|
|
228
253
|
workspaceRoot: string;
|
|
229
254
|
graph: KnowledgeGraph;
|
|
230
255
|
filePaths: string[];
|
|
256
|
+
/** Path to the KùzuDB database for this workspace (used by summarize phase to load prior summaries). */
|
|
257
|
+
dbPath?: string;
|
|
231
258
|
/** Shared file content cache — populated by parse phase, reused by resolve phase (eliminates double I/O) */
|
|
232
259
|
fileCache?: Map<string, string>;
|
|
233
260
|
/** Per-file sorted symbol index for O(1) enclosing-function lookup — built by parse phase */
|