A member that is INHERITED or IMPLICIT has no symbol, so an ambiguity census keyed on (container, member) is blind to it — 18 measured phantoms, and every same-name gate in the resolver has the same hole #199

Closed
opened 2026-09-06 20:01:50 +02:00 by buildagent · 2 comments
Member

Found while inspecting #69's 4,922 new binds at source. Filed separately because the cause is upstream of any one tier and the same blind spot is built into how this project measures ambiguity.

The measurement

Django's Article model appears 47 times in py-django. Ask the index which of them declares an id:

SELECT f.path FROM symbols s JOIN symbols p ON p.id = s.parent_id
JOIN files f ON f.id = s.file_id
WHERE s.name = 'id' AND p.name = 'Article';
-- exactly ONE row: tests/prefetch_related/models.py:186

Every other app's Article.id is Django's implicit primary key. It is real in the language, it is what the source means, and it is nowhere in the source text — so there is no symbol. The pair (Article, id) therefore looks unique, and a resolver that has proven the receiver's type is Article binds it to the one app that happened to write id out.

Same shape for User.email, which lives on AbstractUser and is inherited by every concrete User.

Measured on the pinned corpus, all read at source:

tests/basic/tests.py:49,54,134,149,158,165,168,172,186,324   a.id  -> tests/prefetch_related/models.py:186
tests/field_defaults/tests.py:36                             a.id  -> tests/prefetch_related/models.py:186
tests/foreign_object/tests.py:495                            a3.id -> tests/prefetch_related/models.py:186
tests/auth_tests/test_models.py:281,293                      user.email -> tests/select_related_onetoone/models.py:6
tests/composite_pk/test_create.py:34,49,55,56                user.email -> tests/select_related_onetoone/models.py:6

18 phantoms, every one crossing from one test app into an unrelated one.

Why this is more than a #69 finding

The gate #69 ships refuses a member bind when a competing candidate — a same-named container holding a same-named member — sits nearer than the chosen one. That test, and the ambiguity census built to bound it, are both keyed on the (container, member) PAIR. Both are blind here by construction, because the competing member is inherited or implicit and has no row.

The generalisation worth stating: this project routinely reasons about "is this name ambiguous?" by counting indexed symbols, and an inherited or implicit member is a member the index has never seen. Anywhere that reasoning appears — the tier-3 field gate, name_fallback_count, safe_delete's evidence, check_rename — the same hole exists, and it is silent rather than loud: the count says 1 and the honest answer is "1 that is written down".

Languages exposed by inheritance alone: python, ruby, php, csharp, typescript, rust (trait defaults). Frameworks that mint members without source: Django models and forms, ActiveRecord, C# source generators / partial classes, TypeScript mapped types.

What was measured and REFUSED

Widening #69's gate to test the CONTAINER name rather than the pair (restricted to class/struct/trait/module, since an impl can hold no field):

phantoms removed 12 of 18
phantoms surviving 6
correct binds lost 17

The 17 are real: 15 in crates/rust-analyzer/src/lsp/to_proto.rs reading ide::Runnable.nav/kind/cfg and ide::TestItem.file/text_range — where the source is EXPLICIT about which crate's type it means and a locality test overrules it — plus WSGIRequest.environ and Serializer.stream in py-django. Applied, measured over nine repos, reverted.

So proximity is the wrong instrument. The discriminator these binds need is import evidence (from .models import Article names the app's own class), which is #196's clause.

What would actually close it

Two independent directions, neither attempted:

  1. Make the census honest rather than the gate wider. A same-name count over indexed symbols should be able to say "1 declared, and this container has a base class / metaclass / framework convention I cannot see through". That turns a silent wrong answer into a disclosed one, which is this project's usual move, and it does not cost a single bind.
  2. Emit the inherited surface. Resolve class X(Base) to Base and let a member lookup walk the chain. Large, and it changes the candidate pool everywhere, so it needs its own bind-for-bind pass.

Why the existing gates could not see it

  • precision_gate indexes 0 corpus repositories and scores phantoms only against declared decoys; its whole JavaScript denominator is 1 site. It cannot see any of these.
  • corpus_ratchet counts resolutions. All 18 are resolutions; the count went UP.
  • The (container, member) ambiguity census — the instrument built for exactly this risk — reports these binds as UNAMBIGUOUS.

What found them was an oracle outside the index entirely: reading the referencing file's own import statements from source and asking whether the target's container is named there. 473 of py-django's 976 cross-file new binds were vouched that way; the unvouched remainder is where all 18 sat.

#69 (where they were measured), #196 (relative imports anchoring outside their subtree — the same "which same-named thing did you mean" family, with import evidence as the answer), #165 (a recall loss the corpus could not express; this is its mirror — a precision loss the CENSUS could not express).

Found while inspecting #69's 4,922 new binds at source. Filed separately because the cause is upstream of any one tier and the same blind spot is built into how this project measures ambiguity. ## The measurement Django's `Article` model appears **47 times** in `py-django`. Ask the index which of them declares an `id`: ```sql SELECT f.path FROM symbols s JOIN symbols p ON p.id = s.parent_id JOIN files f ON f.id = s.file_id WHERE s.name = 'id' AND p.name = 'Article'; -- exactly ONE row: tests/prefetch_related/models.py:186 ``` Every other app's `Article.id` is Django's **implicit primary key**. It is real in the language, it is what the source means, and it is nowhere in the source text — so there is no symbol. The pair `(Article, id)` therefore looks **unique**, and a resolver that has proven the receiver's type is `Article` binds it to the one app that happened to write `id` out. Same shape for `User.email`, which lives on `AbstractUser` and is inherited by every concrete `User`. Measured on the pinned corpus, all read at source: ``` tests/basic/tests.py:49,54,134,149,158,165,168,172,186,324 a.id -> tests/prefetch_related/models.py:186 tests/field_defaults/tests.py:36 a.id -> tests/prefetch_related/models.py:186 tests/foreign_object/tests.py:495 a3.id -> tests/prefetch_related/models.py:186 tests/auth_tests/test_models.py:281,293 user.email -> tests/select_related_onetoone/models.py:6 tests/composite_pk/test_create.py:34,49,55,56 user.email -> tests/select_related_onetoone/models.py:6 ``` **18 phantoms**, every one crossing from one test app into an unrelated one. ## Why this is more than a #69 finding The gate #69 ships refuses a member bind when a **competing candidate** — a same-named container holding a same-named member — sits nearer than the chosen one. That test, and the ambiguity census built to bound it, are both keyed on the `(container, member)` PAIR. **Both are blind here by construction**, because the competing member is inherited or implicit and has no row. The generalisation worth stating: **this project routinely reasons about "is this name ambiguous?" by counting indexed symbols, and an inherited or implicit member is a member the index has never seen.** Anywhere that reasoning appears — the tier-3 field gate, `name_fallback_count`, `safe_delete`'s evidence, `check_rename` — the same hole exists, and it is silent rather than loud: the count says 1 and the honest answer is "1 that is written down". Languages exposed by inheritance alone: python, ruby, php, csharp, typescript, rust (trait defaults). Frameworks that mint members without source: Django models and forms, ActiveRecord, C# source generators / partial classes, TypeScript mapped types. ## What was measured and REFUSED Widening #69's gate to test the CONTAINER name rather than the pair (restricted to `class`/`struct`/`trait`/`module`, since an `impl` can hold no field): | | | |---|---:| | phantoms removed | 12 of 18 | | phantoms surviving | 6 | | **correct binds lost** | **17** | The 17 are real: 15 in `crates/rust-analyzer/src/lsp/to_proto.rs` reading `ide::Runnable.nav/kind/cfg` and `ide::TestItem.file/text_range` — where the source is EXPLICIT about which crate's type it means and a locality test overrules it — plus `WSGIRequest.environ` and `Serializer.stream` in py-django. Applied, measured over nine repos, reverted. So proximity is the wrong instrument. The discriminator these binds need is **import evidence** (`from .models import Article` names the app's own class), which is #196's clause. ## What would actually close it Two independent directions, neither attempted: 1. **Make the census honest rather than the gate wider.** A same-name count over indexed symbols should be able to say "1 declared, and this container has a base class / metaclass / framework convention I cannot see through". That turns a silent wrong answer into a disclosed one, which is this project's usual move, and it does not cost a single bind. 2. **Emit the inherited surface.** Resolve `class X(Base)` to `Base` and let a member lookup walk the chain. Large, and it changes the candidate pool everywhere, so it needs its own bind-for-bind pass. ## Why the existing gates could not see it - `precision_gate` indexes **0** corpus repositories and scores phantoms only against declared decoys; its whole JavaScript denominator is 1 site. It cannot see any of these. - `corpus_ratchet` counts resolutions. All 18 are resolutions; the count went UP. - The `(container, member)` ambiguity census — the instrument built for exactly this risk — reports these binds as UNAMBIGUOUS. What found them was an oracle outside the index entirely: reading the referencing file's own `import` statements from source and asking whether the target's container is named there. 473 of py-django's 976 cross-file new binds were vouched that way; the unvouched remainder is where all 18 sat. ## Related #69 (where they were measured), #196 (relative imports anchoring outside their subtree — the same "which same-named thing did you mean" family, with import evidence as the answer), #165 (a recall loss the corpus could not express; this is its mirror — a precision loss the CENSUS could not express).
Author
Member

Direction 1 SHIPPED — the census says "1 declared" now — and the 18 phantoms DO NOT REPRODUCE on master

Resolver-correctness lane, branch lane/declared-member-census off master 1d81180. Not pushed.

First, the measurement, because half of it does not reproduce

The structural claim reproduces exactly. On a fresh index of the pinned py-django with a 1d81180 binary:

SELECT COUNT(*) FROM symbols WHERE name='Article' AND kind='class';       -> 47
SELECT f.path FROM symbols s JOIN symbols p ON p.id=s.parent_id
  JOIN files f ON f.id=s.file_id WHERE s.name='id' AND p.name='Article';
  -> tests/prefetch_related/models.py  (exactly one row)

47 declarations, one writes id down. Verbatim.

The 18 phantoms do not. Zero refs resolve to Article.id or to User.email anywhere in that index, and all twelve of the a.id / user.email sites this issue names are target_id IS NULL:

tests/basic/tests.py:49:29|id|read|a||          <- qualifier `a`, target NULL, resolved_by NULL
… :54 :134 :149 :158 :165 :168 :172 :186 :324, all NULL
tests/auth_tests/test_models.py:281,293|email|read|user||   both NULL
SELECT COUNT(*) FROM refs r JOIN symbols s ON s.id=r.target_id
  JOIN symbols p ON p.id=s.parent_id WHERE s.name='email' AND p.name='User';  -> 0

The reason is not a fix: #69's member arm is not on master. git log --all finds it on a branch (5c90083 feat: tier 1R serves the MEMBER pool, and its origin stops bypassing #57 (#69, #125)), unmerged. So the binds this issue read at source belong to that branch, and #203 reports its fix removes all 18 plus 38 pre-existing ones at zero cost. Nothing in this comment argues against that fix; it says only that a reader should not go looking for these binds on master and conclude the tooling is broken.

What survives untouched is the part that is not about any one tier: the census is blind by construction, and that blindness is upstream of whichever tier consumes it.

What shipped — direction 1, at zero binds

MemberCensusBasis, on BOTH safe_delete and check_rename:

{"container": "Article", "container_kind": "class",
 "same_named_containers": 47, "declared_in": 1,
 "unexpanded_supertypes": ["Model"],
 "semantics": "DECLARED-ONLY: this index records no `inherit`/`implement` relation in any language …"}

ONE CLAUSE, SEVEN LANGUAGES, both halves independently observed:

self.declared_in < self.same_named_containers        // the census disagrees with itself
    && !self.unexpanded_supertypes.is_empty()        // and the container names something unexpandable

Resting on the structural fact this issue identified: no plugin emits inherit or implement. producer_coverage_matrix's per-language cells already say so, and python's says it outright — "class A(B) emits B as a type ref, so a base-class list and a generic argument look identical." So the block does NOT claim to know what is inherited. It reports the three numbers that let a reader falsify the verdict above it.

Consumed two ways, both through existing machinery rather than a new one:

  • safe_delete pushes inherited_surface_unknown, after the reasons.is_empty() test so it QUALIFIES no_evidence_of_use instead of suppressing it, ranked 4 so it outranks the sentence it qualifies.
  • check_rename gains a roster row inherited_members whose status is dark — the channel never RAN, because there is no relation to run it over — so clean falls away through #171's own derivation.

Plus SAFE_DELETE_REASONS: the registry check_rename has had since #171 and safe_delete never did. Its vocabulary lived in a doc comment and a tool description, both prose.

Why the clause is not prose — MEASURED per repo

Either half alone fires on nearly everything. "The container names a supertype" is true of 85.4% of py-django's member symbols. Both together:

repo members fires rate
py-django 46 432 9 715 20.9%
python-flask 618 115 18.6%
ts-zod 1 692 280 16.5%
ruby-sinatra 1 063 156 14.7%
cs-dapper 3 048 426 14.0%
php-guzzle 3 052 299 9.8%
rust-ripgrep 3 478 2 082 59.9%
js-express 0 0 —

rust-ripgrep is high because a Rust method's container is the impl, and inherent impls split across blocks are genuinely ambiguous to a declared-only census. js-express is 0 because CommonJS express declares no member container at all — a fact about the corpus, not a filter.

Article.id is in the firing set.

BINDS: nothing here reaches the resolver, and that is measured rather than asserted

Eight pinned repos indexed with a 1d81180 binary and with this branch's, joined on (path, line, col, kind, name):

py-django     LOST=0 NEW=0 RETARGETED=0   (107491 both sides)
cs-dapper     LOST=0 NEW=0 RETARGETED=0   (3628)
ruby-sinatra  LOST=0 NEW=0 RETARGETED=0   (2850)
ts-zod        LOST=0 NEW=0 RETARGETED=0   (10781)
php-guzzle    LOST=0 NEW=0 RETARGETED=0   (11825)
js-express    LOST=0 NEW=0 RETARGETED=0   (4153)
rust-ripgrep  LOST=0 NEW=0 RETARGETED=0   (15709)
python-flask  LOST=0 NEW=0 RETARGETED=0   (3026)

rust-analyzer not re-indexed (no JavaScript, and this change reads no resolver input).

THREE MUTATIONS SURVIVED FIRST, and the fixture was fixed rather than the assertion

That is the part worth reading, because each survival was a real hole in the test:

  1. Dropping the TYPE_POSITION clause survived. Every fixture's container-level expression sat inside a child symbol's span, so the child exclusion was silently doing the whole job. Two controls now carry a container-level namespace segment (os.name, Foo.Bar) — the shape class SinatraTest < Minitest::Test is full of. Corpus: container-level type refs WITHOUT the bit vs WITH, ruby-sinatra 493 vs 132, py-django 4 077 vs 9 772, cs-dapper 122 vs 440, python-flask 90 vs 138. Now RED.
  2. Widening declared_in < to <= survived. Every control had an empty supertype list, so the second conjunct was false whatever the first said. A fourth fixture file per language now gives each a uniquely-named container that DOES name a supertype. Now RED.
  3. Disabling the child-span exclusion survived. No fixture had a TYPE_POSITION ref inside a member's span. Each control's member now names a type in its own signature. Now RED — for six languages. Ruby cannot express it, and that is recorded in the row rather than skipped: Alone.new emits type with roles=0, and rescue Alone, raise Alone and Alone::CONST emit no ref at all, although raw::ref_role::TYPE_POSITION's own doc lists new X(...) and the rescue class list among its slots. The exempted arm asserts there really are none, so a plugin that starts marking them fails the row.

All nine mutations run to real RED; the full list is in the commit message.

A LIVE PRODUCT DEFECT the census table found, in a language, not in a fixture

The seven-language table failed on its javascript row and on no other: class X extends Base in a .js file emitted no reference row of any kind — not a type ref, not a read. The .ts byte-identical source emitted Base|type|TYPE_POSITION. JavaScript inheritance was invisible to find_references, change_impact and every graph tool.

The two grammars shape class_heritage differently and the loop descended one level unconditionally. The comment on the field_definition arm ten lines above emit_class_heritage predicted exactly this shape in exactly this file, and it was still live. producer_coverage_matrix's javascript inherit cell asserts "class A extends B emits B as a type ref only" — that sentence was false for JavaScript and nothing graded it, because the matrix grades the KIND's absence and not the claim in its reason string.

No pinned repo can express it: js-express declares zero classes and ts-zod has no .js with class … extends, so the fix moves 0 binds on all eight — #165's shape, and a zero delta that is a statement about the corpus rather than about the change. Fixed in the same branch, graded by a plugin test asserting both grammars with each other as control.

No migration ships with it, and that is a decision. A re-parse is what reaches existing indexes (#125's shape), and minting one bumps the schema past the "schema": 62 condition FIVE protected records key on, forcing a bless of all five for a reason unrelated to their contents. It should ride on the next re-parse migration — including the m0063 #125's lane already has pending.

Gates

cargo fmt --all -- --check                                        0
cargo clippy --workspace --all-targets -- -D warnings             0
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
    --document-private-items                                      0
COSI_E2E_LEG=daemon cargo test -p code-index-mcp                  0   57 suites, 0 failed
corpus_ratchet  COSI_CORPUS_REQUIRE=1                             0   executed=7 unavailable=0
                                                                      baseline.json UNMOVED
precision_gate --release -- --nocapture                           0   7/7, phantoms=0 every
                                                                      language, POPULATION printed

executed=7 unavailable=0 is printed, so the ratchet is not the silent unavailable=1 pass.

Two of the workspace run's three failures were this change's, and both are real gates that did their job:

  • every_refs_reading_site_declares_a_ref_kind_stance — my new refs reader was undeclared. A Site entry now records names: ["type"], Import::Excluded, Access::Excluded with the corpus figures above as the reason. Suite re-run 14/14 green.
  • startup_payload_fits_its_token_budget — the two sentences I added to the tool descriptions cost 177 tokens over the bound. TRIMMED, not raised: the disclosure lives in the payload, which is where this project says it belongs; only the reason code stays in the Reasons: enumeration, so the registry and the prose cannot disagree. Suite re-run green.

The third, an_approved_package_that_cannot_load_is_named_with_its_reason, is not this change's and is a load-dependent flake: green in three isolated runs, green with its whole suite, green with every code-index-cli suite together, and failing only inside a full cargo test --workspace taken while three sibling lanes ran their own. Its assertion branches on whether code-index-plugin-host exists beside the CLI binary, which a concurrent build can change under it. Filing as a note rather than a fix.

What is NOT closed

  • The resolver half. The tier-3 field gate and tier 1R's origin live in crates/indexer/src/index.rs, which a sibling lane owns this round for #69/#203. Untouched here, deliberately.
  • name_fallback_count. It is a same-NAME count, not a (container, member) census, so #199's hole reaches it only through ref_count — and on master no phantom bind exists to inflate it. Left as named residual rather than changed on a theory.
  • Direction 2, emitting the inherited surface. Unattempted, and it needs its own bind-for-bind pass exactly as this issue says.
## Direction 1 SHIPPED — the census says "1 declared" now — and the 18 phantoms DO NOT REPRODUCE on master Resolver-correctness lane, branch `lane/declared-member-census` off master `1d81180`. Not pushed. ### First, the measurement, because half of it does not reproduce **The structural claim reproduces exactly.** On a fresh index of the pinned `py-django` with a `1d81180` binary: ``` SELECT COUNT(*) FROM symbols WHERE name='Article' AND kind='class'; -> 47 SELECT f.path FROM symbols s JOIN symbols p ON p.id=s.parent_id JOIN files f ON f.id=s.file_id WHERE s.name='id' AND p.name='Article'; -> tests/prefetch_related/models.py (exactly one row) ``` 47 declarations, one writes `id` down. Verbatim. **The 18 phantoms do not.** Zero refs resolve to `Article.id` or to `User.email` anywhere in that index, and all twelve of the `a.id` / `user.email` sites this issue names are `target_id IS NULL`: ``` tests/basic/tests.py:49:29|id|read|a|| <- qualifier `a`, target NULL, resolved_by NULL … :54 :134 :149 :158 :165 :168 :172 :186 :324, all NULL tests/auth_tests/test_models.py:281,293|email|read|user|| both NULL SELECT COUNT(*) FROM refs r JOIN symbols s ON s.id=r.target_id JOIN symbols p ON p.id=s.parent_id WHERE s.name='email' AND p.name='User'; -> 0 ``` The reason is not a fix: **#69's member arm is not on master.** `git log --all` finds it on a branch (`5c90083 feat: tier 1R serves the MEMBER pool, and its origin stops bypassing #57 (#69, #125)`), unmerged. So the binds this issue read at source belong to that branch, and #203 reports its fix removes all 18 plus 38 pre-existing ones at zero cost. **Nothing in this comment argues against that fix; it says only that a reader should not go looking for these binds on master and conclude the tooling is broken.** What survives untouched is the part that is not about any one tier: *the census is blind by construction, and that blindness is upstream of whichever tier consumes it.* ### What shipped — direction 1, at zero binds `MemberCensusBasis`, on BOTH `safe_delete` and `check_rename`: ```json {"container": "Article", "container_kind": "class", "same_named_containers": 47, "declared_in": 1, "unexpanded_supertypes": ["Model"], "semantics": "DECLARED-ONLY: this index records no `inherit`/`implement` relation in any language …"} ``` **ONE CLAUSE, SEVEN LANGUAGES**, both halves independently observed: ```rust self.declared_in < self.same_named_containers // the census disagrees with itself && !self.unexpanded_supertypes.is_empty() // and the container names something unexpandable ``` Resting on the structural fact this issue identified: **no plugin emits `inherit` or `implement`.** `producer_coverage_matrix`'s per-language cells already say so, and python's says it outright — *"`class A(B)` emits `B` as a `type` ref, so a base-class list and a generic argument look identical."* So the block does NOT claim to know what is inherited. It reports the three numbers that let a reader falsify the verdict above it. Consumed two ways, both through existing machinery rather than a new one: * `safe_delete` pushes `inherited_surface_unknown`, **after** the `reasons.is_empty()` test so it QUALIFIES `no_evidence_of_use` instead of suppressing it, ranked 4 so it outranks the sentence it qualifies. * `check_rename` gains a roster row `inherited_members` whose status is **`dark`** — the channel never RAN, because there is no relation to run it over — so `clean` falls away through #171's own derivation. Plus `SAFE_DELETE_REASONS`: the registry `check_rename` has had since #171 and `safe_delete` never did. Its vocabulary lived in a doc comment and a tool description, both prose. ### Why the clause is not prose — MEASURED per repo Either half alone fires on nearly everything. **"The container names a supertype" is true of 85.4% of py-django's member symbols.** Both together: | repo | members | fires | rate | |---|---:|---:|---:| | py-django | 46 432 | 9 715 | 20.9% | | python-flask | 618 | 115 | 18.6% | | ts-zod | 1 692 | 280 | 16.5% | | ruby-sinatra | 1 063 | 156 | 14.7% | | cs-dapper | 3 048 | 426 | 14.0% | | php-guzzle | 3 052 | 299 | 9.8% | | rust-ripgrep | 3 478 | 2 082 | 59.9% | | js-express | **0** | 0 | — | rust-ripgrep is high because a Rust method's container is the `impl`, and inherent impls split across blocks are genuinely ambiguous to a declared-only census. `js-express` is 0 because CommonJS express declares no member container at all — a fact about the corpus, not a filter. `Article.id` is in the firing set. ### BINDS: nothing here reaches the resolver, and that is measured rather than asserted Eight pinned repos indexed with a `1d81180` binary and with this branch's, joined on `(path, line, col, kind, name)`: ``` py-django LOST=0 NEW=0 RETARGETED=0 (107491 both sides) cs-dapper LOST=0 NEW=0 RETARGETED=0 (3628) ruby-sinatra LOST=0 NEW=0 RETARGETED=0 (2850) ts-zod LOST=0 NEW=0 RETARGETED=0 (10781) php-guzzle LOST=0 NEW=0 RETARGETED=0 (11825) js-express LOST=0 NEW=0 RETARGETED=0 (4153) rust-ripgrep LOST=0 NEW=0 RETARGETED=0 (15709) python-flask LOST=0 NEW=0 RETARGETED=0 (3026) ``` rust-analyzer not re-indexed (no JavaScript, and this change reads no resolver input). ### THREE MUTATIONS SURVIVED FIRST, and the fixture was fixed rather than the assertion That is the part worth reading, because each survival was a real hole in the test: 1. **Dropping the `TYPE_POSITION` clause survived.** Every fixture's container-level expression sat inside a child symbol's span, so the child exclusion was silently doing the whole job. Two controls now carry a container-level namespace segment (`os.name`, `Foo.Bar`) — the shape `class SinatraTest < Minitest::Test` is full of. Corpus: container-level `type` refs WITHOUT the bit vs WITH, ruby-sinatra **493 vs 132**, py-django **4 077 vs 9 772**, cs-dapper 122 vs 440, python-flask 90 vs 138. Now RED. 2. **Widening `declared_in <` to `<=` survived.** Every control had an empty supertype list, so the second conjunct was false whatever the first said. A fourth fixture file per language now gives each a uniquely-named container that DOES name a supertype. Now RED. 3. **Disabling the child-span exclusion survived.** No fixture had a `TYPE_POSITION` ref inside a member's span. Each control's member now names a type in its own signature. Now RED — for six languages. **Ruby cannot express it**, and that is recorded in the row rather than skipped: `Alone.new` emits `type` with `roles=0`, and `rescue Alone`, `raise Alone` and `Alone::CONST` emit no ref at all, although `raw::ref_role::TYPE_POSITION`'s own doc lists `new X(...)` and the rescue class list among its slots. The exempted arm asserts there really are none, so a plugin that starts marking them fails the row. All nine mutations run to real RED; the full list is in the commit message. ### A LIVE PRODUCT DEFECT the census table found, in a language, not in a fixture The seven-language table failed on its `javascript` row and on no other: **`class X extends Base` in a `.js` file emitted no reference row of any kind** — not a `type` ref, not a `read`. The `.ts` byte-identical source emitted `Base|type|TYPE_POSITION`. JavaScript inheritance was invisible to `find_references`, `change_impact` and every graph tool. The two grammars shape `class_heritage` differently and the loop descended one level unconditionally. The comment on the `field_definition` arm ten lines above `emit_class_heritage` predicted exactly this shape in exactly this file, and it was still live. `producer_coverage_matrix`'s javascript `inherit` cell asserts *"`class A extends B` emits `B` as a `type` ref only"* — that sentence was **false for JavaScript** and nothing graded it, because the matrix grades the KIND's absence and not the claim in its reason string. **No pinned repo can express it**: js-express declares zero classes and ts-zod has no `.js` with `class … extends`, so the fix moves 0 binds on all eight — #165's shape, and a zero delta that is a statement about the corpus rather than about the change. Fixed in the same branch, graded by a plugin test asserting both grammars with each other as control. **No migration ships with it, and that is a decision.** A re-parse is what reaches existing indexes (#125's shape), and minting one bumps the schema past the `"schema": 62` condition FIVE protected records key on, forcing a bless of all five for a reason unrelated to their contents. It should ride on the next re-parse migration — including the m0063 #125's lane already has pending. ### Gates ``` cargo fmt --all -- --check 0 cargo clippy --workspace --all-targets -- -D warnings 0 RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items 0 COSI_E2E_LEG=daemon cargo test -p code-index-mcp 0 57 suites, 0 failed corpus_ratchet COSI_CORPUS_REQUIRE=1 0 executed=7 unavailable=0 baseline.json UNMOVED precision_gate --release -- --nocapture 0 7/7, phantoms=0 every language, POPULATION printed ``` `executed=7 unavailable=0` is printed, so the ratchet is not the silent `unavailable=1` pass. **Two of the workspace run's three failures were this change's, and both are real gates that did their job:** * `every_refs_reading_site_declares_a_ref_kind_stance` — my new `refs` reader was undeclared. A `Site` entry now records `names: ["type"]`, `Import::Excluded`, `Access::Excluded` with the corpus figures above as the reason. Suite re-run **14/14 green**. * `startup_payload_fits_its_token_budget` — the two sentences I added to the tool descriptions cost **177 tokens over the bound**. TRIMMED, not raised: the disclosure lives in the payload, which is where this project says it belongs; only the reason code stays in the `Reasons:` enumeration, so the registry and the prose cannot disagree. Suite re-run green. The third, `an_approved_package_that_cannot_load_is_named_with_its_reason`, is **not this change's** and is a load-dependent flake: green in three isolated runs, green with its whole suite, green with every `code-index-cli` suite together, and failing only inside a full `cargo test --workspace` taken while three sibling lanes ran their own. Its assertion branches on whether `code-index-plugin-host` exists beside the CLI binary, which a concurrent build can change under it. Filing as a note rather than a fix. ### What is NOT closed * **The resolver half.** The tier-3 field gate and tier 1R's origin live in `crates/indexer/src/index.rs`, which a sibling lane owns this round for #69/#203. Untouched here, deliberately. * **`name_fallback_count`.** It is a same-NAME count, not a `(container, member)` census, so #199's hole reaches it only through `ref_count` — and on master no phantom bind exists to inflate it. Left as named residual rather than changed on a theory. * **Direction 2, emitting the inherited surface.** Unattempted, and it needs its own bind-for-bind pass exactly as this issue says.
Author
Member

Fixed on master at 4b332ff (merged e425320). The defect was not where this issue put it, and two of its eighteen phantoms are live on master today.

Where the defect actually was

declared_count_may_understate's second conjunct asked whether the symbol's own container names an unexpandable supertype. That container is the one that declares the member — it is the symbol's parent — so a base list there cannot make declared_in short by anything.

The containers whose missing declaration the census is short by are the other same_named_containers − declared_in, and nothing ever looked at them.

New field MemberCensusBasis::undeclared_containers_with_supertypes (Option<u64>, three-state) counts exactly those, spliced from the same supertype_slot_predicate as the list beside it — one clause, seven languages, and it rides #240's SUPERTYPE role rather than span containment. unexpanded_supertypes stays; its doc now says what it is actually for (the override hazard for safe_delete).

I verified the clause myself: flipping NOT EXISTS → EXISTS (ask the containers that do declare it) → RED on every_language_reports_a_declared_only_member_census.

Do this issue's numbers survive? Split verdict, and half of it is new

The structural claim reproduces verbatim. 47 Article classes on py-django, exactly one declares id, at tests/prefetch_related/models.py:186. The new field puts a number on the hole: undeclared_containers_with_supertypes = 46.

The 12 a.id phantoms do NOT reproduce (target_id IS NULL), nor do the 4 composite_pk ones.

But 2 of the 18 DO reproduce, live on master 8a8ea5c:

tests/auth_tests/test_models.py:281  user.email  ->  tests/select_related_onetoone/models.py:6
tests/auth_tests/test_models.py:293  user.email  ->  (same, an unrelated test app's User.email)
resolved_by = 60 = TIER1R_RECEIVER

Read at source: django.contrib.auth's User gets email from AbstractUser. This issue's own follow-up comment (written at 1d81180) said "zero refs resolve to … User.email anywhere" — but #69's member arm landed after it (96d428a), and #203's fix did not remove these two.

So the comment was true when written and is false now. That is a live precision defect and it is filed separately. The census does disclose the conditions for it (same_named=14, declared_in=1, undeclared_with_supertypes=11).

The old firing-rate table re-measures exactly — #240's both halves column reproduces to the row — so the instrument agrees with the tree.

The reachable population, measured

Fresh indexes built with this lane's binary (the shared corpus DBs are pre-#240, supertype_refs = 0, and three were mid-rebuild by sibling lanes — so it indexed into its own /tmp and never wrote to theirs):

repo reachable gap this build fires on
py-django 46,432 11,980 11,682 (25.2%)
rust-analyzer 23,393 20,402 8,727 (37.3%)
rust-ripgrep 3,478 3,147 845 (24.3%)
cs-dapper 3,048 918 302 (9.9%)
js-express 0 0 0

Census movement, read at source

Gains are the mechanism. django/core/serializers/base.py:73 class Serializer: declares stream and names no base, while the json/xml/python/pyyaml/jsonl Serializers all subclass it — the old clause was silent on exactly the bind this issue's body names as "a correct bind lost". Ruby: Sinatra::Base's include Rack::Utils/Helpers/Templates. Rust: impl Log for Logger beside inherent impl Logger, where a trait default method is a member with no symbol. Django Meta/Media are 1,626 of py-django's 2,077 gains, verified real in-tree.

Losses are the coincidence. WSGIRequest(HttpRequest) declares COOKIES; the two other WSGIRequests are bare stubs inside a test method where nothing can supply it. And cs-dapper's 302 → 302 is a coincidence of totals — 65 rows each way, confirmed to be different rows.

Mutations

M1 (restore the old conjunct) → RED, "the supertype is on the container that DOES write the member down, which cannot make declared_in short by anything". M3, M5 → RED. Two entries worth reading:

  • M2 went RED on the WRONG GUARD first: the anchor also matched the same_named_containers statement, so the parameter landed there and it died on "Wrong number of parameters passed to query. Got 3, needed 4". Re-anchored; M2c is RED on left: Some(1) / right: Some(0).
  • M4b was a COMPILE ERROR, not a mutation — recorded as such rather than counted.
  • M4 SURVIVED (drop skip_serializing_if): the daemon never constructs None, so that arm is unreachable in production. Recorded on the test, same reason its sibling field already records.

Token bands attributed by re-running with the old conjunct restored: the predicate change costs ~0; the +21/+32 is the new field, and the −189 on cs-dapper is master's, not this lane's (recorded the day #240 merged). Nothing re-recorded; all inside the 60-token drift allowance.

Residual, stated

The new count asks whether a mechanism is present, not whether that supertype could plausibly carry that member — that needs direction 2 (emit the inherited surface), still unattempted. Rust's rate is high (37.3% on rust-analyzer) because a method's container is the impl and any sibling trait impl of the same type name fires: honest, but noisy for Rust.

baseline.json md5 unmoved (4b8dad0f…), verified post-merge. Closing.

Fixed on `master` at `4b332ff` (merged `e425320`). **The defect was not where this issue put it, and two of its eighteen phantoms are live on master today.** ## Where the defect actually was `declared_count_may_understate`'s second conjunct asked whether **the symbol's own container** names an unexpandable supertype. That container is the one that *declares* the member — it is the symbol's parent — so a base list there **cannot make `declared_in` short by anything**. The containers whose missing declaration the census is short by are the other `same_named_containers − declared_in`, and **nothing ever looked at them**. New field `MemberCensusBasis::undeclared_containers_with_supertypes` (`Option<u64>`, three-state) counts exactly those, spliced from the same `supertype_slot_predicate` as the list beside it — one clause, seven languages, and it rides #240's `SUPERTYPE` role rather than span containment. `unexpanded_supertypes` stays; its doc now says what it is actually for (the **override** hazard for `safe_delete`). I verified the clause myself: flipping `NOT EXISTS` → `EXISTS` (ask the containers that *do* declare it) → **RED** on `every_language_reports_a_declared_only_member_census`. ## Do this issue's numbers survive? Split verdict, and half of it is new **The structural claim reproduces verbatim.** 47 `Article` classes on `py-django`, exactly one declares `id`, at `tests/prefetch_related/models.py:186`. The new field puts a number on the hole: `undeclared_containers_with_supertypes = 46`. **The 12 `a.id` phantoms do NOT reproduce** (`target_id IS NULL`), nor do the 4 `composite_pk` ones. **But 2 of the 18 DO reproduce, live on master `8a8ea5c`:** ``` tests/auth_tests/test_models.py:281 user.email -> tests/select_related_onetoone/models.py:6 tests/auth_tests/test_models.py:293 user.email -> (same, an unrelated test app's User.email) resolved_by = 60 = TIER1R_RECEIVER ``` Read at source: `django.contrib.auth`'s `User` gets `email` from `AbstractUser`. This issue's own follow-up comment (written at `1d81180`) said *"zero refs resolve to … `User.email` anywhere"* — but **#69's member arm landed after it** (`96d428a`), and #203's fix did not remove these two. So the comment was true when written and is false now. **That is a live precision defect and it is filed separately.** The census does disclose the conditions for it (`same_named=14, declared_in=1, undeclared_with_supertypes=11`). The old firing-rate table re-measures exactly — #240's `both halves` column reproduces to the row — so the instrument agrees with the tree. ## The reachable population, measured Fresh indexes built with this lane's binary (the shared corpus DBs are pre-#240, `supertype_refs = 0`, and three were mid-rebuild by sibling lanes — so it indexed into its own `/tmp` and never wrote to theirs): | repo | reachable | gap | this build fires on | |---|---:|---:|---:| | py-django | 46,432 | 11,980 | **11,682 (25.2%)** | | rust-analyzer | 23,393 | 20,402 | **8,727 (37.3%)** | | rust-ripgrep | 3,478 | 3,147 | **845 (24.3%)** | | cs-dapper | 3,048 | 918 | 302 (9.9%) | | js-express | **0** | 0 | 0 | ## Census movement, read at source **Gains are the mechanism.** `django/core/serializers/base.py:73 class Serializer:` declares `stream` and names no base, while the `json`/`xml`/`python`/`pyyaml`/`jsonl` `Serializer`s all subclass it — the old clause was silent on exactly the bind this issue's body names as *"a correct bind lost"*. Ruby: `Sinatra::Base`'s `include Rack::Utils/Helpers/Templates`. Rust: `impl Log for Logger` beside inherent `impl Logger`, where a trait **default method** is a member with no symbol. Django `Meta`/`Media` are 1,626 of py-django's 2,077 gains, verified real in-tree. **Losses are the coincidence.** `WSGIRequest(HttpRequest)` declares `COOKIES`; the two other `WSGIRequest`s are bare stubs inside a test method where nothing can supply it. And **cs-dapper's 302 → 302 is a coincidence of totals** — 65 rows each way, confirmed to be different rows. ## Mutations M1 (restore the old conjunct) → **RED**, *"the supertype is on the container that DOES write the member down, which cannot make `declared_in` short by anything"*. M3, M5 → RED. Two entries worth reading: - **M2 went RED on the WRONG GUARD first**: the anchor also matched the `same_named_containers` statement, so the parameter landed there and it died on *"Wrong number of parameters passed to query. Got 3, needed 4"*. Re-anchored; **M2c** is RED on `left: Some(1) / right: Some(0)`. - **M4b was a COMPILE ERROR, not a mutation** — recorded as such rather than counted. - **M4 SURVIVED** (drop `skip_serializing_if`): the daemon never constructs `None`, so that arm is unreachable in production. Recorded on the test, same reason its sibling field already records. Token bands attributed by re-running with the old conjunct restored: the predicate change costs **~0**; the +21/+32 is the new field, and **the −189 on cs-dapper is master's, not this lane's** (recorded the day #240 merged). Nothing re-recorded; all inside the 60-token drift allowance. ## Residual, stated The new count asks whether a mechanism is *present*, not whether that supertype could plausibly carry *that* member — that needs direction 2 (emit the inherited surface), still unattempted. Rust's rate is high (37.3% on rust-analyzer) because a method's container is the `impl` and any sibling trait impl of the same type name fires: honest, but noisy for Rust. `baseline.json` md5 unmoved (`4b8dad0f…`), verified post-merge. 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#199
No description provided.