@maxgfr/codeindex 2.8.1 → 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.
@@ -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.8.1";
17
+ ENGINE_VERSION = "2.10.0";
18
18
  SCHEMA_VERSION = 4;
19
19
  EXTRACTOR_VERSION = 6;
20
20
  }
@@ -9210,6 +9210,27 @@ function buildDocs(scan2) {
9210
9210
  }
9211
9211
  return docs;
9212
9212
  }
9213
+ function charTrigrams(term) {
9214
+ const padded = `^^${term}$$`;
9215
+ const grams = /* @__PURE__ */ new Set();
9216
+ for (let i2 = 0; i2 + 3 <= padded.length; i2++) grams.add(padded.slice(i2, i2 + 3));
9217
+ return grams;
9218
+ }
9219
+ function diceCoefficient(a, b) {
9220
+ if (!a.size || !b.size) return 0;
9221
+ let inter = 0;
9222
+ for (const g of a) if (b.has(g)) inter++;
9223
+ return 2 * inter / (a.size + b.size);
9224
+ }
9225
+ function buildTrigramIndex(docs) {
9226
+ const index = /* @__PURE__ */ new Map();
9227
+ for (const d of docs) {
9228
+ for (const term of d.tf.keys()) {
9229
+ if (!index.has(term)) index.set(term, charTrigrams(term));
9230
+ }
9231
+ }
9232
+ return index;
9233
+ }
9213
9234
  function searchIndex(scan2, query, opts = {}) {
9214
9235
  const terms = [];
9215
9236
  const seen = /* @__PURE__ */ new Set();
@@ -9233,35 +9254,81 @@ function searchIndex(scan2, query, opts = {}) {
9233
9254
  for (const d of docs) if (d.tf.has(t)) count++;
9234
9255
  df.set(t, count);
9235
9256
  }
9257
+ const fuzzyEnabled = opts.fuzzy ?? true;
9258
+ const fuzzyCandidates = /* @__PURE__ */ new Map();
9259
+ if (fuzzyEnabled) {
9260
+ const unmatched = terms.filter((t) => df.get(t) === 0);
9261
+ if (unmatched.length) {
9262
+ const trigramIndex = buildTrigramIndex(docs);
9263
+ for (const t of unmatched) {
9264
+ const grams = charTrigrams(t);
9265
+ const candidates = [];
9266
+ for (const [vocabTerm, vocabGrams] of trigramIndex) {
9267
+ const dice = diceCoefficient(grams, vocabGrams);
9268
+ if (dice >= FUZZY_DICE_THRESHOLD) candidates.push({ term: vocabTerm, dice });
9269
+ }
9270
+ candidates.sort((a, b) => b.dice - a.dice || byStr(a.term, b.term));
9271
+ fuzzyCandidates.set(t, candidates.slice(0, FUZZY_CAP));
9272
+ }
9273
+ }
9274
+ }
9275
+ const vocabDf = /* @__PURE__ */ new Map();
9276
+ const dfOfVocabTerm = (term) => {
9277
+ const known = df.get(term) ?? vocabDf.get(term);
9278
+ if (known !== void 0) return known;
9279
+ let count = 0;
9280
+ for (const d of docs) if (d.tf.has(term)) count++;
9281
+ vocabDf.set(term, count);
9282
+ return count;
9283
+ };
9236
9284
  const results = [];
9237
9285
  for (const d of docs) {
9238
9286
  let score = 0;
9239
9287
  const matched = [];
9288
+ const symbolTerms = /* @__PURE__ */ new Set();
9289
+ const fuzzyHit = /* @__PURE__ */ new Set();
9240
9290
  for (const t of terms) {
9241
9291
  const tf = d.tf.get(t);
9242
- if (!tf) continue;
9243
- matched.push(t);
9244
- const idf = Math.log(1 + (n - df.get(t) + 0.5) / (df.get(t) + 0.5));
9245
- score += idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * d.len / avgLen));
9292
+ if (tf) {
9293
+ matched.push(t);
9294
+ symbolTerms.add(t);
9295
+ const idf = Math.log(1 + (n - df.get(t) + 0.5) / (df.get(t) + 0.5));
9296
+ score += idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * d.len / avgLen));
9297
+ continue;
9298
+ }
9299
+ const candidates = fuzzyCandidates.get(t);
9300
+ if (!candidates) continue;
9301
+ for (const cand of candidates) {
9302
+ const ctf = d.tf.get(cand.term);
9303
+ if (!ctf) continue;
9304
+ const cdf = dfOfVocabTerm(cand.term);
9305
+ const idf = Math.log(1 + (n - cdf + 0.5) / (cdf + 0.5));
9306
+ const contribution = idf * (ctf * (K1 + 1)) / (ctf + K1 * (1 - B + B * d.len / avgLen));
9307
+ score += contribution * cand.dice;
9308
+ symbolTerms.add(cand.term);
9309
+ fuzzyHit.add(t);
9310
+ }
9246
9311
  }
9247
- if (!matched.length) continue;
9312
+ if (!matched.length && !fuzzyHit.size) continue;
9248
9313
  const scored = d.symbols.map((name2) => {
9249
9314
  const toks = new Set(subtokens(name2));
9250
9315
  let hits = 0;
9251
- for (const t of matched) if (toks.has(t)) hits++;
9316
+ for (const t of symbolTerms) if (toks.has(t)) hits++;
9252
9317
  return { name: name2, hits };
9253
9318
  }).filter((s) => s.hits > 0).sort((a, b) => b.hits - a.hits || byStr(a.name, b.name));
9254
- results.push({
9319
+ const result = {
9255
9320
  file: d.file,
9256
9321
  score: Number(score.toFixed(4)),
9257
9322
  matchedTerms: matched.sort(byStr),
9258
9323
  topSymbols: scored.slice(0, TOP_SYMBOLS).map((s) => s.name)
9259
- });
9324
+ };
9325
+ if (fuzzyHit.size) result.fuzzyTerms = [...fuzzyHit].sort(byStr);
9326
+ results.push(result);
9260
9327
  }
9261
9328
  results.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
9262
9329
  return results.slice(0, opts.limit ?? DEFAULT_LIMIT);
9263
9330
  }
9264
- var K1, B, DEFAULT_LIMIT, TOP_SYMBOLS;
9331
+ var K1, B, DEFAULT_LIMIT, TOP_SYMBOLS, FUZZY_DICE_THRESHOLD, FUZZY_CAP;
9265
9332
  var init_bm25 = __esm({
9266
9333
  "src/bm25.ts"() {
9267
9334
  "use strict";
@@ -9271,6 +9338,279 @@ var init_bm25 = __esm({
9271
9338
  B = 0.75;
9272
9339
  DEFAULT_LIMIT = 20;
9273
9340
  TOP_SYMBOLS = 5;
9341
+ FUZZY_DICE_THRESHOLD = 0.6;
9342
+ FUZZY_CAP = 3;
9343
+ }
9344
+ });
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;
9274
9614
  }
9275
9615
  });
9276
9616
 
@@ -9578,7 +9918,7 @@ var init_deadcode = __esm({
9578
9918
  });
9579
9919
 
9580
9920
  // src/complexity.ts
9581
- import { join as join10 } from "path";
9921
+ import { join as join11 } from "path";
9582
9922
  function complexityOfSource(source) {
9583
9923
  return 1 + (source.match(BRANCH_RE) ?? []).length;
9584
9924
  }
@@ -9588,7 +9928,7 @@ function symbolComplexity(scan2, rel, top = 50) {
9588
9928
  if (f.kind !== "code") continue;
9589
9929
  if (rel && f.rel !== rel) continue;
9590
9930
  if (!f.symbols.length) continue;
9591
- const lines = readText(join10(scan2.root, f.rel)).split("\n");
9931
+ const lines = readText(join11(scan2.root, f.rel)).split("\n");
9592
9932
  for (const s of f.symbols) {
9593
9933
  if (s.kind === "reexport" || s.kind === "reexport-all") continue;
9594
9934
  const end = s.endLine ?? s.line;
@@ -9603,7 +9943,7 @@ function symbolComplexity(scan2, rel, top = 50) {
9603
9943
  }
9604
9944
  function riskHotspots(scan2, churn, top = 20) {
9605
9945
  const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
9606
- const complexity = complexityOfSource(readText(join10(scan2.root, f.rel)));
9946
+ const complexity = complexityOfSource(readText(join11(scan2.root, f.rel)));
9607
9947
  const commits = churn.get(f.rel) ?? 0;
9608
9948
  return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
9609
9949
  });
@@ -9802,10 +10142,31 @@ function callTool(name2, args2) {
9802
10142
  if (name2 === "search") {
9803
10143
  const query = str(args2.query);
9804
10144
  if (!query) throw new Error("`query` is required");
9805
- const results = searchIndex(scanRepo(repo, scanOpts), query, {
9806
- limit: typeof args2.limit === "number" ? args2.limit : void 0
9807
- });
9808
- return JSON.stringify(results, null, 2);
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
+ );
9809
10170
  }
9810
10171
  if (name2 === "check_rules") {
9811
10172
  const rules = parseRules(args2.rules);
@@ -9893,6 +10254,9 @@ var init_mcp = __esm({
9893
10254
  init_memory();
9894
10255
  init_bm25();
9895
10256
  init_rules();
10257
+ init_model();
10258
+ init_embed();
10259
+ init_search();
9896
10260
  repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
9897
10261
  scopeProps = {
9898
10262
  scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
@@ -10105,18 +10469,31 @@ var init_mcp = __esm({
10105
10469
  },
10106
10470
  {
10107
10471
  name: "search",
10108
- 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.',
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).',
10109
10473
  inputSchema: {
10110
10474
  type: "object",
10111
10475
  properties: {
10112
10476
  ...repoProp,
10113
10477
  ...scopeProps,
10114
10478
  query: { type: "string", description: "Natural-language or identifier query" },
10115
- limit: { type: "number", description: "Max results (default 20)" }
10479
+ limit: { type: "number", description: "Max results (default 20)" },
10480
+ fuzzy: {
10481
+ type: "boolean",
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)"
10487
+ }
10116
10488
  },
10117
10489
  required: ["repo", "query"]
10118
10490
  }
10119
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
+ },
10120
10497
  {
10121
10498
  name: "check_rules",
10122
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.',
@@ -10531,6 +10908,41 @@ init_pipeline();
10531
10908
  init_git();
10532
10909
  init_grep();
10533
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
10534
10946
  init_rules();
10535
10947
  init_coupling();
10536
10948
  init_repomap();
@@ -10549,8 +10961,8 @@ init_loader();
10549
10961
  init_pipeline();
10550
10962
  init_graph_json();
10551
10963
  init_symbols_json();
10552
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
10553
- import { join as join11, resolve } from "path";
10964
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
10965
+ import { join as join12, resolve } from "path";
10554
10966
  init_scan();
10555
10967
  init_callers();
10556
10968
  init_workspaces();
@@ -10563,6 +10975,9 @@ init_complexity();
10563
10975
  init_viz();
10564
10976
  init_bm25();
10565
10977
  init_rules();
10978
+ init_model();
10979
+ init_embed();
10980
+ init_search();
10566
10981
  var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
10567
10982
 
10568
10983
  Usage: engine.mjs <command> [flags]
@@ -10580,7 +10995,14 @@ Commands:
10580
10995
  churn Per-file git commit counts (JSON; --since <ref> to bound)
10581
10996
  grep Search: cli.mjs grep <pattern> --repo <dir> (JSON hits)
10582
10997
  search Keyless BM25 lexical search over symbol names, path segments,
10583
- 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
10584
11006
  rules Architecture rules (forbidden edges, cycles, orphans) validated
10585
11007
  against the link-graph: --config <codeindex.rules.json>; exits 1
10586
11008
  on any error-severity violation (a CI gate)
@@ -10606,12 +11028,16 @@ Flags:
10606
11028
  --no-ast Skip tree-sitter grammars even when present (regex tier)
10607
11029
  --config <file> Rules config for \`rules\` (JSON: [{name, from, to, \u2026}])
10608
11030
  --limit <n> Max results for \`search\` (default 20)
11031
+ --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
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)
10609
11035
  --recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
10610
11036
  the JS/TS import gate to unique repo-wide names and labels
10611
11037
  each site corroborated|unique-name
10612
11038
  `;
10613
11039
  function parseFlags(args2) {
10614
- const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false };
11040
+ const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
10615
11041
  for (let i2 = 0; i2 < args2.length; i2++) {
10616
11042
  const a = args2[i2];
10617
11043
  const next = () => {
@@ -10643,6 +11069,8 @@ function parseFlags(args2) {
10643
11069
  else if (a === "--since") flags2.since = next();
10644
11070
  else if (a === "--config") flags2.config = resolve(next());
10645
11071
  else if (a === "--limit") flags2.limit = num();
11072
+ else if (a === "--no-fuzzy") flags2.fuzzy = false;
11073
+ else if (a === "--semantic") flags2.semantic = true;
10646
11074
  else if (a === "--recall") flags2.recall = true;
10647
11075
  else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
10648
11076
  else throw new Error(`unknown flag: ${a}`);
@@ -10679,24 +11107,24 @@ async function runCli(argv) {
10679
11107
  return;
10680
11108
  }
10681
11109
  const flags2 = parseFlags(rest);
10682
- if (!existsSync3(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
11110
+ if (!existsSync4(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
10683
11111
  if (!flags2.noAst) await ensureGrammars(allGrammarKeys());
10684
11112
  if (cmd === "index") {
10685
11113
  if (!flags2.out) throw new Error("index needs --out <dir>");
10686
11114
  const outDir = flags2.out;
10687
11115
  mkdirSync2(outDir, { recursive: true });
10688
- const cachePath = join11(outDir, "cache.json");
11116
+ const cachePath = join12(outDir, "cache.json");
10689
11117
  let cache;
10690
11118
  try {
10691
- const parsed = JSON.parse(readFileSync5(cachePath, "utf8"));
11119
+ const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
10692
11120
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
10693
11121
  cache = new Map(Object.entries(parsed.files));
10694
11122
  }
10695
11123
  } catch {
10696
11124
  }
10697
11125
  const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
10698
- writeFileSync3(join11(outDir, "graph.json"), renderGraphJson(graph));
10699
- writeFileSync3(join11(outDir, "symbols.json"), renderSymbolsJson(symbols));
11126
+ writeFileSync3(join12(outDir, "graph.json"), renderGraphJson(graph));
11127
+ writeFileSync3(join12(outDir, "symbols.json"), renderSymbolsJson(symbols));
10700
11128
  const files = {};
10701
11129
  for (const f of scan2.files) {
10702
11130
  const entry = { hash: f.hash, record: f, size: f.size };
@@ -10708,7 +11136,15 @@ async function runCli(argv) {
10708
11136
  cachePath,
10709
11137
  JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n"
10710
11138
  );
10711
- process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${scan2.capped ? " (capped)" : ""}
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)" : ""}
10712
11148
  `);
10713
11149
  } else if (cmd === "scan") {
10714
11150
  const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
@@ -10745,11 +11181,86 @@ async function runCli(argv) {
10745
11181
  } else if (cmd === "search") {
10746
11182
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
10747
11183
  const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
10748
- const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit });
10749
- emit(JSON.stringify(results, null, 2) + "\n", flags2.out);
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
+ }
10750
11261
  } else if (cmd === "rules") {
10751
11262
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
10752
- const rules = parseRules(JSON.parse(readFileSync5(flags2.config, "utf8")));
11263
+ const rules = parseRules(JSON.parse(readFileSync6(flags2.config, "utf8")));
10753
11264
  const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
10754
11265
  const violations = checkRules(graph, rules);
10755
11266
  const errors = violations.filter((v) => v.severity === "error").length;
@@ -10810,14 +11321,17 @@ ${HELP}`);
10810
11321
  }
10811
11322
  export {
10812
11323
  DEFAULT_MAX_FILES,
11324
+ EMBED_VERSION,
10813
11325
  ENGINE_VERSION,
10814
11326
  EXTRACTOR_VERSION,
10815
11327
  MARKDOWN_EXT,
10816
11328
  SCHEMA_VERSION,
10817
11329
  allGrammarKeys,
10818
11330
  applyCentrality,
11331
+ basicTokenize,
10819
11332
  betweennessOf,
10820
11333
  buildCallerIndex,
11334
+ buildEmbeddingIndex,
10821
11335
  buildGraph,
10822
11336
  buildIndexArtifacts,
10823
11337
  buildModules,
@@ -10840,11 +11354,14 @@ export {
10840
11354
  computeSymbolRefs,
10841
11355
  computeTestMap,
10842
11356
  deleteMemory,
11357
+ deserializeEmbeddings,
10843
11358
  detectCommunities,
10844
11359
  detectWorkspaces,
10845
11360
  diffFiles,
10846
11361
  diffHunks,
11362
+ embedViaEndpoint,
10847
11363
  enclosingSymbol,
11364
+ encode,
10848
11365
  ensureGrammars,
10849
11366
  escapeRegExp,
10850
11367
  extToLang,
@@ -10860,10 +11377,12 @@ export {
10860
11377
  grammarKeyForExt,
10861
11378
  grammarReady,
10862
11379
  grepRepo,
11380
+ hasEmbedModel,
10863
11381
  have,
10864
11382
  headCommit,
10865
11383
  insertAfterSymbol,
10866
11384
  insertBeforeSymbol,
11385
+ intDot,
10867
11386
  isCode,
10868
11387
  isDoc,
10869
11388
  isGitWorktree,
@@ -10874,6 +11393,7 @@ export {
10874
11393
  keywords,
10875
11394
  languageOf,
10876
11395
  listMemories,
11396
+ loadEmbedModel,
10877
11397
  pagerankOf,
10878
11398
  parseGitignore,
10879
11399
  parseRules,
@@ -10890,14 +11410,20 @@ export {
10890
11410
  resolveBaseRef,
10891
11411
  resolveCallEdges,
10892
11412
  resolveDocLink,
11413
+ resolveEmbedEndpoint,
11414
+ resolveEmbedModelDir,
11415
+ resolveEmbedPullUrl,
10893
11416
  resolveImport,
10894
11417
  resolveUniqueSymbol,
10895
11418
  riskHotspots,
11419
+ roundHalfToEven,
10896
11420
  rrf,
10897
11421
  runCli,
10898
11422
  runMcpServer,
10899
11423
  scanRepo,
10900
11424
  searchIndex,
11425
+ searchSemantic,
11426
+ serializeEmbeddings,
10901
11427
  sh,
10902
11428
  sha1,
10903
11429
  shortHash,
@@ -10907,10 +11433,12 @@ export {
10907
11433
  symbolsOverview,
10908
11434
  testsForModule,
10909
11435
  tierForPath,
11436
+ tokenize,
10910
11437
  uniqueSymbolDefs,
10911
11438
  untestedModules,
10912
11439
  untrackedFiles,
10913
11440
  walk,
11441
+ wordpiece,
10914
11442
  writeMemory
10915
11443
  };
10916
11444
  // "Copyright" and "@license" are already caught by DIRECTIVE_RE.