@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.
@@ -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.9.0";
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 join10 } from "path";
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(join10(scan2.root, f.rel)).split("\n");
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(join10(scan2.root, f.rel)));
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 results = searchIndex(scanRepo(repo, scanOpts), query, {
9875
- limit: typeof args2.limit === "number" ? args2.limit : void 0,
9876
- fuzzy: typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0
9877
- });
9878
- 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
+ );
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 existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
10627
- 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";
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 (!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}`);
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 = join11(outDir, "cache.json");
11116
+ const cachePath = join12(outDir, "cache.json");
10766
11117
  let cache;
10767
11118
  try {
10768
- const parsed = JSON.parse(readFileSync5(cachePath, "utf8"));
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(join11(outDir, "graph.json"), renderGraphJson(graph));
10776
- writeFileSync3(join11(outDir, "symbols.json"), renderSymbolsJson(symbols));
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
- 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)" : ""}
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
- const results = searchIndex(scan2, flags2.positional, { limit: flags2.limit, fuzzy: flags2.fuzzy });
10826
- 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
+ }
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(readFileSync5(flags2.config, "utf8")));
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.