feat(query): say why a result set is empty instead of returning a bare 0 - #884
Merged
Conversation
`query_graph(pattern="callers_of", target="X")` returned `result_count: 0` with no signal, and agents read that as "X has no callers". A zero can also mean the graph cannot see the answer: X was never indexed, the graph is behind the working tree, or X's language has a known static-analysis blind spot. Reading it the first way either produces a wrong decision or sends the agent off to grep the repository, which costs thousands of tokens. Empty results from query_graph, get_impact_radius, and semantic_search_nodes now carry a single flat `confidence` string computed in priority order: target not indexed, graph stale, known language/pattern gap, or a positively confirmed real absence. The gap table lives in the new uncertainty module as data, is scoped per pattern so a container-resolution caveat never lands on file_summary, and every entry was verified against the parser as it stands today (#819 PHP include/require, #850/#851 PHP container and constructor injection, #343 npm-aliased imports, #592 Java Spring AOP, plus JS/TS callbacks and route registration, Go structural interface satisfaction, C# DI and Python getattr/registry dispatch). Token cost is the constraint, so the field is emitted only when the result list is empty — responses that carry results are unchanged — and is capped at 140 characters, measured at +105..+130 chars (~27-33 tokens) on a typical empty response. That is a saving, not a cost, against the fallback it prevents. Everything reaching the client goes through _sanitize_name, has whitespace collapsed so a crafted node name cannot forge extra lines, and is clipped to the cap; qualified names drop to their symbol rather than keeping a truncated directory prefix. Any failure computing the marker degrades to omitting it. Refs #314 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fHfGDiZedoxjpKzanHri3
code-review-graph reviewOverall risk: 0.75 (HIGH) — 52 changed function(s)/class(es), 0 affected flow(s), 15 test gap(s) Risk-scored changes
Test gaps
Token savings: this graph-backed report used ~31,427 fewer tokens (~81%) than reading every changed file in full (estimated, chars/4 approximation). Powered by code-review-graph — local-first analysis; no code leaves the CI runner. |
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A zero from the graph is currently indistinguishable from an absence in the code.
query_graph_tool(pattern="callers_of", target="X")returnsresult_count: 0whether X has no callers, X is not indexed, the graph is stale, or the language cannot express that relationship statically. Agents read the zero as proof and act on it. Issues #314, #850, #851 and #819 all describe this failure, and #851 states it directly: a zero with no uncertainty marker is the dangerous direction.Change
Adds one optional response key,
confidence, a short string present only when the result list is empty. First match wins:target not indexed: no node matching 'X', so this 0 is not evidence that none existphp container-resolved and constructor-injected calls are not statically traced, so callers can be missing (#850, #851)'X' is indexed and the graph is current, so this 0 is a real absence, degrading tograph currency unverifiedwhen currency could not be established rather than over-claimingThe gap table lives as data in the new
uncertainty.py, keyed by language and by query pattern, so a container-resolution note appears oncallers_ofandtests_forbut not onfile_summary. Wired intoquery_graph,get_impact_radiusandsemantic_search_nodes. No tool signature changes.Token cost, measured
Worst case is 156 characters. The alternative behaviour, an agent that distrusts the zero and greps the repository, costs orders of magnitude more, so this is a net saving. A test asserts the exact absence of the key on non-empty responses so the budget cannot regress.
Accuracy of the gap table
Every claimed gap was verified against the parser with live parse runs rather than assumed:
type Impl struct { Base }yields only CONTAINS). Issue [Bug]: Go struct and interface embeddings never emitINHERITSedges #834 is open and its fix fix(go): model and resolve embedded type relationships #835 is still in review, so the note is worded to hold either way, on structural typing.references_tois excluded from the call-pattern set and the note says exactly that.Tests
50 new tests in
tests/test_uncertainty.py: unknown target versus real absence, per-language and per-pattern firing, stale detection by commit and by mtime, silent degradation on failure, length cap under hostile unicode and control characters, and the key-absence budget guard.Full suite 2759 passed, 5 skipped, 2 xpassed. ruff and mypy clean.
Relates to #314, #850, #851, #819.