get_dependencies(direction: "in") answers total: 0 with no channel saying candidates were dropped for package-scope ambiguity — the matched_keys half is fixed, this half is not #174

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

Found while authoring hand-verified benchmark questions for #51. Traced to source.

Measured

get_dependencies("Dapper.ProviderTools/BulkCopy.cs", direction: "in")   -> importers.total: 0
get_dependencies("Dapper/SqlMapper.Async.cs",        direction: "in")   -> total: 0

The second is the one that settles it: most of Dapper's test suite carries using Dapper;. A repository-wide zero for the most-imported file in the project is not a sparse answer, it is a dead code path.

The first was the failing clause of a hand-verified benchmark question. Dapper.ProviderTools/BulkCopy.cs:9 is using Dapper.ProviderTools.Internal;, and that file declares that namespace — file_outline shows the module symbol. The importer exists and is one hop away.

Mechanism (read at source)

crates/daemon/src/local_index.rs::matched_keys splits the import module on ., :, / and \, and then requires a candidate key to equal one segment.

The key taken from a C# module symbol is the whole dotted namespace. So the comparison is:

key       = "Dapper.ProviderTools.Internal"
segments  = ["Dapper", "ProviderTools", "Internal"]
key == segment?  -> never

Dapper.ProviderTools.Internal can never equal a segment of itself. Every C# namespace with a dot in it is unreachable through this path, and a namespace without a dot is the rare case.

The tool's own description promises exactly the behaviour that does not happen: "so using mainproject.Framework matches files declaring namespace mainproject.Framework".

By the same code path this is expected to affect PHP (namespaces are \-separated and the key is the whole namespace). That half is inferred from the shared function, not measured — I did not run a PHP repro.

Why existing gates could not see it

The only e2e test on this path is get_dependencies_in_finds_qualified_importers_rust. Rust mod keys are single segments, so key == segment holds trivially for them. The test is green, has always been green, and exercises the one input shape for which the defect is invisible.

That is this repository's own recorded failure class — a test whose fixture is one-sided, passing over a promise it does not exercise. The promise ("mainproject.Framework matches namespace mainproject.Framework") is stated in the tool description and graded by nothing.

Repro

Two C# files: a.cs declaring namespace Foo.Bar.Baz { class A {} } and b.cs containing using Foo.Bar.Baz;. Index, then get_dependencies("a.cs", direction: "in") → total: 0. Change the namespace to a single segment Foo and the same call finds b.cs.

Or on the pinned cs-dapper corpus (sha 72a54c475f75e18cb93cba0809d00a5e6e49efd9): get_dependencies("Dapper/SqlMapper.Async.cs", direction: "in").

What must NOT be done to make this pass

  • Do not switch to substring or suffix matching. Foo.Bar would then match Other.Foo.Bar, and an importer list that is wrong in the permissive direction is worse than one that is empty — an empty answer is at least visibly useless.
  • Do not fix C# alone. The function is shared; PHP is the second suspect and TypeScript path-shaped modules are worth checking too. The rule should rest on a structural fact about how each language's key is formed, not on a language check inside matched_keys.
  • Do not extend get_dependencies_in_finds_qualified_importers_rust. It is green because Rust keys are single-segment; adding assertions to it keeps the one-sided fixture. The test this needs is a multi-segment key per language that has one, and it should be built so that reverting the fix reddens it.
  • Do not soften the tool description to match the behaviour. The description names the exact case that fails; the description is right and the code is wrong.

Measured vs inferred

The three get_dependencies readings, BulkCopy.cs:9, the declared module symbol, the matched_keys splitting rule and the identity of the single e2e test are all measured or read at source. The PHP impact is inferred from the shared code path and is stated as a suspect, not a finding.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

Found while authoring hand-verified benchmark questions for #51. Traced to source. ## Measured ``` get_dependencies("Dapper.ProviderTools/BulkCopy.cs", direction: "in") -> importers.total: 0 get_dependencies("Dapper/SqlMapper.Async.cs", direction: "in") -> total: 0 ``` The second is the one that settles it: most of Dapper's test suite carries `using Dapper;`. A repository-wide zero for the most-imported file in the project is not a sparse answer, it is a dead code path. The first was the failing clause of a hand-verified benchmark question. `Dapper.ProviderTools/BulkCopy.cs:9` is `using Dapper.ProviderTools.Internal;`, and that file declares that namespace — `file_outline` shows the `module` symbol. The importer exists and is one hop away. ## Mechanism (read at source) `crates/daemon/src/local_index.rs::matched_keys` splits the import module on `.`, `:`, `/` and `\`, and then requires a candidate key to **equal one segment**. The key taken from a C# `module` symbol is the **whole dotted namespace**. So the comparison is: ``` key = "Dapper.ProviderTools.Internal" segments = ["Dapper", "ProviderTools", "Internal"] key == segment? -> never ``` `Dapper.ProviderTools.Internal` can never equal a segment of itself. Every C# namespace with a dot in it is unreachable through this path, and a namespace without a dot is the rare case. **The tool's own description promises exactly the behaviour that does not happen**: *"so `using mainproject.Framework` matches files declaring `namespace mainproject.Framework`"*. By the same code path this is expected to affect **PHP** (namespaces are `\`-separated and the key is the whole namespace). **That half is inferred from the shared function, not measured** — I did not run a PHP repro. ## Why existing gates could not see it The only e2e test on this path is **`get_dependencies_in_finds_qualified_importers_rust`**. Rust `mod` keys are **single segments**, so `key == segment` holds trivially for them. The test is green, has always been green, and exercises the one input shape for which the defect is invisible. That is this repository's own recorded failure class — a test whose fixture is one-sided, passing over a promise it does not exercise. The promise ("`mainproject.Framework` matches `namespace mainproject.Framework`") is *stated in the tool description* and graded by nothing. ## Repro Two C# files: `a.cs` declaring `namespace Foo.Bar.Baz { class A {} }` and `b.cs` containing `using Foo.Bar.Baz;`. Index, then `get_dependencies("a.cs", direction: "in")` → `total: 0`. Change the namespace to a single segment `Foo` and the same call finds `b.cs`. Or on the pinned `cs-dapper` corpus (sha `72a54c475f75e18cb93cba0809d00a5e6e49efd9`): `get_dependencies("Dapper/SqlMapper.Async.cs", direction: "in")`. ## What must NOT be done to make this pass - **Do not switch to substring or suffix matching.** `Foo.Bar` would then match `Other.Foo.Bar`, and an importer list that is wrong in the permissive direction is worse than one that is empty — an empty answer is at least visibly useless. - **Do not fix C# alone.** The function is shared; PHP is the second suspect and TypeScript path-shaped modules are worth checking too. The rule should rest on a structural fact about how each language's key is formed, not on a language check inside `matched_keys`. - **Do not extend `get_dependencies_in_finds_qualified_importers_rust`.** It is green because Rust keys are single-segment; adding assertions to it keeps the one-sided fixture. The test this needs is a **multi-segment** key per language that has one, and it should be built so that reverting the fix reddens it. - **Do not soften the tool description to match the behaviour.** The description names the exact case that fails; the description is right and the code is wrong. ## Measured vs inferred The three `get_dependencies` readings, `BulkCopy.cs:9`, the declared `module` symbol, the `matched_keys` splitting rule and the identity of the single e2e test are all measured or read at source. The PHP impact is **inferred** from the shared code path and is stated as a suspect, not a finding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

CONFIRMED for one file, and the issue is WRONG about the other. There are TWO independent causes producing the same zero; one is fixed, one is a NAMED residual.

The two-causes question, settled by measurement

Measured on cs-dapper @ 72a54c475f by replaying importers_of_file's exact algorithm against the indexed DB and instrumenting each stage:

target keys ambiguous rows matched_keys accepted dropped by scope guard total
Dapper.ProviderTools/BulkCopy.cs BulkCopy, Dapper.ProviderTools ∅ 0 0 0
Dapper.ProviderTools/Internal/DynamicBulkCopy.cs DynamicBulkCopy, Dapper.ProviderTools.Internal ∅ 0 0 0
Dapper/SqlMapper.Async.cs SqlMapper.Async, Dapper {Dapper} 18 18 0
  • Cause 1 — matched_keys, the issue's diagnosis. CONFIRMED and FIXED. It fires on the two ProviderTools files. The ambiguity set is empty for both, so the scope guard drops nothing: matched_keys genuinely accepted zero rows.
  • Cause 2 — the package-scope ambiguity guard. The issue attributes this to matched_keys and that is wrong. SqlMapper.Async.cs's key Dapper is a single segment, so matched_keys already matched 18 using Dapper… rows before the fix. All 18 are then dropped because namespace Dapper is declared under three package roots — Dapper/ (46 files), Dapper.Rainbow/ (5), Dapper.SqlBuilder/ (1), 54 files total — and no importer sits in the target's own root. The fix changes nothing for this file, by design.

using Dapper; genuinely does not say which of 54 files it wants, so refusing is defensible. What is not defensible is that the reply says total: 0 with no channel saying candidates were dropped for ambiguity. That disclosure is a separate change and was not made — it is recorded in the importers_of_file doc comment under "THE RESIDUAL, NAMED". The issue body should be corrected: only the ProviderTools measurement supports the matched_keys diagnosis.

The rule shipped — and why the suggested one was rejected

match key_segments.len() {
    0 => false,
    1 => module_segments.contains(&ks[0]),          // unchanged, today's rule
    _ => key.module_path && ks == module_segments,   // new: a module PATH matches WHOLE
}

Keys now carry provenance (MatchKey { text, module_path }): a module-symbol name is a declared path, a file stem is a filename. Structural, not a language check — the function already builds keys from exactly those two places.

The contiguous-subsequence rule suggested to the lane was measured and REJECTED:

corpus old shipped contiguous-subsequence
cs-dapper 58 82 (+24) 984 (+926)
php-guzzle 13505 13526 (+21) 17482 (+3977)

The dominant false positive is not the ancestor case the issue flags but the descendant one: key Dapper.Tests matching using Dapper.Tests.Performance.Dashing;, and GuzzleHttp\Tests matching use GuzzleHttp\Tests\SpyResponse;. And the PHP class-import case that would motivate a looser rule is already answered correctly by the stem key (Psr17SpyFactory stays at 3 importers before and after), so widening the namespace key buys nothing.

False-positive shapes the shipped rule admits, stated:

  1. The pre-existing single-segment trade, bounded by the scope guard. Unchanged.
  2. New: every file declaring the same namespace claims every importer of it — the index knows which namespace an import names, not which of its files. Same granularity the cs-dapper oracle already assumes.
  3. Multi-segment file stems stay inert, so import from "./probe.config" is still not found. Named, deliberately not pinned.

Blast radius: strictly additive, zero rows lost anywhere. ts-zod, js-express, python-flask, py-django, ruby-sinatra, rust-ripgrep: 0 changed rows. The single-segment degeneration is exact — measured, not asserted.

Live, through the real tool over the pinned corpus:

BulkCopy.cs          0 -> 1   [tests/Dapper.Tests/ProviderTests.cs 'Dapper.ProviderTools' :3]
DynamicBulkCopy.cs   0 -> 1   [Dapper.ProviderTools/BulkCopy.cs 'Dapper.ProviderTools.Internal' :9]
SqlMapper.Async.cs   0 -> 0   (cause 2, residual)

Per-language multi-segment-key survey — MEASURED, and PHP is no longer inferred

lang module rows multi-segment verdict
csharp 158 102 has one — covered
php 131 106 has one — covered (measured, the issue only inferred it)
ruby 138 0 module A; module B emits separate single-segment symbols
rust 129 0 mod names are single segments
python 0 module symbols at all — key is the stem only
typescript / javascript 0 module symbols at all — key is the stem only

TS/JS do have multi-segment keys, via the stem (177 files in zod, 63 in express). A stem key carries no language, so the third test grades it once on the reachable instance.

Mutations — real RED

M1 — revert matched_keys to the pre-#174 body. All three e2e tests redden independently — which is why the test was split into three: the first failing assertion ends the body, so a single test would have let the C# arm redden while the PHP arm was never reached.

[csharp] cs/src/CsWidget.cs declares `Acme.Widgets` and cs/src/CsConsumer.cs imports it by that
exact path, so it MUST be listed. ... The tool answered {"results":[],"total":0}
[php]    php/src/PhpWidget.php declares `Bravo\Widgets` ... {"results":[],"total":0}

Same three RED under COSI_E2E_LEG=daemon.

M2 — the permissive fix (contiguous subsequence).

[csharp] cs/src/CsDeepOnly.cs must NOT be an importer of cs/src/CsWidget.cs ... "total":2
[php]    php/src/PhpDeepOnly.php must NOT be an importer of php/src/PhpWidget.php ... "total":2

M3 — provenance flag forced true.

`Acme.Widgets.csproj` is a FILENAME that happens to spell a namespace ... left: Number(1), right: 0

A MUTATION SURVIVED, and it is a finding about the test rather than the fix. M3's first form asserted a TS stem probe.config must not match probe/config. It stayed GREEN. Cause: the model of the pipeline omitted the SQL prefilter i.module LIKE '%probe.config%', where . is a literal — verified, SELECT 'vitest/config' LIKE '%vitest.config%' → 0. The cross-separator collision is unreachable in production, so that assertion could never fail. Replaced with the reachable case found by re-measuring: the .NET <Namespace>.csproj convention. Confirmed live — with the flag forced true, get_dependencies("Dapper.ProviderTools/Dapper.ProviderTools.csproj", "in") returns 1, reporting a build file as the target of a C# using. That one row is the entire difference the flag makes across seven corpora. Both the code doc and the test doc record the deleted assertion and why.

The description was right; the DOC COMMENT was the thing that lied

The issue says "do not soften the tool description" — correct, and it was not touched. But the matched_keys doc comment claimed: "This also keeps the C# case working (using mainproject.Framework ↔ namespace mainproject.Framework, whose segments include Framework)". The key is the whole namespace, never Framework. Corrected.

Gates

cargo fmt -p code-index-daemon --check 0 · clippy -p code-index-daemon --all-targets -D warnings 0 · cargo test -p code-index-daemon --no-fail-fast 0 (73 suites) · new qualified_importers_e2e 0 (3 passed) · same under COSI_E2E_LEG=daemon 0 · RUSTDOCFLAGS="-D warnings" cargo doc 0 · mcp_smoke (109) + disclosure_contract_e2e (38) + 4 more 0 · agent_task_benchmark_cs_dapper with the corpus env 0, 20 questions graded.

clippy first failed on doc_lazy_continuation and cargo doc on private-intra-doc-links (public doc linking private matched_keys); both fixed per CLAUDE.md, the link demoted to a citation keeping the module path.

Records — not blessed

tests/corpus/baseline.json cannot move, and structurally rather than merely by not being run: corpus_ratchet lives in code-index-indexer, which does not link code-index-daemon at all. This is a read-path change to importers_of_file.

tests/bench/ratchet.json cs-dapper is now stale and was not edited: recall 0.7903 → 0.8548, correct → 53/62, tool_tokens 7742 → 7900 (1.020x, inside the 1.05 ceiling; recall is a floor, so the gate is green). Only +1 of that is this fix (dapper.get_dependencies.DynamicBulkCopy flips to passing — the oracle already expects /importers/total == 1 and the exact row, which is what the tool now returns). The other +3 is #172's generic_name fix landing dapper.who_calls.CastResult at 3/3. The block should be re-recorded once all of this batch lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

## CONFIRMED for one file, and the issue is WRONG about the other. There are TWO independent causes producing the same zero; one is fixed, one is a NAMED residual. ### The two-causes question, settled by measurement Measured on `cs-dapper` @ `72a54c475f` by replaying `importers_of_file`'s exact algorithm against the indexed DB and instrumenting each stage: | target | keys | `ambiguous` | rows `matched_keys` accepted | dropped by scope guard | total | |---|---|---|---|---|---| | `Dapper.ProviderTools/BulkCopy.cs` | `BulkCopy`, `Dapper.ProviderTools` | ∅ | **0** | 0 | 0 | | `Dapper.ProviderTools/Internal/DynamicBulkCopy.cs` | `DynamicBulkCopy`, `Dapper.ProviderTools.Internal` | ∅ | **0** | 0 | 0 | | `Dapper/SqlMapper.Async.cs` | `SqlMapper.Async`, `Dapper` | `{Dapper}` | **18** | **18** | 0 | - **Cause 1 — `matched_keys`, the issue's diagnosis. CONFIRMED and FIXED.** It fires on the two `ProviderTools` files. The ambiguity set is *empty* for both, so the scope guard drops nothing: `matched_keys` genuinely accepted zero rows. - **Cause 2 — the package-scope ambiguity guard. The issue attributes this to `matched_keys` and that is wrong.** `SqlMapper.Async.cs`'s key `Dapper` is a **single segment**, so `matched_keys` already matched 18 `using Dapper…` rows *before* the fix. All 18 are then dropped because `namespace Dapper` is declared under **three** package roots — `Dapper/` (46 files), `Dapper.Rainbow/` (5), `Dapper.SqlBuilder/` (1), 54 files total — and no importer sits in the target's own root. **The fix changes nothing for this file, by design.** `using Dapper;` genuinely does not say which of 54 files it wants, so refusing is defensible. **What is not defensible is that the reply says `total: 0` with no channel saying candidates were dropped for ambiguity.** That disclosure is a separate change and was not made — it is recorded in the `importers_of_file` doc comment under "THE RESIDUAL, NAMED". **The issue body should be corrected**: only the `ProviderTools` measurement supports the `matched_keys` diagnosis. ### The rule shipped — and why the suggested one was rejected ```rust match key_segments.len() { 0 => false, 1 => module_segments.contains(&ks[0]), // unchanged, today's rule _ => key.module_path && ks == module_segments, // new: a module PATH matches WHOLE } ``` Keys now carry provenance (`MatchKey { text, module_path }`): a `module`-symbol name is a declared **path**, a file stem is a **filename**. Structural, not a language check — the function already builds keys from exactly those two places. **The contiguous-subsequence rule suggested to the lane was measured and REJECTED:** | corpus | old | shipped | contiguous-subsequence | |---|---|---|---| | cs-dapper | 58 | 82 (**+24**) | 984 (**+926**) | | php-guzzle | 13505 | 13526 (**+21**) | 17482 (**+3977**) | The dominant false positive is not the ancestor case the issue flags but the **descendant** one: key `Dapper.Tests` matching `using Dapper.Tests.Performance.Dashing;`, and `GuzzleHttp\Tests` matching `use GuzzleHttp\Tests\SpyResponse;`. And the PHP class-import case that would motivate a looser rule is **already answered correctly by the stem key** (`Psr17SpyFactory` stays at 3 importers before and after), so widening the namespace key buys nothing. **False-positive shapes the shipped rule admits, stated:** 1. The pre-existing single-segment trade, bounded by the scope guard. Unchanged. 2. **New:** every file declaring the *same* namespace claims every importer of it — the index knows which namespace an import names, not which of its files. Same granularity the `cs-dapper` oracle already assumes. 3. Multi-segment **file stems** stay inert, so `import from "./probe.config"` is still not found. Named, deliberately not pinned. **Blast radius: strictly additive, zero rows lost anywhere.** ts-zod, js-express, python-flask, py-django, ruby-sinatra, rust-ripgrep: **0 changed rows**. The single-segment degeneration is exact — measured, not asserted. Live, through the real tool over the pinned corpus: ``` BulkCopy.cs 0 -> 1 [tests/Dapper.Tests/ProviderTests.cs 'Dapper.ProviderTools' :3] DynamicBulkCopy.cs 0 -> 1 [Dapper.ProviderTools/BulkCopy.cs 'Dapper.ProviderTools.Internal' :9] SqlMapper.Async.cs 0 -> 0 (cause 2, residual) ``` ### Per-language multi-segment-key survey — MEASURED, and PHP is no longer inferred | lang | module rows | multi-segment | verdict | |---|---|---|---| | csharp | 158 | **102** | has one — covered | | php | 131 | **106** | has one — covered (**measured**, the issue only inferred it) | | ruby | 138 | 0 | `module A; module B` emits separate single-segment symbols | | rust | 129 | 0 | `mod` names are single segments | | python | **0 module symbols at all** | — | key is the stem only | | typescript / javascript | **0 module symbols at all** | — | key is the stem only | TS/JS do have multi-segment keys, via the **stem** (177 files in zod, 63 in express). A stem key carries no language, so the third test grades it once on the reachable instance. ### Mutations — real RED **M1 — revert `matched_keys` to the pre-#174 body.** All three e2e tests redden independently — which is why the test was split into three: the first failing assertion ends the body, so a single test would have let the C# arm redden while the PHP arm was never reached. ``` [csharp] cs/src/CsWidget.cs declares `Acme.Widgets` and cs/src/CsConsumer.cs imports it by that exact path, so it MUST be listed. ... The tool answered {"results":[],"total":0} [php] php/src/PhpWidget.php declares `Bravo\Widgets` ... {"results":[],"total":0} ``` Same three RED under `COSI_E2E_LEG=daemon`. **M2 — the permissive fix (contiguous subsequence).** ``` [csharp] cs/src/CsDeepOnly.cs must NOT be an importer of cs/src/CsWidget.cs ... "total":2 [php] php/src/PhpDeepOnly.php must NOT be an importer of php/src/PhpWidget.php ... "total":2 ``` **M3 — provenance flag forced true.** ``` `Acme.Widgets.csproj` is a FILENAME that happens to spell a namespace ... left: Number(1), right: 0 ``` **A MUTATION SURVIVED, and it is a finding about the test rather than the fix.** M3's first form asserted a TS stem `probe.config` must not match `probe/config`. It stayed **GREEN**. Cause: the model of the pipeline omitted the SQL prefilter `i.module LIKE '%probe.config%'`, where `.` is a literal — verified, `SELECT 'vitest/config' LIKE '%vitest.config%'` → `0`. **The cross-separator collision is unreachable in production, so that assertion could never fail.** Replaced with the reachable case found by re-measuring: the .NET `<Namespace>.csproj` convention. Confirmed live — with the flag forced true, `get_dependencies("Dapper.ProviderTools/Dapper.ProviderTools.csproj", "in")` returns **1**, reporting a build file as the target of a C# `using`. That one row is the entire difference the flag makes across seven corpora. Both the code doc and the test doc record the deleted assertion and why. ### The description was right; the DOC COMMENT was the thing that lied The issue says "do not soften the tool description" — correct, and it was not touched. But the `matched_keys` **doc comment** claimed: *"This also keeps the C# case working (`using mainproject.Framework` ↔ `namespace mainproject.Framework`, whose segments include `Framework`)"*. The key is the **whole** namespace, never `Framework`. Corrected. ### Gates `cargo fmt -p code-index-daemon --check` **0** · `clippy -p code-index-daemon --all-targets -D warnings` **0** · `cargo test -p code-index-daemon --no-fail-fast` **0** (73 suites) · new `qualified_importers_e2e` **0** (3 passed) · **same under `COSI_E2E_LEG=daemon`** **0** · `RUSTDOCFLAGS="-D warnings" cargo doc` **0** · `mcp_smoke` (109) + `disclosure_contract_e2e` (38) + 4 more **0** · `agent_task_benchmark_cs_dapper` with the corpus env **0**, 20 questions graded. `clippy` first failed on `doc_lazy_continuation` and `cargo doc` on `private-intra-doc-links` (public doc linking private `matched_keys`); both fixed per CLAUDE.md, the link demoted to a citation keeping the module path. ### Records — not blessed `tests/corpus/baseline.json` **cannot** move, and structurally rather than merely by not being run: `corpus_ratchet` lives in `code-index-indexer`, which does not link `code-index-daemon` at all. This is a read-path change to `importers_of_file`. `tests/bench/ratchet.json` cs-dapper is now stale and was **not** edited: `recall 0.7903 → 0.8548`, `correct → 53/62`, `tool_tokens 7742 → 7900` (1.020x, inside the 1.05 ceiling; recall is a floor, so the gate is green). Only **+1** of that is this fix (`dapper.get_dependencies.DynamicBulkCopy` flips to passing — the oracle already expects `/importers/total == 1` and the exact row, which is what the tool now returns). The other **+3** is #172's `generic_name` fix landing `dapper.who_calls.CastResult` at 3/3. The block should be re-recorded once all of this batch lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

STAYING OPEN, NARROWED — cause 1 fixed, cause 2 live, and the issue's own decisive example still returns total: 0

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

Cause 1 is fixed and graded

crates/daemon/src/local_index.rs:6774 — matched_keys now takes &[MatchKey] with a module_path provenance flag and applies two rules by key shape: a single segment matches any segment; a multi-segment module path must match whole. MatchKey construction at :4020-4042. Three e2e tests pass in both legs (EXIT=0), with a real anti-vacuity guard inside a_multi_segment_file_stem_is_not_a_module_path. Dapper.ProviderTools/BulkCopy.cs and DynamicBulkCopy.cs now answer 1 each.

Cause 2 is live, undisclosed, and tracked nowhere else

The issue's second measured example — "the one that settles it" — still reproduces exactly as written on master:

get_dependencies("Dapper/SqlMapper.Async.cs", direction: "in") -> total: 0

namespace Dapper is a single segment and already matches 18 using Dapper… rows; all 18 are dropped by the package-scope guard because three package roots declare that namespace. importers_of_file's own doc at local_index.rs:3984 says what is wrong with that:

using Dapper; genuinely does not say which of 54 files it wants, so the guard is right to refuse; what is not right is that the reply says total: 0 with no channel saying candidates were dropped for ambiguity. That disclosure is a separate change and is not made here.

I enumerated all open issues (paging past the 50-row API cap): nothing else covers it. Closing this would remove the only release-gate entry for a symptom that is still reproducible verbatim, in the same tool, in the same reply, as the same user-visible zero — which is why it stays here rather than being closed-and-refiled.

Corrected scope

Replace the Mechanism section with:

Two independent causes produce this zero. Cause 1 (matched_keys comparing a dotted namespace against one path segment) is FIXED. Cause 2 remains: a single-segment namespace matches, and every match is then dropped by the package-scope guard. Refusing is correct; reporting total: 0 with no ambiguity channel is not.

Also correct the Measured table: only the two ProviderTools rows ever supported the matched_keys diagnosis.

Do not close this by widening the guard. The guard's refusal is right; the missing thing is a channel in the payload — not prose in the tool description — saying candidates existed and were dropped for package-scope ambiguity.

## STAYING OPEN, NARROWED — cause 1 fixed, cause 2 live, and the issue's own decisive example still returns `total: 0` Close-out lane, master `552e3a2`. **Title corrected.** ### Cause 1 is fixed and graded `crates/daemon/src/local_index.rs:6774` — `matched_keys` now takes `&[MatchKey]` with a `module_path` provenance flag and applies two rules by key shape: a single segment matches any segment; a multi-segment **module path** must match whole. `MatchKey` construction at `:4020-4042`. Three e2e tests pass in **both** legs (**EXIT=0**), with a real anti-vacuity guard inside `a_multi_segment_file_stem_is_not_a_module_path`. `Dapper.ProviderTools/BulkCopy.cs` and `DynamicBulkCopy.cs` now answer 1 each. ### Cause 2 is live, undisclosed, and tracked nowhere else The issue's second measured example — *"the one that settles it"* — still reproduces exactly as written on master: ``` get_dependencies("Dapper/SqlMapper.Async.cs", direction: "in") -> total: 0 ``` `namespace Dapper` is a **single** segment and already matches 18 `using Dapper…` rows; all 18 are dropped by the package-scope guard because three package roots declare that namespace. `importers_of_file`'s own doc at `local_index.rs:3984` says what is wrong with that: > `using Dapper;` genuinely does not say which of 54 files it wants, so the guard is right to refuse; **what is not right is that the reply says `total: 0` with no channel saying candidates were dropped for ambiguity. That disclosure is a separate change and is not made here.** I enumerated all open issues (paging past the 50-row API cap): **nothing else covers it.** Closing this would remove the only release-gate entry for a symptom that is still reproducible verbatim, in the same tool, in the same reply, as the same user-visible zero — which is why it stays here rather than being closed-and-refiled. ### Corrected scope Replace the Mechanism section with: > Two independent causes produce this zero. **Cause 1** (`matched_keys` comparing a dotted namespace against one path segment) is **FIXED**. **Cause 2** remains: a single-segment namespace matches, and every match is then dropped by the package-scope guard. Refusing is correct; reporting `total: 0` with no ambiguity channel is not. Also correct the Measured table: only the two `ProviderTools` rows ever supported the `matched_keys` diagnosis. **Do not close this by widening the guard.** The guard's refusal is right; the missing thing is a channel in the payload — not prose in the tool description — saying candidates existed and were dropped for package-scope ambiguity.
buildagent changed title from get_dependencies(direction: "in") is structurally always empty for C# — matched_keys compares a whole dotted namespace against one path segment, and the only e2e test uses single-segment Rust keys to get_dependencies(direction: "in") answers total: 0 with no channel saying candidates were dropped for package-scope ambiguity — the matched_keys half is fixed, this half is not 2026-09-06 12:48:17 +02:00
Author
Member

Cause 2 FIXED, merged as cbeb655. Graded by crates/mcp-server/tests/scope_ambiguity_e2e.rs (3/3).

importers_of_file now returns an ImportersReply carrying scope_ambiguity { dropped_importers, ambiguous_keys, declaring_package_roots, declaring_files }, counted inside the filter that makes the removals rather than reconstructed afterwards — so the number cannot drift from the thing it describes.

Three-state, as required: the block is emitted only when the guard actually fired, so its absence beside total: 0 is the measured "nothing was dropped", and scope_ambiguity_unavailable carries "this build did not report". Wire-compatible in both directions — a scope_disclosure request flag buys the object, old clients keep the 2-tuple, and RpcIndex decodes both.

Mutations run: dropped_for_scope += 1 deleted → RED on both legs, with the e2e printing {"next_cursor":null,"results":[],"total":0} — this issue's reported reply, byte for byte; scope_ambiguity emitted unconditionally → RED on the control on both legs (dropped_importers: 0, ambiguous_keys: []), which is what stops the disclosure from becoming vacuously present.

Closing on cause 2, which is what this issue is titled for. If the other causes in the body remain live, they are worth their own numbers rather than keeping this one open indefinitely.

Cause 2 FIXED, merged as `cbeb655`. Graded by `crates/mcp-server/tests/scope_ambiguity_e2e.rs` (3/3). `importers_of_file` now returns an `ImportersReply` carrying `scope_ambiguity { dropped_importers, ambiguous_keys, declaring_package_roots, declaring_files }`, **counted inside the filter that makes the removals** rather than reconstructed afterwards — so the number cannot drift from the thing it describes. Three-state, as required: the block is emitted only when the guard actually fired, so its **absence beside `total: 0` is the measured "nothing was dropped"**, and `scope_ambiguity_unavailable` carries "this build did not report". Wire-compatible in both directions — a `scope_disclosure` request flag buys the object, old clients keep the 2-tuple, and `RpcIndex` decodes both. Mutations run: `dropped_for_scope += 1` deleted → RED on both legs, with the e2e printing `{"next_cursor":null,"results":[],"total":0}` — **this issue's reported reply, byte for byte**; `scope_ambiguity` emitted unconditionally → RED on the control on both legs (`dropped_importers: 0, ambiguous_keys: []`), which is what stops the disclosure from becoming vacuously present. Closing on cause 2, which is what this issue is titled for. If the other causes in the body remain live, they are worth their own numbers rather than keeping this one open indefinitely.
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#174
No description provided.