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

Closed
opened 2026-09-06 02:39:01 +02:00 by buildagent · 3 comments
Member

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() and Reset(bool), plus four unqualified calls to Reset in the same file, plus one uniquely-named sibling method as a control.

resolution_gaps      -> { name: "Reset", reason: "ambiguous", count: 4 }
search_symbols       -> BOTH Reset symbols: ref_count: 0, name_fallback_count: 0
find_references(...) -> nothing, on either symbol
control (unique name)-> resolves, resolved_by tier1a_unique_own_file

find_callees on the enclosing symbol does report unresolved_count: 2. So the information is present in the index. Only the caller-side view drops it.

On the pinned cs-dapper corpus this hides Dapper/SqlMapper.cs:258 (ResetTypeHandlers(false);) and :264 (… => ResetTypeHandlers(true);) from find_callers on the bool overload. The bind is not wrong — search_symbols returns the intended symbol at start_line: 266 — the refs simply reach nothing. Both overloads report ref_count: 0 and name_fallback_count: 16.

Why the zero is unearned, and why that is the sharper half

name_fallback_count exists to bound how much ref_count undercounts, and this project ships a 0 there as an earned zero. On the fixture both counters read 0 for 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_call refs. These are call refs — 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_delete sits on the same evidence.

Mechanism — stated as partly inferred

Measured: the resolver classifies these refs as ambiguous (resolution_gaps says so by name and count), find_callees surfaces them as unresolved_count, and no caller-side tool does.

Inferred, not traced: that the omission is because an ambiguous ref is left with no target_id and the caller-side queries join on target_id while 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_gate grades 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_gaps has 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.
  • The corpus ratchets pin resolved as a count; an ambiguous ref is consistently unresolved, so nothing moves.
  • find_callees reporting unresolved_count while find_callers reports 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. Then resolution_gaps, search_symbols("Reset"), find_references on each Reset, and find_callees on Go.

What must NOT be done to make this pass

  • Do not pick an overload. Binding an ambiguous ref to one candidate to make a count non-zero manufactures a phantom, and phantoms are the one thing this project gates absolutely (phantom_count == 0).
  • Do not widen the name-fallback channel to swallow call refs. That channel's contract is receiver-shaped method_calls and its whole value is that resolution: "name_fallback" means something specific. Diluting it makes every existing name-fallback row less informative to buy one more row here.
  • Do not close this by editing the tool description. The zero is wrong as data, not merely under-explained. The fix that would satisfy this issue is a channel that carries "N refs bear this name and were classified ambiguous" to the caller-side tools — the same shape search_symbols already uses for name_fallback_unmeasured, where an absent measurement says so rather than shipping a zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

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()` and `Reset(bool)`, plus four unqualified calls to `Reset` in the same file, plus one uniquely-named sibling method as a control. ``` resolution_gaps -> { name: "Reset", reason: "ambiguous", count: 4 } search_symbols -> BOTH Reset symbols: ref_count: 0, name_fallback_count: 0 find_references(...) -> nothing, on either symbol control (unique name)-> resolves, resolved_by tier1a_unique_own_file ``` **`find_callees` on the *enclosing* symbol does report `unresolved_count: 2`.** So the information is present in the index. Only the caller-side view drops it. On the pinned `cs-dapper` corpus this hides `Dapper/SqlMapper.cs:258` (`ResetTypeHandlers(false);`) and `:264` (`… => ResetTypeHandlers(true);`) from `find_callers` on the `bool` overload. The bind is not wrong — `search_symbols` returns the intended symbol at `start_line: 266` — the refs simply reach nothing. Both overloads report `ref_count: 0` and `name_fallback_count: 16`. ## Why the zero is unearned, and why that is the sharper half `name_fallback_count` exists to bound how much `ref_count` undercounts, and this project ships a `0` there as an **earned** zero. On the fixture both counters read `0` for 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_call`** refs. These are `call` refs — 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_delete` sits on the same evidence. ## Mechanism — stated as partly inferred **Measured:** the resolver classifies these refs as `ambiguous` (`resolution_gaps` says so by name and count), `find_callees` surfaces them as `unresolved_count`, and no caller-side tool does. **Inferred, not traced:** that the omission is because an `ambiguous` ref is left with no `target_id` and the caller-side queries join on `target_id` while 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_gate` grades 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_gaps` has 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. - The corpus ratchets pin `resolved` as a count; an ambiguous ref is consistently unresolved, so nothing moves. - `find_callees` reporting `unresolved_count` while `find_callers` reports 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. Then `resolution_gaps`, `search_symbols("Reset")`, `find_references` on each `Reset`, and `find_callees` on `Go`. ## What must NOT be done to make this pass - **Do not pick an overload.** Binding an ambiguous ref to one candidate to make a count non-zero manufactures a phantom, and phantoms are the one thing this project gates absolutely (`phantom_count == 0`). - **Do not widen the name-fallback channel to swallow `call` refs.** That channel's contract is receiver-shaped `method_call`s and its whole value is that `resolution: "name_fallback"` means something specific. Diluting it makes every existing name-fallback row less informative to buy one more row here. - **Do not close this by editing the tool description.** The zero is *wrong as data*, not merely under-explained. The fix that would satisfy this issue is a channel that carries "N refs bear this name and were classified ambiguous" to the caller-side tools — the same shape `search_symbols` already uses for `name_fallback_unmeasured`, where an absent measurement says so rather than shipping a zero. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

CONFIRMED, 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 ambiguous ref is left with no target_id and the caller-side queries join on target_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_count does not join on target_id — it counts rows where target_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:

Some("class") | Some("trait") | Some("impl") | … =>
   "(r.kind NOT IN ('call','method_call','binding')
      OR r.kind = 'method_call'
      OR (r.kind = 'call' AND r.qualifier IS NOT NULL AND …))"

Four unqualified Reset() sites are kind = 'call' with qualifier 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-shaped method_call" — a qualifier-matched Type::assoc() call IS 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:

_ => "(r.kind NOT IN ('call','method_call','binding') OR r.kind = 'call')"

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 emits pkg.f() as method_call (crates/plugins/src/python.rs:771). So flask.send_from_directory(...) is dropped and a module-level def ships name_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

name_fallback_shape_excluded: <n>

Same-name, same-language, unresolved, call-shaped refs that the call-shape filter behind name_fallback_count REMOVED. 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:

  • Does not pick an overload. Nothing is bound. phantom_count == 0 untouched — precision_gate still 7/7.
  • Does not widen the name-fallback channel. name_fallback_count keeps its definition and its value; resolution: "name_fallback" still means exactly what it meant.
  • Is not a description edit. It is a measured number in the payload. The description change is one sentence that points at it.
  • Uses the shape the issue nominated — a sibling field that qualifies the count, like name_fallback_unmeasured — and it is PAIRED: present exactly when name_fallback_count is, 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_count would 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_gaps holds 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:

symbol ref_count name_fallback_count name_fallback_shape_excluded
Box.Reset() (#173) 0 0 4
Box.Reset(bool) (#173) 0 0 4
Box.OnlyOnce() — control — — 0
python send_thing via pkgmod.send_thing() (#175) 0 0 1
python plain_thing called bare — control — — 0

A control per language, because a field that fires on everything discriminates nothing.

Mutations — run, real RED

  • DATA — complement query counts nothing (excl → "0") → all 3 RED: left: Some(0), right: Some(4), left: Some(0), right: Some(1), and the field must take BOTH values on this fixture … zero=7 nonzero=0.
  • PRODUCTION PREDICATE — complement branches swapped (method arm gets method_call, free-fn arm gets call) → all 3 RED.
  • PAIRING — drop the withheld-row clearing → the page-level test RED on a dangling sibling: ``name_fallback_shape_excludedqualifiesname_fallback_count and must be present exactly with it: {… "name_fallback_unmeasured":"no_use_reference_channel", "name_fallback_shape_excluded":0 …}.
  • A weakened >= 0 form 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. No git 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_counts asks "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 — a method_call bearing 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_excluded is Option, so an older daemon simply omits it, and there is no explicit *_unavailable stamp the way text_window_unavailable / miss_risk_unavailable work. 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

## CONFIRMED, 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 `ambiguous` ref is left with no `target_id` and the caller-side queries join on `target_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_count` does **not** join on `target_id` — it counts rows where `target_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`: ``` Some("class") | Some("trait") | Some("impl") | … => "(r.kind NOT IN ('call','method_call','binding') OR r.kind = 'method_call' OR (r.kind = 'call' AND r.qualifier IS NOT NULL AND …))" ``` Four unqualified `Reset()` sites are `kind = 'call'` with `qualifier 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-shaped `method_call`" — a qualifier-matched `Type::assoc()` `call` IS 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: ``` _ => "(r.kind NOT IN ('call','method_call','binding') OR r.kind = 'call')" ``` 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 emits `pkg.f()` as `method_call` (`crates/plugins/src/python.rs:771`). So `flask.send_from_directory(...)` is dropped and a module-level `def` ships `name_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 ``` name_fallback_shape_excluded: <n> ``` Same-name, same-language, unresolved, **call-shaped** refs that the call-shape filter behind `name_fallback_count` REMOVED. 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: - **Does not pick an overload.** Nothing is bound. `phantom_count == 0` untouched — `precision_gate` still 7/7. - **Does not widen the name-fallback channel.** `name_fallback_count` keeps its definition and its value; `resolution: "name_fallback"` still means exactly what it meant. - **Is not a description edit.** It is a measured number in the payload. The description change is one sentence that points at it. - Uses the shape the issue nominated — a sibling field that qualifies the count, like `name_fallback_unmeasured` — and it is PAIRED: present exactly when `name_fallback_count` is, 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_count` would 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_gaps` holds 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: | symbol | `ref_count` | `name_fallback_count` | `name_fallback_shape_excluded` | |---|---|---|---| | `Box.Reset()` (#173) | 0 | 0 | **4** | | `Box.Reset(bool)` (#173) | 0 | 0 | **4** | | `Box.OnlyOnce()` — control | — | — | **0** | | python `send_thing` via `pkgmod.send_thing()` (#175) | 0 | 0 | **1** | | python `plain_thing` called bare — control | — | — | **0** | A control per language, because a field that fires on everything discriminates nothing. ### Mutations — run, real RED - **DATA** — complement query counts nothing (`excl` → `"0"`) → all 3 RED: `left: Some(0), right: Some(4)`, `left: Some(0), right: Some(1)`, and `the field must take BOTH values on this fixture … zero=7 nonzero=0`. - **PRODUCTION PREDICATE** — complement branches swapped (method arm gets `method_call`, free-fn arm gets `call`) → all 3 RED. - **PAIRING** — drop the withheld-row clearing → the page-level test RED on a dangling sibling: ``name_fallback_shape_excluded` qualifies `name_fallback_count` and must be present exactly with it: {… "name_fallback_unmeasured":"no_use_reference_channel", "name_fallback_shape_excluded":0 …}`. - A weakened `>= 0` form 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 + `touch`ed. No `git 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_counts` asks "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 — a `method_call` bearing 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_excluded` is `Option`, so an older daemon simply omits it, and there is no explicit `*_unavailable` stamp the way `text_window_unavailable` / `miss_risk_unavailable` work. 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.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

STAYING OPEN, NARROWED — the search_symbols half is fixed; the two tools in the title are not

Close-out lane, master 552e3a2. Title corrected.

Fixed, and do not re-litigate this half

crates/daemon/src/local_index.rs — TYPE_FAMILY_CLAUSE (:5441) and FREE_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_excluded ships on symbol rows and is live — every search_symbols reply in this session carries it. shape_excluded_fallback_e2e passes in both legs, including an_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 on find_references / find_callers / find_callees. Its fields are exactly:

graph_semantics, counts_scope, page_rows, page_resolved, page_name_fallback,
symbol_unresolved_calls, authoritative_for_refactor, verification

There is no shape-excluded sibling. Confirmed live on this repo's own index:

find_callers(712582) -> confidence: { page_rows: 2, page_resolved: 2, page_name_fallback: 0 }

So for this issue's own Reset() / Reset(bool) fixture, find_callers still returns an empty page carrying page_name_fallback: 0 with nothing pointing at the 4 ambiguous refs — the unearned zero, in the tool the title names. The implementing lane's e2e table only ever showed search_symbols rows, and the lane did not name this gap.

Corrected scope

Keep the fixture. Replace the Measured block's search_symbols line with:

search_symbols now reports name_fallback_shape_excluded: 4 — fixed. NavigationConfidence (server.rs:7624) has no counterpart, so find_callers / find_references / find_callees still ship page_name_fallback: 0 with no channel saying the call-shape filter removed candidates.

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.

## STAYING OPEN, NARROWED — the `search_symbols` half is fixed; the two tools in the title are not Close-out lane, master `552e3a2`. **Title corrected.** ### Fixed, and do not re-litigate this half `crates/daemon/src/local_index.rs` — `TYPE_FAMILY_CLAUSE` (`:5441`) and `FREE_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_excluded` ships on symbol rows and is live — every `search_symbols` reply in this session carries it. `shape_excluded_fallback_e2e` passes in **both** legs, including `an_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 on `find_references` / `find_callers` / `find_callees`. Its fields are exactly: ``` graph_semantics, counts_scope, page_rows, page_resolved, page_name_fallback, symbol_unresolved_calls, authoritative_for_refactor, verification ``` There is **no shape-excluded sibling**. Confirmed live on this repo's own index: ``` find_callers(712582) -> confidence: { page_rows: 2, page_resolved: 2, page_name_fallback: 0 } ``` So for this issue's own `Reset()` / `Reset(bool)` fixture, `find_callers` still returns an empty page carrying `page_name_fallback: 0` with nothing pointing at the 4 ambiguous refs — the unearned zero, in the tool the title names. The implementing lane's e2e table only ever showed `search_symbols` rows, and the lane did not name this gap. ### Corrected scope Keep the fixture. Replace the Measured block's `search_symbols` line with: > `search_symbols` now reports `name_fallback_shape_excluded: 4` — **fixed**. `NavigationConfidence` (`server.rs:7624`) has no counterpart, so `find_callers` / `find_references` / `find_callees` still ship `page_name_fallback: 0` with no channel saying the call-shape filter removed candidates. 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.
buildagent changed title from An overload-ambiguous ref reaches no caller-side tool — the index knows it is ambiguous and find_callers/find_references report an unearned zero to 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 2026-09-06 12:48:04 +02:00
Author
Member

FIXED, merged as cbeb655. Graded by crates/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. NavigationConfidence now carries the symbol-scoped pair symbol_shape_excluded_refs / symbol_shape_excluded_unavailable (plus symbol_shape_excluded_semantics when non-zero), firing only on a page that actually makes the zero claim, and page_name_fallback_rows is now the ONE predicate the count and its gate share.

find_callees is 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_counts cleared only name_fallback_count on its degradation path, so a row could reach the wire carrying name_fallback_shape_excluded with 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 with name_fallback_count: None, name_fallback_shape_excluded: Some(2).

Closing.

FIXED, merged as `cbeb655`. Graded by `crates/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. `NavigationConfidence` now carries the symbol-scoped pair `symbol_shape_excluded_refs` / `symbol_shape_excluded_unavailable` (plus `symbol_shape_excluded_semantics` when non-zero), firing only on a page that actually makes the zero claim, and `page_name_fallback_rows` is now the ONE predicate the count and its gate share. `find_callees` is 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_counts` cleared only `name_fallback_count` on its degradation path, so a row could reach the wire carrying `name_fallback_shape_excluded` with 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 with `name_fallback_count: None, name_fallback_shape_excluded: Some(2)`. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#173
No description provided.