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