Repository navigation
Description
Version
codebase-memory-mcp 0.9.0
Platform
Linux (x64)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
ui
What happened, and what did you expect?
trace_path(mode=calls, direction=inbound) (and mode=data_flow, which inherits the same CALLS edges) returns an empty callers list for a function that IS called from another file, when the call site uses the simplest possible import shape: a plain from module import func (no alias, no as) followed by a bare, no-suffix call func(x). Confirmed via get_code_snippet(..., include_neighbors=true) too — the node itself reports "callers": 0.
Reproduced on a real ~240k-node Django monorepo with two independent, unrelated function pairs (different source/target modules each time), so it doesn't look like an isolated fluke. Reindexed from scratch (deleted the cached .db, full re-parse, node/edge counts changed confirming a genuine rebuild) — the gap persisted, so it's not a stale-index artifact.
Reproduction
Minimal repro
pkg/callee.py:
def target_fn(x):
return x + 1
pkg/caller.py:
from pkg.callee import target_fn
def wrapper(x):
return target_fn(x)
Steps
1. index_repository(repo_path=..., mode="full")
2. trace_path(function_name="target_fn", direction="inbound", mode="calls")
Expected: callers: [{"name": "wrapper", ...}]
Actual: callers: []
Why this doesn't look like the known issues
Checked #1000 (closed, "import-map direct-hit swallows call suffixes — django loses ~11K CALLS/TESTS edges") — that bug is about imported_symbol.method() calls where a suffix (.method) gets swallowed and mis-resolves to the base node. This repro has no suffix at all — target_fn is called directly, not as an attribute of anything. The issue's own description says the no-alias/no-suffix case (their "bare_alias" test) stays green both before and after the #1000 fix, and the registry suite is 56/56 — so this looks like a distinct, uncovered gap in resolve_import_map (or wherever plain same-name bare imports get resolved), not a regression of #979/#875/#1000.
Impact: any "who calls X" / dead-code / impact-analysis query silently under-reports calleron import style, with no warning that the result may be incomplete.
### Logs
```textDiagnostics trajectory (memory / performance / leak issues)
Project scale (if relevant)
No response
Confirmations
- I searched existing issues and this is not a duplicate.
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Activity
DeusData commentedon Jul 24, 2026
Thanks for separating this from #1000. The minimal case shows that
from module import funcplus a bare call loses the CALLS edge beforetrace_pathserialization. The RED belongs at extraction and resolution level: index the two files, require the caller Function to have exactly one CALLS edge to the imported Function, then verify inbound trace output. High priority for0.9.1-rcunder #592.matuu commentedon Aug 4, 2026
Corroboration from a Python repo (FastAPI + supabase-py, v0.9.0 Linux x64), with the same import shape as the original repro:
from app.supabase_client import get_row_or_none— plain, no alias — and a bare callget_row_or_none(...), no suffix.I wrote an AST-based benchmark to measure this properly rather than by spot checks. Two findings, the second unexpected.
1. The split is clean: 100% intra-file, 0% cross-file
Ground truth is computed from Python's
ast, deliberately narrowed so every case is one a correct indexer must find: bare-name calls only (attribute calls excluded — those need receiver type inference, a defensible miss), resolved throughfrom module import namebindings. Scopebackend/app, 43 files.Scoring
trace_path(direction="inbound")over 41 target functions / 137 caller edges:Not "mostly broken" — a clean binary. Every intra-file caller found, zero cross-file callers found. The aggregate 36.9% is misleading and would hide the diagnosis; the split is what shows it.
Worst cases (all cross-file, all plain from-imports):
This matches the diagnosis in #1354 for TypeScript ("type-aware tier resolves same-file calls only"), which suggests the boundary is in the shared resolution tier rather than the Python parser.
2. The result is not stable across re-indexes
Before writing the benchmark, the same queries returned partial cross-file resolution —
get_user_client→ 10 callers,get_row_or_none→ 8, all of them landing in one single file. Writing the benchmark scripts (new.pyfiles inside the indexed repo) woke the file watcher, which re-indexed on its own. Node/edge counts moved 2788/8504 → 2967/8858. After that, the same queries return 0.So, with no change whatsoever to the source under test:
get_user_clientget_row_or_noneAn earlier revision of this comment reported those 8/10 figures and stated that an explicit
index_repository(mode="full")was a no-op (counters identical). That was accurate at the time, but incomplete: the counts do move on watcher-triggered re-indexes, and the caller results move with them. The answer to "who calls X?" depends on which re-index you happen to catch — which makes the failure hard to reproduce by hand and easy to dismiss as a stale index.Why the failure mode is the real problem
The bad result is indistinguishable from a good one: well-formed
qualified_nameentries, nohas_more, no confidence field (related: #1382, #1355). In a multi-tenant codebase an incomplete caller list is exactly how a missing tenant filter survives review — this repo had two queries missing theirprofessional_idfilter, and a caller list at 0% cross-file recall would have shown none of the sites to audit.Happy to share the benchmark scripts if useful — ~200 lines of stdlib Python, no dependencies, and the answers format is tool-neutral so it can score any indexer.
DeusData commentedon Sep 19, 2026
Thank you for the cross-file measurements and the before/after generation evidence. Missing bare-import resolution and a session observing a changed or stale graph need separate checks; zero inbound edges cannot establish that the source has no callers. The next comparison needs the same source and indexed generation held fixed before attributing the result to resolution. This remains maintainer-owned.
DeusData commentedon Sep 26, 2026
Thank you, @andresramirezmiori, for the minimal two-file case, and @matuu for the AST-based benchmark! Splitting same-file from cross-file results is the check we now use ourselves.
Both shapes resolve on current main:
pkg/callee.py+pkg/caller.pyrepro giveswrapper -> pkg.callee.target_fn.backend/app/...layout with a plainfrom app.supabase_client import get_row_or_noneand a bare call also resolves across files, at 0.97 confidence.The fix came with the Python cross-file resolution work between 0.9.x and v0.11.0, not as one single commit. Could you re-run your benchmark on v0.11.0, on a fresh index of the same source revision, and post the cross-file column? If it reads 100%, we'll close this with your numbers on record.
github-actions commentedon Sep 26, 2026
Thanks for the report! To move this forward we need a bit more so we can reproduce it ourselves:
codebase-memory-mcp --versionyou're onOnce that's here we'll pick it straight back up. Heads-up: issues left
awaiting-reporterare automatically closed after a few weeks of silence, but a comment reopens the door anytime.fix(python): from . import x names the importing package, not its parent
DeusData commentedon Oct 19, 2026
Thank you again for the two-file case and the same-file/cross-file benchmark split. The last update requested a fresh-index v0.11.0 comparison on the same source revision. I have no newer benchmark result to add; that cross-file column remains the outstanding confirmation.
DeusData commentedon Oct 25, 2026
Thank you again for the cross-file benchmark. The requested fresh-index comparison on the same source revision remains the missing confirmation; the cross-file column is the useful result.