An overload-ambiguous ref is disclosed on the symbol row but NOT in navigation confidence — find_callers/find_references still ship page_name_fallback: 0 with no shape-excluded sibling #173
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#173
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 while authoring hand-verified benchmark questions for #51. Reproduced on a purpose-built fixture through the real MCP server.
Measured
A 20-line fixture: one class declaring
Reset()andReset(bool), plus four unqualified calls toResetin the same file, plus one uniquely-named sibling method as a control.find_calleeson the enclosing symbol does reportunresolved_count: 2. So the information is present in the index. Only the caller-side view drops it.On the pinned
cs-dappercorpus this hidesDapper/SqlMapper.cs:258(ResetTypeHandlers(false);) and:264(… => ResetTypeHandlers(true);) fromfind_callerson thebooloverload. The bind is not wrong —search_symbolsreturns the intended symbol atstart_line: 266— the refs simply reach nothing. Both overloads reportref_count: 0andname_fallback_count: 16.Why the zero is unearned, and why that is the sharper half
name_fallback_countexists to bound how muchref_countundercounts, and this project ships a0there as an earned zero. On the fixture both counters read0for a method called four times in its own file.The name-fallback channel does not rescue it, and the reason is structural: that channel carries receiver-shaped
method_callrefs. These arecallrefs — unqualified invocations — so they are not eligible for it at all. The result is that ambiguity, which the resolver has explicitly classified, is rendered to the caller as absence.An agent asking "who calls this overload?" is told nobody does.
safe_deletesits on the same evidence.Mechanism — stated as partly inferred
Measured: the resolver classifies these refs as
ambiguous(resolution_gapssays so by name and count),find_calleessurfaces them asunresolved_count, and no caller-side tool does.Inferred, not traced: that the omission is because an
ambiguousref is left with notarget_idand the caller-side queries join ontarget_idwhile the callee-side query works from the enclosing symbol outward. That reading is consistent with every observation above but I did not read the query, and it should be confirmed before anyone designs against it.Why existing gates could not see it
precision_gategrades phantoms. Refusing to bind an ambiguous name is correct precision behaviour; the defect is that the refusal is not reported to the caller, which no phantom count can see.resolution_gapshas the fact and is a diagnostic surface by design (#45 acceptance 7 keeps resolution diagnostics structurally unable to gate). Nothing cross-checks a diagnostic against a caller-side answer.resolvedas a count; an ambiguous ref is consistently unresolved, so nothing moves.find_calleesreportingunresolved_countwhilefind_callersreports nothing is precisely the kind of two-tools-disagree gap #51's benchmark was built to expose, and it did.Repro
A C# file with
class T { void Reset() {} void Reset(bool b) {} void Only() {} void Go() { Reset(); Reset(); Reset(); Reset(); Only(); } }, indexed through the MCP server. Thenresolution_gaps,search_symbols("Reset"),find_referenceson eachReset, andfind_calleesonGo.What must NOT be done to make this pass
phantom_count == 0).callrefs. That channel's contract is receiver-shapedmethod_calls and its whole value is thatresolution: "name_fallback"means something specific. Diluting it makes every existing name-fallback row less informative to buy one more row here.search_symbolsalready uses forname_fallback_unmeasured, where an absent measurement says so rather than shipping a zero.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
method_calldraws only fromkind='method', and python.rs mints no module symbol at all #175CONFIRMED, and fixed by one clause shared with #175. The inferred mechanism was close but not right, and the correction matters.
The inference, corrected
The issue says, flagging it as inferred: "an
ambiguousref is left with notarget_idand the caller-side queries join ontarget_id." That is true but it is not what produces the unearned zero, and designing against it would have aimed the fix at the wrong query.Traced:
name_fallback_countdoes not join ontarget_id— it counts rows wheretarget_id IS NULL, which is exactly the ambiguous population. What removes them is one line up,crates/daemon/src/local_index.rs::fallback_call_kind_predicate:Four unqualified
Reset()sites arekind = 'call'withqualifier IS NULL. They fail every disjunct. So the issue's second sentence is right for the right reason and its first is right for the wrong one: the channel does not carry them, but not because it is "receiver-shapedmethod_call" — a qualifier-matchedType::assoc()callIS carried (that is I042). The gate is the call shape, and a bare call carries no shape evidence at all.That correction is what made the fix general instead of C#-specific.
The shared mechanism with #175 — one clause, not two patches
#175 is the mirror image in the OTHER arm of the same function:
whose comment names its own assumption — "a qualified
mod::func()is already kind='call'". That holds for Rust and PHP; it is false for Python, which emitspkg.f()asmethod_call(crates/plugins/src/python.rs:771). Soflask.send_from_directory(...)is dropped and a module-leveldefshipsname_fallback_count: 0— the pair our docs call TIGHT — with a live unresolved reference in the index.Two issues, two arms, one structural fact: a counter and its earned-zero gate are two predicates over one population, and only the counter's removals were ever reported.
What shipped
Same-name, same-language, unresolved, call-shaped refs that the call-shape filter behind
name_fallback_countREMOVED. It is the complement of the counting predicate within its own population — computed as one extra grouped query per bucket, keyed off the same two named clause constants (TYPE_FAMILY_CLAUSE/FREE_FN_CLAUSE, extracted so a counter and its disclosure cannot drift), net of anything the I042 qualifier disjunct already put back.It does exactly what the issue asked for and none of what the issue forbade:
phantom_count == 0untouched —precision_gatestill 7/7.name_fallback_countkeeps its definition and its value;resolution: "name_fallback"still means exactly what it meant.name_fallback_unmeasured— and it is PAIRED: present exactly whenname_fallback_countis, so a reader never gets a qualifier with nothing to qualify.What it explicitly is NOT: recall. The excluded rows are, by construction, of a call shape that cannot be a reference to this symbol under static rules — which is precisely why adding them to
name_fallback_countwould manufacture the phantoms the gate forbids. Read it as "this many same-name unresolved calls exist that this counter is structurally unable to classify";resolution_gapsholds their reasons.Measured on this issue's own fixture
New e2e
crates/mcp-server/tests/shape_excluded_fallback_e2e.rs, grading both issues in one tree:ref_countname_fallback_countname_fallback_shape_excludedBox.Reset()(#173)Box.Reset(bool)(#173)Box.OnlyOnce()— controlsend_thingviapkgmod.send_thing()(#175)plain_thingcalled bare — controlA control per language, because a field that fires on everything discriminates nothing.
Mutations — run, real RED
excl→"0") → all 3 RED:left: Some(0), right: Some(4),left: Some(0), right: Some(1), andthe field must take BOTH values on this fixture … zero=7 nonzero=0.method_call, free-fn arm getscall) → all 3 RED.qualifiesname_fallback_countand must be present exactly with it: {… "name_fallback_unmeasured":"no_use_reference_channel", "name_fallback_shape_excluded":0 …}.>= 0form of the assertions was also run and passes — which is why the shipped assertions pin exact counts rather than "went up".All restores
cp+ md5-verified +touched. Nogit checkout.Why the existing gates could not see it — one addition to the issue's list
The issue's four are right. A fifth is sharper: the earned-zero gate and the counter use DIFFERENT predicates. Step (a) of
try_enrich_name_fallback_countsasks "does any row that could record a use of this KIND bear this name?" (deliberately, per #118 — a claim about a NAME). The counter then applies the call-shape filter. A row can therefore pass the gate — amethod_callbearing the name exists — while the counter cannot see that very row, and the zero ships certified. Nothing compared the two predicates.NAMED residual
name_fallback_shape_excludedisOption, so an older daemon simply omits it, and there is no explicit*_unavailablestamp the waytext_window_unavailable/miss_risk_unavailablework. The field doc says absent means NOT REPORTED and never zero, but that is documentation where this repo usually ships a two-way shim. Named, not closed.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
phantom_count == 0bounds ~50 hand-written probes, not the corpus, and it is cited across the tree as an absolute guarantee #188STAYING OPEN, NARROWED — the
search_symbolshalf is fixed; the two tools in the title are notClose-out lane, master
552e3a2. Title corrected.Fixed, and do not re-litigate this half
crates/daemon/src/local_index.rs—TYPE_FAMILY_CLAUSE(:5441) andFREE_FN_CLAUSE(:5445) extracted, with the counter (:5610) and its complement (:5612) genuinely sharing them, so the count and the disclosure cannot drift.name_fallback_shape_excludedships on symbol rows and is live — everysearch_symbolsreply in this session carries it.shape_excluded_fallback_e2epasses in both legs, includingan_overload_ambiguous_method_reports_what_the_filter_removed. EXIT=0.Not fixed — and it is the half this issue's own title names
NavigationConfidence(crates/mcp-server/src/server.rs:7624-7647) is the confidence block onfind_references/find_callers/find_callees. Its fields are exactly:There is no shape-excluded sibling. Confirmed live on this repo's own index:
So for this issue's own
Reset()/Reset(bool)fixture,find_callersstill returns an empty page carryingpage_name_fallback: 0with nothing pointing at the 4 ambiguous refs — the unearned zero, in the tool the title names. The implementing lane's e2e table only ever showedsearch_symbolsrows, and the lane did not name this gap.Corrected scope
Keep the fixture. Replace the Measured block's
search_symbolsline with:The remaining work is one field on
NavigationConfidence, fed from the clauses that already exist, plus the e2e assertion on the navigation leg that the shipped e2e only made on the symbol leg.An overload-ambiguous ref reaches no caller-side tool — the index knows it is ambiguous and find_callers/find_references report an unearned zeroto An overload-ambiguous ref is disclosed on the symbol row but NOT in navigation confidence — find_callers/find_references still shippage_name_fallback: 0with no shape-excluded siblingFIXED, merged as
cbeb655. Graded bycrates/mcp-server/tests/shape_excluded_fallback_e2e.rs(6/6).The first half of this had shipped on the symbol leg only, so the two tools named in the title kept answering an empty page with
page_name_fallback: 0— a filter's zero rendered as a measurement.NavigationConfidencenow carries the symbol-scoped pairsymbol_shape_excluded_refs/symbol_shape_excluded_unavailable(plussymbol_shape_excluded_semanticswhen non-zero), firing only on a page that actually makes the zero claim, andpage_name_fallback_rowsis now the ONE predicate the count and its gate share.find_calleesis deliberately untouched — it was measured as already honest, so widening it would have been change without a defect.A live bug fell out of reading the pair, unrelated to the issue as filed:
enrich_name_fallback_countscleared onlyname_fallback_counton its degradation path, so a row could reach the wire carryingname_fallback_shape_excludedwith nothing left to qualify. The withheld-row arm cleared both and was graded; the error arm was neither.Mutations run: probe gate inverted → 3 RED including
left: None, right: Some(4)and "never both, never neither"; the pre-fix(None, None)→ 2 RED printing the old payload verbatim; the degradation clear deleted → RED withname_fallback_count: None, name_fallback_shape_excluded: Some(2).Closing.