Skip to content

TypeScript: CALLS edges silently missing for some directly-imported functions — trace_path reports callers_total: 0 (v0.10.5) #1682

Description

@softwaredeveto

Summary

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.

Environment

  • codebase-memory-mcp@0.10.5 (npm), Windows 11
  • TypeScript monorepo: NestJS API + React (Vite) + React Native, pnpm workspaces
  • Index: 21,937 nodes / 115,747 edges, ~56 s
  • 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 0 CALLS (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) WHERE b.name = '<symbol>'
RETURN type(e) AS edge, a.name AS source, a.file_path AS file

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

  1. 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.
  2. Let trace_path distinguish callers_total: 0 (no inbound edges) from "0 resolved, N unresolved candidates".
  3. If neither is cheap: document that a zero from trace_path should be cross-checked with search_code / grep before drawing a negative conclusion.

Notes

  • Possibly related but not the same: Call resolution picks the wrong target (or none) for common method names in a Kotlin monorepo #1555 (Kotlin, resolution picks the wrong target; here nothing is produced at all).
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cypherCypher query language parser/executor bugsparsing/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