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/index.cjs
CHANGED
|
@@ -785,9 +785,15 @@ function getDefaultSearchConfig() {
|
|
|
785
785
|
minScore: 0.1,
|
|
786
786
|
includeContext: true,
|
|
787
787
|
hybridWeight: 0.5,
|
|
788
|
+
fusionStrategy: "rrf",
|
|
789
|
+
rrfK: 60,
|
|
790
|
+
rerankTopN: 20,
|
|
788
791
|
contextLines: 0
|
|
789
792
|
};
|
|
790
793
|
}
|
|
794
|
+
function isValidFusionStrategy(value) {
|
|
795
|
+
return value === "weighted" || value === "rrf";
|
|
796
|
+
}
|
|
791
797
|
function getDefaultDebugConfig() {
|
|
792
798
|
return {
|
|
793
799
|
enabled: false,
|
|
@@ -842,6 +848,9 @@ function parseConfig(raw) {
|
|
|
842
848
|
minScore: typeof rawSearch.minScore === "number" ? rawSearch.minScore : defaultSearch.minScore,
|
|
843
849
|
includeContext: typeof rawSearch.includeContext === "boolean" ? rawSearch.includeContext : defaultSearch.includeContext,
|
|
844
850
|
hybridWeight: typeof rawSearch.hybridWeight === "number" ? Math.min(1, Math.max(0, rawSearch.hybridWeight)) : defaultSearch.hybridWeight,
|
|
851
|
+
fusionStrategy: isValidFusionStrategy(rawSearch.fusionStrategy) ? rawSearch.fusionStrategy : defaultSearch.fusionStrategy,
|
|
852
|
+
rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
|
|
853
|
+
rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
|
|
845
854
|
contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines
|
|
846
855
|
};
|
|
847
856
|
const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
|
|
@@ -857,7 +866,32 @@ function parseConfig(raw) {
|
|
|
857
866
|
};
|
|
858
867
|
let embeddingProvider;
|
|
859
868
|
let embeddingModel = void 0;
|
|
860
|
-
|
|
869
|
+
let customProvider = void 0;
|
|
870
|
+
if (input.embeddingProvider === "custom") {
|
|
871
|
+
embeddingProvider = "custom";
|
|
872
|
+
const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
|
|
873
|
+
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) {
|
|
874
|
+
customProvider = {
|
|
875
|
+
baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
|
|
876
|
+
model: rawCustom.model,
|
|
877
|
+
dimensions: rawCustom.dimensions,
|
|
878
|
+
apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
|
|
879
|
+
maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
|
|
880
|
+
timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
|
|
881
|
+
concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
|
|
882
|
+
requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
|
|
883
|
+
};
|
|
884
|
+
if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
|
|
885
|
+
console.warn(
|
|
886
|
+
`[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".`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
throw new Error(
|
|
891
|
+
"embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
} else if (isValidProvider(input.embeddingProvider)) {
|
|
861
895
|
embeddingProvider = input.embeddingProvider;
|
|
862
896
|
if (input.embeddingModel) {
|
|
863
897
|
embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
|
|
@@ -868,6 +902,7 @@ function parseConfig(raw) {
|
|
|
868
902
|
return {
|
|
869
903
|
embeddingProvider,
|
|
870
904
|
embeddingModel,
|
|
905
|
+
customProvider,
|
|
871
906
|
scope: isValidScope(input.scope) ? input.scope : "project",
|
|
872
907
|
include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
|
|
873
908
|
exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
|
|
@@ -2033,12 +2068,39 @@ function getProviderDisplayName(provider) {
|
|
|
2033
2068
|
return "Google (Gemini)";
|
|
2034
2069
|
case "ollama":
|
|
2035
2070
|
return "Ollama (Local)";
|
|
2071
|
+
case "custom":
|
|
2072
|
+
return "Custom (OpenAI-compatible)";
|
|
2036
2073
|
default:
|
|
2037
2074
|
return provider;
|
|
2038
2075
|
}
|
|
2039
2076
|
}
|
|
2077
|
+
function createCustomProviderInfo(config) {
|
|
2078
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2079
|
+
return {
|
|
2080
|
+
provider: "custom",
|
|
2081
|
+
credentials: {
|
|
2082
|
+
provider: "custom",
|
|
2083
|
+
baseUrl,
|
|
2084
|
+
apiKey: config.apiKey
|
|
2085
|
+
},
|
|
2086
|
+
modelInfo: {
|
|
2087
|
+
provider: "custom",
|
|
2088
|
+
model: config.model,
|
|
2089
|
+
dimensions: config.dimensions,
|
|
2090
|
+
maxTokens: config.maxTokens ?? 8192,
|
|
2091
|
+
costPer1MTokens: 0,
|
|
2092
|
+
timeoutMs: config.timeoutMs ?? 3e4
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2040
2096
|
|
|
2041
2097
|
// src/embeddings/provider.ts
|
|
2098
|
+
var CustomProviderNonRetryableError = class extends Error {
|
|
2099
|
+
constructor(message) {
|
|
2100
|
+
super(message);
|
|
2101
|
+
this.name = "CustomProviderNonRetryableError";
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2042
2104
|
function createEmbeddingProvider(configuredProviderInfo) {
|
|
2043
2105
|
switch (configuredProviderInfo.provider) {
|
|
2044
2106
|
case "github-copilot":
|
|
@@ -2049,6 +2111,8 @@ function createEmbeddingProvider(configuredProviderInfo) {
|
|
|
2049
2111
|
return new GoogleEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
|
|
2050
2112
|
case "ollama":
|
|
2051
2113
|
return new OllamaEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
|
|
2114
|
+
case "custom":
|
|
2115
|
+
return new CustomEmbeddingProvider(configuredProviderInfo.credentials, configuredProviderInfo.modelInfo);
|
|
2052
2116
|
default: {
|
|
2053
2117
|
const _exhaustive = configuredProviderInfo;
|
|
2054
2118
|
throw new Error(`Unsupported embedding provider: ${_exhaustive.provider}`);
|
|
@@ -2283,6 +2347,90 @@ var OllamaEmbeddingProvider = class {
|
|
|
2283
2347
|
return this.modelInfo;
|
|
2284
2348
|
}
|
|
2285
2349
|
};
|
|
2350
|
+
var CustomEmbeddingProvider = class {
|
|
2351
|
+
constructor(credentials, modelInfo) {
|
|
2352
|
+
this.credentials = credentials;
|
|
2353
|
+
this.modelInfo = modelInfo;
|
|
2354
|
+
}
|
|
2355
|
+
async embedQuery(query) {
|
|
2356
|
+
const result = await this.embedBatch([query]);
|
|
2357
|
+
return {
|
|
2358
|
+
embedding: result.embeddings[0],
|
|
2359
|
+
tokensUsed: result.totalTokensUsed
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
async embedDocument(document) {
|
|
2363
|
+
const result = await this.embedBatch([document]);
|
|
2364
|
+
return {
|
|
2365
|
+
embedding: result.embeddings[0],
|
|
2366
|
+
tokensUsed: result.totalTokensUsed
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
async embedBatch(texts) {
|
|
2370
|
+
const headers = {
|
|
2371
|
+
"Content-Type": "application/json"
|
|
2372
|
+
};
|
|
2373
|
+
if (this.credentials.apiKey) {
|
|
2374
|
+
headers["Authorization"] = `Bearer ${this.credentials.apiKey}`;
|
|
2375
|
+
}
|
|
2376
|
+
const baseUrl = this.credentials.baseUrl ?? "";
|
|
2377
|
+
const timeoutMs = this.modelInfo.timeoutMs;
|
|
2378
|
+
const controller = new AbortController();
|
|
2379
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2380
|
+
let response;
|
|
2381
|
+
try {
|
|
2382
|
+
response = await fetch(`${baseUrl}/embeddings`, {
|
|
2383
|
+
method: "POST",
|
|
2384
|
+
headers,
|
|
2385
|
+
body: JSON.stringify({
|
|
2386
|
+
model: this.modelInfo.model,
|
|
2387
|
+
input: texts
|
|
2388
|
+
}),
|
|
2389
|
+
signal: controller.signal
|
|
2390
|
+
});
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2393
|
+
throw new Error(`Custom embedding API request timed out after ${timeoutMs}ms for ${baseUrl}/embeddings`);
|
|
2394
|
+
}
|
|
2395
|
+
throw error;
|
|
2396
|
+
} finally {
|
|
2397
|
+
clearTimeout(timeout);
|
|
2398
|
+
}
|
|
2399
|
+
if (!response.ok) {
|
|
2400
|
+
const errorText = await response.text();
|
|
2401
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
2402
|
+
throw new CustomProviderNonRetryableError(`Custom embedding API error (non-retryable): ${response.status} - ${errorText}`);
|
|
2403
|
+
}
|
|
2404
|
+
throw new Error(`Custom embedding API error: ${response.status} - ${errorText}`);
|
|
2405
|
+
}
|
|
2406
|
+
const data = await response.json();
|
|
2407
|
+
if (data.data && Array.isArray(data.data)) {
|
|
2408
|
+
if (data.data.length > 0) {
|
|
2409
|
+
const actualDims = data.data[0].embedding.length;
|
|
2410
|
+
if (actualDims !== this.modelInfo.dimensions) {
|
|
2411
|
+
throw new Error(
|
|
2412
|
+
`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.`
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
if (data.data.length !== texts.length) {
|
|
2417
|
+
throw new Error(
|
|
2418
|
+
`Embedding count mismatch: sent ${texts.length} texts but received ${data.data.length} embeddings. The custom embedding server may not support batch input.`
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
return {
|
|
2422
|
+
embeddings: data.data.map((d) => d.embedding),
|
|
2423
|
+
// Rough estimate: ~4 chars per token. Used as fallback when the server
|
|
2424
|
+
// doesn't return usage.total_tokens (e.g. llama.cpp, some vLLM configs).
|
|
2425
|
+
totalTokensUsed: data.usage?.total_tokens ?? texts.reduce((sum, t) => sum + Math.ceil(t.length / 4), 0)
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
|
|
2429
|
+
}
|
|
2430
|
+
getModelInfo() {
|
|
2431
|
+
return this.modelInfo;
|
|
2432
|
+
}
|
|
2433
|
+
};
|
|
2286
2434
|
|
|
2287
2435
|
// src/utils/files.ts
|
|
2288
2436
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
@@ -2838,6 +2986,9 @@ function hashContent(content) {
|
|
|
2838
2986
|
function hashFile(filePath) {
|
|
2839
2987
|
return native.hashFile(filePath);
|
|
2840
2988
|
}
|
|
2989
|
+
function extractCalls(content, language) {
|
|
2990
|
+
return native.extractCalls(content, language);
|
|
2991
|
+
}
|
|
2841
2992
|
var VectorStore = class {
|
|
2842
2993
|
inner;
|
|
2843
2994
|
dimensions;
|
|
@@ -3169,6 +3320,12 @@ var Database = class {
|
|
|
3169
3320
|
getChunksByFile(filePath) {
|
|
3170
3321
|
return this.inner.getChunksByFile(filePath);
|
|
3171
3322
|
}
|
|
3323
|
+
getChunksByName(name) {
|
|
3324
|
+
return this.inner.getChunksByName(name);
|
|
3325
|
+
}
|
|
3326
|
+
getChunksByNameCi(name) {
|
|
3327
|
+
return this.inner.getChunksByNameCi(name);
|
|
3328
|
+
}
|
|
3172
3329
|
deleteChunksByFile(filePath) {
|
|
3173
3330
|
return this.inner.deleteChunksByFile(filePath);
|
|
3174
3331
|
}
|
|
@@ -3212,6 +3369,73 @@ var Database = class {
|
|
|
3212
3369
|
getStats() {
|
|
3213
3370
|
return this.inner.getStats();
|
|
3214
3371
|
}
|
|
3372
|
+
// ── Symbol methods ──────────────────────────────────────────────
|
|
3373
|
+
upsertSymbol(symbol) {
|
|
3374
|
+
this.inner.upsertSymbol(symbol);
|
|
3375
|
+
}
|
|
3376
|
+
upsertSymbolsBatch(symbols) {
|
|
3377
|
+
if (symbols.length === 0) return;
|
|
3378
|
+
this.inner.upsertSymbolsBatch(symbols);
|
|
3379
|
+
}
|
|
3380
|
+
getSymbolsByFile(filePath) {
|
|
3381
|
+
return this.inner.getSymbolsByFile(filePath);
|
|
3382
|
+
}
|
|
3383
|
+
getSymbolByName(name, filePath) {
|
|
3384
|
+
return this.inner.getSymbolByName(name, filePath) ?? null;
|
|
3385
|
+
}
|
|
3386
|
+
getSymbolsByName(name) {
|
|
3387
|
+
return this.inner.getSymbolsByName(name);
|
|
3388
|
+
}
|
|
3389
|
+
getSymbolsByNameCi(name) {
|
|
3390
|
+
return this.inner.getSymbolsByNameCi(name);
|
|
3391
|
+
}
|
|
3392
|
+
deleteSymbolsByFile(filePath) {
|
|
3393
|
+
return this.inner.deleteSymbolsByFile(filePath);
|
|
3394
|
+
}
|
|
3395
|
+
// ── Call Edge methods ────────────────────────────────────────────
|
|
3396
|
+
upsertCallEdge(edge) {
|
|
3397
|
+
this.inner.upsertCallEdge(edge);
|
|
3398
|
+
}
|
|
3399
|
+
upsertCallEdgesBatch(edges) {
|
|
3400
|
+
if (edges.length === 0) return;
|
|
3401
|
+
this.inner.upsertCallEdgesBatch(edges);
|
|
3402
|
+
}
|
|
3403
|
+
getCallers(targetName, branch) {
|
|
3404
|
+
return this.inner.getCallers(targetName, branch);
|
|
3405
|
+
}
|
|
3406
|
+
getCallersWithContext(targetName, branch) {
|
|
3407
|
+
return this.inner.getCallersWithContext(targetName, branch);
|
|
3408
|
+
}
|
|
3409
|
+
getCallees(symbolId, branch) {
|
|
3410
|
+
return this.inner.getCallees(symbolId, branch);
|
|
3411
|
+
}
|
|
3412
|
+
deleteCallEdgesByFile(filePath) {
|
|
3413
|
+
return this.inner.deleteCallEdgesByFile(filePath);
|
|
3414
|
+
}
|
|
3415
|
+
resolveCallEdge(edgeId, toSymbolId) {
|
|
3416
|
+
this.inner.resolveCallEdge(edgeId, toSymbolId);
|
|
3417
|
+
}
|
|
3418
|
+
// ── Branch Symbol methods ────────────────────────────────────────
|
|
3419
|
+
addSymbolsToBranch(branch, symbolIds) {
|
|
3420
|
+
this.inner.addSymbolsToBranch(branch, symbolIds);
|
|
3421
|
+
}
|
|
3422
|
+
addSymbolsToBranchBatch(branch, symbolIds) {
|
|
3423
|
+
if (symbolIds.length === 0) return;
|
|
3424
|
+
this.inner.addSymbolsToBranchBatch(branch, symbolIds);
|
|
3425
|
+
}
|
|
3426
|
+
getBranchSymbolIds(branch) {
|
|
3427
|
+
return this.inner.getBranchSymbolIds(branch);
|
|
3428
|
+
}
|
|
3429
|
+
clearBranchSymbols(branch) {
|
|
3430
|
+
return this.inner.clearBranchSymbols(branch);
|
|
3431
|
+
}
|
|
3432
|
+
// ── GC methods for symbols/edges ─────────────────────────────────
|
|
3433
|
+
gcOrphanSymbols() {
|
|
3434
|
+
return this.inner.gcOrphanSymbols();
|
|
3435
|
+
}
|
|
3436
|
+
gcOrphanCallEdges() {
|
|
3437
|
+
return this.inner.gcOrphanCallEdges();
|
|
3438
|
+
}
|
|
3215
3439
|
};
|
|
3216
3440
|
|
|
3217
3441
|
// src/git/index.ts
|
|
@@ -3319,6 +3543,29 @@ function getHeadPath(repoRoot) {
|
|
|
3319
3543
|
}
|
|
3320
3544
|
|
|
3321
3545
|
// src/indexer/index.ts
|
|
3546
|
+
var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust"]);
|
|
3547
|
+
var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
3548
|
+
"function_declaration",
|
|
3549
|
+
"function",
|
|
3550
|
+
"arrow_function",
|
|
3551
|
+
"method_definition",
|
|
3552
|
+
"class_declaration",
|
|
3553
|
+
"interface_declaration",
|
|
3554
|
+
"type_alias_declaration",
|
|
3555
|
+
"enum_declaration",
|
|
3556
|
+
"function_definition",
|
|
3557
|
+
"class_definition",
|
|
3558
|
+
"decorated_definition",
|
|
3559
|
+
"method_declaration",
|
|
3560
|
+
"type_declaration",
|
|
3561
|
+
"type_spec",
|
|
3562
|
+
"function_item",
|
|
3563
|
+
"impl_item",
|
|
3564
|
+
"struct_item",
|
|
3565
|
+
"enum_item",
|
|
3566
|
+
"trait_item",
|
|
3567
|
+
"mod_item"
|
|
3568
|
+
]);
|
|
3322
3569
|
function float32ArrayToBuffer(arr) {
|
|
3323
3570
|
const float32 = new Float32Array(arr);
|
|
3324
3571
|
return Buffer.from(float32.buffer);
|
|
@@ -3343,6 +3590,892 @@ function isRateLimitError(error) {
|
|
|
3343
3590
|
return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
|
|
3344
3591
|
}
|
|
3345
3592
|
var INDEX_METADATA_VERSION = "1";
|
|
3593
|
+
var RANKING_TOKEN_CACHE_LIMIT = 4096;
|
|
3594
|
+
var rankingQueryTokenCache = /* @__PURE__ */ new Map();
|
|
3595
|
+
var rankingNameTokenCache = /* @__PURE__ */ new Map();
|
|
3596
|
+
var rankingPathTokenCache = /* @__PURE__ */ new Map();
|
|
3597
|
+
var rankingTextTokenCache = /* @__PURE__ */ new Map();
|
|
3598
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
3599
|
+
"the",
|
|
3600
|
+
"and",
|
|
3601
|
+
"for",
|
|
3602
|
+
"with",
|
|
3603
|
+
"from",
|
|
3604
|
+
"that",
|
|
3605
|
+
"this",
|
|
3606
|
+
"into",
|
|
3607
|
+
"using",
|
|
3608
|
+
"where",
|
|
3609
|
+
"what",
|
|
3610
|
+
"when",
|
|
3611
|
+
"why",
|
|
3612
|
+
"how",
|
|
3613
|
+
"are",
|
|
3614
|
+
"was",
|
|
3615
|
+
"were",
|
|
3616
|
+
"be",
|
|
3617
|
+
"been",
|
|
3618
|
+
"being",
|
|
3619
|
+
"find",
|
|
3620
|
+
"show",
|
|
3621
|
+
"get",
|
|
3622
|
+
"run",
|
|
3623
|
+
"use",
|
|
3624
|
+
"code",
|
|
3625
|
+
"function",
|
|
3626
|
+
"implementation",
|
|
3627
|
+
"retrieve",
|
|
3628
|
+
"results",
|
|
3629
|
+
"result",
|
|
3630
|
+
"search",
|
|
3631
|
+
"pipeline",
|
|
3632
|
+
"top",
|
|
3633
|
+
"in",
|
|
3634
|
+
"on",
|
|
3635
|
+
"of",
|
|
3636
|
+
"to",
|
|
3637
|
+
"by",
|
|
3638
|
+
"as",
|
|
3639
|
+
"or",
|
|
3640
|
+
"an",
|
|
3641
|
+
"a"
|
|
3642
|
+
]);
|
|
3643
|
+
var TEST_PATH_SEGMENTS = [
|
|
3644
|
+
"tests/",
|
|
3645
|
+
"__tests__/",
|
|
3646
|
+
"/test/",
|
|
3647
|
+
"fixtures/",
|
|
3648
|
+
"benchmark",
|
|
3649
|
+
"README",
|
|
3650
|
+
"ARCHITECTURE",
|
|
3651
|
+
"docs/"
|
|
3652
|
+
];
|
|
3653
|
+
var IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS = [
|
|
3654
|
+
"tests/",
|
|
3655
|
+
"__tests__/",
|
|
3656
|
+
"/test/",
|
|
3657
|
+
"fixtures/",
|
|
3658
|
+
"benchmark",
|
|
3659
|
+
"readme",
|
|
3660
|
+
"architecture",
|
|
3661
|
+
"docs/",
|
|
3662
|
+
"examples/",
|
|
3663
|
+
"example/",
|
|
3664
|
+
".github/",
|
|
3665
|
+
"/scripts/",
|
|
3666
|
+
"/migrations/",
|
|
3667
|
+
"/generated/"
|
|
3668
|
+
];
|
|
3669
|
+
var SOURCE_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3670
|
+
"implement",
|
|
3671
|
+
"implementation",
|
|
3672
|
+
"function",
|
|
3673
|
+
"method",
|
|
3674
|
+
"class",
|
|
3675
|
+
"logic",
|
|
3676
|
+
"algorithm",
|
|
3677
|
+
"pipeline",
|
|
3678
|
+
"indexer",
|
|
3679
|
+
"where"
|
|
3680
|
+
]);
|
|
3681
|
+
var DOC_TEST_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3682
|
+
"test",
|
|
3683
|
+
"tests",
|
|
3684
|
+
"fixture",
|
|
3685
|
+
"fixtures",
|
|
3686
|
+
"benchmark",
|
|
3687
|
+
"readme",
|
|
3688
|
+
"docs",
|
|
3689
|
+
"documentation"
|
|
3690
|
+
]);
|
|
3691
|
+
var DOC_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3692
|
+
"readme",
|
|
3693
|
+
"docs",
|
|
3694
|
+
"documentation",
|
|
3695
|
+
"guide",
|
|
3696
|
+
"usage"
|
|
3697
|
+
]);
|
|
3698
|
+
function setBoundedCache(cache, key, value) {
|
|
3699
|
+
if (cache.size >= RANKING_TOKEN_CACHE_LIMIT) {
|
|
3700
|
+
const oldest = cache.keys().next().value;
|
|
3701
|
+
if (oldest !== void 0) {
|
|
3702
|
+
cache.delete(oldest);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
cache.set(key, value);
|
|
3706
|
+
}
|
|
3707
|
+
function tokenizeTextForRanking(text) {
|
|
3708
|
+
if (!text) {
|
|
3709
|
+
return /* @__PURE__ */ new Set();
|
|
3710
|
+
}
|
|
3711
|
+
const lowered = text.toLowerCase();
|
|
3712
|
+
const cache = rankingQueryTokenCache.get(lowered) ?? rankingTextTokenCache.get(lowered);
|
|
3713
|
+
if (cache) {
|
|
3714
|
+
return cache;
|
|
3715
|
+
}
|
|
3716
|
+
const tokens = new Set(
|
|
3717
|
+
lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1 && !STOPWORDS.has(token))
|
|
3718
|
+
);
|
|
3719
|
+
setBoundedCache(rankingQueryTokenCache, lowered, tokens);
|
|
3720
|
+
setBoundedCache(rankingTextTokenCache, lowered, tokens);
|
|
3721
|
+
return tokens;
|
|
3722
|
+
}
|
|
3723
|
+
function splitPathTokens(filePath) {
|
|
3724
|
+
const lowered = filePath.toLowerCase();
|
|
3725
|
+
const cache = rankingPathTokenCache.get(lowered);
|
|
3726
|
+
if (cache) {
|
|
3727
|
+
return cache;
|
|
3728
|
+
}
|
|
3729
|
+
const normalized = lowered.replace(/[^a-z0-9/._-]/g, " ").split(/[/._-]+/).filter((token) => token.length > 1);
|
|
3730
|
+
const tokens = new Set(normalized);
|
|
3731
|
+
setBoundedCache(rankingPathTokenCache, lowered, tokens);
|
|
3732
|
+
return tokens;
|
|
3733
|
+
}
|
|
3734
|
+
function splitNameTokens(name) {
|
|
3735
|
+
if (!name) {
|
|
3736
|
+
return /* @__PURE__ */ new Set();
|
|
3737
|
+
}
|
|
3738
|
+
const lowered = name.toLowerCase();
|
|
3739
|
+
const cache = rankingNameTokenCache.get(lowered);
|
|
3740
|
+
if (cache) {
|
|
3741
|
+
return cache;
|
|
3742
|
+
}
|
|
3743
|
+
const tokens = new Set(
|
|
3744
|
+
lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1)
|
|
3745
|
+
);
|
|
3746
|
+
setBoundedCache(rankingNameTokenCache, lowered, tokens);
|
|
3747
|
+
return tokens;
|
|
3748
|
+
}
|
|
3749
|
+
function chunkTypeBoost(chunkType) {
|
|
3750
|
+
switch (chunkType) {
|
|
3751
|
+
case "function":
|
|
3752
|
+
case "function_declaration":
|
|
3753
|
+
case "method":
|
|
3754
|
+
case "method_definition":
|
|
3755
|
+
case "class":
|
|
3756
|
+
case "class_declaration":
|
|
3757
|
+
return 0.2;
|
|
3758
|
+
case "interface":
|
|
3759
|
+
case "type":
|
|
3760
|
+
case "enum":
|
|
3761
|
+
case "struct":
|
|
3762
|
+
case "impl":
|
|
3763
|
+
case "trait":
|
|
3764
|
+
case "module":
|
|
3765
|
+
return 0.1;
|
|
3766
|
+
default:
|
|
3767
|
+
return 0;
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
function isTestOrDocPath(filePath) {
|
|
3771
|
+
return TEST_PATH_SEGMENTS.some((segment) => filePath.includes(segment));
|
|
3772
|
+
}
|
|
3773
|
+
function isLikelyImplementationPath(filePath) {
|
|
3774
|
+
const lowered = filePath.toLowerCase();
|
|
3775
|
+
if (IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS.some((segment) => lowered.includes(segment))) {
|
|
3776
|
+
return false;
|
|
3777
|
+
}
|
|
3778
|
+
const ext = lowered.split(".").pop() ?? "";
|
|
3779
|
+
if (["md", "mdx", "txt", "rst", "adoc", "snap", "json", "yaml", "yml", "lock"].includes(ext)) {
|
|
3780
|
+
return false;
|
|
3781
|
+
}
|
|
3782
|
+
return true;
|
|
3783
|
+
}
|
|
3784
|
+
function classifyQueryIntent(tokens) {
|
|
3785
|
+
const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
|
|
3786
|
+
const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
|
|
3787
|
+
return sourceIntentHits >= docTestIntentHits ? "source" : "doc_test";
|
|
3788
|
+
}
|
|
3789
|
+
function classifyQueryIntentRaw(query) {
|
|
3790
|
+
const lowerQuery = query.toLowerCase();
|
|
3791
|
+
const docTestRawHits = Array.from(DOC_TEST_INTENT_HINTS).filter(
|
|
3792
|
+
(hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)
|
|
3793
|
+
).length;
|
|
3794
|
+
const sourceRawHits = [
|
|
3795
|
+
"implement",
|
|
3796
|
+
"implementation",
|
|
3797
|
+
"implements",
|
|
3798
|
+
"function",
|
|
3799
|
+
"method",
|
|
3800
|
+
"class",
|
|
3801
|
+
"logic",
|
|
3802
|
+
"algorithm",
|
|
3803
|
+
"pipeline",
|
|
3804
|
+
"indexer"
|
|
3805
|
+
].filter((hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)).length;
|
|
3806
|
+
if (docTestRawHits > sourceRawHits) {
|
|
3807
|
+
return "doc_test";
|
|
3808
|
+
}
|
|
3809
|
+
if (sourceRawHits > docTestRawHits) {
|
|
3810
|
+
return "source";
|
|
3811
|
+
}
|
|
3812
|
+
const hasWhereIsPattern = /\bwhere\s+is\b/.test(lowerQuery);
|
|
3813
|
+
const hasIdentifierHints = extractIdentifierHints(query).length > 0;
|
|
3814
|
+
if (hasWhereIsPattern && hasIdentifierHints && docTestRawHits === 0) {
|
|
3815
|
+
return "source";
|
|
3816
|
+
}
|
|
3817
|
+
const queryTokens = Array.from(tokenizeTextForRanking(query));
|
|
3818
|
+
return classifyQueryIntent(queryTokens);
|
|
3819
|
+
}
|
|
3820
|
+
function classifyDocIntent(tokens) {
|
|
3821
|
+
const docHits = tokens.filter((t) => DOC_INTENT_HINTS.has(t)).length;
|
|
3822
|
+
const testHits = tokens.filter((t) => ["test", "tests", "fixture", "fixtures", "benchmark"].includes(t)).length;
|
|
3823
|
+
if (docHits > 0 && testHits === 0) return "docs";
|
|
3824
|
+
if (testHits > 0 && docHits === 0) return "test";
|
|
3825
|
+
if (testHits > 0 || docHits > 0) return "mixed";
|
|
3826
|
+
return "none";
|
|
3827
|
+
}
|
|
3828
|
+
function isImplementationChunkType(chunkType) {
|
|
3829
|
+
return [
|
|
3830
|
+
"export_statement",
|
|
3831
|
+
"function",
|
|
3832
|
+
"function_declaration",
|
|
3833
|
+
"method",
|
|
3834
|
+
"method_definition",
|
|
3835
|
+
"class",
|
|
3836
|
+
"class_declaration",
|
|
3837
|
+
"interface",
|
|
3838
|
+
"type",
|
|
3839
|
+
"enum",
|
|
3840
|
+
"module"
|
|
3841
|
+
].includes(chunkType);
|
|
3842
|
+
}
|
|
3843
|
+
function extractIdentifierHints(query) {
|
|
3844
|
+
const identifiers = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
3845
|
+
return identifiers.filter((id) => id.length >= 3).filter((id) => {
|
|
3846
|
+
const lower = id.toLowerCase();
|
|
3847
|
+
if (STOPWORDS.has(lower)) return false;
|
|
3848
|
+
return /[A-Z]/.test(id) || id.includes("_") || id.endsWith("Results") || id.endsWith("Result");
|
|
3849
|
+
}).map((id) => id.toLowerCase());
|
|
3850
|
+
}
|
|
3851
|
+
function extractCodeTermHints(query) {
|
|
3852
|
+
const terms = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
3853
|
+
return terms.map((term) => term.toLowerCase()).filter((term) => term.length >= 3).filter((term) => !STOPWORDS.has(term));
|
|
3854
|
+
}
|
|
3855
|
+
function normalizeIdentifierVariants(identifier) {
|
|
3856
|
+
const lower = identifier.toLowerCase();
|
|
3857
|
+
const compact = lower.replace(/[^a-z0-9]/g, "");
|
|
3858
|
+
const snake = compact.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
3859
|
+
const kebab = snake.replace(/_/g, "-");
|
|
3860
|
+
const variants = [lower, compact, snake, kebab].filter((value) => value.length > 0);
|
|
3861
|
+
return Array.from(new Set(variants));
|
|
3862
|
+
}
|
|
3863
|
+
function scoreIdentifierMatch(name, filePath, hints) {
|
|
3864
|
+
const nameLower = (name ?? "").toLowerCase();
|
|
3865
|
+
const pathLower = filePath.toLowerCase();
|
|
3866
|
+
let best = 0;
|
|
3867
|
+
for (const hint of hints) {
|
|
3868
|
+
const variants = normalizeIdentifierVariants(hint);
|
|
3869
|
+
for (const variant of variants) {
|
|
3870
|
+
if (nameLower === variant) {
|
|
3871
|
+
best = Math.max(best, 1);
|
|
3872
|
+
} else if (nameLower.includes(variant)) {
|
|
3873
|
+
best = Math.max(best, 0.8);
|
|
3874
|
+
} else if (pathLower.includes(variant)) {
|
|
3875
|
+
best = Math.max(best, 0.6);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
return best;
|
|
3880
|
+
}
|
|
3881
|
+
function extractPrimaryIdentifierQueryHint(query) {
|
|
3882
|
+
const identifiers = extractIdentifierHints(query);
|
|
3883
|
+
if (identifiers.length > 0) {
|
|
3884
|
+
return identifiers[0] ?? null;
|
|
3885
|
+
}
|
|
3886
|
+
const codeTerms = extractCodeTermHints(query);
|
|
3887
|
+
const best = codeTerms.find((term) => term.length >= 6);
|
|
3888
|
+
return best ?? null;
|
|
3889
|
+
}
|
|
3890
|
+
var FILE_PATH_HINT_EXTENSIONS = [
|
|
3891
|
+
"ts",
|
|
3892
|
+
"tsx",
|
|
3893
|
+
"js",
|
|
3894
|
+
"jsx",
|
|
3895
|
+
"mjs",
|
|
3896
|
+
"cjs",
|
|
3897
|
+
"mts",
|
|
3898
|
+
"cts",
|
|
3899
|
+
"py",
|
|
3900
|
+
"rs",
|
|
3901
|
+
"go",
|
|
3902
|
+
"java",
|
|
3903
|
+
"kt",
|
|
3904
|
+
"kts",
|
|
3905
|
+
"swift",
|
|
3906
|
+
"rb",
|
|
3907
|
+
"php",
|
|
3908
|
+
"c",
|
|
3909
|
+
"h",
|
|
3910
|
+
"cc",
|
|
3911
|
+
"cpp",
|
|
3912
|
+
"cxx",
|
|
3913
|
+
"hpp",
|
|
3914
|
+
"cs",
|
|
3915
|
+
"scala",
|
|
3916
|
+
"lua",
|
|
3917
|
+
"sh",
|
|
3918
|
+
"bash",
|
|
3919
|
+
"zsh",
|
|
3920
|
+
"json",
|
|
3921
|
+
"yaml",
|
|
3922
|
+
"yml",
|
|
3923
|
+
"toml"
|
|
3924
|
+
];
|
|
3925
|
+
var FILE_PATH_HINT_SUFFIX_REGEX = new RegExp(
|
|
3926
|
+
"\\s+\\bin\\s+[\"'`]?((?:\\.\\/)?(?:[A-Za-z0-9._-]+\\/)+[A-Za-z0-9._-]+\\.(?:" + FILE_PATH_HINT_EXTENSIONS.join("|") + "))[\"'`]?[\\])}>.,;!?]*\\s*$",
|
|
3927
|
+
"i"
|
|
3928
|
+
);
|
|
3929
|
+
function normalizeFilePathForHintMatch(filePath) {
|
|
3930
|
+
return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
|
|
3931
|
+
}
|
|
3932
|
+
function pathMatchesHint(filePath, hint) {
|
|
3933
|
+
const normalizedPath = normalizeFilePathForHintMatch(filePath);
|
|
3934
|
+
const normalizedHint = normalizeFilePathForHintMatch(hint);
|
|
3935
|
+
return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
|
|
3936
|
+
}
|
|
3937
|
+
function extractFilePathHint(query) {
|
|
3938
|
+
const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
|
|
3939
|
+
const rawPath = match?.[1];
|
|
3940
|
+
if (!rawPath) {
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3943
|
+
return rawPath.replace(/^\.\//, "");
|
|
3944
|
+
}
|
|
3945
|
+
function stripFilePathHint(query) {
|
|
3946
|
+
const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
|
|
3947
|
+
return stripped.length > 0 ? stripped : query;
|
|
3948
|
+
}
|
|
3949
|
+
function buildDeterministicIdentifierPass(query, candidates, limit) {
|
|
3950
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
3951
|
+
return [];
|
|
3952
|
+
}
|
|
3953
|
+
const primary = extractPrimaryIdentifierQueryHint(query);
|
|
3954
|
+
if (!primary) {
|
|
3955
|
+
return [];
|
|
3956
|
+
}
|
|
3957
|
+
const filePathHint = extractFilePathHint(query);
|
|
3958
|
+
const primaryVariants = normalizeIdentifierVariants(primary);
|
|
3959
|
+
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);
|
|
3960
|
+
const deterministic = candidates.filter(
|
|
3961
|
+
(candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
|
|
3962
|
+
).map((candidate) => {
|
|
3963
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
3964
|
+
const pathLower = candidate.metadata.filePath.toLowerCase();
|
|
3965
|
+
let maxMatch = 0;
|
|
3966
|
+
const nameMatchesPrimary = primaryVariants.some(
|
|
3967
|
+
(variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
|
|
3968
|
+
);
|
|
3969
|
+
const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
|
|
3970
|
+
for (const hint of hints) {
|
|
3971
|
+
const variants = normalizeIdentifierVariants(hint);
|
|
3972
|
+
for (const variant of variants) {
|
|
3973
|
+
if (nameLower === variant) {
|
|
3974
|
+
maxMatch = Math.max(maxMatch, 1);
|
|
3975
|
+
} else if (nameLower.includes(variant)) {
|
|
3976
|
+
maxMatch = Math.max(maxMatch, 0.85);
|
|
3977
|
+
} else if (pathLower.includes(variant)) {
|
|
3978
|
+
maxMatch = Math.max(maxMatch, 0.7);
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
if (pathMatchesFileHint && nameMatchesPrimary) {
|
|
3983
|
+
maxMatch = Math.max(maxMatch, 1);
|
|
3984
|
+
}
|
|
3985
|
+
return {
|
|
3986
|
+
candidate,
|
|
3987
|
+
maxMatch,
|
|
3988
|
+
pathMatchesFileHint,
|
|
3989
|
+
nameMatchesPrimary
|
|
3990
|
+
};
|
|
3991
|
+
}).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
|
|
3992
|
+
const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
|
|
3993
|
+
const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
|
|
3994
|
+
if (aAnchored !== bAnchored) return bAnchored - aAnchored;
|
|
3995
|
+
if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
|
|
3996
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
3997
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
3998
|
+
}).slice(0, Math.max(limit * 2, 12));
|
|
3999
|
+
return deterministic.map((entry) => ({
|
|
4000
|
+
id: entry.candidate.id,
|
|
4001
|
+
score: entry.pathMatchesFileHint && entry.nameMatchesPrimary ? 0.995 : Math.min(1, 0.9 + entry.maxMatch * 0.09),
|
|
4002
|
+
metadata: entry.candidate.metadata
|
|
4003
|
+
}));
|
|
4004
|
+
}
|
|
4005
|
+
function fuseResultsWeighted(semanticResults, keywordResults, keywordWeight, limit) {
|
|
4006
|
+
const semanticWeight = 1 - keywordWeight;
|
|
4007
|
+
const fusedScores = /* @__PURE__ */ new Map();
|
|
4008
|
+
for (const r of semanticResults) {
|
|
4009
|
+
fusedScores.set(r.id, {
|
|
4010
|
+
score: r.score * semanticWeight,
|
|
4011
|
+
metadata: r.metadata
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
for (const r of keywordResults) {
|
|
4015
|
+
const existing = fusedScores.get(r.id);
|
|
4016
|
+
if (existing) {
|
|
4017
|
+
existing.score += r.score * keywordWeight;
|
|
4018
|
+
} else {
|
|
4019
|
+
fusedScores.set(r.id, {
|
|
4020
|
+
score: r.score * keywordWeight,
|
|
4021
|
+
metadata: r.metadata
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
|
|
4026
|
+
id,
|
|
4027
|
+
score: data.score,
|
|
4028
|
+
metadata: data.metadata
|
|
4029
|
+
}));
|
|
4030
|
+
results.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4031
|
+
return results.slice(0, limit);
|
|
4032
|
+
}
|
|
4033
|
+
function fuseResultsRrf(semanticResults, keywordResults, rrfK, limit) {
|
|
4034
|
+
const maxPossibleRaw = 2 / (rrfK + 1);
|
|
4035
|
+
const rankByIdSemantic = /* @__PURE__ */ new Map();
|
|
4036
|
+
const rankByIdKeyword = /* @__PURE__ */ new Map();
|
|
4037
|
+
const metadataById = /* @__PURE__ */ new Map();
|
|
4038
|
+
semanticResults.forEach((result, index) => {
|
|
4039
|
+
rankByIdSemantic.set(result.id, index + 1);
|
|
4040
|
+
metadataById.set(result.id, result.metadata);
|
|
4041
|
+
});
|
|
4042
|
+
keywordResults.forEach((result, index) => {
|
|
4043
|
+
rankByIdKeyword.set(result.id, index + 1);
|
|
4044
|
+
if (!metadataById.has(result.id)) {
|
|
4045
|
+
metadataById.set(result.id, result.metadata);
|
|
4046
|
+
}
|
|
4047
|
+
});
|
|
4048
|
+
const allIds = /* @__PURE__ */ new Set([...rankByIdSemantic.keys(), ...rankByIdKeyword.keys()]);
|
|
4049
|
+
const fused = [];
|
|
4050
|
+
for (const id of allIds) {
|
|
4051
|
+
const semanticRank = rankByIdSemantic.get(id);
|
|
4052
|
+
const keywordRank = rankByIdKeyword.get(id);
|
|
4053
|
+
const semanticScore = semanticRank ? 1 / (rrfK + semanticRank) : 0;
|
|
4054
|
+
const keywordScore = keywordRank ? 1 / (rrfK + keywordRank) : 0;
|
|
4055
|
+
const metadata = metadataById.get(id);
|
|
4056
|
+
if (!metadata) continue;
|
|
4057
|
+
fused.push({
|
|
4058
|
+
id,
|
|
4059
|
+
score: maxPossibleRaw > 0 ? (semanticScore + keywordScore) / maxPossibleRaw : 0,
|
|
4060
|
+
metadata
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
fused.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4064
|
+
return fused.slice(0, limit);
|
|
4065
|
+
}
|
|
4066
|
+
function rerankResults(query, candidates, rerankTopN, options) {
|
|
4067
|
+
if (rerankTopN <= 0 || candidates.length <= 1) {
|
|
4068
|
+
return candidates;
|
|
4069
|
+
}
|
|
4070
|
+
const topN = Math.min(rerankTopN, candidates.length);
|
|
4071
|
+
const queryTokens = tokenizeTextForRanking(query);
|
|
4072
|
+
if (queryTokens.size === 0) {
|
|
4073
|
+
return candidates;
|
|
4074
|
+
}
|
|
4075
|
+
const queryTokenList = Array.from(queryTokens);
|
|
4076
|
+
const intent = classifyQueryIntentRaw(query);
|
|
4077
|
+
const docIntent = classifyDocIntent(queryTokenList);
|
|
4078
|
+
const preferSourcePaths = options?.prioritizeSourcePaths ?? intent === "source";
|
|
4079
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4080
|
+
const head = candidates.slice(0, topN).map((candidate, idx) => {
|
|
4081
|
+
const pathTokens = splitPathTokens(candidate.metadata.filePath);
|
|
4082
|
+
const nameTokens = splitNameTokens(candidate.metadata.name ?? "");
|
|
4083
|
+
const chunkTypeTokens = tokenizeTextForRanking(candidate.metadata.chunkType);
|
|
4084
|
+
let exactOrPrefixNameHits = 0;
|
|
4085
|
+
let pathOverlap = 0;
|
|
4086
|
+
let chunkTypeHits = 0;
|
|
4087
|
+
for (const token of queryTokenList) {
|
|
4088
|
+
if (nameTokens.has(token)) {
|
|
4089
|
+
exactOrPrefixNameHits += 1;
|
|
4090
|
+
} else {
|
|
4091
|
+
for (const nameToken of nameTokens) {
|
|
4092
|
+
if (nameToken.startsWith(token) || token.startsWith(nameToken)) {
|
|
4093
|
+
exactOrPrefixNameHits += 1;
|
|
4094
|
+
break;
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
if (pathTokens.has(token)) {
|
|
4099
|
+
pathOverlap += 1;
|
|
4100
|
+
}
|
|
4101
|
+
if (chunkTypeTokens.has(token)) {
|
|
4102
|
+
chunkTypeHits += 1;
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
const likelyTestOrDoc = isTestOrDocPath(candidate.metadata.filePath);
|
|
4106
|
+
const lowerPath = candidate.metadata.filePath.toLowerCase();
|
|
4107
|
+
const lowerName = (candidate.metadata.name ?? "").toLowerCase();
|
|
4108
|
+
const hasIdentifierMatch = identifierHints.some((id) => lowerPath.includes(id) || lowerName.includes(id));
|
|
4109
|
+
const implementationPathBoost = preferSourcePaths && isLikelyImplementationPath(candidate.metadata.filePath) ? 0.08 : 0;
|
|
4110
|
+
const isReadmePath = candidate.metadata.filePath.toLowerCase().includes("readme");
|
|
4111
|
+
const testDocPenalty = preferSourcePaths && likelyTestOrDoc ? 0.12 : 0;
|
|
4112
|
+
const readmeDocBoost = !preferSourcePaths && isReadmePath ? 0.08 : 0;
|
|
4113
|
+
const identifierBoost = hasIdentifierMatch ? 0.12 : 0;
|
|
4114
|
+
const tokenCoverage = queryTokenList.length > 0 ? (exactOrPrefixNameHits + pathOverlap + chunkTypeHits) / queryTokenList.length : 0;
|
|
4115
|
+
const coverageBoost = Math.min(0.12, tokenCoverage * 0.06);
|
|
4116
|
+
const deterministicBoost = exactOrPrefixNameHits * 0.08 + pathOverlap * 0.03 + chunkTypeHits * 0.02 + coverageBoost + identifierBoost + implementationPathBoost - testDocPenalty + readmeDocBoost + chunkTypeBoost(candidate.metadata.chunkType);
|
|
4117
|
+
return {
|
|
4118
|
+
candidate,
|
|
4119
|
+
boostedScore: candidate.score + deterministicBoost,
|
|
4120
|
+
originalIndex: idx,
|
|
4121
|
+
hasIdentifierMatch,
|
|
4122
|
+
implementationChunk: isImplementationChunkType(candidate.metadata.chunkType),
|
|
4123
|
+
isLikelyImplementationPath: isLikelyImplementationPath(candidate.metadata.filePath),
|
|
4124
|
+
isTestOrDocPath: likelyTestOrDoc,
|
|
4125
|
+
isReadmePath
|
|
4126
|
+
};
|
|
4127
|
+
});
|
|
4128
|
+
head.sort((a, b) => {
|
|
4129
|
+
if (b.boostedScore !== a.boostedScore) return b.boostedScore - a.boostedScore;
|
|
4130
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
4131
|
+
if (a.originalIndex !== b.originalIndex) return a.originalIndex - b.originalIndex;
|
|
4132
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
4133
|
+
});
|
|
4134
|
+
if (preferSourcePaths) {
|
|
4135
|
+
head.sort((a, b) => {
|
|
4136
|
+
const aId = a.hasIdentifierMatch ? 1 : 0;
|
|
4137
|
+
const bId = b.hasIdentifierMatch ? 1 : 0;
|
|
4138
|
+
if (aId !== bId) return bId - aId;
|
|
4139
|
+
const aImpl = a.implementationChunk ? 1 : 0;
|
|
4140
|
+
const bImpl = b.implementationChunk ? 1 : 0;
|
|
4141
|
+
if (aImpl !== bImpl) return bImpl - aImpl;
|
|
4142
|
+
const aImplementationPath = a.isLikelyImplementationPath ? 1 : 0;
|
|
4143
|
+
const bImplementationPath = b.isLikelyImplementationPath ? 1 : 0;
|
|
4144
|
+
if (aImplementationPath !== bImplementationPath) return bImplementationPath - aImplementationPath;
|
|
4145
|
+
const aTestDoc = a.isTestOrDocPath ? 1 : 0;
|
|
4146
|
+
const bTestDoc = b.isTestOrDocPath ? 1 : 0;
|
|
4147
|
+
if (aTestDoc !== bTestDoc) return aTestDoc - bTestDoc;
|
|
4148
|
+
return 0;
|
|
4149
|
+
});
|
|
4150
|
+
} else if (docIntent === "docs") {
|
|
4151
|
+
head.sort((a, b) => {
|
|
4152
|
+
const aReadme = a.isReadmePath ? 1 : 0;
|
|
4153
|
+
const bReadme = b.isReadmePath ? 1 : 0;
|
|
4154
|
+
if (aReadme !== bReadme) return bReadme - aReadme;
|
|
4155
|
+
return 0;
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
const tail = candidates.slice(topN);
|
|
4159
|
+
return [...head.map((entry) => entry.candidate), ...tail];
|
|
4160
|
+
}
|
|
4161
|
+
function rankHybridResults(query, semanticResults, keywordResults, options) {
|
|
4162
|
+
const overfetchLimit = Math.max(options.limit * 4, options.limit);
|
|
4163
|
+
const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
|
|
4164
|
+
const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
|
|
4165
|
+
const rerankPool = fused.slice(0, rerankPoolLimit);
|
|
4166
|
+
const intent = classifyQueryIntentRaw(query);
|
|
4167
|
+
return rerankResults(query, rerankPool, options.rerankTopN, {
|
|
4168
|
+
prioritizeSourcePaths: intent === "source"
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
function rankSemanticOnlyResults(query, semanticResults, options) {
|
|
4172
|
+
const overfetchLimit = Math.max(options.limit * 4, options.limit);
|
|
4173
|
+
const bounded = semanticResults.slice(0, overfetchLimit);
|
|
4174
|
+
return rerankResults(query, bounded, options.rerankTopN, {
|
|
4175
|
+
prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds) {
|
|
4179
|
+
if (combined.length === 0) {
|
|
4180
|
+
return combined;
|
|
4181
|
+
}
|
|
4182
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
4183
|
+
return combined;
|
|
4184
|
+
}
|
|
4185
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4186
|
+
if (identifierHints.length === 0) {
|
|
4187
|
+
return combined;
|
|
4188
|
+
}
|
|
4189
|
+
const combinedById = new Map(combined.map((candidate) => [candidate.id, candidate]));
|
|
4190
|
+
const candidateUnion = /* @__PURE__ */ new Map();
|
|
4191
|
+
for (const candidate of semanticCandidates) {
|
|
4192
|
+
candidateUnion.set(candidate.id, candidate);
|
|
4193
|
+
}
|
|
4194
|
+
for (const candidate of keywordCandidates) {
|
|
4195
|
+
if (!candidateUnion.has(candidate.id)) {
|
|
4196
|
+
candidateUnion.set(candidate.id, candidate);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
if (database) {
|
|
4200
|
+
for (const identifier of identifierHints) {
|
|
4201
|
+
const symbols = database.getSymbolsByName(identifier);
|
|
4202
|
+
for (const symbol of symbols) {
|
|
4203
|
+
const chunks = database.getChunksByFile(symbol.filePath);
|
|
4204
|
+
for (const chunk of chunks) {
|
|
4205
|
+
if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
|
|
4206
|
+
continue;
|
|
4207
|
+
}
|
|
4208
|
+
const chunkType = chunk.nodeType ?? "other";
|
|
4209
|
+
if (!isImplementationChunkType(chunkType)) {
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
if (!isLikelyImplementationPath(chunk.filePath)) {
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4215
|
+
if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
|
|
4216
|
+
continue;
|
|
4217
|
+
}
|
|
4218
|
+
const existing = combinedById.get(chunk.chunkId) ?? candidateUnion.get(chunk.chunkId);
|
|
4219
|
+
const metadata = existing?.metadata ?? {
|
|
4220
|
+
filePath: chunk.filePath,
|
|
4221
|
+
startLine: chunk.startLine,
|
|
4222
|
+
endLine: chunk.endLine,
|
|
4223
|
+
chunkType,
|
|
4224
|
+
name: chunk.name ?? void 0,
|
|
4225
|
+
language: chunk.language,
|
|
4226
|
+
hash: chunk.contentHash
|
|
4227
|
+
};
|
|
4228
|
+
const baselineScore = existing?.score ?? 0.5;
|
|
4229
|
+
candidateUnion.set(chunk.chunkId, {
|
|
4230
|
+
id: chunk.chunkId,
|
|
4231
|
+
score: Math.min(1, baselineScore + 0.5),
|
|
4232
|
+
metadata
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
const promoted = [];
|
|
4239
|
+
for (const candidate of candidateUnion.values()) {
|
|
4240
|
+
const filePathLower = candidate.metadata.filePath.toLowerCase();
|
|
4241
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
4242
|
+
const exactIdentifierMatch = identifierHints.some((hint) => nameLower === hint);
|
|
4243
|
+
const hasIdentifierMatch = exactIdentifierMatch || identifierHints.some(
|
|
4244
|
+
(hint) => nameLower.includes(hint) || filePathLower.includes(hint)
|
|
4245
|
+
);
|
|
4246
|
+
if (!hasIdentifierMatch) {
|
|
4247
|
+
continue;
|
|
4248
|
+
}
|
|
4249
|
+
if (!isImplementationChunkType(candidate.metadata.chunkType)) {
|
|
4250
|
+
continue;
|
|
4251
|
+
}
|
|
4252
|
+
if (!isLikelyImplementationPath(candidate.metadata.filePath)) {
|
|
4253
|
+
continue;
|
|
4254
|
+
}
|
|
4255
|
+
const existing = combinedById.get(candidate.id) ?? candidate;
|
|
4256
|
+
const rescueBoost = exactIdentifierMatch ? 0.45 : 0.25;
|
|
4257
|
+
const boostedScore = Math.min(1, Math.max(existing.score, candidate.score) + rescueBoost);
|
|
4258
|
+
promoted.push({
|
|
4259
|
+
id: existing.id,
|
|
4260
|
+
score: boostedScore,
|
|
4261
|
+
metadata: existing.metadata
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
if (promoted.length === 0) {
|
|
4265
|
+
return combined;
|
|
4266
|
+
}
|
|
4267
|
+
promoted.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4268
|
+
const promotedIds = new Set(promoted.map((candidate) => candidate.id));
|
|
4269
|
+
const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
|
|
4270
|
+
return [...promoted, ...remainder];
|
|
4271
|
+
}
|
|
4272
|
+
function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates) {
|
|
4273
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
4274
|
+
return [];
|
|
4275
|
+
}
|
|
4276
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4277
|
+
const codeTermHints = extractCodeTermHints(query);
|
|
4278
|
+
if (identifierHints.length === 0 && codeTermHints.length === 0) {
|
|
4279
|
+
return [];
|
|
4280
|
+
}
|
|
4281
|
+
const symbolCandidates = /* @__PURE__ */ new Map();
|
|
4282
|
+
const filePathHint = extractFilePathHint(query);
|
|
4283
|
+
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
4284
|
+
const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
|
|
4285
|
+
if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
4288
|
+
const chunkType = chunk.nodeType ?? "other";
|
|
4289
|
+
if (!isImplementationChunkType(chunkType)) {
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
if (!isLikelyImplementationPath(chunk.filePath)) {
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
4295
|
+
const nameLower = (chunk.name ?? "").toLowerCase();
|
|
4296
|
+
const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
|
|
4297
|
+
const base = baseScore ?? (exactName ? 0.99 : 0.88);
|
|
4298
|
+
const existing = symbolCandidates.get(chunk.chunkId);
|
|
4299
|
+
if (!existing || base > existing.score) {
|
|
4300
|
+
symbolCandidates.set(chunk.chunkId, {
|
|
4301
|
+
id: chunk.chunkId,
|
|
4302
|
+
score: base,
|
|
4303
|
+
metadata: {
|
|
4304
|
+
filePath: chunk.filePath,
|
|
4305
|
+
startLine: chunk.startLine,
|
|
4306
|
+
endLine: chunk.endLine,
|
|
4307
|
+
chunkType,
|
|
4308
|
+
name: chunk.name ?? void 0,
|
|
4309
|
+
language: chunk.language,
|
|
4310
|
+
hash: chunk.contentHash
|
|
4311
|
+
}
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4314
|
+
};
|
|
4315
|
+
const normalizedHints = identifierHints.flatMap((hint) => [
|
|
4316
|
+
hint,
|
|
4317
|
+
hint.replace(/_/g, ""),
|
|
4318
|
+
hint.replace(/_/g, "-")
|
|
4319
|
+
]).filter((hint, idx, arr) => hint.length >= 3 && arr.indexOf(hint) === idx).slice(0, 6);
|
|
4320
|
+
for (const identifier of normalizedHints) {
|
|
4321
|
+
const symbols = [
|
|
4322
|
+
...database.getSymbolsByName(identifier),
|
|
4323
|
+
...database.getSymbolsByNameCi(identifier)
|
|
4324
|
+
];
|
|
4325
|
+
const chunksByName = [
|
|
4326
|
+
...database.getChunksByName(identifier),
|
|
4327
|
+
...database.getChunksByNameCi(identifier)
|
|
4328
|
+
];
|
|
4329
|
+
const normalizedIdentifier = identifier.replace(/_/g, "");
|
|
4330
|
+
const dedupSymbols = /* @__PURE__ */ new Map();
|
|
4331
|
+
for (const symbol of symbols) {
|
|
4332
|
+
dedupSymbols.set(symbol.id, symbol);
|
|
4333
|
+
}
|
|
4334
|
+
for (const symbol of dedupSymbols.values()) {
|
|
4335
|
+
const chunks = database.getChunksByFile(symbol.filePath);
|
|
4336
|
+
for (const chunk of chunks) {
|
|
4337
|
+
if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
|
|
4338
|
+
continue;
|
|
4339
|
+
}
|
|
4340
|
+
upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
const dedupChunksByName = /* @__PURE__ */ new Map();
|
|
4344
|
+
for (const chunk of chunksByName) {
|
|
4345
|
+
dedupChunksByName.set(chunk.chunkId, chunk);
|
|
4346
|
+
}
|
|
4347
|
+
for (const chunk of dedupChunksByName.values()) {
|
|
4348
|
+
upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
if (filePathHint && primaryHint) {
|
|
4352
|
+
const primaryChunks = [
|
|
4353
|
+
...database.getChunksByName(primaryHint),
|
|
4354
|
+
...database.getChunksByNameCi(primaryHint)
|
|
4355
|
+
];
|
|
4356
|
+
const dedupPrimaryChunks = /* @__PURE__ */ new Map();
|
|
4357
|
+
for (const chunk of primaryChunks) {
|
|
4358
|
+
dedupPrimaryChunks.set(chunk.chunkId, chunk);
|
|
4359
|
+
}
|
|
4360
|
+
for (const chunk of dedupPrimaryChunks.values()) {
|
|
4361
|
+
if (!pathMatchesHint(chunk.filePath, filePathHint)) {
|
|
4362
|
+
continue;
|
|
4363
|
+
}
|
|
4364
|
+
const normalizedPrimary = primaryHint.replace(/_/g, "");
|
|
4365
|
+
upsertChunkCandidate(chunk, primaryHint, normalizedPrimary, 1);
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
const ranked = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4369
|
+
if (ranked.length === 0) {
|
|
4370
|
+
const implementationFallback = fallbackCandidates.filter(
|
|
4371
|
+
(candidate) => isImplementationChunkType(candidate.metadata.chunkType) && isLikelyImplementationPath(candidate.metadata.filePath)
|
|
4372
|
+
);
|
|
4373
|
+
for (const candidate of implementationFallback) {
|
|
4374
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
4375
|
+
const pathLower = candidate.metadata.filePath.toLowerCase();
|
|
4376
|
+
const exactHintMatch = normalizedHints.some((hint) => nameLower === hint || nameLower.replace(/_/g, "") === hint.replace(/_/g, ""));
|
|
4377
|
+
const tokenizedName = tokenizeTextForRanking(nameLower);
|
|
4378
|
+
const tokenHits = codeTermHints.filter((term) => tokenizedName.has(term) || pathLower.includes(term)).length;
|
|
4379
|
+
if (!exactHintMatch && tokenHits === 0) {
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
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));
|
|
4383
|
+
symbolCandidates.set(candidate.id, {
|
|
4384
|
+
id: candidate.id,
|
|
4385
|
+
score: laneScore,
|
|
4386
|
+
metadata: candidate.metadata
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
if (symbolCandidates.size === 0) {
|
|
4390
|
+
const queryTokenSet = tokenizeTextForRanking(query);
|
|
4391
|
+
const rankedFallback = implementationFallback.map((candidate) => {
|
|
4392
|
+
const nameTokens = tokenizeTextForRanking(candidate.metadata.name ?? "");
|
|
4393
|
+
const pathTokens = splitPathTokens(candidate.metadata.filePath);
|
|
4394
|
+
let overlap = 0;
|
|
4395
|
+
for (const token of queryTokenSet) {
|
|
4396
|
+
if (nameTokens.has(token) || pathTokens.has(token)) {
|
|
4397
|
+
overlap += 1;
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
const overlapScore = queryTokenSet.size > 0 ? overlap / queryTokenSet.size : 0;
|
|
4401
|
+
return {
|
|
4402
|
+
candidate,
|
|
4403
|
+
overlapScore
|
|
4404
|
+
};
|
|
4405
|
+
}).filter((entry) => entry.overlapScore > 0).sort((a, b) => b.overlapScore - a.overlapScore || b.candidate.score - a.candidate.score).slice(0, Math.max(limit, 3));
|
|
4406
|
+
for (const entry of rankedFallback) {
|
|
4407
|
+
symbolCandidates.set(entry.candidate.id, {
|
|
4408
|
+
id: entry.candidate.id,
|
|
4409
|
+
score: Math.min(0.94, Math.max(entry.candidate.score, 0.8 + entry.overlapScore * 0.1)),
|
|
4410
|
+
metadata: entry.candidate.metadata
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4416
|
+
return withFallback.slice(0, Math.max(limit * 2, limit));
|
|
4417
|
+
}
|
|
4418
|
+
function buildIdentifierDefinitionLane(query, candidates, limit) {
|
|
4419
|
+
if (classifyQueryIntentRaw(query) !== "source") {
|
|
4420
|
+
return [];
|
|
4421
|
+
}
|
|
4422
|
+
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
4423
|
+
if (!primaryHint) {
|
|
4424
|
+
return [];
|
|
4425
|
+
}
|
|
4426
|
+
const hints = [primaryHint, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].slice(0, 8);
|
|
4427
|
+
const scored = candidates.filter(
|
|
4428
|
+
(candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
|
|
4429
|
+
).map((candidate) => {
|
|
4430
|
+
const matchScore = scoreIdentifierMatch(candidate.metadata.name, candidate.metadata.filePath, hints);
|
|
4431
|
+
return {
|
|
4432
|
+
candidate,
|
|
4433
|
+
matchScore
|
|
4434
|
+
};
|
|
4435
|
+
}).filter((entry) => entry.matchScore > 0).sort((a, b) => {
|
|
4436
|
+
if (b.matchScore !== a.matchScore) return b.matchScore - a.matchScore;
|
|
4437
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
4438
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
4439
|
+
}).slice(0, Math.max(limit * 2, 10));
|
|
4440
|
+
return scored.map((entry) => ({
|
|
4441
|
+
id: entry.candidate.id,
|
|
4442
|
+
score: Math.min(1, 0.9 + entry.matchScore * 0.09),
|
|
4443
|
+
metadata: entry.candidate.metadata
|
|
4444
|
+
}));
|
|
4445
|
+
}
|
|
4446
|
+
function mergeTieredResults(symbolLane, hybridLane, limit) {
|
|
4447
|
+
if (symbolLane.length === 0) {
|
|
4448
|
+
return hybridLane.slice(0, limit);
|
|
4449
|
+
}
|
|
4450
|
+
const out = [];
|
|
4451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4452
|
+
for (const candidate of symbolLane) {
|
|
4453
|
+
if (seen.has(candidate.id)) continue;
|
|
4454
|
+
out.push(candidate);
|
|
4455
|
+
seen.add(candidate.id);
|
|
4456
|
+
if (out.length >= limit) return out;
|
|
4457
|
+
}
|
|
4458
|
+
for (const candidate of hybridLane) {
|
|
4459
|
+
if (seen.has(candidate.id)) continue;
|
|
4460
|
+
out.push(candidate);
|
|
4461
|
+
seen.add(candidate.id);
|
|
4462
|
+
if (out.length >= limit) return out;
|
|
4463
|
+
}
|
|
4464
|
+
return out;
|
|
4465
|
+
}
|
|
4466
|
+
function unionCandidates(semanticCandidates, keywordCandidates) {
|
|
4467
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4468
|
+
for (const candidate of semanticCandidates) {
|
|
4469
|
+
byId.set(candidate.id, candidate);
|
|
4470
|
+
}
|
|
4471
|
+
for (const candidate of keywordCandidates) {
|
|
4472
|
+
const existing = byId.get(candidate.id);
|
|
4473
|
+
if (!existing || candidate.score > existing.score) {
|
|
4474
|
+
byId.set(candidate.id, candidate);
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
return Array.from(byId.values());
|
|
4478
|
+
}
|
|
3346
4479
|
var Indexer = class {
|
|
3347
4480
|
config;
|
|
3348
4481
|
projectRoot;
|
|
@@ -3468,19 +4601,33 @@ var Indexer = class {
|
|
|
3468
4601
|
return { concurrency: 5, intervalMs: 200, minRetryMs: 1e3, maxRetryMs: 3e4 };
|
|
3469
4602
|
case "ollama":
|
|
3470
4603
|
return { concurrency: 5, intervalMs: 0, minRetryMs: 500, maxRetryMs: 5e3 };
|
|
4604
|
+
case "custom": {
|
|
4605
|
+
const customConfig = this.config.customProvider;
|
|
4606
|
+
return {
|
|
4607
|
+
concurrency: customConfig?.concurrency ?? 3,
|
|
4608
|
+
intervalMs: customConfig?.requestIntervalMs ?? 1e3,
|
|
4609
|
+
minRetryMs: 1e3,
|
|
4610
|
+
maxRetryMs: 3e4
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
3471
4613
|
default:
|
|
3472
4614
|
return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
|
|
3473
4615
|
}
|
|
3474
4616
|
}
|
|
3475
4617
|
async initialize() {
|
|
3476
|
-
if (this.config.embeddingProvider === "
|
|
4618
|
+
if (this.config.embeddingProvider === "custom") {
|
|
4619
|
+
if (!this.config.customProvider) {
|
|
4620
|
+
throw new Error("embeddingProvider is 'custom' but customProvider config is missing.");
|
|
4621
|
+
}
|
|
4622
|
+
this.configuredProviderInfo = createCustomProviderInfo(this.config.customProvider);
|
|
4623
|
+
} else if (this.config.embeddingProvider === "auto") {
|
|
3477
4624
|
this.configuredProviderInfo = await tryDetectProvider();
|
|
3478
4625
|
} else {
|
|
3479
4626
|
this.configuredProviderInfo = await detectEmbeddingProvider(this.config.embeddingProvider, this.config.embeddingModel);
|
|
3480
4627
|
}
|
|
3481
4628
|
if (!this.configuredProviderInfo) {
|
|
3482
4629
|
throw new Error(
|
|
3483
|
-
"No embedding provider available. Configure GitHub, OpenAI, Google, or
|
|
4630
|
+
"No embedding provider available. Configure GitHub Copilot, OpenAI, Google, Ollama, or a custom OpenAI-compatible endpoint."
|
|
3484
4631
|
);
|
|
3485
4632
|
}
|
|
3486
4633
|
this.logger.info("Initializing indexer", {
|
|
@@ -3490,9 +4637,6 @@ var Indexer = class {
|
|
|
3490
4637
|
});
|
|
3491
4638
|
this.provider = createEmbeddingProvider(this.configuredProviderInfo);
|
|
3492
4639
|
await import_fs4.promises.mkdir(this.indexPath, { recursive: true });
|
|
3493
|
-
if (this.checkForInterruptedIndexing()) {
|
|
3494
|
-
await this.recoverFromInterruptedIndexing();
|
|
3495
|
-
}
|
|
3496
4640
|
const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
|
|
3497
4641
|
const storePath = path5.join(this.indexPath, "vectors");
|
|
3498
4642
|
this.store = new VectorStore(storePath, dimensions);
|
|
@@ -3513,6 +4657,9 @@ var Indexer = class {
|
|
|
3513
4657
|
const dbPath = path5.join(this.indexPath, "codebase.db");
|
|
3514
4658
|
const dbIsNew = !(0, import_fs4.existsSync)(dbPath);
|
|
3515
4659
|
this.database = new Database(dbPath);
|
|
4660
|
+
if (this.checkForInterruptedIndexing()) {
|
|
4661
|
+
await this.recoverFromInterruptedIndexing();
|
|
4662
|
+
}
|
|
3516
4663
|
if (dbIsNew && this.store.count() > 0) {
|
|
3517
4664
|
this.migrateFromLegacyIndex();
|
|
3518
4665
|
}
|
|
@@ -3831,6 +4978,79 @@ var Indexer = class {
|
|
|
3831
4978
|
if (chunkDataBatch.length > 0) {
|
|
3832
4979
|
database.upsertChunksBatch(chunkDataBatch);
|
|
3833
4980
|
}
|
|
4981
|
+
const allSymbolIds = /* @__PURE__ */ new Set();
|
|
4982
|
+
const symbolsByFile = /* @__PURE__ */ new Map();
|
|
4983
|
+
for (let i = 0; i < parsedFiles.length; i++) {
|
|
4984
|
+
const parsed = parsedFiles[i];
|
|
4985
|
+
const changedFile = changedFiles[i];
|
|
4986
|
+
database.deleteCallEdgesByFile(parsed.path);
|
|
4987
|
+
database.deleteSymbolsByFile(parsed.path);
|
|
4988
|
+
const fileSymbols = [];
|
|
4989
|
+
for (const chunk of parsed.chunks) {
|
|
4990
|
+
if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
|
|
4991
|
+
const symbolId = `sym_${hashContent(parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine).slice(0, 16)}`;
|
|
4992
|
+
const symbol = {
|
|
4993
|
+
id: symbolId,
|
|
4994
|
+
filePath: parsed.path,
|
|
4995
|
+
name: chunk.name,
|
|
4996
|
+
kind: chunk.chunkType,
|
|
4997
|
+
startLine: chunk.startLine,
|
|
4998
|
+
startCol: 0,
|
|
4999
|
+
endLine: chunk.endLine,
|
|
5000
|
+
endCol: 0,
|
|
5001
|
+
language: chunk.language
|
|
5002
|
+
};
|
|
5003
|
+
fileSymbols.push(symbol);
|
|
5004
|
+
allSymbolIds.add(symbolId);
|
|
5005
|
+
}
|
|
5006
|
+
const symbolsByName = /* @__PURE__ */ new Map();
|
|
5007
|
+
for (const symbol of fileSymbols) {
|
|
5008
|
+
const existing = symbolsByName.get(symbol.name) ?? [];
|
|
5009
|
+
existing.push(symbol);
|
|
5010
|
+
symbolsByName.set(symbol.name, existing);
|
|
5011
|
+
}
|
|
5012
|
+
if (fileSymbols.length > 0) {
|
|
5013
|
+
database.upsertSymbolsBatch(fileSymbols);
|
|
5014
|
+
symbolsByFile.set(parsed.path, fileSymbols);
|
|
5015
|
+
}
|
|
5016
|
+
const fileLanguage = parsed.chunks[0]?.language;
|
|
5017
|
+
if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) continue;
|
|
5018
|
+
const callSites = extractCalls(changedFile.content, fileLanguage);
|
|
5019
|
+
if (callSites.length === 0) continue;
|
|
5020
|
+
const edges = [];
|
|
5021
|
+
for (const site of callSites) {
|
|
5022
|
+
const enclosingSymbol = fileSymbols.find(
|
|
5023
|
+
(sym) => site.line >= sym.startLine && site.line <= sym.endLine
|
|
5024
|
+
);
|
|
5025
|
+
if (!enclosingSymbol) continue;
|
|
5026
|
+
const edgeId = `edge_${hashContent(enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column).slice(0, 16)}`;
|
|
5027
|
+
edges.push({
|
|
5028
|
+
id: edgeId,
|
|
5029
|
+
fromSymbolId: enclosingSymbol.id,
|
|
5030
|
+
targetName: site.calleeName,
|
|
5031
|
+
toSymbolId: void 0,
|
|
5032
|
+
callType: site.callType,
|
|
5033
|
+
line: site.line,
|
|
5034
|
+
col: site.column,
|
|
5035
|
+
isResolved: false
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
if (edges.length > 0) {
|
|
5039
|
+
database.upsertCallEdgesBatch(edges);
|
|
5040
|
+
for (const edge of edges) {
|
|
5041
|
+
const candidates = symbolsByName.get(edge.targetName);
|
|
5042
|
+
if (candidates && candidates.length === 1) {
|
|
5043
|
+
database.resolveCallEdge(edge.id, candidates[0].id);
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
for (const filePath of unchangedFilePaths) {
|
|
5049
|
+
const existingSymbols = database.getSymbolsByFile(filePath);
|
|
5050
|
+
for (const sym of existingSymbols) {
|
|
5051
|
+
allSymbolIds.add(sym.id);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
3834
5054
|
let removedCount = 0;
|
|
3835
5055
|
for (const [chunkId] of existingChunks) {
|
|
3836
5056
|
if (!currentChunkIds.has(chunkId)) {
|
|
@@ -3852,6 +5072,8 @@ var Indexer = class {
|
|
|
3852
5072
|
if (pendingChunks.length === 0 && removedCount === 0) {
|
|
3853
5073
|
database.clearBranch(this.currentBranch);
|
|
3854
5074
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5075
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5076
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
3855
5077
|
this.fileHashCache = currentFileHashes;
|
|
3856
5078
|
this.saveFileHashCache();
|
|
3857
5079
|
stats.durationMs = Date.now() - startTime;
|
|
@@ -3868,6 +5090,8 @@ var Indexer = class {
|
|
|
3868
5090
|
if (pendingChunks.length === 0) {
|
|
3869
5091
|
database.clearBranch(this.currentBranch);
|
|
3870
5092
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5093
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5094
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
3871
5095
|
store.save();
|
|
3872
5096
|
invertedIndex.save();
|
|
3873
5097
|
this.fileHashCache = currentFileHashes;
|
|
@@ -3933,6 +5157,7 @@ var Indexer = class {
|
|
|
3933
5157
|
minTimeout: Math.max(this.config.indexing.retryDelayMs, providerRateLimits.minRetryMs),
|
|
3934
5158
|
maxTimeout: providerRateLimits.maxRetryMs,
|
|
3935
5159
|
factor: 2,
|
|
5160
|
+
shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
|
|
3936
5161
|
onFailedAttempt: (error) => {
|
|
3937
5162
|
const message = getErrorMessage(error);
|
|
3938
5163
|
if (isRateLimitError(error)) {
|
|
@@ -4007,6 +5232,8 @@ var Indexer = class {
|
|
|
4007
5232
|
});
|
|
4008
5233
|
database.clearBranch(this.currentBranch);
|
|
4009
5234
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5235
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5236
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
4010
5237
|
store.save();
|
|
4011
5238
|
invertedIndex.save();
|
|
4012
5239
|
this.fileHashCache = currentFileHashes;
|
|
@@ -4117,15 +5344,22 @@ var Indexer = class {
|
|
|
4117
5344
|
}
|
|
4118
5345
|
const maxResults = limit ?? this.config.search.maxResults;
|
|
4119
5346
|
const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
|
|
5347
|
+
const fusionStrategy = this.config.search.fusionStrategy;
|
|
5348
|
+
const rrfK = this.config.search.rrfK;
|
|
5349
|
+
const rerankTopN = this.config.search.rerankTopN;
|
|
4120
5350
|
const filterByBranch = options?.filterByBranch ?? true;
|
|
4121
5351
|
this.logger.search("debug", "Starting search", {
|
|
4122
5352
|
query,
|
|
4123
5353
|
maxResults,
|
|
4124
5354
|
hybridWeight,
|
|
5355
|
+
fusionStrategy,
|
|
5356
|
+
rrfK,
|
|
5357
|
+
rerankTopN,
|
|
4125
5358
|
filterByBranch
|
|
4126
5359
|
});
|
|
4127
5360
|
const embeddingStartTime = import_perf_hooks.performance.now();
|
|
4128
|
-
const
|
|
5361
|
+
const embeddingQuery = stripFilePathHint(query);
|
|
5362
|
+
const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
|
|
4129
5363
|
const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
|
|
4130
5364
|
const vectorStartTime = import_perf_hooks.performance.now();
|
|
4131
5365
|
const semanticResults = store.search(embedding, maxResults * 4);
|
|
@@ -4133,16 +5367,74 @@ var Indexer = class {
|
|
|
4133
5367
|
const keywordStartTime = import_perf_hooks.performance.now();
|
|
4134
5368
|
const keywordResults = await this.keywordSearch(query, maxResults * 4);
|
|
4135
5369
|
const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
|
|
4136
|
-
const fusionStartTime = import_perf_hooks.performance.now();
|
|
4137
|
-
const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
|
|
4138
|
-
const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
|
|
4139
5370
|
let branchChunkIds = null;
|
|
4140
5371
|
if (filterByBranch && this.currentBranch !== "default") {
|
|
4141
5372
|
branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
|
|
4142
5373
|
}
|
|
4143
|
-
const
|
|
5374
|
+
const prefilterStartTime = import_perf_hooks.performance.now();
|
|
5375
|
+
const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
|
|
5376
|
+
const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
|
|
5377
|
+
const prefilteredKeyword = shouldPrefilterByBranch && branchChunkIds ? keywordResults.filter((r) => branchChunkIds.has(r.id)) : keywordResults;
|
|
5378
|
+
const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
|
|
5379
|
+
const keywordCandidates = shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0 ? keywordResults : prefilteredKeyword;
|
|
5380
|
+
const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
|
|
5381
|
+
if (branchChunkIds && branchChunkIds.size === 0) {
|
|
5382
|
+
this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
|
|
5383
|
+
branch: this.currentBranch
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5386
|
+
if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
|
|
5387
|
+
this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
|
|
5388
|
+
branch: this.currentBranch
|
|
5389
|
+
});
|
|
5390
|
+
}
|
|
5391
|
+
if (shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0) {
|
|
5392
|
+
this.logger.search("warn", "Branch prefilter produced no keyword overlap, using unfiltered keyword candidates", {
|
|
5393
|
+
branch: this.currentBranch
|
|
5394
|
+
});
|
|
5395
|
+
}
|
|
5396
|
+
const fusionStartTime = import_perf_hooks.performance.now();
|
|
5397
|
+
const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
|
|
5398
|
+
fusionStrategy,
|
|
5399
|
+
rrfK,
|
|
5400
|
+
rerankTopN,
|
|
5401
|
+
limit: maxResults,
|
|
5402
|
+
hybridWeight
|
|
5403
|
+
});
|
|
5404
|
+
const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
|
|
5405
|
+
const rescued = promoteIdentifierMatches(
|
|
5406
|
+
query,
|
|
5407
|
+
combined,
|
|
5408
|
+
semanticCandidates,
|
|
5409
|
+
keywordCandidates,
|
|
5410
|
+
database,
|
|
5411
|
+
branchChunkIds
|
|
5412
|
+
);
|
|
5413
|
+
const union = unionCandidates(semanticCandidates, keywordCandidates);
|
|
5414
|
+
const deterministicIdentifierLane = buildDeterministicIdentifierPass(
|
|
5415
|
+
query,
|
|
5416
|
+
union,
|
|
5417
|
+
maxResults
|
|
5418
|
+
);
|
|
5419
|
+
const identifierLane = buildIdentifierDefinitionLane(
|
|
5420
|
+
query,
|
|
5421
|
+
union,
|
|
5422
|
+
maxResults
|
|
5423
|
+
);
|
|
5424
|
+
const symbolLane = buildSymbolDefinitionLane(
|
|
5425
|
+
query,
|
|
5426
|
+
database,
|
|
5427
|
+
branchChunkIds,
|
|
5428
|
+
maxResults,
|
|
5429
|
+
union
|
|
5430
|
+
);
|
|
5431
|
+
const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
|
|
5432
|
+
const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
|
|
5433
|
+
const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
|
|
5434
|
+
const sourceIntent = classifyQueryIntentRaw(query) === "source";
|
|
5435
|
+
const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
|
|
5436
|
+
const baseFiltered = tiered.filter((r) => {
|
|
4144
5437
|
if (r.score < this.config.search.minScore) return false;
|
|
4145
|
-
if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
|
|
4146
5438
|
if (options?.fileType) {
|
|
4147
5439
|
const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
|
|
4148
5440
|
if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
|
|
@@ -4155,7 +5447,11 @@ var Indexer = class {
|
|
|
4155
5447
|
if (r.metadata.chunkType !== options.chunkType) return false;
|
|
4156
5448
|
}
|
|
4157
5449
|
return true;
|
|
4158
|
-
})
|
|
5450
|
+
});
|
|
5451
|
+
const implementationOnly = baseFiltered.filter(
|
|
5452
|
+
(r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
|
|
5453
|
+
);
|
|
5454
|
+
const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
|
|
4159
5455
|
const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
|
|
4160
5456
|
this.logger.recordSearch(totalSearchMs, {
|
|
4161
5457
|
embeddingMs,
|
|
@@ -4170,6 +5466,7 @@ var Indexer = class {
|
|
|
4170
5466
|
embeddingMs: Math.round(embeddingMs * 100) / 100,
|
|
4171
5467
|
vectorMs: Math.round(vectorMs * 100) / 100,
|
|
4172
5468
|
keywordMs: Math.round(keywordMs * 100) / 100,
|
|
5469
|
+
prefilterMs: Math.round(prefilterMs * 100) / 100,
|
|
4173
5470
|
fusionMs: Math.round(fusionMs * 100) / 100
|
|
4174
5471
|
});
|
|
4175
5472
|
const metadataOnly = options?.metadataOnly ?? false;
|
|
@@ -4223,34 +5520,6 @@ var Indexer = class {
|
|
|
4223
5520
|
results.sort((a, b) => b.score - a.score);
|
|
4224
5521
|
return results.slice(0, limit);
|
|
4225
5522
|
}
|
|
4226
|
-
fuseResults(semanticResults, keywordResults, keywordWeight, limit) {
|
|
4227
|
-
const semanticWeight = 1 - keywordWeight;
|
|
4228
|
-
const fusedScores = /* @__PURE__ */ new Map();
|
|
4229
|
-
for (const r of semanticResults) {
|
|
4230
|
-
fusedScores.set(r.id, {
|
|
4231
|
-
score: r.score * semanticWeight,
|
|
4232
|
-
metadata: r.metadata
|
|
4233
|
-
});
|
|
4234
|
-
}
|
|
4235
|
-
for (const r of keywordResults) {
|
|
4236
|
-
const existing = fusedScores.get(r.id);
|
|
4237
|
-
if (existing) {
|
|
4238
|
-
existing.score += r.score * keywordWeight;
|
|
4239
|
-
} else {
|
|
4240
|
-
fusedScores.set(r.id, {
|
|
4241
|
-
score: r.score * keywordWeight,
|
|
4242
|
-
metadata: r.metadata
|
|
4243
|
-
});
|
|
4244
|
-
}
|
|
4245
|
-
}
|
|
4246
|
-
const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
|
|
4247
|
-
id,
|
|
4248
|
-
score: data.score,
|
|
4249
|
-
metadata: data.metadata
|
|
4250
|
-
}));
|
|
4251
|
-
results.sort((a, b) => b.score - a.score);
|
|
4252
|
-
return results.slice(0, limit);
|
|
4253
|
-
}
|
|
4254
5523
|
async getStatus() {
|
|
4255
5524
|
const { store, configuredProviderInfo } = await this.ensureInitialized();
|
|
4256
5525
|
return {
|
|
@@ -4273,6 +5542,7 @@ var Indexer = class {
|
|
|
4273
5542
|
this.fileHashCache.clear();
|
|
4274
5543
|
this.saveFileHashCache();
|
|
4275
5544
|
database.clearBranch(this.currentBranch);
|
|
5545
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
4276
5546
|
database.deleteMetadata("index.version");
|
|
4277
5547
|
database.deleteMetadata("index.embeddingProvider");
|
|
4278
5548
|
database.deleteMetadata("index.embeddingModel");
|
|
@@ -4301,6 +5571,8 @@ var Indexer = class {
|
|
|
4301
5571
|
removedCount++;
|
|
4302
5572
|
}
|
|
4303
5573
|
database.deleteChunksByFile(filePath);
|
|
5574
|
+
database.deleteCallEdgesByFile(filePath);
|
|
5575
|
+
database.deleteSymbolsByFile(filePath);
|
|
4304
5576
|
removedFilePaths.push(filePath);
|
|
4305
5577
|
}
|
|
4306
5578
|
}
|
|
@@ -4310,6 +5582,8 @@ var Indexer = class {
|
|
|
4310
5582
|
}
|
|
4311
5583
|
const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
|
|
4312
5584
|
const gcOrphanChunks = database.gcOrphanChunks();
|
|
5585
|
+
const gcOrphanSymbols = database.gcOrphanSymbols();
|
|
5586
|
+
const gcOrphanCallEdges = database.gcOrphanCallEdges();
|
|
4313
5587
|
this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
|
|
4314
5588
|
this.logger.gc("info", "Health check complete", {
|
|
4315
5589
|
removedStale: removedCount,
|
|
@@ -4317,7 +5591,7 @@ var Indexer = class {
|
|
|
4317
5591
|
orphanChunks: gcOrphanChunks,
|
|
4318
5592
|
removedFiles: removedFilePaths.length
|
|
4319
5593
|
});
|
|
4320
|
-
return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
|
|
5594
|
+
return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
|
|
4321
5595
|
}
|
|
4322
5596
|
async retryFailedBatches() {
|
|
4323
5597
|
const { store, provider, invertedIndex } = await this.ensureInitialized();
|
|
@@ -4423,9 +5697,29 @@ var Indexer = class {
|
|
|
4423
5697
|
if (filterByBranch && this.currentBranch !== "default") {
|
|
4424
5698
|
branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
|
|
4425
5699
|
}
|
|
4426
|
-
const
|
|
5700
|
+
const prefilterStartTime = import_perf_hooks.performance.now();
|
|
5701
|
+
const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
|
|
5702
|
+
const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
|
|
5703
|
+
const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
|
|
5704
|
+
const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
|
|
5705
|
+
if (branchChunkIds && branchChunkIds.size === 0) {
|
|
5706
|
+
this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
|
|
5707
|
+
branch: this.currentBranch
|
|
5708
|
+
});
|
|
5709
|
+
}
|
|
5710
|
+
if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
|
|
5711
|
+
this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
|
|
5712
|
+
branch: this.currentBranch
|
|
5713
|
+
});
|
|
5714
|
+
}
|
|
5715
|
+
const rerankTopN = this.config.search.rerankTopN;
|
|
5716
|
+
const ranked = rankSemanticOnlyResults(code, semanticCandidates, {
|
|
5717
|
+
rerankTopN,
|
|
5718
|
+
limit,
|
|
5719
|
+
prioritizeSourcePaths: false
|
|
5720
|
+
});
|
|
5721
|
+
const filtered = ranked.filter((r) => {
|
|
4427
5722
|
if (r.score < this.config.search.minScore) return false;
|
|
4428
|
-
if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
|
|
4429
5723
|
if (options?.excludeFile) {
|
|
4430
5724
|
if (r.metadata.filePath === options.excludeFile) return false;
|
|
4431
5725
|
}
|
|
@@ -4454,7 +5748,8 @@ var Indexer = class {
|
|
|
4454
5748
|
results: filtered.length,
|
|
4455
5749
|
totalMs: Math.round(totalSearchMs * 100) / 100,
|
|
4456
5750
|
embeddingMs: Math.round(embeddingMs * 100) / 100,
|
|
4457
|
-
vectorMs: Math.round(vectorMs * 100) / 100
|
|
5751
|
+
vectorMs: Math.round(vectorMs * 100) / 100,
|
|
5752
|
+
prefilterMs: Math.round(prefilterMs * 100) / 100
|
|
4458
5753
|
});
|
|
4459
5754
|
return Promise.all(
|
|
4460
5755
|
filtered.map(async (r) => {
|
|
@@ -4483,6 +5778,14 @@ var Indexer = class {
|
|
|
4483
5778
|
})
|
|
4484
5779
|
);
|
|
4485
5780
|
}
|
|
5781
|
+
async getCallers(targetName) {
|
|
5782
|
+
const { database } = await this.ensureInitialized();
|
|
5783
|
+
return database.getCallersWithContext(targetName, this.currentBranch);
|
|
5784
|
+
}
|
|
5785
|
+
async getCallees(symbolId) {
|
|
5786
|
+
const { database } = await this.ensureInitialized();
|
|
5787
|
+
return database.getCallees(symbolId, this.currentBranch);
|
|
5788
|
+
}
|
|
4486
5789
|
};
|
|
4487
5790
|
|
|
4488
5791
|
// node_modules/chokidar/index.js
|
|
@@ -6541,7 +7844,7 @@ ${formatted.join("\n")}
|
|
|
6541
7844
|
Use Read tool to examine specific files.`;
|
|
6542
7845
|
}
|
|
6543
7846
|
function formatHealthCheck(result) {
|
|
6544
|
-
if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
|
|
7847
|
+
if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0 && result.gcOrphanSymbols === 0 && result.gcOrphanCallEdges === 0) {
|
|
6545
7848
|
return "Index is healthy. No stale entries found.";
|
|
6546
7849
|
}
|
|
6547
7850
|
const lines = [`Health check complete:`];
|
|
@@ -6554,6 +7857,12 @@ function formatHealthCheck(result) {
|
|
|
6554
7857
|
if (result.gcOrphanChunks > 0) {
|
|
6555
7858
|
lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
|
|
6556
7859
|
}
|
|
7860
|
+
if (result.gcOrphanSymbols > 0) {
|
|
7861
|
+
lines.push(` Garbage collected orphan symbols: ${result.gcOrphanSymbols}`);
|
|
7862
|
+
}
|
|
7863
|
+
if (result.gcOrphanCallEdges > 0) {
|
|
7864
|
+
lines.push(` Garbage collected orphan call edges: ${result.gcOrphanCallEdges}`);
|
|
7865
|
+
}
|
|
6557
7866
|
if (result.filePaths.length > 0) {
|
|
6558
7867
|
lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
|
|
6559
7868
|
}
|
|
@@ -6753,6 +8062,42 @@ var codebase_search = (0, import_plugin.tool)({
|
|
|
6753
8062
|
${formatSearchResults(results, "score")}`;
|
|
6754
8063
|
}
|
|
6755
8064
|
});
|
|
8065
|
+
var call_graph = (0, import_plugin.tool)({
|
|
8066
|
+
description: "Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies between functions.",
|
|
8067
|
+
args: {
|
|
8068
|
+
name: z.string().describe("Function or method name to query"),
|
|
8069
|
+
direction: z.enum(["callers", "callees"]).default("callers").describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
|
|
8070
|
+
symbolId: z.string().optional().describe("Symbol ID (required for 'callees' direction, returned by previous call_graph queries)")
|
|
8071
|
+
},
|
|
8072
|
+
async execute(args) {
|
|
8073
|
+
const indexer = getIndexer();
|
|
8074
|
+
if (args.direction === "callees") {
|
|
8075
|
+
if (!args.symbolId) {
|
|
8076
|
+
return "Error: 'symbolId' is required when direction is 'callees'. First use direction='callers' to find the symbol ID.";
|
|
8077
|
+
}
|
|
8078
|
+
const callees = await indexer.getCallees(args.symbolId);
|
|
8079
|
+
if (callees.length === 0) {
|
|
8080
|
+
return `No callees found for symbol ${args.symbolId}. The function may not call any other tracked functions.`;
|
|
8081
|
+
}
|
|
8082
|
+
const formatted2 = callees.map(
|
|
8083
|
+
(e, i) => `[${i + 1}] \u2192 ${e.targetName} (${e.callType}) at line ${e.line}${e.isResolved ? ` [resolved: ${e.toSymbolId}]` : " [unresolved]"}`
|
|
8084
|
+
);
|
|
8085
|
+
return `${args.name} calls ${callees.length} function(s):
|
|
8086
|
+
|
|
8087
|
+
${formatted2.join("\n")}`;
|
|
8088
|
+
}
|
|
8089
|
+
const callers = await indexer.getCallers(args.name);
|
|
8090
|
+
if (callers.length === 0) {
|
|
8091
|
+
return `No callers found for "${args.name}". It may not be called by any tracked function, or the index needs updating.`;
|
|
8092
|
+
}
|
|
8093
|
+
const formatted = callers.map(
|
|
8094
|
+
(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]"}`
|
|
8095
|
+
);
|
|
8096
|
+
return `"${args.name}" is called by ${callers.length} function(s):
|
|
8097
|
+
|
|
8098
|
+
${formatted.join("\n")}`;
|
|
8099
|
+
}
|
|
8100
|
+
});
|
|
6756
8101
|
|
|
6757
8102
|
// src/commands/loader.ts
|
|
6758
8103
|
var import_fs5 = require("fs");
|
|
@@ -6857,7 +8202,8 @@ var plugin = async ({ directory }) => {
|
|
|
6857
8202
|
index_health_check,
|
|
6858
8203
|
index_metrics,
|
|
6859
8204
|
index_logs,
|
|
6860
|
-
find_similar
|
|
8205
|
+
find_similar,
|
|
8206
|
+
call_graph
|
|
6861
8207
|
},
|
|
6862
8208
|
async config(cfg) {
|
|
6863
8209
|
cfg.command = cfg.command ?? {};
|