You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Status: Confirmed bug, not yet reported upstream as of 2026-08-11. Target:facebookexperimental/semcode GitHub issue tracker. Reproduction commit:main at 301fff6 (also present on feat/mcp-http-transport). Suggested issue title:Bug: git-aware lookups silently return wrong-branch results when target branch lacks the symbol
Summary
When a semcode database is indexed across multiple git branches (semcode-index --branches A,B), the git-aware lookup methods silently return symbol versions from a different branch when the requested symbol does not exist at the requested branch/SHA. Instead of returning None (which the caller's code path correctly handles), the implementations fall back to the non-git-awarefind_function(name) / find_type(name) / find_typedef(name) queries, which ignore the requested SHA entirely and return any indexed version of the symbol.
This contradicts the project's own CLAUDE.md documentation and the doc comments on find_function() itself, and produces misleading user-facing output such as "No functions call function 'foo'" even when foo does not exist on the requested branch at all.
Affected versions / context
Introduced / exposed by: PR Multi branch indexing #8 "Multi branch indexing" (merged 2026-01-13), which added --branches for the indexer and a branch parameter for MCP queries.
Reproduced on main at commit 301fff6 (and on the feat/mcp-http-transport branch which does not modify src/database/connection.rs).
The fallback code paths predate PR Multi branch indexing #8 — they originate in bd7c08ff ("diffinfo: optimize performance with single-scan caller index", 2026-02-04) for find_function_git_aware and fcbbf9d ("Add a symbol mapping table") for find_function_with_manifest. PR Multi branch indexing #8 made the buggy behavior user-visible by giving callers a way to request a specific branch.
Reproduction
Index two branches, where one contains a function and the other does not:
# repo with branches: main (no `foo`), feature-x (has `foo`, called by `bar`)
semcode-index --source /path/to/repo --branches main,feature-x
Query find_callers with branch=main for foo (which does not exist on main):
semcode --database /path/to/repo
# then in REPL or via MCP:
find_callers foo --branch main
Actual output:
Finding all functions that call: foo
Info: No functions call function 'foo'
— i.e. the function was "found" (the Some(entity) branch was taken in mcp_show_callers), but the callers list is empty.
Expected output:
Error: Function or macro 'foo' not found in database
— i.e. find_function_git_aware should have returned None because foo does not exist at the main commit.
Root cause
src/database/connection.rs — find_function_git_aware (line 825) has two fallback points that bypass the requested git_sha:
pubasyncfnfind_function_git_aware(&self,name:&str,git_sha:&str,) -> Result<Option<FunctionInfo>>{ifletSome(func) = self.workdir_find_function(name){returnOk(Some(func));// (a) workdir overlay — separate concern}let git_manifest = self.generate_git_manifest(git_sha).await?;if git_manifest.is_empty(){
tracing::info!("No files resolved for '{}' at commit '{}' - falling back to non-git lookup",
name, git_sha
);returnself.find_function(name).await;// (b) FALLBACK — wrong-branch result}self.find_function_with_manifest(name,&git_manifest).await}
And find_function_with_manifest (line 847) has a second fallback at line 869-872:
if resolved_hashes.is_empty(){// Fallback: do a regular find to get any available functionsreturnself.find_function(name).await;// (c) FALLBACK — wrong-branch result}
find_function(name) (line 810) is documented to only be used when git SHA cannot be determined or for administrative/debug operations:
/// # When to Use This Method/// - Fallback when git SHA cannot be determined (not in a git repository)/// - Administrative/debugging operations that need to see all versions////// # Behavior/// Returns the "best match" from all indexed versions without considering git history.
But the fallbacks at (b) and (c) call it with a known git_sha already in scope, contradicting the doc comment. The result is a FunctionInfo whose git_file_hash corresponds to a different commit than the one the caller asked about.
A similar two-stage interaction explains the user-facing "No callers" message:
mcp_show_callers (src/bin/semcode-mcp.rs:284) calls find_function_git_aware(name, git_sha). The fallback returns Some(entity) from a different branch.
It then calls get_function_callers_git_aware(name, git_sha) (line 2041), which does not have a wrong-branch fallback — it correctly returns an empty Vec because no callers reference the requested SHA's version of the function (the function doesn't exist there).
The MCP handler sees Some(entity) + empty callers, and prints "Info: No functions call function 'foo'" — implying the function exists on the requested branch with no callers, when in fact the function does not exist on the requested branch at all.
Same-pattern bugs elsewhere
The identical fallback shape appears in several other git-aware methods, all with the same defect:
Method
File:line of fallback
Fallback condition
find_function_git_aware
src/database/connection.rs:841
git_manifest.is_empty()
find_function_with_manifest (internal)
src/database/connection.rs:871
resolved_hashes.is_empty()
find_types_git_aware
src/database/connection.rs:1393-1403
resolved_hashes.is_empty() → find_type(name)
find_typedef_git_aware (first fallback)
src/database/connection.rs:1586-1593
resolved_hashes.is_empty() → find_typedef(name)
find_typedef_git_aware (second fallback)
src/database/connection.rs:1603-1610
hash matched but no typedef found → find_typedef(name)
Methods that do not have this bug (correctly return empty on miss):
get_function_callees_git_aware (connection.rs:2319) — returns Ok(Vec::new()) when manifest is empty.
grep_function_bodies_git_aware (connection.rs:3387) — filters by manifest; misses become empty results, no fallback. This is the correct pattern.
get_distinct_reference_counts_git_aware (connection.rs:1784) — filters by manifest, no fallback.
The fact that grep_function_bodies_git_aware implements the "manifest miss → empty result" pattern correctly in the same file is strong evidence that the fallbacks in find_function* / find_type* / find_typedef* are an oversight rather than an intentional design choice.
find_function_git_aware (function body lookup) — get_function_callers_git_aware itself is correct, but its caller already saw the wrong entity
find_calls
same shape — find_function_git_aware + correct get_function_callees_git_aware
find_callchain
recurses through find_function_git_aware at each hop — bug compounds across the chain
Documentation conflict
The project's CLAUDE.md ("Git-Aware Operations" section) explicitly warns:
Without git-aware lookups:
Users may jump to outdated function definitions
Call chains may include deleted or renamed functions
Type information may not match current code structure
Results are confusing and incorrect for active development Remember: When in doubt, use git-aware functions!
The fallbacks make git-aware lookups behave like non-git-aware lookups exactly when the difference matters most (the requested branch doesn't contain the symbol).
Suggested fix
Two viable approaches:
Option A — Pass "explicit SHA" through the call chain. Add an explicit: bool (or enum GitShaSource { Explicit, DefaultHead }) parameter to find_function_git_aware and friends. The MCP handler already has this information in resolve_git_sha_or_branch (src/bin/semcode-mcp.rs:2668) — currently that information is lost at the DB boundary. When explicit is true, the fallbacks return Ok(None) / Ok(Vec::new()) instead of calling find_function/find_type/find_typedef. When explicit is false (non-git repo or default HEAD), keep the current fallback behavior for back-compat.
Option B — Drop the fallbacks entirely.find_function_git_aware and friends always return None / empty when the manifest doesn't contain the symbol's file at the requested SHA. The non-git-awarefind_function(name) continues to exist for the "non-git repository" and "administrative/debug" use cases documented on find_function itself; the caller would call it directly when they genuinely want SHA-agnostic lookup.
Option B is simpler and more consistent with grep_function_bodies_git_aware's existing correct behavior. The risk is identifying any callers that rely on the fallback for legitimate reasons (e.g., a non-git repo path where git_sha is "" or some default — generate_git_manifest returns empty there, triggering the fallback).
A secondary, smaller issue: resolve_git_sha_or_branch (src/bin/semcode-mcp.rs:2668-2686) silently falls through to the default HEAD when git::resolve_branch fails, with only an eprintln! warning. A user who explicitly requests branch=feature-x and types the branch name wrong gets results from the wrong commit (HEAD) with no error. Worth fixing alongside the main bug: when the caller explicitly requested a branch, resolution failure should produce a clear error, not a silent fallback.
Minimal regression test (suggested)
Create a small git repo with two branches where branch A contains a function foo and branch B does not.
Summary
When a semcode database is indexed across multiple git branches (
semcode-index --branches A,B), thegit-awarelookup methods silently return symbol versions from a different branch when the requested symbol does not exist at the requested branch/SHA. Instead of returningNone(which the caller's code path correctly handles), the implementations fall back to the non-git-awarefind_function(name)/find_type(name)/find_typedef(name)queries, which ignore the requested SHA entirely and return any indexed version of the symbol.This contradicts the project's own
CLAUDE.mddocumentation and the doc comments onfind_function()itself, and produces misleading user-facing output such as"No functions call function 'foo'"even whenfoodoes not exist on the requested branch at all.Affected versions / context
--branchesfor the indexer and abranchparameter for MCP queries.mainat commit301fff6(and on thefeat/mcp-http-transportbranch which does not modifysrc/database/connection.rs).bd7c08ff("diffinfo: optimize performance with single-scan caller index", 2026-02-04) forfind_function_git_awareandfcbbf9d("Add a symbol mapping table") forfind_function_with_manifest. PR Multi branch indexing #8 made the buggy behavior user-visible by giving callers a way to request a specific branch.Reproduction
Index two branches, where one contains a function and the other does not:
# repo with branches: main (no `foo`), feature-x (has `foo`, called by `bar`) semcode-index --source /path/to/repo --branches main,feature-xQuery
find_callerswithbranch=mainforfoo(which does not exist onmain):semcode --database /path/to/repo # then in REPL or via MCP: find_callers foo --branch mainActual output:
— i.e. the function was "found" (the
Some(entity)branch was taken inmcp_show_callers), but the callers list is empty.Expected output:
— i.e.
find_function_git_awareshould have returnedNonebecausefoodoes not exist at themaincommit.Root cause
src/database/connection.rs—find_function_git_aware(line 825) has two fallback points that bypass the requestedgit_sha:And
find_function_with_manifest(line 847) has a second fallback at line 869-872:find_function(name)(line 810) is documented to only be used when git SHA cannot be determined or for administrative/debug operations:But the fallbacks at (b) and (c) call it with a known git_sha already in scope, contradicting the doc comment. The result is a
FunctionInfowhosegit_file_hashcorresponds to a different commit than the one the caller asked about.A similar two-stage interaction explains the user-facing "No callers" message:
mcp_show_callers(src/bin/semcode-mcp.rs:284) callsfind_function_git_aware(name, git_sha). The fallback returnsSome(entity)from a different branch.get_function_callers_git_aware(name, git_sha)(line 2041), which does not have a wrong-branch fallback — it correctly returns an emptyVecbecause no callers reference the requested SHA's version of the function (the function doesn't exist there).Some(entity)+ empty callers, and prints"Info: No functions call function 'foo'"— implying the function exists on the requested branch with no callers, when in fact the function does not exist on the requested branch at all.Same-pattern bugs elsewhere
The identical fallback shape appears in several other
git-awaremethods, all with the same defect:find_function_git_awaresrc/database/connection.rs:841git_manifest.is_empty()find_function_with_manifest(internal)src/database/connection.rs:871resolved_hashes.is_empty()find_types_git_awaresrc/database/connection.rs:1393-1403resolved_hashes.is_empty()→find_type(name)find_typedef_git_aware(first fallback)src/database/connection.rs:1586-1593resolved_hashes.is_empty()→find_typedef(name)find_typedef_git_aware(second fallback)src/database/connection.rs:1603-1610find_typedef(name)find_all_functions_git_awaresrc/database/connection.rs:1033-1041resolved_hashes.is_empty()→find_all_by_name_unfiltered(name)Methods that do not have this bug (correctly return empty on miss):
get_function_callees_git_aware(connection.rs:2319) — returnsOk(Vec::new())when manifest is empty.grep_function_bodies_git_aware(connection.rs:3387) — filters by manifest; misses become empty results, no fallback. This is the correct pattern.get_distinct_reference_counts_git_aware(connection.rs:1784) — filters by manifest, no fallback.The fact that
grep_function_bodies_git_awareimplements the "manifest miss → empty result" pattern correctly in the same file is strong evidence that the fallbacks infind_function*/find_type*/find_typedef*are an oversight rather than an intentional design choice.Affected MCP tools
Five MCP tools see the wrong-branch behavior:
find_functionfind_function_git_aware+find_all_functions_git_awarefind_typefind_type_git_aware,find_types_git_aware,find_typedef_git_awarefind_callersfind_function_git_aware(function body lookup) —get_function_callers_git_awareitself is correct, but its caller already saw the wrong entityfind_callsfind_function_git_aware+ correctget_function_callees_git_awarefind_callchainfind_function_git_awareat each hop — bug compounds across the chainDocumentation conflict
The project's
CLAUDE.md("Git-Aware Operations" section) explicitly warns:The fallbacks make
git-awarelookups behave like non-git-awarelookups exactly when the difference matters most (the requested branch doesn't contain the symbol).Suggested fix
Two viable approaches:
Option A — Pass "explicit SHA" through the call chain. Add an
explicit: bool(orenum GitShaSource { Explicit, DefaultHead }) parameter tofind_function_git_awareand friends. The MCP handler already has this information inresolve_git_sha_or_branch(src/bin/semcode-mcp.rs:2668) — currently that information is lost at the DB boundary. Whenexplicitis true, the fallbacks returnOk(None)/Ok(Vec::new())instead of callingfind_function/find_type/find_typedef. Whenexplicitis false (non-git repo or default HEAD), keep the current fallback behavior for back-compat.Option B — Drop the fallbacks entirely.
find_function_git_awareand friends always returnNone/ empty when the manifest doesn't contain the symbol's file at the requested SHA. The non-git-awarefind_function(name)continues to exist for the "non-git repository" and "administrative/debug" use cases documented onfind_functionitself; the caller would call it directly when they genuinely want SHA-agnostic lookup.Option B is simpler and more consistent with
grep_function_bodies_git_aware's existing correct behavior. The risk is identifying any callers that rely on the fallback for legitimate reasons (e.g., a non-git repo path wheregit_shais""or some default —generate_git_manifestreturns empty there, triggering the fallback).A secondary, smaller issue:
resolve_git_sha_or_branch(src/bin/semcode-mcp.rs:2668-2686) silently falls through to the default HEAD whengit::resolve_branchfails, with only aneprintln!warning. A user who explicitly requestsbranch=feature-xand types the branch name wrong gets results from the wrong commit (HEAD) with no error. Worth fixing alongside the main bug: when the caller explicitly requested a branch, resolution failure should produce a clear error, not a silent fallback.Minimal regression test (suggested)
fooand branch B does not.semcode-index --source /path --branches A,Bfind_function(name="foo", branch="B")returnsNone(or an MCP "not found" error).find_function(name="foo", branch="A")returns theAversion offoo.find_type,find_typedef,find_callers,find_calls,find_callchain.