resolver: profile-driven member/property binding with zero-phantom capability gates #69
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.
Depends on
Reference
h-dv/code-index#69
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?
Follow-on from I040.
name_fallback_countnow DISCLOSES the shortfall on attribute access; this would reduce it.The measured case
A Python
@property disarmedaccessed 6 times: all 6 refs exist in therefstable, only the same-fileself.one resolves.search_symbolsreportsref_count: 1, name_fallback_count: 5. The customer's original report measured 2 vs a ground truth of 5 in their own repo.Why they don't resolve, and why it looks fixable
The same code as a method call resolves 2/2. The asymmetry is structural:
refs.kind = 'method_call' AND refs.qualified = 0 AND refs.qualifier IS NOT NULLkind='type',qualified=1with a dotted-path qualifier, so they route to tier 1Q, which anchors on file stems/modules — where a variable name likecfgcan never anchor.The binding facts already exist: the fixture produced
cfg → LeaseConfigbinding rows in python, ts, csharp, php and rust.Sketch
A "tier 1P" mirroring tier 1R at
index.rs, reusingrecv_bindsunchanged:kind='type' AND qualified=1 AND target_id IS NULL AND qualifier NOT LIKE '%.%'(bare identifier ⇒ not a module path), plus ruby'skind='method_call'rows which already match tier 1R's input shaperecv_boundCTE, verbatim)m.kind = 'method'tom.kind IN ('method','field')Resolved rows then fall into the existing
member_accessre-kind, which is correct: a property is not a type edge.What it cannot buy
anything.disarmedwhere the receiver has no annotationSo the disclosure stays load-bearing permanently regardless of how far this gets. That is the argument for having shipped
name_fallback_countfirst.Risk
Real. The module-path collision (
pkg.CONSTANTalso arrives askind='type',qualified=1,roles=0) is contained by requiring a dot-free qualifier WITH an in-scope binding row, but this needs the full adversarial-review treatment plus per-languageoracle.tomlforbid_resolveddecoys before it can pass the zero-phantom gate.Estimate ~1 week including oracle decoys and a review round.
Runtime-plugin architecture revision
Do not ship this as a new hardcoded language list. Under #77 it becomes a closed core resolver algorithm selected by a language-profile capability.
The algorithm’s authority is bounded by:
Builtin Python/TypeScript/C# fixtures are the first implementation and compatibility proof. A dynamic package may request the algorithm but cannot provide custom SQL or broaden its candidate predicate.
Add acceptance cases for an inert package, a package granted only member candidates, language-id spoofing, mixed-language files and generation rollback. Existing zero-phantom oracles remain the merge gate.
resolver: tier 1P — resolve attribute/property access for python, typescript, csharpto resolver: profile-driven member/property binding with zero-phantom capability gatesTriage 2026-09-06: LEFT OPEN — the work is genuinely undone, but this issue's stated input predicate is DEAD and the spec needs rewriting before anyone picks it up.
The predicate this issue is written against no longer exists
This issue specifies its input as
kind='type' AND qualified=1. Since I059, member/attribute access refs are emitted asRefKind::READ/WRITEwithqualified: true—crates/plugins/src/common.rs:407-420— never askind='type'. Confirmed for the two languages this issue says are dark (they now emit; see #68, closed today) and for the four it says emittype.So the population this issue describes is empty. Anyone implementing it as written would build a tier that matches nothing.
The work itself is NOT done — verified
crates/indexer/src/index.rs:6542-6549—WHERE refs.target_id IS NULL … AND refs.kind = 'method_call' AND refs.qualified = 0 AND refs.qualifier IS NOT NULL. Noread/writerow can enter it, so widening the candidate join is moot until the input widens.search_text("tier 1P")returns exactly one hit, and it is a mission doc (_prdoc/missions/I040-payload-honesty-and-coverage.md:163).recv_bindsappears only inindex.rsandcrates/indexer/tests/pool_capability_registry.rs:462.What exists instead, and it is not the same thing
Generic-tier resolution of access refs plus a tier-3 field gate:
index.rs:7686unbinds anyDATA_MEMBER_ONLYread/write that bound a non-field, and:7732/:7756re-kind the rest totype/member_access. That is phantom control, not receiver binding — it stops wrong answers rather than producing right ones.The scope estimate has also moved
2f16e22records the reassessment: "the real change is two edits, not the issue's week" — and that it was left undone deliberately, for a reason worth keeping:That is the right instinct on this repo, where the standing rule is inspect binds, not deltas — three cuts once raised the resolved count and every extra bind was a phantom.
What this issue needs before work starts
read/write+qualified: true+DATA_MEMBER_ONLYworld. Thekind='type' AND qualified=1predicate should be struck, not reinterpreted.I have not attempted (1) — rewriting an issue's specification is a product decision, not a triage one.
🤖 Triage lane, 2026-09-06, master
45cf6e4method_calldraws only fromkind='method', and python.rs mints no module symbol at all #175IMPLEMENTED — tier 1R serves the MEMBER pool. +4 922 binds, 0 lost over nine pinned repos, bind-for-bind, with five measured phantoms found and refused
Resolver-recall lane, worktree off master
fc329a8. Baseline binary built from that commit and md5-pinned; every number below is a bind-set diff, never a count delta.The spec was dead, and here is what replaced it
This issue specified its input as
kind='type' AND qualified=1. That population is empty since I059 phase 2 re-kinded member access toread/write. The predicate is struck, not reinterpreted. The shipped input is:Three structural clauses, none of which asks whether anything RESOLVED, so the resolve firewall is intact. The bare-identifier cut is the access-ref spelling of
recv_proof::UNCAPTURED— a dotted qualifier is a module path or a field PATH, and neither has arecv_bindsrow. Self receivers are excluded because tier 1Q already anchors them to the enclosing file, which is the one place co-location is a fact about the receiver's type.The candidate side is not a hand-widened list
The issue proposed
m.kind IN ('method','field'). What shipped instead: tier 1R'sm.kind = 'method'was a hand-copied instance of theWHEN 1arm of the symbol-side pool-membership expression already used by thesymbol_bucketsbase pool gate. Both now splice onepool_class_admits(pool, sym), driven by the ref's ownpool_class. So a method call still draws fromkind='method'alone, a member-pool ref draws from the member pool, and the two copies cannot widen on one side only.recv_calls/recv_boundcarrypool_classfor the same reason: one file can hold a method call and a field read through the same receiver, and they must not draw from each other's pool.Result, bind-for-bind, joined on the full row (path:line:col, kind, name, target, rule)
Every one of the new binds carries
resolved_by = 60(TIER1R_RECEIVER). Nothing leaked into another tier.js-expressandruby-sinatragaining nothing is the issue's own prediction holding: untyped JS has no binding rows to key on, and Ruby'sread/writeare@ivaraccesses whose qualifier is a self receiver.Bounded with an oracle OUTSIDE the resolver
Each rust crate's own
Cargo.toml, over the 3 231 rust-analyzer new binds: 2 039 same-crate, 1 192 cross-crate, 2 undeclared — and both of those, read at source, are correct binds travellingide-db'spub use base_dbre-export (crates/ide/src/fetch_crates.rs:37and:42, where the parameter is annotated&ide_db::base_db::ExtraCrateData). rust-ripgrep: 326 new binds, 0 cross-crate.Second census, on every repo: new binds whose
(container, member, lang)triple is AMBIGUOUS project-wide — the only ones that can be a receiver-type phantom. 70 of 4 859 in the first cut. All read at source.FIVE PHANTOMS, and this is the finding the issue was deferred for
The earlier reassessment said "fields are the most collision-prone kind there is". The corpus agrees, and it is measurable rather than a hunch:
benchmarks/Dapper.Tests.Performance/LegacyTests.cs:231post.Idtests/Dapper.Tests/SharedTypes/Post.csbenchmarks/Dapper.Tests.Performance/Post.cs:275tests/foreign_object/tests.py:521referrer.articletests/indexes/models.pytests/foreign_object/models/article.pytests/serializers/tests.py:355player.teamtests/model_formsets/models.pytests/serializers/models/base.py:364The mechanism is tier 1R's ORIGIN gate, not the widening: a proven receiver type fixes the type NAME, not the FILE, and the evidence that picks between five
Postclasses or threePlayermodels is a file-key STEM match (from .models importmatches everymodels.pyin the tree) or a C# namespace prefix. Both are blind to which same-named container the source means.This is NOT pre-existing in a form I can point at, and I checked rather than assumed. A census of the baseline's rule-60 cross-file binds finds zero with an ambiguous container on py-django (871 cross-file) or cs-dapper (2). Method names are not the collision-prone kind. So the widening does introduce this class, and I did not get to leave it.
The gate that removes them, and the two versions that were measured and rejected
Shipped in
tier1r_row_gates: refuse a MEMBER-pool bind when a competing candidate — a same-named container holding a member of the same name and pool — sits inside the referencing file's own directory subtree, and the chosen one is outside it. Every one of the five had exactly that.Two earlier versions were built, measured over all nine repos and rejected:
crates/hir-ty/src/infer/cast.rsreadsctx.table, a field ofstruct InferenceContextincrates/hir-ty/src/infer.rs, and thirteenimpl InferenceContextblocks sit in the nearer subtree. Animplcan never hold a field, so they were never candidates.Final gate, versus the ungated widening: 5 phantoms refused, 5 correct binds refused (
crates/rust-analyzer/src/lsp/to_proto.rs:1914-1942,test_item.id/label/kind/parent/runnable—ide::TestItemandlsp_ext::TestItemare same-named types in the same directory, and the refusal is the honest "cannot tell"), and +70 correct binds GAINED on py-django, because removing a wrong candidate makes a previously-ambiguous group unique. All 70 aretests/<app>/tests.py -> tests/<app>/models.pyunder Django's per-app test convention;Article.headlinealone has 24 same-named containers project-wide. Net: 4 859 -> 4 922 new binds, and the phantom count goes to 0.Final ambiguity census on the shipped set: 0 on cs-dapper, php-guzzle, python-flask, ts-zod, rust-ripgrep; py-django 102 and rust-analyzer 31, of which the cross-file ones were all read at source and are correct (
ide_db::generated::lints::Lint,ide::Runnable,hir::MovedOutOfRef,hir_def::MatchArm,django.forms.DecimalField,django.utils.tree.Node,django.contrib.gis.geos.Point).Tests, with the mutations RUN
resolver::member_access_through_a_proven_receiver_resolves_cross_file— the issue's owncfg.disarmedshape.MUTATION: revert
recv_calls' WHERE tomethod_call AND qualified = 0. RED:left: None, right: Some("core/src/lease.rs").resolver::a_nearer_competing_member_refuses_the_distant_bind— the phantom class, minimized.MUTATION: delete the gate clause. RED: binds
other/models.py, the phantom, verbatim.This test's first version was VACUOUS and the mutation is what caught it. With both candidates named
models.pyboth are admitted, the count is 2, nothing binds, and deleting the gate changed nothing — the mutation passed GREEN. The shipped fixture puts the correct candidate inapp/pkg/models/base.py, whose stem the relative import does not name, so the origin gate admits exactly the wrong file. That asymmetry is the defect and the fixture now carries it.provenance_invariants:TIER1R_RECEIVER's promise wasrefs.kind = 'method_call'and is nowqualifier IS NOT NULLin one of the two shapesrecv_callsadmits. The old spelling was not a technicality — 1 227 of 1 626 tier-1R rows on this workspace's own index are no longer method calls, and the test went red on exactly that before the promise was widened.m0063re-parse migration (shared with #125): MUTATION — swap the body for m0062's re-heal. RED,left: 0, right: 2.Migration
m0063_pattern_reads_and_member_receivers, a RE-PARSE. Spec 04 requires a heal for every resolution-rule change; a re-parse is strictly stronger and #125 needs one anyway. Details on #125.What this still cannot buy — unchanged from the issue's own list
Ruby (no typed bindings), untyped JavaScript, and anything whose receiver has no annotation. Both corpora prove it: 0 new binds on
ruby-sinatraandjs-express. Thename_fallback_countdisclosure stays load-bearing permanently.Residual, NAMED
The origin gate's stem/namespace anchor is still what decides between same-named containers; the shipped gate only refuses where a nearer competitor was demonstrably passed over. Two of the three phantom families were a RELATIVE import (
from .models import) matching a file-key stem OUTSIDE the importing file's own directory subtree — which is a language-level impossibility and looks like one clean clause inbuild_recv_origin. It is not attempted here: it is shared with every other tier, so it would move the corpus for reasons unrelated to this issue, and it deserves its own bind-for-bind pass. Same family as #134 and #168.LineIndexphantoms survive on rust-analyzer because tier 1Q anchors on a file STEM — directions 1 and 2 (package_root_of/lib) measured and REFUSED, direction 3 is what remains #168Visibility::Unknownhas no wire slot, and it costs a packaged JavaScript 19% of ALL its resolutions — MEASURED #166from .models importreaches everymodels.pyin the tree, and it produced 3 of #69's 5 measured phantoms #196Gate results, the FOUR baselines this moves, and a COST finding I did not expect
Follow-up to the implementation comment. Nothing here is blessed — the drifts are reported with their evidence and a draft reason, for a decision by whoever owns the records.
Green
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo doc --workspace --no-deps --document-private-items(RUSTDOCFLAGS=-D warningsshape)precision_gatephantoms=0,recall=1.000in every languagecode-index-indexer --libresolverprovenance_invariantspool_capability_registrycode-index-pluginsprecision_gateis cited as the required gate and nothing more: it indexes 0 corpus repositories and scores phantoms only against declared decoys, so it is not evidence about any of the binds above.The four baselines that move, and every one of them moves for the SAME reason
tests/corpus/baseline.json:tests/corpus/stage-baseline.json— onlyrule.tier1r_receivermoved, on every repo, by exactly the resolved delta:tests/corpus/tier3-baseline.json:No other rule moved anywhere.
rule.tier1r_receiveraccounts for 100 % of every count in all four files, which is the strongest form of "this is the change and nothing else": if the widening had disturbed another tier, a second rule would have moved.Draft reason, for whoever blesses
AND A COST FINDING —
corpus_costis red, on SQLite's own countersThis is the one I did not anticipate, and it is a real regression report rather than a loaded runner:
vm_stepis SQLite's VM step counter, not the clock.Attributed, by building and measuring the variants. The first cut was worse — cs-dapper +42.1 %, ts-zod +36.1 %, and
autoindexandfullscan_stepover the band on five repos too. Two mistakes, both found by this gate and by nothing else:temp.file_ancestorwas built for EVERY file, when the only directory prefixes the gate can ask about are those a member-poolrecv_boundrow names. Restricting it putautoindexandfullscan_stepback inside the band on every repo.temp.recv_nearerwas driven offrecv_bound(one row per REF) instead of the distinct key set. Fixing that with an inlineSELECT DISTINCTmade ts-zod worse — +65 % — because SQLite materialised the subquery and re-scanned it. Materialising the key set into its own WITHOUT-ROWID table with a primary key, and pinning the join order withCROSS JOIN, brought it to zero.With both fixed, the gate machinery costs nothing measurable: the numbers above are within 0.03 % of a build with the gate and both relations disabled entirely. All of the remaining drift is the widening itself — tier 1R's input is now method calls plus member accesses, and member accesses are the more numerous kind.
So the cost question is a clean one, with no implementation waste left in it: is +6.7 % to +34.9 % more SQLite work an acceptable price for +4 922 resolved references and 0 lost? That is a product decision,
tests/corpus/cost-baseline.jsonis bless-gated, and I have not touched it. The bind set is byte-identical before and after both optimisations, verified by re-indexing all nine repos and diffing (TOTAL DIFF = 0).One test that failed only under load, and passes alone
index_coverage_facts_e2e::a_refused_file_is_not_reported_as_indexed_and_currentfailed once during a contended full-workspace run withcoverage_reasons: ["index_reconciling"]— the daemon was still inRuntimePhase::Reconcilingwhen the assertion ran. It passes in isolation (2 passed). Recording it because a longer reconcile makes that window wider, and the widening does lengthen the reconcile; the test waits forverdict != "pending", which is not the same condition as "the daemon is steady".RE-MEASURED ON TOP OF
9d0e06a— the cost was 4x overstated,vm_stepis now INSIDE the band on every repo, and the residual tracks one numberThe earlier cost comment measured against
fc329a8. That overstated this change's price by roughly four times, and the reason is mechanical: the widening multiplies the rows enteringCREATE TEMP TABLE recv_bound, and onfc329a8that statement carried an unsargable span test evaluated over every function in the file, per row. My cost was being multiplied by someone else's bug.Note on provenance:
9d0e06ais NOT onorigin/master. Master is4f866e5, which does not contain it;9d0e06asits directly onfc329a8, my own base. I rebased onto it because it is the baseline this will be judged against once it lands, and everything below says so.Rebase verified three ways before any measurement:
9d0e06a's bind set is byte-identical tofc329a8's on all nine repos (its own claim, independently confirmed,TOTAL DIFF = 0); my rebased lane's bind set is byte-identical to my pre-rebase measurement (TOTAL DIFF = 0); and against the new baseline it is still +4 922 / 0 lost.1 + 2. Cost and binds, side by side, all seven tier-1 repos
9d0e06adriver × is how much bigger tier 1R's input gets:
(method_call rows + member rows admitted) / method_call rows, counted on the baseline index.The last column is the one that decides the trade. Shipped together with
9d0e06a, cold-indexvm_stepgoes DOWN on every repo againsttests/corpus/cost-baseline.json— −1.3% to −11.0%.corpus_costreports exactly onevm_stepline now, and it is js-express improving past the −10% floor. The +5% ceiling is not breached onvm_stepanywhere.Answering the question you actually asked: no, the expensive repo is not the one being paid for
cs-dapper pays the highest percentage (+8.60%) for 106 binds. rust-ripgrep pays +2.09% for 326 — the most binds and the cheapest, at 4,630 opcodes each, an order of magnitude better than ts-zod's 81,846.
But the sharper fact is that the cost gate's seven repos hold 583 of the 4 922 binds — 12%. The two repos where 88% of the gain lands, rust-analyzer (+3,231) and py-django (+1,108), are not in
corpus_cost's set at all, so the 21,535 opcodes/bind above is computed on the least favourable eighth of the population. I am not going to extrapolate it — they are rust and python, the two languages with the best ratios in the table, but that is an inference and the measurement does not exist. That gap is itself worth recording: the cost gate and the recall gate are measured on different repo sets, so no single number in this project currently prices a bind.3. Is the residual inherent? Yes, and here is the control that proves it
ruby-sinatra: driver × 1.00, cost +0.26%. Ruby admits no member rows (its access refs are@ivarwith self receivers), so the change adds no rows there — and adds essentially no cost. The%column and thedriver ×column move together across all seven.Statement-level attribution on cs-dapper, the worst repo, with the instrument
9d0e06abuilt (cost_attribution.rs), baseline vs lane:CREATE TEMP TABLE recv_boundINSERT temp.recv_callsINSERT temp.recv_decisions(the ident arm)INSERT temp.recv_bindsfile_ancestor,recv_nearer,recv_nearer_keys~84% is three tier-1R statements whose input I enlarged. The gate machinery does not reach the top six statements — after the two fixes in the previous comment it costs nothing measurable, and this is the independent confirmation of that.
The third shape, built and refused. The obvious remaining idea: a member ref whose receiver has no
bindingrow of that name anywhere in its file can never surviverecv_bound's inner join —Foo.BarwhereFoonames a type or a namespace, which is most member access in C# and TypeScript, and it is exactly why js-express pays +1.86% for zero binds. Dropping those rows beforerecv_boundruns, by a primary-key-prefix seek intorecv_binds, cannot change a bind (the condition is the one the join already enforces).Measured, it is WORSE on every repo: cs-dapper 23,459,161 → 23,516,351, ts-zod 112,707,893 → 112,925,482. The delete costs more than the rows cost the join, because the
(file_id, name)primary-key probe was already discarding them for less than a scan ofrecv_calls. Applied and reverted;index.rsis md5-identical to its pre-experiment snapshot.So: the residual is one index seek per additional member reference. There is no shape left that I can see which pays for itself, and the control repo says the cost is the row count and nothing structural.
What is still over a ceiling, and what it is
fullscan_step, on five repos: cs-dapper +14.1%, ts-zod +10.5%, python-flask +9.0%, php-guzzle +6.6%, js-express +5.5%. Same cause, not a plan regression — the temp relations tier 1R scans (recv_calls,recv_bound) hold 1.3–1.9× the rows, and a full scan of a bigger table visits more rows by definition. Measured share: with the gate and both its relations disabled entirely, cs-dapper'sfullscan_stepis 306,775 against 310,861 with them, so the gate accounts for 10% of that delta and the widening for 90%.sortis flat everywhere;autoindexis back inside the band on every repo after the fixes in the previous comment.The bind-per-opcode case, made rather than argued by analogy
You are right that the m0061 precedent does not transfer: that paid once and repaid per call, and this has no per-call saving. The case here is different in kind and should be judged as such.
9d0e06athe cold index gets CHEAPER on every repo (−1.3% to −11.0%) while resolving 4 922 more references. If the two land together, there is no cost to weigh at all — the ledger is positive on both axes.9d0e06a, it is +4.41% across the seven measured repos. Against m0061's +3.84% that is the same order, for a recall change rather than a latency one.cost-baseline.json.Everything else on the rebased tree
fmt0 ·clippy0 ·cargo doc --document-private-items0 · indexer--lib382 ·resolver95 ·provenance_invariants3 ·pool_capability_registry17 ·receiver_phantom25 (9d0e06a's own new suite, green under the widening) ·code-index-plugins272+8+2 ·precision_gate7/7, phantoms=0, recall 1.000.corpus_ratchet/corpus_stage/corpus_tier3_ratchetdrift exactly as reported before — binds are unchanged by the rebase, so those three tables stand verbatim. Nothing blessed.BIND INSPECTION — 18 phantoms found in 4,922. Not a clean bill, and the class that produced them was invisible to the census I built
Asked for binds rather than deltas, weighted to rust-analyzer and py-django. Here they are, including the part I got wrong.
The diff on the requested key
Joined on
(path, line, col, kind, occurrence), occurrence assigned within each position/kind group over an id-independent ordering:RETARGETEDis the outcome a whole-row diff hides and I had not reported before: no pre-existing bind was silently re-aimed.What a phantom of this class structurally needs
A tier-1R member bind is
x.m -> T.mwhere a binding row typesxasTand exactly one indexed(container T, member m)survives the origin gate, visibility and the nearer-competitor gate. It can be wrong three ways:Tamong indexed rivals. Needs ≥2 indexed containers namedTeach holding a memberm. Enumerable, and enumerated 100%: 133 of 4,922 (2.7%). Of those, 104 have the target INSIDE the referencing file's own subtree (the gate's guarantee); 29 have it outside — the entire decisive set — and I read all 29 at source. All 29 correct. Zero exposure on cs-dapper, ts-zod, php-guzzle, python-flask, rust-ripgrep — and cs-dapper's zero is EARNED by the gate, it was 2.T::new()convention). Not enumerable. 41-bind stratified random sample read at source, 41 correct, including two provisional bindings (TypeAliasSignature::of,QualifierCtx::default).Tnames something not indexed. The Cargo oracle over all 1,192 rust-analyzer cross-crate binds: 2 undeclared, both read, both correct re-export binds.AND THE CLASS I MISSED — P1b, 18 phantoms
P1 asks whether the
(container, member)PAIR is ambiguous. It is blind when the true owner's member is INHERITED OR IMPLICIT and therefore has no symbol at all. Django is full of this, and an import-statement oracle over py-django's 976 cross-file binds — reading the source, not the index — is what surfaced it:Articlehas 47 declarations in this repo andSELECT … WHERE name='id' AND parent='Article'returns exactly one row —tests/prefetch_related/models.py, the only app that writesidout. Every other app'sArticle.idis Django's implicit primary key: real in the language, absent from the source, therefore absent from the index. Same forUser.email, which lives onAbstractUser. So the pair is unique and P1 sees nothing.Enumerated: 151 of 4,922 binds (3.07%) have an ambiguous CONTAINER NAME with the target outside the ref's subtree — the complete exposed population. Triaged by target family; the cross-app ones read at source:
Article.id -> tests/prefetch_related/User.email -> tests/select_related_onetoone/TestModel.lastmod -> tests/sitemaps_tests/models.pyurls/subdir)Runnable/TestItem/Highlight/Attr/PathSegment/MatchArm/Lint/MovedOutOfRefide::TestItem,hir_expand'sAttr)django/…library targets18 wrong, 133 correct, in the fully enumerated exposed class.
The gate variant that would catch them — BUILT, MEASURED, REFUSED
Widen the nearer-competitor test from "a same-named container holding a same-named member" to "a same-named container of a data-member-holding kind" (
class/struct/trait/module, excludingimpl— the reason the first container-level attempt cost 46 binds).User.email; I did not trace whytests/composite_pk/models/tenant.py's localUserfails to fire the gate, and I am flagging that rather than guessing)to_proto.rsreadingide::Runnable.nav/kind/cfgandide::TestItem.file/text_range, all read at source), 2 on py-django (WSGIRequest.environ,Serializer.stream)17 correct for 12 wrong, with 6 wrong left behind. Refused on the numbers. Reverted;
index.rsmd5-identical to its pre-experiment snapshot.The reason it is the wrong instrument: in every rust-analyzer loss the source is EXPLICIT about which type it means (
ide::Runnable), and a locality test overrules that. The discriminator these 18 need is import evidence, not proximity — which is #196's clause, not a tuning of this gate.The sentence you asked for
Basis, stated so the number can be attacked: the 18 are enumerated, not extrapolated — they are the whole of the two cross-app families inside a 151-bind population that was enumerated exhaustively at the container level and 133-bind population enumerated exhaustively at the pair level. The 4,904 is a residual claim, not 4,904 individual verifications: ~79 distinct sites were read at source (41 stratified random, 29 exhaustive P1-outside, ~9 family representatives), zero wrong outside the 18, and the two enumerable phantom classes are closed. A binomial bound from the random arm alone is only ≤7% at 95%; the enumeration is what carries the claim, not the sample.
The #165 question, answered directly
You asked whether the corpus can express the failure. It can, and it did — twice. The pair-level class produced 5 phantoms (caught pre-merge, gate built); the container-level class produced these 18. Unlike #165's hyphenated-crate blind spot, nothing here was invisible to the corpus — it was invisible to my census, and the source-level import oracle is what saw past it. The one thing the corpus still cannot show is a repo where the receiver type comes from a dependency with an in-project namesake in a language with no manifest (python/PHP/TS); rust has the Cargo oracle, those three have nothing.
Provenance and two dogfood findings
9d0e06awas not onorigin/masterwhen I checked (master was4f866e5); it is now, and9d0e06a..origin/mastertouchesindex.rswith comment-only changes, so my measurements stand against current master without re-running.Our own MCP tools could not do this work.
read_codeon a corpus path returnspath_outside_known_rootsandsearch_symbols("MethodCallee")returnssymbol_not_found— the pinned corpus is not a linked project, so every source read here wassed. That is the same gap #175's comment recorded forresolution_gaps, and it now blocks the bind-inspection workflow this project asks every resolver lane to perform.from .models importreaches everymodels.pyin the tree, and it produced 3 of #69's 5 measured phantoms #196HELD behind #196 — and the sequencing is now measured rather than assumed
Agreed with the hold. The 18 are new, this issue's title says zero-phantom, and 0.37% is a size not an argument. Recording the check that makes the sequencing evidence-backed instead of resting on my suggestion.
#196's clause reaches all 18
All five referencing files carry a relative import whose normalized key is
models, whichtemp.file_keysholds as the basename stem of everymodels.pyin the repository:Four of the 18 do not merely fall silent — they retarget to the correct symbol:
tests/composite_pk/models/tenant.py:20really isemail = models.EmailField(unique=True), socomposite_pk's fouruser.emailbinds move from a foreign app to their own. The other 14 have no local candidate (Article.idis Django's implicit PK,auth_tests'Userisdjango.contrib.auth's) and become honest silence. Both are right.And the collateral on this change is bounded
Of the 4,922 new binds, 2,899 have a target outside the referencing file's own subtree. Of those, the number admitted through a relative-import stem — the exact mechanism #196 would withdraw:
18 of the 32 are the phantoms, so at most 14 correct binds are exposed, and 32 is an upper bound. Zero exposure on rust-analyzer, which carries 3,231 of the 4,922. The two changes are close to orthogonal, which is what makes the sequencing cheap rather than a gamble.
What this branch owes on re-merge
+4,922 / 0 lost / 0 retargetedis measured against9d0e06a. #196 touchestemp.file_keys, which feeds four tiers, so that baseline dies when #196 lands and everything here must be re-measured: the bind diff, the P1 and P1b censuses (the exposed populations change), and the cost table. I would rather say that now than have a stale table quoted at merge time.If #196 is shown not to supply the evidence
Two fallbacks, so the hold has an exit that is not "wait indefinitely":
LineIndexbinds are — in the code, in the bless reason, and with the enumeration that bounds them. Weaker than this issue's title deserves, but it is the project's existing precedent for a measured, named residual.The proximity variant stays refused: 12 of 18 for 17 correct binds, measured over nine repos, reverted.
Branch unchanged at
54339ebon9d0e06a,wip-labelled, unpushed, working tree clean.The bypass is real, the fix is five lines in
build_recv_origin— 18 phantoms become 2, and 38 PRE-EXISTING phantoms go with them, at zero cost to correct bindsYour reframing was right and it was worth more than the hold. Rebased onto
a80eb61; new baseline built and re-measured from scratch.The bypass, confirmed at source
build_recv_origin(#65, untouched by this branch —git diff a80eb61 -- index.rsnames it nowhere) does this for every import row:Its own comment says the enumeration "is exactly … the same enumeration
temp.import_key_relis built from". It reproduces the enumeration and not the exclusion beside it —index.rs:4771, "relative specifiers (leading dot) are excluded — they carry no whole-segment file key and are handled bytemp.import_rel". Sofrom .models import Articlelends the keymodelsto everymodels.pyin the tree, through a route that has been open since #65 and that #57 had already closed on the arm next to it.Three variants built and measured over nine repos
A — skip relative specifiers, no replacement (the literal mirror of
:4771). REFUSED. Kills 16 of 18, butbuild_recv_originnever consultedimport_relat all, so the evidence is removed with nothing put back: 48 pre-existing binds lost and 369 of this change's, and ts-zod collapses from 88 new binds to 3 — TypeScript imports are almost entirely relative.B — consult
temp.import_relfor a relative specifier, keep the stem lookup for absolute ones. Five lines. SHIPPED.C — the subtree test. Not built: B dominates it on the measurement below, and B is a path-RESOLVED relation rather than a proximity heuristic.
B, bind-for-bind against
a80eb61Every repo except py-django is byte-identical to the pre-fix branch —
import_relsupplies exactly what the stem lookup was giving, and the__init__.pyre-export caveat you flagged did not bite here.The 38 "losses" are pre-existing PHANTOMS, read at source
Model.saveis inherited fromdjango.db.models.Model(indexed,django/db/models/base.py). Onlytests/model_forms/models.pyandtests/save_delete_hooks/models.pyoverride it, so the(Article, save)pair looks unique and every app's.save()in the corpus bound to one of those two. All 38 are cross-app; 30 tomodel_forms#save, 7 tosave_delete_hooks#save, 1 to#delete. This is #199's class exactly — an inherited member with no symbol — on METHOD calls, shipped in the resolver today, and it has nothing to do with #69 except that fixing my arm's bypass removes it.The ledger
The correct count is unchanged at 4,904 — B refused exactly the 16 phantoms and nothing else. The tree after this change has fewer phantoms than master does, which is a stronger position than "0.37% is small" and is the argument the zero-phantom title deserves.
The 2 survivors, with their cause named
tests/auth_tests/test_models.py:281,293user.email -> tests/select_related_onetoone/models.py:6. Not a relative import —from django.contrib.auth.models import (…), an absolute dotted module whose interior segment run offersmodels, which matches everymodels.pybasename stem. That is the #134/#168 stem family, not this one, and neither #57's exclusion nor B touches it. Recorded rather than chased.Gates on B
fmt0 ·clippy0 · indexer--lib383 ·resolver95 ·provenance_invariants3 ·pool_capability_registry17 ·receiver_phantom25 ·tier3_origin_gate2 ·precision_gate7/7, phantoms=0. Full workspace + daemon-leg e2e in flight.corpus_cost:rel_mapcosts nothing measurable (cs-dapper 23,459,161 → 23,466,285, +0.03%). Against the re-recorded baseline only ts-zod +6.8% and cs-dapper +8.6% onvm_stepnow exceed — rust-analyzer, py-django, php-guzzle, rust-ripgrep and python-flask are inside the band.Two corrections back to you
cbeb655is not onorigin/master. Master isa80eb61, andgit merge-base --is-ancestor cbeb655 origin/mastersays no. I rebased ontoa80eb61and verified the facts your message relies on directly in that tree (:4771's exclusion,import_relat:4409,build_recv_originreadingfile_keys) — they are all present, so the conclusion holds regardless of where the merge landed.file_keys, and the fix turned out to live here, so the re-measure was mine to do and I have now done it.Branch
e1baef2ona80eb61,wip-labelled, unpushed, tree clean.Full gate run on
e1baef2— greenThe two legs that were in flight when the previous comment posted:
Complete gate state for this branch:
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace --no-fail-fastCOSI_E2E_LEG=daemon cargo test -p code-index-mcpcargo doc --workspace --no-deps --document-private-itemsprecision_gatephantoms=0, recall 1.000corpus_ratchet/corpus_stage/corpus_tier3_ratchet/corpus_costThe four corpus records still need re-recording, and the reasons change with the new numbers.
baseline.json,stage-baseline.jsonandtier3-baseline.jsonnow have to account for py-django going DOWN by 38 as well as up — that is the pre-existing-phantom removal, and a reason that only says "+4,906" would hide it. Draft:build_recv_originbypassed #57 for four years of commits, and nothing could have told a reader #203from .models importreaches everymodels.pyin the tree, and it produced 3 of #69's 5 measured phantoms #196Three of four done; the fourth is blocked on a box quiet enough to measure, and I will not fake it
Rebased onto
1d81180(verified byrev-parse, not a log line —merge-base --is-ancestorconfirms). Branch is now two commits:wipis gone. Re-measured on the new base before touching any record: +4,906 / −38 / 0 RETARGETED, identical to thea80eb61reading — the only resolver-adjacent delta between those bases iswalker.rs(the Windows symlink fix).1 + 2. Four records re-recorded, with the −38 and the 2 survivors visible
baseline.json,stage-baseline.jsonandtier3-baseline.jsonall carry the same sentence, because a reason showing only a delta would hide the half that matters:cost-baseline.jsoncarries its own attribution instead: the per-statementvm_stepsplit, the ruby-sinatra 1.00× control, and the three cheaper shapes built and measured worse.3. Gates on
518411dfmt0 ·clippy0 ·cargo test --workspace— 313 ok, 1 failed (below) · daemon-leg e2e 0 ·corpus_ratchet0 ·corpus_stage0 ·corpus_tier3_ratchet0 ·corpus_cost0 ·precision_gate7/7 phantoms=0 ·cargo doc0.4. Filed as #203
The
build_recv_originfinding, framed as you put it: a comment claimed an enumeration was "exactly the same" astemp.import_key_rel's and reproduced it without the exclusion sitting beside it. Prose the code contradicted, invisible to every gate — the equivalence oracle proves the decision pass matches its own oracle, and both consume the same wrong relation.THE ONE OUTSTANDING GATE —
ruby_package_cost, and it is a guard workingm0063 takes the schema to 63 and the record refuses to compare against conditions it was not taken under. This is the third time this guard has fired on a real schema bump and it is right every time.
The SQLite dimensions — which the record itself calls "the measurement" — have barely moved, and there is no mechanism by which they could: ruby-sinatra gains 0 binds from this change (measured: 0 member rows enter
recv_calls, because Ruby's access refs are@ivarwith self receivers), and m0063 is a re-parse that does not run on a cold index.vm_stepfullscan_stepsortautoindexvm_step(control)So the re-record is forced by schema identity, not drift — the same finding the schema-62 bless recorded.
wall_ratio_pctis the one I will not write yet. Blessed at 121. Readings tonight: 126 (load 17.8), 143 (load 13.4), 146 (load unknown, during a full workspace run). This record's own history says the spread tracks load and not code — eight passes across two schemas read 203…414 — and its instruction says isolated. A survey loop is running that takes a reading only whenps -C cargois empty and the 1-minute load is below 5; the box has been at 13–23 with three or four cargo processes since it started.I am not blessing a wall ratio measured at load 18. That would assert less than it appears to, which is the exact failure the guard's message names. If no quiet window arrives, the honest options are to record it with the conditions stated plainly — which this record has precedent for, twice, in its own
_superseded_reasons— or to wait for a quiet box. That is a call about the record, so it is yours; I would take the wait, because the four SQLite dimensions are already inside band and the only thing gained by rushing is a number nobody can rely on.Everything else is ready. Branch
518411don1d81180, tree clean, unpushed.ruby-package-cost.json: no quiet window in an hour, and it is not one of the four you asked me to re-recordClosing the loop on the one outstanding gate.
The survey found zero acceptable readings. A loop polled every 20 s for an hour, taking a reading only when
ps -C cargowas empty and the 1-minute load was under 5. The box never qualified: load ran 13 → 18 → 21 → 23 → 27 with three to five cargo processes throughout, and it is higher now than when the loop started. Log is empty ofREADINGlines — that is the measurement.It is also outside the scope you gave me. You asked for "the four bless-gated corpus records", and the four I reported red were
baseline.json,stage-baseline.json,tier3-baseline.jsonandcost-baseline.json. All four are re-recorded and green.ruby-package-cost.jsonis a fifth, and it is on this lane's protected list with the standing instruction to bring evidence and a draft reason rather than bless it myself. So I am doing that.Everything the re-record needs except one number
The four SQLite dimensions — which the record's own comment calls "the measurement", deterministic to ~0.02% and "a property of the statements and the data, not of the machine" — are all inside band under schema 63:
vm_stepfullscan_stepsortautoindexvm_step(control)And there is no mechanism by which this change could move them: ruby-sinatra gains 0 binds, because 0 member rows enter
recv_callsthere (Ruby's access refs are@ivarwith self receivers, which this arm excludes), and m0063 is a re-parse that never runs on a cold index. The re-record is forced by schema identity, not drift — the same finding as the schema-62 bless, which said so in as many words.Draft reason, for whoever runs it on a quiet box
wall_ratio_pctis deliberately left blank in that draft. Blessed at 121; tonight's readings were 126 (load 17.8), 143 (load 13.4) and 146 (during a full workspace run). This record's own_superseded_reasonsdocument that the spread tracks load and not code — eight passes across two schemas read 203 … 414, and one pass was rejected specifically because a sibling lane started a cargo build. Writing any of tonight's numbers would put a load artifact into a band whose only job is to see a slow wasm guest.Two honest ways forward, and the choice is yours because it is a protected record:
I would take (1). The gate is currently doing exactly what it was built to do, and the only thing a hurried bless buys is a number that asserts less than it appears to.
Nothing else is outstanding. Branch
518411don1d81180, tree clean, unpushed; the other four records green.The survey loop has now closed, and its verdict is explicit
I reported "zero acceptable readings" while the loop was still inside its window, which was accurate but not yet finished. It has now terminated on its own:
Zero qualifying passes in the full hour — every 20 s, requiring
ps -C cargoempty and 1-minute load under 5. That is the measured basis for refusing the band, rather than an impression formed part-way through.This lane contributes no load to the isolated run. Four
cargo test --workspace/-p code-index-mcpprocesses are live on the box and none has a working directory under/tmp/cosi-lane-recall— they belong to sibling lanes. My worktree is idle and clean at518411d.Handoff is as described: the draft reason for
ruby-package-cost.jsonis in the comment above withwall_ratio_pctleft blank and the conditions line left as<load>/<pct>; the four SQLite dimensions are measured and inside band. Nothing else from me.build_recv_originbypassed #57 for four years of commits, and nothing could have told a reader #203build_recv_originbypassed #57 for four years of commits, and nothing could have told a reader #203user.emailbinds across unrelated Django test apps via TIER1R_RECEIVER — 2 live phantoms on master, and #199's own comment says they were gone #246