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
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#199
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while 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
Articlemodel appears 47 times inpy-django. Ask the index which of them declares anid:Every other app's
Article.idis 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 isArticlebinds it to the one app that happened to writeidout.Same shape for
User.email, which lives onAbstractUserand is inherited by every concreteUser.Measured on the pinned corpus, all read at source:
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 animplcan hold no field):The 17 are real: 15 in
crates/rust-analyzer/src/lsp/to_proto.rsreadingide::Runnable.nav/kind/cfgandide::TestItem.file/text_range— where the source is EXPLICIT about which crate's type it means and a locality test overrules it — plusWSGIRequest.environandSerializer.streamin 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 Articlenames the app's own class), which is #196's clause.What would actually close it
Two independent directions, neither attempted:
class X(Base)toBaseand 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_gateindexes 0 corpus repositories and scores phantoms only against declared decoys; its whole JavaScript denominator is 1 site. It cannot see any of these.corpus_ratchetcounts resolutions. All 18 are resolutions; the count went UP.(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
importstatements 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).
build_recv_originbypassed #57 for four years of commits, and nothing could have told a reader #203Direction 1 SHIPPED — the census says "1 declared" now — and the 18 phantoms DO NOT REPRODUCE on master
Resolver-correctness lane, branch
lane/declared-member-censusoff master1d81180. 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-djangowith a1d81180binary:47 declarations, one writes
iddown. Verbatim.The 18 phantoms do not. Zero refs resolve to
Article.idor toUser.emailanywhere in that index, and all twelve of thea.id/user.emailsites this issue names aretarget_id IS NULL:The reason is not a fix: #69's member arm is not on master.
git log --allfinds 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 BOTHsafe_deleteandcheck_rename:ONE CLAUSE, SEVEN LANGUAGES, both halves independently observed:
Resting on the structural fact this issue identified: no plugin emits
inheritorimplement.producer_coverage_matrix's per-language cells already say so, and python's says it outright — "class A(B)emitsBas atyperef, 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_deletepushesinherited_surface_unknown, after thereasons.is_empty()test so it QUALIFIESno_evidence_of_useinstead of suppressing it, ranked 4 so it outranks the sentence it qualifies.check_renamegains a roster rowinherited_memberswhose status isdark— the channel never RAN, because there is no relation to run it over — socleanfalls away through #171's own derivation.Plus
SAFE_DELETE_REASONS: the registrycheck_renamehas had since #171 andsafe_deletenever 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:
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-expressis 0 because CommonJS express declares no member container at all — a fact about the corpus, not a filter.Article.idis in the firing set.BINDS: nothing here reaches the resolver, and that is measured rather than asserted
Eight pinned repos indexed with a
1d81180binary and with this branch's, joined on(path, line, col, kind, name):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:
TYPE_POSITIONclause 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 shapeclass SinatraTest < Minitest::Testis full of. Corpus: container-leveltyperefs 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.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.TYPE_POSITIONref 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.newemitstypewithroles=0, andrescue Alone,raise AloneandAlone::CONSTemit no ref at all, althoughraw::ref_role::TYPE_POSITION's own doc listsnew 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
javascriptrow and on no other:class X extends Basein a.jsfile emitted no reference row of any kind — not atyperef, not aread. The.tsbyte-identical source emittedBase|type|TYPE_POSITION. JavaScript inheritance was invisible tofind_references,change_impactand every graph tool.The two grammars shape
class_heritagedifferently and the loop descended one level unconditionally. The comment on thefield_definitionarm ten lines aboveemit_class_heritagepredicted exactly this shape in exactly this file, and it was still live.producer_coverage_matrix's javascriptinheritcell asserts "class A extends BemitsBas atyperef 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
.jswithclass … 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": 62condition 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
executed=7 unavailable=0is printed, so the ratchet is not the silentunavailable=1pass.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 newrefsreader was undeclared. ASiteentry now recordsnames: ["type"],Import::Excluded,Access::Excludedwith 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 theReasons: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 everycode-index-clisuite together, and failing only inside a fullcargo test --workspacetaken while three sibling lanes ran their own. Its assertion branches on whethercode-index-plugin-hostexists beside the CLI binary, which a concurrent build can change under it. Filing as a note rather than a fix.What is NOT closed
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 throughref_count— and on master no phantom bind exists to inflate it. Left as named residual rather than changed on a theory.MemberCensusBasis::unexpanded_supertypesis an unboundedVec<String>in a reply — no LIMIT, no cap, no truncation disclosure #205unexpanded_supertypesdoes not hold supertypes: it holds any TYPE_POSITION ref in the container span, so a Rust enum ships its variant payload types #240Fixed on
masterat4b332ff(mergede425320). 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 makedeclared_inshort 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 samesupertype_slot_predicateas the list beside it — one clause, seven languages, and it rides #240'sSUPERTYPErole rather than span containment.unexpanded_supertypesstays; its doc now says what it is actually for (the override hazard forsafe_delete).I verified the clause myself: flipping
NOT EXISTS→EXISTS(ask the containers that do declare it) → RED onevery_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
Articleclasses onpy-django, exactly one declaresid, attests/prefetch_related/models.py:186. The new field puts a number on the hole:undeclared_containers_with_supertypes = 46.The 12
a.idphantoms do NOT reproduce (target_id IS NULL), nor do the 4composite_pkones.But 2 of the 18 DO reproduce, live on master
8a8ea5c:Read at source:
django.contrib.auth'sUsergetsemailfromAbstractUser. This issue's own follow-up comment (written at1d81180) said "zero refs resolve to …User.emailanywhere" — 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 halvescolumn 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/tmpand never wrote to theirs):Census movement, read at source
Gains are the mechanism.
django/core/serializers/base.py:73 class Serializer:declaresstreamand names no base, while thejson/xml/python/pyyaml/jsonlSerializers 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'sinclude Rack::Utils/Helpers/Templates. Rust:impl Log for Loggerbeside inherentimpl Logger, where a trait default method is a member with no symbol. DjangoMeta/Mediaare 1,626 of py-django's 2,077 gains, verified real in-tree.Losses are the coincidence.
WSGIRequest(HttpRequest)declaresCOOKIES; the two otherWSGIRequests 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_inshort by anything". M3, M5 → RED. Two entries worth reading:same_named_containersstatement, 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 onleft: Some(1) / right: Some(0).skip_serializing_if): the daemon never constructsNone, 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
impland any sibling trait impl of the same type name fires: honest, but noisy for Rust.baseline.jsonmd5 unmoved (4b8dad0f…), verified post-merge. Closing.user.emailbinds across unrelated Django test apps via TIER1R_RECEIVER — 2 live phantoms on master, and #199's own comment says they were gone #246