#849 found get_affected_flows returning ~247k tokens inside a workflow
documented as "5 tool calls, 800 tokens total". PR #853 capped that one
tool. Measuring all 30 registered tools against a real 5.6k-node graph
found the same class of bug in ten more places, several of them on the
default path:
list_communities 206,858 tokens with DEFAULT arguments
get_community 134,781 default / 535,618 with members
get_architecture_overview 625,012 in standard mode
refactor dead_code 47,312 / suggest 38,246
detect_changes 46,089 for a ONE-file diff
get_surprising_connections 1,287,174 at top_n=10**6
get_hub_nodes 555,848 / get_bridge_nodes 317,202
get_review_context 4,720,622 on a whole-repo diff
PR #853's own fix was also only half a fix: standard mode carries a full
steps list per flow (~980 tokens each), so its 50-flow cap still produced
~49k tokens, and max_flows=0 disabled the limit entirely.
Every list-returning tool now follows one contract, the one #853
established: `total` (or a per-list `*_total`) always reports the
untruncated count, `truncated` marks the cut, and the summary line says
how many of how many are shown. Bounds are validated the way query.py
validates max_results - booleans rejected, values below 1 rejected.
detail_level="minimal" was added to the analysis and refactor tools,
projecting to the same compact field sets their siblings use.
Where a count alone cannot bound a response, a shared budget does:
get_flow and get_affected_flows spend a step budget, get_review_context
and detect_changes spend a source-line budget. Ceilings that depend on
payload size depend on detail_level, mirroring query.py capping
minimal-mode results at five.
Two behaviour changes are deliberate and their #853 tests are updated in
this commit: get_affected_flows standard mode now caps at 25 flows
(minimal at 500), and max_flows=0 keeps its "no caller limit" meaning
while still obeying the ceiling - an escape hatch that returns a quarter
of a million tokens is the bug, not a feature.
Default behaviour stays backward compatible in shape: an existing caller
passing nothing still gets a valid response, just bounded.
tests/test_token_budget.py records the measured budget table as
reviewable data and pins it three ways: per-tool default and worst-case
token ceilings, exact truncated list lengths against the imported
ceiling constants, and the ceiling constants themselves. Removing a cap,
raising a ceiling, or adding an unbounded field fails it. The fixture
graph builds once per module and the whole file runs in ~7s, offline.
Reported, not fixed: code_review_graph/tools/query.py is owned elsewhere
and four of its tools remain unbounded - get_impact_radius (3.4M tokens;
changed_nodes and edges ignore max_results, which is not even exposed on
the MCP signature), find_large_functions (737k), traverse_graph (385k)
and semantic_search_nodes, whose limit/token_budget are neither
validated nor capped. Their default budgets are still asserted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fHfGDiZedoxjpKzanHri3
The wheel force-included docs/LLM-OPTIMIZED-REFERENCE.md to
code_review_graph/docs/. In a normal wheel that is harmless, but in an
editable install the modules live in the source tree behind a .pth, so
hatchling materialised a stub site-packages/code_review_graph/ containing only
docs/. That stub shadowed the real package, and the console script died with
"No module named code_review_graph.cli".
It looked intermittent because sys.path[0] is the cwd: from the repo root the
real package was found first and masked the stub, so it only failed from other
directories -- and every `uv run` sync recreated the stub.
The file now lives inside the package at code_review_graph/docs/, so the wheel
ships it via `packages` with no force-include and no stub. Verified: console
script works from any cwd and survives a `uv run` sync, and a wheel built and
installed into a clean venv still resolves get_docs_section (status ok).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNv8JqBb46stATZYUinQtn