Repository navigation
Python: bare intra-module call resolves to 0 CALLS edges despite 6 real, unambiguous same-module callers #2193
Description
Activity
- addedparsing/qualityGraph extraction bugs, false positives, missing edgesGraph extraction bugs, false positives, missing edgeswindowsWindows-specific issuesWindows-specific issues
on Sep 12, 2026 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_DIRenv vars documented in CONFIGURATION.md route around an unrelated local DACL issue on my machine). Three tests, in order:- Minimal fixture matching the described shape (7 functions, one bare intra-module call site each):
trace_path --function-name resolve --project ... --direction inboundfound all 6 real callers, strategylsp, confidence 0.95. - The actual real file (528 lines) in isolation: same result, 6 of 6, correctly resolved.
- 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), strategyheuristicat full scale, confidences 0.01 to 0.95.
Then I tried the exact input string the original report actually used, the
path:line:Type:nameform (tools/memory/issue_board.py:249:Function:resolve), as--function-name. That is not a validfunction_namevalue 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 mistakepath:line:Type:nameversuspath#nameI 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 resolvealone against the full project correctly reportsstatus: ambiguouswith 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.- Minimal fixture matching the described shape (7 functions, one bare intra-module call site each):
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.
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.
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_pathreports 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:
resolveattools/memory/issue_board.py:249in 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 namedresolve.trace symbol:tools/memory/issue_board.py:249:Function:resolve --direction inbound --depth 1on a freshly rebuilt index (not incremental) returns 1 edge (the file's ownDEFINESedge) and 0CALLSedges. 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 at9c6f45b,v0.10.8-574-g9c6f45b, so 574 commits past the shippedv0.10.8binary I actually ran the query against):resolveis a common English word and also the name of an unrelated Pythonstr/Path-adjacent method elsewhere in the ecosystem, so I looked specifically for name-collision suppression logic.src/pipeline/pass_calls.chas a Python-only bare-call guard (suppress_weak_local_binding_call, gated oncall->callee_is_locally_boundfrompython_callee_is_bound_parameter/cbm_walk_python_param_is_boundininternal/cbm/extract_unified.c) added very recently and unreleased,0b0d143then97517a4(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 caseresolveis 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-strcmpverified, so not a bucket-collision bug) andresolve_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 bycbm_registry_resolve_cache_begin/endaround each file inpass_parallel.c, keyed only oncallee_namebut 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 withsecure 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:
trace_pathread-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_pathreports 0 callers" against a healthy underlying graph, different languages).Logs
No stderr captured; the failing call was a live
trace_pathquery 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.