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