@maxgfr/codeindex 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/docs/MIGRATION.md +29 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +57 -2
- package/scripts/engine.mjs +474 -23
- package/src/embed/encode.ts +133 -0
- package/src/embed/endpoint.ts +55 -0
- package/src/embed/index.ts +125 -0
- package/src/embed/model.ts +117 -0
- package/src/embed/search.ts +90 -0
- package/src/engine-cli.ts +107 -5
- package/src/engine.ts +21 -0
- package/src/mcp.ts +43 -6
- package/src/types.ts +1 -1
package/scripts/engine.mjs
CHANGED
|
@@ -14,7 +14,7 @@ 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.10.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
19
|
EXTRACTOR_VERSION = 6;
|
|
20
20
|
}
|
|
@@ -9343,6 +9343,277 @@ var init_bm25 = __esm({
|
|
|
9343
9343
|
}
|
|
9344
9344
|
});
|
|
9345
9345
|
|
|
9346
|
+
// src/embed/model.ts
|
|
9347
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
9348
|
+
import { join as join10 } from "path";
|
|
9349
|
+
function resolveEmbedModelDir(repo) {
|
|
9350
|
+
const env = process.env.CODEINDEX_EMBED_DIR;
|
|
9351
|
+
const candidates = [];
|
|
9352
|
+
if (env) candidates.push(env);
|
|
9353
|
+
if (repo) candidates.push(join10(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
|
|
9354
|
+
candidates.push(join10(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
|
|
9355
|
+
for (const c2 of candidates) {
|
|
9356
|
+
if (existsSync3(join10(c2, "model.json"))) return c2;
|
|
9357
|
+
}
|
|
9358
|
+
return void 0;
|
|
9359
|
+
}
|
|
9360
|
+
function hasEmbedModel(repo) {
|
|
9361
|
+
return resolveEmbedModelDir(repo) !== void 0;
|
|
9362
|
+
}
|
|
9363
|
+
function loadEmbedModel(dir) {
|
|
9364
|
+
if (!dir) return void 0;
|
|
9365
|
+
const path = join10(dir, "model.json");
|
|
9366
|
+
if (!existsSync3(path)) return void 0;
|
|
9367
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
9368
|
+
const { modelId, dim, vocab, weights } = raw;
|
|
9369
|
+
if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${path}`);
|
|
9370
|
+
if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${path}`);
|
|
9371
|
+
if (!Array.isArray(vocab) || !Array.isArray(weights) || vocab.length !== weights.length) {
|
|
9372
|
+
throw new Error(`embed model: vocab/weights length mismatch in ${path}`);
|
|
9373
|
+
}
|
|
9374
|
+
const vocabSize = vocab.length;
|
|
9375
|
+
const flat = new Float64Array(vocabSize * dim);
|
|
9376
|
+
const vmap = /* @__PURE__ */ new Map();
|
|
9377
|
+
for (let i2 = 0; i2 < vocabSize; i2++) {
|
|
9378
|
+
const tok = vocab[i2];
|
|
9379
|
+
if (typeof tok !== "string") throw new Error(`embed model: non-string vocab entry at ${i2}`);
|
|
9380
|
+
if (!vmap.has(tok)) vmap.set(tok, i2);
|
|
9381
|
+
const row = weights[i2];
|
|
9382
|
+
if (!Array.isArray(row) || row.length !== dim) {
|
|
9383
|
+
throw new Error(`embed model: row ${i2} has length ${row?.length}, expected ${dim}`);
|
|
9384
|
+
}
|
|
9385
|
+
for (let d = 0; d < dim; d++) flat[i2 * dim + d] = Number(row[d]);
|
|
9386
|
+
}
|
|
9387
|
+
const unk = typeof raw.unk === "string" ? raw.unk : "[UNK]";
|
|
9388
|
+
const unkId = vmap.has(unk) ? vmap.get(unk) : -1;
|
|
9389
|
+
return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
|
|
9390
|
+
}
|
|
9391
|
+
function resolveEmbedPullUrl() {
|
|
9392
|
+
const url = process.env.CODEINDEX_EMBED_URL;
|
|
9393
|
+
return url && url.trim() ? url.trim() : void 0;
|
|
9394
|
+
}
|
|
9395
|
+
var EMBED_VERSION, DEFAULT_EMBED_DIRNAME;
|
|
9396
|
+
var init_model = __esm({
|
|
9397
|
+
"src/embed/model.ts"() {
|
|
9398
|
+
"use strict";
|
|
9399
|
+
EMBED_VERSION = 1;
|
|
9400
|
+
DEFAULT_EMBED_DIRNAME = "models";
|
|
9401
|
+
}
|
|
9402
|
+
});
|
|
9403
|
+
|
|
9404
|
+
// src/embed/encode.ts
|
|
9405
|
+
function basicTokenize(text) {
|
|
9406
|
+
const spaced = foldText(text).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
9407
|
+
const out2 = [];
|
|
9408
|
+
for (const part of spaced.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
9409
|
+
if (part) out2.push(part);
|
|
9410
|
+
}
|
|
9411
|
+
return out2;
|
|
9412
|
+
}
|
|
9413
|
+
function wordpiece(word, model) {
|
|
9414
|
+
if (!word) return [];
|
|
9415
|
+
const ids = [];
|
|
9416
|
+
let start2 = 0;
|
|
9417
|
+
const n = word.length;
|
|
9418
|
+
while (start2 < n) {
|
|
9419
|
+
let end = n;
|
|
9420
|
+
let match = -1;
|
|
9421
|
+
while (end > start2) {
|
|
9422
|
+
const piece = start2 === 0 ? word.slice(start2, end) : "##" + word.slice(start2, end);
|
|
9423
|
+
const id = model.vocab.get(piece);
|
|
9424
|
+
if (id !== void 0) {
|
|
9425
|
+
match = id;
|
|
9426
|
+
break;
|
|
9427
|
+
}
|
|
9428
|
+
end--;
|
|
9429
|
+
}
|
|
9430
|
+
if (match === -1) return model.unkId >= 0 ? [model.unkId] : [];
|
|
9431
|
+
ids.push(match);
|
|
9432
|
+
start2 = end;
|
|
9433
|
+
}
|
|
9434
|
+
return ids;
|
|
9435
|
+
}
|
|
9436
|
+
function tokenize(text, model) {
|
|
9437
|
+
const ids = [];
|
|
9438
|
+
for (const word of basicTokenize(text)) {
|
|
9439
|
+
for (const id of wordpiece(word, model)) ids.push(id);
|
|
9440
|
+
}
|
|
9441
|
+
return ids;
|
|
9442
|
+
}
|
|
9443
|
+
function roundHalfToEven(x) {
|
|
9444
|
+
const f = Math.floor(x);
|
|
9445
|
+
const diff = x - f;
|
|
9446
|
+
if (diff < 0.5) return f;
|
|
9447
|
+
if (diff > 0.5) return f + 1;
|
|
9448
|
+
return f % 2 === 0 ? f : f + 1;
|
|
9449
|
+
}
|
|
9450
|
+
function encode(model, text) {
|
|
9451
|
+
const { dim, weights } = model;
|
|
9452
|
+
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
|
+
let sumsq = 0;
|
|
9463
|
+
for (let d = 0; d < dim; d++) sumsq += pooled[d] * pooled[d];
|
|
9464
|
+
const norm2 = Math.sqrt(sumsq);
|
|
9465
|
+
if (norm2 === 0) return out2;
|
|
9466
|
+
for (let d = 0; d < dim; d++) {
|
|
9467
|
+
let q = roundHalfToEven(pooled[d] / norm2 * QUANT);
|
|
9468
|
+
if (q > QUANT) q = QUANT;
|
|
9469
|
+
else if (q < -QUANT) q = -QUANT;
|
|
9470
|
+
out2[d] = q;
|
|
9471
|
+
}
|
|
9472
|
+
return out2;
|
|
9473
|
+
}
|
|
9474
|
+
function intDot(a, b) {
|
|
9475
|
+
const n = Math.min(a.length, b.length);
|
|
9476
|
+
let dot = 0;
|
|
9477
|
+
for (let i2 = 0; i2 < n; i2++) dot += a[i2] * b[i2];
|
|
9478
|
+
return dot;
|
|
9479
|
+
}
|
|
9480
|
+
var QUANT;
|
|
9481
|
+
var init_encode = __esm({
|
|
9482
|
+
"src/embed/encode.ts"() {
|
|
9483
|
+
"use strict";
|
|
9484
|
+
init_util();
|
|
9485
|
+
QUANT = 127;
|
|
9486
|
+
}
|
|
9487
|
+
});
|
|
9488
|
+
|
|
9489
|
+
// src/embed/index.ts
|
|
9490
|
+
function symbolText(rel, name2, signature, summary) {
|
|
9491
|
+
return [name2, signature ?? "", summary ?? "", rel.replace(/\//g, " ")].join("\n");
|
|
9492
|
+
}
|
|
9493
|
+
function fileText(rel, title, summary, headings) {
|
|
9494
|
+
return [title ?? "", summary ?? "", ...headings, rel.replace(/\//g, " ")].join("\n");
|
|
9495
|
+
}
|
|
9496
|
+
function buildEmbeddingIndex(scan2, model) {
|
|
9497
|
+
const records = [];
|
|
9498
|
+
for (const f of scan2.files) {
|
|
9499
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9500
|
+
let hadSymbol = false;
|
|
9501
|
+
for (const s of f.symbols) {
|
|
9502
|
+
if (seen.has(s.name)) continue;
|
|
9503
|
+
seen.add(s.name);
|
|
9504
|
+
hadSymbol = true;
|
|
9505
|
+
records.push({
|
|
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
|
+
});
|
|
9511
|
+
}
|
|
9512
|
+
if (!hadSymbol) {
|
|
9513
|
+
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
|
+
}
|
|
9518
|
+
}
|
|
9519
|
+
return { embedVersion: EMBED_VERSION, modelId: model.modelId, dim: model.dim, records };
|
|
9520
|
+
}
|
|
9521
|
+
function serializeEmbeddings(index) {
|
|
9522
|
+
const header = JSON.stringify({
|
|
9523
|
+
embedVersion: index.embedVersion,
|
|
9524
|
+
modelId: index.modelId,
|
|
9525
|
+
dim: index.dim,
|
|
9526
|
+
count: index.records.length,
|
|
9527
|
+
records: index.records.map((r) => ({ file: r.file, symbol: r.symbol ?? "", line: r.line ?? 0 }))
|
|
9528
|
+
});
|
|
9529
|
+
const headerBuf = Buffer.from(header, "utf8");
|
|
9530
|
+
const body2 = Buffer.alloc(index.records.length * index.dim);
|
|
9531
|
+
let off = 0;
|
|
9532
|
+
for (const r of index.records) {
|
|
9533
|
+
for (let d = 0; d < index.dim; d++) body2.writeInt8(r.vec[d] ?? 0, off++);
|
|
9534
|
+
}
|
|
9535
|
+
const out2 = Buffer.alloc(8 + headerBuf.length + body2.length);
|
|
9536
|
+
out2.write(MAGIC, 0, "ascii");
|
|
9537
|
+
out2.writeUInt32LE(headerBuf.length, 4);
|
|
9538
|
+
headerBuf.copy(out2, 8);
|
|
9539
|
+
body2.copy(out2, 8 + headerBuf.length);
|
|
9540
|
+
return out2;
|
|
9541
|
+
}
|
|
9542
|
+
function deserializeEmbeddings(bytes) {
|
|
9543
|
+
const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
9544
|
+
if (buf.length < 8 || buf.toString("ascii", 0, 4) !== MAGIC) {
|
|
9545
|
+
throw new Error("embeddings.bin: bad magic (not a codeindex embeddings artifact)");
|
|
9546
|
+
}
|
|
9547
|
+
const headerLen = buf.readUInt32LE(4);
|
|
9548
|
+
const header = JSON.parse(buf.toString("utf8", 8, 8 + headerLen));
|
|
9549
|
+
const bodyOff = 8 + headerLen;
|
|
9550
|
+
const { dim } = header;
|
|
9551
|
+
const records = header.records.map((m, i2) => {
|
|
9552
|
+
const vec = new Int8Array(dim);
|
|
9553
|
+
for (let d = 0; d < dim; d++) vec[d] = buf.readInt8(bodyOff + i2 * dim + d);
|
|
9554
|
+
const rec = { file: m.file, vec };
|
|
9555
|
+
if (m.symbol) rec.symbol = m.symbol;
|
|
9556
|
+
if (m.line) rec.line = m.line;
|
|
9557
|
+
return rec;
|
|
9558
|
+
});
|
|
9559
|
+
return { embedVersion: header.embedVersion, modelId: header.modelId, dim, records };
|
|
9560
|
+
}
|
|
9561
|
+
var MAGIC;
|
|
9562
|
+
var init_embed = __esm({
|
|
9563
|
+
"src/embed/index.ts"() {
|
|
9564
|
+
"use strict";
|
|
9565
|
+
init_encode();
|
|
9566
|
+
init_model();
|
|
9567
|
+
MAGIC = "CIE1";
|
|
9568
|
+
}
|
|
9569
|
+
});
|
|
9570
|
+
|
|
9571
|
+
// src/embed/search.ts
|
|
9572
|
+
function searchSemantic(scan2, query, index, opts = {}) {
|
|
9573
|
+
const limit = opts.limit ?? DEFAULT_LIMIT2;
|
|
9574
|
+
const lexical = searchIndex(scan2, query, { limit: Math.max(limit, 50), fuzzy: opts.fuzzy });
|
|
9575
|
+
if (!opts.model || !index || index.records.length === 0) {
|
|
9576
|
+
return lexical.slice(0, limit);
|
|
9577
|
+
}
|
|
9578
|
+
const q = encode(opts.model, query);
|
|
9579
|
+
const bestByFile = /* @__PURE__ */ new Map();
|
|
9580
|
+
for (const r of index.records) {
|
|
9581
|
+
const dot = intDot(q, r.vec);
|
|
9582
|
+
const prev = bestByFile.get(r.file);
|
|
9583
|
+
if (!prev || dot > prev.score) bestByFile.set(r.file, { score: dot, symbol: r.symbol });
|
|
9584
|
+
}
|
|
9585
|
+
const semList = [...bestByFile.entries()].filter(([, v]) => v.score > 0).sort((a, b) => b[1].score - a[1].score || byStr(a[0], b[0])).map(([file]) => file);
|
|
9586
|
+
const lexList = lexical.map((r) => r.file);
|
|
9587
|
+
const fused = rrf([lexList, semList], (f) => f, opts.rrfK ?? RRF_K);
|
|
9588
|
+
const lexByFile = new Map(lexical.map((r) => [r.file, r]));
|
|
9589
|
+
const results = [...fused.entries()].sort((a, b) => b[1] - a[1] || byStr(a[0], b[0])).map(([file, score]) => {
|
|
9590
|
+
const lex = lexByFile.get(file);
|
|
9591
|
+
const res = {
|
|
9592
|
+
file,
|
|
9593
|
+
score: Number(score.toFixed(4)),
|
|
9594
|
+
matchedTerms: lex?.matchedTerms ?? [],
|
|
9595
|
+
topSymbols: lex?.topSymbols ?? []
|
|
9596
|
+
};
|
|
9597
|
+
const sem = bestByFile.get(file);
|
|
9598
|
+
if (sem?.symbol) res.semanticSymbol = sem.symbol;
|
|
9599
|
+
if (lex?.fuzzyTerms) res.fuzzyTerms = lex.fuzzyTerms;
|
|
9600
|
+
return res;
|
|
9601
|
+
});
|
|
9602
|
+
return results.slice(0, limit);
|
|
9603
|
+
}
|
|
9604
|
+
var DEFAULT_LIMIT2, RRF_K;
|
|
9605
|
+
var init_search = __esm({
|
|
9606
|
+
"src/embed/search.ts"() {
|
|
9607
|
+
"use strict";
|
|
9608
|
+
init_util();
|
|
9609
|
+
init_sort();
|
|
9610
|
+
init_bm25();
|
|
9611
|
+
init_encode();
|
|
9612
|
+
DEFAULT_LIMIT2 = 20;
|
|
9613
|
+
RRF_K = 60;
|
|
9614
|
+
}
|
|
9615
|
+
});
|
|
9616
|
+
|
|
9346
9617
|
// src/rules.ts
|
|
9347
9618
|
function isEntrypointLike(rel) {
|
|
9348
9619
|
const base = rel.split("/").pop();
|
|
@@ -9647,7 +9918,7 @@ var init_deadcode = __esm({
|
|
|
9647
9918
|
});
|
|
9648
9919
|
|
|
9649
9920
|
// src/complexity.ts
|
|
9650
|
-
import { join as
|
|
9921
|
+
import { join as join11 } from "path";
|
|
9651
9922
|
function complexityOfSource(source) {
|
|
9652
9923
|
return 1 + (source.match(BRANCH_RE) ?? []).length;
|
|
9653
9924
|
}
|
|
@@ -9657,7 +9928,7 @@ function symbolComplexity(scan2, rel, top = 50) {
|
|
|
9657
9928
|
if (f.kind !== "code") continue;
|
|
9658
9929
|
if (rel && f.rel !== rel) continue;
|
|
9659
9930
|
if (!f.symbols.length) continue;
|
|
9660
|
-
const lines = readText(
|
|
9931
|
+
const lines = readText(join11(scan2.root, f.rel)).split("\n");
|
|
9661
9932
|
for (const s of f.symbols) {
|
|
9662
9933
|
if (s.kind === "reexport" || s.kind === "reexport-all") continue;
|
|
9663
9934
|
const end = s.endLine ?? s.line;
|
|
@@ -9672,7 +9943,7 @@ function symbolComplexity(scan2, rel, top = 50) {
|
|
|
9672
9943
|
}
|
|
9673
9944
|
function riskHotspots(scan2, churn, top = 20) {
|
|
9674
9945
|
const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
|
|
9675
|
-
const complexity = complexityOfSource(readText(
|
|
9946
|
+
const complexity = complexityOfSource(readText(join11(scan2.root, f.rel)));
|
|
9676
9947
|
const commits = churn.get(f.rel) ?? 0;
|
|
9677
9948
|
return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
|
|
9678
9949
|
});
|
|
@@ -9871,11 +10142,31 @@ function callTool(name2, args2) {
|
|
|
9871
10142
|
if (name2 === "search") {
|
|
9872
10143
|
const query = str(args2.query);
|
|
9873
10144
|
if (!query) throw new Error("`query` is required");
|
|
9874
|
-
const
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
10145
|
+
const scan2 = scanRepo(repo, scanOpts);
|
|
10146
|
+
const limit = typeof args2.limit === "number" ? args2.limit : void 0;
|
|
10147
|
+
const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
|
|
10148
|
+
if (args2.semantic === true) {
|
|
10149
|
+
const modelDir = resolveEmbedModelDir(repo);
|
|
10150
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
10151
|
+
if (model) {
|
|
10152
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
10153
|
+
return JSON.stringify(searchSemantic(scan2, query, index, { model, limit, fuzzy }), null, 2);
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
|
|
10157
|
+
}
|
|
10158
|
+
if (name2 === "embed_status") {
|
|
10159
|
+
const modelDir = resolveEmbedModelDir(repo);
|
|
10160
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
10161
|
+
return JSON.stringify(
|
|
10162
|
+
{
|
|
10163
|
+
embedVersion: EMBED_VERSION,
|
|
10164
|
+
model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
|
|
10165
|
+
endpoint: process.env.CODEINDEX_EMBED_ENDPOINT ?? null
|
|
10166
|
+
},
|
|
10167
|
+
null,
|
|
10168
|
+
2
|
|
10169
|
+
);
|
|
9879
10170
|
}
|
|
9880
10171
|
if (name2 === "check_rules") {
|
|
9881
10172
|
const rules = parseRules(args2.rules);
|
|
@@ -9963,6 +10254,9 @@ var init_mcp = __esm({
|
|
|
9963
10254
|
init_memory();
|
|
9964
10255
|
init_bm25();
|
|
9965
10256
|
init_rules();
|
|
10257
|
+
init_model();
|
|
10258
|
+
init_embed();
|
|
10259
|
+
init_search();
|
|
9966
10260
|
repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
|
|
9967
10261
|
scopeProps = {
|
|
9968
10262
|
scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
|
|
@@ -10175,7 +10469,7 @@ var init_mcp = __esm({
|
|
|
10175
10469
|
},
|
|
10176
10470
|
{
|
|
10177
10471
|
name: "search",
|
|
10178
|
-
description: 'Natural-language-ish lexical search: BM25 ranking (k1=1.2, b=0.75) over symbol names (camelCase/snake_case subtokens), file path segments, markdown headings and summary lines. NOT embeddings \u2014 deterministic, diacritic-folded, zero API keys. Answers "where is auth handled?"-style queries with ranked files, matched terms and top symbols. Query terms with zero document frequency get a deterministic trigram-fuzzy fallback (typo-tolerant) unless `fuzzy: false`.',
|
|
10472
|
+
description: 'Natural-language-ish lexical search: BM25 ranking (k1=1.2, b=0.75) over symbol names (camelCase/snake_case subtokens), file path segments, markdown headings and summary lines. NOT embeddings by default \u2014 deterministic, diacritic-folded, zero API keys. Answers "where is auth handled?"-style queries with ranked files, matched terms and top symbols. Query terms with zero document frequency get a deterministic trigram-fuzzy fallback (typo-tolerant) unless `fuzzy: false`. Set `semantic: true` to RRF-fuse the deterministic static-embedding tier when a model asset is present (degrades to lexical otherwise \u2014 see embed_status).',
|
|
10179
10473
|
inputSchema: {
|
|
10180
10474
|
type: "object",
|
|
10181
10475
|
properties: {
|
|
@@ -10186,11 +10480,20 @@ var init_mcp = __esm({
|
|
|
10186
10480
|
fuzzy: {
|
|
10187
10481
|
type: "boolean",
|
|
10188
10482
|
description: "Trigram fuzzy fallback for query terms with zero document frequency (default true)"
|
|
10483
|
+
},
|
|
10484
|
+
semantic: {
|
|
10485
|
+
type: "boolean",
|
|
10486
|
+
description: "RRF-fuse the deterministic static-embedding tier with lexical when a model asset is present (default false; silently lexical-only when no model)"
|
|
10189
10487
|
}
|
|
10190
10488
|
},
|
|
10191
10489
|
required: ["repo", "query"]
|
|
10192
10490
|
}
|
|
10193
10491
|
},
|
|
10492
|
+
{
|
|
10493
|
+
name: "embed_status",
|
|
10494
|
+
description: "Report the deterministic static-embedding tier: whether a model asset is resolved (opt-in, never shipped in the package), its modelId/dim, EMBED_VERSION, and any configured HTTP endpoint. Use to check whether `search` with semantic:true will actually fuse embeddings or degrade to lexical.",
|
|
10495
|
+
inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] }
|
|
10496
|
+
},
|
|
10194
10497
|
{
|
|
10195
10498
|
name: "check_rules",
|
|
10196
10499
|
description: 'Validate dependency-cruiser-style architecture rules against the link-graph. Rules (inline JSON array): forbidden edges {name, from, to, kind?, severity?, comment?} with glob paths, plus builtins {name, builtin: "cycles"|"orphans"} (module-level import cycles; edge-less code files). Returns deterministic violations with severity error|warn \u2014 a CI gate.',
|
|
@@ -10605,6 +10908,41 @@ init_pipeline();
|
|
|
10605
10908
|
init_git();
|
|
10606
10909
|
init_grep();
|
|
10607
10910
|
init_bm25();
|
|
10911
|
+
init_model();
|
|
10912
|
+
init_encode();
|
|
10913
|
+
init_embed();
|
|
10914
|
+
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
|
|
10608
10946
|
init_rules();
|
|
10609
10947
|
init_coupling();
|
|
10610
10948
|
init_repomap();
|
|
@@ -10623,8 +10961,8 @@ init_loader();
|
|
|
10623
10961
|
init_pipeline();
|
|
10624
10962
|
init_graph_json();
|
|
10625
10963
|
init_symbols_json();
|
|
10626
|
-
import { existsSync as
|
|
10627
|
-
import { join as
|
|
10964
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
10965
|
+
import { join as join12, resolve } from "path";
|
|
10628
10966
|
init_scan();
|
|
10629
10967
|
init_callers();
|
|
10630
10968
|
init_workspaces();
|
|
@@ -10637,6 +10975,9 @@ init_complexity();
|
|
|
10637
10975
|
init_viz();
|
|
10638
10976
|
init_bm25();
|
|
10639
10977
|
init_rules();
|
|
10978
|
+
init_model();
|
|
10979
|
+
init_embed();
|
|
10980
|
+
init_search();
|
|
10640
10981
|
var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
|
|
10641
10982
|
|
|
10642
10983
|
Usage: engine.mjs <command> [flags]
|
|
@@ -10654,7 +10995,14 @@ Commands:
|
|
|
10654
10995
|
churn Per-file git commit counts (JSON; --since <ref> to bound)
|
|
10655
10996
|
grep Search: cli.mjs grep <pattern> --repo <dir> (JSON hits)
|
|
10656
10997
|
search Keyless BM25 lexical search over symbol names, path segments,
|
|
10657
|
-
markdown headings and summaries: cli.mjs search "<query>" --repo <dir
|
|
10998
|
+
markdown headings and summaries: cli.mjs search "<query>" --repo <dir>.
|
|
10999
|
+
--semantic fuses in the deterministic static-embedding tier (RRF)
|
|
11000
|
+
when a model is present; degrades to lexical (exit 0) when absent
|
|
11001
|
+
embed Deterministic static-embedding tier (opt-in by model asset):
|
|
11002
|
+
embed status Report the resolved model + EMBED_VERSION (JSON)
|
|
11003
|
+
embed build Write embeddings.bin into --out <dir> from the repo
|
|
11004
|
+
embed pull Fetch the model asset into CODEINDEX_EMBED_DIR (or
|
|
11005
|
+
<repo>/.codeindex/models/) \u2014 needs CODEINDEX_EMBED_URL
|
|
10658
11006
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
10659
11007
|
against the link-graph: --config <codeindex.rules.json>; exits 1
|
|
10660
11008
|
on any error-severity violation (a CI gate)
|
|
@@ -10682,12 +11030,14 @@ Flags:
|
|
|
10682
11030
|
--limit <n> Max results for \`search\` (default 20)
|
|
10683
11031
|
--no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
|
|
10684
11032
|
with zero document frequency (default: enabled)
|
|
11033
|
+
--semantic \`search\`: RRF-fuse the deterministic static-embedding tier
|
|
11034
|
+
with lexical (needs a model asset; lexical-only otherwise)
|
|
10685
11035
|
--recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
|
|
10686
11036
|
the JS/TS import gate to unique repo-wide names and labels
|
|
10687
11037
|
each site corroborated|unique-name
|
|
10688
11038
|
`;
|
|
10689
11039
|
function parseFlags(args2) {
|
|
10690
|
-
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true };
|
|
11040
|
+
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
10691
11041
|
for (let i2 = 0; i2 < args2.length; i2++) {
|
|
10692
11042
|
const a = args2[i2];
|
|
10693
11043
|
const next = () => {
|
|
@@ -10720,6 +11070,7 @@ function parseFlags(args2) {
|
|
|
10720
11070
|
else if (a === "--config") flags2.config = resolve(next());
|
|
10721
11071
|
else if (a === "--limit") flags2.limit = num();
|
|
10722
11072
|
else if (a === "--no-fuzzy") flags2.fuzzy = false;
|
|
11073
|
+
else if (a === "--semantic") flags2.semantic = true;
|
|
10723
11074
|
else if (a === "--recall") flags2.recall = true;
|
|
10724
11075
|
else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
|
|
10725
11076
|
else throw new Error(`unknown flag: ${a}`);
|
|
@@ -10756,24 +11107,24 @@ async function runCli(argv) {
|
|
|
10756
11107
|
return;
|
|
10757
11108
|
}
|
|
10758
11109
|
const flags2 = parseFlags(rest);
|
|
10759
|
-
if (!
|
|
11110
|
+
if (!existsSync4(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
|
|
10760
11111
|
if (!flags2.noAst) await ensureGrammars(allGrammarKeys());
|
|
10761
11112
|
if (cmd === "index") {
|
|
10762
11113
|
if (!flags2.out) throw new Error("index needs --out <dir>");
|
|
10763
11114
|
const outDir = flags2.out;
|
|
10764
11115
|
mkdirSync2(outDir, { recursive: true });
|
|
10765
|
-
const cachePath =
|
|
11116
|
+
const cachePath = join12(outDir, "cache.json");
|
|
10766
11117
|
let cache;
|
|
10767
11118
|
try {
|
|
10768
|
-
const parsed = JSON.parse(
|
|
11119
|
+
const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
|
|
10769
11120
|
if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
|
|
10770
11121
|
cache = new Map(Object.entries(parsed.files));
|
|
10771
11122
|
}
|
|
10772
11123
|
} catch {
|
|
10773
11124
|
}
|
|
10774
11125
|
const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
|
|
10775
|
-
writeFileSync3(
|
|
10776
|
-
writeFileSync3(
|
|
11126
|
+
writeFileSync3(join12(outDir, "graph.json"), renderGraphJson(graph));
|
|
11127
|
+
writeFileSync3(join12(outDir, "symbols.json"), renderSymbolsJson(symbols));
|
|
10777
11128
|
const files = {};
|
|
10778
11129
|
for (const f of scan2.files) {
|
|
10779
11130
|
const entry = { hash: f.hash, record: f, size: f.size };
|
|
@@ -10785,7 +11136,15 @@ async function runCli(argv) {
|
|
|
10785
11136
|
cachePath,
|
|
10786
11137
|
JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n"
|
|
10787
11138
|
);
|
|
10788
|
-
|
|
11139
|
+
let embedNote = "";
|
|
11140
|
+
const modelDir = resolveEmbedModelDir(flags2.repo);
|
|
11141
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11142
|
+
if (model) {
|
|
11143
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
11144
|
+
writeFileSync3(join12(outDir, "embeddings.bin"), serializeEmbeddings(index));
|
|
11145
|
+
embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
|
|
11146
|
+
}
|
|
11147
|
+
process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
|
|
10789
11148
|
`);
|
|
10790
11149
|
} else if (cmd === "scan") {
|
|
10791
11150
|
const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
|
|
@@ -10822,11 +11181,86 @@ async function runCli(argv) {
|
|
|
10822
11181
|
} else if (cmd === "search") {
|
|
10823
11182
|
if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
|
|
10824
11183
|
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
10825
|
-
|
|
10826
|
-
|
|
11184
|
+
if (flags2.semantic) {
|
|
11185
|
+
const modelDir = resolveEmbedModelDir(flags2.repo);
|
|
11186
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
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
|
+
);
|
|
11191
|
+
const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
11192
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
|
|
11193
|
+
} else {
|
|
11194
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
11195
|
+
const results = searchSemantic(scan2, flags2.positional, index, { model, limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
11196
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
|
|
11197
|
+
}
|
|
11198
|
+
} else {
|
|
11199
|
+
const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
|
|
11200
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
|
|
11201
|
+
}
|
|
11202
|
+
} else if (cmd === "embed") {
|
|
11203
|
+
const sub = flags2.positional;
|
|
11204
|
+
const modelDir = resolveEmbedModelDir(flags2.repo);
|
|
11205
|
+
if (sub === "status") {
|
|
11206
|
+
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
11207
|
+
const status = {
|
|
11208
|
+
embedVersion: EMBED_VERSION,
|
|
11209
|
+
model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
|
|
11210
|
+
endpoint: process.env.CODEINDEX_EMBED_ENDPOINT ?? null
|
|
11211
|
+
};
|
|
11212
|
+
emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
|
|
11213
|
+
} else if (sub === "build") {
|
|
11214
|
+
if (!flags2.out) throw new Error("embed build needs --out <dir>");
|
|
11215
|
+
if (!modelDir) {
|
|
11216
|
+
process.stderr.write("codeindex: no embedding model present \u2014 run `codeindex embed pull` first (nothing written)\n");
|
|
11217
|
+
process.exitCode = 1;
|
|
11218
|
+
return;
|
|
11219
|
+
}
|
|
11220
|
+
const model = loadEmbedModel(modelDir);
|
|
11221
|
+
mkdirSync2(flags2.out, { recursive: true });
|
|
11222
|
+
const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
|
|
11223
|
+
const index = buildEmbeddingIndex(scan2, model);
|
|
11224
|
+
writeFileSync3(join12(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
|
|
11225
|
+
process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
|
|
11226
|
+
`);
|
|
11227
|
+
} else if (sub === "pull") {
|
|
11228
|
+
const url = resolveEmbedPullUrl();
|
|
11229
|
+
if (!url) {
|
|
11230
|
+
process.stderr.write(
|
|
11231
|
+
"codeindex: no model URL configured. The official static-embedding asset is not published yet.\nSet CODEINDEX_EMBED_URL to a model.json URL (optionally CODEINDEX_EMBED_DIR as the destination), then re-run `codeindex embed pull`.\n"
|
|
11232
|
+
);
|
|
11233
|
+
process.exitCode = 1;
|
|
11234
|
+
return;
|
|
11235
|
+
}
|
|
11236
|
+
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join12(flags2.repo, ".codeindex", "models");
|
|
11237
|
+
mkdirSync2(destDir, { recursive: true });
|
|
11238
|
+
process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join12(destDir, "model.json")}
|
|
11239
|
+
`);
|
|
11240
|
+
const res = await fetch(url);
|
|
11241
|
+
if (!res.ok) {
|
|
11242
|
+
process.stderr.write(`codeindex: pull failed \u2014 HTTP ${res.status} from ${url}
|
|
11243
|
+
`);
|
|
11244
|
+
process.exitCode = 1;
|
|
11245
|
+
return;
|
|
11246
|
+
}
|
|
11247
|
+
const body2 = await res.text();
|
|
11248
|
+
try {
|
|
11249
|
+
JSON.parse(body2);
|
|
11250
|
+
} catch {
|
|
11251
|
+
process.stderr.write("codeindex: pull failed \u2014 response is not a valid model.json (expected JSON)\n");
|
|
11252
|
+
process.exitCode = 1;
|
|
11253
|
+
return;
|
|
11254
|
+
}
|
|
11255
|
+
writeFileSync3(join12(destDir, "model.json"), body2);
|
|
11256
|
+
process.stderr.write(`codeindex: model written to ${join12(destDir, "model.json")}
|
|
11257
|
+
`);
|
|
11258
|
+
} else {
|
|
11259
|
+
throw new Error("embed needs a subcommand: pull | build | status");
|
|
11260
|
+
}
|
|
10827
11261
|
} else if (cmd === "rules") {
|
|
10828
11262
|
if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
10829
|
-
const rules = parseRules(JSON.parse(
|
|
11263
|
+
const rules = parseRules(JSON.parse(readFileSync6(flags2.config, "utf8")));
|
|
10830
11264
|
const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
|
|
10831
11265
|
const violations = checkRules(graph, rules);
|
|
10832
11266
|
const errors = violations.filter((v) => v.severity === "error").length;
|
|
@@ -10887,14 +11321,17 @@ ${HELP}`);
|
|
|
10887
11321
|
}
|
|
10888
11322
|
export {
|
|
10889
11323
|
DEFAULT_MAX_FILES,
|
|
11324
|
+
EMBED_VERSION,
|
|
10890
11325
|
ENGINE_VERSION,
|
|
10891
11326
|
EXTRACTOR_VERSION,
|
|
10892
11327
|
MARKDOWN_EXT,
|
|
10893
11328
|
SCHEMA_VERSION,
|
|
10894
11329
|
allGrammarKeys,
|
|
10895
11330
|
applyCentrality,
|
|
11331
|
+
basicTokenize,
|
|
10896
11332
|
betweennessOf,
|
|
10897
11333
|
buildCallerIndex,
|
|
11334
|
+
buildEmbeddingIndex,
|
|
10898
11335
|
buildGraph,
|
|
10899
11336
|
buildIndexArtifacts,
|
|
10900
11337
|
buildModules,
|
|
@@ -10917,11 +11354,14 @@ export {
|
|
|
10917
11354
|
computeSymbolRefs,
|
|
10918
11355
|
computeTestMap,
|
|
10919
11356
|
deleteMemory,
|
|
11357
|
+
deserializeEmbeddings,
|
|
10920
11358
|
detectCommunities,
|
|
10921
11359
|
detectWorkspaces,
|
|
10922
11360
|
diffFiles,
|
|
10923
11361
|
diffHunks,
|
|
11362
|
+
embedViaEndpoint,
|
|
10924
11363
|
enclosingSymbol,
|
|
11364
|
+
encode,
|
|
10925
11365
|
ensureGrammars,
|
|
10926
11366
|
escapeRegExp,
|
|
10927
11367
|
extToLang,
|
|
@@ -10937,10 +11377,12 @@ export {
|
|
|
10937
11377
|
grammarKeyForExt,
|
|
10938
11378
|
grammarReady,
|
|
10939
11379
|
grepRepo,
|
|
11380
|
+
hasEmbedModel,
|
|
10940
11381
|
have,
|
|
10941
11382
|
headCommit,
|
|
10942
11383
|
insertAfterSymbol,
|
|
10943
11384
|
insertBeforeSymbol,
|
|
11385
|
+
intDot,
|
|
10944
11386
|
isCode,
|
|
10945
11387
|
isDoc,
|
|
10946
11388
|
isGitWorktree,
|
|
@@ -10951,6 +11393,7 @@ export {
|
|
|
10951
11393
|
keywords,
|
|
10952
11394
|
languageOf,
|
|
10953
11395
|
listMemories,
|
|
11396
|
+
loadEmbedModel,
|
|
10954
11397
|
pagerankOf,
|
|
10955
11398
|
parseGitignore,
|
|
10956
11399
|
parseRules,
|
|
@@ -10967,14 +11410,20 @@ export {
|
|
|
10967
11410
|
resolveBaseRef,
|
|
10968
11411
|
resolveCallEdges,
|
|
10969
11412
|
resolveDocLink,
|
|
11413
|
+
resolveEmbedEndpoint,
|
|
11414
|
+
resolveEmbedModelDir,
|
|
11415
|
+
resolveEmbedPullUrl,
|
|
10970
11416
|
resolveImport,
|
|
10971
11417
|
resolveUniqueSymbol,
|
|
10972
11418
|
riskHotspots,
|
|
11419
|
+
roundHalfToEven,
|
|
10973
11420
|
rrf,
|
|
10974
11421
|
runCli,
|
|
10975
11422
|
runMcpServer,
|
|
10976
11423
|
scanRepo,
|
|
10977
11424
|
searchIndex,
|
|
11425
|
+
searchSemantic,
|
|
11426
|
+
serializeEmbeddings,
|
|
10978
11427
|
sh,
|
|
10979
11428
|
sha1,
|
|
10980
11429
|
shortHash,
|
|
@@ -10984,10 +11433,12 @@ export {
|
|
|
10984
11433
|
symbolsOverview,
|
|
10985
11434
|
testsForModule,
|
|
10986
11435
|
tierForPath,
|
|
11436
|
+
tokenize,
|
|
10987
11437
|
uniqueSymbolDefs,
|
|
10988
11438
|
untestedModules,
|
|
10989
11439
|
untrackedFiles,
|
|
10990
11440
|
walk,
|
|
11441
|
+
wordpiece,
|
|
10991
11442
|
writeMemory
|
|
10992
11443
|
};
|
|
10993
11444
|
// "Copyright" and "@license" are already caught by DIRECTIVE_RE.
|