Summary
codegraph callers|callees|impact <symbol> merge the edges of every same-named definition into one flat result printed under the bare symbol name. The merge is deliberate (// Merge impact subgraphs across all exact-matching symbols, src/bin/codegraph.ts:2030), but there is no --file option to scope the query and nothing in the output says a merge happened. When a name exists in more than one file — render, init, get, handle, String — the answer is a union that is true of no individual definition.
The MCP tools already solve both halves. handleCallers (src/mcp/tools.ts:2130), handleCallees (:2203) and handleImpact (:2273) each accept a file argument and route through groupDefinitions (:2093), so every definition keeps its own edge set, the header says how many definitions were found, and a file that matches nothing is reported rather than ignored.
The CLI commands read the same index and answer the same question with neither capability — while their own header comment states the purpose is "CLI parity with the MCP graph tools (codegraph_callers/callees/impact) so the traversal queries work in scripts, CI, and git hooks without a running MCP server" (src/bin/codegraph.ts:1843). The surface documented as the no-MCP-server equivalent is the one that silently merges.
Root cause
callers (src/bin/codegraph.ts:1848), callees (:1927), impact (:2005). All three declare only -p/--path, -j/--json, and -l/--limit (-d/--depth for impact). callees shows the shape; callers is structurally identical:
const matches = cg.searchNodes(symbol, { limit: 50 });
...
for (const match of matches) {
const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
if (!exactMatch && matches.length > 1) continue;
for (const c of cg.getCallees(match.node.id)) {
if (!seen.has(c.node.id)) { // <- one dedupe set ACROSS definitions
seen.add(c.node.id);
allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
}
}
}
...
console.log(JSON.stringify({ symbol, callees: limited }, null, 2));
Every exact-name match feeds one allCallees array behind a single seen set, and the JSON envelope is { symbol, callees } — the definition an edge belongs to is discarded, so a consumer cannot recover it even in --json mode. impact (:2030) does the same with mergedNodes/seenEdges.
The MCP path, same repo, same index:
// src/mcp/tools.ts:2203 handleCallees
const fileFilter = typeof args.file === 'string' ? args.file : undefined;
const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter);
const filterNote = filteredOut
? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
: '';
groupDefinitions keys by ${n.filePath}|${n.qualifiedName}.
Repro (executed end-to-end)
Six files — two handle definitions in different directories, each calling a different function and each called by a different caller:
a/alpha.js function alpha() { return 1; }
b/beta.js function beta() { return 2; }
a/svc.js function handle() { return alpha(); }
b/svc.js function handle() { return beta(); }
a/main.js function aMain() { return handle(); }
b/main.js function bMain() { return handle(); }
codegraph init on that directory, then:
$ codegraph callees --json -- handle
alpha (a/alpha.js), beta (b/beta.js)
$ codegraph callers --json -- handle
aMain (a/main.js), bMain (b/main.js)
$ codegraph callees --file a/svc.js --json -- handle
error: unknown option '--file'
Neither handle calls both alpha and beta; neither is called by both aMain and bMain. impact --json -- handle returns one merged affected set (nodeCount: 4) spanning both definitions' blast radii under a single symbol: "handle".
The MCP tools on the same index, captured from a real codegraph serve --mcp stdio session:
codegraph_callees {symbol: "handle"}
**Callees of handle — 2 distinct definitions (narrow with `file`)**
**handle** (function) — a/svc.js:2
- alpha (function) - a/alpha.js:1
**handle** (function) — b/svc.js:2
- beta (function) - b/beta.js:1
codegraph_callees {symbol: "handle", file: "a/svc.js"}
**Callees of handle (1 found)**
- alpha (function) - a/alpha.js:1
codegraph_callers {symbol: "handle"}
**Callers of handle — 2 distinct definitions (narrow with `file`)**
**handle** (function) — a/svc.js:2
- aMain (function) - a/main.js:2
**handle** (function) — b/svc.js:2
- bMain (function) - b/main.js:2
codegraph_impact {symbol: "handle"}
**Impact of handle — 2 distinct definitions (each with its own blast radius; narrow with `file`)**
...
Attributed and scopable on the MCP side; merged and unscopable on the CLI side.
Relationship to #1473 / PR #1481 (checked immediately before filing)
Different defect, and it survives that fix. #1473 and PR #1481 address the case where the typed name does not exist and a fuzzy hit is substituted. Here the name exists several times and every match is exact. PR #1481's rewritten callees block keeps the merge:
const exact = matches.filter((m) => isCliExactSymbolMatch(m.node.name, symbol));
...
for (const match of exact) {
for (const c of cg.getCallees(match.node.id)) { ... allCallees.push(...) }
}
With that PR applied, the repro above still returns alpha, beta, and --file is still not an option. (I read the callees hunk in full; the callers and impact hunks in that diff follow the same pattern.)
Suggested direction
Give the three CLI commands the --file option and the grouped output the MCP handlers already implement — ideally by routing them through groupDefinitions rather than reimplementing, so the two surfaces cannot drift again. In --json, nesting edges under their definition ([{definition: {filePath, startLine}, callees: [...]}, ...]) would make the attribution machine-readable; a flat {symbol, callees} cannot express a two-definition answer at all.
If merging stays the CLI default, the MCP side's — N distinct definitions header is a ready precedent for disclosing it.
Environment
Registry package 1.5.0; origin/main @ a7db24d read for the source references above. Node v22.23.1, Linux 6.6.87 (WSL2, Ubuntu 24.04). Repro directory indexed from scratch with codegraph init; MCP output captured from codegraph serve --mcp over stdio.
Summary
codegraph callers|callees|impact <symbol>merge the edges of every same-named definition into one flat result printed under the bare symbol name. The merge is deliberate (// Merge impact subgraphs across all exact-matching symbols,src/bin/codegraph.ts:2030), but there is no--fileoption to scope the query and nothing in the output says a merge happened. When a name exists in more than one file —render,init,get,handle,String— the answer is a union that is true of no individual definition.The MCP tools already solve both halves.
handleCallers(src/mcp/tools.ts:2130),handleCallees(:2203) andhandleImpact(:2273) each accept afileargument and route throughgroupDefinitions(:2093), so every definition keeps its own edge set, the header says how many definitions were found, and afilethat matches nothing is reported rather than ignored.The CLI commands read the same index and answer the same question with neither capability — while their own header comment states the purpose is "CLI parity with the MCP graph tools (codegraph_callers/callees/impact) so the traversal queries work in scripts, CI, and git hooks without a running MCP server" (
src/bin/codegraph.ts:1843). The surface documented as the no-MCP-server equivalent is the one that silently merges.Root cause
callers(src/bin/codegraph.ts:1848),callees(:1927),impact(:2005). All three declare only-p/--path,-j/--json, and-l/--limit(-d/--depthforimpact).calleesshows the shape;callersis structurally identical:Every exact-name match feeds one
allCalleesarray behind a singleseenset, and the JSON envelope is{ symbol, callees }— the definition an edge belongs to is discarded, so a consumer cannot recover it even in--jsonmode.impact(:2030) does the same withmergedNodes/seenEdges.The MCP path, same repo, same index:
groupDefinitionskeys by${n.filePath}|${n.qualifiedName}.Repro (executed end-to-end)
Six files — two
handledefinitions in different directories, each calling a different function and each called by a different caller:codegraph initon that directory, then:Neither
handlecalls bothalphaandbeta; neither is called by bothaMainandbMain.impact --json -- handlereturns one mergedaffectedset (nodeCount: 4) spanning both definitions' blast radii under a singlesymbol: "handle".The MCP tools on the same index, captured from a real
codegraph serve --mcpstdio session:Attributed and scopable on the MCP side; merged and unscopable on the CLI side.
Relationship to #1473 / PR #1481 (checked immediately before filing)
Different defect, and it survives that fix. #1473 and PR #1481 address the case where the typed name does not exist and a fuzzy hit is substituted. Here the name exists several times and every match is exact. PR #1481's rewritten
calleesblock keeps the merge:With that PR applied, the repro above still returns
alpha, beta, and--fileis still not an option. (I read thecalleeshunk in full; thecallersandimpacthunks in that diff follow the same pattern.)Suggested direction
Give the three CLI commands the
--fileoption and the grouped output the MCP handlers already implement — ideally by routing them throughgroupDefinitionsrather than reimplementing, so the two surfaces cannot drift again. In--json, nesting edges under their definition ([{definition: {filePath, startLine}, callees: [...]}, ...]) would make the attribution machine-readable; a flat{symbol, callees}cannot express a two-definition answer at all.If merging stays the CLI default, the MCP side's
— N distinct definitionsheader is a ready precedent for disclosing it.Environment
Registry package
1.5.0;origin/main@a7db24dread for the source references above. Node v22.23.1, Linux 6.6.87 (WSL2, Ubuntu 24.04). Repro directory indexed from scratch withcodegraph init; MCP output captured fromcodegraph serve --mcpover stdio.