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
In a TypeScript monorepo, CALLS edges are missing for some directly-imported function calls, while structurally identical calls to a sibling helper resolve correctly. Because the misses are silent, trace_path(direction="inbound") returns callers_total: 0, which is easy to read as "this function has no callers" when it actually has several.
I'm not reporting "call resolution is broken" — most of the graph looks good. I'm reporting that the failure is indistinguishable from a true negative, and that it isn't explained by any call-shape rule I could find.
Queried via cli query_graph and the MCP trace_path / query_graph tools (same results)
Observed
Three symbols, same repo, same index, same query shape:
#
symbol
how it is called
graph
actual (grep)
A
helperA — exported function in modules/pricing/util.ts
import { helperA } + direct call
✅ 5 production callers, function-level
matches
B
helperB — exported function in modules/campaign/engine.ts
import { helperB } + direct call
❌ 0CALLS (only DEFINES)
3 production call sites
C
ServiceX.methodY — class method
this.injected.methodY(...) (NestJS DI)
❌ 0 production; 13 USAGE edges, all in *.spec.ts
11 call sites / 6 files
A and B have the identical call shape — plain named import, direct invocation, callers are class methods in other modules. One resolves, the other does not. That's what makes this hard to work around: there is no rule I can give a user (or myself) for when to trust a zero.
Query used (same for all three):
MATCH (a)-[e]->(b) WHEREb.name='<symbol>'RETURNtype(e) ASedge, a.nameASsource, a.file_pathASfile
For B the only inbound edges are DEFINES. For C the only inbound edges are USAGE from test files — interestingly the test mocks ({ methodY: jest.fn() }) resolve perfectly (13/13, exactly matching grep), while none of the 11 real call sites do.
What I ruled out
Query syntax — the same MATCH (a)-[e]->(b) WHERE b.name = ... pattern returns rows for A, and MATCH (a)-[c:CALLS]->(b) ... LIMIT 5 returns rows generally.
Node missing — B and C both exist as Function / Method nodes with correct file_path and line ranges; search_graph finds them.
Test filtering — trace_path excludes tests by default, but the missing callers are production files; include_tests does not change it.
Wrong edge type — checked CALLS, USAGE, and unfiltered MATCH (a)-[e]->(b).
Ambiguity — B has a same-named function in another workspace package; I traced the fully qualified name explicitly.
Impact
trace_path reports callers_total: 0 with no signal that resolution failed. The server instructions do say "Coverage is best-effort, never proof of completeness", and that's the right framing — but at the API surface a genuine zero and a resolution failure are the same value. Anything doing impact analysis ("is this safe to change?") will read a miss as "safe".
Suggestions (in preference order)
Surface unresolved call sites. The indexer already records strategy / confidence on CALLS edges and has a "missed graph" for partially parsed files. If a call expression was seen but not resolved to a target, exposing it (an unresolved marker, or a per-symbol unresolved_callsites count on trace_path) would turn a silent zero into an actionable one.
Let trace_path distinguishcallers_total: 0 (no inbound edges) from "0 resolved, N unresolved candidates".
If neither is cheap: document that a zero from trace_path should be cross-checked with search_code / grep before drawing a negative conclusion.
I have not isolated a minimal reproducer — I did not want to claim one I hadn't verified. The repo is private, but I'm happy to run any diagnostic command against it and paste output, or to try a specific hypothesis about what distinguishes A from B.
Everything above is measured, not inferred; identifiers are anonymized because the repository is private.
Thanks for the tool — the graph and the visualizer are genuinely useful, which is why this particular sharp edge seemed worth reporting.
Summary
In a TypeScript monorepo,
CALLSedges are missing for some directly-imported function calls, while structurally identical calls to a sibling helper resolve correctly. Because the misses are silent,trace_path(direction="inbound")returnscallers_total: 0, which is easy to read as "this function has no callers" when it actually has several.I'm not reporting "call resolution is broken" — most of the graph looks good. I'm reporting that the failure is indistinguishable from a true negative, and that it isn't explained by any call-shape rule I could find.
Environment
codebase-memory-mcp@0.10.5(npm), Windows 11cli query_graphand the MCPtrace_path/query_graphtools (same results)Observed
Three symbols, same repo, same index, same query shape:
helperA— exported function inmodules/pricing/util.tsimport { helperA }+ direct callhelperB— exported function inmodules/campaign/engine.tsimport { helperB }+ direct callCALLS(onlyDEFINES)ServiceX.methodY— class methodthis.injected.methodY(...)(NestJS DI)USAGEedges, all in*.spec.tsA and B have the identical call shape — plain named import, direct invocation, callers are class methods in other modules. One resolves, the other does not. That's what makes this hard to work around: there is no rule I can give a user (or myself) for when to trust a zero.
Query used (same for all three):
For B the only inbound edges are
DEFINES. For C the only inbound edges areUSAGEfrom test files — interestingly the test mocks ({ methodY: jest.fn() }) resolve perfectly (13/13, exactly matching grep), while none of the 11 real call sites do.What I ruled out
MATCH (a)-[e]->(b) WHERE b.name = ...pattern returns rows for A, andMATCH (a)-[c:CALLS]->(b) ... LIMIT 5returns rows generally.Function/Methodnodes with correctfile_pathand line ranges;search_graphfinds them.trace_pathexcludes tests by default, but the missing callers are production files;include_testsdoes not change it.CALLS,USAGE, and unfilteredMATCH (a)-[e]->(b).Impact
trace_pathreportscallers_total: 0with no signal that resolution failed. The server instructions do say "Coverage is best-effort, never proof of completeness", and that's the right framing — but at the API surface a genuine zero and a resolution failure are the same value. Anything doing impact analysis ("is this safe to change?") will read a miss as "safe".Suggestions (in preference order)
strategy/confidenceonCALLSedges and has a "missed graph" for partially parsed files. If a call expression was seen but not resolved to a target, exposing it (anunresolvedmarker, or a per-symbolunresolved_callsitescount ontrace_path) would turn a silent zero into an actionable one.trace_pathdistinguishcallers_total: 0(no inbound edges) from "0 resolved, N unresolved candidates".trace_pathshould be cross-checked withsearch_code/ grep before drawing a negative conclusion.Notes
Thanks for the tool — the graph and the visualizer are genuinely useful, which is why this particular sharp edge seemed worth reporting.