opencode-codebase-index 0.5.1 → 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 : {};
@@ -2936,6 +2945,9 @@ function hashContent(content) {
2936
2945
  function hashFile(filePath) {
2937
2946
  return native.hashFile(filePath);
2938
2947
  }
2948
+ function extractCalls(content, language) {
2949
+ return native.extractCalls(content, language);
2950
+ }
2939
2951
  var VectorStore = class {
2940
2952
  inner;
2941
2953
  dimensions;
@@ -3267,6 +3279,12 @@ var Database = class {
3267
3279
  getChunksByFile(filePath) {
3268
3280
  return this.inner.getChunksByFile(filePath);
3269
3281
  }
3282
+ getChunksByName(name) {
3283
+ return this.inner.getChunksByName(name);
3284
+ }
3285
+ getChunksByNameCi(name) {
3286
+ return this.inner.getChunksByNameCi(name);
3287
+ }
3270
3288
  deleteChunksByFile(filePath) {
3271
3289
  return this.inner.deleteChunksByFile(filePath);
3272
3290
  }
@@ -3310,6 +3328,73 @@ var Database = class {
3310
3328
  getStats() {
3311
3329
  return this.inner.getStats();
3312
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
+ }
3313
3398
  };
3314
3399
 
3315
3400
  // src/git/index.ts
@@ -3410,6 +3495,29 @@ function getBranchOrDefault(repoRoot) {
3410
3495
  }
3411
3496
 
3412
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
+ ]);
3413
3521
  function float32ArrayToBuffer(arr) {
3414
3522
  const float32 = new Float32Array(arr);
3415
3523
  return Buffer.from(float32.buffer);
@@ -3434,6 +3542,892 @@ function isRateLimitError(error) {
3434
3542
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
3435
3543
  }
3436
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
+ }
3437
4431
  var Indexer = class {
3438
4432
  config;
3439
4433
  projectRoot;
@@ -3936,6 +4930,79 @@ var Indexer = class {
3936
4930
  if (chunkDataBatch.length > 0) {
3937
4931
  database.upsertChunksBatch(chunkDataBatch);
3938
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
+ }
3939
5006
  let removedCount = 0;
3940
5007
  for (const [chunkId] of existingChunks) {
3941
5008
  if (!currentChunkIds.has(chunkId)) {
@@ -3957,6 +5024,8 @@ var Indexer = class {
3957
5024
  if (pendingChunks.length === 0 && removedCount === 0) {
3958
5025
  database.clearBranch(this.currentBranch);
3959
5026
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5027
+ database.clearBranchSymbols(this.currentBranch);
5028
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
3960
5029
  this.fileHashCache = currentFileHashes;
3961
5030
  this.saveFileHashCache();
3962
5031
  stats.durationMs = Date.now() - startTime;
@@ -3973,6 +5042,8 @@ var Indexer = class {
3973
5042
  if (pendingChunks.length === 0) {
3974
5043
  database.clearBranch(this.currentBranch);
3975
5044
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5045
+ database.clearBranchSymbols(this.currentBranch);
5046
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
3976
5047
  store.save();
3977
5048
  invertedIndex.save();
3978
5049
  this.fileHashCache = currentFileHashes;
@@ -4113,6 +5184,8 @@ var Indexer = class {
4113
5184
  });
4114
5185
  database.clearBranch(this.currentBranch);
4115
5186
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5187
+ database.clearBranchSymbols(this.currentBranch);
5188
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
4116
5189
  store.save();
4117
5190
  invertedIndex.save();
4118
5191
  this.fileHashCache = currentFileHashes;
@@ -4223,15 +5296,22 @@ var Indexer = class {
4223
5296
  }
4224
5297
  const maxResults = limit ?? this.config.search.maxResults;
4225
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;
4226
5302
  const filterByBranch = options?.filterByBranch ?? true;
4227
5303
  this.logger.search("debug", "Starting search", {
4228
5304
  query,
4229
5305
  maxResults,
4230
5306
  hybridWeight,
5307
+ fusionStrategy,
5308
+ rrfK,
5309
+ rerankTopN,
4231
5310
  filterByBranch
4232
5311
  });
4233
5312
  const embeddingStartTime = performance2.now();
4234
- const embedding = await this.getQueryEmbedding(query, provider);
5313
+ const embeddingQuery = stripFilePathHint(query);
5314
+ const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
4235
5315
  const embeddingMs = performance2.now() - embeddingStartTime;
4236
5316
  const vectorStartTime = performance2.now();
4237
5317
  const semanticResults = store.search(embedding, maxResults * 4);
@@ -4239,16 +5319,74 @@ var Indexer = class {
4239
5319
  const keywordStartTime = performance2.now();
4240
5320
  const keywordResults = await this.keywordSearch(query, maxResults * 4);
4241
5321
  const keywordMs = performance2.now() - keywordStartTime;
4242
- const fusionStartTime = performance2.now();
4243
- const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
4244
- const fusionMs = performance2.now() - fusionStartTime;
4245
5322
  let branchChunkIds = null;
4246
5323
  if (filterByBranch && this.currentBranch !== "default") {
4247
5324
  branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
4248
5325
  }
4249
- 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) => {
4250
5389
  if (r.score < this.config.search.minScore) return false;
4251
- if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
4252
5390
  if (options?.fileType) {
4253
5391
  const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
4254
5392
  if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
@@ -4261,7 +5399,11 @@ var Indexer = class {
4261
5399
  if (r.metadata.chunkType !== options.chunkType) return false;
4262
5400
  }
4263
5401
  return true;
4264
- }).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);
4265
5407
  const totalSearchMs = performance2.now() - searchStartTime;
4266
5408
  this.logger.recordSearch(totalSearchMs, {
4267
5409
  embeddingMs,
@@ -4276,6 +5418,7 @@ var Indexer = class {
4276
5418
  embeddingMs: Math.round(embeddingMs * 100) / 100,
4277
5419
  vectorMs: Math.round(vectorMs * 100) / 100,
4278
5420
  keywordMs: Math.round(keywordMs * 100) / 100,
5421
+ prefilterMs: Math.round(prefilterMs * 100) / 100,
4279
5422
  fusionMs: Math.round(fusionMs * 100) / 100
4280
5423
  });
4281
5424
  const metadataOnly = options?.metadataOnly ?? false;
@@ -4329,34 +5472,6 @@ var Indexer = class {
4329
5472
  results.sort((a, b) => b.score - a.score);
4330
5473
  return results.slice(0, limit);
4331
5474
  }
4332
- fuseResults(semanticResults, keywordResults, keywordWeight, limit) {
4333
- const semanticWeight = 1 - keywordWeight;
4334
- const fusedScores = /* @__PURE__ */ new Map();
4335
- for (const r of semanticResults) {
4336
- fusedScores.set(r.id, {
4337
- score: r.score * semanticWeight,
4338
- metadata: r.metadata
4339
- });
4340
- }
4341
- for (const r of keywordResults) {
4342
- const existing = fusedScores.get(r.id);
4343
- if (existing) {
4344
- existing.score += r.score * keywordWeight;
4345
- } else {
4346
- fusedScores.set(r.id, {
4347
- score: r.score * keywordWeight,
4348
- metadata: r.metadata
4349
- });
4350
- }
4351
- }
4352
- const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
4353
- id,
4354
- score: data.score,
4355
- metadata: data.metadata
4356
- }));
4357
- results.sort((a, b) => b.score - a.score);
4358
- return results.slice(0, limit);
4359
- }
4360
5475
  async getStatus() {
4361
5476
  const { store, configuredProviderInfo } = await this.ensureInitialized();
4362
5477
  return {
@@ -4379,6 +5494,7 @@ var Indexer = class {
4379
5494
  this.fileHashCache.clear();
4380
5495
  this.saveFileHashCache();
4381
5496
  database.clearBranch(this.currentBranch);
5497
+ database.clearBranchSymbols(this.currentBranch);
4382
5498
  database.deleteMetadata("index.version");
4383
5499
  database.deleteMetadata("index.embeddingProvider");
4384
5500
  database.deleteMetadata("index.embeddingModel");
@@ -4407,6 +5523,8 @@ var Indexer = class {
4407
5523
  removedCount++;
4408
5524
  }
4409
5525
  database.deleteChunksByFile(filePath);
5526
+ database.deleteCallEdgesByFile(filePath);
5527
+ database.deleteSymbolsByFile(filePath);
4410
5528
  removedFilePaths.push(filePath);
4411
5529
  }
4412
5530
  }
@@ -4416,6 +5534,8 @@ var Indexer = class {
4416
5534
  }
4417
5535
  const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
4418
5536
  const gcOrphanChunks = database.gcOrphanChunks();
5537
+ const gcOrphanSymbols = database.gcOrphanSymbols();
5538
+ const gcOrphanCallEdges = database.gcOrphanCallEdges();
4419
5539
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
4420
5540
  this.logger.gc("info", "Health check complete", {
4421
5541
  removedStale: removedCount,
@@ -4423,7 +5543,7 @@ var Indexer = class {
4423
5543
  orphanChunks: gcOrphanChunks,
4424
5544
  removedFiles: removedFilePaths.length
4425
5545
  });
4426
- return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
5546
+ return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
4427
5547
  }
4428
5548
  async retryFailedBatches() {
4429
5549
  const { store, provider, invertedIndex } = await this.ensureInitialized();
@@ -4529,9 +5649,29 @@ var Indexer = class {
4529
5649
  if (filterByBranch && this.currentBranch !== "default") {
4530
5650
  branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
4531
5651
  }
4532
- 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) => {
4533
5674
  if (r.score < this.config.search.minScore) return false;
4534
- if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
4535
5675
  if (options?.excludeFile) {
4536
5676
  if (r.metadata.filePath === options.excludeFile) return false;
4537
5677
  }
@@ -4560,7 +5700,8 @@ var Indexer = class {
4560
5700
  results: filtered.length,
4561
5701
  totalMs: Math.round(totalSearchMs * 100) / 100,
4562
5702
  embeddingMs: Math.round(embeddingMs * 100) / 100,
4563
- vectorMs: Math.round(vectorMs * 100) / 100
5703
+ vectorMs: Math.round(vectorMs * 100) / 100,
5704
+ prefilterMs: Math.round(prefilterMs * 100) / 100
4564
5705
  });
4565
5706
  return Promise.all(
4566
5707
  filtered.map(async (r) => {
@@ -4589,6 +5730,14 @@ var Indexer = class {
4589
5730
  })
4590
5731
  );
4591
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
+ }
4592
5741
  };
4593
5742
 
4594
5743
  // src/mcp-server.ts
@@ -4791,7 +5940,7 @@ Use Read tool to examine specific files.` }] };
4791
5940
  async () => {
4792
5941
  await ensureInitialized();
4793
5942
  const result = await indexer.healthCheck();
4794
- 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) {
4795
5944
  return { content: [{ type: "text", text: "Index is healthy. No stale entries found." }] };
4796
5945
  }
4797
5946
  const lines = [`Health check complete:`];
@@ -4804,6 +5953,12 @@ Use Read tool to examine specific files.` }] };
4804
5953
  if (result.gcOrphanChunks > 0) {
4805
5954
  lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
4806
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
+ }
4807
5962
  if (result.filePaths.length > 0) {
4808
5963
  lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
4809
5964
  }
@@ -4892,6 +6047,43 @@ ${truncateContent(r.content)}
4892
6047
  ${formatted.join("\n\n")}` }] };
4893
6048
  }
4894
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
+ );
4895
6087
  server.prompt(
4896
6088
  "search",
4897
6089
  "Search codebase by meaning using semantic search",