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/index.cjs CHANGED
@@ -785,9 +785,15 @@ function getDefaultSearchConfig() {
785
785
  minScore: 0.1,
786
786
  includeContext: true,
787
787
  hybridWeight: 0.5,
788
+ fusionStrategy: "rrf",
789
+ rrfK: 60,
790
+ rerankTopN: 20,
788
791
  contextLines: 0
789
792
  };
790
793
  }
794
+ function isValidFusionStrategy(value) {
795
+ return value === "weighted" || value === "rrf";
796
+ }
791
797
  function getDefaultDebugConfig() {
792
798
  return {
793
799
  enabled: false,
@@ -842,6 +848,9 @@ function parseConfig(raw) {
842
848
  minScore: typeof rawSearch.minScore === "number" ? rawSearch.minScore : defaultSearch.minScore,
843
849
  includeContext: typeof rawSearch.includeContext === "boolean" ? rawSearch.includeContext : defaultSearch.includeContext,
844
850
  hybridWeight: typeof rawSearch.hybridWeight === "number" ? Math.min(1, Math.max(0, rawSearch.hybridWeight)) : defaultSearch.hybridWeight,
851
+ fusionStrategy: isValidFusionStrategy(rawSearch.fusionStrategy) ? rawSearch.fusionStrategy : defaultSearch.fusionStrategy,
852
+ rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
853
+ rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
845
854
  contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines
846
855
  };
847
856
  const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
@@ -2977,6 +2986,9 @@ function hashContent(content) {
2977
2986
  function hashFile(filePath) {
2978
2987
  return native.hashFile(filePath);
2979
2988
  }
2989
+ function extractCalls(content, language) {
2990
+ return native.extractCalls(content, language);
2991
+ }
2980
2992
  var VectorStore = class {
2981
2993
  inner;
2982
2994
  dimensions;
@@ -3308,6 +3320,12 @@ var Database = class {
3308
3320
  getChunksByFile(filePath) {
3309
3321
  return this.inner.getChunksByFile(filePath);
3310
3322
  }
3323
+ getChunksByName(name) {
3324
+ return this.inner.getChunksByName(name);
3325
+ }
3326
+ getChunksByNameCi(name) {
3327
+ return this.inner.getChunksByNameCi(name);
3328
+ }
3311
3329
  deleteChunksByFile(filePath) {
3312
3330
  return this.inner.deleteChunksByFile(filePath);
3313
3331
  }
@@ -3351,6 +3369,73 @@ var Database = class {
3351
3369
  getStats() {
3352
3370
  return this.inner.getStats();
3353
3371
  }
3372
+ // ── Symbol methods ──────────────────────────────────────────────
3373
+ upsertSymbol(symbol) {
3374
+ this.inner.upsertSymbol(symbol);
3375
+ }
3376
+ upsertSymbolsBatch(symbols) {
3377
+ if (symbols.length === 0) return;
3378
+ this.inner.upsertSymbolsBatch(symbols);
3379
+ }
3380
+ getSymbolsByFile(filePath) {
3381
+ return this.inner.getSymbolsByFile(filePath);
3382
+ }
3383
+ getSymbolByName(name, filePath) {
3384
+ return this.inner.getSymbolByName(name, filePath) ?? null;
3385
+ }
3386
+ getSymbolsByName(name) {
3387
+ return this.inner.getSymbolsByName(name);
3388
+ }
3389
+ getSymbolsByNameCi(name) {
3390
+ return this.inner.getSymbolsByNameCi(name);
3391
+ }
3392
+ deleteSymbolsByFile(filePath) {
3393
+ return this.inner.deleteSymbolsByFile(filePath);
3394
+ }
3395
+ // ── Call Edge methods ────────────────────────────────────────────
3396
+ upsertCallEdge(edge) {
3397
+ this.inner.upsertCallEdge(edge);
3398
+ }
3399
+ upsertCallEdgesBatch(edges) {
3400
+ if (edges.length === 0) return;
3401
+ this.inner.upsertCallEdgesBatch(edges);
3402
+ }
3403
+ getCallers(targetName, branch) {
3404
+ return this.inner.getCallers(targetName, branch);
3405
+ }
3406
+ getCallersWithContext(targetName, branch) {
3407
+ return this.inner.getCallersWithContext(targetName, branch);
3408
+ }
3409
+ getCallees(symbolId, branch) {
3410
+ return this.inner.getCallees(symbolId, branch);
3411
+ }
3412
+ deleteCallEdgesByFile(filePath) {
3413
+ return this.inner.deleteCallEdgesByFile(filePath);
3414
+ }
3415
+ resolveCallEdge(edgeId, toSymbolId) {
3416
+ this.inner.resolveCallEdge(edgeId, toSymbolId);
3417
+ }
3418
+ // ── Branch Symbol methods ────────────────────────────────────────
3419
+ addSymbolsToBranch(branch, symbolIds) {
3420
+ this.inner.addSymbolsToBranch(branch, symbolIds);
3421
+ }
3422
+ addSymbolsToBranchBatch(branch, symbolIds) {
3423
+ if (symbolIds.length === 0) return;
3424
+ this.inner.addSymbolsToBranchBatch(branch, symbolIds);
3425
+ }
3426
+ getBranchSymbolIds(branch) {
3427
+ return this.inner.getBranchSymbolIds(branch);
3428
+ }
3429
+ clearBranchSymbols(branch) {
3430
+ return this.inner.clearBranchSymbols(branch);
3431
+ }
3432
+ // ── GC methods for symbols/edges ─────────────────────────────────
3433
+ gcOrphanSymbols() {
3434
+ return this.inner.gcOrphanSymbols();
3435
+ }
3436
+ gcOrphanCallEdges() {
3437
+ return this.inner.gcOrphanCallEdges();
3438
+ }
3354
3439
  };
3355
3440
 
3356
3441
  // src/git/index.ts
@@ -3458,6 +3543,29 @@ function getHeadPath(repoRoot) {
3458
3543
  }
3459
3544
 
3460
3545
  // src/indexer/index.ts
3546
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust"]);
3547
+ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
3548
+ "function_declaration",
3549
+ "function",
3550
+ "arrow_function",
3551
+ "method_definition",
3552
+ "class_declaration",
3553
+ "interface_declaration",
3554
+ "type_alias_declaration",
3555
+ "enum_declaration",
3556
+ "function_definition",
3557
+ "class_definition",
3558
+ "decorated_definition",
3559
+ "method_declaration",
3560
+ "type_declaration",
3561
+ "type_spec",
3562
+ "function_item",
3563
+ "impl_item",
3564
+ "struct_item",
3565
+ "enum_item",
3566
+ "trait_item",
3567
+ "mod_item"
3568
+ ]);
3461
3569
  function float32ArrayToBuffer(arr) {
3462
3570
  const float32 = new Float32Array(arr);
3463
3571
  return Buffer.from(float32.buffer);
@@ -3482,6 +3590,892 @@ function isRateLimitError(error) {
3482
3590
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
3483
3591
  }
3484
3592
  var INDEX_METADATA_VERSION = "1";
3593
+ var RANKING_TOKEN_CACHE_LIMIT = 4096;
3594
+ var rankingQueryTokenCache = /* @__PURE__ */ new Map();
3595
+ var rankingNameTokenCache = /* @__PURE__ */ new Map();
3596
+ var rankingPathTokenCache = /* @__PURE__ */ new Map();
3597
+ var rankingTextTokenCache = /* @__PURE__ */ new Map();
3598
+ var STOPWORDS = /* @__PURE__ */ new Set([
3599
+ "the",
3600
+ "and",
3601
+ "for",
3602
+ "with",
3603
+ "from",
3604
+ "that",
3605
+ "this",
3606
+ "into",
3607
+ "using",
3608
+ "where",
3609
+ "what",
3610
+ "when",
3611
+ "why",
3612
+ "how",
3613
+ "are",
3614
+ "was",
3615
+ "were",
3616
+ "be",
3617
+ "been",
3618
+ "being",
3619
+ "find",
3620
+ "show",
3621
+ "get",
3622
+ "run",
3623
+ "use",
3624
+ "code",
3625
+ "function",
3626
+ "implementation",
3627
+ "retrieve",
3628
+ "results",
3629
+ "result",
3630
+ "search",
3631
+ "pipeline",
3632
+ "top",
3633
+ "in",
3634
+ "on",
3635
+ "of",
3636
+ "to",
3637
+ "by",
3638
+ "as",
3639
+ "or",
3640
+ "an",
3641
+ "a"
3642
+ ]);
3643
+ var TEST_PATH_SEGMENTS = [
3644
+ "tests/",
3645
+ "__tests__/",
3646
+ "/test/",
3647
+ "fixtures/",
3648
+ "benchmark",
3649
+ "README",
3650
+ "ARCHITECTURE",
3651
+ "docs/"
3652
+ ];
3653
+ var IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS = [
3654
+ "tests/",
3655
+ "__tests__/",
3656
+ "/test/",
3657
+ "fixtures/",
3658
+ "benchmark",
3659
+ "readme",
3660
+ "architecture",
3661
+ "docs/",
3662
+ "examples/",
3663
+ "example/",
3664
+ ".github/",
3665
+ "/scripts/",
3666
+ "/migrations/",
3667
+ "/generated/"
3668
+ ];
3669
+ var SOURCE_INTENT_HINTS = /* @__PURE__ */ new Set([
3670
+ "implement",
3671
+ "implementation",
3672
+ "function",
3673
+ "method",
3674
+ "class",
3675
+ "logic",
3676
+ "algorithm",
3677
+ "pipeline",
3678
+ "indexer",
3679
+ "where"
3680
+ ]);
3681
+ var DOC_TEST_INTENT_HINTS = /* @__PURE__ */ new Set([
3682
+ "test",
3683
+ "tests",
3684
+ "fixture",
3685
+ "fixtures",
3686
+ "benchmark",
3687
+ "readme",
3688
+ "docs",
3689
+ "documentation"
3690
+ ]);
3691
+ var DOC_INTENT_HINTS = /* @__PURE__ */ new Set([
3692
+ "readme",
3693
+ "docs",
3694
+ "documentation",
3695
+ "guide",
3696
+ "usage"
3697
+ ]);
3698
+ function setBoundedCache(cache, key, value) {
3699
+ if (cache.size >= RANKING_TOKEN_CACHE_LIMIT) {
3700
+ const oldest = cache.keys().next().value;
3701
+ if (oldest !== void 0) {
3702
+ cache.delete(oldest);
3703
+ }
3704
+ }
3705
+ cache.set(key, value);
3706
+ }
3707
+ function tokenizeTextForRanking(text) {
3708
+ if (!text) {
3709
+ return /* @__PURE__ */ new Set();
3710
+ }
3711
+ const lowered = text.toLowerCase();
3712
+ const cache = rankingQueryTokenCache.get(lowered) ?? rankingTextTokenCache.get(lowered);
3713
+ if (cache) {
3714
+ return cache;
3715
+ }
3716
+ const tokens = new Set(
3717
+ lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1 && !STOPWORDS.has(token))
3718
+ );
3719
+ setBoundedCache(rankingQueryTokenCache, lowered, tokens);
3720
+ setBoundedCache(rankingTextTokenCache, lowered, tokens);
3721
+ return tokens;
3722
+ }
3723
+ function splitPathTokens(filePath) {
3724
+ const lowered = filePath.toLowerCase();
3725
+ const cache = rankingPathTokenCache.get(lowered);
3726
+ if (cache) {
3727
+ return cache;
3728
+ }
3729
+ const normalized = lowered.replace(/[^a-z0-9/._-]/g, " ").split(/[/._-]+/).filter((token) => token.length > 1);
3730
+ const tokens = new Set(normalized);
3731
+ setBoundedCache(rankingPathTokenCache, lowered, tokens);
3732
+ return tokens;
3733
+ }
3734
+ function splitNameTokens(name) {
3735
+ if (!name) {
3736
+ return /* @__PURE__ */ new Set();
3737
+ }
3738
+ const lowered = name.toLowerCase();
3739
+ const cache = rankingNameTokenCache.get(lowered);
3740
+ if (cache) {
3741
+ return cache;
3742
+ }
3743
+ const tokens = new Set(
3744
+ lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1)
3745
+ );
3746
+ setBoundedCache(rankingNameTokenCache, lowered, tokens);
3747
+ return tokens;
3748
+ }
3749
+ function chunkTypeBoost(chunkType) {
3750
+ switch (chunkType) {
3751
+ case "function":
3752
+ case "function_declaration":
3753
+ case "method":
3754
+ case "method_definition":
3755
+ case "class":
3756
+ case "class_declaration":
3757
+ return 0.2;
3758
+ case "interface":
3759
+ case "type":
3760
+ case "enum":
3761
+ case "struct":
3762
+ case "impl":
3763
+ case "trait":
3764
+ case "module":
3765
+ return 0.1;
3766
+ default:
3767
+ return 0;
3768
+ }
3769
+ }
3770
+ function isTestOrDocPath(filePath) {
3771
+ return TEST_PATH_SEGMENTS.some((segment) => filePath.includes(segment));
3772
+ }
3773
+ function isLikelyImplementationPath(filePath) {
3774
+ const lowered = filePath.toLowerCase();
3775
+ if (IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS.some((segment) => lowered.includes(segment))) {
3776
+ return false;
3777
+ }
3778
+ const ext = lowered.split(".").pop() ?? "";
3779
+ if (["md", "mdx", "txt", "rst", "adoc", "snap", "json", "yaml", "yml", "lock"].includes(ext)) {
3780
+ return false;
3781
+ }
3782
+ return true;
3783
+ }
3784
+ function classifyQueryIntent(tokens) {
3785
+ const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
3786
+ const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
3787
+ return sourceIntentHits >= docTestIntentHits ? "source" : "doc_test";
3788
+ }
3789
+ function classifyQueryIntentRaw(query) {
3790
+ const lowerQuery = query.toLowerCase();
3791
+ const docTestRawHits = Array.from(DOC_TEST_INTENT_HINTS).filter(
3792
+ (hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)
3793
+ ).length;
3794
+ const sourceRawHits = [
3795
+ "implement",
3796
+ "implementation",
3797
+ "implements",
3798
+ "function",
3799
+ "method",
3800
+ "class",
3801
+ "logic",
3802
+ "algorithm",
3803
+ "pipeline",
3804
+ "indexer"
3805
+ ].filter((hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)).length;
3806
+ if (docTestRawHits > sourceRawHits) {
3807
+ return "doc_test";
3808
+ }
3809
+ if (sourceRawHits > docTestRawHits) {
3810
+ return "source";
3811
+ }
3812
+ const hasWhereIsPattern = /\bwhere\s+is\b/.test(lowerQuery);
3813
+ const hasIdentifierHints = extractIdentifierHints(query).length > 0;
3814
+ if (hasWhereIsPattern && hasIdentifierHints && docTestRawHits === 0) {
3815
+ return "source";
3816
+ }
3817
+ const queryTokens = Array.from(tokenizeTextForRanking(query));
3818
+ return classifyQueryIntent(queryTokens);
3819
+ }
3820
+ function classifyDocIntent(tokens) {
3821
+ const docHits = tokens.filter((t) => DOC_INTENT_HINTS.has(t)).length;
3822
+ const testHits = tokens.filter((t) => ["test", "tests", "fixture", "fixtures", "benchmark"].includes(t)).length;
3823
+ if (docHits > 0 && testHits === 0) return "docs";
3824
+ if (testHits > 0 && docHits === 0) return "test";
3825
+ if (testHits > 0 || docHits > 0) return "mixed";
3826
+ return "none";
3827
+ }
3828
+ function isImplementationChunkType(chunkType) {
3829
+ return [
3830
+ "export_statement",
3831
+ "function",
3832
+ "function_declaration",
3833
+ "method",
3834
+ "method_definition",
3835
+ "class",
3836
+ "class_declaration",
3837
+ "interface",
3838
+ "type",
3839
+ "enum",
3840
+ "module"
3841
+ ].includes(chunkType);
3842
+ }
3843
+ function extractIdentifierHints(query) {
3844
+ const identifiers = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
3845
+ return identifiers.filter((id) => id.length >= 3).filter((id) => {
3846
+ const lower = id.toLowerCase();
3847
+ if (STOPWORDS.has(lower)) return false;
3848
+ return /[A-Z]/.test(id) || id.includes("_") || id.endsWith("Results") || id.endsWith("Result");
3849
+ }).map((id) => id.toLowerCase());
3850
+ }
3851
+ function extractCodeTermHints(query) {
3852
+ const terms = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
3853
+ return terms.map((term) => term.toLowerCase()).filter((term) => term.length >= 3).filter((term) => !STOPWORDS.has(term));
3854
+ }
3855
+ function normalizeIdentifierVariants(identifier) {
3856
+ const lower = identifier.toLowerCase();
3857
+ const compact = lower.replace(/[^a-z0-9]/g, "");
3858
+ const snake = compact.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
3859
+ const kebab = snake.replace(/_/g, "-");
3860
+ const variants = [lower, compact, snake, kebab].filter((value) => value.length > 0);
3861
+ return Array.from(new Set(variants));
3862
+ }
3863
+ function scoreIdentifierMatch(name, filePath, hints) {
3864
+ const nameLower = (name ?? "").toLowerCase();
3865
+ const pathLower = filePath.toLowerCase();
3866
+ let best = 0;
3867
+ for (const hint of hints) {
3868
+ const variants = normalizeIdentifierVariants(hint);
3869
+ for (const variant of variants) {
3870
+ if (nameLower === variant) {
3871
+ best = Math.max(best, 1);
3872
+ } else if (nameLower.includes(variant)) {
3873
+ best = Math.max(best, 0.8);
3874
+ } else if (pathLower.includes(variant)) {
3875
+ best = Math.max(best, 0.6);
3876
+ }
3877
+ }
3878
+ }
3879
+ return best;
3880
+ }
3881
+ function extractPrimaryIdentifierQueryHint(query) {
3882
+ const identifiers = extractIdentifierHints(query);
3883
+ if (identifiers.length > 0) {
3884
+ return identifiers[0] ?? null;
3885
+ }
3886
+ const codeTerms = extractCodeTermHints(query);
3887
+ const best = codeTerms.find((term) => term.length >= 6);
3888
+ return best ?? null;
3889
+ }
3890
+ var FILE_PATH_HINT_EXTENSIONS = [
3891
+ "ts",
3892
+ "tsx",
3893
+ "js",
3894
+ "jsx",
3895
+ "mjs",
3896
+ "cjs",
3897
+ "mts",
3898
+ "cts",
3899
+ "py",
3900
+ "rs",
3901
+ "go",
3902
+ "java",
3903
+ "kt",
3904
+ "kts",
3905
+ "swift",
3906
+ "rb",
3907
+ "php",
3908
+ "c",
3909
+ "h",
3910
+ "cc",
3911
+ "cpp",
3912
+ "cxx",
3913
+ "hpp",
3914
+ "cs",
3915
+ "scala",
3916
+ "lua",
3917
+ "sh",
3918
+ "bash",
3919
+ "zsh",
3920
+ "json",
3921
+ "yaml",
3922
+ "yml",
3923
+ "toml"
3924
+ ];
3925
+ var FILE_PATH_HINT_SUFFIX_REGEX = new RegExp(
3926
+ "\\s+\\bin\\s+[\"'`]?((?:\\.\\/)?(?:[A-Za-z0-9._-]+\\/)+[A-Za-z0-9._-]+\\.(?:" + FILE_PATH_HINT_EXTENSIONS.join("|") + "))[\"'`]?[\\])}>.,;!?]*\\s*$",
3927
+ "i"
3928
+ );
3929
+ function normalizeFilePathForHintMatch(filePath) {
3930
+ return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
3931
+ }
3932
+ function pathMatchesHint(filePath, hint) {
3933
+ const normalizedPath = normalizeFilePathForHintMatch(filePath);
3934
+ const normalizedHint = normalizeFilePathForHintMatch(hint);
3935
+ return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
3936
+ }
3937
+ function extractFilePathHint(query) {
3938
+ const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
3939
+ const rawPath = match?.[1];
3940
+ if (!rawPath) {
3941
+ return null;
3942
+ }
3943
+ return rawPath.replace(/^\.\//, "");
3944
+ }
3945
+ function stripFilePathHint(query) {
3946
+ const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
3947
+ return stripped.length > 0 ? stripped : query;
3948
+ }
3949
+ function buildDeterministicIdentifierPass(query, candidates, limit) {
3950
+ if (classifyQueryIntentRaw(query) !== "source") {
3951
+ return [];
3952
+ }
3953
+ const primary = extractPrimaryIdentifierQueryHint(query);
3954
+ if (!primary) {
3955
+ return [];
3956
+ }
3957
+ const filePathHint = extractFilePathHint(query);
3958
+ const primaryVariants = normalizeIdentifierVariants(primary);
3959
+ const hints = [primary, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].map((value) => value.toLowerCase()).filter((value, idx, arr) => value.length >= 3 && arr.indexOf(value) === idx).slice(0, 8);
3960
+ const deterministic = candidates.filter(
3961
+ (candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
3962
+ ).map((candidate) => {
3963
+ const nameLower = (candidate.metadata.name ?? "").toLowerCase();
3964
+ const pathLower = candidate.metadata.filePath.toLowerCase();
3965
+ let maxMatch = 0;
3966
+ const nameMatchesPrimary = primaryVariants.some(
3967
+ (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
3968
+ );
3969
+ const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
3970
+ for (const hint of hints) {
3971
+ const variants = normalizeIdentifierVariants(hint);
3972
+ for (const variant of variants) {
3973
+ if (nameLower === variant) {
3974
+ maxMatch = Math.max(maxMatch, 1);
3975
+ } else if (nameLower.includes(variant)) {
3976
+ maxMatch = Math.max(maxMatch, 0.85);
3977
+ } else if (pathLower.includes(variant)) {
3978
+ maxMatch = Math.max(maxMatch, 0.7);
3979
+ }
3980
+ }
3981
+ }
3982
+ if (pathMatchesFileHint && nameMatchesPrimary) {
3983
+ maxMatch = Math.max(maxMatch, 1);
3984
+ }
3985
+ return {
3986
+ candidate,
3987
+ maxMatch,
3988
+ pathMatchesFileHint,
3989
+ nameMatchesPrimary
3990
+ };
3991
+ }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
3992
+ const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
3993
+ const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
3994
+ if (aAnchored !== bAnchored) return bAnchored - aAnchored;
3995
+ if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
3996
+ if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
3997
+ return a.candidate.id.localeCompare(b.candidate.id);
3998
+ }).slice(0, Math.max(limit * 2, 12));
3999
+ return deterministic.map((entry) => ({
4000
+ id: entry.candidate.id,
4001
+ score: entry.pathMatchesFileHint && entry.nameMatchesPrimary ? 0.995 : Math.min(1, 0.9 + entry.maxMatch * 0.09),
4002
+ metadata: entry.candidate.metadata
4003
+ }));
4004
+ }
4005
+ function fuseResultsWeighted(semanticResults, keywordResults, keywordWeight, limit) {
4006
+ const semanticWeight = 1 - keywordWeight;
4007
+ const fusedScores = /* @__PURE__ */ new Map();
4008
+ for (const r of semanticResults) {
4009
+ fusedScores.set(r.id, {
4010
+ score: r.score * semanticWeight,
4011
+ metadata: r.metadata
4012
+ });
4013
+ }
4014
+ for (const r of keywordResults) {
4015
+ const existing = fusedScores.get(r.id);
4016
+ if (existing) {
4017
+ existing.score += r.score * keywordWeight;
4018
+ } else {
4019
+ fusedScores.set(r.id, {
4020
+ score: r.score * keywordWeight,
4021
+ metadata: r.metadata
4022
+ });
4023
+ }
4024
+ }
4025
+ const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
4026
+ id,
4027
+ score: data.score,
4028
+ metadata: data.metadata
4029
+ }));
4030
+ results.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4031
+ return results.slice(0, limit);
4032
+ }
4033
+ function fuseResultsRrf(semanticResults, keywordResults, rrfK, limit) {
4034
+ const maxPossibleRaw = 2 / (rrfK + 1);
4035
+ const rankByIdSemantic = /* @__PURE__ */ new Map();
4036
+ const rankByIdKeyword = /* @__PURE__ */ new Map();
4037
+ const metadataById = /* @__PURE__ */ new Map();
4038
+ semanticResults.forEach((result, index) => {
4039
+ rankByIdSemantic.set(result.id, index + 1);
4040
+ metadataById.set(result.id, result.metadata);
4041
+ });
4042
+ keywordResults.forEach((result, index) => {
4043
+ rankByIdKeyword.set(result.id, index + 1);
4044
+ if (!metadataById.has(result.id)) {
4045
+ metadataById.set(result.id, result.metadata);
4046
+ }
4047
+ });
4048
+ const allIds = /* @__PURE__ */ new Set([...rankByIdSemantic.keys(), ...rankByIdKeyword.keys()]);
4049
+ const fused = [];
4050
+ for (const id of allIds) {
4051
+ const semanticRank = rankByIdSemantic.get(id);
4052
+ const keywordRank = rankByIdKeyword.get(id);
4053
+ const semanticScore = semanticRank ? 1 / (rrfK + semanticRank) : 0;
4054
+ const keywordScore = keywordRank ? 1 / (rrfK + keywordRank) : 0;
4055
+ const metadata = metadataById.get(id);
4056
+ if (!metadata) continue;
4057
+ fused.push({
4058
+ id,
4059
+ score: maxPossibleRaw > 0 ? (semanticScore + keywordScore) / maxPossibleRaw : 0,
4060
+ metadata
4061
+ });
4062
+ }
4063
+ fused.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4064
+ return fused.slice(0, limit);
4065
+ }
4066
+ function rerankResults(query, candidates, rerankTopN, options) {
4067
+ if (rerankTopN <= 0 || candidates.length <= 1) {
4068
+ return candidates;
4069
+ }
4070
+ const topN = Math.min(rerankTopN, candidates.length);
4071
+ const queryTokens = tokenizeTextForRanking(query);
4072
+ if (queryTokens.size === 0) {
4073
+ return candidates;
4074
+ }
4075
+ const queryTokenList = Array.from(queryTokens);
4076
+ const intent = classifyQueryIntentRaw(query);
4077
+ const docIntent = classifyDocIntent(queryTokenList);
4078
+ const preferSourcePaths = options?.prioritizeSourcePaths ?? intent === "source";
4079
+ const identifierHints = extractIdentifierHints(query);
4080
+ const head = candidates.slice(0, topN).map((candidate, idx) => {
4081
+ const pathTokens = splitPathTokens(candidate.metadata.filePath);
4082
+ const nameTokens = splitNameTokens(candidate.metadata.name ?? "");
4083
+ const chunkTypeTokens = tokenizeTextForRanking(candidate.metadata.chunkType);
4084
+ let exactOrPrefixNameHits = 0;
4085
+ let pathOverlap = 0;
4086
+ let chunkTypeHits = 0;
4087
+ for (const token of queryTokenList) {
4088
+ if (nameTokens.has(token)) {
4089
+ exactOrPrefixNameHits += 1;
4090
+ } else {
4091
+ for (const nameToken of nameTokens) {
4092
+ if (nameToken.startsWith(token) || token.startsWith(nameToken)) {
4093
+ exactOrPrefixNameHits += 1;
4094
+ break;
4095
+ }
4096
+ }
4097
+ }
4098
+ if (pathTokens.has(token)) {
4099
+ pathOverlap += 1;
4100
+ }
4101
+ if (chunkTypeTokens.has(token)) {
4102
+ chunkTypeHits += 1;
4103
+ }
4104
+ }
4105
+ const likelyTestOrDoc = isTestOrDocPath(candidate.metadata.filePath);
4106
+ const lowerPath = candidate.metadata.filePath.toLowerCase();
4107
+ const lowerName = (candidate.metadata.name ?? "").toLowerCase();
4108
+ const hasIdentifierMatch = identifierHints.some((id) => lowerPath.includes(id) || lowerName.includes(id));
4109
+ const implementationPathBoost = preferSourcePaths && isLikelyImplementationPath(candidate.metadata.filePath) ? 0.08 : 0;
4110
+ const isReadmePath = candidate.metadata.filePath.toLowerCase().includes("readme");
4111
+ const testDocPenalty = preferSourcePaths && likelyTestOrDoc ? 0.12 : 0;
4112
+ const readmeDocBoost = !preferSourcePaths && isReadmePath ? 0.08 : 0;
4113
+ const identifierBoost = hasIdentifierMatch ? 0.12 : 0;
4114
+ const tokenCoverage = queryTokenList.length > 0 ? (exactOrPrefixNameHits + pathOverlap + chunkTypeHits) / queryTokenList.length : 0;
4115
+ const coverageBoost = Math.min(0.12, tokenCoverage * 0.06);
4116
+ const deterministicBoost = exactOrPrefixNameHits * 0.08 + pathOverlap * 0.03 + chunkTypeHits * 0.02 + coverageBoost + identifierBoost + implementationPathBoost - testDocPenalty + readmeDocBoost + chunkTypeBoost(candidate.metadata.chunkType);
4117
+ return {
4118
+ candidate,
4119
+ boostedScore: candidate.score + deterministicBoost,
4120
+ originalIndex: idx,
4121
+ hasIdentifierMatch,
4122
+ implementationChunk: isImplementationChunkType(candidate.metadata.chunkType),
4123
+ isLikelyImplementationPath: isLikelyImplementationPath(candidate.metadata.filePath),
4124
+ isTestOrDocPath: likelyTestOrDoc,
4125
+ isReadmePath
4126
+ };
4127
+ });
4128
+ head.sort((a, b) => {
4129
+ if (b.boostedScore !== a.boostedScore) return b.boostedScore - a.boostedScore;
4130
+ if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
4131
+ if (a.originalIndex !== b.originalIndex) return a.originalIndex - b.originalIndex;
4132
+ return a.candidate.id.localeCompare(b.candidate.id);
4133
+ });
4134
+ if (preferSourcePaths) {
4135
+ head.sort((a, b) => {
4136
+ const aId = a.hasIdentifierMatch ? 1 : 0;
4137
+ const bId = b.hasIdentifierMatch ? 1 : 0;
4138
+ if (aId !== bId) return bId - aId;
4139
+ const aImpl = a.implementationChunk ? 1 : 0;
4140
+ const bImpl = b.implementationChunk ? 1 : 0;
4141
+ if (aImpl !== bImpl) return bImpl - aImpl;
4142
+ const aImplementationPath = a.isLikelyImplementationPath ? 1 : 0;
4143
+ const bImplementationPath = b.isLikelyImplementationPath ? 1 : 0;
4144
+ if (aImplementationPath !== bImplementationPath) return bImplementationPath - aImplementationPath;
4145
+ const aTestDoc = a.isTestOrDocPath ? 1 : 0;
4146
+ const bTestDoc = b.isTestOrDocPath ? 1 : 0;
4147
+ if (aTestDoc !== bTestDoc) return aTestDoc - bTestDoc;
4148
+ return 0;
4149
+ });
4150
+ } else if (docIntent === "docs") {
4151
+ head.sort((a, b) => {
4152
+ const aReadme = a.isReadmePath ? 1 : 0;
4153
+ const bReadme = b.isReadmePath ? 1 : 0;
4154
+ if (aReadme !== bReadme) return bReadme - aReadme;
4155
+ return 0;
4156
+ });
4157
+ }
4158
+ const tail = candidates.slice(topN);
4159
+ return [...head.map((entry) => entry.candidate), ...tail];
4160
+ }
4161
+ function rankHybridResults(query, semanticResults, keywordResults, options) {
4162
+ const overfetchLimit = Math.max(options.limit * 4, options.limit);
4163
+ const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
4164
+ const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
4165
+ const rerankPool = fused.slice(0, rerankPoolLimit);
4166
+ const intent = classifyQueryIntentRaw(query);
4167
+ return rerankResults(query, rerankPool, options.rerankTopN, {
4168
+ prioritizeSourcePaths: intent === "source"
4169
+ });
4170
+ }
4171
+ function rankSemanticOnlyResults(query, semanticResults, options) {
4172
+ const overfetchLimit = Math.max(options.limit * 4, options.limit);
4173
+ const bounded = semanticResults.slice(0, overfetchLimit);
4174
+ return rerankResults(query, bounded, options.rerankTopN, {
4175
+ prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
4176
+ });
4177
+ }
4178
+ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds) {
4179
+ if (combined.length === 0) {
4180
+ return combined;
4181
+ }
4182
+ if (classifyQueryIntentRaw(query) !== "source") {
4183
+ return combined;
4184
+ }
4185
+ const identifierHints = extractIdentifierHints(query);
4186
+ if (identifierHints.length === 0) {
4187
+ return combined;
4188
+ }
4189
+ const combinedById = new Map(combined.map((candidate) => [candidate.id, candidate]));
4190
+ const candidateUnion = /* @__PURE__ */ new Map();
4191
+ for (const candidate of semanticCandidates) {
4192
+ candidateUnion.set(candidate.id, candidate);
4193
+ }
4194
+ for (const candidate of keywordCandidates) {
4195
+ if (!candidateUnion.has(candidate.id)) {
4196
+ candidateUnion.set(candidate.id, candidate);
4197
+ }
4198
+ }
4199
+ if (database) {
4200
+ for (const identifier of identifierHints) {
4201
+ const symbols = database.getSymbolsByName(identifier);
4202
+ for (const symbol of symbols) {
4203
+ const chunks = database.getChunksByFile(symbol.filePath);
4204
+ for (const chunk of chunks) {
4205
+ if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
4206
+ continue;
4207
+ }
4208
+ const chunkType = chunk.nodeType ?? "other";
4209
+ if (!isImplementationChunkType(chunkType)) {
4210
+ continue;
4211
+ }
4212
+ if (!isLikelyImplementationPath(chunk.filePath)) {
4213
+ continue;
4214
+ }
4215
+ if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
4216
+ continue;
4217
+ }
4218
+ const existing = combinedById.get(chunk.chunkId) ?? candidateUnion.get(chunk.chunkId);
4219
+ const metadata = existing?.metadata ?? {
4220
+ filePath: chunk.filePath,
4221
+ startLine: chunk.startLine,
4222
+ endLine: chunk.endLine,
4223
+ chunkType,
4224
+ name: chunk.name ?? void 0,
4225
+ language: chunk.language,
4226
+ hash: chunk.contentHash
4227
+ };
4228
+ const baselineScore = existing?.score ?? 0.5;
4229
+ candidateUnion.set(chunk.chunkId, {
4230
+ id: chunk.chunkId,
4231
+ score: Math.min(1, baselineScore + 0.5),
4232
+ metadata
4233
+ });
4234
+ }
4235
+ }
4236
+ }
4237
+ }
4238
+ const promoted = [];
4239
+ for (const candidate of candidateUnion.values()) {
4240
+ const filePathLower = candidate.metadata.filePath.toLowerCase();
4241
+ const nameLower = (candidate.metadata.name ?? "").toLowerCase();
4242
+ const exactIdentifierMatch = identifierHints.some((hint) => nameLower === hint);
4243
+ const hasIdentifierMatch = exactIdentifierMatch || identifierHints.some(
4244
+ (hint) => nameLower.includes(hint) || filePathLower.includes(hint)
4245
+ );
4246
+ if (!hasIdentifierMatch) {
4247
+ continue;
4248
+ }
4249
+ if (!isImplementationChunkType(candidate.metadata.chunkType)) {
4250
+ continue;
4251
+ }
4252
+ if (!isLikelyImplementationPath(candidate.metadata.filePath)) {
4253
+ continue;
4254
+ }
4255
+ const existing = combinedById.get(candidate.id) ?? candidate;
4256
+ const rescueBoost = exactIdentifierMatch ? 0.45 : 0.25;
4257
+ const boostedScore = Math.min(1, Math.max(existing.score, candidate.score) + rescueBoost);
4258
+ promoted.push({
4259
+ id: existing.id,
4260
+ score: boostedScore,
4261
+ metadata: existing.metadata
4262
+ });
4263
+ }
4264
+ if (promoted.length === 0) {
4265
+ return combined;
4266
+ }
4267
+ promoted.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4268
+ const promotedIds = new Set(promoted.map((candidate) => candidate.id));
4269
+ const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
4270
+ return [...promoted, ...remainder];
4271
+ }
4272
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates) {
4273
+ if (classifyQueryIntentRaw(query) !== "source") {
4274
+ return [];
4275
+ }
4276
+ const identifierHints = extractIdentifierHints(query);
4277
+ const codeTermHints = extractCodeTermHints(query);
4278
+ if (identifierHints.length === 0 && codeTermHints.length === 0) {
4279
+ return [];
4280
+ }
4281
+ const symbolCandidates = /* @__PURE__ */ new Map();
4282
+ const filePathHint = extractFilePathHint(query);
4283
+ const primaryHint = extractPrimaryIdentifierQueryHint(query);
4284
+ const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
4285
+ if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
4286
+ return;
4287
+ }
4288
+ const chunkType = chunk.nodeType ?? "other";
4289
+ if (!isImplementationChunkType(chunkType)) {
4290
+ return;
4291
+ }
4292
+ if (!isLikelyImplementationPath(chunk.filePath)) {
4293
+ return;
4294
+ }
4295
+ const nameLower = (chunk.name ?? "").toLowerCase();
4296
+ const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
4297
+ const base = baseScore ?? (exactName ? 0.99 : 0.88);
4298
+ const existing = symbolCandidates.get(chunk.chunkId);
4299
+ if (!existing || base > existing.score) {
4300
+ symbolCandidates.set(chunk.chunkId, {
4301
+ id: chunk.chunkId,
4302
+ score: base,
4303
+ metadata: {
4304
+ filePath: chunk.filePath,
4305
+ startLine: chunk.startLine,
4306
+ endLine: chunk.endLine,
4307
+ chunkType,
4308
+ name: chunk.name ?? void 0,
4309
+ language: chunk.language,
4310
+ hash: chunk.contentHash
4311
+ }
4312
+ });
4313
+ }
4314
+ };
4315
+ const normalizedHints = identifierHints.flatMap((hint) => [
4316
+ hint,
4317
+ hint.replace(/_/g, ""),
4318
+ hint.replace(/_/g, "-")
4319
+ ]).filter((hint, idx, arr) => hint.length >= 3 && arr.indexOf(hint) === idx).slice(0, 6);
4320
+ for (const identifier of normalizedHints) {
4321
+ const symbols = [
4322
+ ...database.getSymbolsByName(identifier),
4323
+ ...database.getSymbolsByNameCi(identifier)
4324
+ ];
4325
+ const chunksByName = [
4326
+ ...database.getChunksByName(identifier),
4327
+ ...database.getChunksByNameCi(identifier)
4328
+ ];
4329
+ const normalizedIdentifier = identifier.replace(/_/g, "");
4330
+ const dedupSymbols = /* @__PURE__ */ new Map();
4331
+ for (const symbol of symbols) {
4332
+ dedupSymbols.set(symbol.id, symbol);
4333
+ }
4334
+ for (const symbol of dedupSymbols.values()) {
4335
+ const chunks = database.getChunksByFile(symbol.filePath);
4336
+ for (const chunk of chunks) {
4337
+ if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
4338
+ continue;
4339
+ }
4340
+ upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
4341
+ }
4342
+ }
4343
+ const dedupChunksByName = /* @__PURE__ */ new Map();
4344
+ for (const chunk of chunksByName) {
4345
+ dedupChunksByName.set(chunk.chunkId, chunk);
4346
+ }
4347
+ for (const chunk of dedupChunksByName.values()) {
4348
+ upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
4349
+ }
4350
+ }
4351
+ if (filePathHint && primaryHint) {
4352
+ const primaryChunks = [
4353
+ ...database.getChunksByName(primaryHint),
4354
+ ...database.getChunksByNameCi(primaryHint)
4355
+ ];
4356
+ const dedupPrimaryChunks = /* @__PURE__ */ new Map();
4357
+ for (const chunk of primaryChunks) {
4358
+ dedupPrimaryChunks.set(chunk.chunkId, chunk);
4359
+ }
4360
+ for (const chunk of dedupPrimaryChunks.values()) {
4361
+ if (!pathMatchesHint(chunk.filePath, filePathHint)) {
4362
+ continue;
4363
+ }
4364
+ const normalizedPrimary = primaryHint.replace(/_/g, "");
4365
+ upsertChunkCandidate(chunk, primaryHint, normalizedPrimary, 1);
4366
+ }
4367
+ }
4368
+ const ranked = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4369
+ if (ranked.length === 0) {
4370
+ const implementationFallback = fallbackCandidates.filter(
4371
+ (candidate) => isImplementationChunkType(candidate.metadata.chunkType) && isLikelyImplementationPath(candidate.metadata.filePath)
4372
+ );
4373
+ for (const candidate of implementationFallback) {
4374
+ const nameLower = (candidate.metadata.name ?? "").toLowerCase();
4375
+ const pathLower = candidate.metadata.filePath.toLowerCase();
4376
+ const exactHintMatch = normalizedHints.some((hint) => nameLower === hint || nameLower.replace(/_/g, "") === hint.replace(/_/g, ""));
4377
+ const tokenizedName = tokenizeTextForRanking(nameLower);
4378
+ const tokenHits = codeTermHints.filter((term) => tokenizedName.has(term) || pathLower.includes(term)).length;
4379
+ if (!exactHintMatch && tokenHits === 0) {
4380
+ continue;
4381
+ }
4382
+ const laneScore = exactHintMatch ? Math.min(1, Math.max(candidate.score, 0.97)) : Math.min(0.95, Math.max(candidate.score, 0.82 + tokenHits * 0.03));
4383
+ symbolCandidates.set(candidate.id, {
4384
+ id: candidate.id,
4385
+ score: laneScore,
4386
+ metadata: candidate.metadata
4387
+ });
4388
+ }
4389
+ if (symbolCandidates.size === 0) {
4390
+ const queryTokenSet = tokenizeTextForRanking(query);
4391
+ const rankedFallback = implementationFallback.map((candidate) => {
4392
+ const nameTokens = tokenizeTextForRanking(candidate.metadata.name ?? "");
4393
+ const pathTokens = splitPathTokens(candidate.metadata.filePath);
4394
+ let overlap = 0;
4395
+ for (const token of queryTokenSet) {
4396
+ if (nameTokens.has(token) || pathTokens.has(token)) {
4397
+ overlap += 1;
4398
+ }
4399
+ }
4400
+ const overlapScore = queryTokenSet.size > 0 ? overlap / queryTokenSet.size : 0;
4401
+ return {
4402
+ candidate,
4403
+ overlapScore
4404
+ };
4405
+ }).filter((entry) => entry.overlapScore > 0).sort((a, b) => b.overlapScore - a.overlapScore || b.candidate.score - a.candidate.score).slice(0, Math.max(limit, 3));
4406
+ for (const entry of rankedFallback) {
4407
+ symbolCandidates.set(entry.candidate.id, {
4408
+ id: entry.candidate.id,
4409
+ score: Math.min(0.94, Math.max(entry.candidate.score, 0.8 + entry.overlapScore * 0.1)),
4410
+ metadata: entry.candidate.metadata
4411
+ });
4412
+ }
4413
+ }
4414
+ }
4415
+ const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4416
+ return withFallback.slice(0, Math.max(limit * 2, limit));
4417
+ }
4418
+ function buildIdentifierDefinitionLane(query, candidates, limit) {
4419
+ if (classifyQueryIntentRaw(query) !== "source") {
4420
+ return [];
4421
+ }
4422
+ const primaryHint = extractPrimaryIdentifierQueryHint(query);
4423
+ if (!primaryHint) {
4424
+ return [];
4425
+ }
4426
+ const hints = [primaryHint, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].slice(0, 8);
4427
+ const scored = candidates.filter(
4428
+ (candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
4429
+ ).map((candidate) => {
4430
+ const matchScore = scoreIdentifierMatch(candidate.metadata.name, candidate.metadata.filePath, hints);
4431
+ return {
4432
+ candidate,
4433
+ matchScore
4434
+ };
4435
+ }).filter((entry) => entry.matchScore > 0).sort((a, b) => {
4436
+ if (b.matchScore !== a.matchScore) return b.matchScore - a.matchScore;
4437
+ if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
4438
+ return a.candidate.id.localeCompare(b.candidate.id);
4439
+ }).slice(0, Math.max(limit * 2, 10));
4440
+ return scored.map((entry) => ({
4441
+ id: entry.candidate.id,
4442
+ score: Math.min(1, 0.9 + entry.matchScore * 0.09),
4443
+ metadata: entry.candidate.metadata
4444
+ }));
4445
+ }
4446
+ function mergeTieredResults(symbolLane, hybridLane, limit) {
4447
+ if (symbolLane.length === 0) {
4448
+ return hybridLane.slice(0, limit);
4449
+ }
4450
+ const out = [];
4451
+ const seen = /* @__PURE__ */ new Set();
4452
+ for (const candidate of symbolLane) {
4453
+ if (seen.has(candidate.id)) continue;
4454
+ out.push(candidate);
4455
+ seen.add(candidate.id);
4456
+ if (out.length >= limit) return out;
4457
+ }
4458
+ for (const candidate of hybridLane) {
4459
+ if (seen.has(candidate.id)) continue;
4460
+ out.push(candidate);
4461
+ seen.add(candidate.id);
4462
+ if (out.length >= limit) return out;
4463
+ }
4464
+ return out;
4465
+ }
4466
+ function unionCandidates(semanticCandidates, keywordCandidates) {
4467
+ const byId = /* @__PURE__ */ new Map();
4468
+ for (const candidate of semanticCandidates) {
4469
+ byId.set(candidate.id, candidate);
4470
+ }
4471
+ for (const candidate of keywordCandidates) {
4472
+ const existing = byId.get(candidate.id);
4473
+ if (!existing || candidate.score > existing.score) {
4474
+ byId.set(candidate.id, candidate);
4475
+ }
4476
+ }
4477
+ return Array.from(byId.values());
4478
+ }
3485
4479
  var Indexer = class {
3486
4480
  config;
3487
4481
  projectRoot;
@@ -3984,6 +4978,79 @@ var Indexer = class {
3984
4978
  if (chunkDataBatch.length > 0) {
3985
4979
  database.upsertChunksBatch(chunkDataBatch);
3986
4980
  }
4981
+ const allSymbolIds = /* @__PURE__ */ new Set();
4982
+ const symbolsByFile = /* @__PURE__ */ new Map();
4983
+ for (let i = 0; i < parsedFiles.length; i++) {
4984
+ const parsed = parsedFiles[i];
4985
+ const changedFile = changedFiles[i];
4986
+ database.deleteCallEdgesByFile(parsed.path);
4987
+ database.deleteSymbolsByFile(parsed.path);
4988
+ const fileSymbols = [];
4989
+ for (const chunk of parsed.chunks) {
4990
+ if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
4991
+ const symbolId = `sym_${hashContent(parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine).slice(0, 16)}`;
4992
+ const symbol = {
4993
+ id: symbolId,
4994
+ filePath: parsed.path,
4995
+ name: chunk.name,
4996
+ kind: chunk.chunkType,
4997
+ startLine: chunk.startLine,
4998
+ startCol: 0,
4999
+ endLine: chunk.endLine,
5000
+ endCol: 0,
5001
+ language: chunk.language
5002
+ };
5003
+ fileSymbols.push(symbol);
5004
+ allSymbolIds.add(symbolId);
5005
+ }
5006
+ const symbolsByName = /* @__PURE__ */ new Map();
5007
+ for (const symbol of fileSymbols) {
5008
+ const existing = symbolsByName.get(symbol.name) ?? [];
5009
+ existing.push(symbol);
5010
+ symbolsByName.set(symbol.name, existing);
5011
+ }
5012
+ if (fileSymbols.length > 0) {
5013
+ database.upsertSymbolsBatch(fileSymbols);
5014
+ symbolsByFile.set(parsed.path, fileSymbols);
5015
+ }
5016
+ const fileLanguage = parsed.chunks[0]?.language;
5017
+ if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) continue;
5018
+ const callSites = extractCalls(changedFile.content, fileLanguage);
5019
+ if (callSites.length === 0) continue;
5020
+ const edges = [];
5021
+ for (const site of callSites) {
5022
+ const enclosingSymbol = fileSymbols.find(
5023
+ (sym) => site.line >= sym.startLine && site.line <= sym.endLine
5024
+ );
5025
+ if (!enclosingSymbol) continue;
5026
+ const edgeId = `edge_${hashContent(enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column).slice(0, 16)}`;
5027
+ edges.push({
5028
+ id: edgeId,
5029
+ fromSymbolId: enclosingSymbol.id,
5030
+ targetName: site.calleeName,
5031
+ toSymbolId: void 0,
5032
+ callType: site.callType,
5033
+ line: site.line,
5034
+ col: site.column,
5035
+ isResolved: false
5036
+ });
5037
+ }
5038
+ if (edges.length > 0) {
5039
+ database.upsertCallEdgesBatch(edges);
5040
+ for (const edge of edges) {
5041
+ const candidates = symbolsByName.get(edge.targetName);
5042
+ if (candidates && candidates.length === 1) {
5043
+ database.resolveCallEdge(edge.id, candidates[0].id);
5044
+ }
5045
+ }
5046
+ }
5047
+ }
5048
+ for (const filePath of unchangedFilePaths) {
5049
+ const existingSymbols = database.getSymbolsByFile(filePath);
5050
+ for (const sym of existingSymbols) {
5051
+ allSymbolIds.add(sym.id);
5052
+ }
5053
+ }
3987
5054
  let removedCount = 0;
3988
5055
  for (const [chunkId] of existingChunks) {
3989
5056
  if (!currentChunkIds.has(chunkId)) {
@@ -4005,6 +5072,8 @@ var Indexer = class {
4005
5072
  if (pendingChunks.length === 0 && removedCount === 0) {
4006
5073
  database.clearBranch(this.currentBranch);
4007
5074
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5075
+ database.clearBranchSymbols(this.currentBranch);
5076
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
4008
5077
  this.fileHashCache = currentFileHashes;
4009
5078
  this.saveFileHashCache();
4010
5079
  stats.durationMs = Date.now() - startTime;
@@ -4021,6 +5090,8 @@ var Indexer = class {
4021
5090
  if (pendingChunks.length === 0) {
4022
5091
  database.clearBranch(this.currentBranch);
4023
5092
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5093
+ database.clearBranchSymbols(this.currentBranch);
5094
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
4024
5095
  store.save();
4025
5096
  invertedIndex.save();
4026
5097
  this.fileHashCache = currentFileHashes;
@@ -4161,6 +5232,8 @@ var Indexer = class {
4161
5232
  });
4162
5233
  database.clearBranch(this.currentBranch);
4163
5234
  database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
5235
+ database.clearBranchSymbols(this.currentBranch);
5236
+ database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
4164
5237
  store.save();
4165
5238
  invertedIndex.save();
4166
5239
  this.fileHashCache = currentFileHashes;
@@ -4271,15 +5344,22 @@ var Indexer = class {
4271
5344
  }
4272
5345
  const maxResults = limit ?? this.config.search.maxResults;
4273
5346
  const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
5347
+ const fusionStrategy = this.config.search.fusionStrategy;
5348
+ const rrfK = this.config.search.rrfK;
5349
+ const rerankTopN = this.config.search.rerankTopN;
4274
5350
  const filterByBranch = options?.filterByBranch ?? true;
4275
5351
  this.logger.search("debug", "Starting search", {
4276
5352
  query,
4277
5353
  maxResults,
4278
5354
  hybridWeight,
5355
+ fusionStrategy,
5356
+ rrfK,
5357
+ rerankTopN,
4279
5358
  filterByBranch
4280
5359
  });
4281
5360
  const embeddingStartTime = import_perf_hooks.performance.now();
4282
- const embedding = await this.getQueryEmbedding(query, provider);
5361
+ const embeddingQuery = stripFilePathHint(query);
5362
+ const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
4283
5363
  const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
4284
5364
  const vectorStartTime = import_perf_hooks.performance.now();
4285
5365
  const semanticResults = store.search(embedding, maxResults * 4);
@@ -4287,16 +5367,74 @@ var Indexer = class {
4287
5367
  const keywordStartTime = import_perf_hooks.performance.now();
4288
5368
  const keywordResults = await this.keywordSearch(query, maxResults * 4);
4289
5369
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
4290
- const fusionStartTime = import_perf_hooks.performance.now();
4291
- const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
4292
- const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
4293
5370
  let branchChunkIds = null;
4294
5371
  if (filterByBranch && this.currentBranch !== "default") {
4295
5372
  branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
4296
5373
  }
4297
- const filtered = combined.filter((r) => {
5374
+ const prefilterStartTime = import_perf_hooks.performance.now();
5375
+ const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
5376
+ const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
5377
+ const prefilteredKeyword = shouldPrefilterByBranch && branchChunkIds ? keywordResults.filter((r) => branchChunkIds.has(r.id)) : keywordResults;
5378
+ const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
5379
+ const keywordCandidates = shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0 ? keywordResults : prefilteredKeyword;
5380
+ const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
5381
+ if (branchChunkIds && branchChunkIds.size === 0) {
5382
+ this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
5383
+ branch: this.currentBranch
5384
+ });
5385
+ }
5386
+ if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
5387
+ this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
5388
+ branch: this.currentBranch
5389
+ });
5390
+ }
5391
+ if (shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0) {
5392
+ this.logger.search("warn", "Branch prefilter produced no keyword overlap, using unfiltered keyword candidates", {
5393
+ branch: this.currentBranch
5394
+ });
5395
+ }
5396
+ const fusionStartTime = import_perf_hooks.performance.now();
5397
+ const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
5398
+ fusionStrategy,
5399
+ rrfK,
5400
+ rerankTopN,
5401
+ limit: maxResults,
5402
+ hybridWeight
5403
+ });
5404
+ const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
5405
+ const rescued = promoteIdentifierMatches(
5406
+ query,
5407
+ combined,
5408
+ semanticCandidates,
5409
+ keywordCandidates,
5410
+ database,
5411
+ branchChunkIds
5412
+ );
5413
+ const union = unionCandidates(semanticCandidates, keywordCandidates);
5414
+ const deterministicIdentifierLane = buildDeterministicIdentifierPass(
5415
+ query,
5416
+ union,
5417
+ maxResults
5418
+ );
5419
+ const identifierLane = buildIdentifierDefinitionLane(
5420
+ query,
5421
+ union,
5422
+ maxResults
5423
+ );
5424
+ const symbolLane = buildSymbolDefinitionLane(
5425
+ query,
5426
+ database,
5427
+ branchChunkIds,
5428
+ maxResults,
5429
+ union
5430
+ );
5431
+ const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
5432
+ const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
5433
+ const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
5434
+ const sourceIntent = classifyQueryIntentRaw(query) === "source";
5435
+ const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
5436
+ const baseFiltered = tiered.filter((r) => {
4298
5437
  if (r.score < this.config.search.minScore) return false;
4299
- if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
4300
5438
  if (options?.fileType) {
4301
5439
  const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
4302
5440
  if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
@@ -4309,7 +5447,11 @@ var Indexer = class {
4309
5447
  if (r.metadata.chunkType !== options.chunkType) return false;
4310
5448
  }
4311
5449
  return true;
4312
- }).slice(0, maxResults);
5450
+ });
5451
+ const implementationOnly = baseFiltered.filter(
5452
+ (r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
5453
+ );
5454
+ const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
4313
5455
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
4314
5456
  this.logger.recordSearch(totalSearchMs, {
4315
5457
  embeddingMs,
@@ -4324,6 +5466,7 @@ var Indexer = class {
4324
5466
  embeddingMs: Math.round(embeddingMs * 100) / 100,
4325
5467
  vectorMs: Math.round(vectorMs * 100) / 100,
4326
5468
  keywordMs: Math.round(keywordMs * 100) / 100,
5469
+ prefilterMs: Math.round(prefilterMs * 100) / 100,
4327
5470
  fusionMs: Math.round(fusionMs * 100) / 100
4328
5471
  });
4329
5472
  const metadataOnly = options?.metadataOnly ?? false;
@@ -4377,34 +5520,6 @@ var Indexer = class {
4377
5520
  results.sort((a, b) => b.score - a.score);
4378
5521
  return results.slice(0, limit);
4379
5522
  }
4380
- fuseResults(semanticResults, keywordResults, keywordWeight, limit) {
4381
- const semanticWeight = 1 - keywordWeight;
4382
- const fusedScores = /* @__PURE__ */ new Map();
4383
- for (const r of semanticResults) {
4384
- fusedScores.set(r.id, {
4385
- score: r.score * semanticWeight,
4386
- metadata: r.metadata
4387
- });
4388
- }
4389
- for (const r of keywordResults) {
4390
- const existing = fusedScores.get(r.id);
4391
- if (existing) {
4392
- existing.score += r.score * keywordWeight;
4393
- } else {
4394
- fusedScores.set(r.id, {
4395
- score: r.score * keywordWeight,
4396
- metadata: r.metadata
4397
- });
4398
- }
4399
- }
4400
- const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
4401
- id,
4402
- score: data.score,
4403
- metadata: data.metadata
4404
- }));
4405
- results.sort((a, b) => b.score - a.score);
4406
- return results.slice(0, limit);
4407
- }
4408
5523
  async getStatus() {
4409
5524
  const { store, configuredProviderInfo } = await this.ensureInitialized();
4410
5525
  return {
@@ -4427,6 +5542,7 @@ var Indexer = class {
4427
5542
  this.fileHashCache.clear();
4428
5543
  this.saveFileHashCache();
4429
5544
  database.clearBranch(this.currentBranch);
5545
+ database.clearBranchSymbols(this.currentBranch);
4430
5546
  database.deleteMetadata("index.version");
4431
5547
  database.deleteMetadata("index.embeddingProvider");
4432
5548
  database.deleteMetadata("index.embeddingModel");
@@ -4455,6 +5571,8 @@ var Indexer = class {
4455
5571
  removedCount++;
4456
5572
  }
4457
5573
  database.deleteChunksByFile(filePath);
5574
+ database.deleteCallEdgesByFile(filePath);
5575
+ database.deleteSymbolsByFile(filePath);
4458
5576
  removedFilePaths.push(filePath);
4459
5577
  }
4460
5578
  }
@@ -4464,6 +5582,8 @@ var Indexer = class {
4464
5582
  }
4465
5583
  const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
4466
5584
  const gcOrphanChunks = database.gcOrphanChunks();
5585
+ const gcOrphanSymbols = database.gcOrphanSymbols();
5586
+ const gcOrphanCallEdges = database.gcOrphanCallEdges();
4467
5587
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
4468
5588
  this.logger.gc("info", "Health check complete", {
4469
5589
  removedStale: removedCount,
@@ -4471,7 +5591,7 @@ var Indexer = class {
4471
5591
  orphanChunks: gcOrphanChunks,
4472
5592
  removedFiles: removedFilePaths.length
4473
5593
  });
4474
- return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
5594
+ return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
4475
5595
  }
4476
5596
  async retryFailedBatches() {
4477
5597
  const { store, provider, invertedIndex } = await this.ensureInitialized();
@@ -4577,9 +5697,29 @@ var Indexer = class {
4577
5697
  if (filterByBranch && this.currentBranch !== "default") {
4578
5698
  branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
4579
5699
  }
4580
- const filtered = semanticResults.filter((r) => {
5700
+ const prefilterStartTime = import_perf_hooks.performance.now();
5701
+ const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
5702
+ const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
5703
+ const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
5704
+ const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
5705
+ if (branchChunkIds && branchChunkIds.size === 0) {
5706
+ this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
5707
+ branch: this.currentBranch
5708
+ });
5709
+ }
5710
+ if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
5711
+ this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
5712
+ branch: this.currentBranch
5713
+ });
5714
+ }
5715
+ const rerankTopN = this.config.search.rerankTopN;
5716
+ const ranked = rankSemanticOnlyResults(code, semanticCandidates, {
5717
+ rerankTopN,
5718
+ limit,
5719
+ prioritizeSourcePaths: false
5720
+ });
5721
+ const filtered = ranked.filter((r) => {
4581
5722
  if (r.score < this.config.search.minScore) return false;
4582
- if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
4583
5723
  if (options?.excludeFile) {
4584
5724
  if (r.metadata.filePath === options.excludeFile) return false;
4585
5725
  }
@@ -4608,7 +5748,8 @@ var Indexer = class {
4608
5748
  results: filtered.length,
4609
5749
  totalMs: Math.round(totalSearchMs * 100) / 100,
4610
5750
  embeddingMs: Math.round(embeddingMs * 100) / 100,
4611
- vectorMs: Math.round(vectorMs * 100) / 100
5751
+ vectorMs: Math.round(vectorMs * 100) / 100,
5752
+ prefilterMs: Math.round(prefilterMs * 100) / 100
4612
5753
  });
4613
5754
  return Promise.all(
4614
5755
  filtered.map(async (r) => {
@@ -4637,6 +5778,14 @@ var Indexer = class {
4637
5778
  })
4638
5779
  );
4639
5780
  }
5781
+ async getCallers(targetName) {
5782
+ const { database } = await this.ensureInitialized();
5783
+ return database.getCallersWithContext(targetName, this.currentBranch);
5784
+ }
5785
+ async getCallees(symbolId) {
5786
+ const { database } = await this.ensureInitialized();
5787
+ return database.getCallees(symbolId, this.currentBranch);
5788
+ }
4640
5789
  };
4641
5790
 
4642
5791
  // node_modules/chokidar/index.js
@@ -6695,7 +7844,7 @@ ${formatted.join("\n")}
6695
7844
  Use Read tool to examine specific files.`;
6696
7845
  }
6697
7846
  function formatHealthCheck(result) {
6698
- if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
7847
+ if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0 && result.gcOrphanSymbols === 0 && result.gcOrphanCallEdges === 0) {
6699
7848
  return "Index is healthy. No stale entries found.";
6700
7849
  }
6701
7850
  const lines = [`Health check complete:`];
@@ -6708,6 +7857,12 @@ function formatHealthCheck(result) {
6708
7857
  if (result.gcOrphanChunks > 0) {
6709
7858
  lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
6710
7859
  }
7860
+ if (result.gcOrphanSymbols > 0) {
7861
+ lines.push(` Garbage collected orphan symbols: ${result.gcOrphanSymbols}`);
7862
+ }
7863
+ if (result.gcOrphanCallEdges > 0) {
7864
+ lines.push(` Garbage collected orphan call edges: ${result.gcOrphanCallEdges}`);
7865
+ }
6711
7866
  if (result.filePaths.length > 0) {
6712
7867
  lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
6713
7868
  }
@@ -6907,6 +8062,42 @@ var codebase_search = (0, import_plugin.tool)({
6907
8062
  ${formatSearchResults(results, "score")}`;
6908
8063
  }
6909
8064
  });
8065
+ var call_graph = (0, import_plugin.tool)({
8066
+ description: "Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies between functions.",
8067
+ args: {
8068
+ name: z.string().describe("Function or method name to query"),
8069
+ direction: z.enum(["callers", "callees"]).default("callers").describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
8070
+ symbolId: z.string().optional().describe("Symbol ID (required for 'callees' direction, returned by previous call_graph queries)")
8071
+ },
8072
+ async execute(args) {
8073
+ const indexer = getIndexer();
8074
+ if (args.direction === "callees") {
8075
+ if (!args.symbolId) {
8076
+ return "Error: 'symbolId' is required when direction is 'callees'. First use direction='callers' to find the symbol ID.";
8077
+ }
8078
+ const callees = await indexer.getCallees(args.symbolId);
8079
+ if (callees.length === 0) {
8080
+ return `No callees found for symbol ${args.symbolId}. The function may not call any other tracked functions.`;
8081
+ }
8082
+ const formatted2 = callees.map(
8083
+ (e, i) => `[${i + 1}] \u2192 ${e.targetName} (${e.callType}) at line ${e.line}${e.isResolved ? ` [resolved: ${e.toSymbolId}]` : " [unresolved]"}`
8084
+ );
8085
+ return `${args.name} calls ${callees.length} function(s):
8086
+
8087
+ ${formatted2.join("\n")}`;
8088
+ }
8089
+ const callers = await indexer.getCallers(args.name);
8090
+ if (callers.length === 0) {
8091
+ return `No callers found for "${args.name}". It may not be called by any tracked function, or the index needs updating.`;
8092
+ }
8093
+ const formatted = callers.map(
8094
+ (e, i) => `[${i + 1}] \u2190 from ${e.fromSymbolName ?? "<unknown>"} in ${e.fromSymbolFilePath ?? "<unknown file>"} [${e.fromSymbolId}] (${e.callType}) at line ${e.line}${e.isResolved ? " [resolved]" : " [unresolved]"}`
8095
+ );
8096
+ return `"${args.name}" is called by ${callers.length} function(s):
8097
+
8098
+ ${formatted.join("\n")}`;
8099
+ }
8100
+ });
6910
8101
 
6911
8102
  // src/commands/loader.ts
6912
8103
  var import_fs5 = require("fs");
@@ -7011,7 +8202,8 @@ var plugin = async ({ directory }) => {
7011
8202
  index_health_check,
7012
8203
  index_metrics,
7013
8204
  index_logs,
7014
- find_similar
8205
+ find_similar,
8206
+ call_graph
7015
8207
  },
7016
8208
  async config(cfg) {
7017
8209
  cfg.command = cfg.command ?? {};