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.ts types → edge points to the .d.ts stub 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.ts declaration should not create a CALLS-target node separate from the implementation (merge / alias the two), or
trace_path and reverse queries should union nodes that share name + 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.
Version
codebase-memory-mcp0.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.tsfile, the indexer creates two distinctFunctionnodes for the same logical symbol.CALLSedges from consumers are then partitioned across the two nodes depending on how each consumer imports the symbol:.d.tstypes → edge points to the.d.tsstub nodeAs 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
tsconfigpath 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)
Index the repo, then:
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:.d.tsdeclaration should not create aCALLS-target node separate from the implementation (merge / alias the two), ortrace_pathand reverse queries should union nodes that sharename+signature, orActual
Two separate
Functionnodes; callers partitioned by import style;trace_pathreturns 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.tsdeclaration 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.