Summary
codegraph callers|callees (default --limit 20) and codegraph query (default --limit 10) cut their results and emit nothing that says a cut happened:
--json returns { symbol, callers: [...] } — no total, no truncated, no echo of the applied limit. query --json returns a bare array, so there is not even an envelope a total could live in.
- The human path prints the truncated count as if it were the total:
Callers of "target" (20): when there are 50.
The true total is in scope at the line that discards it (allCallers.length), so nothing has to be recomputed to report it.
This matters more than a missing convenience field, because getCallers merges imports, calls, references and instantiates into one array in edge-insertion order. Which rows survive the cut is therefore arbitrary with respect to kind, and a consumer cannot tell a complete answer from a slice of one. In the minimal repro below the survivors happen to be all function; on a real repository (below) they happened to be all file, so the default answer for two symbols contained zero of the symbol-level callers the command exists to return.
codegraph_explore — the one tool the MCP server exposes by default — already does this right, marking its own elisions inline (+5 more, +27 more). This report is only asking for that existing pattern on the callers/callees/query path.
Adjacent but distinct: #1512 concerns same-named definitions being merged in these same functions. This one is about the cut, and is orthogonal to that fix.
Root cause
src/bin/codegraph.ts, callers (identical shape in callees at :2069):
const limit = parseInt(options.limit || '20', 10); // :1957
...
const limited = allCallers.slice(0, limit); // :1991
if (options.json) {
console.log(JSON.stringify({ symbol, callers: limited }, null, 2)); // :1994 <- no total
} else if (limited.length === 0) {
info(`No callers found for "${symbol}"`);
} else {
console.log(chalk.bold(`\nCallers of "${symbol}" (${limited.length}):\n`)); // :1998 <- truncated count printed as the total
}
allCallers.length is live at :1991 and dropped.
Repro A — minimal, executed end-to-end
26 files: one definition, 25 modules that each import it and call it from a function.
mkdir repro && cd repro
printf 'def target():\n return 1\n' > lib.py
for i in $(seq -w 0 24); do
printf 'from lib import target\n\n\ndef caller_%s():\n return target()\n' "$i" > "c$i.py"
done
codegraph init . -y # 26 files, 77 nodes, 101 edges
$ codegraph callers target --json | jq '.callers | length, (group_by(.kind)[] | {(.[0].kind): length})'
20
{"function": 20}
$ codegraph callers target --limit 500 --json | jq '.callers | length, (group_by(.kind)[] | {(.[0].kind): length})'
50
{"file": 25}
{"function": 25}
$ codegraph callers target | head -2
Callers of "target" (20):
There are 50. The command says 20, and the JSON gives a consumer no way to learn otherwise.
query in the same project:
$ codegraph query target --json | jq length # 10
$ codegraph query target --limit 500 --json | jq length # 26
Repro B — a real repository, executed end-to-end
A 207-file Python package (5 113 nodes, 15 247 edges), five well-connected symbols, default limit vs --limit 500:
| symbol |
default |
full |
symbol-level callers hidden by the default |
MACDZoneAnalyzer |
20 (0 symbols, 20 files) |
79 (58 symbols, 21 files) |
all 58 |
ZoneInfo |
20 (0 symbols) |
62 (33 symbols) |
all 33 |
ZoneFeaturesAnalyzer |
20 (11 symbols) |
51 (42 symbols) |
31 |
analyze_zones |
20 (20 symbols) |
72 (49 symbols) |
29 |
get_logger |
20 (20 symbols) |
104 (40 symbols) |
20 |
On the first two, the file rows from imports occupied the first 21 positions of allCallers, so the default returned only files. A reader with no reason to suspect a limit would reasonably conclude the tool models callers as file-level import fan-in rather than call sites. It does not — that is the truncation talking.
Suggested fix
Minimal, and consistent with what explore already does:
- JSON — carry the counts, e.g.
{ symbol, callers, total: allCallers.length, limit, truncated: allCallers.length > limit }. Emitting the block unconditionally (with truncated: false when everything fit) is worth a thought: an absent field is itself ambiguous to a machine consumer, which cannot distinguish "not truncated" from "this version does not report truncation".
- Human — print the total in the header and a footer when cut, e.g.
Callers of "target" (20 of 50 — pass --limit to widen).
- Same for
callees (:2069) and query. query returns a bare array today, so it needs either an envelope or a stderr note.
Environment
CodeGraph 1.6.0 (npm @colbymchenry/codegraph), Linux x64, Node v24.12.0, Python targets, telemetry off via DO_NOT_TRACK=1. Source read at 6a056ec.
Not claimed here
- I am not asking for a particular ordering of
allCallers. The order is an implementation detail and may well be fine; the request is only that the cut be visible.
- The MCP
callers/callees handlers were not tested — this is the CLI path.
Found while benchmarking CodeGraph against my own Python code-graph tool on a shared, hash-pinned scope; the full measurement, including where CodeGraph came out ahead, is public in that project's tool card. The first run of this query is what produced the wrong conclusion described above, which is why it seemed worth reporting rather than working around.
Summary
codegraph callers|callees(default--limit 20) andcodegraph query(default--limit 10) cut their results and emit nothing that says a cut happened:--jsonreturns{ symbol, callers: [...] }— no total, notruncated, no echo of the applied limit.query --jsonreturns a bare array, so there is not even an envelope a total could live in.Callers of "target" (20):when there are 50.The true total is in scope at the line that discards it (
allCallers.length), so nothing has to be recomputed to report it.This matters more than a missing convenience field, because
getCallersmergesimports,calls,referencesandinstantiatesinto one array in edge-insertion order. Which rows survive the cut is therefore arbitrary with respect to kind, and a consumer cannot tell a complete answer from a slice of one. In the minimal repro below the survivors happen to be allfunction; on a real repository (below) they happened to be allfile, so the default answer for two symbols contained zero of the symbol-level callers the command exists to return.codegraph_explore— the one tool the MCP server exposes by default — already does this right, marking its own elisions inline (+5 more,+27 more). This report is only asking for that existing pattern on thecallers/callees/querypath.Adjacent but distinct: #1512 concerns same-named definitions being merged in these same functions. This one is about the cut, and is orthogonal to that fix.
Root cause
src/bin/codegraph.ts,callers(identical shape incalleesat:2069):allCallers.lengthis live at:1991and dropped.Repro A — minimal, executed end-to-end
26 files: one definition, 25 modules that each import it and call it from a function.
There are 50. The command says 20, and the JSON gives a consumer no way to learn otherwise.
queryin the same project:Repro B — a real repository, executed end-to-end
A 207-file Python package (5 113 nodes, 15 247 edges), five well-connected symbols, default limit vs
--limit 500:MACDZoneAnalyzerZoneInfoZoneFeaturesAnalyzeranalyze_zonesget_loggerOn the first two, the file rows from
importsoccupied the first 21 positions ofallCallers, so the default returned only files. A reader with no reason to suspect a limit would reasonably conclude the tool models callers as file-level import fan-in rather than call sites. It does not — that is the truncation talking.Suggested fix
Minimal, and consistent with what
explorealready does:{ symbol, callers, total: allCallers.length, limit, truncated: allCallers.length > limit }. Emitting the block unconditionally (withtruncated: falsewhen everything fit) is worth a thought: an absent field is itself ambiguous to a machine consumer, which cannot distinguish "not truncated" from "this version does not report truncation".Callers of "target" (20 of 50 — pass --limit to widen).callees(:2069) andquery.queryreturns a bare array today, so it needs either an envelope or a stderr note.Environment
CodeGraph 1.6.0 (npm
@colbymchenry/codegraph), Linux x64, Node v24.12.0, Python targets, telemetry off viaDO_NOT_TRACK=1. Source read at6a056ec.Not claimed here
allCallers. The order is an implementation detail and may well be fine; the request is only that the cut be visible.callers/calleeshandlers were not tested — this is the CLI path.Found while benchmarking CodeGraph against my own Python code-graph tool on a shared, hash-pinned scope; the full measurement, including where CodeGraph came out ahead, is public in that project's tool card. The first run of this query is what produced the wrong conclusion described above, which is why it seemed worth reporting rather than working around.