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.cjs CHANGED
@@ -776,9 +776,15 @@ function getDefaultSearchConfig() {
776
776
  minScore: 0.1,
777
777
  includeContext: true,
778
778
  hybridWeight: 0.5,
779
+ fusionStrategy: "rrf",
780
+ rrfK: 60,
781
+ rerankTopN: 20,
779
782
  contextLines: 0
780
783
  };
781
784
  }
785
+ function isValidFusionStrategy(value) {
786
+ return value === "weighted" || value === "rrf";
787
+ }
782
788
  function getDefaultDebugConfig() {
783
789
  return {
784
790
  enabled: false,
@@ -833,6 +839,9 @@ function parseConfig(raw) {
833
839
  minScore: typeof rawSearch.minScore === "number" ? rawSearch.minScore : defaultSearch.minScore,
834
840
  includeContext: typeof rawSearch.includeContext === "boolean" ? rawSearch.includeContext : defaultSearch.includeContext,
835
841
  hybridWeight: typeof rawSearch.hybridWeight === "number" ? Math.min(1, Math.max(0, rawSearch.hybridWeight)) : defaultSearch.hybridWeight,
842
+ fusionStrategy: isValidFusionStrategy(rawSearch.fusionStrategy) ? rawSearch.fusionStrategy : defaultSearch.fusionStrategy,
843
+ rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
844
+ rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
836
845
  contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines
837
846
  };
838
847
  const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
@@ -2931,6 +2940,9 @@ function hashContent(content) {
2931
2940
  function hashFile(filePath) {
2932
2941
  return native.hashFile(filePath);
2933
2942
  }
2943
+ function extractCalls(content, language) {
2944
+ return native.extractCalls(content, language);
2945
+ }
2934
2946
  var VectorStore = class {
2935
2947
  inner;
2936
2948
  dimensions;
@@ -3262,6 +3274,12 @@ var Database = class {
3262
3274
  getChunksByFile(filePath) {
3263
3275
  return this.inner.getChunksByFile(filePath);
3264
3276
  }
3277
+ getChunksByName(name) {
3278
+ return this.inner.getChunksByName(name);
3279
+ }
3280
+ getChunksByNameCi(name) {
3281
+ return this.inner.getChunksByNameCi(name);
3282
+ }
3265
3283
  deleteChunksByFile(filePath) {
3266
3284
  return this.inner.deleteChunksByFile(filePath);
3267
3285
  }
@@ -3305,6 +3323,73 @@ var Database = class {
3305
3323
  getStats() {
3306
3324
  return this.inner.getStats();
3307
3325
  }
3326
+ // ── Symbol methods ──────────────────────────────────────────────
3327
+ upsertSymbol(symbol) {
3328
+ this.inner.upsertSymbol(symbol);
3329
+ }
3330
+ upsertSymbolsBatch(symbols) {
3331
+ if (symbols.length === 0) return;
3332
+ this.inner.upsertSymbolsBatch(symbols);
3333
+ }
3334
+ getSymbolsByFile(filePath) {
3335
+ return this.inner.getSymbolsByFile(filePath);
3336
+ }
3337
+ getSymbolByName(name, filePath) {
3338
+ return this.inner.getSymbolByName(name, filePath) ?? null;
3339
+ }
3340
+ getSymbolsByName(name) {
3341
+ return this.inner.getSymbolsByName(name);
3342
+ }
3343
+ getSymbolsByNameCi(name) {
3344
+ return this.inner.getSymbolsByNameCi(name);
3345
+ }
3346
+ deleteSymbolsByFile(filePath) {
3347
+ return this.inner.deleteSymbolsByFile(filePath);
3348
+ }
3349
+ // ── Call Edge methods ────────────────────────────────────────────
3350
+ upsertCallEdge(edge) {
3351
+ this.inner.upsertCallEdge(edge);
3352
+ }
3353
+ upsertCallEdgesBatch(edges) {
3354
+ if (edges.length === 0) return;
3355
+ this.inner.upsertCallEdgesBatch(edges);
3356
+ }
3357
+ getCallers(targetName, branch) {
3358
+ return this.inner.getCallers(targetName, branch);
3359
+ }
3360
+ getCallersWithContext(targetName, branch) {
3361
+ return this.inner.getCallersWithContext(targetName, branch);
3362
+ }
3363
+ getCallees(symbolId, branch) {
3364
+ return this.inner.getCallees(symbolId, branch);
3365
+ }
3366
+ deleteCallEdgesByFile(filePath) {
3367
+ return this.inner.deleteCallEdgesByFile(filePath);
3368
+ }
3369
+ resolveCallEdge(edgeId, toSymbolId) {
3370
+ this.inner.resolveCallEdge(edgeId, toSymbolId);
3371
+ }
3372
+ // ── Branch Symbol methods ────────────────────────────────────────
3373
+ addSymbolsToBranch(branch, symbolIds) {
3374
+ this.inner.addSymbolsToBranch(branch, symbolIds);
3375
+ }
3376
+ addSymbolsToBranchBatch(branch, symbolIds) {
3377
+ if (symbolIds.length === 0) return;
3378
+ this.inner.addSymbolsToBranchBatch(branch, symbolIds);
3379
+ }
3380
+ getBranchSymbolIds(branch) {
3381
+ return this.inner.getBranchSymbolIds(branch);
3382
+ }
3383
+ clearBranchSymbols(branch) {
3384
+ return this.inner.clearBranchSymbols(branch);
3385
+ }
3386
+ // ── GC methods for symbols/edges ─────────────────────────────────
3387
+ gcOrphanSymbols() {
3388
+ return this.inner.gcOrphanSymbols();
3389
+ }
3390
+ gcOrphanCallEdges() {
3391
+ return this.inner.gcOrphanCallEdges();
3392
+ }
3308
3393
  };
3309
3394
 
3310
3395
  // src/git/index.ts
@@ -3405,6 +3490,29 @@ function getBranchOrDefault(repoRoot) {
3405
3490
  }
3406
3491
 
3407
3492
  // src/indexer/index.ts
3493
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust"]);
3494
+ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
3495
+ "function_declaration",
3496
+ "function",
3497
+ "arrow_function",
3498
+ "method_definition",
3499
+ "class_declaration",
3500
+ "interface_declaration",
3501
+ "type_alias_declaration",
3502
+ "enum_declaration",
3503
+ "function_definition",
3504
+ "class_definition",
3505
+ "decorated_definition",
3506
+ "method_declaration",
3507
+ "type_declaration",
3508
+ "type_spec",
3509
+ "function_item",
3510
+ "impl_item",
3511
+ "struct_item",
3512
+ "enum_item",
3513
+ "trait_item",
3514
+ "mod_item"
3515
+ ]);
3408
3516
  function float32ArrayToBuffer(arr) {
3409
3517
  const float32 = new Float32Array(arr);
3410
3518
  return Buffer.from(float32.buffer);
@@ -3429,6 +3537,892 @@ function isRateLimitError(error) {
3429
3537
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
3430
3538
  }
3431
3539
  var INDEX_METADATA_VERSION = "1";
3540
+ var RANKING_TOKEN_CACHE_LIMIT = 4096;
3541
+ var rankingQueryTokenCache = /* @__PURE__ */ new Map();
3542
+ var rankingNameTokenCache = /* @__PURE__ */ new Map();
3543
+ var rankingPathTokenCache = /* @__PURE__ */ new Map();
3544
+ var rankingTextTokenCache = /* @__PURE__ */ new Map();
3545
+ var STOPWORDS = /* @__PURE__ */ new Set([
3546
+ "the",
3547
+ "and",
3548
+ "for",
3549
+ "with",
3550
+ "from",
3551
+ "that",
3552
+ "this",
3553
+ "into",
3554
+ "using",
3555
+ "where",
3556
+ "what",
3557
+ "when",
3558
+ "why",
3559
+ "how",
3560
+ "are",
3561
+ "was",
3562
+ "were",
3563
+ "be",
3564
+ "been",
3565
+ "being",
3566
+ "find",
3567
+ "show",
3568
+ "get",
3569
+ "run",
3570
+ "use",
3571
+ "code",
3572
+ "function",
3573
+ "implementation",
3574
+ "retrieve",
3575
+ "results",
3576
+ "result",
3577
+ "search",
3578
+ "pipeline",
3579
+ "top",
3580
+ "in",
3581
+ "on",
3582
+ "of",
3583
+ "to",
3584
+ "by",
3585
+ "as",
3586
+ "or",
3587
+ "an",
3588
+ "a"
3589
+ ]);
3590
+ var TEST_PATH_SEGMENTS = [
3591
+ "tests/",
3592
+ "__tests__/",
3593
+ "/test/",
3594
+ "fixtures/",
3595
+ "benchmark",
3596
+ "README",
3597
+ "ARCHITECTURE",
3598
+ "docs/"
3599
+ ];
3600
+ var IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS = [
3601
+ "tests/",
3602
+ "__tests__/",
3603
+ "/test/",
3604
+ "fixtures/",
3605
+ "benchmark",
3606
+ "readme",
3607
+ "architecture",
3608
+ "docs/",
3609
+ "examples/",
3610
+ "example/",
3611
+ ".github/",
3612
+ "/scripts/",
3613
+ "/migrations/",
3614
+ "/generated/"
3615
+ ];
3616
+ var SOURCE_INTENT_HINTS = /* @__PURE__ */ new Set([
3617
+ "implement",
3618
+ "implementation",
3619
+ "function",
3620
+ "method",
3621
+ "class",
3622
+ "logic",
3623
+ "algorithm",
3624
+ "pipeline",
3625
+ "indexer",
3626
+ "where"
3627
+ ]);
3628
+ var DOC_TEST_INTENT_HINTS = /* @__PURE__ */ new Set([
3629
+ "test",
3630
+ "tests",
3631
+ "fixture",
3632
+ "fixtures",
3633
+ "benchmark",
3634
+ "readme",
3635
+ "docs",
3636
+ "documentation"
3637
+ ]);
3638
+ var DOC_INTENT_HINTS = /* @__PURE__ */ new Set([
3639
+ "readme",
3640
+ "docs",
3641
+ "documentation",
3642
+ "guide",
3643
+ "usage"
3644
+ ]);
3645
+ function setBoundedCache(cache, key, value) {
3646
+ if (cache.size >= RANKING_TOKEN_CACHE_LIMIT) {
3647
+ const oldest = cache.keys().next().value;
3648
+ if (oldest !== void 0) {
3649
+ cache.delete(oldest);
3650
+ }
3651
+ }
3652
+ cache.set(key, value);
3653
+ }
3654
+ function tokenizeTextForRanking(text) {
3655
+ if (!text) {
3656
+ return /* @__PURE__ */ new Set();
3657
+ }
3658
+ const lowered = text.toLowerCase();
3659
+ const cache = rankingQueryTokenCache.get(lowered) ?? rankingTextTokenCache.get(lowered);
3660
+ if (cache) {
3661
+ return cache;
3662
+ }
3663
+ const tokens = new Set(
3664
+ lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1 && !STOPWORDS.has(token))
3665
+ );
3666
+ setBoundedCache(rankingQueryTokenCache, lowered, tokens);
3667
+ setBoundedCache(rankingTextTokenCache, lowered, tokens);
3668
+ return tokens;
3669
+ }
3670
+ function splitPathTokens(filePath) {
3671
+ const lowered = filePath.toLowerCase();
3672
+ const cache = rankingPathTokenCache.get(lowered);
3673
+ if (cache) {
3674
+ return cache;
3675
+ }
3676
+ const normalized = lowered.replace(/[^a-z0-9/._-]/g, " ").split(/[/._-]+/).filter((token) => token.length > 1);
3677
+ const tokens = new Set(normalized);
3678
+ setBoundedCache(rankingPathTokenCache, lowered, tokens);
3679
+ return tokens;
3680
+ }
3681
+ function splitNameTokens(name) {
3682
+ if (!name) {
3683
+ return /* @__PURE__ */ new Set();
3684
+ }
3685
+ const lowered = name.toLowerCase();
3686
+ const cache = rankingNameTokenCache.get(lowered);
3687
+ if (cache) {
3688
+ return cache;
3689
+ }
3690
+ const tokens = new Set(
3691
+ lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1)
3692
+ );
3693
+ setBoundedCache(rankingNameTokenCache, lowered, tokens);
3694
+ return tokens;
3695
+ }
3696
+ function chunkTypeBoost(chunkType) {
3697
+ switch (chunkType) {
3698
+ case "function":
3699
+ case "function_declaration":
3700
+ case "method":
3701
+ case "method_definition":
3702
+ case "class":
3703
+ case "class_declaration":
3704
+ return 0.2;
3705
+ case "interface":
3706
+ case "type":
3707
+ case "enum":
3708
+ case "struct":
3709
+ case "impl":
3710
+ case "trait":
3711
+ case "module":
3712
+ return 0.1;
3713
+ default:
3714
+ return 0;
3715
+ }
3716
+ }
3717
+ function isTestOrDocPath(filePath) {
3718
+ return TEST_PATH_SEGMENTS.some((segment) => filePath.includes(segment));
3719
+ }
3720
+ function isLikelyImplementationPath(filePath) {
3721
+ const lowered = filePath.toLowerCase();
3722
+ if (IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS.some((segment) => lowered.includes(segment))) {
3723
+ return false;
3724
+ }
3725
+ const ext = lowered.split(".").pop() ?? "";
3726
+ if (["md", "mdx", "txt", "rst", "adoc", "snap", "json", "yaml", "yml", "lock"].includes(ext)) {
3727
+ return false;
3728
+ }
3729
+ return true;
3730
+ }
3731
+ function classifyQueryIntent(tokens) {
3732
+ const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
3733
+ const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
3734
+ return sourceIntentHits >= docTestIntentHits ? "source" : "doc_test";
3735
+ }
3736
+ function classifyQueryIntentRaw(query) {
3737
+ const lowerQuery = query.toLowerCase();
3738
+ const docTestRawHits = Array.from(DOC_TEST_INTENT_HINTS).filter(
3739
+ (hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)
3740
+ ).length;
3741
+ const sourceRawHits = [
3742
+ "implement",
3743
+ "implementation",
3744
+ "implements",
3745
+ "function",
3746
+ "method",
3747
+ "class",
3748
+ "logic",
3749
+ "algorithm",
3750
+ "pipeline",
3751
+ "indexer"
3752
+ ].filter((hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)).length;
3753
+ if (docTestRawHits > sourceRawHits) {
3754
+ return "doc_test";
3755
+ }
3756
+ if (sourceRawHits > docTestRawHits) {
3757
+ return "source";
3758
+ }
3759
+ const hasWhereIsPattern = /\bwhere\s+is\b/.test(lowerQuery);
3760
+ const hasIdentifierHints = extractIdentifierHints(query).length > 0;
3761
+ if (hasWhereIsPattern && hasIdentifierHints && docTestRawHits === 0) {
3762
+ return "source";
3763
+ }
3764
+ const queryTokens = Array.from(tokenizeTextForRanking(query));
3765
+ return classifyQueryIntent(queryTokens);
3766
+ }
3767
+ function classifyDocIntent(tokens) {
3768
+ const docHits = tokens.filter((t) => DOC_INTENT_HINTS.has(t)).length;
3769
+ const testHits = tokens.filter((t) => ["test", "tests", "fixture", "fixtures", "benchmark"].includes(t)).length;
3770
+ if (docHits > 0 && testHits === 0) return "docs";
3771
+ if (testHits > 0 && docHits === 0) return "test";
3772
+ if (testHits > 0 || docHits > 0) return "mixed";
3773
+ return "none";
3774
+ }
3775
+ function isImplementationChunkType(chunkType) {
3776
+ return [
3777
+ "export_statement",
3778
+ "function",
3779
+ "function_declaration",
3780
+ "method",
3781
+ "method_definition",
3782
+ "class",
3783
+ "class_declaration",
3784
+ "interface",
3785
+ "type",
3786
+ "enum",
3787
+ "module"
3788
+ ].includes(chunkType);
3789
+ }
3790
+ function extractIdentifierHints(query) {
3791
+ const identifiers = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
3792
+ return identifiers.filter((id) => id.length >= 3).filter((id) => {
3793
+ const lower = id.toLowerCase();
3794
+ if (STOPWORDS.has(lower)) return false;
3795
+ return /[A-Z]/.test(id) || id.includes("_") || id.endsWith("Results") || id.endsWith("Result");
3796
+ }).map((id) => id.toLowerCase());
3797
+ }
3798
+ function extractCodeTermHints(query) {
3799
+ const terms = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
3800
+ return terms.map((term) => term.toLowerCase()).filter((term) => term.length >= 3).filter((term) => !STOPWORDS.has(term));
3801
+ }
3802
+ function normalizeIdentifierVariants(identifier) {
3803
+ const lower = identifier.toLowerCase();
3804
+ const compact = lower.replace(/[^a-z0-9]/g, "");
3805
+ const snake = compact.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
3806
+ const kebab = snake.replace(/_/g, "-");
3807
+ const variants = [lower, compact, snake, kebab].filter((value) => value.length > 0);
3808
+ return Array.from(new Set(variants));
3809
+ }
3810
+ function scoreIdentifierMatch(name, filePath, hints) {
3811
+ const nameLower = (name ?? "").toLowerCase();
3812
+ const pathLower = filePath.toLowerCase();
3813
+ let best = 0;
3814
+ for (const hint of hints) {
3815
+ const variants = normalizeIdentifierVariants(hint);
3816
+ for (const variant of variants) {
3817
+ if (nameLower === variant) {
3818
+ best = Math.max(best, 1);
3819
+ } else if (nameLower.includes(variant)) {
3820
+ best = Math.max(best, 0.8);
3821
+ } else if (pathLower.includes(variant)) {
3822
+ best = Math.max(best, 0.6);
3823
+ }
3824
+ }
3825
+ }
3826
+ return best;
3827
+ }
3828
+ function extractPrimaryIdentifierQueryHint(query) {
3829
+ const identifiers = extractIdentifierHints(query);
3830
+ if (identifiers.length > 0) {
3831
+ return identifiers[0] ?? null;
3832
+ }
3833
+ const codeTerms = extractCodeTermHints(query);
3834
+ const best = codeTerms.find((term) => term.length >= 6);
3835
+ return best ?? null;
3836
+ }
3837
+ var FILE_PATH_HINT_EXTENSIONS = [
3838
+ "ts",
3839
+ "tsx",
3840
+ "js",
3841
+ "jsx",
3842
+ "mjs",
3843
+ "cjs",
3844
+ "mts",
3845
+ "cts",
3846
+ "py",
3847
+ "rs",
3848
+ "go",
3849
+ "java",
3850
+ "kt",
3851
+ "kts",
3852
+ "swift",
3853
+ "rb",
3854
+ "php",
3855
+ "c",
3856
+ "h",
3857
+ "cc",
3858
+ "cpp",
3859
+ "cxx",
3860
+ "hpp",
3861
+ "cs",
3862
+ "scala",
3863
+ "lua",
3864
+ "sh",
3865
+ "bash",
3866
+ "zsh",
3867
+ "json",
3868
+ "yaml",
3869
+ "yml",
3870
+ "toml"
3871
+ ];
3872
+ var FILE_PATH_HINT_SUFFIX_REGEX = new RegExp(
3873
+ "\\s+\\bin\\s+[\"'`]?((?:\\.\\/)?(?:[A-Za-z0-9._-]+\\/)+[A-Za-z0-9._-]+\\.(?:" + FILE_PATH_HINT_EXTENSIONS.join("|") + "))[\"'`]?[\\])}>.,;!?]*\\s*$",
3874
+ "i"
3875
+ );
3876
+ function normalizeFilePathForHintMatch(filePath) {
3877
+ return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
3878
+ }
3879
+ function pathMatchesHint(filePath, hint) {
3880
+ const normalizedPath = normalizeFilePathForHintMatch(filePath);
3881
+ const normalizedHint = normalizeFilePathForHintMatch(hint);
3882
+ return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
3883
+ }
3884
+ function extractFilePathHint(query) {
3885
+ const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
3886
+ const rawPath = match?.[1];
3887
+ if (!rawPath) {
3888
+ return null;
3889
+ }
3890
+ return rawPath.replace(/^\.\//, "");
3891
+ }
3892
+ function stripFilePathHint(query) {
3893
+ const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
3894
+ return stripped.length > 0 ? stripped : query;
3895
+ }
3896
+ function buildDeterministicIdentifierPass(query, candidates, limit) {
3897
+ if (classifyQueryIntentRaw(query) !== "source") {
3898
+ return [];
3899
+ }
3900
+ const primary = extractPrimaryIdentifierQueryHint(query);
3901
+ if (!primary) {
3902
+ return [];
3903
+ }
3904
+ const filePathHint = extractFilePathHint(query);
3905
+ const primaryVariants = normalizeIdentifierVariants(primary);
3906
+ 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);
3907
+ const deterministic = candidates.filter(
3908
+ (candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
3909
+ ).map((candidate) => {
3910
+ const nameLower = (candidate.metadata.name ?? "").toLowerCase();
3911
+ const pathLower = candidate.metadata.filePath.toLowerCase();
3912
+ let maxMatch = 0;
3913
+ const nameMatchesPrimary = primaryVariants.some(
3914
+ (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
3915
+ );
3916
+ const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
3917
+ for (const hint of hints) {
3918
+ const variants = normalizeIdentifierVariants(hint);
3919
+ for (const variant of variants) {
3920
+ if (nameLower === variant) {
3921
+ maxMatch = Math.max(maxMatch, 1);
3922
+ } else if (nameLower.includes(variant)) {
3923
+ maxMatch = Math.max(maxMatch, 0.85);
3924
+ } else if (pathLower.includes(variant)) {
3925
+ maxMatch = Math.max(maxMatch, 0.7);
3926
+ }
3927
+ }
3928
+ }
3929
+ if (pathMatchesFileHint && nameMatchesPrimary) {
3930
+ maxMatch = Math.max(maxMatch, 1);
3931
+ }
3932
+ return {
3933
+ candidate,
3934
+ maxMatch,
3935
+ pathMatchesFileHint,
3936
+ nameMatchesPrimary
3937
+ };
3938
+ }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
3939
+ const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
3940
+ const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
3941
+ if (aAnchored !== bAnchored) return bAnchored - aAnchored;
3942
+ if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
3943
+ if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
3944
+ return a.candidate.id.localeCompare(b.candidate.id);
3945
+ }).slice(0, Math.max(limit * 2, 12));
3946
+ return deterministic.map((entry) => ({
3947
+ id: entry.candidate.id,
3948
+ score: entry.pathMatchesFileHint && entry.nameMatchesPrimary ? 0.995 : Math.min(1, 0.9 + entry.maxMatch * 0.09),
3949
+ metadata: entry.candidate.metadata
3950
+ }));
3951
+ }
3952
+ function fuseResultsWeighted(semanticResults, keywordResults, keywordWeight, limit) {
3953
+ const semanticWeight = 1 - keywordWeight;
3954
+ const fusedScores = /* @__PURE__ */ new Map();
3955
+ for (const r of semanticResults) {
3956
+ fusedScores.set(r.id, {
3957
+ score: r.score * semanticWeight,
3958
+ metadata: r.metadata
3959
+ });
3960
+ }
3961
+ for (const r of keywordResults) {
3962
+ const existing = fusedScores.get(r.id);
3963
+ if (existing) {
3964
+ existing.score += r.score * keywordWeight;
3965
+ } else {
3966
+ fusedScores.set(r.id, {
3967
+ score: r.score * keywordWeight,
3968
+ metadata: r.metadata
3969
+ });
3970
+ }
3971
+ }
3972
+ const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
3973
+ id,
3974
+ score: data.score,
3975
+ metadata: data.metadata
3976
+ }));
3977
+ results.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
3978
+ return results.slice(0, limit);
3979
+ }
3980
+ function fuseResultsRrf(semanticResults, keywordResults, rrfK, limit) {
3981
+ const maxPossibleRaw = 2 / (rrfK + 1);
3982
+ const rankByIdSemantic = /* @__PURE__ */ new Map();
3983
+ const rankByIdKeyword = /* @__PURE__ */ new Map();
3984
+ const metadataById = /* @__PURE__ */ new Map();
3985
+ semanticResults.forEach((result, index) => {
3986
+ rankByIdSemantic.set(result.id, index + 1);
3987
+ metadataById.set(result.id, result.metadata);
3988
+ });
3989
+ keywordResults.forEach((result, index) => {
3990
+ rankByIdKeyword.set(result.id, index + 1);
3991
+ if (!metadataById.has(result.id)) {
3992
+ metadataById.set(result.id, result.metadata);
3993
+ }
3994
+ });
3995
+ const allIds = /* @__PURE__ */ new Set([...rankByIdSemantic.keys(), ...rankByIdKeyword.keys()]);
3996
+ const fused = [];
3997
+ for (const id of allIds) {
3998
+ const semanticRank = rankByIdSemantic.get(id);
3999
+ const keywordRank = rankByIdKeyword.get(id);
4000
+ const semanticScore = semanticRank ? 1 / (rrfK + semanticRank) : 0;
4001
+ const keywordScore = keywordRank ? 1 / (rrfK + keywordRank) : 0;
4002
+ const metadata = metadataById.get(id);
4003
+ if (!metadata) continue;
4004
+ fused.push({
4005
+ id,
4006
+ score: maxPossibleRaw > 0 ? (semanticScore + keywordScore) / maxPossibleRaw : 0,
4007
+ metadata
4008
+ });
4009
+ }
4010
+ fused.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4011
+ return fused.slice(0, limit);
4012
+ }
4013
+ function rerankResults(query, candidates, rerankTopN, options) {
4014
+ if (rerankTopN <= 0 || candidates.length <= 1) {
4015
+ return candidates;
4016
+ }
4017
+ const topN = Math.min(rerankTopN, candidates.length);
4018
+ const queryTokens = tokenizeTextForRanking(query);
4019
+ if (queryTokens.size === 0) {
4020
+ return candidates;
4021
+ }
4022
+ const queryTokenList = Array.from(queryTokens);
4023
+ const intent = classifyQueryIntentRaw(query);
4024
+ const docIntent = classifyDocIntent(queryTokenList);
4025
+ const preferSourcePaths = options?.prioritizeSourcePaths ?? intent === "source";
4026
+ const identifierHints = extractIdentifierHints(query);
4027
+ const head = candidates.slice(0, topN).map((candidate, idx) => {
4028
+ const pathTokens = splitPathTokens(candidate.metadata.filePath);
4029
+ const nameTokens = splitNameTokens(candidate.metadata.name ?? "");
4030
+ const chunkTypeTokens = tokenizeTextForRanking(candidate.metadata.chunkType);
4031
+ let exactOrPrefixNameHits = 0;
4032
+ let pathOverlap = 0;
4033
+ let chunkTypeHits = 0;
4034
+ for (const token of queryTokenList) {
4035
+ if (nameTokens.has(token)) {
4036
+ exactOrPrefixNameHits += 1;
4037
+ } else {
4038
+ for (const nameToken of nameTokens) {
4039
+ if (nameToken.startsWith(token) || token.startsWith(nameToken)) {
4040
+ exactOrPrefixNameHits += 1;
4041
+ break;
4042
+ }
4043
+ }
4044
+ }
4045
+ if (pathTokens.has(token)) {
4046
+ pathOverlap += 1;
4047
+ }
4048
+ if (chunkTypeTokens.has(token)) {
4049
+ chunkTypeHits += 1;
4050
+ }
4051
+ }
4052
+ const likelyTestOrDoc = isTestOrDocPath(candidate.metadata.filePath);
4053
+ const lowerPath = candidate.metadata.filePath.toLowerCase();
4054
+ const lowerName = (candidate.metadata.name ?? "").toLowerCase();
4055
+ const hasIdentifierMatch = identifierHints.some((id) => lowerPath.includes(id) || lowerName.includes(id));
4056
+ const implementationPathBoost = preferSourcePaths && isLikelyImplementationPath(candidate.metadata.filePath) ? 0.08 : 0;
4057
+ const isReadmePath = candidate.metadata.filePath.toLowerCase().includes("readme");
4058
+ const testDocPenalty = preferSourcePaths && likelyTestOrDoc ? 0.12 : 0;
4059
+ const readmeDocBoost = !preferSourcePaths && isReadmePath ? 0.08 : 0;
4060
+ const identifierBoost = hasIdentifierMatch ? 0.12 : 0;
4061
+ const tokenCoverage = queryTokenList.length > 0 ? (exactOrPrefixNameHits + pathOverlap + chunkTypeHits) / queryTokenList.length : 0;
4062
+ const coverageBoost = Math.min(0.12, tokenCoverage * 0.06);
4063
+ const deterministicBoost = exactOrPrefixNameHits * 0.08 + pathOverlap * 0.03 + chunkTypeHits * 0.02 + coverageBoost + identifierBoost + implementationPathBoost - testDocPenalty + readmeDocBoost + chunkTypeBoost(candidate.metadata.chunkType);
4064
+ return {
4065
+ candidate,
4066
+ boostedScore: candidate.score + deterministicBoost,
4067
+ originalIndex: idx,
4068
+ hasIdentifierMatch,
4069
+ implementationChunk: isImplementationChunkType(candidate.metadata.chunkType),
4070
+ isLikelyImplementationPath: isLikelyImplementationPath(candidate.metadata.filePath),
4071
+ isTestOrDocPath: likelyTestOrDoc,
4072
+ isReadmePath
4073
+ };
4074
+ });
4075
+ head.sort((a, b) => {
4076
+ if (b.boostedScore !== a.boostedScore) return b.boostedScore - a.boostedScore;
4077
+ if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
4078
+ if (a.originalIndex !== b.originalIndex) return a.originalIndex - b.originalIndex;
4079
+ return a.candidate.id.localeCompare(b.candidate.id);
4080
+ });
4081
+ if (preferSourcePaths) {
4082
+ head.sort((a, b) => {
4083
+ const aId = a.hasIdentifierMatch ? 1 : 0;
4084
+ const bId = b.hasIdentifierMatch ? 1 : 0;
4085
+ if (aId !== bId) return bId - aId;
4086
+ const aImpl = a.implementationChunk ? 1 : 0;
4087
+ const bImpl = b.implementationChunk ? 1 : 0;
4088
+ if (aImpl !== bImpl) return bImpl - aImpl;
4089
+ const aImplementationPath = a.isLikelyImplementationPath ? 1 : 0;
4090
+ const bImplementationPath = b.isLikelyImplementationPath ? 1 : 0;
4091
+ if (aImplementationPath !== bImplementationPath) return bImplementationPath - aImplementationPath;
4092
+ const aTestDoc = a.isTestOrDocPath ? 1 : 0;
4093
+ const bTestDoc = b.isTestOrDocPath ? 1 : 0;
4094
+ if (aTestDoc !== bTestDoc) return aTestDoc - bTestDoc;
4095
+ return 0;
4096
+ });
4097
+ } else if (docIntent === "docs") {
4098
+ head.sort((a, b) => {
4099
+ const aReadme = a.isReadmePath ? 1 : 0;
4100
+ const bReadme = b.isReadmePath ? 1 : 0;
4101
+ if (aReadme !== bReadme) return bReadme - aReadme;
4102
+ return 0;
4103
+ });
4104
+ }
4105
+ const tail = candidates.slice(topN);
4106
+ return [...head.map((entry) => entry.candidate), ...tail];
4107
+ }
4108
+ function rankHybridResults(query, semanticResults, keywordResults, options) {
4109
+ const overfetchLimit = Math.max(options.limit * 4, options.limit);
4110
+ const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
4111
+ const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
4112
+ const rerankPool = fused.slice(0, rerankPoolLimit);
4113
+ const intent = classifyQueryIntentRaw(query);
4114
+ return rerankResults(query, rerankPool, options.rerankTopN, {
4115
+ prioritizeSourcePaths: intent === "source"
4116
+ });
4117
+ }
4118
+ function rankSemanticOnlyResults(query, semanticResults, options) {
4119
+ const overfetchLimit = Math.max(options.limit * 4, options.limit);
4120
+ const bounded = semanticResults.slice(0, overfetchLimit);
4121
+ return rerankResults(query, bounded, options.rerankTopN, {
4122
+ prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
4123
+ });
4124
+ }
4125
+ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds) {
4126
+ if (combined.length === 0) {
4127
+ return combined;
4128
+ }
4129
+ if (classifyQueryIntentRaw(query) !== "source") {
4130
+ return combined;
4131
+ }
4132
+ const identifierHints = extractIdentifierHints(query);
4133
+ if (identifierHints.length === 0) {
4134
+ return combined;
4135
+ }
4136
+ const combinedById = new Map(combined.map((candidate) => [candidate.id, candidate]));
4137
+ const candidateUnion = /* @__PURE__ */ new Map();
4138
+ for (const candidate of semanticCandidates) {
4139
+ candidateUnion.set(candidate.id, candidate);
4140
+ }
4141
+ for (const candidate of keywordCandidates) {
4142
+ if (!candidateUnion.has(candidate.id)) {
4143
+ candidateUnion.set(candidate.id, candidate);
4144
+ }
4145
+ }
4146
+ if (database) {
4147
+ for (const identifier of identifierHints) {
4148
+ const symbols = database.getSymbolsByName(identifier);
4149
+ for (const symbol of symbols) {
4150
+ const chunks = database.getChunksByFile(symbol.filePath);
4151
+ for (const chunk of chunks) {
4152
+ if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
4153
+ continue;
4154
+ }
4155
+ const chunkType = chunk.nodeType ?? "other";
4156
+ if (!isImplementationChunkType(chunkType)) {
4157
+ continue;
4158
+ }
4159
+ if (!isLikelyImplementationPath(chunk.filePath)) {
4160
+ continue;
4161
+ }
4162
+ if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
4163
+ continue;
4164
+ }
4165
+ const existing = combinedById.get(chunk.chunkId) ?? candidateUnion.get(chunk.chunkId);
4166
+ const metadata = existing?.metadata ?? {
4167
+ filePath: chunk.filePath,
4168
+ startLine: chunk.startLine,
4169
+ endLine: chunk.endLine,
4170
+ chunkType,
4171
+ name: chunk.name ?? void 0,
4172
+ language: chunk.language,
4173
+ hash: chunk.contentHash
4174
+ };
4175
+ const baselineScore = existing?.score ?? 0.5;
4176
+ candidateUnion.set(chunk.chunkId, {
4177
+ id: chunk.chunkId,
4178
+ score: Math.min(1, baselineScore + 0.5),
4179
+ metadata
4180
+ });
4181
+ }
4182
+ }
4183
+ }
4184
+ }
4185
+ const promoted = [];
4186
+ for (const candidate of candidateUnion.values()) {
4187
+ const filePathLower = candidate.metadata.filePath.toLowerCase();
4188
+ const nameLower = (candidate.metadata.name ?? "").toLowerCase();
4189
+ const exactIdentifierMatch = identifierHints.some((hint) => nameLower === hint);
4190
+ const hasIdentifierMatch = exactIdentifierMatch || identifierHints.some(
4191
+ (hint) => nameLower.includes(hint) || filePathLower.includes(hint)
4192
+ );
4193
+ if (!hasIdentifierMatch) {
4194
+ continue;
4195
+ }
4196
+ if (!isImplementationChunkType(candidate.metadata.chunkType)) {
4197
+ continue;
4198
+ }
4199
+ if (!isLikelyImplementationPath(candidate.metadata.filePath)) {
4200
+ continue;
4201
+ }
4202
+ const existing = combinedById.get(candidate.id) ?? candidate;
4203
+ const rescueBoost = exactIdentifierMatch ? 0.45 : 0.25;
4204
+ const boostedScore = Math.min(1, Math.max(existing.score, candidate.score) + rescueBoost);
4205
+ promoted.push({
4206
+ id: existing.id,
4207
+ score: boostedScore,
4208
+ metadata: existing.metadata
4209
+ });
4210
+ }
4211
+ if (promoted.length === 0) {
4212
+ return combined;
4213
+ }
4214
+ promoted.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4215
+ const promotedIds = new Set(promoted.map((candidate) => candidate.id));
4216
+ const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
4217
+ return [...promoted, ...remainder];
4218
+ }
4219
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates) {
4220
+ if (classifyQueryIntentRaw(query) !== "source") {
4221
+ return [];
4222
+ }
4223
+ const identifierHints = extractIdentifierHints(query);
4224
+ const codeTermHints = extractCodeTermHints(query);
4225
+ if (identifierHints.length === 0 && codeTermHints.length === 0) {
4226
+ return [];
4227
+ }
4228
+ const symbolCandidates = /* @__PURE__ */ new Map();
4229
+ const filePathHint = extractFilePathHint(query);
4230
+ const primaryHint = extractPrimaryIdentifierQueryHint(query);
4231
+ const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
4232
+ if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
4233
+ return;
4234
+ }
4235
+ const chunkType = chunk.nodeType ?? "other";
4236
+ if (!isImplementationChunkType(chunkType)) {
4237
+ return;
4238
+ }
4239
+ if (!isLikelyImplementationPath(chunk.filePath)) {
4240
+ return;
4241
+ }
4242
+ const nameLower = (chunk.name ?? "").toLowerCase();
4243
+ const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
4244
+ const base = baseScore ?? (exactName ? 0.99 : 0.88);
4245
+ const existing = symbolCandidates.get(chunk.chunkId);
4246
+ if (!existing || base > existing.score) {
4247
+ symbolCandidates.set(chunk.chunkId, {
4248
+ id: chunk.chunkId,
4249
+ score: base,
4250
+ metadata: {
4251
+ filePath: chunk.filePath,
4252
+ startLine: chunk.startLine,
4253
+ endLine: chunk.endLine,
4254
+ chunkType,
4255
+ name: chunk.name ?? void 0,
4256
+ language: chunk.language,
4257
+ hash: chunk.contentHash
4258
+ }
4259
+ });
4260
+ }
4261
+ };
4262
+ const normalizedHints = identifierHints.flatMap((hint) => [
4263
+ hint,
4264
+ hint.replace(/_/g, ""),
4265
+ hint.replace(/_/g, "-")
4266
+ ]).filter((hint, idx, arr) => hint.length >= 3 && arr.indexOf(hint) === idx).slice(0, 6);
4267
+ for (const identifier of normalizedHints) {
4268
+ const symbols = [
4269
+ ...database.getSymbolsByName(identifier),
4270
+ ...database.getSymbolsByNameCi(identifier)
4271
+ ];
4272
+ const chunksByName = [
4273
+ ...database.getChunksByName(identifier),
4274
+ ...database.getChunksByNameCi(identifier)
4275
+ ];
4276
+ const normalizedIdentifier = identifier.replace(/_/g, "");
4277
+ const dedupSymbols = /* @__PURE__ */ new Map();
4278
+ for (const symbol of symbols) {
4279
+ dedupSymbols.set(symbol.id, symbol);
4280
+ }
4281
+ for (const symbol of dedupSymbols.values()) {
4282
+ const chunks = database.getChunksByFile(symbol.filePath);
4283
+ for (const chunk of chunks) {
4284
+ if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
4285
+ continue;
4286
+ }
4287
+ upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
4288
+ }
4289
+ }
4290
+ const dedupChunksByName = /* @__PURE__ */ new Map();
4291
+ for (const chunk of chunksByName) {
4292
+ dedupChunksByName.set(chunk.chunkId, chunk);
4293
+ }
4294
+ for (const chunk of dedupChunksByName.values()) {
4295
+ upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
4296
+ }
4297
+ }
4298
+ if (filePathHint && primaryHint) {
4299
+ const primaryChunks = [
4300
+ ...database.getChunksByName(primaryHint),
4301
+ ...database.getChunksByNameCi(primaryHint)
4302
+ ];
4303
+ const dedupPrimaryChunks = /* @__PURE__ */ new Map();
4304
+ for (const chunk of primaryChunks) {
4305
+ dedupPrimaryChunks.set(chunk.chunkId, chunk);
4306
+ }
4307
+ for (const chunk of dedupPrimaryChunks.values()) {
4308
+ if (!pathMatchesHint(chunk.filePath, filePathHint)) {
4309
+ continue;
4310
+ }
4311
+ const normalizedPrimary = primaryHint.replace(/_/g, "");
4312
+ upsertChunkCandidate(chunk, primaryHint, normalizedPrimary, 1);
4313
+ }
4314
+ }
4315
+ const ranked = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4316
+ if (ranked.length === 0) {
4317
+ const implementationFallback = fallbackCandidates.filter(
4318
+ (candidate) => isImplementationChunkType(candidate.metadata.chunkType) && isLikelyImplementationPath(candidate.metadata.filePath)
4319
+ );
4320
+ for (const candidate of implementationFallback) {
4321
+ const nameLower = (candidate.metadata.name ?? "").toLowerCase();
4322
+ const pathLower = candidate.metadata.filePath.toLowerCase();
4323
+ const exactHintMatch = normalizedHints.some((hint) => nameLower === hint || nameLower.replace(/_/g, "") === hint.replace(/_/g, ""));
4324
+ const tokenizedName = tokenizeTextForRanking(nameLower);
4325
+ const tokenHits = codeTermHints.filter((term) => tokenizedName.has(term) || pathLower.includes(term)).length;
4326
+ if (!exactHintMatch && tokenHits === 0) {
4327
+ continue;
4328
+ }
4329
+ 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));
4330
+ symbolCandidates.set(candidate.id, {
4331
+ id: candidate.id,
4332
+ score: laneScore,
4333
+ metadata: candidate.metadata
4334
+ });
4335
+ }
4336
+ if (symbolCandidates.size === 0) {
4337
+ const queryTokenSet = tokenizeTextForRanking(query);
4338
+ const rankedFallback = implementationFallback.map((candidate) => {
4339
+ const nameTokens = tokenizeTextForRanking(candidate.metadata.name ?? "");
4340
+ const pathTokens = splitPathTokens(candidate.metadata.filePath);
4341
+ let overlap = 0;
4342
+ for (const token of queryTokenSet) {
4343
+ if (nameTokens.has(token) || pathTokens.has(token)) {
4344
+ overlap += 1;
4345
+ }
4346
+ }
4347
+ const overlapScore = queryTokenSet.size > 0 ? overlap / queryTokenSet.size : 0;
4348
+ return {
4349
+ candidate,
4350
+ overlapScore
4351
+ };
4352
+ }).filter((entry) => entry.overlapScore > 0).sort((a, b) => b.overlapScore - a.overlapScore || b.candidate.score - a.candidate.score).slice(0, Math.max(limit, 3));
4353
+ for (const entry of rankedFallback) {
4354
+ symbolCandidates.set(entry.candidate.id, {
4355
+ id: entry.candidate.id,
4356
+ score: Math.min(0.94, Math.max(entry.candidate.score, 0.8 + entry.overlapScore * 0.1)),
4357
+ metadata: entry.candidate.metadata
4358
+ });
4359
+ }
4360
+ }
4361
+ }
4362
+ const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4363
+ return withFallback.slice(0, Math.max(limit * 2, limit));
4364
+ }
4365
+ function buildIdentifierDefinitionLane(query, candidates, limit) {
4366
+ if (classifyQueryIntentRaw(query) !== "source") {
4367
+ return [];
4368
+ }
4369
+ const primaryHint = extractPrimaryIdentifierQueryHint(query);
4370
+ if (!primaryHint) {
4371
+ return [];
4372
+ }
4373
+ const hints = [primaryHint, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].slice(0, 8);
4374
+ const scored = candidates.filter(
4375
+ (candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
4376
+ ).map((candidate) => {
4377
+ const matchScore = scoreIdentifierMatch(candidate.metadata.name, candidate.metadata.filePath, hints);
4378
+ return {
4379
+ candidate,
4380
+ matchScore
4381
+ };
4382
+ }).filter((entry) => entry.matchScore > 0).sort((a, b) => {
4383
+ if (b.matchScore !== a.matchScore) return b.matchScore - a.matchScore;
4384
+ if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
4385
+ return a.candidate.id.localeCompare(b.candidate.id);
4386
+ }).slice(0, Math.max(limit * 2, 10));
4387
+ return scored.map((entry) => ({
4388
+ id: entry.candidate.id,
4389
+ score: Math.min(1, 0.9 + entry.matchScore * 0.09),
4390
+ metadata: entry.candidate.metadata
4391
+ }));
4392
+ }
4393
+ function mergeTieredResults(symbolLane, hybridLane, limit) {
4394
+ if (symbolLane.length === 0) {
4395
+ return hybridLane.slice(0, limit);
4396
+ }
4397
+ const out = [];
4398
+ const seen = /* @__PURE__ */ new Set();
4399
+ for (const candidate of symbolLane) {
4400
+ if (seen.has(candidate.id)) continue;
4401
+ out.push(candidate);
4402
+ seen.add(candidate.id);
4403
+ if (out.length >= limit) return out;
4404
+ }
4405
+ for (const candidate of hybridLane) {
4406
+ if (seen.has(candidate.id)) continue;
4407
+ out.push(candidate);
4408
+ seen.add(candidate.id);
4409
+ if (out.length >= limit) return out;
4410
+ }
4411
+ return out;
4412
+ }
4413
+ function unionCandidates(semanticCandidates, keywordCandidates) {
4414
+ const byId = /* @__PURE__ */ new Map();
4415
+ for (const candidate of semanticCandidates) {
4416
+ byId.set(candidate.id, candidate);
4417
+ }
4418
+ for (const candidate of keywordCandidates) {
4419
+ const existing = byId.get(candidate.id);
4420
+ if (!existing || candidate.score > existing.score) {
4421
+ byId.set(candidate.id, candidate);
4422
+ }
4423
+ }
4424
+ return Array.from(byId.values());
4425
+ }
3432
4426
  var Indexer = class {
3433
4427
  config;
3434
4428
  projectRoot;
@@ -3931,6 +4925,79 @@ var Indexer = class {
3931
4925
  if (chunkDataBatch.length > 0) {
3932
4926
  database.upsertChunksBatch(chunkDataBatch);
3933
4927
  }
4928
+ const allSymbolIds = /* @__PURE__ */ new Set();
4929
+ const symbolsByFile = /* @__PURE__ */ new Map();
4930
+ for (let i = 0; i < parsedFiles.length; i++) {
4931
+ const parsed = parsedFiles[i];
4932
+ const changedFile = changedFiles[i];
4933
+ database.deleteCallEdgesByFile(parsed.path);
4934
+ database.deleteSymbolsByFile(parsed.path);
4935
+ const fileSymbols = [];
4936
+ for (const chunk of parsed.chunks) {
4937
+ if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
4938
+ const symbolId = `sym_${hashContent(parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine).slice(0, 16)}`;
4939
+ const symbol = {
4940
+ id: symbolId,
4941
+ filePath: parsed.path,
4942
+ name: chunk.name,
4943
+ kind: chunk.chunkType,
4944
+ startLine: chunk.startLine,
4945
+ startCol: 0,
4946
+ endLine: chunk.endLine,
4947
+ endCol: 0,
4948
+ language: chunk.language
4949
+ };
4950
+ fileSymbols.push(symbol);
4951
+ allSymbolIds.add(symbolId);
4952
+ }
4953
+ const symbolsByName = /* @__PURE__ */ new Map();
4954
+ for (const symbol of fileSymbols) {
4955
+ const existing = symbolsByName.get(symbol.name) ?? [];
4956
+ existing.push(symbol);
4957
+ symbolsByName.set(symbol.name, existing);
4958
+ }
4959
+ if (fileSymbols.length > 0) {
4960
+ database.upsertSymbolsBatch(fileSymbols);
4961
+ symbolsByFile.set(parsed.path, fileSymbols);
4962
+ }
4963
+ const fileLanguage = parsed.chunks[0]?.language;
4964
+ if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) continue;
4965
+ const callSites = extractCalls(changedFile.content, fileLanguage);
4966
+ if (callSites.length === 0) continue;
4967
+ const edges = [];
4968
+ for (const site of callSites) {
4969
+ const enclosingSymbol = fileSymbols.find(
4970
+ (sym) => site.line >= sym.startLine && site.line <= sym.endLine
4971
+ );
4972
+ if (!enclosingSymbol) continue;
4973
+ const edgeId = `edge_${hashContent(enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column).slice(0, 16)}`;
4974
+ edges.push({
4975
+ id: edgeId,
4976
+ fromSymbolId: enclosingSymbol.id,
4977
+ targetName: site.calleeName,
4978
+ toSymbolId: void 0,
4979
+ callType: site.callType,
4980
+ line: site.line,
4981
+ col: site.column,
4982
+ isResolved: false
4983
+ });
4984
+ }
4985
+ if (edges.length > 0) {
4986
+ database.upsertCallEdgesBatch(edges);
4987
+ for (const edge of edges) {
4988
+ const candidates = symbolsByName.get(edge.targetName);
4989
+ if (candidates && candidates.length === 1) {
4990
+ database.resolveCallEdge(edge.id, candidates[0].id);
4991
+ }
4992
+ }
4993
+ }
4994
+ }
4995
+ for (const filePath of unchangedFilePaths) {
4996
+ const existingSymbols = database.getSymbolsByFile(filePath);
4997
+ for (const sym of existingSymbols) {
4998
+ allSymbolIds.add(sym.id);
4999
+ }
5000
+ }
3934
5001
  let removedCount = 0;
3935
5002
  for (const [chunkId] of existingChunks) {
3936
5003
  if (!currentChunkIds.has(chunkId)) {
@@ -3952,6 +5019,8 @@ var Indexer = class {
3952
5019
  if (pendingChunks.length === 0 && removedCount === 0) {
3953
5020
  database.clearBranch(this.currentBranch);
3954
5021
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5022
+ database.clearBranchSymbols(this.currentBranch);
5023
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
3955
5024
  this.fileHashCache = currentFileHashes;
3956
5025
  this.saveFileHashCache();
3957
5026
  stats.durationMs = Date.now() - startTime;
@@ -3968,6 +5037,8 @@ var Indexer = class {
3968
5037
  if (pendingChunks.length === 0) {
3969
5038
  database.clearBranch(this.currentBranch);
3970
5039
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5040
+ database.clearBranchSymbols(this.currentBranch);
5041
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
3971
5042
  store.save();
3972
5043
  invertedIndex.save();
3973
5044
  this.fileHashCache = currentFileHashes;
@@ -4108,6 +5179,8 @@ var Indexer = class {
4108
5179
  });
4109
5180
  database.clearBranch(this.currentBranch);
4110
5181
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5182
+ database.clearBranchSymbols(this.currentBranch);
5183
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
4111
5184
  store.save();
4112
5185
  invertedIndex.save();
4113
5186
  this.fileHashCache = currentFileHashes;
@@ -4218,15 +5291,22 @@ var Indexer = class {
4218
5291
  }
4219
5292
  const maxResults = limit ?? this.config.search.maxResults;
4220
5293
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
5294
+ const fusionStrategy = this.config.search.fusionStrategy;
5295
+ const rrfK = this.config.search.rrfK;
5296
+ const rerankTopN = this.config.search.rerankTopN;
4221
5297
  const filterByBranch = options?.filterByBranch ?? true;
4222
5298
  this.logger.search("debug", "Starting search", {
4223
5299
  query,
4224
5300
  maxResults,
4225
5301
  hybridWeight,
5302
+ fusionStrategy,
5303
+ rrfK,
5304
+ rerankTopN,
4226
5305
  filterByBranch
4227
5306
  });
4228
5307
  const embeddingStartTime = import_perf_hooks.performance.now();
4229
- const embedding = await this.getQueryEmbedding(query, provider);
5308
+ const embeddingQuery = stripFilePathHint(query);
5309
+ const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
4230
5310
  const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
4231
5311
  const vectorStartTime = import_perf_hooks.performance.now();
4232
5312
  const semanticResults = store.search(embedding, maxResults * 4);
@@ -4234,16 +5314,74 @@ var Indexer = class {
4234
5314
  const keywordStartTime = import_perf_hooks.performance.now();
4235
5315
  const keywordResults = await this.keywordSearch(query, maxResults * 4);
4236
5316
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
4237
- const fusionStartTime = import_perf_hooks.performance.now();
4238
- const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
4239
- const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
4240
5317
  let branchChunkIds = null;
4241
5318
  if (filterByBranch && this.currentBranch !== "default") {
4242
5319
  branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
4243
5320
  }
4244
- const filtered = combined.filter((r) => {
5321
+ const prefilterStartTime = import_perf_hooks.performance.now();
5322
+ const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
5323
+ const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
5324
+ const prefilteredKeyword = shouldPrefilterByBranch && branchChunkIds ? keywordResults.filter((r) => branchChunkIds.has(r.id)) : keywordResults;
5325
+ const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
5326
+ const keywordCandidates = shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0 ? keywordResults : prefilteredKeyword;
5327
+ const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
5328
+ if (branchChunkIds && branchChunkIds.size === 0) {
5329
+ this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
5330
+ branch: this.currentBranch
5331
+ });
5332
+ }
5333
+ if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
5334
+ this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
5335
+ branch: this.currentBranch
5336
+ });
5337
+ }
5338
+ if (shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0) {
5339
+ this.logger.search("warn", "Branch prefilter produced no keyword overlap, using unfiltered keyword candidates", {
5340
+ branch: this.currentBranch
5341
+ });
5342
+ }
5343
+ const fusionStartTime = import_perf_hooks.performance.now();
5344
+ const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
5345
+ fusionStrategy,
5346
+ rrfK,
5347
+ rerankTopN,
5348
+ limit: maxResults,
5349
+ hybridWeight
5350
+ });
5351
+ const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
5352
+ const rescued = promoteIdentifierMatches(
5353
+ query,
5354
+ combined,
5355
+ semanticCandidates,
5356
+ keywordCandidates,
5357
+ database,
5358
+ branchChunkIds
5359
+ );
5360
+ const union = unionCandidates(semanticCandidates, keywordCandidates);
5361
+ const deterministicIdentifierLane = buildDeterministicIdentifierPass(
5362
+ query,
5363
+ union,
5364
+ maxResults
5365
+ );
5366
+ const identifierLane = buildIdentifierDefinitionLane(
5367
+ query,
5368
+ union,
5369
+ maxResults
5370
+ );
5371
+ const symbolLane = buildSymbolDefinitionLane(
5372
+ query,
5373
+ database,
5374
+ branchChunkIds,
5375
+ maxResults,
5376
+ union
5377
+ );
5378
+ const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
5379
+ const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
5380
+ const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
5381
+ const sourceIntent = classifyQueryIntentRaw(query) === "source";
5382
+ const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
5383
+ const baseFiltered = tiered.filter((r) => {
4245
5384
  if (r.score < this.config.search.minScore) return false;
4246
- if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
4247
5385
  if (options?.fileType) {
4248
5386
  const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
4249
5387
  if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
@@ -4256,7 +5394,11 @@ var Indexer = class {
4256
5394
  if (r.metadata.chunkType !== options.chunkType) return false;
4257
5395
  }
4258
5396
  return true;
4259
- }).slice(0, maxResults);
5397
+ });
5398
+ const implementationOnly = baseFiltered.filter(
5399
+ (r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
5400
+ );
5401
+ const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
4260
5402
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
4261
5403
  this.logger.recordSearch(totalSearchMs, {
4262
5404
  embeddingMs,
@@ -4271,6 +5413,7 @@ var Indexer = class {
4271
5413
  embeddingMs: Math.round(embeddingMs * 100) / 100,
4272
5414
  vectorMs: Math.round(vectorMs * 100) / 100,
4273
5415
  keywordMs: Math.round(keywordMs * 100) / 100,
5416
+ prefilterMs: Math.round(prefilterMs * 100) / 100,
4274
5417
  fusionMs: Math.round(fusionMs * 100) / 100
4275
5418
  });
4276
5419
  const metadataOnly = options?.metadataOnly ?? false;
@@ -4324,34 +5467,6 @@ var Indexer = class {
4324
5467
  results.sort((a, b) => b.score - a.score);
4325
5468
  return results.slice(0, limit);
4326
5469
  }
4327
- fuseResults(semanticResults, keywordResults, keywordWeight, limit) {
4328
- const semanticWeight = 1 - keywordWeight;
4329
- const fusedScores = /* @__PURE__ */ new Map();
4330
- for (const r of semanticResults) {
4331
- fusedScores.set(r.id, {
4332
- score: r.score * semanticWeight,
4333
- metadata: r.metadata
4334
- });
4335
- }
4336
- for (const r of keywordResults) {
4337
- const existing = fusedScores.get(r.id);
4338
- if (existing) {
4339
- existing.score += r.score * keywordWeight;
4340
- } else {
4341
- fusedScores.set(r.id, {
4342
- score: r.score * keywordWeight,
4343
- metadata: r.metadata
4344
- });
4345
- }
4346
- }
4347
- const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
4348
- id,
4349
- score: data.score,
4350
- metadata: data.metadata
4351
- }));
4352
- results.sort((a, b) => b.score - a.score);
4353
- return results.slice(0, limit);
4354
- }
4355
5470
  async getStatus() {
4356
5471
  const { store, configuredProviderInfo } = await this.ensureInitialized();
4357
5472
  return {
@@ -4374,6 +5489,7 @@ var Indexer = class {
4374
5489
  this.fileHashCache.clear();
4375
5490
  this.saveFileHashCache();
4376
5491
  database.clearBranch(this.currentBranch);
5492
+ database.clearBranchSymbols(this.currentBranch);
4377
5493
  database.deleteMetadata("index.version");
4378
5494
  database.deleteMetadata("index.embeddingProvider");
4379
5495
  database.deleteMetadata("index.embeddingModel");
@@ -4402,6 +5518,8 @@ var Indexer = class {
4402
5518
  removedCount++;
4403
5519
  }
4404
5520
  database.deleteChunksByFile(filePath);
5521
+ database.deleteCallEdgesByFile(filePath);
5522
+ database.deleteSymbolsByFile(filePath);
4405
5523
  removedFilePaths.push(filePath);
4406
5524
  }
4407
5525
  }
@@ -4411,6 +5529,8 @@ var Indexer = class {
4411
5529
  }
4412
5530
  const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
4413
5531
  const gcOrphanChunks = database.gcOrphanChunks();
5532
+ const gcOrphanSymbols = database.gcOrphanSymbols();
5533
+ const gcOrphanCallEdges = database.gcOrphanCallEdges();
4414
5534
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
4415
5535
  this.logger.gc("info", "Health check complete", {
4416
5536
  removedStale: removedCount,
@@ -4418,7 +5538,7 @@ var Indexer = class {
4418
5538
  orphanChunks: gcOrphanChunks,
4419
5539
  removedFiles: removedFilePaths.length
4420
5540
  });
4421
- return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
5541
+ return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
4422
5542
  }
4423
5543
  async retryFailedBatches() {
4424
5544
  const { store, provider, invertedIndex } = await this.ensureInitialized();
@@ -4524,9 +5644,29 @@ var Indexer = class {
4524
5644
  if (filterByBranch && this.currentBranch !== "default") {
4525
5645
  branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
4526
5646
  }
4527
- const filtered = semanticResults.filter((r) => {
5647
+ const prefilterStartTime = import_perf_hooks.performance.now();
5648
+ const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
5649
+ const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
5650
+ const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
5651
+ const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
5652
+ if (branchChunkIds && branchChunkIds.size === 0) {
5653
+ this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
5654
+ branch: this.currentBranch
5655
+ });
5656
+ }
5657
+ if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
5658
+ this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
5659
+ branch: this.currentBranch
5660
+ });
5661
+ }
5662
+ const rerankTopN = this.config.search.rerankTopN;
5663
+ const ranked = rankSemanticOnlyResults(code, semanticCandidates, {
5664
+ rerankTopN,
5665
+ limit,
5666
+ prioritizeSourcePaths: false
5667
+ });
5668
+ const filtered = ranked.filter((r) => {
4528
5669
  if (r.score < this.config.search.minScore) return false;
4529
- if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
4530
5670
  if (options?.excludeFile) {
4531
5671
  if (r.metadata.filePath === options.excludeFile) return false;
4532
5672
  }
@@ -4555,7 +5695,8 @@ var Indexer = class {
4555
5695
  results: filtered.length,
4556
5696
  totalMs: Math.round(totalSearchMs * 100) / 100,
4557
5697
  embeddingMs: Math.round(embeddingMs * 100) / 100,
4558
- vectorMs: Math.round(vectorMs * 100) / 100
5698
+ vectorMs: Math.round(vectorMs * 100) / 100,
5699
+ prefilterMs: Math.round(prefilterMs * 100) / 100
4559
5700
  });
4560
5701
  return Promise.all(
4561
5702
  filtered.map(async (r) => {
@@ -4584,6 +5725,14 @@ var Indexer = class {
4584
5725
  })
4585
5726
  );
4586
5727
  }
5728
+ async getCallers(targetName) {
5729
+ const { database } = await this.ensureInitialized();
5730
+ return database.getCallersWithContext(targetName, this.currentBranch);
5731
+ }
5732
+ async getCallees(symbolId) {
5733
+ const { database } = await this.ensureInitialized();
5734
+ return database.getCallees(symbolId, this.currentBranch);
5735
+ }
4587
5736
  };
4588
5737
 
4589
5738
  // src/mcp-server.ts
@@ -4786,7 +5935,7 @@ Use Read tool to examine specific files.` }] };
4786
5935
  async () => {
4787
5936
  await ensureInitialized();
4788
5937
  const result = await indexer.healthCheck();
4789
- if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
5938
+ if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0 && result.gcOrphanSymbols === 0 && result.gcOrphanCallEdges === 0) {
4790
5939
  return { content: [{ type: "text", text: "Index is healthy. No stale entries found." }] };
4791
5940
  }
4792
5941
  const lines = [`Health check complete:`];
@@ -4799,6 +5948,12 @@ Use Read tool to examine specific files.` }] };
4799
5948
  if (result.gcOrphanChunks > 0) {
4800
5949
  lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
4801
5950
  }
5951
+ if (result.gcOrphanSymbols > 0) {
5952
+ lines.push(` Garbage collected orphan symbols: ${result.gcOrphanSymbols}`);
5953
+ }
5954
+ if (result.gcOrphanCallEdges > 0) {
5955
+ lines.push(` Garbage collected orphan call edges: ${result.gcOrphanCallEdges}`);
5956
+ }
4802
5957
  if (result.filePaths.length > 0) {
4803
5958
  lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
4804
5959
  }
@@ -4887,6 +6042,43 @@ ${truncateContent(r.content)}
4887
6042
  ${formatted.join("\n\n")}` }] };
4888
6043
  }
4889
6044
  );
6045
+ server.tool(
6046
+ "call_graph",
6047
+ "Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies.",
6048
+ {
6049
+ name: import_zod.z.string().describe("Function or method name to query"),
6050
+ direction: import_zod.z.enum(["callers", "callees"]).default("callers").describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
6051
+ symbolId: import_zod.z.string().optional().describe("Symbol ID (required for 'callees' direction)")
6052
+ },
6053
+ async (args) => {
6054
+ await ensureInitialized();
6055
+ if (args.direction === "callees") {
6056
+ if (!args.symbolId) {
6057
+ return { content: [{ type: "text", text: "Error: 'symbolId' is required when direction is 'callees'." }] };
6058
+ }
6059
+ const callees = await indexer.getCallees(args.symbolId);
6060
+ if (callees.length === 0) {
6061
+ return { content: [{ type: "text", text: `No callees found for symbol ${args.symbolId}.` }] };
6062
+ }
6063
+ const formatted2 = callees.map(
6064
+ (e, i) => `[${i + 1}] \u2192 ${e.targetName} (${e.callType}) at line ${e.line}${e.isResolved ? ` [resolved: ${e.toSymbolId}]` : " [unresolved]"}`
6065
+ );
6066
+ return { content: [{ type: "text", text: `Callees (${callees.length}):
6067
+
6068
+ ${formatted2.join("\n")}` }] };
6069
+ }
6070
+ const callers = await indexer.getCallers(args.name);
6071
+ if (callers.length === 0) {
6072
+ return { content: [{ type: "text", text: `No callers found for "${args.name}".` }] };
6073
+ }
6074
+ const formatted = callers.map(
6075
+ (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]"}`
6076
+ );
6077
+ return { content: [{ type: "text", text: `"${args.name}" is called by ${callers.length} function(s):
6078
+
6079
+ ${formatted.join("\n")}` }] };
6080
+ }
6081
+ );
4890
6082
  server.prompt(
4891
6083
  "search",
4892
6084
  "Search codebase by meaning using semantic search",