opencode-codebase-index 0.5.0 → 0.5.2
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 +76 -10
- package/commands/call-graph.md +24 -0
- package/dist/cli.cjs +1393 -47
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1393 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1393 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1393 -47
- package/dist/index.js.map +1 -1
- package/native/codebase-index-native.darwin-arm64.node +0 -0
- package/native/codebase-index-native.darwin-x64.node +0 -0
- package/native/codebase-index-native.linux-arm64-gnu.node +0 -0
- package/native/codebase-index-native.linux-x64-gnu.node +0 -0
- package/native/codebase-index-native.win32-x64-msvc.node +0 -0
- package/package.json +2 -3
- package/skill/SKILL.md +13 -1
package/dist/cli.cjs
CHANGED
|
@@ -776,9 +776,15 @@ function getDefaultSearchConfig() {
|
|
|
776
776
|
minScore: 0.1,
|
|
777
777
|
includeContext: true,
|
|
778
778
|
hybridWeight: 0.5,
|
|
779
|
+
fusionStrategy: "rrf",
|
|
780
|
+
rrfK: 60,
|
|
781
|
+
rerankTopN: 20,
|
|
779
782
|
contextLines: 0
|
|
780
783
|
};
|
|
781
784
|
}
|
|
785
|
+
function isValidFusionStrategy(value) {
|
|
786
|
+
return value === "weighted" || value === "rrf";
|
|
787
|
+
}
|
|
782
788
|
function getDefaultDebugConfig() {
|
|
783
789
|
return {
|
|
784
790
|
enabled: false,
|
|
@@ -833,6 +839,9 @@ function parseConfig(raw) {
|
|
|
833
839
|
minScore: typeof rawSearch.minScore === "number" ? rawSearch.minScore : defaultSearch.minScore,
|
|
834
840
|
includeContext: typeof rawSearch.includeContext === "boolean" ? rawSearch.includeContext : defaultSearch.includeContext,
|
|
835
841
|
hybridWeight: typeof rawSearch.hybridWeight === "number" ? Math.min(1, Math.max(0, rawSearch.hybridWeight)) : defaultSearch.hybridWeight,
|
|
842
|
+
fusionStrategy: isValidFusionStrategy(rawSearch.fusionStrategy) ? rawSearch.fusionStrategy : defaultSearch.fusionStrategy,
|
|
843
|
+
rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
|
|
844
|
+
rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
|
|
836
845
|
contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines
|
|
837
846
|
};
|
|
838
847
|
const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
|
|
@@ -848,7 +857,32 @@ function parseConfig(raw) {
|
|
|
848
857
|
};
|
|
849
858
|
let embeddingProvider;
|
|
850
859
|
let embeddingModel = void 0;
|
|
851
|
-
|
|
860
|
+
let customProvider = void 0;
|
|
861
|
+
if (input.embeddingProvider === "custom") {
|
|
862
|
+
embeddingProvider = "custom";
|
|
863
|
+
const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
|
|
864
|
+
if (rawCustom && typeof rawCustom.baseUrl === "string" && rawCustom.baseUrl.trim().length > 0 && typeof rawCustom.model === "string" && rawCustom.model.trim().length > 0 && typeof rawCustom.dimensions === "number" && Number.isInteger(rawCustom.dimensions) && rawCustom.dimensions > 0) {
|
|
865
|
+
customProvider = {
|
|
866
|
+
baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
|
|
867
|
+
model: rawCustom.model,
|
|
868
|
+
dimensions: rawCustom.dimensions,
|
|
869
|
+
apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
|
|
870
|
+
maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
|
|
871
|
+
timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
|
|
872
|
+
concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
|
|
873
|
+
requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
|
|
874
|
+
};
|
|
875
|
+
if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
|
|
876
|
+
console.warn(
|
|
877
|
+
`[codebase-index] Warning: customProvider.baseUrl ("${customProvider.baseUrl}") does not end with an API version path like /v1. The plugin appends /embeddings automatically, so the full URL will be "${customProvider.baseUrl}/embeddings". If your provider expects /v1/embeddings, set baseUrl to "${customProvider.baseUrl}/v1".`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
} else {
|
|
881
|
+
throw new Error(
|
|
882
|
+
"embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
} else if (isValidProvider(input.embeddingProvider)) {
|
|
852
886
|
embeddingProvider = input.embeddingProvider;
|
|
853
887
|
if (input.embeddingModel) {
|
|
854
888
|
embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
|
|
@@ -859,6 +893,7 @@ function parseConfig(raw) {
|
|
|
859
893
|
return {
|
|
860
894
|
embeddingProvider,
|
|
861
895
|
embeddingModel,
|
|
896
|
+
customProvider,
|
|
862
897
|
scope: isValidScope(input.scope) ? input.scope : "project",
|
|
863
898
|
include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
|
|
864
899
|
exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
|
|
@@ -2028,12 +2063,39 @@ function getProviderDisplayName(provider) {
|
|
|
2028
2063
|
return "Google (Gemini)";
|
|
2029
2064
|
case "ollama":
|
|
2030
2065
|
return "Ollama (Local)";
|
|
2066
|
+
case "custom":
|
|
2067
|
+
return "Custom (OpenAI-compatible)";
|
|
2031
2068
|
default:
|
|
2032
2069
|
return provider;
|
|
2033
2070
|
}
|
|
2034
2071
|
}
|
|
2072
|
+
function createCustomProviderInfo(config) {
|
|
2073
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2074
|
+
return {
|
|
2075
|
+
provider: "custom",
|
|
2076
|
+
credentials: {
|
|
2077
|
+
provider: "custom",
|
|
2078
|
+
baseUrl,
|
|
2079
|
+
apiKey: config.apiKey
|
|
2080
|
+
},
|
|
2081
|
+
modelInfo: {
|
|
2082
|
+
provider: "custom",
|
|
2083
|
+
model: config.model,
|
|
2084
|
+
dimensions: config.dimensions,
|
|
2085
|
+
maxTokens: config.maxTokens ?? 8192,
|
|
2086
|
+
costPer1MTokens: 0,
|
|
2087
|
+
timeoutMs: config.timeoutMs ?? 3e4
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2035
2091
|
|
|
2036
2092
|
// src/embeddings/provider.ts
|
|
2093
|
+
var CustomProviderNonRetryableError = class extends Error {
|
|
2094
|
+
constructor(message) {
|
|
2095
|
+
super(message);
|
|
2096
|
+
this.name = "CustomProviderNonRetryableError";
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2037
2099
|
function createEmbeddingProvider(configuredProviderInfo) {
|
|
2038
2100
|
switch (configuredProviderInfo.provider) {
|
|
2039
2101
|
case "github-copilot":
|
|
@@ -2044,6 +2106,8 @@ function createEmbeddingProvider(configuredProviderInfo) {
|
|
|
2044
2106
|
return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
|
|
2045
2107
|
case "ollama":
|
|
2046
2108
|
return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
|
|
2109
|
+
case "custom":
|
|
2110
|
+
return new CustomEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
|
|
2047
2111
|
default: {
|
|
2048
2112
|
const _exhaustive = configuredProviderInfo;
|
|
2049
2113
|
throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
|
|
@@ -2278,6 +2342,90 @@ var OllamaEmbeddingProvider = class {
|
|
|
2278
2342
|
return this.modelInfo;
|
|
2279
2343
|
}
|
|
2280
2344
|
};
|
|
2345
|
+
var CustomEmbeddingProvider = class {
|
|
2346
|
+
constructor(credentials, modelInfo) {
|
|
2347
|
+
this.credentials = credentials;
|
|
2348
|
+
this.modelInfo = modelInfo;
|
|
2349
|
+
}
|
|
2350
|
+
async embedQuery(query) {
|
|
2351
|
+
const result = await this.embedBatch([query]);
|
|
2352
|
+
return {
|
|
2353
|
+
embedding: result.embeddings[0],
|
|
2354
|
+
tokensUsed: result.totalTokensUsed
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
async embedDocument(document) {
|
|
2358
|
+
const result = await this.embedBatch([document]);
|
|
2359
|
+
return {
|
|
2360
|
+
embedding: result.embeddings[0],
|
|
2361
|
+
tokensUsed: result.totalTokensUsed
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
async embedBatch(texts) {
|
|
2365
|
+
const headers = {
|
|
2366
|
+
"Content-Type": "application/json"
|
|
2367
|
+
};
|
|
2368
|
+
if (this.credentials.apiKey) {
|
|
2369
|
+
headers["Authorization"] = `Bearer ${this.credentials.apiKey}`;
|
|
2370
|
+
}
|
|
2371
|
+
const baseUrl = this.credentials.baseUrl ?? "";
|
|
2372
|
+
const timeoutMs = this.modelInfo.timeoutMs;
|
|
2373
|
+
const controller = new AbortController();
|
|
2374
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2375
|
+
let response;
|
|
2376
|
+
try {
|
|
2377
|
+
response = await fetch(`${baseUrl}/embeddings`, {
|
|
2378
|
+
method: "POST",
|
|
2379
|
+
headers,
|
|
2380
|
+
body: JSON.stringify({
|
|
2381
|
+
model: this.modelInfo.model,
|
|
2382
|
+
input: texts
|
|
2383
|
+
}),
|
|
2384
|
+
signal: controller.signal
|
|
2385
|
+
});
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2388
|
+
throw new Error(`Custom embedding API request timed out after ${timeoutMs}ms for ${baseUrl}/embeddings`);
|
|
2389
|
+
}
|
|
2390
|
+
throw error;
|
|
2391
|
+
} finally {
|
|
2392
|
+
clearTimeout(timeout);
|
|
2393
|
+
}
|
|
2394
|
+
if (!response.ok) {
|
|
2395
|
+
const errorText = await response.text();
|
|
2396
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
2397
|
+
throw new CustomProviderNonRetryableError(`Custom embedding API error (non-retryable): ${response.status} - ${errorText}`);
|
|
2398
|
+
}
|
|
2399
|
+
throw new Error(`Custom embedding API error: ${response.status} - ${errorText}`);
|
|
2400
|
+
}
|
|
2401
|
+
const data = await response.json();
|
|
2402
|
+
if (data.data && Array.isArray(data.data)) {
|
|
2403
|
+
if (data.data.length > 0) {
|
|
2404
|
+
const actualDims = data.data[0].embedding.length;
|
|
2405
|
+
if (actualDims !== this.modelInfo.dimensions) {
|
|
2406
|
+
throw new Error(
|
|
2407
|
+
`Dimension mismatch: customProvider.dimensions is ${this.modelInfo.dimensions}, but the API returned vectors with ${actualDims} dimensions. Update your config to match the model's actual output dimensions.`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (data.data.length !== texts.length) {
|
|
2412
|
+
throw new Error(
|
|
2413
|
+
`Embedding count mismatch: sent ${texts.length} texts but received ${data.data.length} embeddings. The custom embedding server may not support batch input.`
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
embeddings: data.data.map((d) => d.embedding),
|
|
2418
|
+
// Rough estimate: ~4 chars per token. Used as fallback when the server
|
|
2419
|
+
// doesn't return usage.total_tokens (e.g. llama.cpp, some vLLM configs).
|
|
2420
|
+
totalTokensUsed: data.usage?.total_tokens ?? texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0)
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
|
|
2424
|
+
}
|
|
2425
|
+
getModelInfo() {
|
|
2426
|
+
return this.modelInfo;
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2281
2429
|
|
|
2282
2430
|
// src/utils/files.ts
|
|
2283
2431
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
@@ -2792,6 +2940,9 @@ function hashContent(content) {
|
|
|
2792
2940
|
function hashFile(filePath) {
|
|
2793
2941
|
return native.hashFile(filePath);
|
|
2794
2942
|
}
|
|
2943
|
+
function extractCalls(content, language) {
|
|
2944
|
+
return native.extractCalls(content, language);
|
|
2945
|
+
}
|
|
2795
2946
|
var VectorStore = class {
|
|
2796
2947
|
inner;
|
|
2797
2948
|
dimensions;
|
|
@@ -3123,6 +3274,12 @@ var Database = class {
|
|
|
3123
3274
|
getChunksByFile(filePath) {
|
|
3124
3275
|
return this.inner.getChunksByFile(filePath);
|
|
3125
3276
|
}
|
|
3277
|
+
getChunksByName(name) {
|
|
3278
|
+
return this.inner.getChunksByName(name);
|
|
3279
|
+
}
|
|
3280
|
+
getChunksByNameCi(name) {
|
|
3281
|
+
return this.inner.getChunksByNameCi(name);
|
|
3282
|
+
}
|
|
3126
3283
|
deleteChunksByFile(filePath) {
|
|
3127
3284
|
return this.inner.deleteChunksByFile(filePath);
|
|
3128
3285
|
}
|
|
@@ -3166,6 +3323,73 @@ var Database = class {
|
|
|
3166
3323
|
getStats() {
|
|
3167
3324
|
return this.inner.getStats();
|
|
3168
3325
|
}
|
|
3326
|
+
// ── Symbol methods ──────────────────────────────────────────────
|
|
3327
|
+
upsertSymbol(symbol) {
|
|
3328
|
+
this.inner.upsertSymbol(symbol);
|
|
3329
|
+
}
|
|
3330
|
+
upsertSymbolsBatch(symbols) {
|
|
3331
|
+
if (symbols.length === 0) return;
|
|
3332
|
+
this.inner.upsertSymbolsBatch(symbols);
|
|
3333
|
+
}
|
|
3334
|
+
getSymbolsByFile(filePath) {
|
|
3335
|
+
return this.inner.getSymbolsByFile(filePath);
|
|
3336
|
+
}
|
|
3337
|
+
getSymbolByName(name, filePath) {
|
|
3338
|
+
return this.inner.getSymbolByName(name, filePath) ?? null;
|
|
3339
|
+
}
|
|
3340
|
+
getSymbolsByName(name) {
|
|
3341
|
+
return this.inner.getSymbolsByName(name);
|
|
3342
|
+
}
|
|
3343
|
+
getSymbolsByNameCi(name) {
|
|
3344
|
+
return this.inner.getSymbolsByNameCi(name);
|
|
3345
|
+
}
|
|
3346
|
+
deleteSymbolsByFile(filePath) {
|
|
3347
|
+
return this.inner.deleteSymbolsByFile(filePath);
|
|
3348
|
+
}
|
|
3349
|
+
// ── Call Edge methods ────────────────────────────────────────────
|
|
3350
|
+
upsertCallEdge(edge) {
|
|
3351
|
+
this.inner.upsertCallEdge(edge);
|
|
3352
|
+
}
|
|
3353
|
+
upsertCallEdgesBatch(edges) {
|
|
3354
|
+
if (edges.length === 0) return;
|
|
3355
|
+
this.inner.upsertCallEdgesBatch(edges);
|
|
3356
|
+
}
|
|
3357
|
+
getCallers(targetName, branch) {
|
|
3358
|
+
return this.inner.getCallers(targetName, branch);
|
|
3359
|
+
}
|
|
3360
|
+
getCallersWithContext(targetName, branch) {
|
|
3361
|
+
return this.inner.getCallersWithContext(targetName, branch);
|
|
3362
|
+
}
|
|
3363
|
+
getCallees(symbolId, branch) {
|
|
3364
|
+
return this.inner.getCallees(symbolId, branch);
|
|
3365
|
+
}
|
|
3366
|
+
deleteCallEdgesByFile(filePath) {
|
|
3367
|
+
return this.inner.deleteCallEdgesByFile(filePath);
|
|
3368
|
+
}
|
|
3369
|
+
resolveCallEdge(edgeId, toSymbolId) {
|
|
3370
|
+
this.inner.resolveCallEdge(edgeId, toSymbolId);
|
|
3371
|
+
}
|
|
3372
|
+
// ── Branch Symbol methods ────────────────────────────────────────
|
|
3373
|
+
addSymbolsToBranch(branch, symbolIds) {
|
|
3374
|
+
this.inner.addSymbolsToBranch(branch, symbolIds);
|
|
3375
|
+
}
|
|
3376
|
+
addSymbolsToBranchBatch(branch, symbolIds) {
|
|
3377
|
+
if (symbolIds.length === 0) return;
|
|
3378
|
+
this.inner.addSymbolsToBranchBatch(branch, symbolIds);
|
|
3379
|
+
}
|
|
3380
|
+
getBranchSymbolIds(branch) {
|
|
3381
|
+
return this.inner.getBranchSymbolIds(branch);
|
|
3382
|
+
}
|
|
3383
|
+
clearBranchSymbols(branch) {
|
|
3384
|
+
return this.inner.clearBranchSymbols(branch);
|
|
3385
|
+
}
|
|
3386
|
+
// ── GC methods for symbols/edges ─────────────────────────────────
|
|
3387
|
+
gcOrphanSymbols() {
|
|
3388
|
+
return this.inner.gcOrphanSymbols();
|
|
3389
|
+
}
|
|
3390
|
+
gcOrphanCallEdges() {
|
|
3391
|
+
return this.inner.gcOrphanCallEdges();
|
|
3392
|
+
}
|
|
3169
3393
|
};
|
|
3170
3394
|
|
|
3171
3395
|
// src/git/index.ts
|
|
@@ -3266,6 +3490,29 @@ function getBranchOrDefault(repoRoot) {
|
|
|
3266
3490
|
}
|
|
3267
3491
|
|
|
3268
3492
|
// src/indexer/index.ts
|
|
3493
|
+
var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust"]);
|
|
3494
|
+
var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
3495
|
+
"function_declaration",
|
|
3496
|
+
"function",
|
|
3497
|
+
"arrow_function",
|
|
3498
|
+
"method_definition",
|
|
3499
|
+
"class_declaration",
|
|
3500
|
+
"interface_declaration",
|
|
3501
|
+
"type_alias_declaration",
|
|
3502
|
+
"enum_declaration",
|
|
3503
|
+
"function_definition",
|
|
3504
|
+
"class_definition",
|
|
3505
|
+
"decorated_definition",
|
|
3506
|
+
"method_declaration",
|
|
3507
|
+
"type_declaration",
|
|
3508
|
+
"type_spec",
|
|
3509
|
+
"function_item",
|
|
3510
|
+
"impl_item",
|
|
3511
|
+
"struct_item",
|
|
3512
|
+
"enum_item",
|
|
3513
|
+
"trait_item",
|
|
3514
|
+
"mod_item"
|
|
3515
|
+
]);
|
|
3269
3516
|
function float32ArrayToBuffer(arr) {
|
|
3270
3517
|
const float32 = new Float32Array(arr);
|
|
3271
3518
|
return Buffer.from(float32.buffer);
|
|
@@ -3290,6 +3537,892 @@ function isRateLimitError(error) {
|
|
|
3290
3537
|
return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
|
|
3291
3538
|
}
|
|
3292
3539
|
var INDEX_METADATA_VERSION = "1";
|
|
3540
|
+
var RANKING_TOKEN_CACHE_LIMIT = 4096;
|
|
3541
|
+
var rankingQueryTokenCache = /* @__PURE__ */ new Map();
|
|
3542
|
+
var rankingNameTokenCache = /* @__PURE__ */ new Map();
|
|
3543
|
+
var rankingPathTokenCache = /* @__PURE__ */ new Map();
|
|
3544
|
+
var rankingTextTokenCache = /* @__PURE__ */ new Map();
|
|
3545
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
3546
|
+
"the",
|
|
3547
|
+
"and",
|
|
3548
|
+
"for",
|
|
3549
|
+
"with",
|
|
3550
|
+
"from",
|
|
3551
|
+
"that",
|
|
3552
|
+
"this",
|
|
3553
|
+
"into",
|
|
3554
|
+
"using",
|
|
3555
|
+
"where",
|
|
3556
|
+
"what",
|
|
3557
|
+
"when",
|
|
3558
|
+
"why",
|
|
3559
|
+
"how",
|
|
3560
|
+
"are",
|
|
3561
|
+
"was",
|
|
3562
|
+
"were",
|
|
3563
|
+
"be",
|
|
3564
|
+
"been",
|
|
3565
|
+
"being",
|
|
3566
|
+
"find",
|
|
3567
|
+
"show",
|
|
3568
|
+
"get",
|
|
3569
|
+
"run",
|
|
3570
|
+
"use",
|
|
3571
|
+
"code",
|
|
3572
|
+
"function",
|
|
3573
|
+
"implementation",
|
|
3574
|
+
"retrieve",
|
|
3575
|
+
"results",
|
|
3576
|
+
"result",
|
|
3577
|
+
"search",
|
|
3578
|
+
"pipeline",
|
|
3579
|
+
"top",
|
|
3580
|
+
"in",
|
|
3581
|
+
"on",
|
|
3582
|
+
"of",
|
|
3583
|
+
"to",
|
|
3584
|
+
"by",
|
|
3585
|
+
"as",
|
|
3586
|
+
"or",
|
|
3587
|
+
"an",
|
|
3588
|
+
"a"
|
|
3589
|
+
]);
|
|
3590
|
+
var TEST_PATH_SEGMENTS = [
|
|
3591
|
+
"tests/",
|
|
3592
|
+
"__tests__/",
|
|
3593
|
+
"/test/",
|
|
3594
|
+
"fixtures/",
|
|
3595
|
+
"benchmark",
|
|
3596
|
+
"README",
|
|
3597
|
+
"ARCHITECTURE",
|
|
3598
|
+
"docs/"
|
|
3599
|
+
];
|
|
3600
|
+
var IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS = [
|
|
3601
|
+
"tests/",
|
|
3602
|
+
"__tests__/",
|
|
3603
|
+
"/test/",
|
|
3604
|
+
"fixtures/",
|
|
3605
|
+
"benchmark",
|
|
3606
|
+
"readme",
|
|
3607
|
+
"architecture",
|
|
3608
|
+
"docs/",
|
|
3609
|
+
"examples/",
|
|
3610
|
+
"example/",
|
|
3611
|
+
".github/",
|
|
3612
|
+
"/scripts/",
|
|
3613
|
+
"/migrations/",
|
|
3614
|
+
"/generated/"
|
|
3615
|
+
];
|
|
3616
|
+
var SOURCE_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3617
|
+
"implement",
|
|
3618
|
+
"implementation",
|
|
3619
|
+
"function",
|
|
3620
|
+
"method",
|
|
3621
|
+
"class",
|
|
3622
|
+
"logic",
|
|
3623
|
+
"algorithm",
|
|
3624
|
+
"pipeline",
|
|
3625
|
+
"indexer",
|
|
3626
|
+
"where"
|
|
3627
|
+
]);
|
|
3628
|
+
var DOC_TEST_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3629
|
+
"test",
|
|
3630
|
+
"tests",
|
|
3631
|
+
"fixture",
|
|
3632
|
+
"fixtures",
|
|
3633
|
+
"benchmark",
|
|
3634
|
+
"readme",
|
|
3635
|
+
"docs",
|
|
3636
|
+
"documentation"
|
|
3637
|
+
]);
|
|
3638
|
+
var DOC_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3639
|
+
"readme",
|
|
3640
|
+
"docs",
|
|
3641
|
+
"documentation",
|
|
3642
|
+
"guide",
|
|
3643
|
+
"usage"
|
|
3644
|
+
]);
|
|
3645
|
+
function setBoundedCache(cache, key, value) {
|
|
3646
|
+
if (cache.size >= RANKING_TOKEN_CACHE_LIMIT) {
|
|
3647
|
+
const oldest = cache.keys().next().value;
|
|
3648
|
+
if (oldest !== void 0) {
|
|
3649
|
+
cache.delete(oldest);
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
cache.set(key, value);
|
|
3653
|
+
}
|
|
3654
|
+
function tokenizeTextForRanking(text) {
|
|
3655
|
+
if (!text) {
|
|
3656
|
+
return /* @__PURE__ */ new Set();
|
|
3657
|
+
}
|
|
3658
|
+
const lowered = text.toLowerCase();
|
|
3659
|
+
const cache = rankingQueryTokenCache.get(lowered) ?? rankingTextTokenCache.get(lowered);
|
|
3660
|
+
if (cache) {
|
|
3661
|
+
return cache;
|
|
3662
|
+
}
|
|
3663
|
+
const tokens = new Set(
|
|
3664
|
+
lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1 && !STOPWORDS.has(token))
|
|
3665
|
+
);
|
|
3666
|
+
setBoundedCache(rankingQueryTokenCache, lowered, tokens);
|
|
3667
|
+
setBoundedCache(rankingTextTokenCache, lowered, tokens);
|
|
3668
|
+
return tokens;
|
|
3669
|
+
}
|
|
3670
|
+
function splitPathTokens(filePath) {
|
|
3671
|
+
const lowered = filePath.toLowerCase();
|
|
3672
|
+
const cache = rankingPathTokenCache.get(lowered);
|
|
3673
|
+
if (cache) {
|
|
3674
|
+
return cache;
|
|
3675
|
+
}
|
|
3676
|
+
const normalized = lowered.replace(/[^a-z0-9/._-]/g, " ").split(/[/._-]+/).filter((token) => token.length > 1);
|
|
3677
|
+
const tokens = new Set(normalized);
|
|
3678
|
+
setBoundedCache(rankingPathTokenCache, lowered, tokens);
|
|
3679
|
+
return tokens;
|
|
3680
|
+
}
|
|
3681
|
+
function splitNameTokens(name) {
|
|
3682
|
+
if (!name) {
|
|
3683
|
+
return /* @__PURE__ */ new Set();
|
|
3684
|
+
}
|
|
3685
|
+
const lowered = name.toLowerCase();
|
|
3686
|
+
const cache = rankingNameTokenCache.get(lowered);
|
|
3687
|
+
if (cache) {
|
|
3688
|
+
return cache;
|
|
3689
|
+
}
|
|
3690
|
+
const tokens = new Set(
|
|
3691
|
+
lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1)
|
|
3692
|
+
);
|
|
3693
|
+
setBoundedCache(rankingNameTokenCache, lowered, tokens);
|
|
3694
|
+
return tokens;
|
|
3695
|
+
}
|
|
3696
|
+
function chunkTypeBoost(chunkType) {
|
|
3697
|
+
switch (chunkType) {
|
|
3698
|
+
case "function":
|
|
3699
|
+
case "function_declaration":
|
|
3700
|
+
case "method":
|
|
3701
|
+
case "method_definition":
|
|
3702
|
+
case "class":
|
|
3703
|
+
case "class_declaration":
|
|
3704
|
+
return 0.2;
|
|
3705
|
+
case "interface":
|
|
3706
|
+
case "type":
|
|
3707
|
+
case "enum":
|
|
3708
|
+
case "struct":
|
|
3709
|
+
case "impl":
|
|
3710
|
+
case "trait":
|
|
3711
|
+
case "module":
|
|
3712
|
+
return 0.1;
|
|
3713
|
+
default:
|
|
3714
|
+
return 0;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
function isTestOrDocPath(filePath) {
|
|
3718
|
+
return TEST_PATH_SEGMENTS.some((segment) => filePath.includes(segment));
|
|
3719
|
+
}
|
|
3720
|
+
function isLikelyImplementationPath(filePath) {
|
|
3721
|
+
const lowered = filePath.toLowerCase();
|
|
3722
|
+
if (IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS.some((segment) => lowered.includes(segment))) {
|
|
3723
|
+
return false;
|
|
3724
|
+
}
|
|
3725
|
+
const ext = lowered.split(".").pop() ?? "";
|
|
3726
|
+
if (["md", "mdx", "txt", "rst", "adoc", "snap", "json", "yaml", "yml", "lock"].includes(ext)) {
|
|
3727
|
+
return false;
|
|
3728
|
+
}
|
|
3729
|
+
return true;
|
|
3730
|
+
}
|
|
3731
|
+
function classifyQueryIntent(tokens) {
|
|
3732
|
+
const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
|
|
3733
|
+
const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
|
|
3734
|
+
return sourceIntentHits >= docTestIntentHits ? "source" : "doc_test";
|
|
3735
|
+
}
|
|
3736
|
+
function classifyQueryIntentRaw(query) {
|
|
3737
|
+
const lowerQuery = query.toLowerCase();
|
|
3738
|
+
const docTestRawHits = Array.from(DOC_TEST_INTENT_HINTS).filter(
|
|
3739
|
+
(hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)
|
|
3740
|
+
).length;
|
|
3741
|
+
const sourceRawHits = [
|
|
3742
|
+
"implement",
|
|
3743
|
+
"implementation",
|
|
3744
|
+
"implements",
|
|
3745
|
+
"function",
|
|
3746
|
+
"method",
|
|
3747
|
+
"class",
|
|
3748
|
+
"logic",
|
|
3749
|
+
"algorithm",
|
|
3750
|
+
"pipeline",
|
|
3751
|
+
"indexer"
|
|
3752
|
+
].filter((hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)).length;
|
|
3753
|
+
if (docTestRawHits > sourceRawHits) {
|
|
3754
|
+
return "doc_test";
|
|
3755
|
+
}
|
|
3756
|
+
if (sourceRawHits > docTestRawHits) {
|
|
3757
|
+
return "source";
|
|
3758
|
+
}
|
|
3759
|
+
const hasWhereIsPattern = /\bwhere\s+is\b/.test(lowerQuery);
|
|
3760
|
+
const hasIdentifierHints = extractIdentifierHints(query).length > 0;
|
|
3761
|
+
if (hasWhereIsPattern && hasIdentifierHints && docTestRawHits === 0) {
|
|
3762
|
+
return "source";
|
|
3763
|
+
}
|
|
3764
|
+
const queryTokens = Array.from(tokenizeTextForRanking(query));
|
|
3765
|
+
return classifyQueryIntent(queryTokens);
|
|
3766
|
+
}
|
|
3767
|
+
function classifyDocIntent(tokens) {
|
|
3768
|
+
const docHits = tokens.filter((t) => DOC_INTENT_HINTS.has(t)).length;
|
|
3769
|
+
const testHits = tokens.filter((t) => ["test", "tests", "fixture", "fixtures", "benchmark"].includes(t)).length;
|
|
3770
|
+
if (docHits > 0 && testHits === 0) return "docs";
|
|
3771
|
+
if (testHits > 0 && docHits === 0) return "test";
|
|
3772
|
+
if (testHits > 0 || docHits > 0) return "mixed";
|
|
3773
|
+
return "none";
|
|
3774
|
+
}
|
|
3775
|
+
function isImplementationChunkType(chunkType) {
|
|
3776
|
+
return [
|
|
3777
|
+
"export_statement",
|
|
3778
|
+
"function",
|
|
3779
|
+
"function_declaration",
|
|
3780
|
+
"method",
|
|
3781
|
+
"method_definition",
|
|
3782
|
+
"class",
|
|
3783
|
+
"class_declaration",
|
|
3784
|
+
"interface",
|
|
3785
|
+
"type",
|
|
3786
|
+
"enum",
|
|
3787
|
+
"module"
|
|
3788
|
+
].includes(chunkType);
|
|
3789
|
+
}
|
|
3790
|
+
function extractIdentifierHints(query) {
|
|
3791
|
+
const identifiers = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
3792
|
+
return identifiers.filter((id) => id.length >= 3).filter((id) => {
|
|
3793
|
+
const lower = id.toLowerCase();
|
|
3794
|
+
if (STOPWORDS.has(lower)) return false;
|
|
3795
|
+
return /[A-Z]/.test(id) || id.includes("_") || id.endsWith("Results") || id.endsWith("Result");
|
|
3796
|
+
}).map((id) => id.toLowerCase());
|
|
3797
|
+
}
|
|
3798
|
+
function extractCodeTermHints(query) {
|
|
3799
|
+
const terms = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
3800
|
+
return terms.map((term) => term.toLowerCase()).filter((term) => term.length >= 3).filter((term) => !STOPWORDS.has(term));
|
|
3801
|
+
}
|
|
3802
|
+
function normalizeIdentifierVariants(identifier) {
|
|
3803
|
+
const lower = identifier.toLowerCase();
|
|
3804
|
+
const compact = lower.replace(/[^a-z0-9]/g, "");
|
|
3805
|
+
const snake = compact.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
3806
|
+
const kebab = snake.replace(/_/g, "-");
|
|
3807
|
+
const variants = [lower, compact, snake, kebab].filter((value) => value.length > 0);
|
|
3808
|
+
return Array.from(new Set(variants));
|
|
3809
|
+
}
|
|
3810
|
+
function scoreIdentifierMatch(name, filePath, hints) {
|
|
3811
|
+
const nameLower = (name ?? "").toLowerCase();
|
|
3812
|
+
const pathLower = filePath.toLowerCase();
|
|
3813
|
+
let best = 0;
|
|
3814
|
+
for (const hint of hints) {
|
|
3815
|
+
const variants = normalizeIdentifierVariants(hint);
|
|
3816
|
+
for (const variant of variants) {
|
|
3817
|
+
if (nameLower === variant) {
|
|
3818
|
+
best = Math.max(best, 1);
|
|
3819
|
+
} else if (nameLower.includes(variant)) {
|
|
3820
|
+
best = Math.max(best, 0.8);
|
|
3821
|
+
} else if (pathLower.includes(variant)) {
|
|
3822
|
+
best = Math.max(best, 0.6);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
return best;
|
|
3827
|
+
}
|
|
3828
|
+
function extractPrimaryIdentifierQueryHint(query) {
|
|
3829
|
+
const identifiers = extractIdentifierHints(query);
|
|
3830
|
+
if (identifiers.length > 0) {
|
|
3831
|
+
return identifiers[0] ?? null;
|
|
3832
|
+
}
|
|
3833
|
+
const codeTerms = extractCodeTermHints(query);
|
|
3834
|
+
const best = codeTerms.find((term) => term.length >= 6);
|
|
3835
|
+
return best ?? null;
|
|
3836
|
+
}
|
|
3837
|
+
var FILE_PATH_HINT_EXTENSIONS = [
|
|
3838
|
+
"ts",
|
|
3839
|
+
"tsx",
|
|
3840
|
+
"js",
|
|
3841
|
+
"jsx",
|
|
3842
|
+
"mjs",
|
|
3843
|
+
"cjs",
|
|
3844
|
+
"mts",
|
|
3845
|
+
"cts",
|
|
3846
|
+
"py",
|
|
3847
|
+
"rs",
|
|
3848
|
+
"go",
|
|
3849
|
+
"java",
|
|
3850
|
+
"kt",
|
|
3851
|
+
"kts",
|
|
3852
|
+
"swift",
|
|
3853
|
+
"rb",
|
|
3854
|
+
"php",
|
|
3855
|
+
"c",
|
|
3856
|
+
"h",
|
|
3857
|
+
"cc",
|
|
3858
|
+
"cpp",
|
|
3859
|
+
"cxx",
|
|
3860
|
+
"hpp",
|
|
3861
|
+
"cs",
|
|
3862
|
+
"scala",
|
|
3863
|
+
"lua",
|
|
3864
|
+
"sh",
|
|
3865
|
+
"bash",
|
|
3866
|
+
"zsh",
|
|
3867
|
+
"json",
|
|
3868
|
+
"yaml",
|
|
3869
|
+
"yml",
|
|
3870
|
+
"toml"
|
|
3871
|
+
];
|
|
3872
|
+
var FILE_PATH_HINT_SUFFIX_REGEX = new RegExp(
|
|
3873
|
+
"\\s+\\bin\\s+[\"'`]?((?:\\.\\/)?(?:[A-Za-z0-9._-]+\\/)+[A-Za-z0-9._-]+\\.(?:" + FILE_PATH_HINT_EXTENSIONS.join("|") + "))[\"'`]?[\\])}>.,;!?]*\\s*$",
|
|
3874
|
+
"i"
|
|
3875
|
+
);
|
|
3876
|
+
function normalizeFilePathForHintMatch(filePath) {
|
|
3877
|
+
return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
|
|
3878
|
+
}
|
|
3879
|
+
function pathMatchesHint(filePath, hint) {
|
|
3880
|
+
const normalizedPath = normalizeFilePathForHintMatch(filePath);
|
|
3881
|
+
const normalizedHint = normalizeFilePathForHintMatch(hint);
|
|
3882
|
+
return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
|
|
3883
|
+
}
|
|
3884
|
+
function extractFilePathHint(query) {
|
|
3885
|
+
const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
|
|
3886
|
+
const rawPath = match?.[1];
|
|
3887
|
+
if (!rawPath) {
|
|
3888
|
+
return null;
|
|
3889
|
+
}
|
|
3890
|
+
return rawPath.replace(/^\.\//, "");
|
|
3891
|
+
}
|
|
3892
|
+
function stripFilePathHint(query) {
|
|
3893
|
+
const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
|
|
3894
|
+
return stripped.length > 0 ? stripped : query;
|
|
3895
|
+
}
|
|
3896
|
+
function buildDeterministicIdentifierPass(query, candidates, limit) {
|
|
3897
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
3898
|
+
return [];
|
|
3899
|
+
}
|
|
3900
|
+
const primary = extractPrimaryIdentifierQueryHint(query);
|
|
3901
|
+
if (!primary) {
|
|
3902
|
+
return [];
|
|
3903
|
+
}
|
|
3904
|
+
const filePathHint = extractFilePathHint(query);
|
|
3905
|
+
const primaryVariants = normalizeIdentifierVariants(primary);
|
|
3906
|
+
const hints = [primary, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].map((value) => value.toLowerCase()).filter((value, idx, arr) => value.length >= 3 && arr.indexOf(value) === idx).slice(0, 8);
|
|
3907
|
+
const deterministic = candidates.filter(
|
|
3908
|
+
(candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
|
|
3909
|
+
).map((candidate) => {
|
|
3910
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
3911
|
+
const pathLower = candidate.metadata.filePath.toLowerCase();
|
|
3912
|
+
let maxMatch = 0;
|
|
3913
|
+
const nameMatchesPrimary = primaryVariants.some(
|
|
3914
|
+
(variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
|
|
3915
|
+
);
|
|
3916
|
+
const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
|
|
3917
|
+
for (const hint of hints) {
|
|
3918
|
+
const variants = normalizeIdentifierVariants(hint);
|
|
3919
|
+
for (const variant of variants) {
|
|
3920
|
+
if (nameLower === variant) {
|
|
3921
|
+
maxMatch = Math.max(maxMatch, 1);
|
|
3922
|
+
} else if (nameLower.includes(variant)) {
|
|
3923
|
+
maxMatch = Math.max(maxMatch, 0.85);
|
|
3924
|
+
} else if (pathLower.includes(variant)) {
|
|
3925
|
+
maxMatch = Math.max(maxMatch, 0.7);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
if (pathMatchesFileHint && nameMatchesPrimary) {
|
|
3930
|
+
maxMatch = Math.max(maxMatch, 1);
|
|
3931
|
+
}
|
|
3932
|
+
return {
|
|
3933
|
+
candidate,
|
|
3934
|
+
maxMatch,
|
|
3935
|
+
pathMatchesFileHint,
|
|
3936
|
+
nameMatchesPrimary
|
|
3937
|
+
};
|
|
3938
|
+
}).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
|
|
3939
|
+
const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
|
|
3940
|
+
const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
|
|
3941
|
+
if (aAnchored !== bAnchored) return bAnchored - aAnchored;
|
|
3942
|
+
if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
|
|
3943
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
3944
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
3945
|
+
}).slice(0, Math.max(limit * 2, 12));
|
|
3946
|
+
return deterministic.map((entry) => ({
|
|
3947
|
+
id: entry.candidate.id,
|
|
3948
|
+
score: entry.pathMatchesFileHint && entry.nameMatchesPrimary ? 0.995 : Math.min(1, 0.9 + entry.maxMatch * 0.09),
|
|
3949
|
+
metadata: entry.candidate.metadata
|
|
3950
|
+
}));
|
|
3951
|
+
}
|
|
3952
|
+
function fuseResultsWeighted(semanticResults, keywordResults, keywordWeight, limit) {
|
|
3953
|
+
const semanticWeight = 1 - keywordWeight;
|
|
3954
|
+
const fusedScores = /* @__PURE__ */ new Map();
|
|
3955
|
+
for (const r of semanticResults) {
|
|
3956
|
+
fusedScores.set(r.id, {
|
|
3957
|
+
score: r.score * semanticWeight,
|
|
3958
|
+
metadata: r.metadata
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
for (const r of keywordResults) {
|
|
3962
|
+
const existing = fusedScores.get(r.id);
|
|
3963
|
+
if (existing) {
|
|
3964
|
+
existing.score += r.score * keywordWeight;
|
|
3965
|
+
} else {
|
|
3966
|
+
fusedScores.set(r.id, {
|
|
3967
|
+
score: r.score * keywordWeight,
|
|
3968
|
+
metadata: r.metadata
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
|
|
3973
|
+
id,
|
|
3974
|
+
score: data.score,
|
|
3975
|
+
metadata: data.metadata
|
|
3976
|
+
}));
|
|
3977
|
+
results.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
3978
|
+
return results.slice(0, limit);
|
|
3979
|
+
}
|
|
3980
|
+
function fuseResultsRrf(semanticResults, keywordResults, rrfK, limit) {
|
|
3981
|
+
const maxPossibleRaw = 2 / (rrfK + 1);
|
|
3982
|
+
const rankByIdSemantic = /* @__PURE__ */ new Map();
|
|
3983
|
+
const rankByIdKeyword = /* @__PURE__ */ new Map();
|
|
3984
|
+
const metadataById = /* @__PURE__ */ new Map();
|
|
3985
|
+
semanticResults.forEach((result, index) => {
|
|
3986
|
+
rankByIdSemantic.set(result.id, index + 1);
|
|
3987
|
+
metadataById.set(result.id, result.metadata);
|
|
3988
|
+
});
|
|
3989
|
+
keywordResults.forEach((result, index) => {
|
|
3990
|
+
rankByIdKeyword.set(result.id, index + 1);
|
|
3991
|
+
if (!metadataById.has(result.id)) {
|
|
3992
|
+
metadataById.set(result.id, result.metadata);
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
const allIds = /* @__PURE__ */ new Set([...rankByIdSemantic.keys(), ...rankByIdKeyword.keys()]);
|
|
3996
|
+
const fused = [];
|
|
3997
|
+
for (const id of allIds) {
|
|
3998
|
+
const semanticRank = rankByIdSemantic.get(id);
|
|
3999
|
+
const keywordRank = rankByIdKeyword.get(id);
|
|
4000
|
+
const semanticScore = semanticRank ? 1 / (rrfK + semanticRank) : 0;
|
|
4001
|
+
const keywordScore = keywordRank ? 1 / (rrfK + keywordRank) : 0;
|
|
4002
|
+
const metadata = metadataById.get(id);
|
|
4003
|
+
if (!metadata) continue;
|
|
4004
|
+
fused.push({
|
|
4005
|
+
id,
|
|
4006
|
+
score: maxPossibleRaw > 0 ? (semanticScore + keywordScore) / maxPossibleRaw : 0,
|
|
4007
|
+
metadata
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
fused.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4011
|
+
return fused.slice(0, limit);
|
|
4012
|
+
}
|
|
4013
|
+
function rerankResults(query, candidates, rerankTopN, options) {
|
|
4014
|
+
if (rerankTopN <= 0 || candidates.length <= 1) {
|
|
4015
|
+
return candidates;
|
|
4016
|
+
}
|
|
4017
|
+
const topN = Math.min(rerankTopN, candidates.length);
|
|
4018
|
+
const queryTokens = tokenizeTextForRanking(query);
|
|
4019
|
+
if (queryTokens.size === 0) {
|
|
4020
|
+
return candidates;
|
|
4021
|
+
}
|
|
4022
|
+
const queryTokenList = Array.from(queryTokens);
|
|
4023
|
+
const intent = classifyQueryIntentRaw(query);
|
|
4024
|
+
const docIntent = classifyDocIntent(queryTokenList);
|
|
4025
|
+
const preferSourcePaths = options?.prioritizeSourcePaths ?? intent === "source";
|
|
4026
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4027
|
+
const head = candidates.slice(0, topN).map((candidate, idx) => {
|
|
4028
|
+
const pathTokens = splitPathTokens(candidate.metadata.filePath);
|
|
4029
|
+
const nameTokens = splitNameTokens(candidate.metadata.name ?? "");
|
|
4030
|
+
const chunkTypeTokens = tokenizeTextForRanking(candidate.metadata.chunkType);
|
|
4031
|
+
let exactOrPrefixNameHits = 0;
|
|
4032
|
+
let pathOverlap = 0;
|
|
4033
|
+
let chunkTypeHits = 0;
|
|
4034
|
+
for (const token of queryTokenList) {
|
|
4035
|
+
if (nameTokens.has(token)) {
|
|
4036
|
+
exactOrPrefixNameHits += 1;
|
|
4037
|
+
} else {
|
|
4038
|
+
for (const nameToken of nameTokens) {
|
|
4039
|
+
if (nameToken.startsWith(token) || token.startsWith(nameToken)) {
|
|
4040
|
+
exactOrPrefixNameHits += 1;
|
|
4041
|
+
break;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
if (pathTokens.has(token)) {
|
|
4046
|
+
pathOverlap += 1;
|
|
4047
|
+
}
|
|
4048
|
+
if (chunkTypeTokens.has(token)) {
|
|
4049
|
+
chunkTypeHits += 1;
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
const likelyTestOrDoc = isTestOrDocPath(candidate.metadata.filePath);
|
|
4053
|
+
const lowerPath = candidate.metadata.filePath.toLowerCase();
|
|
4054
|
+
const lowerName = (candidate.metadata.name ?? "").toLowerCase();
|
|
4055
|
+
const hasIdentifierMatch = identifierHints.some((id) => lowerPath.includes(id) || lowerName.includes(id));
|
|
4056
|
+
const implementationPathBoost = preferSourcePaths && isLikelyImplementationPath(candidate.metadata.filePath) ? 0.08 : 0;
|
|
4057
|
+
const isReadmePath = candidate.metadata.filePath.toLowerCase().includes("readme");
|
|
4058
|
+
const testDocPenalty = preferSourcePaths && likelyTestOrDoc ? 0.12 : 0;
|
|
4059
|
+
const readmeDocBoost = !preferSourcePaths && isReadmePath ? 0.08 : 0;
|
|
4060
|
+
const identifierBoost = hasIdentifierMatch ? 0.12 : 0;
|
|
4061
|
+
const tokenCoverage = queryTokenList.length > 0 ? (exactOrPrefixNameHits + pathOverlap + chunkTypeHits) / queryTokenList.length : 0;
|
|
4062
|
+
const coverageBoost = Math.min(0.12, tokenCoverage * 0.06);
|
|
4063
|
+
const deterministicBoost = exactOrPrefixNameHits * 0.08 + pathOverlap * 0.03 + chunkTypeHits * 0.02 + coverageBoost + identifierBoost + implementationPathBoost - testDocPenalty + readmeDocBoost + chunkTypeBoost(candidate.metadata.chunkType);
|
|
4064
|
+
return {
|
|
4065
|
+
candidate,
|
|
4066
|
+
boostedScore: candidate.score + deterministicBoost,
|
|
4067
|
+
originalIndex: idx,
|
|
4068
|
+
hasIdentifierMatch,
|
|
4069
|
+
implementationChunk: isImplementationChunkType(candidate.metadata.chunkType),
|
|
4070
|
+
isLikelyImplementationPath: isLikelyImplementationPath(candidate.metadata.filePath),
|
|
4071
|
+
isTestOrDocPath: likelyTestOrDoc,
|
|
4072
|
+
isReadmePath
|
|
4073
|
+
};
|
|
4074
|
+
});
|
|
4075
|
+
head.sort((a, b) => {
|
|
4076
|
+
if (b.boostedScore !== a.boostedScore) return b.boostedScore - a.boostedScore;
|
|
4077
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
4078
|
+
if (a.originalIndex !== b.originalIndex) return a.originalIndex - b.originalIndex;
|
|
4079
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
4080
|
+
});
|
|
4081
|
+
if (preferSourcePaths) {
|
|
4082
|
+
head.sort((a, b) => {
|
|
4083
|
+
const aId = a.hasIdentifierMatch ? 1 : 0;
|
|
4084
|
+
const bId = b.hasIdentifierMatch ? 1 : 0;
|
|
4085
|
+
if (aId !== bId) return bId - aId;
|
|
4086
|
+
const aImpl = a.implementationChunk ? 1 : 0;
|
|
4087
|
+
const bImpl = b.implementationChunk ? 1 : 0;
|
|
4088
|
+
if (aImpl !== bImpl) return bImpl - aImpl;
|
|
4089
|
+
const aImplementationPath = a.isLikelyImplementationPath ? 1 : 0;
|
|
4090
|
+
const bImplementationPath = b.isLikelyImplementationPath ? 1 : 0;
|
|
4091
|
+
if (aImplementationPath !== bImplementationPath) return bImplementationPath - aImplementationPath;
|
|
4092
|
+
const aTestDoc = a.isTestOrDocPath ? 1 : 0;
|
|
4093
|
+
const bTestDoc = b.isTestOrDocPath ? 1 : 0;
|
|
4094
|
+
if (aTestDoc !== bTestDoc) return aTestDoc - bTestDoc;
|
|
4095
|
+
return 0;
|
|
4096
|
+
});
|
|
4097
|
+
} else if (docIntent === "docs") {
|
|
4098
|
+
head.sort((a, b) => {
|
|
4099
|
+
const aReadme = a.isReadmePath ? 1 : 0;
|
|
4100
|
+
const bReadme = b.isReadmePath ? 1 : 0;
|
|
4101
|
+
if (aReadme !== bReadme) return bReadme - aReadme;
|
|
4102
|
+
return 0;
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
const tail = candidates.slice(topN);
|
|
4106
|
+
return [...head.map((entry) => entry.candidate), ...tail];
|
|
4107
|
+
}
|
|
4108
|
+
function rankHybridResults(query, semanticResults, keywordResults, options) {
|
|
4109
|
+
const overfetchLimit = Math.max(options.limit * 4, options.limit);
|
|
4110
|
+
const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
|
|
4111
|
+
const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
|
|
4112
|
+
const rerankPool = fused.slice(0, rerankPoolLimit);
|
|
4113
|
+
const intent = classifyQueryIntentRaw(query);
|
|
4114
|
+
return rerankResults(query, rerankPool, options.rerankTopN, {
|
|
4115
|
+
prioritizeSourcePaths: intent === "source"
|
|
4116
|
+
});
|
|
4117
|
+
}
|
|
4118
|
+
function rankSemanticOnlyResults(query, semanticResults, options) {
|
|
4119
|
+
const overfetchLimit = Math.max(options.limit * 4, options.limit);
|
|
4120
|
+
const bounded = semanticResults.slice(0, overfetchLimit);
|
|
4121
|
+
return rerankResults(query, bounded, options.rerankTopN, {
|
|
4122
|
+
prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds) {
|
|
4126
|
+
if (combined.length === 0) {
|
|
4127
|
+
return combined;
|
|
4128
|
+
}
|
|
4129
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
4130
|
+
return combined;
|
|
4131
|
+
}
|
|
4132
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4133
|
+
if (identifierHints.length === 0) {
|
|
4134
|
+
return combined;
|
|
4135
|
+
}
|
|
4136
|
+
const combinedById = new Map(combined.map((candidate) => [candidate.id, candidate]));
|
|
4137
|
+
const candidateUnion = /* @__PURE__ */ new Map();
|
|
4138
|
+
for (const candidate of semanticCandidates) {
|
|
4139
|
+
candidateUnion.set(candidate.id, candidate);
|
|
4140
|
+
}
|
|
4141
|
+
for (const candidate of keywordCandidates) {
|
|
4142
|
+
if (!candidateUnion.has(candidate.id)) {
|
|
4143
|
+
candidateUnion.set(candidate.id, candidate);
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
if (database) {
|
|
4147
|
+
for (const identifier of identifierHints) {
|
|
4148
|
+
const symbols = database.getSymbolsByName(identifier);
|
|
4149
|
+
for (const symbol of symbols) {
|
|
4150
|
+
const chunks = database.getChunksByFile(symbol.filePath);
|
|
4151
|
+
for (const chunk of chunks) {
|
|
4152
|
+
if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
|
|
4153
|
+
continue;
|
|
4154
|
+
}
|
|
4155
|
+
const chunkType = chunk.nodeType ?? "other";
|
|
4156
|
+
if (!isImplementationChunkType(chunkType)) {
|
|
4157
|
+
continue;
|
|
4158
|
+
}
|
|
4159
|
+
if (!isLikelyImplementationPath(chunk.filePath)) {
|
|
4160
|
+
continue;
|
|
4161
|
+
}
|
|
4162
|
+
if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
|
|
4163
|
+
continue;
|
|
4164
|
+
}
|
|
4165
|
+
const existing = combinedById.get(chunk.chunkId) ?? candidateUnion.get(chunk.chunkId);
|
|
4166
|
+
const metadata = existing?.metadata ?? {
|
|
4167
|
+
filePath: chunk.filePath,
|
|
4168
|
+
startLine: chunk.startLine,
|
|
4169
|
+
endLine: chunk.endLine,
|
|
4170
|
+
chunkType,
|
|
4171
|
+
name: chunk.name ?? void 0,
|
|
4172
|
+
language: chunk.language,
|
|
4173
|
+
hash: chunk.contentHash
|
|
4174
|
+
};
|
|
4175
|
+
const baselineScore = existing?.score ?? 0.5;
|
|
4176
|
+
candidateUnion.set(chunk.chunkId, {
|
|
4177
|
+
id: chunk.chunkId,
|
|
4178
|
+
score: Math.min(1, baselineScore + 0.5),
|
|
4179
|
+
metadata
|
|
4180
|
+
});
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
const promoted = [];
|
|
4186
|
+
for (const candidate of candidateUnion.values()) {
|
|
4187
|
+
const filePathLower = candidate.metadata.filePath.toLowerCase();
|
|
4188
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
4189
|
+
const exactIdentifierMatch = identifierHints.some((hint) => nameLower === hint);
|
|
4190
|
+
const hasIdentifierMatch = exactIdentifierMatch || identifierHints.some(
|
|
4191
|
+
(hint) => nameLower.includes(hint) || filePathLower.includes(hint)
|
|
4192
|
+
);
|
|
4193
|
+
if (!hasIdentifierMatch) {
|
|
4194
|
+
continue;
|
|
4195
|
+
}
|
|
4196
|
+
if (!isImplementationChunkType(candidate.metadata.chunkType)) {
|
|
4197
|
+
continue;
|
|
4198
|
+
}
|
|
4199
|
+
if (!isLikelyImplementationPath(candidate.metadata.filePath)) {
|
|
4200
|
+
continue;
|
|
4201
|
+
}
|
|
4202
|
+
const existing = combinedById.get(candidate.id) ?? candidate;
|
|
4203
|
+
const rescueBoost = exactIdentifierMatch ? 0.45 : 0.25;
|
|
4204
|
+
const boostedScore = Math.min(1, Math.max(existing.score, candidate.score) + rescueBoost);
|
|
4205
|
+
promoted.push({
|
|
4206
|
+
id: existing.id,
|
|
4207
|
+
score: boostedScore,
|
|
4208
|
+
metadata: existing.metadata
|
|
4209
|
+
});
|
|
4210
|
+
}
|
|
4211
|
+
if (promoted.length === 0) {
|
|
4212
|
+
return combined;
|
|
4213
|
+
}
|
|
4214
|
+
promoted.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4215
|
+
const promotedIds = new Set(promoted.map((candidate) => candidate.id));
|
|
4216
|
+
const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
|
|
4217
|
+
return [...promoted, ...remainder];
|
|
4218
|
+
}
|
|
4219
|
+
function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates) {
|
|
4220
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
4221
|
+
return [];
|
|
4222
|
+
}
|
|
4223
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4224
|
+
const codeTermHints = extractCodeTermHints(query);
|
|
4225
|
+
if (identifierHints.length === 0 && codeTermHints.length === 0) {
|
|
4226
|
+
return [];
|
|
4227
|
+
}
|
|
4228
|
+
const symbolCandidates = /* @__PURE__ */ new Map();
|
|
4229
|
+
const filePathHint = extractFilePathHint(query);
|
|
4230
|
+
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
4231
|
+
const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
|
|
4232
|
+
if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
const chunkType = chunk.nodeType ?? "other";
|
|
4236
|
+
if (!isImplementationChunkType(chunkType)) {
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
if (!isLikelyImplementationPath(chunk.filePath)) {
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
const nameLower = (chunk.name ?? "").toLowerCase();
|
|
4243
|
+
const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
|
|
4244
|
+
const base = baseScore ?? (exactName ? 0.99 : 0.88);
|
|
4245
|
+
const existing = symbolCandidates.get(chunk.chunkId);
|
|
4246
|
+
if (!existing || base > existing.score) {
|
|
4247
|
+
symbolCandidates.set(chunk.chunkId, {
|
|
4248
|
+
id: chunk.chunkId,
|
|
4249
|
+
score: base,
|
|
4250
|
+
metadata: {
|
|
4251
|
+
filePath: chunk.filePath,
|
|
4252
|
+
startLine: chunk.startLine,
|
|
4253
|
+
endLine: chunk.endLine,
|
|
4254
|
+
chunkType,
|
|
4255
|
+
name: chunk.name ?? void 0,
|
|
4256
|
+
language: chunk.language,
|
|
4257
|
+
hash: chunk.contentHash
|
|
4258
|
+
}
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
4262
|
+
const normalizedHints = identifierHints.flatMap((hint) => [
|
|
4263
|
+
hint,
|
|
4264
|
+
hint.replace(/_/g, ""),
|
|
4265
|
+
hint.replace(/_/g, "-")
|
|
4266
|
+
]).filter((hint, idx, arr) => hint.length >= 3 && arr.indexOf(hint) === idx).slice(0, 6);
|
|
4267
|
+
for (const identifier of normalizedHints) {
|
|
4268
|
+
const symbols = [
|
|
4269
|
+
...database.getSymbolsByName(identifier),
|
|
4270
|
+
...database.getSymbolsByNameCi(identifier)
|
|
4271
|
+
];
|
|
4272
|
+
const chunksByName = [
|
|
4273
|
+
...database.getChunksByName(identifier),
|
|
4274
|
+
...database.getChunksByNameCi(identifier)
|
|
4275
|
+
];
|
|
4276
|
+
const normalizedIdentifier = identifier.replace(/_/g, "");
|
|
4277
|
+
const dedupSymbols = /* @__PURE__ */ new Map();
|
|
4278
|
+
for (const symbol of symbols) {
|
|
4279
|
+
dedupSymbols.set(symbol.id, symbol);
|
|
4280
|
+
}
|
|
4281
|
+
for (const symbol of dedupSymbols.values()) {
|
|
4282
|
+
const chunks = database.getChunksByFile(symbol.filePath);
|
|
4283
|
+
for (const chunk of chunks) {
|
|
4284
|
+
if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
|
|
4285
|
+
continue;
|
|
4286
|
+
}
|
|
4287
|
+
upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
const dedupChunksByName = /* @__PURE__ */ new Map();
|
|
4291
|
+
for (const chunk of chunksByName) {
|
|
4292
|
+
dedupChunksByName.set(chunk.chunkId, chunk);
|
|
4293
|
+
}
|
|
4294
|
+
for (const chunk of dedupChunksByName.values()) {
|
|
4295
|
+
upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
if (filePathHint && primaryHint) {
|
|
4299
|
+
const primaryChunks = [
|
|
4300
|
+
...database.getChunksByName(primaryHint),
|
|
4301
|
+
...database.getChunksByNameCi(primaryHint)
|
|
4302
|
+
];
|
|
4303
|
+
const dedupPrimaryChunks = /* @__PURE__ */ new Map();
|
|
4304
|
+
for (const chunk of primaryChunks) {
|
|
4305
|
+
dedupPrimaryChunks.set(chunk.chunkId, chunk);
|
|
4306
|
+
}
|
|
4307
|
+
for (const chunk of dedupPrimaryChunks.values()) {
|
|
4308
|
+
if (!pathMatchesHint(chunk.filePath, filePathHint)) {
|
|
4309
|
+
continue;
|
|
4310
|
+
}
|
|
4311
|
+
const normalizedPrimary = primaryHint.replace(/_/g, "");
|
|
4312
|
+
upsertChunkCandidate(chunk, primaryHint, normalizedPrimary, 1);
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
const ranked = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4316
|
+
if (ranked.length === 0) {
|
|
4317
|
+
const implementationFallback = fallbackCandidates.filter(
|
|
4318
|
+
(candidate) => isImplementationChunkType(candidate.metadata.chunkType) && isLikelyImplementationPath(candidate.metadata.filePath)
|
|
4319
|
+
);
|
|
4320
|
+
for (const candidate of implementationFallback) {
|
|
4321
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
4322
|
+
const pathLower = candidate.metadata.filePath.toLowerCase();
|
|
4323
|
+
const exactHintMatch = normalizedHints.some((hint) => nameLower === hint || nameLower.replace(/_/g, "") === hint.replace(/_/g, ""));
|
|
4324
|
+
const tokenizedName = tokenizeTextForRanking(nameLower);
|
|
4325
|
+
const tokenHits = codeTermHints.filter((term) => tokenizedName.has(term) || pathLower.includes(term)).length;
|
|
4326
|
+
if (!exactHintMatch && tokenHits === 0) {
|
|
4327
|
+
continue;
|
|
4328
|
+
}
|
|
4329
|
+
const laneScore = exactHintMatch ? Math.min(1, Math.max(candidate.score, 0.97)) : Math.min(0.95, Math.max(candidate.score, 0.82 + tokenHits * 0.03));
|
|
4330
|
+
symbolCandidates.set(candidate.id, {
|
|
4331
|
+
id: candidate.id,
|
|
4332
|
+
score: laneScore,
|
|
4333
|
+
metadata: candidate.metadata
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
if (symbolCandidates.size === 0) {
|
|
4337
|
+
const queryTokenSet = tokenizeTextForRanking(query);
|
|
4338
|
+
const rankedFallback = implementationFallback.map((candidate) => {
|
|
4339
|
+
const nameTokens = tokenizeTextForRanking(candidate.metadata.name ?? "");
|
|
4340
|
+
const pathTokens = splitPathTokens(candidate.metadata.filePath);
|
|
4341
|
+
let overlap = 0;
|
|
4342
|
+
for (const token of queryTokenSet) {
|
|
4343
|
+
if (nameTokens.has(token) || pathTokens.has(token)) {
|
|
4344
|
+
overlap += 1;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
const overlapScore = queryTokenSet.size > 0 ? overlap / queryTokenSet.size : 0;
|
|
4348
|
+
return {
|
|
4349
|
+
candidate,
|
|
4350
|
+
overlapScore
|
|
4351
|
+
};
|
|
4352
|
+
}).filter((entry) => entry.overlapScore > 0).sort((a, b) => b.overlapScore - a.overlapScore || b.candidate.score - a.candidate.score).slice(0, Math.max(limit, 3));
|
|
4353
|
+
for (const entry of rankedFallback) {
|
|
4354
|
+
symbolCandidates.set(entry.candidate.id, {
|
|
4355
|
+
id: entry.candidate.id,
|
|
4356
|
+
score: Math.min(0.94, Math.max(entry.candidate.score, 0.8 + entry.overlapScore * 0.1)),
|
|
4357
|
+
metadata: entry.candidate.metadata
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4363
|
+
return withFallback.slice(0, Math.max(limit * 2, limit));
|
|
4364
|
+
}
|
|
4365
|
+
function buildIdentifierDefinitionLane(query, candidates, limit) {
|
|
4366
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
4367
|
+
return [];
|
|
4368
|
+
}
|
|
4369
|
+
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
4370
|
+
if (!primaryHint) {
|
|
4371
|
+
return [];
|
|
4372
|
+
}
|
|
4373
|
+
const hints = [primaryHint, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].slice(0, 8);
|
|
4374
|
+
const scored = candidates.filter(
|
|
4375
|
+
(candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
|
|
4376
|
+
).map((candidate) => {
|
|
4377
|
+
const matchScore = scoreIdentifierMatch(candidate.metadata.name, candidate.metadata.filePath, hints);
|
|
4378
|
+
return {
|
|
4379
|
+
candidate,
|
|
4380
|
+
matchScore
|
|
4381
|
+
};
|
|
4382
|
+
}).filter((entry) => entry.matchScore > 0).sort((a, b) => {
|
|
4383
|
+
if (b.matchScore !== a.matchScore) return b.matchScore - a.matchScore;
|
|
4384
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
4385
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
4386
|
+
}).slice(0, Math.max(limit * 2, 10));
|
|
4387
|
+
return scored.map((entry) => ({
|
|
4388
|
+
id: entry.candidate.id,
|
|
4389
|
+
score: Math.min(1, 0.9 + entry.matchScore * 0.09),
|
|
4390
|
+
metadata: entry.candidate.metadata
|
|
4391
|
+
}));
|
|
4392
|
+
}
|
|
4393
|
+
function mergeTieredResults(symbolLane, hybridLane, limit) {
|
|
4394
|
+
if (symbolLane.length === 0) {
|
|
4395
|
+
return hybridLane.slice(0, limit);
|
|
4396
|
+
}
|
|
4397
|
+
const out = [];
|
|
4398
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4399
|
+
for (const candidate of symbolLane) {
|
|
4400
|
+
if (seen.has(candidate.id)) continue;
|
|
4401
|
+
out.push(candidate);
|
|
4402
|
+
seen.add(candidate.id);
|
|
4403
|
+
if (out.length >= limit) return out;
|
|
4404
|
+
}
|
|
4405
|
+
for (const candidate of hybridLane) {
|
|
4406
|
+
if (seen.has(candidate.id)) continue;
|
|
4407
|
+
out.push(candidate);
|
|
4408
|
+
seen.add(candidate.id);
|
|
4409
|
+
if (out.length >= limit) return out;
|
|
4410
|
+
}
|
|
4411
|
+
return out;
|
|
4412
|
+
}
|
|
4413
|
+
function unionCandidates(semanticCandidates, keywordCandidates) {
|
|
4414
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4415
|
+
for (const candidate of semanticCandidates) {
|
|
4416
|
+
byId.set(candidate.id, candidate);
|
|
4417
|
+
}
|
|
4418
|
+
for (const candidate of keywordCandidates) {
|
|
4419
|
+
const existing = byId.get(candidate.id);
|
|
4420
|
+
if (!existing || candidate.score > existing.score) {
|
|
4421
|
+
byId.set(candidate.id, candidate);
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
return Array.from(byId.values());
|
|
4425
|
+
}
|
|
3293
4426
|
var Indexer = class {
|
|
3294
4427
|
config;
|
|
3295
4428
|
projectRoot;
|
|
@@ -3415,19 +4548,33 @@ var Indexer = class {
|
|
|
3415
4548
|
return { concurrency: 5, intervalMs: 200, minRetryMs: 1e3, maxRetryMs: 3e4 };
|
|
3416
4549
|
case "ollama":
|
|
3417
4550
|
return { concurrency: 5, intervalMs: 0, minRetryMs: 500, maxRetryMs: 5e3 };
|
|
4551
|
+
case "custom": {
|
|
4552
|
+
const customConfig = this.config.customProvider;
|
|
4553
|
+
return {
|
|
4554
|
+
concurrency: customConfig?.concurrency ?? 3,
|
|
4555
|
+
intervalMs: customConfig?.requestIntervalMs ?? 1e3,
|
|
4556
|
+
minRetryMs: 1e3,
|
|
4557
|
+
maxRetryMs: 3e4
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
3418
4560
|
default:
|
|
3419
4561
|
return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
|
|
3420
4562
|
}
|
|
3421
4563
|
}
|
|
3422
4564
|
async initialize() {
|
|
3423
|
-
if (this.config.embeddingProvider === "
|
|
4565
|
+
if (this.config.embeddingProvider === "custom") {
|
|
4566
|
+
if (!this.config.customProvider) {
|
|
4567
|
+
throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
|
|
4568
|
+
}
|
|
4569
|
+
this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
|
|
4570
|
+
} else if (this.config.embeddingProvider === "auto") {
|
|
3424
4571
|
this.configuredProviderInfo = await tryDetectProvider();
|
|
3425
4572
|
} else {
|
|
3426
4573
|
this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
|
|
3427
4574
|
}
|
|
3428
4575
|
if (!this.configuredProviderInfo) {
|
|
3429
4576
|
throw new Error(
|
|
3430
|
-
"No embedding provider available. Configure GitHub, OpenAI, Google, or
|
|
4577
|
+
"No embedding provider available. Configure GitHub Copilot, OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
|
|
3431
4578
|
);
|
|
3432
4579
|
}
|
|
3433
4580
|
this.logger.info("Initializing indexer", {
|
|
@@ -3437,9 +4584,6 @@ var Indexer = class {
|
|
|
3437
4584
|
});
|
|
3438
4585
|
this.provider = createEmbeddingProvider(this.configuredProviderInfo);
|
|
3439
4586
|
await import_fs4.promises.mkdir(this.indexPath, { recursive: true });
|
|
3440
|
-
if (this.checkForInterruptedIndexing()) {
|
|
3441
|
-
await this.recoverFromInterruptedIndexing();
|
|
3442
|
-
}
|
|
3443
4587
|
const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
|
|
3444
4588
|
const storePath = path5.join(this.indexPath, "vectors");
|
|
3445
4589
|
this.store = new VectorStore(storePath, dimensions);
|
|
@@ -3460,6 +4604,9 @@ var Indexer = class {
|
|
|
3460
4604
|
const dbPath = path5.join(this.indexPath, "codebase.db");
|
|
3461
4605
|
const dbIsNew = !(0, import_fs4.existsSync)(dbPath);
|
|
3462
4606
|
this.database = new Database(dbPath);
|
|
4607
|
+
if (this.checkForInterruptedIndexing()) {
|
|
4608
|
+
await this.recoverFromInterruptedIndexing();
|
|
4609
|
+
}
|
|
3463
4610
|
if (dbIsNew && this.store.count() > 0) {
|
|
3464
4611
|
this.migrateFromLegacyIndex();
|
|
3465
4612
|
}
|
|
@@ -3778,6 +4925,79 @@ var Indexer = class {
|
|
|
3778
4925
|
if (chunkDataBatch.length > 0) {
|
|
3779
4926
|
database.upsertChunksBatch(chunkDataBatch);
|
|
3780
4927
|
}
|
|
4928
|
+
const allSymbolIds = /* @__PURE__ */ new Set();
|
|
4929
|
+
const symbolsByFile = /* @__PURE__ */ new Map();
|
|
4930
|
+
for (let i = 0; i < parsedFiles.length; i++) {
|
|
4931
|
+
const parsed = parsedFiles[i];
|
|
4932
|
+
const changedFile = changedFiles[i];
|
|
4933
|
+
database.deleteCallEdgesByFile(parsed.path);
|
|
4934
|
+
database.deleteSymbolsByFile(parsed.path);
|
|
4935
|
+
const fileSymbols = [];
|
|
4936
|
+
for (const chunk of parsed.chunks) {
|
|
4937
|
+
if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
|
|
4938
|
+
const symbolId = `sym_${hashContent(parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine).slice(0, 16)}`;
|
|
4939
|
+
const symbol = {
|
|
4940
|
+
id: symbolId,
|
|
4941
|
+
filePath: parsed.path,
|
|
4942
|
+
name: chunk.name,
|
|
4943
|
+
kind: chunk.chunkType,
|
|
4944
|
+
startLine: chunk.startLine,
|
|
4945
|
+
startCol: 0,
|
|
4946
|
+
endLine: chunk.endLine,
|
|
4947
|
+
endCol: 0,
|
|
4948
|
+
language: chunk.language
|
|
4949
|
+
};
|
|
4950
|
+
fileSymbols.push(symbol);
|
|
4951
|
+
allSymbolIds.add(symbolId);
|
|
4952
|
+
}
|
|
4953
|
+
const symbolsByName = /* @__PURE__ */ new Map();
|
|
4954
|
+
for (const symbol of fileSymbols) {
|
|
4955
|
+
const existing = symbolsByName.get(symbol.name) ?? [];
|
|
4956
|
+
existing.push(symbol);
|
|
4957
|
+
symbolsByName.set(symbol.name, existing);
|
|
4958
|
+
}
|
|
4959
|
+
if (fileSymbols.length > 0) {
|
|
4960
|
+
database.upsertSymbolsBatch(fileSymbols);
|
|
4961
|
+
symbolsByFile.set(parsed.path, fileSymbols);
|
|
4962
|
+
}
|
|
4963
|
+
const fileLanguage = parsed.chunks[0]?.language;
|
|
4964
|
+
if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) continue;
|
|
4965
|
+
const callSites = extractCalls(changedFile.content, fileLanguage);
|
|
4966
|
+
if (callSites.length === 0) continue;
|
|
4967
|
+
const edges = [];
|
|
4968
|
+
for (const site of callSites) {
|
|
4969
|
+
const enclosingSymbol = fileSymbols.find(
|
|
4970
|
+
(sym) => site.line >= sym.startLine && site.line <= sym.endLine
|
|
4971
|
+
);
|
|
4972
|
+
if (!enclosingSymbol) continue;
|
|
4973
|
+
const edgeId = `edge_${hashContent(enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column).slice(0, 16)}`;
|
|
4974
|
+
edges.push({
|
|
4975
|
+
id: edgeId,
|
|
4976
|
+
fromSymbolId: enclosingSymbol.id,
|
|
4977
|
+
targetName: site.calleeName,
|
|
4978
|
+
toSymbolId: void 0,
|
|
4979
|
+
callType: site.callType,
|
|
4980
|
+
line: site.line,
|
|
4981
|
+
col: site.column,
|
|
4982
|
+
isResolved: false
|
|
4983
|
+
});
|
|
4984
|
+
}
|
|
4985
|
+
if (edges.length > 0) {
|
|
4986
|
+
database.upsertCallEdgesBatch(edges);
|
|
4987
|
+
for (const edge of edges) {
|
|
4988
|
+
const candidates = symbolsByName.get(edge.targetName);
|
|
4989
|
+
if (candidates && candidates.length === 1) {
|
|
4990
|
+
database.resolveCallEdge(edge.id, candidates[0].id);
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
for (const filePath of unchangedFilePaths) {
|
|
4996
|
+
const existingSymbols = database.getSymbolsByFile(filePath);
|
|
4997
|
+
for (const sym of existingSymbols) {
|
|
4998
|
+
allSymbolIds.add(sym.id);
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
3781
5001
|
let removedCount = 0;
|
|
3782
5002
|
for (const [chunkId] of existingChunks) {
|
|
3783
5003
|
if (!currentChunkIds.has(chunkId)) {
|
|
@@ -3799,6 +5019,8 @@ var Indexer = class {
|
|
|
3799
5019
|
if (pendingChunks.length === 0 && removedCount === 0) {
|
|
3800
5020
|
database.clearBranch(this.currentBranch);
|
|
3801
5021
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5022
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5023
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
3802
5024
|
this.fileHashCache = currentFileHashes;
|
|
3803
5025
|
this.saveFileHashCache();
|
|
3804
5026
|
stats.durationMs = Date.now() - startTime;
|
|
@@ -3815,6 +5037,8 @@ var Indexer = class {
|
|
|
3815
5037
|
if (pendingChunks.length === 0) {
|
|
3816
5038
|
database.clearBranch(this.currentBranch);
|
|
3817
5039
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5040
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5041
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
3818
5042
|
store.save();
|
|
3819
5043
|
invertedIndex.save();
|
|
3820
5044
|
this.fileHashCache = currentFileHashes;
|
|
@@ -3880,6 +5104,7 @@ var Indexer = class {
|
|
|
3880
5104
|
minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
|
|
3881
5105
|
maxTimeout: providerRateLimits.maxRetryMs,
|
|
3882
5106
|
factor: 2,
|
|
5107
|
+
shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
|
|
3883
5108
|
onFailedAttempt: (error) => {
|
|
3884
5109
|
const message = getErrorMessage(error);
|
|
3885
5110
|
if (isRateLimitError(error)) {
|
|
@@ -3954,6 +5179,8 @@ var Indexer = class {
|
|
|
3954
5179
|
});
|
|
3955
5180
|
database.clearBranch(this.currentBranch);
|
|
3956
5181
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5182
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5183
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
3957
5184
|
store.save();
|
|
3958
5185
|
invertedIndex.save();
|
|
3959
5186
|
this.fileHashCache = currentFileHashes;
|
|
@@ -4064,15 +5291,22 @@ var Indexer = class {
|
|
|
4064
5291
|
}
|
|
4065
5292
|
const maxResults = limit ?? this.config.search.maxResults;
|
|
4066
5293
|
const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
|
|
5294
|
+
const fusionStrategy = this.config.search.fusionStrategy;
|
|
5295
|
+
const rrfK = this.config.search.rrfK;
|
|
5296
|
+
const rerankTopN = this.config.search.rerankTopN;
|
|
4067
5297
|
const filterByBranch = options?.filterByBranch ?? true;
|
|
4068
5298
|
this.logger.search("debug", "Starting search", {
|
|
4069
5299
|
query,
|
|
4070
5300
|
maxResults,
|
|
4071
5301
|
hybridWeight,
|
|
5302
|
+
fusionStrategy,
|
|
5303
|
+
rrfK,
|
|
5304
|
+
rerankTopN,
|
|
4072
5305
|
filterByBranch
|
|
4073
5306
|
});
|
|
4074
5307
|
const embeddingStartTime = import_perf_hooks.performance.now();
|
|
4075
|
-
const
|
|
5308
|
+
const embeddingQuery = stripFilePathHint(query);
|
|
5309
|
+
const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
|
|
4076
5310
|
const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
|
|
4077
5311
|
const vectorStartTime = import_perf_hooks.performance.now();
|
|
4078
5312
|
const semanticResults = store.search(embedding, maxResults * 4);
|
|
@@ -4080,16 +5314,74 @@ var Indexer = class {
|
|
|
4080
5314
|
const keywordStartTime = import_perf_hooks.performance.now();
|
|
4081
5315
|
const keywordResults = await this.keywordSearch(query, maxResults * 4);
|
|
4082
5316
|
const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
|
|
4083
|
-
const fusionStartTime = import_perf_hooks.performance.now();
|
|
4084
|
-
const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
|
|
4085
|
-
const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
|
|
4086
5317
|
let branchChunkIds = null;
|
|
4087
5318
|
if (filterByBranch && this.currentBranch !== "default") {
|
|
4088
5319
|
branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
|
|
4089
5320
|
}
|
|
4090
|
-
const
|
|
5321
|
+
const prefilterStartTime = import_perf_hooks.performance.now();
|
|
5322
|
+
const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
|
|
5323
|
+
const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
|
|
5324
|
+
const prefilteredKeyword = shouldPrefilterByBranch && branchChunkIds ? keywordResults.filter((r) => branchChunkIds.has(r.id)) : keywordResults;
|
|
5325
|
+
const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
|
|
5326
|
+
const keywordCandidates = shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0 ? keywordResults : prefilteredKeyword;
|
|
5327
|
+
const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
|
|
5328
|
+
if (branchChunkIds && branchChunkIds.size === 0) {
|
|
5329
|
+
this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
|
|
5330
|
+
branch: this.currentBranch
|
|
5331
|
+
});
|
|
5332
|
+
}
|
|
5333
|
+
if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
|
|
5334
|
+
this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
|
|
5335
|
+
branch: this.currentBranch
|
|
5336
|
+
});
|
|
5337
|
+
}
|
|
5338
|
+
if (shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0) {
|
|
5339
|
+
this.logger.search("warn", "Branch prefilter produced no keyword overlap, using unfiltered keyword candidates", {
|
|
5340
|
+
branch: this.currentBranch
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
5343
|
+
const fusionStartTime = import_perf_hooks.performance.now();
|
|
5344
|
+
const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
|
|
5345
|
+
fusionStrategy,
|
|
5346
|
+
rrfK,
|
|
5347
|
+
rerankTopN,
|
|
5348
|
+
limit: maxResults,
|
|
5349
|
+
hybridWeight
|
|
5350
|
+
});
|
|
5351
|
+
const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
|
|
5352
|
+
const rescued = promoteIdentifierMatches(
|
|
5353
|
+
query,
|
|
5354
|
+
combined,
|
|
5355
|
+
semanticCandidates,
|
|
5356
|
+
keywordCandidates,
|
|
5357
|
+
database,
|
|
5358
|
+
branchChunkIds
|
|
5359
|
+
);
|
|
5360
|
+
const union = unionCandidates(semanticCandidates, keywordCandidates);
|
|
5361
|
+
const deterministicIdentifierLane = buildDeterministicIdentifierPass(
|
|
5362
|
+
query,
|
|
5363
|
+
union,
|
|
5364
|
+
maxResults
|
|
5365
|
+
);
|
|
5366
|
+
const identifierLane = buildIdentifierDefinitionLane(
|
|
5367
|
+
query,
|
|
5368
|
+
union,
|
|
5369
|
+
maxResults
|
|
5370
|
+
);
|
|
5371
|
+
const symbolLane = buildSymbolDefinitionLane(
|
|
5372
|
+
query,
|
|
5373
|
+
database,
|
|
5374
|
+
branchChunkIds,
|
|
5375
|
+
maxResults,
|
|
5376
|
+
union
|
|
5377
|
+
);
|
|
5378
|
+
const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
|
|
5379
|
+
const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
|
|
5380
|
+
const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
|
|
5381
|
+
const sourceIntent = classifyQueryIntentRaw(query) === "source";
|
|
5382
|
+
const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
|
|
5383
|
+
const baseFiltered = tiered.filter((r) => {
|
|
4091
5384
|
if (r.score < this.config.search.minScore) return false;
|
|
4092
|
-
if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
|
|
4093
5385
|
if (options?.fileType) {
|
|
4094
5386
|
const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
|
|
4095
5387
|
if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
|
|
@@ -4102,7 +5394,11 @@ var Indexer = class {
|
|
|
4102
5394
|
if (r.metadata.chunkType !== options.chunkType) return false;
|
|
4103
5395
|
}
|
|
4104
5396
|
return true;
|
|
4105
|
-
})
|
|
5397
|
+
});
|
|
5398
|
+
const implementationOnly = baseFiltered.filter(
|
|
5399
|
+
(r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
|
|
5400
|
+
);
|
|
5401
|
+
const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
|
|
4106
5402
|
const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
|
|
4107
5403
|
this.logger.recordSearch(totalSearchMs, {
|
|
4108
5404
|
embeddingMs,
|
|
@@ -4117,6 +5413,7 @@ var Indexer = class {
|
|
|
4117
5413
|
embeddingMs: Math.round(embeddingMs * 100) / 100,
|
|
4118
5414
|
vectorMs: Math.round(vectorMs * 100) / 100,
|
|
4119
5415
|
keywordMs: Math.round(keywordMs * 100) / 100,
|
|
5416
|
+
prefilterMs: Math.round(prefilterMs * 100) / 100,
|
|
4120
5417
|
fusionMs: Math.round(fusionMs * 100) / 100
|
|
4121
5418
|
});
|
|
4122
5419
|
const metadataOnly = options?.metadataOnly ?? false;
|
|
@@ -4170,34 +5467,6 @@ var Indexer = class {
|
|
|
4170
5467
|
results.sort((a, b) => b.score - a.score);
|
|
4171
5468
|
return results.slice(0, limit);
|
|
4172
5469
|
}
|
|
4173
|
-
fuseResults(semanticResults, keywordResults, keywordWeight, limit) {
|
|
4174
|
-
const semanticWeight = 1 - keywordWeight;
|
|
4175
|
-
const fusedScores = /* @__PURE__ */ new Map();
|
|
4176
|
-
for (const r of semanticResults) {
|
|
4177
|
-
fusedScores.set(r.id, {
|
|
4178
|
-
score: r.score * semanticWeight,
|
|
4179
|
-
metadata: r.metadata
|
|
4180
|
-
});
|
|
4181
|
-
}
|
|
4182
|
-
for (const r of keywordResults) {
|
|
4183
|
-
const existing = fusedScores.get(r.id);
|
|
4184
|
-
if (existing) {
|
|
4185
|
-
existing.score += r.score * keywordWeight;
|
|
4186
|
-
} else {
|
|
4187
|
-
fusedScores.set(r.id, {
|
|
4188
|
-
score: r.score * keywordWeight,
|
|
4189
|
-
metadata: r.metadata
|
|
4190
|
-
});
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
|
|
4194
|
-
id,
|
|
4195
|
-
score: data.score,
|
|
4196
|
-
metadata: data.metadata
|
|
4197
|
-
}));
|
|
4198
|
-
results.sort((a, b) => b.score - a.score);
|
|
4199
|
-
return results.slice(0, limit);
|
|
4200
|
-
}
|
|
4201
5470
|
async getStatus() {
|
|
4202
5471
|
const { store, configuredProviderInfo } = await this.ensureInitialized();
|
|
4203
5472
|
return {
|
|
@@ -4220,6 +5489,7 @@ var Indexer = class {
|
|
|
4220
5489
|
this.fileHashCache.clear();
|
|
4221
5490
|
this.saveFileHashCache();
|
|
4222
5491
|
database.clearBranch(this.currentBranch);
|
|
5492
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
4223
5493
|
database.deleteMetadata("index.version");
|
|
4224
5494
|
database.deleteMetadata("index.embeddingProvider");
|
|
4225
5495
|
database.deleteMetadata("index.embeddingModel");
|
|
@@ -4248,6 +5518,8 @@ var Indexer = class {
|
|
|
4248
5518
|
removedCount++;
|
|
4249
5519
|
}
|
|
4250
5520
|
database.deleteChunksByFile(filePath);
|
|
5521
|
+
database.deleteCallEdgesByFile(filePath);
|
|
5522
|
+
database.deleteSymbolsByFile(filePath);
|
|
4251
5523
|
removedFilePaths.push(filePath);
|
|
4252
5524
|
}
|
|
4253
5525
|
}
|
|
@@ -4257,6 +5529,8 @@ var Indexer = class {
|
|
|
4257
5529
|
}
|
|
4258
5530
|
const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
|
|
4259
5531
|
const gcOrphanChunks = database.gcOrphanChunks();
|
|
5532
|
+
const gcOrphanSymbols = database.gcOrphanSymbols();
|
|
5533
|
+
const gcOrphanCallEdges = database.gcOrphanCallEdges();
|
|
4260
5534
|
this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
|
|
4261
5535
|
this.logger.gc("info", "Health check complete", {
|
|
4262
5536
|
removedStale: removedCount,
|
|
@@ -4264,7 +5538,7 @@ var Indexer = class {
|
|
|
4264
5538
|
orphanChunks: gcOrphanChunks,
|
|
4265
5539
|
removedFiles: removedFilePaths.length
|
|
4266
5540
|
});
|
|
4267
|
-
return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
|
|
5541
|
+
return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
|
|
4268
5542
|
}
|
|
4269
5543
|
async retryFailedBatches() {
|
|
4270
5544
|
const { store, provider, invertedIndex } = await this.ensureInitialized();
|
|
@@ -4370,9 +5644,29 @@ var Indexer = class {
|
|
|
4370
5644
|
if (filterByBranch && this.currentBranch !== "default") {
|
|
4371
5645
|
branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
|
|
4372
5646
|
}
|
|
4373
|
-
const
|
|
5647
|
+
const prefilterStartTime = import_perf_hooks.performance.now();
|
|
5648
|
+
const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
|
|
5649
|
+
const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
|
|
5650
|
+
const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
|
|
5651
|
+
const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
|
|
5652
|
+
if (branchChunkIds && branchChunkIds.size === 0) {
|
|
5653
|
+
this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
|
|
5654
|
+
branch: this.currentBranch
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
|
|
5658
|
+
this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
|
|
5659
|
+
branch: this.currentBranch
|
|
5660
|
+
});
|
|
5661
|
+
}
|
|
5662
|
+
const rerankTopN = this.config.search.rerankTopN;
|
|
5663
|
+
const ranked = rankSemanticOnlyResults(code, semanticCandidates, {
|
|
5664
|
+
rerankTopN,
|
|
5665
|
+
limit,
|
|
5666
|
+
prioritizeSourcePaths: false
|
|
5667
|
+
});
|
|
5668
|
+
const filtered = ranked.filter((r) => {
|
|
4374
5669
|
if (r.score < this.config.search.minScore) return false;
|
|
4375
|
-
if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
|
|
4376
5670
|
if (options?.excludeFile) {
|
|
4377
5671
|
if (r.metadata.filePath === options.excludeFile) return false;
|
|
4378
5672
|
}
|
|
@@ -4401,7 +5695,8 @@ var Indexer = class {
|
|
|
4401
5695
|
results: filtered.length,
|
|
4402
5696
|
totalMs: Math.round(totalSearchMs * 100) / 100,
|
|
4403
5697
|
embeddingMs: Math.round(embeddingMs * 100) / 100,
|
|
4404
|
-
vectorMs: Math.round(vectorMs * 100) / 100
|
|
5698
|
+
vectorMs: Math.round(vectorMs * 100) / 100,
|
|
5699
|
+
prefilterMs: Math.round(prefilterMs * 100) / 100
|
|
4405
5700
|
});
|
|
4406
5701
|
return Promise.all(
|
|
4407
5702
|
filtered.map(async (r) => {
|
|
@@ -4430,6 +5725,14 @@ var Indexer = class {
|
|
|
4430
5725
|
})
|
|
4431
5726
|
);
|
|
4432
5727
|
}
|
|
5728
|
+
async getCallers(targetName) {
|
|
5729
|
+
const { database } = await this.ensureInitialized();
|
|
5730
|
+
return database.getCallersWithContext(targetName, this.currentBranch);
|
|
5731
|
+
}
|
|
5732
|
+
async getCallees(symbolId) {
|
|
5733
|
+
const { database } = await this.ensureInitialized();
|
|
5734
|
+
return database.getCallees(symbolId, this.currentBranch);
|
|
5735
|
+
}
|
|
4433
5736
|
};
|
|
4434
5737
|
|
|
4435
5738
|
// src/mcp-server.ts
|
|
@@ -4517,7 +5820,7 @@ var CHUNK_TYPE_ENUM = [
|
|
|
4517
5820
|
function createMcpServer(projectRoot, config) {
|
|
4518
5821
|
const server = new import_mcp.McpServer({
|
|
4519
5822
|
name: "opencode-codebase-index",
|
|
4520
|
-
version: "0.5.
|
|
5823
|
+
version: "0.5.1"
|
|
4521
5824
|
});
|
|
4522
5825
|
const indexer = new Indexer(projectRoot, config);
|
|
4523
5826
|
let initialized = false;
|
|
@@ -4632,7 +5935,7 @@ Use Read tool to examine specific files.` }] };
|
|
|
4632
5935
|
async () => {
|
|
4633
5936
|
await ensureInitialized();
|
|
4634
5937
|
const result = await indexer.healthCheck();
|
|
4635
|
-
if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
|
|
5938
|
+
if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0 && result.gcOrphanSymbols === 0 && result.gcOrphanCallEdges === 0) {
|
|
4636
5939
|
return { content: [{ type: "text", text: "Index is healthy. No stale entries found." }] };
|
|
4637
5940
|
}
|
|
4638
5941
|
const lines = [`Health check complete:`];
|
|
@@ -4645,6 +5948,12 @@ Use Read tool to examine specific files.` }] };
|
|
|
4645
5948
|
if (result.gcOrphanChunks > 0) {
|
|
4646
5949
|
lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
|
|
4647
5950
|
}
|
|
5951
|
+
if (result.gcOrphanSymbols > 0) {
|
|
5952
|
+
lines.push(` Garbage collected orphan symbols: ${result.gcOrphanSymbols}`);
|
|
5953
|
+
}
|
|
5954
|
+
if (result.gcOrphanCallEdges > 0) {
|
|
5955
|
+
lines.push(` Garbage collected orphan call edges: ${result.gcOrphanCallEdges}`);
|
|
5956
|
+
}
|
|
4648
5957
|
if (result.filePaths.length > 0) {
|
|
4649
5958
|
lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
|
|
4650
5959
|
}
|
|
@@ -4733,6 +6042,43 @@ ${truncateContent(r.content)}
|
|
|
4733
6042
|
${formatted.join("\n\n")}` }] };
|
|
4734
6043
|
}
|
|
4735
6044
|
);
|
|
6045
|
+
server.tool(
|
|
6046
|
+
"call_graph",
|
|
6047
|
+
"Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies.",
|
|
6048
|
+
{
|
|
6049
|
+
name: import_zod.z.string().describe("Function or method name to query"),
|
|
6050
|
+
direction: import_zod.z.enum(["callers", "callees"]).default("callers").describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
|
|
6051
|
+
symbolId: import_zod.z.string().optional().describe("Symbol ID (required for 'callees' direction)")
|
|
6052
|
+
},
|
|
6053
|
+
async (args) => {
|
|
6054
|
+
await ensureInitialized();
|
|
6055
|
+
if (args.direction === "callees") {
|
|
6056
|
+
if (!args.symbolId) {
|
|
6057
|
+
return { content: [{ type: "text", text: "Error: 'symbolId' is required when direction is 'callees'." }] };
|
|
6058
|
+
}
|
|
6059
|
+
const callees = await indexer.getCallees(args.symbolId);
|
|
6060
|
+
if (callees.length === 0) {
|
|
6061
|
+
return { content: [{ type: "text", text: `No callees found for symbol ${args.symbolId}.` }] };
|
|
6062
|
+
}
|
|
6063
|
+
const formatted2 = callees.map(
|
|
6064
|
+
(e, i) => `[${i + 1}] \u2192 ${e.targetName} (${e.callType}) at line ${e.line}${e.isResolved ? ` [resolved: ${e.toSymbolId}]` : " [unresolved]"}`
|
|
6065
|
+
);
|
|
6066
|
+
return { content: [{ type: "text", text: `Callees (${callees.length}):
|
|
6067
|
+
|
|
6068
|
+
${formatted2.join("\n")}` }] };
|
|
6069
|
+
}
|
|
6070
|
+
const callers = await indexer.getCallers(args.name);
|
|
6071
|
+
if (callers.length === 0) {
|
|
6072
|
+
return { content: [{ type: "text", text: `No callers found for "${args.name}".` }] };
|
|
6073
|
+
}
|
|
6074
|
+
const formatted = callers.map(
|
|
6075
|
+
(e, i) => `[${i + 1}] \u2190 from ${e.fromSymbolName ?? "<unknown>"} in ${e.fromSymbolFilePath ?? "<unknown file>"} [${e.fromSymbolId}] (${e.callType}) at line ${e.line}${e.isResolved ? " [resolved]" : " [unresolved]"}`
|
|
6076
|
+
);
|
|
6077
|
+
return { content: [{ type: "text", text: `"${args.name}" is called by ${callers.length} function(s):
|
|
6078
|
+
|
|
6079
|
+
${formatted.join("\n")}` }] };
|
|
6080
|
+
}
|
|
6081
|
+
);
|
|
4736
6082
|
server.prompt(
|
|
4737
6083
|
"search",
|
|
4738
6084
|
"Search codebase by meaning using semantic search",
|