@maxgfr/codeindex 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -2
- package/package.json +1 -1
- package/scripts/engine.d.mts +18 -3
- package/scripts/engine.mjs +248 -95
- package/src/embed/encode.ts +26 -16
- package/src/embed/endpoint.ts +110 -21
- package/src/embed/index.ts +30 -14
- package/src/embed/search.ts +9 -3
- package/src/engine-cli.ts +99 -22
- package/src/engine.ts +17 -5
- package/src/extract/code.ts +15 -3
- package/src/mcp.ts +31 -17
- package/src/types.ts +6 -3
package/scripts/engine.mjs
CHANGED
|
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.
|
|
17
|
+
ENGINE_VERSION = "2.11.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
|
-
EXTRACTOR_VERSION =
|
|
19
|
+
EXTRACTOR_VERSION = 7;
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -6340,12 +6340,14 @@ function extractImports(ext, content) {
|
|
|
6340
6340
|
}
|
|
6341
6341
|
return [...specs].map((spec) => ({ kind: "import", spec }));
|
|
6342
6342
|
}
|
|
6343
|
-
function extractReexports(rel, content) {
|
|
6343
|
+
function extractReexports(rel, content, localSymbols) {
|
|
6344
6344
|
if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
|
|
6345
6345
|
const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
|
|
6346
6346
|
const out2 = [];
|
|
6347
6347
|
const seen = /* @__PURE__ */ new Set();
|
|
6348
6348
|
const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
|
|
6349
|
+
const localKindOf = /* @__PURE__ */ new Map();
|
|
6350
|
+
for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
|
|
6349
6351
|
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
6350
6352
|
let m;
|
|
6351
6353
|
while ((m = named.exec(content)) && out2.length < 60) {
|
|
@@ -6353,12 +6355,14 @@ function extractReexports(rel, content) {
|
|
|
6353
6355
|
for (const part of m[1].split(",")) {
|
|
6354
6356
|
const p = part.trim().replace(/^type\s+/, "");
|
|
6355
6357
|
const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
|
|
6358
|
+
const orig = as ? as[1] : p;
|
|
6356
6359
|
const name2 = as ? as[2] : p;
|
|
6357
6360
|
if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
|
|
6358
6361
|
seen.add(name2);
|
|
6362
|
+
const mirroredKind = !from ? localKindOf.get(orig) : void 0;
|
|
6359
6363
|
out2.push({
|
|
6360
6364
|
name: name2,
|
|
6361
|
-
kind: "reexport",
|
|
6365
|
+
kind: mirroredKind ?? "reexport",
|
|
6362
6366
|
file: rel,
|
|
6363
6367
|
line: lineAt(m.index),
|
|
6364
6368
|
signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
|
|
@@ -6411,7 +6415,7 @@ function extractCode(rel, ext, content) {
|
|
|
6411
6415
|
const ast = extractAst(rel, ext, content);
|
|
6412
6416
|
const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
|
|
6413
6417
|
const known = new Set(symbols.map((s) => s.name));
|
|
6414
|
-
const reexports = extractReexports(rel, content).filter((s) => !known.has(s.name));
|
|
6418
|
+
const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
|
|
6415
6419
|
return {
|
|
6416
6420
|
symbols: [...symbols, ...reexports],
|
|
6417
6421
|
summary: topDocComment(content),
|
|
@@ -9447,30 +9451,34 @@ function roundHalfToEven(x) {
|
|
|
9447
9451
|
if (diff > 0.5) return f + 1;
|
|
9448
9452
|
return f % 2 === 0 ? f : f + 1;
|
|
9449
9453
|
}
|
|
9450
|
-
function
|
|
9451
|
-
const
|
|
9454
|
+
function quantize(vec) {
|
|
9455
|
+
const dim = vec.length;
|
|
9452
9456
|
const out2 = new Int8Array(dim);
|
|
9453
|
-
const ids = tokenize(text, model);
|
|
9454
|
-
if (ids.length === 0) return out2;
|
|
9455
|
-
const pooled = new Float64Array(dim);
|
|
9456
|
-
for (const id of ids) {
|
|
9457
|
-
const base = id * dim;
|
|
9458
|
-
for (let d = 0; d < dim; d++) pooled[d] += weights[base + d];
|
|
9459
|
-
}
|
|
9460
|
-
const inv = 1 / ids.length;
|
|
9461
|
-
for (let d = 0; d < dim; d++) pooled[d] *= inv;
|
|
9462
9457
|
let sumsq = 0;
|
|
9463
|
-
for (let d = 0; d < dim; d++) sumsq +=
|
|
9458
|
+
for (let d = 0; d < dim; d++) sumsq += vec[d] * vec[d];
|
|
9464
9459
|
const norm2 = Math.sqrt(sumsq);
|
|
9465
9460
|
if (norm2 === 0) return out2;
|
|
9466
9461
|
for (let d = 0; d < dim; d++) {
|
|
9467
|
-
let q = roundHalfToEven(
|
|
9462
|
+
let q = roundHalfToEven(vec[d] / norm2 * QUANT);
|
|
9468
9463
|
if (q > QUANT) q = QUANT;
|
|
9469
9464
|
else if (q < -QUANT) q = -QUANT;
|
|
9470
9465
|
out2[d] = q;
|
|
9471
9466
|
}
|
|
9472
9467
|
return out2;
|
|
9473
9468
|
}
|
|
9469
|
+
function encode(model, text) {
|
|
9470
|
+
const { dim, weights } = model;
|
|
9471
|
+
const ids = tokenize(text, model);
|
|
9472
|
+
if (ids.length === 0) return new Int8Array(dim);
|
|
9473
|
+
const pooled = new Float64Array(dim);
|
|
9474
|
+
for (const id of ids) {
|
|
9475
|
+
const base = id * dim;
|
|
9476
|
+
for (let d = 0; d < dim; d++) pooled[d] += weights[base + d];
|
|
9477
|
+
}
|
|
9478
|
+
const inv = 1 / ids.length;
|
|
9479
|
+
for (let d = 0; d < dim; d++) pooled[d] *= inv;
|
|
9480
|
+
return quantize(pooled);
|
|
9481
|
+
}
|
|
9474
9482
|
function intDot(a, b) {
|
|
9475
9483
|
const n = Math.min(a.length, b.length);
|
|
9476
9484
|
let dot = 0;
|
|
@@ -9493,8 +9501,8 @@ function symbolText(rel, name2, signature, summary) {
|
|
|
9493
9501
|
function fileText(rel, title, summary, headings) {
|
|
9494
9502
|
return [title ?? "", summary ?? "", ...headings, rel.replace(/\//g, " ")].join("\n");
|
|
9495
9503
|
}
|
|
9496
|
-
function
|
|
9497
|
-
const
|
|
9504
|
+
function embeddingUnits(scan2) {
|
|
9505
|
+
const units = [];
|
|
9498
9506
|
for (const f of scan2.files) {
|
|
9499
9507
|
const seen = /* @__PURE__ */ new Set();
|
|
9500
9508
|
let hadSymbol = false;
|
|
@@ -9502,20 +9510,22 @@ function buildEmbeddingIndex(scan2, model) {
|
|
|
9502
9510
|
if (seen.has(s.name)) continue;
|
|
9503
9511
|
seen.add(s.name);
|
|
9504
9512
|
hadSymbol = true;
|
|
9505
|
-
|
|
9506
|
-
file: f.rel,
|
|
9507
|
-
symbol: s.name,
|
|
9508
|
-
line: s.line,
|
|
9509
|
-
vec: encode(model, symbolText(f.rel, s.name, s.signature, f.summary))
|
|
9510
|
-
});
|
|
9513
|
+
units.push({ file: f.rel, symbol: s.name, line: s.line, text: symbolText(f.rel, s.name, s.signature, f.summary) });
|
|
9511
9514
|
}
|
|
9512
9515
|
if (!hadSymbol) {
|
|
9513
9516
|
const text = fileText(f.rel, f.title, f.summary, f.headings);
|
|
9514
|
-
if (text.replace(/\s+/g, "")) {
|
|
9515
|
-
records.push({ file: f.rel, vec: encode(model, text) });
|
|
9516
|
-
}
|
|
9517
|
+
if (text.replace(/\s+/g, "")) units.push({ file: f.rel, text });
|
|
9517
9518
|
}
|
|
9518
9519
|
}
|
|
9520
|
+
return units;
|
|
9521
|
+
}
|
|
9522
|
+
function buildEmbeddingIndex(scan2, model) {
|
|
9523
|
+
const records = embeddingUnits(scan2).map((u) => {
|
|
9524
|
+
const rec = { file: u.file, vec: encode(model, u.text) };
|
|
9525
|
+
if (u.symbol !== void 0) rec.symbol = u.symbol;
|
|
9526
|
+
if (u.line !== void 0) rec.line = u.line;
|
|
9527
|
+
return rec;
|
|
9528
|
+
});
|
|
9519
9529
|
return { embedVersion: EMBED_VERSION, modelId: model.modelId, dim: model.dim, records };
|
|
9520
9530
|
}
|
|
9521
9531
|
function serializeEmbeddings(index) {
|
|
@@ -9572,10 +9582,10 @@ var init_embed = __esm({
|
|
|
9572
9582
|
function searchSemantic(scan2, query, index, opts = {}) {
|
|
9573
9583
|
const limit = opts.limit ?? DEFAULT_LIMIT2;
|
|
9574
9584
|
const lexical = searchIndex(scan2, query, { limit: Math.max(limit, 50), fuzzy: opts.fuzzy });
|
|
9575
|
-
|
|
9585
|
+
const q = opts.queryVec ?? (opts.model ? encode(opts.model, query) : void 0);
|
|
9586
|
+
if (!q || !index || index.records.length === 0) {
|
|
9576
9587
|
return lexical.slice(0, limit);
|
|
9577
9588
|
}
|
|
9578
|
-
const q = encode(opts.model, query);
|
|
9579
9589
|
const bestByFile = /* @__PURE__ */ new Map();
|
|
9580
9590
|
for (const r of index.records) {
|
|
9581
9591
|
const dot = intDot(q, r.vec);
|
|
@@ -9614,6 +9624,99 @@ var init_search = __esm({
|
|
|
9614
9624
|
}
|
|
9615
9625
|
});
|
|
9616
9626
|
|
|
9627
|
+
// src/embed/endpoint.ts
|
|
9628
|
+
function resolveEmbedEndpoint(opts = {}) {
|
|
9629
|
+
const url = opts.url ?? process.env.CODEINDEX_EMBED_ENDPOINT;
|
|
9630
|
+
return url && url.trim() ? url.trim() : void 0;
|
|
9631
|
+
}
|
|
9632
|
+
function stripTrailingSlash(url) {
|
|
9633
|
+
return url.replace(/\/+$/, "");
|
|
9634
|
+
}
|
|
9635
|
+
function embedEndpointUrl(base) {
|
|
9636
|
+
const b = stripTrailingSlash(base);
|
|
9637
|
+
return b.endsWith("/embed") ? b : b + "/embed";
|
|
9638
|
+
}
|
|
9639
|
+
function healthzUrl(base) {
|
|
9640
|
+
return stripTrailingSlash(base).replace(/\/embed$/, "") + "/healthz";
|
|
9641
|
+
}
|
|
9642
|
+
function resolveTimeout(opts) {
|
|
9643
|
+
if (typeof opts.timeoutMs === "number") return opts.timeoutMs;
|
|
9644
|
+
const env = Number(process.env.CODEINDEX_EMBED_TIMEOUT_MS);
|
|
9645
|
+
return Number.isFinite(env) && env > 0 ? env : 3e4;
|
|
9646
|
+
}
|
|
9647
|
+
async function embedViaEndpoint(texts, opts = {}) {
|
|
9648
|
+
const base = resolveEmbedEndpoint(opts);
|
|
9649
|
+
if (!base) throw new Error("no embedding endpoint configured (set CODEINDEX_EMBED_ENDPOINT or pass opts.url)");
|
|
9650
|
+
const url = embedEndpointUrl(base);
|
|
9651
|
+
const controller = new AbortController();
|
|
9652
|
+
const timer = setTimeout(() => controller.abort(), resolveTimeout(opts));
|
|
9653
|
+
try {
|
|
9654
|
+
const res = await fetch(url, {
|
|
9655
|
+
method: "POST",
|
|
9656
|
+
headers: { "content-type": "application/json", ...opts.headers ?? {} },
|
|
9657
|
+
body: JSON.stringify({ texts }),
|
|
9658
|
+
signal: controller.signal
|
|
9659
|
+
});
|
|
9660
|
+
if (!res.ok) throw new Error(`embedding endpoint ${url} returned HTTP ${res.status}`);
|
|
9661
|
+
const data = await res.json();
|
|
9662
|
+
const vectors = data.vectors;
|
|
9663
|
+
if (!Array.isArray(vectors) || !vectors.every((v) => Array.isArray(v) && v.every((x) => typeof x === "number"))) {
|
|
9664
|
+
throw new Error(`embedding endpoint ${url} returned a malformed { vectors } payload`);
|
|
9665
|
+
}
|
|
9666
|
+
return vectors;
|
|
9667
|
+
} finally {
|
|
9668
|
+
clearTimeout(timer);
|
|
9669
|
+
}
|
|
9670
|
+
}
|
|
9671
|
+
async function probeEndpoint(base, opts = {}) {
|
|
9672
|
+
const controller = new AbortController();
|
|
9673
|
+
const timer = setTimeout(() => controller.abort(), resolveTimeout(opts));
|
|
9674
|
+
try {
|
|
9675
|
+
const res = await fetch(healthzUrl(base), { signal: controller.signal, headers: opts.headers });
|
|
9676
|
+
return res.ok;
|
|
9677
|
+
} catch {
|
|
9678
|
+
return false;
|
|
9679
|
+
} finally {
|
|
9680
|
+
clearTimeout(timer);
|
|
9681
|
+
}
|
|
9682
|
+
}
|
|
9683
|
+
async function encodeQueryViaEndpoint(query, opts = {}) {
|
|
9684
|
+
const [vec] = await embedViaEndpoint([query], opts);
|
|
9685
|
+
if (!vec) throw new Error("embedding endpoint returned no vector for the query");
|
|
9686
|
+
return quantize(vec);
|
|
9687
|
+
}
|
|
9688
|
+
async function buildEndpointIndex(scan2, opts = {}) {
|
|
9689
|
+
const units = embeddingUnits(scan2);
|
|
9690
|
+
const batchSize = opts.batchSize && opts.batchSize > 0 ? opts.batchSize : 64;
|
|
9691
|
+
const records = [];
|
|
9692
|
+
let dim = 0;
|
|
9693
|
+
for (let i2 = 0; i2 < units.length; i2 += batchSize) {
|
|
9694
|
+
const batch = units.slice(i2, i2 + batchSize);
|
|
9695
|
+
const vectors = await embedViaEndpoint(batch.map((u) => u.text), opts);
|
|
9696
|
+
if (vectors.length !== batch.length) {
|
|
9697
|
+
throw new Error(`embedding endpoint returned ${vectors.length} vectors for ${batch.length} texts`);
|
|
9698
|
+
}
|
|
9699
|
+
for (let j = 0; j < batch.length; j++) {
|
|
9700
|
+
const u = batch[j];
|
|
9701
|
+
const vec = quantize(vectors[j]);
|
|
9702
|
+
if (vec.length > dim) dim = vec.length;
|
|
9703
|
+
const rec = { file: u.file, vec };
|
|
9704
|
+
if (u.symbol !== void 0) rec.symbol = u.symbol;
|
|
9705
|
+
if (u.line !== void 0) rec.line = u.line;
|
|
9706
|
+
records.push(rec);
|
|
9707
|
+
}
|
|
9708
|
+
}
|
|
9709
|
+
return { embedVersion: EMBED_VERSION, modelId: "endpoint", dim, records };
|
|
9710
|
+
}
|
|
9711
|
+
var init_endpoint = __esm({
|
|
9712
|
+
"src/embed/endpoint.ts"() {
|
|
9713
|
+
"use strict";
|
|
9714
|
+
init_encode();
|
|
9715
|
+
init_model();
|
|
9716
|
+
init_embed();
|
|
9717
|
+
}
|
|
9718
|
+
});
|
|
9719
|
+
|
|
9617
9720
|
// src/rules.ts
|
|
9618
9721
|
function isEntrypointLike(rel) {
|
|
9619
9722
|
const base = rel.split("/").pop();
|
|
@@ -10009,7 +10112,7 @@ function str(v) {
|
|
|
10009
10112
|
function strArray(v) {
|
|
10010
10113
|
return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
|
|
10011
10114
|
}
|
|
10012
|
-
function callTool(name2, args2) {
|
|
10115
|
+
async function callTool(name2, args2) {
|
|
10013
10116
|
const repo = str(args2.repo);
|
|
10014
10117
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
10015
10118
|
const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
|
|
@@ -10146,6 +10249,16 @@ function callTool(name2, args2) {
|
|
|
10146
10249
|
const limit = typeof args2.limit === "number" ? args2.limit : void 0;
|
|
10147
10250
|
const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
|
|
10148
10251
|
if (args2.semantic === true) {
|
|
10252
|
+
const endpoint = resolveEmbedEndpoint();
|
|
10253
|
+
if (endpoint) {
|
|
10254
|
+
try {
|
|
10255
|
+
const index = await buildEndpointIndex(scan2);
|
|
10256
|
+
const queryVec = await encodeQueryViaEndpoint(query);
|
|
10257
|
+
return JSON.stringify(searchSemantic(scan2, query, index, { queryVec, limit, fuzzy }), null, 2);
|
|
10258
|
+
} catch {
|
|
10259
|
+
return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
|
|
10260
|
+
}
|
|
10261
|
+
}
|
|
10149
10262
|
const modelDir = resolveEmbedModelDir(repo);
|
|
10150
10263
|
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
10151
10264
|
if (model) {
|
|
@@ -10158,15 +10271,16 @@ function callTool(name2, args2) {
|
|
|
10158
10271
|
if (name2 === "embed_status") {
|
|
10159
10272
|
const modelDir = resolveEmbedModelDir(repo);
|
|
10160
10273
|
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
},
|
|
10167
|
-
null
|
|
10168
|
-
|
|
10169
|
-
);
|
|
10274
|
+
const endpoint = resolveEmbedEndpoint();
|
|
10275
|
+
const mode = endpoint ? "endpoint" : model ? "static" : "none";
|
|
10276
|
+
const status = {
|
|
10277
|
+
embedVersion: EMBED_VERSION,
|
|
10278
|
+
mode,
|
|
10279
|
+
model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
|
|
10280
|
+
endpoint: endpoint ?? null
|
|
10281
|
+
};
|
|
10282
|
+
if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
|
|
10283
|
+
return JSON.stringify(status, null, 2);
|
|
10170
10284
|
}
|
|
10171
10285
|
if (name2 === "check_rules") {
|
|
10172
10286
|
const rules = parseRules(args2.rules);
|
|
@@ -10192,9 +10306,9 @@ async function runMcpServer() {
|
|
|
10192
10306
|
continue;
|
|
10193
10307
|
}
|
|
10194
10308
|
const requests = Array.isArray(parsed) ? parsed : [parsed];
|
|
10195
|
-
for (const req of requests) handle2(req);
|
|
10309
|
+
for (const req of requests) await handle2(req);
|
|
10196
10310
|
}
|
|
10197
|
-
function handle2(req) {
|
|
10311
|
+
async function handle2(req) {
|
|
10198
10312
|
if (req.id === void 0 || req.id === null) return;
|
|
10199
10313
|
try {
|
|
10200
10314
|
if (req.method === "initialize") {
|
|
@@ -10215,7 +10329,7 @@ async function runMcpServer() {
|
|
|
10215
10329
|
const name2 = str(params.name) ?? "";
|
|
10216
10330
|
const args2 = params.arguments ?? {};
|
|
10217
10331
|
try {
|
|
10218
|
-
const text = callTool(name2, args2);
|
|
10332
|
+
const text = await callTool(name2, args2);
|
|
10219
10333
|
send({ id: req.id, result: { content: [{ type: "text", text }] } });
|
|
10220
10334
|
} catch (e) {
|
|
10221
10335
|
send({
|
|
@@ -10257,6 +10371,7 @@ var init_mcp = __esm({
|
|
|
10257
10371
|
init_model();
|
|
10258
10372
|
init_embed();
|
|
10259
10373
|
init_search();
|
|
10374
|
+
init_endpoint();
|
|
10260
10375
|
repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
|
|
10261
10376
|
scopeProps = {
|
|
10262
10377
|
scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
|
|
@@ -10483,7 +10598,7 @@ var init_mcp = __esm({
|
|
|
10483
10598
|
},
|
|
10484
10599
|
semantic: {
|
|
10485
10600
|
type: "boolean",
|
|
10486
|
-
description: "RRF-fuse
|
|
10601
|
+
description: "RRF-fuse an embedding tier with lexical (default false). Precedence: the HTTP endpoint (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model. Degrades silently to lexical-only when neither is available/reachable \u2014 see embed_status."
|
|
10487
10602
|
}
|
|
10488
10603
|
},
|
|
10489
10604
|
required: ["repo", "query"]
|
|
@@ -10491,7 +10606,7 @@ var init_mcp = __esm({
|
|
|
10491
10606
|
},
|
|
10492
10607
|
{
|
|
10493
10608
|
name: "embed_status",
|
|
10494
|
-
description: "Report the
|
|
10609
|
+
description: "Report the embedding tier: the effective mode (none/static/endpoint; endpoint > static model), the resolved model (opt-in, never shipped in the package) with its modelId/dim, EMBED_VERSION, and the configured HTTP endpoint with its reachability. Use to check whether `search` with semantic:true will fuse embeddings or degrade to lexical.",
|
|
10495
10610
|
inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] }
|
|
10496
10611
|
},
|
|
10497
10612
|
{
|
|
@@ -10912,37 +11027,7 @@ init_model();
|
|
|
10912
11027
|
init_encode();
|
|
10913
11028
|
init_embed();
|
|
10914
11029
|
init_search();
|
|
10915
|
-
|
|
10916
|
-
// src/embed/endpoint.ts
|
|
10917
|
-
function resolveEmbedEndpoint(opts = {}) {
|
|
10918
|
-
const url = opts.url ?? process.env.CODEINDEX_EMBED_ENDPOINT;
|
|
10919
|
-
return url && url.trim() ? url.trim() : void 0;
|
|
10920
|
-
}
|
|
10921
|
-
async function embedViaEndpoint(texts, opts = {}) {
|
|
10922
|
-
const url = resolveEmbedEndpoint(opts);
|
|
10923
|
-
if (!url) throw new Error("no embedding endpoint configured (set CODEINDEX_EMBED_ENDPOINT or pass opts.url)");
|
|
10924
|
-
const controller = new AbortController();
|
|
10925
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 3e4);
|
|
10926
|
-
try {
|
|
10927
|
-
const res = await fetch(url, {
|
|
10928
|
-
method: "POST",
|
|
10929
|
-
headers: { "content-type": "application/json", ...opts.headers ?? {} },
|
|
10930
|
-
body: JSON.stringify({ texts }),
|
|
10931
|
-
signal: controller.signal
|
|
10932
|
-
});
|
|
10933
|
-
if (!res.ok) throw new Error(`embedding endpoint ${url} returned HTTP ${res.status}`);
|
|
10934
|
-
const data = await res.json();
|
|
10935
|
-
const vectors = data.vectors;
|
|
10936
|
-
if (!Array.isArray(vectors) || !vectors.every((v) => Array.isArray(v) && v.every((x) => typeof x === "number"))) {
|
|
10937
|
-
throw new Error(`embedding endpoint ${url} returned a malformed { vectors } payload`);
|
|
10938
|
-
}
|
|
10939
|
-
return vectors;
|
|
10940
|
-
} finally {
|
|
10941
|
-
clearTimeout(timer);
|
|
10942
|
-
}
|
|
10943
|
-
}
|
|
10944
|
-
|
|
10945
|
-
// src/engine.ts
|
|
11030
|
+
init_endpoint();
|
|
10946
11031
|
init_rules();
|
|
10947
11032
|
init_coupling();
|
|
10948
11033
|
init_repomap();
|
|
@@ -10978,6 +11063,8 @@ init_rules();
|
|
|
10978
11063
|
init_model();
|
|
10979
11064
|
init_embed();
|
|
10980
11065
|
init_search();
|
|
11066
|
+
init_endpoint();
|
|
11067
|
+
init_util();
|
|
10981
11068
|
var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
|
|
10982
11069
|
|
|
10983
11070
|
Usage: engine.mjs <command> [flags]
|
|
@@ -10996,13 +11083,17 @@ Commands:
|
|
|
10996
11083
|
grep Search: cli.mjs grep <pattern> --repo <dir> (JSON hits)
|
|
10997
11084
|
search Keyless BM25 lexical search over symbol names, path segments,
|
|
10998
11085
|
markdown headings and summaries: cli.mjs search "<query>" --repo <dir>.
|
|
10999
|
-
--semantic fuses in
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
embed
|
|
11086
|
+
--semantic fuses in an embedding tier (RRF) \u2014 the HTTP endpoint
|
|
11087
|
+
(CODEINDEX_EMBED_ENDPOINT) if set, else a local static model;
|
|
11088
|
+
degrades to lexical (exit 0) when neither is available/reachable
|
|
11089
|
+
embed Embedding tiers (opt-in). Precedence: endpoint > static model:
|
|
11090
|
+
embed status Effective mode (none/static/endpoint), model +
|
|
11091
|
+
EMBED_VERSION, and endpoint reachability (JSON)
|
|
11092
|
+
embed build Write embeddings.bin into --out <dir> (static tier)
|
|
11004
11093
|
embed pull Fetch the model asset into CODEINDEX_EMBED_DIR (or
|
|
11005
11094
|
<repo>/.codeindex/models/) \u2014 needs CODEINDEX_EMBED_URL
|
|
11095
|
+
embed serve Print (or --run) the docker command that starts the
|
|
11096
|
+
containerized embedding server (rich tier)
|
|
11006
11097
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
11007
11098
|
against the link-graph: --config <codeindex.rules.json>; exits 1
|
|
11008
11099
|
on any error-severity violation (a CI gate)
|
|
@@ -11030,8 +11121,10 @@ Flags:
|
|
|
11030
11121
|
--limit <n> Max results for \`search\` (default 20)
|
|
11031
11122
|
--no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
|
|
11032
11123
|
with zero document frequency (default: enabled)
|
|
11033
|
-
--semantic \`search\`: RRF-fuse
|
|
11034
|
-
|
|
11124
|
+
--semantic \`search\`: RRF-fuse an embedding tier with lexical \u2014 the
|
|
11125
|
+
HTTP endpoint if CODEINDEX_EMBED_ENDPOINT is set, else a
|
|
11126
|
+
local static model (lexical-only when neither is available)
|
|
11127
|
+
--run \`embed serve\`: run the docker command instead of printing it
|
|
11035
11128
|
--recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
|
|
11036
11129
|
the JS/TS import gate to unique repo-wide names and labels
|
|
11037
11130
|
each site corroborated|unique-name
|
|
@@ -11072,6 +11165,7 @@ function parseFlags(args2) {
|
|
|
11072
11165
|
else if (a === "--no-fuzzy") flags2.fuzzy = false;
|
|
11073
11166
|
else if (a === "--semantic") flags2.semantic = true;
|
|
11074
11167
|
else if (a === "--recall") flags2.recall = true;
|
|
11168
|
+
else if (a === "--run") flags2.run = true;
|
|
11075
11169
|
else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
|
|
11076
11170
|
else throw new Error(`unknown flag: ${a}`);
|
|
11077
11171
|
}
|
|
@@ -11182,18 +11276,37 @@ async function runCli(argv) {
|
|
|
11182
11276
|
if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
|
|
11183
11277
|
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
11184
11278
|
if (flags2.semantic) {
|
|
11185
|
-
const
|
|
11186
|
-
const
|
|
11187
|
-
if (!model) {
|
|
11188
|
-
process.stderr.write(
|
|
11189
|
-
"codeindex: semantic search unavailable (no embedding model present) \u2014 returning lexical results; run `codeindex embed pull` to enable it\n"
|
|
11190
|
-
);
|
|
11279
|
+
const endpoint = resolveEmbedEndpoint();
|
|
11280
|
+
const lexical = () => {
|
|
11191
11281
|
const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
11192
11282
|
emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
|
|
11283
|
+
};
|
|
11284
|
+
if (endpoint) {
|
|
11285
|
+
try {
|
|
11286
|
+
const index = await buildEndpointIndex(scan2);
|
|
11287
|
+
const queryVec = await encodeQueryViaEndpoint(flags2.positional);
|
|
11288
|
+
const results = searchSemantic(scan2, flags2.positional, index, { queryVec, limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
11289
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
|
|
11290
|
+
} catch (e) {
|
|
11291
|
+
process.stderr.write(
|
|
11292
|
+
`codeindex: embedding endpoint ${endpoint} unavailable (${e instanceof Error ? e.message : e}) \u2014 returning lexical results
|
|
11293
|
+
`
|
|
11294
|
+
);
|
|
11295
|
+
lexical();
|
|
11296
|
+
}
|
|
11193
11297
|
} else {
|
|
11194
|
-
const
|
|
11195
|
-
const
|
|
11196
|
-
|
|
11298
|
+
const modelDir = resolveEmbedModelDir(flags2.repo);
|
|
11299
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11300
|
+
if (!model) {
|
|
11301
|
+
process.stderr.write(
|
|
11302
|
+
"codeindex: semantic search unavailable (no embedding model or endpoint) \u2014 returning lexical results; run `codeindex embed pull` or set CODEINDEX_EMBED_ENDPOINT to enable it\n"
|
|
11303
|
+
);
|
|
11304
|
+
lexical();
|
|
11305
|
+
} else {
|
|
11306
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
11307
|
+
const results = searchSemantic(scan2, flags2.positional, index, { model, limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
11308
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
|
|
11309
|
+
}
|
|
11197
11310
|
}
|
|
11198
11311
|
} else {
|
|
11199
11312
|
const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
@@ -11204,12 +11317,45 @@ async function runCli(argv) {
|
|
|
11204
11317
|
const modelDir = resolveEmbedModelDir(flags2.repo);
|
|
11205
11318
|
if (sub === "status") {
|
|
11206
11319
|
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11320
|
+
const endpoint = resolveEmbedEndpoint();
|
|
11321
|
+
const mode = endpoint ? "endpoint" : model ? "static" : "none";
|
|
11207
11322
|
const status = {
|
|
11208
11323
|
embedVersion: EMBED_VERSION,
|
|
11324
|
+
mode,
|
|
11209
11325
|
model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
|
|
11210
|
-
endpoint:
|
|
11326
|
+
endpoint: endpoint ?? null
|
|
11211
11327
|
};
|
|
11328
|
+
if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
|
|
11212
11329
|
emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
|
|
11330
|
+
} else if (sub === "serve") {
|
|
11331
|
+
const dockerArgs = ["run", "-d", "-p", "8756:8756", "ghcr.io/maxgfr/codeindex-embed:latest"];
|
|
11332
|
+
const oneLiner = `docker ${dockerArgs.join(" ")}`;
|
|
11333
|
+
if (!have("docker")) {
|
|
11334
|
+
process.stderr.write(
|
|
11335
|
+
"codeindex: docker not found on PATH. Install Docker, then run:\n " + oneLiner + "\n"
|
|
11336
|
+
);
|
|
11337
|
+
process.exitCode = 1;
|
|
11338
|
+
return;
|
|
11339
|
+
}
|
|
11340
|
+
if (flags2.run) {
|
|
11341
|
+
process.stderr.write(`codeindex: starting embedding server \u2192 ${oneLiner}
|
|
11342
|
+
`);
|
|
11343
|
+
const res = sh("docker", dockerArgs);
|
|
11344
|
+
if (res.stdout.trim()) process.stdout.write(res.stdout.trim() + "\n");
|
|
11345
|
+
if (!res.ok) {
|
|
11346
|
+
process.stderr.write(res.stderr || "codeindex: docker run failed\n");
|
|
11347
|
+
process.exitCode = 1;
|
|
11348
|
+
return;
|
|
11349
|
+
}
|
|
11350
|
+
process.stderr.write(
|
|
11351
|
+
'codeindex: server starting on http://localhost:8756 \u2014 then:\n CODEINDEX_EMBED_ENDPOINT=http://localhost:8756 codeindex search "<query>" --repo . --semantic\n'
|
|
11352
|
+
);
|
|
11353
|
+
} else {
|
|
11354
|
+
process.stdout.write(oneLiner + "\n");
|
|
11355
|
+
process.stderr.write(
|
|
11356
|
+
'codeindex: run the line above to start the embedding server (or `embed serve --run`), then:\n CODEINDEX_EMBED_ENDPOINT=http://localhost:8756 codeindex search "<query>" --repo . --semantic\n'
|
|
11357
|
+
);
|
|
11358
|
+
}
|
|
11213
11359
|
} else if (sub === "build") {
|
|
11214
11360
|
if (!flags2.out) throw new Error("embed build needs --out <dir>");
|
|
11215
11361
|
if (!modelDir) {
|
|
@@ -11256,7 +11402,7 @@ async function runCli(argv) {
|
|
|
11256
11402
|
process.stderr.write(`codeindex: model written to ${join12(destDir, "model.json")}
|
|
11257
11403
|
`);
|
|
11258
11404
|
} else {
|
|
11259
|
-
throw new Error("embed needs a subcommand:
|
|
11405
|
+
throw new Error("embed needs a subcommand: status | build | pull | serve");
|
|
11260
11406
|
}
|
|
11261
11407
|
} else if (cmd === "rules") {
|
|
11262
11408
|
if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
@@ -11332,6 +11478,7 @@ export {
|
|
|
11332
11478
|
betweennessOf,
|
|
11333
11479
|
buildCallerIndex,
|
|
11334
11480
|
buildEmbeddingIndex,
|
|
11481
|
+
buildEndpointIndex,
|
|
11335
11482
|
buildGraph,
|
|
11336
11483
|
buildIndexArtifacts,
|
|
11337
11484
|
buildModules,
|
|
@@ -11359,9 +11506,12 @@ export {
|
|
|
11359
11506
|
detectWorkspaces,
|
|
11360
11507
|
diffFiles,
|
|
11361
11508
|
diffHunks,
|
|
11509
|
+
embedEndpointUrl,
|
|
11362
11510
|
embedViaEndpoint,
|
|
11511
|
+
embeddingUnits,
|
|
11363
11512
|
enclosingSymbol,
|
|
11364
11513
|
encode,
|
|
11514
|
+
encodeQueryViaEndpoint,
|
|
11365
11515
|
ensureGrammars,
|
|
11366
11516
|
escapeRegExp,
|
|
11367
11517
|
extToLang,
|
|
@@ -11380,6 +11530,7 @@ export {
|
|
|
11380
11530
|
hasEmbedModel,
|
|
11381
11531
|
have,
|
|
11382
11532
|
headCommit,
|
|
11533
|
+
healthzUrl,
|
|
11383
11534
|
insertAfterSymbol,
|
|
11384
11535
|
insertBeforeSymbol,
|
|
11385
11536
|
intDot,
|
|
@@ -11397,6 +11548,8 @@ export {
|
|
|
11397
11548
|
pagerankOf,
|
|
11398
11549
|
parseGitignore,
|
|
11399
11550
|
parseRules,
|
|
11551
|
+
probeEndpoint,
|
|
11552
|
+
quantize,
|
|
11400
11553
|
rankHotspots,
|
|
11401
11554
|
rankedKeywords,
|
|
11402
11555
|
readMemory,
|
package/src/embed/encode.ts
CHANGED
|
@@ -87,14 +87,36 @@ export function roundHalfToEven(x: number): number {
|
|
|
87
87
|
return f % 2 === 0 ? f : f + 1; // exactly .5 → nearest even
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// L2-normalize a float vector and quantize it to int8 at the fixed 1/127 scale
|
|
91
|
+
// (round-half-to-even, clamp to [-127,127]) — the SHARED tail of both tiers. The
|
|
92
|
+
// static encoder feeds its mean-pooled vector here; the endpoint tier feeds each
|
|
93
|
+
// float embedding it receives. Because the quantization is byte-identical, a
|
|
94
|
+
// given float vector maps to the same int8 bytes and the integer-dot ranking is
|
|
95
|
+
// consistent across tiers. A zero-norm input yields an all-zero vector (never
|
|
96
|
+
// NaN — it simply ranks last).
|
|
97
|
+
export function quantize(vec: ArrayLike<number>): Int8Array {
|
|
98
|
+
const dim = vec.length;
|
|
99
|
+
const out = new Int8Array(dim);
|
|
100
|
+
let sumsq = 0;
|
|
101
|
+
for (let d = 0; d < dim; d++) sumsq += vec[d]! * vec[d]!;
|
|
102
|
+
const norm = Math.sqrt(sumsq);
|
|
103
|
+
if (norm === 0) return out; // zero vector — nothing to rank on
|
|
104
|
+
for (let d = 0; d < dim; d++) {
|
|
105
|
+
let q = roundHalfToEven((vec[d]! / norm) * QUANT);
|
|
106
|
+
if (q > QUANT) q = QUANT;
|
|
107
|
+
else if (q < -QUANT) q = -QUANT;
|
|
108
|
+
out[d] = q;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
90
113
|
// Encode a text to an int8 unit-ish vector. Empty / all-OOV / zero-norm inputs
|
|
91
114
|
// yield an all-zero vector (dot-products against it are 0 — it simply ranks
|
|
92
115
|
// last), never NaN. Pure and deterministic: same model + text → same bytes.
|
|
93
116
|
export function encode(model: StaticEmbedModel, text: string): Int8Array {
|
|
94
117
|
const { dim, weights } = model;
|
|
95
|
-
const out = new Int8Array(dim);
|
|
96
118
|
const ids = tokenize(text, model);
|
|
97
|
-
if (ids.length === 0) return
|
|
119
|
+
if (ids.length === 0) return new Int8Array(dim);
|
|
98
120
|
|
|
99
121
|
// mean-pool in double precision, fixed order.
|
|
100
122
|
const pooled = new Float64Array(dim);
|
|
@@ -105,20 +127,8 @@ export function encode(model: StaticEmbedModel, text: string): Int8Array {
|
|
|
105
127
|
const inv = 1 / ids.length;
|
|
106
128
|
for (let d = 0; d < dim; d++) pooled[d]! *= inv;
|
|
107
129
|
|
|
108
|
-
// L2-normalize.
|
|
109
|
-
|
|
110
|
-
for (let d = 0; d < dim; d++) sumsq += pooled[d]! * pooled[d]!;
|
|
111
|
-
const norm = Math.sqrt(sumsq);
|
|
112
|
-
if (norm === 0) return out; // zero vector — nothing to rank on
|
|
113
|
-
|
|
114
|
-
// quantize to int8 at the fixed 1/127 scale, round-half-to-even, clamp.
|
|
115
|
-
for (let d = 0; d < dim; d++) {
|
|
116
|
-
let q = roundHalfToEven((pooled[d]! / norm) * QUANT);
|
|
117
|
-
if (q > QUANT) q = QUANT;
|
|
118
|
-
else if (q < -QUANT) q = -QUANT;
|
|
119
|
-
out[d] = q;
|
|
120
|
-
}
|
|
121
|
-
return out;
|
|
130
|
+
// L2-normalize + int8-quantize (the shared tail, identical to the endpoint tier).
|
|
131
|
+
return quantize(pooled);
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
// Integer dot product of two int8 vectors (the ranking primitive). Widened to a
|