Skip to content

CLI callers/callees/impact merge every same-named definition into one unlabeled result and expose no --file, while the MCP tools group and filter on the same index — the "CLI parity" surface returns a union true of no single definition — verified end-to-end #1512

Description

@inth3shadows

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions