Skip to content

trace_path/callers returns empty for plain "from module import func" + bare call across files (Python) #1237

Open

Description

@andresramirezmiori

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

```text

Diagnostics 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

  1. added
    parsing/qualityGraph extraction bugs, false positives, missing edges
    ux/behaviorDisplay bugs, docs, adoption UX
    windowsWindows-specific issues
    on Jul 23, 2026
  2. added
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    windowsWindows-specific issues
    on Jul 24, 2026
  3. added this to the 0.9.1-rc milestone on Jul 24, 2026
  4. DeusData commented on Jul 24, 2026

    @DeusData
    Owner

    Thanks for separating this from #1000. The minimal case shows that from module import func plus a bare call loses the CALLS edge before trace_path serialization. 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 for 0.9.1-rc under #592.

  5. matuu commented on Aug 4, 2026

    @matuu

    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 call get_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 through from module import name bindings. Scope backend/app, 43 files.

    Scoring trace_path(direction="inbound") over 41 target functions / 137 caller edges:

    same-file cross-file total fabricated
    v0.9.0 100.0% 0.0% 36.9% 1

    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):

    app.supabase_client.get_user_client        0/35 callers
    app.supabase_client.get_row_or_none        0/15 callers
    app.supabase_client.get_service_client      0/7 callers
    app.payers.service.find_or_create_payer     0/3 callers
    app.payers.service.build_subject            0/2 callers
    

    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 .py files 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:

    Symbol Before watcher re-index After
    get_user_client 10 callers 0
    get_row_or_none 8 callers 0

    An 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_name entries, no has_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 their professional_id filter, 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.

  6. DeusData commented on Sep 19, 2026

    @DeusData
    Owner

    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.

  7. DeusData commented on Sep 26, 2026

    @DeusData
    Owner

    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:

    • Your pkg/callee.py + pkg/caller.py repro gives wrapper -> pkg.callee.target_fn.
    • A backend/app/... layout with a plain from app.supabase_client import get_row_or_none and 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.

  8. added
    awaiting-reporterWaiting on the reporter for info/repro; stale bot will warn then close
    on Sep 26, 2026
  9. github-actions commented on Sep 26, 2026

    @github-actions

    Thanks for the report! To move this forward we need a bit more so we can reproduce it ourselves:

    • the codebase-memory-mcp --version you're on
    • the exact steps or command you ran
    • a public repo (or a small dummy snippet) that shows the problem — please don't paste proprietary code

    Once that's here we'll pick it straight back up. Heads-up: issues left awaiting-reporter are automatically closed after a few weeks of silence, but a comment reopens the door anytime.

  10. DeusData commented on Oct 19, 2026

    @DeusData
    Owner

    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.

  11. DeusData commented on Oct 25, 2026

    @DeusData
    Owner

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting-reporterWaiting on the reporter for info/repro; stale bot will warn then closebugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.ux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions