find_callers returns a structural zero with no disclosure, while search_symbols discloses the same fact fully #99
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#99
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during #84 Phase 2 by the SDK lane, and reproduced independently.
The asymmetry
A
const,static,moduleorimplis used as a bare identifier, and the extractors do not record a reference kind that captures such a use. So a zero on one of those symbols is structural — the population was empty and nothing could ever have been counted — not a measurement that nothing uses it.search_symbolssays exactly that, at length:find_callerson the same symbol says nothing:The
confidenceblock is generic — it is the same text for a function with genuinely no callers. Nothing in the reply distinguishes "nothing calls this" from "a const cannot have callers and this tool cannot answer the question you asked".Why this matters more than it looks
This is the project's own central rule — which zeros are uninformative — applied on one tool and not its neighbour. An agent that has learned to trust
name_fallback_unmeasuredfromsearch_symbolswill readfind_callers' baretotal: 0as the measured absence it is not, and conclude the symbol is dead.search_symbolsexplicitly warns against that conclusion;find_callersinvites it.It is also the exact shape of the defect closed in #97: a fact the system knows, disclosed on one surface and silent on another, where the silent one looks like a normal answer.
What the right answer looks like
find_callersalready knows the symbol's kind and language, which is allsearch_symbolsuses to decide. When the target is a kind with no use-reference channel, the reply should say so and name the tool that CAN answer —find_references, which spans ref kinds that are not calls — rather than returning an unqualified empty list.Suggested shape, reusing the existing vocabulary rather than inventing a second one:
Absent when it does not apply, present when it does — so a bare
total: 0keeps meaning "measured, and nothing calls it".Check the neighbours too
find_calleesandchange_impactseed from the same graph and may carry the same silence for the same kinds. Whatever shape is chosen should be applied wherever the population can be structurally empty, not patched ontofind_callersalone — the fix for #97 covered all 23 tools by construction for exactly this reason.Fixed by the same single mechanism as #101 — full detail in #101's close comment.
The part specific to this issue:
annotate_evidence_gapsreadsSymbolRow::name_fallback_unmeasuredoffget_symbolfor everysymbol_id/symbol_idsin the request, and publishes it in theevidence_gapsblock. That is deliberately the same field from the same producer thatsearch_symbolsalready publishes, so the two tools cannot come to disagree about the same symbol — which was the asymmetry this issue reported.Covered:
find_callers,find_callees,change_impact,find_references,safe_delete,check_rename. The grader sits above the router, so this is not a per-tool patch and a future tool 25 is covered without opting in.The mutation that proves it is not vacuous:
probe_unmeasured_population→ empty goes RED withleft: None / right: Some(2)on a body that is verbatim this issue's bug report.One deliberate narrowing, made on evidence
My first version also downgraded the absence verdict on this fact — i.e.
safe_deletewould dropno_evidence_of_usewhen the population was unmeasured. The pre-existingrefactor_tools_disclose_a_saturated_fts_candidate_windowwent red onWidget, an ordinary struct, becauseno_use_reference_channelfires wherever the index demonstrates there is no use-channel for a (kind, lang) pair — which on a small repo is most kinds. A guard that fires on nearly everything discloses nothing.So the split is: #99 gets disclosure beside the verdict; #101 gets the downgrade. The reasoning is written into
downgrade_absence_verdict's doc so it is not silently re-widened later.Known limit, stated rather than implied
explain_dependency'sfrom/toandrepo_map'sfocus_idscarry symbol ids under generic names, and the grader reads onlysymbol_id/symbol_ids. Widening to generic names would make a futurefrom: "2026-01-01"a symbol probe. Documented atseed_symbol_ids.A structural zero is still a zero — a function nothing calls reports a bare
total: 0with no block, because that zero is earned.evidence_gapsgrader, so the same data is served graded through tools and ungraded through resources #107change_impactreturns an empty, confident answer wherefind_callersfinds 5 call sites — and nothing in its payload says why #122Response::Unclaimedis the same silent-loss door as #115, still open: three causes collapse into one count that reads as the designed case #123change_impactreturns an empty, confident answer wherefind_callersfinds 5 call sites — and nothing in its payload says why #122archive_refusedreaches nocoverage_reasonscode, so an agent's answer is qualified by nothing #124response_format: "concise"dropsinfluenceandresolved_by, so the dynamic-influence disclosure is invisible in the cheapest format we ship #139evidence_gaps.partial_sources_in_index: 1fires on EVERY reply whilepartial_sourcesis never populated — a three-state disclosure that only ever renders its unfalsifiable state #200