Repository navigation
Description
Version
codebase-memory-mcp 0.8.1
Platform
(pick yours — mine was Linux (x64))
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
A single import { A, B } from './lib' statement — two named symbols, one specifier, zero ambiguity, no aliasing involved at all — produces exactly one IMPORTS edge in the graph, carrying only one of the two local_name values. The other imported (and actually used) symbol has no edge at all: it's as if that half of the import statement was never parsed.
Expected: the graph should represent both A and B as being imported by the consumer — either as two separate IMPORTS edges (one per symbol) or as a single edge whose local_name/equivalent property is a list of all symbols imported from that specifier. Either way, no imported-and-used symbol should be silently dropped.
This causes real correctness problems downstream: "who imports X" / caller / dead-code / impact-analysis queries return zero results for whichever symbol got dropped, even though the symbol is genuinely imported and called in the file. Worse, it's not limited to those queries — see "Root cause" below: the same dropped edge also breaks cross-file call resolution for that symbol, not just the import edge itself.
Reproduction
- Minimal dummy repo (no tsconfig paths/aliases needed — plain relative import, to isolate this from any alias-resolution issue):
src/lib.ts:
export const A = {
method: () => 'value',
};
export const B = {
check: () => true,
};src/consumer.ts:
import { A, B } from './lib';
export function run() {
return B.check() && A.method();
}- Commands:
codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/multi-import-demo"}'
codebase-memory-mcp cli query_graph '{"project":"multi-import-demo","query":"MATCH (f)-[r:IMPORTS]->(t) WHERE f.file_path CONTAINS \"consumer.ts\" RETURN t.file_path, r.local_name"}'
(adjust project to whatever name the indexer assigns your repo)
- Result vs expected:
- Actual (reproduced and confirmed against the built binary): exactly 1 row returned —
[["src/lib.ts","B"]].Ahas zero representation as anIMPORTSedge, even though it's imported and called in the file. - Expected: 2 rows (or 1 row with both names captured) — both symbols visible.
- Actual (reproduced and confirmed against the built binary): exactly 1 row returned —
Real-world instance this was distilled from: a production TypeScript monorepo, import { A, B } from '@lib' in a consumer file, resolving cleanly to src/app/lib (no alias ambiguity — @lib is a bare, non-wildcard tsconfig entry). Graph kept the B edge; A had zero representation, even though search_graph/grep both confirm it's imported and called in that file.
Root cause (verified by reading the edge-writer and reproducing against the built binary)
cbm_gbuf_insert_edge() in src/graph_buffer/graph_buffer.c dedups every edge purely on (source_id, target_id, type) — see make_edge_key() (~line 138) and its use inside cbm_gbuf_insert_edge() (~line 914). Two named imports from the same specifier resolve to the same (source_file, target_file) pair, so the second IMPORTS edge collides on that key with the first. On collision the function does not merge or reject — it replaces the existing edge's properties_json outright:
cbm_gbuf_edge_t *existing = cbm_ht_get(gb->edge_by_key, key);
if (existing) {
/* Merge properties (just replace for now) */
if (properties_json && strcmp(properties_json, "{}") != 0) {
free(existing->properties_json);
existing->properties_json = heap_strdup(properties_json);
}
return existing->id;
}The (just replace for now) comment suggests this was a known stub, never finished.
This has a bigger blast radius than just "who imports X" queries: src/pipeline/pass_calls.c, pass_usages.c, pass_semantic.c, and pass_lsp_cross.c all build their local_name → resolved module QN maps for cross-file call resolution by walking each file's IMPORTS edges and parsing exactly one local_name out of each edge's properties_json (strstr(props, "\"local_name\":\""), single value, no list handling). So the dropped symbol's calls also fail to resolve cross-file — this bug silently degrades call-graph accuracy, not just import-edge visibility.
Suggested fix direction
Key IMPORTS edges specifically on (source_id, target_id, type, local_name) instead of (source_id, target_id, type), so two different symbols imported from the same specifier become two distinct edges. This requires no changes to any of the four downstream consumers listed above — they already iterate edges and read one local_name per edge; they will simply see both edges once dedup stops collapsing them. Other edge types (CALLS, HTTP_CALLS, DEFINES, ...) should keep their current (source, target, type) key unchanged, since collapsing multiple call sites of the same callee into one edge there is presumably intentional.
Logs
(none — pure graph-construction/dedup issue, not a crash/perf issue)
Diagnostics trajectory (memory / performance / leak issues)
N/A
Project scale (if relevant)
Reproduces on a 2-file dummy repo; also observed on a ~3,300-node / ~7,600-edge production repo (multiple files affected, not isolated to one).
Confirmations
- I searched existing issues and this is not a duplicate. (Related but distinct: ES/TS module specifiers produce zero IMPORTS edges — pipeline resolves by name only #180, closed, covers zero IMPORTS edges from unresolved package-name specifiers entirely — a different failure mode from this one, where resolution succeeds but only one of several names from the same specifier is kept.)
- My reproduction uses shareable code (a dummy snippet), not proprietary code.
Activity
DeusData commentedon Jul 3, 2026
Thanks for the precise IMPORTS-edge report. Triage: parsing/import graph correctness under #592.
A multi-name import must preserve each imported local symbol; silently replacing one edge with another makes downstream import/users queries unreliable. I see #770 already targets this. We will review it against this issue and make sure the regression covers two named imports from the same specifier.
fix(graph): IMPORTS edges keyed by local_name across buffer, store an…
fix(ui): open project store read-only for graph-view endpoints
Merge pull request #2343 from DeusData/fix/issue-2133
fix(cross-repo): a pre-DeusData#768 store no longer aborts ["*"] or w…
feat(cross-repo): report run status and skipped legacy stores (#2133)
test(cross-repo): free every matcher result the tests receive