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