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