Skip to content

Python: bare intra-module call resolves to 0 CALLS edges despite 6 real, unambiguous same-module callers #2193

Description

@ademczuk

Version

codebase-memory-mcp 0.10.8

Platform

Windows

Install channel

GitHub release archive / install.sh / install.ps1

What happened, and what did you expect?

trace_path reports zero inbound CALLS edges for a plain module-level Python function with real, unambiguous, same-module callers. Expected the six real callers to show up.

Target: resolve at tools/memory/issue_board.py:249 in a private repo, a bare top-level function (def resolve(task_id, repo=None)), called bare (not as a method, not via any alias) from six other top-level functions in the same file: claim, release, block, unblock, finish, main. None of those functions, and no function anywhere in the file, binds a parameter literally named resolve.

trace symbol:tools/memory/issue_board.py:249:Function:resolve --direction inbound --depth 1 on a freshly rebuilt index (not incremental) returns 1 edge (the file's own DEFINES edge) and 0 CALLS edges. Freshness check reported the index current. Corpus-wide, the same index reports 93,004 unresolved_calls against 46,278 total edges, so unresolved call references outnumber the entire edge set roughly 2:1, for whatever that ratio is worth as a symptom.

As a control, I built a Graft (a different, independent code-graph tool) wiring graph for the same repo and queried the same target (tools/memory/issue_board.py#resolve) for inbound calls. It correctly returned all six real callers. So the six callers are genuinely there and genuinely resolvable by static analysis; something in this tool's Python path is dropping them.

Reproduction

I don't have a minimal public repro repo, but I traced the likely mechanism against main (my clone is at 9c6f45b, v0.10.8-574-g9c6f45b, so 574 commits past the shipped v0.10.8 binary I actually ran the query against):

# tools/memory/issue_board.py, shape of the real file
def resolve(task_id, repo=None):
    ...

def claim(task_id, ...):
    n = resolve(task_id, repo=repo)   # bare call, same module, no shadowing anywhere
    ...

def release(task_id, ...):
    n = resolve(task_id, repo=repo)
# ...and four more call sites, same shape, in block/unblock/finish/main

resolve is a common English word and also the name of an unrelated Python str/Path-adjacent method elsewhere in the ecosystem, so I looked specifically for name-collision suppression logic. src/pipeline/pass_calls.c has a Python-only bare-call guard (suppress_weak_local_binding_call, gated on call->callee_is_locally_bound from python_callee_is_bound_parameter / cbm_walk_python_param_is_bound in internal/cbm/extract_unified.c) added very recently and unreleased, 0b0d143 then 97517a4 (PR #1912, "fix(python): suppress bare calls shadowed by an enclosing parameter"), distilled from #1386.

By that PR's own stated design and its own worked example (def _run_with_heavy_slot(run): return run()), the guard should fire only when the callee name is a currently bound parameter of an enclosing scope. In this case resolve is never a parameter anywhere in the file, so by design the guard should not apply, but the live query result matches exactly what firing it (or something else with the same effect) would produce.

I read py_param_slot (open addressing, hash-then-strcmp verified, so not a bucket-collision bug) and resolve_same_module/registry_resolve_chain (candidate construction looks correct: module_qn + "." + callee_name) and could not find the defect by static reading alone. I also checked and ruled out the per-file _resolve_cache (correctly scoped by cbm_registry_resolve_cache_begin/end around each file in pass_parallel.c, keyed only on callee_name but that's sound because it's reset per file, not a cross-file poisoning bug).

I could not get a live local repro of my own: codebase-memory-mcp cli --json trace_path ... refused on this machine with secure CLI coordination could not be created (endpoint): C:\Users\andre\AppData: DACL entry 0 grants mutation rights ... to untrusted identity, which is this box's own environment, not a defect in the tool, and the MCP stdio connection I was using dropped mid-session. So everything above the reproduction is from a real, already-run query (not synthetic), but I was not able to re-run it myself with tracing/logging to pin the exact function.

Two candidate areas I'd point a maintainer at, in order of suspicion:

  1. The new bare-call parameter-shadowing guard (PR fix(python): suppress bare calls shadowed by an enclosing parameter #1912) over-firing on a case its own design says it shouldn't touch.
  2. A trace_path read-side bug independent of extraction, matching the shape of TypeScript: CALLS edges silently missing for some directly-imported functions — trace_path reports callers_total: 0 (v0.10.5) #1682 and TypeScript: cross-file method calls produce no CALLS edge — type-aware tier resolves same-file calls only, so trace_path(inbound) returns 0 callers #1354 (both "trace_path reports 0 callers" against a healthy underlying graph, different languages).

Logs

No stderr captured; the failing call was a live trace_path query via MCP, not a CLI invocation with logging. Happy to gather --progress/verbose logs if a maintainer confirms the CLI DACL issue above is unrelated noise and can point me at how to work around it, or if there's a way to run this against a public repo I can share results from directly.

Activity

  1. ademczuk commented on Sep 12, 2026

    @ademczuk
    Author

    RETRACTING THE CORE CLAIM. I did further testing after filing this and it does not hold up. Sorry for the noise, leaving the full trail below rather than deleting the issue.

    I got a working local CLI repro after filing (the CBM_RUNTIME_DIR/CBM_CACHE_DIR env vars documented in CONFIGURATION.md route around an unrelated local DACL issue on my machine). Three tests, in order:

    1. Minimal fixture matching the described shape (7 functions, one bare intra-module call site each): trace_path --function-name resolve --project ... --direction inbound found all 6 real callers, strategy lsp, confidence 0.95.
    2. The actual real file (528 lines) in isolation: same result, 6 of 6, correctly resolved.
    3. The real, full project (34,516 nodes, 144,754 edges, current HEAD), queried by the fully-qualified name (--function-name C-Projects-agentreef.tools.memory.issue_board.resolve): found all 7 real callers correctly (6 plus one more the original report did not check for), strategy heuristic at full scale, confidences 0.01 to 0.95.

    Then I tried the exact input string the original report actually used, the path:line:Type:name form (tools/memory/issue_board.py:249:Function:resolve), as --function-name. That is not a valid function_name value for this endpoint: {"error":"function not found","hint":"Use search_graph(name_pattern=...) to find the exact name, then pass it to trace_path."}, a clean, explicit error, not a silent empty success.

    I cannot rule out that the MCP-protocol call (as opposed to this CLI) handles that malformed input differently and returns something that reads as "0 callers" rather than an explicit not-found, since I was not able to reproduce the original query through the MCP connection itself (it disconnected mid-session and I moved to the CLI instead). But the extraction and resolution pipeline itself, tested three ways including at full project scale on the real code, produces the correct answer. So the likely explanation is a malformed symbol identifier fed to trace_path, the same class of mistake path:line:Type:name versus path#name I made myself against a different tool earlier the same evening, not a defect in Python call extraction or resolution.

    Also worth noting for anyone hitting the ambiguity path: --function-name resolve alone against the full project correctly reports status: ambiguous with 13 named candidates rather than guessing or silently returning nothing, which is the right behavior and not part of this report.

    Leaving this open only if a maintainer wants to harden the MCP-level input validation to return the same explicit "function not found" for a path:line:Type:name-shaped string that the CLI already gives; happy to close it myself if that is not worth a tracked item.

  2. ademczuk commented on Sep 12, 2026

    @ademczuk
    Author

    Closing per the retraction above: the extraction/resolution pipeline is correct on further testing, the original symptom traces to a malformed symbol identifier rather than a code defect.

  3. DeusData commented on Sep 19, 2026

    @DeusData
    Owner

    Thank you for following this through with the isolated fixture, the real file, and the full-project comparison, and for correcting the original report publicly. That follow-up is useful evidence, not noise. We will leave this closed in line with your retraction; if a separate MCP-only input-handling discrepancy can be reproduced, it can be tracked on its own without reopening the Python extraction claim. Thanks again for preserving the investigation trail.

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

    parsing/qualityGraph extraction bugs, false positives, missing edgeswindowsWindows-specific issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions