Repository navigation
trace_path / reverse-dependency returns an INCOMPLETE caller set when a symbol is duplicated by an ambient .d.ts declaration (callers silently split by import style) #546
Description
Version
codebase-memory-mcp 0.8.1 (CLI)
Summary
When an exported symbol is defined in real source and also re-declared (signature only, no body) in an ambient .d.ts file, the indexer creates two distinct Function nodes for the same logical symbol. CALLS edges from consumers are then partitioned across the two nodes depending on how each consumer imports the symbol:
- consumers importing via the path alias that the
.d.tstypes → edge points to the.d.tsstub node - consumers importing via the relative path to the real file → edge points to the implementation node
As a result, trace_path(name, mode=calls) resolves the name to one of the two nodes and returns only that node's callers, silently omitting the rest. There is no warning that the symbol is fragmented, so reverse-dependency / impact analysis under-reports consumers.
Environment (where this naturally occurs)
A TypeScript monorepo where an app consumes a workspace package through a tsconfig path alias, and the app ships a hand-written ambient declaration file to type that alias. This is a common pattern, so the duplicate-node split is easy to hit in real repos.
Minimal reproduction (synthetic)
repo/
packages/widget/src/scroll.ts
export function alignToEdge(el: HTMLElement): () => void { /* real impl */ }
packages/widget/src/internalConsumer.ts
import { alignToEdge } from './scroll' // relative import
alignToEdge(node)
app/types/widget-shim.d.ts
export function alignToEdge(el: HTMLElement): () => void // signature only, no body
app/src/externalConsumer.ts
import { alignToEdge } from '@widget' // path-alias import, typed by widget-shim.d.ts
alignToEdge(node)
# tsconfig paths: "@widget": ["packages/widget/src"] (alias typed by the .d.ts shim)
Index the repo, then:
query_graph: MATCH (n {name:'alignToEdge'}) RETURN n.qualified_name
-> 2 nodes: packages/widget/src/scroll.ts AND app/types/widget-shim.d.ts
trace_path(function_name='alignToEdge', mode='calls')
-> returns callers of ONE node only (e.g. externalConsumer), missing internalConsumer
query_graph: MATCH (n)-[:CALLS]->(t {qualified_name:'<impl node>'}) RETURN n -> internalConsumer
query_graph: MATCH (n)-[:CALLS]->(t {qualified_name:'<.d.ts node>'}) RETURN n -> externalConsumer
Neither query alone yields the full {internalConsumer, externalConsumer} set.
Expected
Reverse-dependency / trace_path(mode=calls) for a logical symbol should return all of its callers, regardless of whether each caller imported via the real path or via an alias typed by an ambient .d.ts. Either:
- a body-less ambient
.d.tsdeclaration should not create aCALLS-target node separate from the implementation (merge / alias the two), or trace_pathand reverse queries should union nodes that sharename+signature, or- at minimum, the result should flag that the symbol resolves to multiple nodes so callers know the set is partial.
Actual
Two separate Function nodes; callers partitioned by import style; trace_path returns a partial set with no indication it is incomplete.
Impact
This directly undercuts the impact-analysis / blast-radius use case the tool is built for: "who depends on this symbol if I change it?" returns a confidently partial answer, which is worse than no answer for change-safety decisions.
Question
Is the duplicate node from a body-less ambient .d.ts declaration intended? If so, is there a recommended query pattern to retrieve the unified caller set across alias-typed and real-path consumers? Happy to provide more detail.
Activity
DeusData commentedon Jun 22, 2026
Hey, thx for noticing, will check ASAP
test(repro): fix harness include + #408 comment; add #571, #523, #546
fix: trace_path returns full caller set across .d.ts-duplicated symbo…
mvanhorn commentedon Jun 27, 2026
Implemented in #650.
test(repro): match MCP-envelope escaping in trace_path repros (#480/#514
DeusData commentedon Jun 28, 2026
This issue now has a first-class, durable reproduction in the cumulative bug-reproduction suite landing in #667 — a test that pins the exact failing behaviour, so it's tracked and guarded against silent regression. Where #667 also lands the root-cause fix, that's called out in the PR; otherwise the reproduction makes the eventual fix straightforward and verifiable.
The reproduction goes live once #667 merges. (Posted from the release branch's QA pass.)
fix: 17 reproduce-first issue + new-bug fixes (per-case verified)
style: clang-format issue-batch additions (cypher/mcp)
DeusData commentedon Jul 1, 2026
Fixed on
mainin3f754cfviabfs_union_same_name(src/mcp/mcp.c):trace_pathnow BFSs from every graph node sharing the resolved name and merges the results deduplicated by node id, so when a symbol is split across a real implementation and a body-less ambient.d.tsstub — withCALLSedges partitioned by import style — the full caller/callee set is returned instead of only one node's.A follow-up (#748) additionally hardens the surrounding resolution so that genuinely-different same-named functions are disambiguated rather than conflated, while the impl-plus-
.d.ts-stub case here keeps unioning. Both are guarded by reproduce-first tests.Closing as completed. Thanks @mvanhorn for the precise root-cause report (and the independent fix in #650).
fix(mcp): trace_path must not conflate distinct same-named functions