@maxgfr/codeindex 2.11.0 → 2.12.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 +2 -0
- package/docs/MIGRATION.md +2 -1
- package/docs/SEMANTIC.md +242 -0
- package/package.json +3 -2
- package/scripts/engine.d.mts +16 -4
- package/scripts/engine.mjs +185 -88
- package/src/callers.ts +0 -0
- package/src/embed/model.ts +50 -11
- package/src/engine-cli.ts +13 -17
- package/src/engine.ts +5 -1
- package/src/extract/code.ts +54 -58
- package/src/lang/common.ts +75 -0
- package/src/lang/registry.ts +21 -6
- package/src/mcp.ts +81 -12
- package/src/types.ts +13 -3
package/scripts/engine.mjs
CHANGED
|
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.
|
|
17
|
+
ENGINE_VERSION = "2.12.0";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
|
-
EXTRACTOR_VERSION =
|
|
19
|
+
EXTRACTOR_VERSION = 9;
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -731,7 +731,58 @@ function scan(rel, content, lang, rules) {
|
|
|
731
731
|
function extToLang(ext) {
|
|
732
732
|
return EXT_LANG[ext] ?? "other";
|
|
733
733
|
}
|
|
734
|
-
|
|
734
|
+
function extractReexports(rel, content, localSymbols) {
|
|
735
|
+
if (!REEXPORT_EXTS.has(rel.slice(rel.lastIndexOf(".")))) return [];
|
|
736
|
+
const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
|
|
737
|
+
const out2 = [];
|
|
738
|
+
const seen = /* @__PURE__ */ new Set();
|
|
739
|
+
const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
|
|
740
|
+
const localDeclOf = /* @__PURE__ */ new Map();
|
|
741
|
+
for (const s of localSymbols) if (!localDeclOf.has(s.name)) localDeclOf.set(s.name, s);
|
|
742
|
+
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
743
|
+
let m;
|
|
744
|
+
while ((m = named.exec(content)) && out2.length < 60) {
|
|
745
|
+
const from = m[2];
|
|
746
|
+
for (const part of m[1].split(",")) {
|
|
747
|
+
const p = part.trim().replace(/^type\s+/, "");
|
|
748
|
+
const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
|
|
749
|
+
const orig = as ? as[1] : p;
|
|
750
|
+
const name2 = as ? as[2] : p;
|
|
751
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
|
|
752
|
+
seen.add(name2);
|
|
753
|
+
const decl = !from ? localDeclOf.get(orig) : void 0;
|
|
754
|
+
out2.push({
|
|
755
|
+
name: name2,
|
|
756
|
+
kind: decl?.kind ?? "reexport",
|
|
757
|
+
file: rel,
|
|
758
|
+
line: decl ? decl.line : lineAt(m.index),
|
|
759
|
+
...decl?.endLine !== void 0 ? { endLine: decl.endLine } : {},
|
|
760
|
+
signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
|
|
761
|
+
exported: true,
|
|
762
|
+
lang
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
|
|
767
|
+
while ((m = star.exec(content)) && out2.length < 60) {
|
|
768
|
+
const ns = m[1];
|
|
769
|
+
const from = m[2];
|
|
770
|
+
const key = "*" + (ns ?? from);
|
|
771
|
+
if (seen.has(key)) continue;
|
|
772
|
+
seen.add(key);
|
|
773
|
+
out2.push({
|
|
774
|
+
name: ns ?? `* (${from})`,
|
|
775
|
+
kind: ns ? "reexport" : "reexport-all",
|
|
776
|
+
file: rel,
|
|
777
|
+
line: lineAt(m.index),
|
|
778
|
+
signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
|
|
779
|
+
exported: true,
|
|
780
|
+
lang
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return out2;
|
|
784
|
+
}
|
|
785
|
+
var EXT_LANG, REEXPORT_EXTS;
|
|
735
786
|
var init_common = __esm({
|
|
736
787
|
"src/lang/common.ts"() {
|
|
737
788
|
"use strict";
|
|
@@ -799,6 +850,7 @@ var init_common = __esm({
|
|
|
799
850
|
".svelte": "svelte",
|
|
800
851
|
".astro": "astro"
|
|
801
852
|
};
|
|
853
|
+
REEXPORT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
802
854
|
}
|
|
803
855
|
});
|
|
804
856
|
|
|
@@ -1231,12 +1283,18 @@ var init_scala = __esm({
|
|
|
1231
1283
|
// src/lang/registry.ts
|
|
1232
1284
|
function extractSymbols(rel, ext, content) {
|
|
1233
1285
|
const extractor = BY_EXT.get(ext);
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1286
|
+
let symbols;
|
|
1287
|
+
if (!extractor) symbols = [];
|
|
1288
|
+
else {
|
|
1289
|
+
try {
|
|
1290
|
+
symbols = extractor.extract(rel, content);
|
|
1291
|
+
} catch {
|
|
1292
|
+
symbols = [];
|
|
1293
|
+
}
|
|
1239
1294
|
}
|
|
1295
|
+
const known = new Set(symbols.map((s) => s.name));
|
|
1296
|
+
const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
|
|
1297
|
+
return reexports.length ? [...symbols, ...reexports] : symbols;
|
|
1240
1298
|
}
|
|
1241
1299
|
function languageOf(ext) {
|
|
1242
1300
|
return BY_EXT.get(ext)?.lang ?? extToLang(ext);
|
|
@@ -6340,58 +6398,9 @@ function extractImports(ext, content) {
|
|
|
6340
6398
|
}
|
|
6341
6399
|
return [...specs].map((spec) => ({ kind: "import", spec }));
|
|
6342
6400
|
}
|
|
6343
|
-
function
|
|
6344
|
-
if (!JS_TS.has(rel.slice(rel.lastIndexOf(".")))) return [];
|
|
6345
|
-
const lang = /\.(ts|tsx|mts|cts)$/.test(rel) ? "typescript" : "javascript";
|
|
6346
|
-
const out2 = [];
|
|
6347
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6348
|
-
const lineAt = (idx) => content.slice(0, idx).split(/\r?\n/).length;
|
|
6349
|
-
const localKindOf = /* @__PURE__ */ new Map();
|
|
6350
|
-
for (const s of localSymbols) if (!localKindOf.has(s.name)) localKindOf.set(s.name, s.kind);
|
|
6351
|
-
const named = /export\s*\{([\s\S]*?)\}\s*(?:from\s*['"]([^'"]+)['"])?\s*;?/g;
|
|
6352
|
-
let m;
|
|
6353
|
-
while ((m = named.exec(content)) && out2.length < 60) {
|
|
6354
|
-
const from = m[2];
|
|
6355
|
-
for (const part of m[1].split(",")) {
|
|
6356
|
-
const p = part.trim().replace(/^type\s+/, "");
|
|
6357
|
-
const as = /^(\S+)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(p);
|
|
6358
|
-
const orig = as ? as[1] : p;
|
|
6359
|
-
const name2 = as ? as[2] : p;
|
|
6360
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(name2) || name2 === "default" || seen.has(name2)) continue;
|
|
6361
|
-
seen.add(name2);
|
|
6362
|
-
const mirroredKind = !from ? localKindOf.get(orig) : void 0;
|
|
6363
|
-
out2.push({
|
|
6364
|
-
name: name2,
|
|
6365
|
-
kind: mirroredKind ?? "reexport",
|
|
6366
|
-
file: rel,
|
|
6367
|
-
line: lineAt(m.index),
|
|
6368
|
-
signature: from ? `export { ${name2} } from "${from}"` : `export { ${name2} }`,
|
|
6369
|
-
exported: true,
|
|
6370
|
-
lang
|
|
6371
|
-
});
|
|
6372
|
-
}
|
|
6373
|
-
}
|
|
6374
|
-
const star = /export\s*\*\s*(?:as\s+([A-Za-z_$][\w$]*)\s+)?from\s*['"]([^'"]+)['"]/g;
|
|
6375
|
-
while ((m = star.exec(content)) && out2.length < 60) {
|
|
6376
|
-
const ns = m[1];
|
|
6377
|
-
const from = m[2];
|
|
6378
|
-
const key = "*" + (ns ?? from);
|
|
6379
|
-
if (seen.has(key)) continue;
|
|
6380
|
-
seen.add(key);
|
|
6381
|
-
out2.push({
|
|
6382
|
-
name: ns ?? `* (${from})`,
|
|
6383
|
-
kind: ns ? "reexport" : "reexport-all",
|
|
6384
|
-
file: rel,
|
|
6385
|
-
line: lineAt(m.index),
|
|
6386
|
-
signature: `export * ${ns ? `as ${ns} ` : ""}from "${from}"`,
|
|
6387
|
-
exported: true,
|
|
6388
|
-
lang
|
|
6389
|
-
});
|
|
6390
|
-
}
|
|
6391
|
-
return out2;
|
|
6392
|
-
}
|
|
6393
|
-
function collectCallsRegex(content) {
|
|
6401
|
+
function collectCallsRegex(content, symbols = []) {
|
|
6394
6402
|
const out2 = /* @__PURE__ */ new Map();
|
|
6403
|
+
const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
|
|
6395
6404
|
const lines = content.split("\n");
|
|
6396
6405
|
const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
6397
6406
|
for (let i2 = 0; i2 < lines.length && out2.size < 512; i2++) {
|
|
@@ -6399,13 +6408,28 @@ function collectCallsRegex(content) {
|
|
|
6399
6408
|
const trimmed = line.trimStart();
|
|
6400
6409
|
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
|
|
6401
6410
|
CALL_RE.lastIndex = 0;
|
|
6411
|
+
let probe;
|
|
6412
|
+
const introducerCaught = /* @__PURE__ */ new Set();
|
|
6413
|
+
while ((probe = CALL_RE.exec(line)) !== null) {
|
|
6414
|
+
const name2 = probe[2];
|
|
6415
|
+
const key = `${name2} ${i2 + 1}`;
|
|
6416
|
+
if (ownDefLines.has(key) && DEF_INTRODUCERS.test(line.slice(0, probe.index))) introducerCaught.add(key);
|
|
6417
|
+
}
|
|
6418
|
+
CALL_RE.lastIndex = 0;
|
|
6402
6419
|
let m;
|
|
6420
|
+
const fallbackExcluded = /* @__PURE__ */ new Set();
|
|
6403
6421
|
while ((m = CALL_RE.exec(line)) !== null && out2.size < 512) {
|
|
6404
6422
|
const receiver = m[1];
|
|
6405
6423
|
const name2 = m[2];
|
|
6406
6424
|
if (name2.length < 2 || CALL_KEYWORDS.has(name2)) continue;
|
|
6407
6425
|
if (DEF_INTRODUCERS.test(line.slice(0, m.index))) continue;
|
|
6408
6426
|
const key = `${name2} ${i2 + 1}`;
|
|
6427
|
+
if (ownDefLines.has(key) && !introducerCaught.has(key)) {
|
|
6428
|
+
if (!fallbackExcluded.has(key)) {
|
|
6429
|
+
fallbackExcluded.add(key);
|
|
6430
|
+
continue;
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6409
6433
|
if (!out2.has(key)) out2.set(key, receiver ? { name: name2, line: i2 + 1, receiver } : { name: name2, line: i2 + 1 });
|
|
6410
6434
|
}
|
|
6411
6435
|
}
|
|
@@ -6426,7 +6450,9 @@ function extractCode(rel, ext, content) {
|
|
|
6426
6450
|
idents: ast?.idents,
|
|
6427
6451
|
// AST call sites when a grammar parsed the file; the conservative regex
|
|
6428
6452
|
// collector otherwise, so caller indexes exist without the wasm sidecar.
|
|
6429
|
-
|
|
6453
|
+
// `symbols` (this file's own regex-extracted defs) lets the collector
|
|
6454
|
+
// exclude a definition's own name+line from its call candidates.
|
|
6455
|
+
calls: ast ? ast.calls : collectCallsRegex(content, symbols),
|
|
6430
6456
|
importedNames: ast?.importedNames
|
|
6431
6457
|
};
|
|
6432
6458
|
}
|
|
@@ -6436,6 +6462,7 @@ var init_code = __esm({
|
|
|
6436
6462
|
"use strict";
|
|
6437
6463
|
init_registry();
|
|
6438
6464
|
init_extract();
|
|
6465
|
+
init_common();
|
|
6439
6466
|
JS_TS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6440
6467
|
PY = /* @__PURE__ */ new Set([".py", ".pyi"]);
|
|
6441
6468
|
C_CPP = /* @__PURE__ */ new Set([".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"]);
|
|
@@ -7705,8 +7732,11 @@ function buildCallerIndex(scan2, importPairs, opts = {}) {
|
|
|
7705
7732
|
function enclosingSymbol(scan2, file, line) {
|
|
7706
7733
|
const f = scan2.files.find((x) => x.rel === file);
|
|
7707
7734
|
if (!f?.symbols.length) return void 0;
|
|
7735
|
+
return enclosingAmong(f.symbols, line);
|
|
7736
|
+
}
|
|
7737
|
+
function enclosingAmong(symbols, line) {
|
|
7708
7738
|
let best;
|
|
7709
|
-
for (const s of
|
|
7739
|
+
for (const s of symbols) {
|
|
7710
7740
|
if (REFERENCE_KINDS3.has(s.kind)) continue;
|
|
7711
7741
|
if (s.line > line) continue;
|
|
7712
7742
|
if (s.endLine !== void 0 && line > s.endLine) continue;
|
|
@@ -7716,6 +7746,29 @@ function enclosingSymbol(scan2, file, line) {
|
|
|
7716
7746
|
}
|
|
7717
7747
|
return best;
|
|
7718
7748
|
}
|
|
7749
|
+
function buildRawCallerIndex(scan2) {
|
|
7750
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7751
|
+
for (const f of scan2.files) {
|
|
7752
|
+
if (!f.calls?.length) continue;
|
|
7753
|
+
const symbols = f.symbols.filter((s) => !REFERENCE_KINDS3.has(s.kind));
|
|
7754
|
+
for (const c2 of f.calls) {
|
|
7755
|
+
const site = { file: f.rel, line: c2.line };
|
|
7756
|
+
if (c2.receiver !== void 0) site.receiver = c2.receiver;
|
|
7757
|
+
const enc = enclosingAmong(symbols, c2.line);
|
|
7758
|
+
if (enc) site.enclosingSymbol = enc;
|
|
7759
|
+
let arr = byName.get(c2.name);
|
|
7760
|
+
if (!arr) byName.set(c2.name, arr = []);
|
|
7761
|
+
arr.push(site);
|
|
7762
|
+
}
|
|
7763
|
+
}
|
|
7764
|
+
const index = /* @__PURE__ */ new Map();
|
|
7765
|
+
for (const name2 of [...byName.keys()].sort(byStr)) {
|
|
7766
|
+
const sites = byName.get(name2);
|
|
7767
|
+
sites.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
|
|
7768
|
+
index.set(name2, sites);
|
|
7769
|
+
}
|
|
7770
|
+
return index;
|
|
7771
|
+
}
|
|
7719
7772
|
var REFERENCE_KINDS3;
|
|
7720
7773
|
var init_callers = __esm({
|
|
7721
7774
|
"src/callers.ts"() {
|
|
@@ -9348,6 +9401,7 @@ var init_bm25 = __esm({
|
|
|
9348
9401
|
});
|
|
9349
9402
|
|
|
9350
9403
|
// src/embed/model.ts
|
|
9404
|
+
import { createHash as createHash2 } from "crypto";
|
|
9351
9405
|
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
9352
9406
|
import { join as join10 } from "path";
|
|
9353
9407
|
function resolveEmbedModelDir(repo) {
|
|
@@ -9393,15 +9447,30 @@ function loadEmbedModel(dir) {
|
|
|
9393
9447
|
return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
|
|
9394
9448
|
}
|
|
9395
9449
|
function resolveEmbedPullUrl() {
|
|
9396
|
-
const
|
|
9397
|
-
|
|
9450
|
+
const env = process.env.CODEINDEX_EMBED_URL;
|
|
9451
|
+
if (env && env.trim()) return { url: env.trim() };
|
|
9452
|
+
return { url: DEFAULT_EMBED_URL, sha256: EMBED_ASSET_SHA256 };
|
|
9453
|
+
}
|
|
9454
|
+
async function fetchEmbedModel(url, expectedSha256) {
|
|
9455
|
+
const res = await fetch(url);
|
|
9456
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
|
|
9457
|
+
const body2 = await res.text();
|
|
9458
|
+
if (expectedSha256) {
|
|
9459
|
+
const got = createHash2("sha256").update(body2).digest("hex");
|
|
9460
|
+
if (got !== expectedSha256) {
|
|
9461
|
+
throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
|
|
9462
|
+
}
|
|
9463
|
+
}
|
|
9464
|
+
return body2;
|
|
9398
9465
|
}
|
|
9399
|
-
var EMBED_VERSION, DEFAULT_EMBED_DIRNAME;
|
|
9466
|
+
var EMBED_VERSION, DEFAULT_EMBED_DIRNAME, DEFAULT_EMBED_URL, EMBED_ASSET_SHA256;
|
|
9400
9467
|
var init_model = __esm({
|
|
9401
9468
|
"src/embed/model.ts"() {
|
|
9402
9469
|
"use strict";
|
|
9403
9470
|
EMBED_VERSION = 1;
|
|
9404
9471
|
DEFAULT_EMBED_DIRNAME = "models";
|
|
9472
|
+
DEFAULT_EMBED_URL = "https://github.com/maxgfr/codeindex/releases/download/embed-model-v1/model.json";
|
|
9473
|
+
EMBED_ASSET_SHA256 = "163ad053eab4e9a80d421ed4164f32292c83290f02fbbe6fe4b9b1cd6ea18d34";
|
|
9405
9474
|
}
|
|
9406
9475
|
});
|
|
9407
9476
|
|
|
@@ -10103,7 +10172,9 @@ var init_viz = __esm({
|
|
|
10103
10172
|
// src/mcp.ts
|
|
10104
10173
|
var mcp_exports = {};
|
|
10105
10174
|
__export(mcp_exports, {
|
|
10106
|
-
|
|
10175
|
+
memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
|
|
10176
|
+
runMcpServer: () => runMcpServer,
|
|
10177
|
+
scanFingerprint: () => scanFingerprint
|
|
10107
10178
|
});
|
|
10108
10179
|
import { createInterface } from "readline";
|
|
10109
10180
|
function str(v) {
|
|
@@ -10112,6 +10183,19 @@ function str(v) {
|
|
|
10112
10183
|
function strArray(v) {
|
|
10113
10184
|
return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
|
|
10114
10185
|
}
|
|
10186
|
+
function errMessage(e) {
|
|
10187
|
+
return e instanceof Error ? e.message : String(e);
|
|
10188
|
+
}
|
|
10189
|
+
function scanFingerprint(scan2) {
|
|
10190
|
+
return sha1(scan2.files.map((f) => `${f.rel}:${f.hash}`).join("\n"));
|
|
10191
|
+
}
|
|
10192
|
+
async function memoizedEmbeddingIndex(key, build) {
|
|
10193
|
+
const cacheKey = `${key.mode}:${key.identity}:${scanFingerprint(key.scan)}`;
|
|
10194
|
+
if (embeddingIndexCache && embeddingIndexCache.key === cacheKey) return embeddingIndexCache.index;
|
|
10195
|
+
const index = await build();
|
|
10196
|
+
embeddingIndexCache = { key: cacheKey, index };
|
|
10197
|
+
return index;
|
|
10198
|
+
}
|
|
10115
10199
|
async function callTool(name2, args2) {
|
|
10116
10200
|
const repo = str(args2.repo);
|
|
10117
10201
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
@@ -10252,19 +10336,35 @@ async function callTool(name2, args2) {
|
|
|
10252
10336
|
const endpoint = resolveEmbedEndpoint();
|
|
10253
10337
|
if (endpoint) {
|
|
10254
10338
|
try {
|
|
10255
|
-
const index = await buildEndpointIndex(scan2);
|
|
10339
|
+
const index = await memoizedEmbeddingIndex({ mode: "endpoint", identity: endpoint, scan: scan2 }, () => buildEndpointIndex(scan2));
|
|
10256
10340
|
const queryVec = await encodeQueryViaEndpoint(query);
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10341
|
+
const results2 = searchSemantic(scan2, query, index, { queryVec, limit, fuzzy });
|
|
10342
|
+
return JSON.stringify({ results: results2, tier: "endpoint" }, null, 2);
|
|
10343
|
+
} catch (e) {
|
|
10344
|
+
const results2 = searchIndex(scan2, query, { limit, fuzzy });
|
|
10345
|
+
return JSON.stringify(
|
|
10346
|
+
{ results: results2, tier: "lexical", degradedReason: `embedding endpoint failed: ${errMessage(e)}` },
|
|
10347
|
+
null,
|
|
10348
|
+
2
|
|
10349
|
+
);
|
|
10260
10350
|
}
|
|
10261
10351
|
}
|
|
10262
10352
|
const modelDir = resolveEmbedModelDir(repo);
|
|
10263
10353
|
const model = modelDir ? loadEmbedModel(modelDir) : void 0;
|
|
10264
10354
|
if (model) {
|
|
10265
|
-
const index =
|
|
10266
|
-
|
|
10355
|
+
const index = await memoizedEmbeddingIndex(
|
|
10356
|
+
{ mode: "static", identity: `${modelDir}#${model.modelId}`, scan: scan2 },
|
|
10357
|
+
() => buildEmbeddingIndex(scan2, model)
|
|
10358
|
+
);
|
|
10359
|
+
const results2 = searchSemantic(scan2, query, index, { model, limit, fuzzy });
|
|
10360
|
+
return JSON.stringify({ results: results2, tier: "static" }, null, 2);
|
|
10267
10361
|
}
|
|
10362
|
+
const results = searchIndex(scan2, query, { limit, fuzzy });
|
|
10363
|
+
return JSON.stringify(
|
|
10364
|
+
{ results, tier: "lexical", degradedReason: "no embedding endpoint or static model configured \u2014 see embed_status" },
|
|
10365
|
+
null,
|
|
10366
|
+
2
|
|
10367
|
+
);
|
|
10268
10368
|
}
|
|
10269
10369
|
return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
|
|
10270
10370
|
}
|
|
@@ -10345,7 +10445,7 @@ async function runMcpServer() {
|
|
|
10345
10445
|
}
|
|
10346
10446
|
}
|
|
10347
10447
|
}
|
|
10348
|
-
var repoProp, scopeProps, TOOLS;
|
|
10448
|
+
var repoProp, scopeProps, TOOLS, embeddingIndexCache;
|
|
10349
10449
|
var init_mcp = __esm({
|
|
10350
10450
|
"src/mcp.ts"() {
|
|
10351
10451
|
"use strict";
|
|
@@ -10372,6 +10472,7 @@ var init_mcp = __esm({
|
|
|
10372
10472
|
init_embed();
|
|
10373
10473
|
init_search();
|
|
10374
10474
|
init_endpoint();
|
|
10475
|
+
init_hash();
|
|
10375
10476
|
repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
|
|
10376
10477
|
scopeProps = {
|
|
10377
10478
|
scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
|
|
@@ -10584,7 +10685,7 @@ var init_mcp = __esm({
|
|
|
10584
10685
|
},
|
|
10585
10686
|
{
|
|
10586
10687
|
name: "search",
|
|
10587
|
-
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
|
|
10688
|
+
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 an embedding tier (HTTP endpoint, else a local static model) with lexical \u2014 the response then wraps the ranked list as `{ results, tier, degradedReason? }`, `tier` being "endpoint"/"static" when fusion happened or "lexical" (with `degradedReason`) when it did not (see embed_status). Without `semantic`, the response is the bare ranked array, unchanged.',
|
|
10588
10689
|
inputSchema: {
|
|
10589
10690
|
type: "object",
|
|
10590
10691
|
properties: {
|
|
@@ -10598,7 +10699,7 @@ var init_mcp = __esm({
|
|
|
10598
10699
|
},
|
|
10599
10700
|
semantic: {
|
|
10600
10701
|
type: "boolean",
|
|
10601
|
-
description:
|
|
10702
|
+
description: 'RRF-fuse an embedding tier with lexical (default false). Precedence: the HTTP endpoint (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model. The response reports the effective tier as a top-level `tier` field ("endpoint"/"static" on success, "lexical" plus `degradedReason` when neither is available/reachable) instead of degrading silently \u2014 see embed_status.'
|
|
10602
10703
|
}
|
|
10603
10704
|
},
|
|
10604
10705
|
required: ["repo", "query"]
|
|
@@ -11090,8 +11191,9 @@ Commands:
|
|
|
11090
11191
|
embed status Effective mode (none/static/endpoint), model +
|
|
11091
11192
|
EMBED_VERSION, and endpoint reachability (JSON)
|
|
11092
11193
|
embed build Write embeddings.bin into --out <dir> (static tier)
|
|
11093
|
-
embed pull Fetch the model asset into CODEINDEX_EMBED_DIR
|
|
11094
|
-
<repo>/.codeindex/models/)
|
|
11194
|
+
embed pull Fetch the official model asset into CODEINDEX_EMBED_DIR
|
|
11195
|
+
(or <repo>/.codeindex/models/); sha256-verified. Override
|
|
11196
|
+
the source with CODEINDEX_EMBED_URL
|
|
11095
11197
|
embed serve Print (or --run) the docker command that starts the
|
|
11096
11198
|
containerized embedding server (rich tier)
|
|
11097
11199
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
@@ -11371,26 +11473,20 @@ async function runCli(argv) {
|
|
|
11371
11473
|
process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
|
|
11372
11474
|
`);
|
|
11373
11475
|
} else if (sub === "pull") {
|
|
11374
|
-
const url = resolveEmbedPullUrl();
|
|
11375
|
-
if (!url) {
|
|
11376
|
-
process.stderr.write(
|
|
11377
|
-
"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"
|
|
11378
|
-
);
|
|
11379
|
-
process.exitCode = 1;
|
|
11380
|
-
return;
|
|
11381
|
-
}
|
|
11476
|
+
const { url, sha256 } = resolveEmbedPullUrl();
|
|
11382
11477
|
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join12(flags2.repo, ".codeindex", "models");
|
|
11383
11478
|
mkdirSync2(destDir, { recursive: true });
|
|
11384
11479
|
process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join12(destDir, "model.json")}
|
|
11385
11480
|
`);
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11481
|
+
let body2;
|
|
11482
|
+
try {
|
|
11483
|
+
body2 = await fetchEmbedModel(url, sha256);
|
|
11484
|
+
} catch (e) {
|
|
11485
|
+
process.stderr.write(`codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
|
|
11389
11486
|
`);
|
|
11390
11487
|
process.exitCode = 1;
|
|
11391
11488
|
return;
|
|
11392
11489
|
}
|
|
11393
|
-
const body2 = await res.text();
|
|
11394
11490
|
try {
|
|
11395
11491
|
JSON.parse(body2);
|
|
11396
11492
|
} catch {
|
|
@@ -11482,6 +11578,7 @@ export {
|
|
|
11482
11578
|
buildGraph,
|
|
11483
11579
|
buildIndexArtifacts,
|
|
11484
11580
|
buildModules,
|
|
11581
|
+
buildRawCallerIndex,
|
|
11485
11582
|
buildResolveContext,
|
|
11486
11583
|
buildSymbolIndex,
|
|
11487
11584
|
byKey,
|
package/src/callers.ts
CHANGED
|
Binary file
|
package/src/embed/model.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
|
|
@@ -20,12 +21,24 @@ import { join } from "node:path";
|
|
|
20
21
|
// graph.json / symbols.json consumers.
|
|
21
22
|
export const EMBED_VERSION = 1;
|
|
22
23
|
|
|
23
|
-
// The default
|
|
24
|
-
// URL is intentionally NOT hard-coded yet (see resolveEmbedPullUrl): the asset
|
|
25
|
-
// is unpublished, so `pull` fails cleanly asking for CODEINDEX_EMBED_URL rather
|
|
26
|
-
// than fetching a phantom.
|
|
24
|
+
// The default sub-directory a `pull` writes into and a `status` reports.
|
|
27
25
|
export const DEFAULT_EMBED_DIRNAME = "models";
|
|
28
26
|
|
|
27
|
+
// The official static-embedding asset, published as the dedicated GitHub release
|
|
28
|
+
// `embed-model-v1` (tag deliberately outside the `v<semver>` semantic-release
|
|
29
|
+
// namespace, so publishing it triggers no CI). `pull` uses this when
|
|
30
|
+
// CODEINDEX_EMBED_URL is unset. Reproduce the asset byte-for-byte with the pinned
|
|
31
|
+
// toolchain in scripts/embed-asset/.
|
|
32
|
+
export const DEFAULT_EMBED_URL =
|
|
33
|
+
"https://github.com/maxgfr/codeindex/releases/download/embed-model-v1/model.json";
|
|
34
|
+
|
|
35
|
+
// sha256 of the published model.json. `pull` verifies the downloaded bytes
|
|
36
|
+
// against this ONLY when it fetched the built-in default (a custom
|
|
37
|
+
// CODEINDEX_EMBED_URL is the user's own mirror and keeps the un-verified
|
|
38
|
+
// behavior). Regenerate alongside DEFAULT_EMBED_URL when re-cutting the asset:
|
|
39
|
+
// `shasum -a 256 model.json`.
|
|
40
|
+
export const EMBED_ASSET_SHA256 = "163ad053eab4e9a80d421ed4164f32292c83290f02fbbe6fe4b9b1cd6ea18d34";
|
|
41
|
+
|
|
29
42
|
// A loaded static-embedding model. `vocab` maps a wordpiece token to its row
|
|
30
43
|
// index; `weights` is the flat row-major (vocabSize × dim) matrix as IEEE-754
|
|
31
44
|
// doubles (float rows widen to double exactly, so mean-pool stays deterministic).
|
|
@@ -107,11 +120,37 @@ export function loadEmbedModel(dir?: string): StaticEmbedModel | undefined {
|
|
|
107
120
|
return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// the
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
// What `embed pull` fetches, and whether to verify it. CODEINDEX_EMBED_URL wins
|
|
124
|
+
// outright (the user's explicit override / private mirror) and carries NO
|
|
125
|
+
// sha256, so a custom asset keeps the current un-verified behavior. With no env,
|
|
126
|
+
// we fall back to the built-in official asset AND its pinned sha256, so `pull`
|
|
127
|
+
// can prove integrity of the default download.
|
|
128
|
+
export interface EmbedPullTarget {
|
|
129
|
+
url: string;
|
|
130
|
+
sha256?: string; // present only for the built-in default → verify
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function resolveEmbedPullUrl(): EmbedPullTarget {
|
|
134
|
+
const env = process.env.CODEINDEX_EMBED_URL;
|
|
135
|
+
if (env && env.trim()) return { url: env.trim() };
|
|
136
|
+
return { url: DEFAULT_EMBED_URL, sha256: EMBED_ASSET_SHA256 };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fetch a model.json body over HTTP, following redirects (GitHub release assets
|
|
140
|
+
// 302 to a CDN — native fetch follows by default). When `expectedSha256` is
|
|
141
|
+
// given, the downloaded bytes are hashed and MUST match or this throws with a
|
|
142
|
+
// clear message and returns nothing — so a corrupt/tampered default asset never
|
|
143
|
+
// gets written. A non-2xx response also throws. Verification and I/O are split
|
|
144
|
+
// from the CLI so both the success and the sha-mismatch paths are unit-testable.
|
|
145
|
+
export async function fetchEmbedModel(url: string, expectedSha256?: string): Promise<string> {
|
|
146
|
+
const res = await fetch(url);
|
|
147
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
|
|
148
|
+
const body = await res.text();
|
|
149
|
+
if (expectedSha256) {
|
|
150
|
+
const got = createHash("sha256").update(body).digest("hex");
|
|
151
|
+
if (got !== expectedSha256) {
|
|
152
|
+
throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return body;
|
|
117
156
|
}
|
package/src/engine-cli.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { symbolComplexity, riskHotspots } from "./complexity.js";
|
|
|
19
19
|
import { renderMermaid } from "./viz.js";
|
|
20
20
|
import { searchIndex } from "./bm25.js";
|
|
21
21
|
import { checkRules, parseRules } from "./rules.js";
|
|
22
|
-
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, resolveEmbedPullUrl } from "./embed/model.js";
|
|
22
|
+
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, resolveEmbedPullUrl, fetchEmbedModel } from "./embed/model.js";
|
|
23
23
|
import { buildEmbeddingIndex, serializeEmbeddings } from "./embed/index.js";
|
|
24
24
|
import { searchSemantic } from "./embed/search.js";
|
|
25
25
|
import {
|
|
@@ -55,8 +55,9 @@ Commands:
|
|
|
55
55
|
embed status Effective mode (none/static/endpoint), model +
|
|
56
56
|
EMBED_VERSION, and endpoint reachability (JSON)
|
|
57
57
|
embed build Write embeddings.bin into --out <dir> (static tier)
|
|
58
|
-
embed pull Fetch the model asset into CODEINDEX_EMBED_DIR
|
|
59
|
-
<repo>/.codeindex/models/)
|
|
58
|
+
embed pull Fetch the official model asset into CODEINDEX_EMBED_DIR
|
|
59
|
+
(or <repo>/.codeindex/models/); sha256-verified. Override
|
|
60
|
+
the source with CODEINDEX_EMBED_URL
|
|
60
61
|
embed serve Print (or --run) the docker command that starts the
|
|
61
62
|
containerized embedding server (rich tier)
|
|
62
63
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
@@ -387,26 +388,21 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
387
388
|
writeFileSync(join(flags.out, "embeddings.bin"), serializeEmbeddings(index));
|
|
388
389
|
process.stderr.write(`codeindex: ${index.records.length} embedding records → ${flags.out}/embeddings.bin (model ${model.modelId})\n`);
|
|
389
390
|
} else if (sub === "pull") {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
process.stderr.write(
|
|
394
|
-
"codeindex: no model URL configured. The official static-embedding asset is not published yet.\n" +
|
|
395
|
-
"Set CODEINDEX_EMBED_URL to a model.json URL (optionally CODEINDEX_EMBED_DIR as the destination), then re-run `codeindex embed pull`.\n",
|
|
396
|
-
);
|
|
397
|
-
process.exitCode = 1;
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
391
|
+
// Default: the official published asset + its pinned sha256. A user-set
|
|
392
|
+
// CODEINDEX_EMBED_URL overrides both (mirror/custom model, no verification).
|
|
393
|
+
const { url, sha256 } = resolveEmbedPullUrl();
|
|
400
394
|
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join(flags.repo, ".codeindex", "models");
|
|
401
395
|
mkdirSync(destDir, { recursive: true });
|
|
402
396
|
process.stderr.write(`codeindex: fetching model from ${url} → ${join(destDir, "model.json")}\n`);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
397
|
+
let body: string;
|
|
398
|
+
try {
|
|
399
|
+
// Follows redirects (GitHub → CDN) and verifies sha256 for the default asset.
|
|
400
|
+
body = await fetchEmbedModel(url, sha256);
|
|
401
|
+
} catch (e) {
|
|
402
|
+
process.stderr.write(`codeindex: pull failed — ${e instanceof Error ? e.message : String(e)} (nothing written)\n`);
|
|
406
403
|
process.exitCode = 1;
|
|
407
404
|
return;
|
|
408
405
|
}
|
|
409
|
-
const body = await res.text();
|
|
410
406
|
try {
|
|
411
407
|
JSON.parse(body);
|
|
412
408
|
} catch {
|
package/src/engine.ts
CHANGED
|
@@ -55,7 +55,7 @@ export { buildModules, isTestFile, tierForPath } from "./modules.js";
|
|
|
55
55
|
export type { ModuleInfo } from "./modules.js";
|
|
56
56
|
export { buildGraph, uniqueSymbolDefs } from "./graph.js";
|
|
57
57
|
export { resolveCallEdges } from "./calls.js";
|
|
58
|
-
export { buildCallerIndex, enclosingSymbol, computeImportPairs } from "./callers.js";
|
|
58
|
+
export { buildCallerIndex, buildRawCallerIndex, enclosingSymbol, computeImportPairs } from "./callers.js";
|
|
59
59
|
export { symbolsOverview, findSymbol, findReferences } from "./query.js";
|
|
60
60
|
export { resolveUniqueSymbol, replaceSymbolBody, insertAfterSymbol, insertBeforeSymbol } from "./edit.js";
|
|
61
61
|
export type { EditResult } from "./edit.js";
|
|
@@ -143,6 +143,10 @@ export type { ArchRule, ForbiddenEdgeRule, BuiltinRule, RuleSeverity, RuleViolat
|
|
|
143
143
|
// Caller-index recall mode (issue #7) — options for buildCallerIndex above.
|
|
144
144
|
export type { CallerIndexOptions } from "./callers.js";
|
|
145
145
|
|
|
146
|
+
// Raw-recall caller index (issue #8) — ungated, def-resolution-free companion
|
|
147
|
+
// to buildCallerIndex above; see its JSDoc in callers.ts for the contract.
|
|
148
|
+
export type { RawCallerIndex, RawCallerSite } from "./callers.js";
|
|
149
|
+
|
|
146
150
|
// Behavioral analytics (git-history mining) + the token-budgeted repo map.
|
|
147
151
|
export { changeCoupling, rankHotspots } from "./coupling.js";
|
|
148
152
|
export type { ChangeCoupling, CouplingOptions, Hotspot } from "./coupling.js";
|