@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.
- package/README.md +39 -0
- package/docs/MIGRATION.md +38 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +59 -2
- package/scripts/engine.mjs +560 -32
- package/src/bm25.ts +118 -9
- 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 +111 -5
- package/src/engine.ts +21 -0
- package/src/mcp.ts +48 -5
- package/src/types.ts +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { RepoScan } from "../scan.js";
|
|
2
|
+
import { rrf } from "../util.js";
|
|
3
|
+
import { byStr } from "../sort.js";
|
|
4
|
+
import { searchIndex, type SearchOptions, type SearchResult } from "../bm25.js";
|
|
5
|
+
import { encode, intDot } from "./encode.js";
|
|
6
|
+
import type { EmbeddingIndex } from "./index.js";
|
|
7
|
+
import type { StaticEmbedModel } from "./model.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_LIMIT = 20;
|
|
10
|
+
const RRF_K = 60; // reuse the engine-wide RRF damping constant
|
|
11
|
+
|
|
12
|
+
export interface SemanticSearchOptions extends SearchOptions {
|
|
13
|
+
// The loaded static model, needed to encode the QUERY into the same int8 space
|
|
14
|
+
// as the corpus. Absent → the search degrades to pure lexical (no throw).
|
|
15
|
+
model?: StaticEmbedModel;
|
|
16
|
+
// RRF damping (default 60), exposed for parity with the shared rrf helper.
|
|
17
|
+
rrfK?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A fused result. Extends the lexical SearchResult additively: `semanticSymbol`
|
|
21
|
+
// is the corpus symbol whose embedding was closest to the query for this file
|
|
22
|
+
// (absent when the file was contributed only by the lexical side, or when the
|
|
23
|
+
// search degraded to lexical).
|
|
24
|
+
export interface SemanticSearchResult extends SearchResult {
|
|
25
|
+
semanticSymbol?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// RRF-fused semantic + lexical search. The two rankings live on incomparable
|
|
29
|
+
// scales (BM25 score vs integer dot product), so we fuse by RANK via the shared
|
|
30
|
+
// `rrf` helper (k=60) rather than any linear score blend. Deterministic end to
|
|
31
|
+
// end: lexical is deterministic, the semantic ranking sorts by exact integer dot
|
|
32
|
+
// (ties broken by path), and the fused list sorts by RRF score (ties by path).
|
|
33
|
+
//
|
|
34
|
+
// DEGRADATION: with no model, no index, or an empty index, this returns the pure
|
|
35
|
+
// lexical ranking unchanged (as SemanticSearchResult[] with no `semanticSymbol`)
|
|
36
|
+
// — the caller stays on exit 0 and simply prints a note.
|
|
37
|
+
export function searchSemantic(
|
|
38
|
+
scan: RepoScan,
|
|
39
|
+
query: string,
|
|
40
|
+
index: EmbeddingIndex | undefined,
|
|
41
|
+
opts: SemanticSearchOptions = {},
|
|
42
|
+
): SemanticSearchResult[] {
|
|
43
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
44
|
+
// Pull a deeper lexical list than the final limit so RRF has enough overlap to
|
|
45
|
+
// fuse meaningfully, then trim after fusion.
|
|
46
|
+
const lexical = searchIndex(scan, query, { limit: Math.max(limit, 50), fuzzy: opts.fuzzy });
|
|
47
|
+
|
|
48
|
+
if (!opts.model || !index || index.records.length === 0) {
|
|
49
|
+
return lexical.slice(0, limit); // pure-lexical degradation
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const q = encode(opts.model, query);
|
|
53
|
+
// Best (highest integer dot) record per file, remembering its symbol.
|
|
54
|
+
const bestByFile = new Map<string, { score: number; symbol?: string }>();
|
|
55
|
+
for (const r of index.records) {
|
|
56
|
+
const dot = intDot(q, r.vec);
|
|
57
|
+
const prev = bestByFile.get(r.file);
|
|
58
|
+
if (!prev || dot > prev.score) bestByFile.set(r.file, { score: dot, symbol: r.symbol });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Semantic ranked file list: positive similarity only, best-first, ties by path.
|
|
62
|
+
const semList = [...bestByFile.entries()]
|
|
63
|
+
.filter(([, v]) => v.score > 0)
|
|
64
|
+
.sort((a, b) => b[1].score - a[1].score || byStr(a[0], b[0]))
|
|
65
|
+
.map(([file]) => file);
|
|
66
|
+
const lexList = lexical.map((r) => r.file);
|
|
67
|
+
|
|
68
|
+
// Fuse the two ranked lists (identity keyOf — items are already file paths).
|
|
69
|
+
const fused = rrf<string>([lexList, semList], (f) => f, opts.rrfK ?? RRF_K);
|
|
70
|
+
|
|
71
|
+
const lexByFile = new Map(lexical.map((r) => [r.file, r] as const));
|
|
72
|
+
const results: SemanticSearchResult[] = [...fused.entries()]
|
|
73
|
+
.sort((a, b) => b[1] - a[1] || byStr(a[0], b[0]))
|
|
74
|
+
.map(([file, score]) => {
|
|
75
|
+
const lex = lexByFile.get(file);
|
|
76
|
+
const res: SemanticSearchResult = {
|
|
77
|
+
file,
|
|
78
|
+
score: Number(score.toFixed(4)),
|
|
79
|
+
matchedTerms: lex?.matchedTerms ?? [],
|
|
80
|
+
topSymbols: lex?.topSymbols ?? [],
|
|
81
|
+
};
|
|
82
|
+
const sem = bestByFile.get(file);
|
|
83
|
+
if (sem?.symbol) res.semanticSymbol = sem.symbol;
|
|
84
|
+
if (lex?.fuzzyTerms) res.fuzzyTerms = lex.fuzzyTerms;
|
|
85
|
+
return res;
|
|
86
|
+
});
|
|
87
|
+
return results.slice(0, limit);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { DEFAULT_LIMIT as SEMANTIC_DEFAULT_LIMIT };
|
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)
|
|
@@ -63,6 +73,10 @@ Flags:
|
|
|
63
73
|
--no-ast Skip tree-sitter grammars even when present (regex tier)
|
|
64
74
|
--config <file> Rules config for \`rules\` (JSON: [{name, from, to, …}])
|
|
65
75
|
--limit <n> Max results for \`search\` (default 20)
|
|
76
|
+
--no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
|
|
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)
|
|
66
80
|
--recall \`callers\`: recall-oriented binding (issue #7) — relaxes
|
|
67
81
|
the JS/TS import gate to unique repo-wide names and labels
|
|
68
82
|
each site corroborated|unique-name
|
|
@@ -84,13 +98,15 @@ interface CliFlags {
|
|
|
84
98
|
budgetTokens?: number;
|
|
85
99
|
config?: string; // rules config path
|
|
86
100
|
limit?: number; // search result cap
|
|
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)
|
|
87
103
|
recall?: boolean; // callers: recall-oriented binding
|
|
88
104
|
projectRoot?: string; // scip: override Metadata.project_root
|
|
89
105
|
positional?: string; // e.g. the grep pattern or search query
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
function parseFlags(args: string[]): CliFlags {
|
|
93
|
-
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false };
|
|
109
|
+
const flags: CliFlags = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
|
|
94
110
|
for (let i = 0; i < args.length; i++) {
|
|
95
111
|
const a = args[i]!;
|
|
96
112
|
const next = (): string => {
|
|
@@ -122,6 +138,8 @@ function parseFlags(args: string[]): CliFlags {
|
|
|
122
138
|
else if (a === "--since") flags.since = next();
|
|
123
139
|
else if (a === "--config") flags.config = resolve(next());
|
|
124
140
|
else if (a === "--limit") flags.limit = num();
|
|
141
|
+
else if (a === "--no-fuzzy") flags.fuzzy = false;
|
|
142
|
+
else if (a === "--semantic") flags.semantic = true;
|
|
125
143
|
else if (a === "--recall") flags.recall = true;
|
|
126
144
|
else if (!a.startsWith("--") && flags.positional === undefined) flags.positional = a;
|
|
127
145
|
else throw new Error(`unknown flag: ${a}`);
|
|
@@ -199,7 +217,18 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
199
217
|
cachePath,
|
|
200
218
|
JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n",
|
|
201
219
|
);
|
|
202
|
-
|
|
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`);
|
|
203
232
|
} else if (cmd === "scan") {
|
|
204
233
|
const { scan } = buildIndexArtifacts(flags.repo, scanOptions(flags));
|
|
205
234
|
const summary = {
|
|
@@ -234,8 +263,85 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
234
263
|
} else if (cmd === "search") {
|
|
235
264
|
if (!flags.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
|
|
236
265
|
const scan = scanRepo(flags.repo, scanOptions(flags));
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
}
|
|
239
345
|
} else if (cmd === "rules") {
|
|
240
346
|
if (!flags.config) throw new Error("rules needs --config <codeindex.rules.json>");
|
|
241
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.',
|
|
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: {
|
|
@@ -274,10 +277,26 @@ const TOOLS = [
|
|
|
274
277
|
...scopeProps,
|
|
275
278
|
query: { type: "string", description: "Natural-language or identifier query" },
|
|
276
279
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
280
|
+
fuzzy: {
|
|
281
|
+
type: "boolean",
|
|
282
|
+
description:
|
|
283
|
+
"Trigram fuzzy fallback for query terms with zero document frequency (default true)",
|
|
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
|
+
},
|
|
277
290
|
},
|
|
278
291
|
required: ["repo", "query"],
|
|
279
292
|
},
|
|
280
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
|
+
},
|
|
281
300
|
{
|
|
282
301
|
name: "check_rules",
|
|
283
302
|
description:
|
|
@@ -435,10 +454,34 @@ function callTool(name: string, args: Record<string, unknown>): string {
|
|
|
435
454
|
if (name === "search") {
|
|
436
455
|
const query = str(args.query);
|
|
437
456
|
if (!query) throw new Error("`query` is required");
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
);
|
|
442
485
|
}
|
|
443
486
|
if (name === "check_rules") {
|
|
444
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
|