@maxgfr/codeindex 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/docs/MIGRATION.md +29 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +57 -2
- package/scripts/engine.mjs +474 -23
- package/src/embed/encode.ts +133 -0
- package/src/embed/endpoint.ts +55 -0
- package/src/embed/index.ts +125 -0
- package/src/embed/model.ts +117 -0
- package/src/embed/search.ts +90 -0
- package/src/engine-cli.ts +107 -5
- package/src/engine.ts +21 -0
- package/src/mcp.ts +43 -6
- package/src/types.ts +1 -1
package/src/engine-cli.ts
CHANGED
|
@@ -19,6 +19,9 @@ 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";
|
|
23
|
+
import { buildEmbeddingIndex, serializeEmbeddings } from "./embed/index.js";
|
|
24
|
+
import { searchSemantic } from "./embed/search.js";
|
|
22
25
|
|
|
23
26
|
const HELP = `codeindex engine v${ENGINE_VERSION} — deterministic repo indexing
|
|
24
27
|
|
|
@@ -37,7 +40,14 @@ Commands:
|
|
|
37
40
|
churn Per-file git commit counts (JSON; --since <ref> to bound)
|
|
38
41
|
grep Search: cli.mjs grep <pattern> --repo <dir> (JSON hits)
|
|
39
42
|
search Keyless BM25 lexical search over symbol names, path segments,
|
|
40
|
-
markdown headings and summaries: cli.mjs search "<query>" --repo <dir
|
|
43
|
+
markdown headings and summaries: cli.mjs search "<query>" --repo <dir>.
|
|
44
|
+
--semantic fuses in the deterministic static-embedding tier (RRF)
|
|
45
|
+
when a model is present; degrades to lexical (exit 0) when absent
|
|
46
|
+
embed Deterministic static-embedding tier (opt-in by model asset):
|
|
47
|
+
embed status Report the resolved model + EMBED_VERSION (JSON)
|
|
48
|
+
embed build Write embeddings.bin into --out <dir> from the repo
|
|
49
|
+
embed pull Fetch the model asset into CODEINDEX_EMBED_DIR (or
|
|
50
|
+
<repo>/.codeindex/models/) — needs CODEINDEX_EMBED_URL
|
|
41
51
|
rules Architecture rules (forbidden edges, cycles, orphans) validated
|
|
42
52
|
against the link-graph: --config <codeindex.rules.json>; exits 1
|
|
43
53
|
on any error-severity violation (a CI gate)
|
|
@@ -65,6 +75,8 @@ Flags:
|
|
|
65
75
|
--limit <n> Max results for \`search\` (default 20)
|
|
66
76
|
--no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
|
|
67
77
|
with zero document frequency (default: enabled)
|
|
78
|
+
--semantic \`search\`: RRF-fuse the deterministic static-embedding tier
|
|
79
|
+
with lexical (needs a model asset; lexical-only otherwise)
|
|
68
80
|
--recall \`callers\`: recall-oriented binding (issue #7) — relaxes
|
|
69
81
|
the JS/TS import gate to unique repo-wide names and labels
|
|
70
82
|
each site corroborated|unique-name
|
|
@@ -87,13 +99,14 @@ interface CliFlags {
|
|
|
87
99
|
config?: string; // rules config path
|
|
88
100
|
limit?: number; // search result cap
|
|
89
101
|
fuzzy: boolean; // search: trigram fuzzy fallback for df==0 terms (default true)
|
|
102
|
+
semantic: boolean; // search: RRF-fuse the static-embedding tier (default false)
|
|
90
103
|
recall?: boolean; // callers: recall-oriented binding
|
|
91
104
|
projectRoot?: string; // scip: override Metadata.project_root
|
|
92
105
|
positional?: string; // e.g. the grep pattern or search query
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
function parseFlags(args: string[]): CliFlags {
|
|
96
|
-
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true };
|
|
109
|
+
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
97
110
|
for (let i = 0; i < args.length; i++) {
|
|
98
111
|
const a = args[i]!;
|
|
99
112
|
const next = (): string => {
|
|
@@ -126,6 +139,7 @@ function parseFlags(args: string[]): CliFlags {
|
|
|
126
139
|
else if (a === "--config") flags.config = resolve(next());
|
|
127
140
|
else if (a === "--limit") flags.limit = num();
|
|
128
141
|
else if (a === "--no-fuzzy") flags.fuzzy = false;
|
|
142
|
+
else if (a === "--semantic") flags.semantic = true;
|
|
129
143
|
else if (a === "--recall") flags.recall = true;
|
|
130
144
|
else if (!a.startsWith("--") && flags.positional === undefined) flags.positional = a;
|
|
131
145
|
else throw new Error(`unknown flag: ${a}`);
|
|
@@ -203,7 +217,18 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
203
217
|
cachePath,
|
|
204
218
|
JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n",
|
|
205
219
|
);
|
|
206
|
-
|
|
220
|
+
// Deterministic embeddings sidecar: written next to graph.json ONLY when a
|
|
221
|
+
// model asset is present (opt-in). Silently skipped otherwise — no model, no
|
|
222
|
+
// embeddings.bin, no impact on the graph/symbols consumers.
|
|
223
|
+
let embedNote = "";
|
|
224
|
+
const modelDir = resolveEmbedModelDir(flags.repo);
|
|
225
|
+
const model = modelDir ? loadEmbedModel(modelDir) : undefined;
|
|
226
|
+
if (model) {
|
|
227
|
+
const index = buildEmbeddingIndex(scan, model);
|
|
228
|
+
writeFileSync(join(outDir, "embeddings.bin"), serializeEmbeddings(index));
|
|
229
|
+
embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
|
|
230
|
+
}
|
|
231
|
+
process.stderr.write(`codeindex: ${scan.files.length} files → ${outDir}/graph.json + symbols.json${embedNote}${scan.capped ? " (capped)" : ""}\n`);
|
|
207
232
|
} else if (cmd === "scan") {
|
|
208
233
|
const { scan } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
209
234
|
const summary = {
|
|
@@ -238,8 +263,85 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
238
263
|
} else if (cmd === "search") {
|
|
239
264
|
if (!flags.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
|
|
240
265
|
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
241
|
-
|
|
242
|
-
|
|
266
|
+
if (flags.semantic) {
|
|
267
|
+
const modelDir = resolveEmbedModelDir(flags.repo);
|
|
268
|
+
const model = modelDir ? loadEmbedModel(modelDir) : undefined;
|
|
269
|
+
if (!model) {
|
|
270
|
+
// Degradation: --semantic without a model → lexical results + a stderr
|
|
271
|
+
// note, exit 0. The results shape is a superset of the lexical one.
|
|
272
|
+
process.stderr.write(
|
|
273
|
+
"codeindex: semantic search unavailable (no embedding model present) — returning lexical results; run `codeindex embed pull` to enable it\n",
|
|
274
|
+
);
|
|
275
|
+
const results = searchIndex(scan, flags.positional, { limit: flags.limit, fuzzy: flags.fuzzy });
|
|
276
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags.out);
|
|
277
|
+
} else {
|
|
278
|
+
const index = buildEmbeddingIndex(scan, model);
|
|
279
|
+
const results = searchSemantic(scan, flags.positional, index, { model, limit: flags.limit, fuzzy: flags.fuzzy });
|
|
280
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags.out);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
const results = searchIndex(scan, flags.positional, { limit: flags.limit, fuzzy: flags.fuzzy });
|
|
284
|
+
emit(JSON.stringify(results, null, 2) + "\n", flags.out);
|
|
285
|
+
}
|
|
286
|
+
} else if (cmd === "embed") {
|
|
287
|
+
const sub = flags.positional;
|
|
288
|
+
const modelDir = resolveEmbedModelDir(flags.repo);
|
|
289
|
+
if (sub === "status") {
|
|
290
|
+
const model = modelDir ? loadEmbedModel(modelDir) : undefined;
|
|
291
|
+
const status = {
|
|
292
|
+
embedVersion: EMBED_VERSION,
|
|
293
|
+
model: model
|
|
294
|
+
? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize }
|
|
295
|
+
: { present: false },
|
|
296
|
+
endpoint: process.env.CODEINDEX_EMBED_ENDPOINT ?? null,
|
|
297
|
+
};
|
|
298
|
+
emit(JSON.stringify(status, null, 2) + "\n", flags.out);
|
|
299
|
+
} else if (sub === "build") {
|
|
300
|
+
if (!flags.out) throw new Error("embed build needs --out <dir>");
|
|
301
|
+
if (!modelDir) {
|
|
302
|
+
process.stderr.write("codeindex: no embedding model present — run `codeindex embed pull` first (nothing written)\n");
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const model = loadEmbedModel(modelDir)!;
|
|
307
|
+
mkdirSync(flags.out, { recursive: true });
|
|
308
|
+
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
309
|
+
const index = buildEmbeddingIndex(scan, model);
|
|
310
|
+
writeFileSync(join(flags.out, "embeddings.bin"), serializeEmbeddings(index));
|
|
311
|
+
process.stderr.write(`codeindex: ${index.records.length} embedding records → ${flags.out}/embeddings.bin (model ${model.modelId})\n`);
|
|
312
|
+
} else if (sub === "pull") {
|
|
313
|
+
const url = resolveEmbedPullUrl();
|
|
314
|
+
if (!url) {
|
|
315
|
+
// Clean, actionable failure — the official asset is not published yet.
|
|
316
|
+
process.stderr.write(
|
|
317
|
+
"codeindex: no model URL configured. The official static-embedding asset is not published yet.\n" +
|
|
318
|
+
"Set CODEINDEX_EMBED_URL to a model.json URL (optionally CODEINDEX_EMBED_DIR as the destination), then re-run `codeindex embed pull`.\n",
|
|
319
|
+
);
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const destDir = process.env.CODEINDEX_EMBED_DIR ?? join(flags.repo, ".codeindex", "models");
|
|
324
|
+
mkdirSync(destDir, { recursive: true });
|
|
325
|
+
process.stderr.write(`codeindex: fetching model from ${url} → ${join(destDir, "model.json")}\n`);
|
|
326
|
+
const res = await fetch(url);
|
|
327
|
+
if (!res.ok) {
|
|
328
|
+
process.stderr.write(`codeindex: pull failed — HTTP ${res.status} from ${url}\n`);
|
|
329
|
+
process.exitCode = 1;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const body = await res.text();
|
|
333
|
+
try {
|
|
334
|
+
JSON.parse(body);
|
|
335
|
+
} catch {
|
|
336
|
+
process.stderr.write("codeindex: pull failed — response is not a valid model.json (expected JSON)\n");
|
|
337
|
+
process.exitCode = 1;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
writeFileSync(join(destDir, "model.json"), body);
|
|
341
|
+
process.stderr.write(`codeindex: model written to ${join(destDir, "model.json")}\n`);
|
|
342
|
+
} else {
|
|
343
|
+
throw new Error("embed needs a subcommand: pull | build | status");
|
|
344
|
+
}
|
|
243
345
|
} else if (cmd === "rules") {
|
|
244
346
|
if (!flags.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
245
347
|
const rules = parseRules(JSON.parse(readFileSync(flags.config, "utf8")));
|
package/src/engine.ts
CHANGED
|
@@ -103,6 +103,27 @@ export type { SearchHit, GrepOptions } from "./grep.js";
|
|
|
103
103
|
export { searchIndex, subtokens } from "./bm25.js";
|
|
104
104
|
export type { SearchOptions, SearchResult } from "./bm25.js";
|
|
105
105
|
|
|
106
|
+
// Deterministic static-embedding tier (v2.10.0): a keyless, byte-deterministic
|
|
107
|
+
// semantic search, opt-in by model-asset presence (models NEVER ship in the
|
|
108
|
+
// tarball). EMBED_VERSION is dedicated to the embeddings.bin sidecar and is
|
|
109
|
+
// independent of SCHEMA_VERSION / EXTRACTOR_VERSION.
|
|
110
|
+
export {
|
|
111
|
+
EMBED_VERSION,
|
|
112
|
+
resolveEmbedModelDir,
|
|
113
|
+
hasEmbedModel,
|
|
114
|
+
loadEmbedModel,
|
|
115
|
+
resolveEmbedPullUrl,
|
|
116
|
+
} from "./embed/model.js";
|
|
117
|
+
export type { StaticEmbedModel } from "./embed/model.js";
|
|
118
|
+
export { encode, tokenize, wordpiece, basicTokenize, roundHalfToEven, intDot } from "./embed/encode.js";
|
|
119
|
+
export { buildEmbeddingIndex, serializeEmbeddings, deserializeEmbeddings } from "./embed/index.js";
|
|
120
|
+
export type { EmbeddingIndex, EmbeddingRecord } from "./embed/index.js";
|
|
121
|
+
export { searchSemantic } from "./embed/search.js";
|
|
122
|
+
export type { SemanticSearchOptions, SemanticSearchResult } from "./embed/search.js";
|
|
123
|
+
// HTTP endpoint tier (v2.11 preview — NOT wired into the CLI/MCP here).
|
|
124
|
+
export { embedViaEndpoint, resolveEmbedEndpoint } from "./embed/endpoint.js";
|
|
125
|
+
export type { EmbedEndpointOptions } from "./embed/endpoint.js";
|
|
126
|
+
|
|
106
127
|
// Architecture rules: forbidden edges + cycles/orphans builtins (issue #4).
|
|
107
128
|
export { checkRules, parseRules } from "./rules.js";
|
|
108
129
|
export type { ArchRule, ForbiddenEdgeRule, BuiltinRule, RuleSeverity, RuleViolation } from "./rules.js";
|
package/src/mcp.ts
CHANGED
|
@@ -25,6 +25,9 @@ import { replaceSymbolBody, insertAfterSymbol, insertBeforeSymbol } from "./edit
|
|
|
25
25
|
import { writeMemory, readMemory, deleteMemory, listMemories } from "./memory.js";
|
|
26
26
|
import { searchIndex } from "./bm25.js";
|
|
27
27
|
import { checkRules, parseRules } from "./rules.js";
|
|
28
|
+
import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel } from "./embed/model.js";
|
|
29
|
+
import { buildEmbeddingIndex } from "./embed/index.js";
|
|
30
|
+
import { searchSemantic } from "./embed/search.js";
|
|
28
31
|
|
|
29
32
|
interface RpcRequest {
|
|
30
33
|
jsonrpc: "2.0";
|
|
@@ -266,7 +269,7 @@ const TOOLS = [
|
|
|
266
269
|
{
|
|
267
270
|
name: "search",
|
|
268
271
|
description:
|
|
269
|
-
'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 — 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`.',
|
|
272
|
+
'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 — 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 — see embed_status).',
|
|
270
273
|
inputSchema: {
|
|
271
274
|
type: "object",
|
|
272
275
|
properties: {
|
|
@@ -279,10 +282,21 @@ const TOOLS = [
|
|
|
279
282
|
description:
|
|
280
283
|
"Trigram fuzzy fallback for query terms with zero document frequency (default true)",
|
|
281
284
|
},
|
|
285
|
+
semantic: {
|
|
286
|
+
type: "boolean",
|
|
287
|
+
description:
|
|
288
|
+
"RRF-fuse the deterministic static-embedding tier with lexical when a model asset is present (default false; silently lexical-only when no model)",
|
|
289
|
+
},
|
|
282
290
|
},
|
|
283
291
|
required: ["repo", "query"],
|
|
284
292
|
},
|
|
285
293
|
},
|
|
294
|
+
{
|
|
295
|
+
name: "embed_status",
|
|
296
|
+
description:
|
|
297
|
+
"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.",
|
|
298
|
+
inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] },
|
|
299
|
+
},
|
|
286
300
|
{
|
|
287
301
|
name: "check_rules",
|
|
288
302
|
description:
|
|
@@ -440,11 +454,34 @@ function callTool(name: string, args: Record<string, unknown>): string {
|
|
|
440
454
|
if (name === "search") {
|
|
441
455
|
const query = str(args.query);
|
|
442
456
|
if (!query) throw new Error("`query` is required");
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
457
|
+
const scan = scanRepo(repo, scanOpts);
|
|
458
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
459
|
+
const fuzzy = typeof args.fuzzy === "boolean" ? args.fuzzy : undefined;
|
|
460
|
+
if (args.semantic === true) {
|
|
461
|
+
const modelDir = resolveEmbedModelDir(repo);
|
|
462
|
+
const model = modelDir ? loadEmbedModel(modelDir) : undefined;
|
|
463
|
+
if (model) {
|
|
464
|
+
const index = buildEmbeddingIndex(scan, model);
|
|
465
|
+
return JSON.stringify(searchSemantic(scan, query, index, { model, limit, fuzzy }), null, 2);
|
|
466
|
+
}
|
|
467
|
+
// Degrade to lexical (opt-in tier not activated) — no throw.
|
|
468
|
+
}
|
|
469
|
+
return JSON.stringify(searchIndex(scan, query, { limit, fuzzy }), null, 2);
|
|
470
|
+
}
|
|
471
|
+
if (name === "embed_status") {
|
|
472
|
+
const modelDir = resolveEmbedModelDir(repo);
|
|
473
|
+
const model = modelDir ? loadEmbedModel(modelDir) : undefined;
|
|
474
|
+
return JSON.stringify(
|
|
475
|
+
{
|
|
476
|
+
embedVersion: EMBED_VERSION,
|
|
477
|
+
model: model
|
|
478
|
+
? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize }
|
|
479
|
+
: { present: false },
|
|
480
|
+
endpoint: process.env.CODEINDEX_EMBED_ENDPOINT ?? null,
|
|
481
|
+
},
|
|
482
|
+
null,
|
|
483
|
+
2,
|
|
484
|
+
);
|
|
448
485
|
}
|
|
449
486
|
if (name === "check_rules") {
|
|
450
487
|
const rules = parseRules(args.rules); // throws a descriptive error on a malformed payload
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Single source of truth for the engine version the bundle reports. Kept in
|
|
2
2
|
// lockstep with package.json by the release pipeline. Do not edit by hand
|
|
3
3
|
// outside a release.
|
|
4
|
-
export const ENGINE_VERSION = "2.
|
|
4
|
+
export const ENGINE_VERSION = "2.10.0";
|
|
5
5
|
|
|
6
6
|
// Bumped whenever the on-disk artifact shape changes, so a consumer can reject
|
|
7
7
|
// an index written by an incompatible engine instead of misreading it. The
|