An UNQUALIFIED destructuring use still produces no reference row at all (JS/C#), PHP+Ruby have no written exemption, and there is no cross-language fixture — the qualified case was fixed by 2f16e22 #125

Open
opened 2026-09-04 18:34:12 +02:00 by buildagent · 3 comments
Member

Dogfood finding from the #76 declarative-tier retirement, where it nearly caused a field to be read as dead.

The measurement

ClaimDecl::tier (crates/package/src/manifest.rs) reports:

ref_count: 0
name_fallback_count: 2
find_references(506592) → 2 rows, both resolution: "name_fallback", target_id: null

One of those two is match c.tier in the same file, where c iterates &self.claims: Vec<ClaimDecl>.

But the use in crates/package/src/digest.rs:

let ClaimDecl { language, tier, .. } = claim;

produces no ref row whatsoever. It is in neither counter — not in ref_count, not in name_fallback_count, not returned by find_references.

That site is not incidental: it is where the field is written into the package digest, which is package identity. It is arguably the most important use of the field in the tree.

Why this is the more dangerous half of #118

#118 is a falsely earned zero — a #[tool] method reports ref_count: 0, name_fallback_count: 0 with no unmeasured flag. Bad, but the population was at least considered.

Here the reference is never extracted, so no counter can see it and no disclosure can qualify it. name_fallback_count: 2 happens to be non-zero for unrelated reasons, and that is the only thing that stopped the field being read as dead in this case. Had the two match c.tier sites not existed, ClaimDecl::tier would have presented as a completely unused field — with a fully earned zero — while being load-bearing for package identity.

safe_delete inherits this directly: a field used only through destructuring looks removable.

Scope to establish before fixing

Rust destructuring has several shapes and they should be measured rather than assumed:

  • let Struct { field, .. } = value;
  • if let / while let / match patterns binding fields
  • function-parameter destructuring
  • nested and @-bound patterns
  • shorthand (field) versus renaming (field: local)

And the same question applies to the other six languages — JS/TS object destructuring and Python starred/tuple unpacking are the obvious analogues. Do not fix Rust alone; the standing rule here is one clause resting on a structural fact across all languages, not a per-language patch.

What closing it needs

  1. Emit a reference row for a field named in a destructuring pattern, with the right name_span — the pattern position, since that is where a reader would edit.
  2. A cross-language fixture: every language that has destructuring gets a case, in the shape of the existing seven-language projection gates.
  3. Mutation: delete the pattern arm and the count must go red per language, not once.
  4. Check the effect on precision_gate — new ref rows are new resolution opportunities, and this must not manufacture phantoms. Zero phantoms is non-negotiable.

#118 (a falsely earned zero from search_symbols) — same family, but that one at least measures the population. Found alongside a note that the name_fallback_count: 2 disclosure is what prevented the wrong conclusion here, which is the disclosure machinery working exactly as intended.

Dogfood finding from the #76 declarative-tier retirement, where it nearly caused a field to be read as dead. ## The measurement `ClaimDecl::tier` (`crates/package/src/manifest.rs`) reports: ``` ref_count: 0 name_fallback_count: 2 find_references(506592) → 2 rows, both resolution: "name_fallback", target_id: null ``` One of those two is `match c.tier` **in the same file**, where `c` iterates `&self.claims: Vec<ClaimDecl>`. But the use in `crates/package/src/digest.rs`: ```rust let ClaimDecl { language, tier, .. } = claim; ``` **produces no ref row whatsoever.** It is in neither counter — not in `ref_count`, not in `name_fallback_count`, not returned by `find_references`. That site is not incidental: it is where the field is written into the package digest, which is package **identity**. It is arguably the most important use of the field in the tree. ## Why this is the more dangerous half of #118 #118 is a falsely *earned* zero — a `#[tool]` method reports `ref_count: 0, name_fallback_count: 0` with no `unmeasured` flag. Bad, but the population was at least *considered*. Here the reference is **never extracted**, so no counter can see it and no disclosure can qualify it. `name_fallback_count: 2` happens to be non-zero for unrelated reasons, and that is the only thing that stopped the field being read as dead in this case. Had the two `match c.tier` sites not existed, `ClaimDecl::tier` would have presented as a completely unused field — with a fully *earned* zero — while being load-bearing for package identity. `safe_delete` inherits this directly: a field used only through destructuring looks removable. ## Scope to establish before fixing Rust destructuring has several shapes and they should be measured rather than assumed: - `let Struct { field, .. } = value;` - `if let` / `while let` / `match` patterns binding fields - function-parameter destructuring - nested and `@`-bound patterns - shorthand (`field`) versus renaming (`field: local`) And the same question applies to the other six languages — JS/TS object destructuring and Python starred/tuple unpacking are the obvious analogues. **Do not fix Rust alone**; the standing rule here is one clause resting on a structural fact across all languages, not a per-language patch. ## What closing it needs 1. Emit a reference row for a field named in a destructuring pattern, with the right `name_span` — the pattern position, since that is where a reader would edit. 2. A **cross-language** fixture: every language that has destructuring gets a case, in the shape of the existing seven-language projection gates. 3. Mutation: delete the pattern arm and the count must go red **per language**, not once. 4. Check the effect on `precision_gate` — new ref rows are new resolution opportunities, and this must not manufacture phantoms. Zero phantoms is non-negotiable. ## Related #118 (a falsely earned zero from `search_symbols`) — same family, but that one at least measures the population. Found alongside a note that the `name_fallback_count: 2` disclosure is what prevented the wrong conclusion here, which is the disclosure machinery working exactly as intended.
Author
Member

Triage 2026-09-06: LEFT OPEN — PARTIAL. Four languages emit, but only for qualified patterns, so this issue's headline is still true for the JS majority case.

Landed in 2f16e22. The work is good and the trade is defensible; the issue should not close on it as written.

What landed

  • crates/plugins/src/rust.rs:317 — "struct_pattern" → emit_pattern_field_reads (:1234)
  • crates/plugins/src/typescript.rs:483 — "object_pattern" → emit_pattern_property_reads (:1618)
  • crates/plugins/src/csharp.rs:736 — "property_pattern_clause" (:1643)
  • crates/plugins/src/python.rs:261 — class_pattern keyword → emit_pattern_attribute_read (:1016)

The motivating case is genuinely covered: let ClaimDecl { language, tier, .. } = claim; names its type, so Rust emits.

Why PARTIAL — three reasons

1. The headline is still true for unqualified patterns. crates/plugins/src/typescript.rs:1663 and crates/plugins/src/csharp.rs:1663 are literally if qualifier.is_none() { return; } — no ref row of any kind. The comment at typescript.rs:1656-1662 says so: "untyped JavaScript destructuring is now invisible again."

That is a defensible engineering decision, and the reasoning is excellent — the first version produced six phantoms, and counting would have missed them. function BlogLayout({ children }: { children: ReactNode }) in a docs .tsx bound children to a get children() in a zod test file in another package, four times; express's const { address, port } = server.address() bound var address = url inside res.redirect. Every phantom was unqualified; all 11 qualified patterns bound correctly. One structural clause across four languages, and two right answers given up to stop six wrong ones shipping as resolved. That is the right call.

But "a destructuring use produces no reference row at all" remains shipped behaviour for untyped JS, which is most JS. Closing on this issue's own text would overstate what was achieved.

2. No cross-language fixture. Grading is four plugin-local unit tests (rust.rs:2222, typescript.rs:2240, csharp.rs:2136, python.rs:1252), not a fixture "in the shape of the existing seven-language projection gates", which is criterion 2.

3. The PHP/Ruby exemption is not recorded in the tree. It exists only in 2f16e22's commit message — "PHP and Ruby emit nothing because their destructuring is by array/hash KEY, not member name — a language fact, not a gap." Searching #125 across *.rs/*.md/*.json/*.toml: zero hits in crates/plugins/src/php.rs and crates/plugins/src/ruby.rs, and zero hits for that reasoning anywhere.

That is the shape this repo keeps filing against itself: a language exemption that lives only in a commit message. The next reader of the producer-coverage matrix sees two empty cells and no reason. Put the sentence at the two sites.

What closing needs

  1. The PHP/Ruby language-fact exemption written at the sites, so the empty cells are explained where they are read.
  2. Either a cross-language fixture in the seven-language projection shape, or an amendment saying the four plugin-local tests are the accepted grading.
  3. A decision on unqualified patterns: accept the recall loss explicitly in this issue's text (with the six-phantom measurement as the reason), or leave it as the open half.

Item 1 is small and worth doing regardless of 2 and 3.

🤖 Triage lane, 2026-09-06, master 45cf6e4

## Triage 2026-09-06: LEFT OPEN — **PARTIAL.** Four languages emit, but only for *qualified* patterns, so this issue's headline is still true for the JS majority case. Landed in `2f16e22`. The work is good and the trade is defensible; the issue should not close on it as written. ### What landed - `crates/plugins/src/rust.rs:317` — `"struct_pattern"` → `emit_pattern_field_reads` (`:1234`) - `crates/plugins/src/typescript.rs:483` — `"object_pattern"` → `emit_pattern_property_reads` (`:1618`) - `crates/plugins/src/csharp.rs:736` — `"property_pattern_clause"` (`:1643`) - `crates/plugins/src/python.rs:261` — `class_pattern` keyword → `emit_pattern_attribute_read` (`:1016`) The motivating case is genuinely covered: `let ClaimDecl { language, tier, .. } = claim;` names its type, so Rust emits. ### Why PARTIAL — three reasons **1. The headline is still true for unqualified patterns.** `crates/plugins/src/typescript.rs:1663` and `crates/plugins/src/csharp.rs:1663` are literally `if qualifier.is_none() { return; }` — **no ref row of any kind**. The comment at `typescript.rs:1656-1662` says so: *"untyped JavaScript destructuring is now invisible again."* That is a **defensible engineering decision**, and the reasoning is excellent — the first version produced **six phantoms**, and counting would have missed them. `function BlogLayout({ children }: { children: ReactNode })` in a docs `.tsx` bound `children` to a `get children()` in a zod **test file in another package**, four times; express's `const { address, port } = server.address()` bound `var address = url` inside `res.redirect`. Every phantom was unqualified; all 11 qualified patterns bound correctly. One structural clause across four languages, and two right answers given up to stop six wrong ones shipping as `resolved`. That is the right call. But "a destructuring use produces no reference row at all" remains **shipped behaviour** for untyped JS, which is most JS. Closing on this issue's own text would overstate what was achieved. **2. No cross-language fixture.** Grading is four plugin-local unit tests (`rust.rs:2222`, `typescript.rs:2240`, `csharp.rs:2136`, `python.rs:1252`), not a fixture *"in the shape of the existing seven-language projection gates"*, which is criterion 2. **3. The PHP/Ruby exemption is not recorded in the tree.** It exists only in `2f16e22`'s commit message — *"PHP and Ruby emit nothing because their destructuring is by array/hash KEY, not member name — a language fact, not a gap."* Searching `#125` across `*.rs`/`*.md`/`*.json`/`*.toml`: **zero hits** in `crates/plugins/src/php.rs` and `crates/plugins/src/ruby.rs`, and zero hits for that reasoning anywhere. That is the shape this repo keeps filing against itself: **a language exemption that lives only in a commit message**. The next reader of the producer-coverage matrix sees two empty cells and no reason. Put the sentence at the two sites. ### What closing needs 1. The PHP/Ruby language-fact exemption **written at the sites**, so the empty cells are explained where they are read. 2. Either a cross-language fixture in the seven-language projection shape, or an amendment saying the four plugin-local tests are the accepted grading. 3. A decision on unqualified patterns: accept the recall loss explicitly in this issue's text (with the six-phantom measurement as the reason), or leave it as the open half. Item 1 is small and worth doing regardless of 2 and 3. 🤖 Triage lane, 2026-09-06, master `45cf6e4`
Author
Member

STAYING OPEN, NARROWED — the qualified case is fixed; the headline is still shipped behaviour for untyped JS

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

Fixed by 2f16e22 "feat: destructuring member reads and a tier-3 origin gate, both bind-inspected"

language emitter
rust crates/plugins/src/rust.rs:317 emit_pattern_field_reads
typescript crates/plugins/src/typescript.rs:483 emit_pattern_property_reads
csharp crates/plugins/src/csharp.rs:736
python crates/plugins/src/python.rs:261

Four unit tests pass (a_struct_pattern_reads_every_member_it_names, an_object_pattern_reads_every_property_its_annotation_anchors, a_property_pattern_reads_every_member_it_names, a_class_pattern_reads_every_attribute_it_names) — EXIT=0. precision_gate 7/7 with phantoms=0 and recall 1.000 on all seven languages — criterion 4 met. An end-to-end probe with a 552e3a2 binary on the issue's exact motivating shape emits and resolves the row.

Three residuals, all still true byte-for-byte on master

  1. The headline remains literally true for the JS majority case. crates/plugins/src/typescript.rs:1663 and crates/plugins/src/csharp.rs:1656 are if qualifier.is_none() { return; }, and the TS unit test asserts the withholding — const { theta } = obj; and ({ zeta: h } = obj) must emit no read/write at all. The trade is well-measured (six corpus phantoms against two correct binds, typescript.rs:1640-1662) and is the right call, but an unqualified destructuring use still produces no reference row.
  2. No cross-language fixture (criterion 2). Grading is four plugin-local unit tests. The only other #125 mention outside the four plugins is a comment in crates/mcp-server/tests/overview_payload_budget_e2e.rs:1466.
  3. The PHP/Ruby exemption is not in the tree. search_text("#125") returns 6 files; zero hits in php.rs or ruby.rs, and emit_pattern* exists in neither. The reason — "their destructuring is by array/hash KEY, not member name — a language fact, not a gap" — lives only in 2f16e22's commit message, which is exactly the "a decision nobody wrote down is indistinguishable from a bug" state this project files issues about.

Corrected scope

Retitle as above; record in the body that qualified patterns in rust/TS/C#/python were closed by 2f16e22, with the six-phantom measurement as the accepted reason for the unqualified recall loss. Remaining work: (a) the PHP/Ruby exemption written at the two sites, (b) the cross-language fixture decision — either build it or record why four plugin tests are the right grading.

NEW DEFECT, and it is the more urgent finding

An extractor change that adds ref rows shipped with no re-heal migration, so no existing index sees this fix — including this repo's own.

On the live dogfood index (installed daemon 0.26.1 (8d90075), which contains 2f16e22), the motivating symbol still reports the defect verbatim:

search_symbols("tier", crates/package/src/manifest.rs)
  -> ClaimDecl::tier   ref_count: 0   name_fallback_count: 2
find_references(...)  -> 2 rows, both name_fallback (manifest.rs:921, corpus/mod.rs:100)
  -> crates/package/src/digest.rs:376  `let ClaimDecl { language, tier, .. } = claim;`  is ABSENT

A fresh index of the same shape with the 552e3a2 binary emits and resolves that row, so the difference is staleness, not code: git show --stat 2f16e22 adds no migration, and the series still ends at m0061_file_refs_rollup.rs. Indexing skips files by stat/hash, so a plugin upgrade never re-extracts an unchanged file — every pre-existing index keeps its pre-fix rows until each affected file happens to be edited. This repo has fixed exactly this before with a no-invalidation re-heal migration (the m0017/m0019 pattern). Concretely: safe_delete and ref_count on ClaimDecl::tier still read as dead on the daemon a user is running today, which is the failure mode this issue was filed about.

## STAYING OPEN, NARROWED — the qualified case is fixed; the headline is still shipped behaviour for untyped JS Close-out lane, master `552e3a2`. **Title corrected.** ### Fixed by `2f16e22` *"feat: destructuring member reads and a tier-3 origin gate, both bind-inspected"* | language | emitter | |---|---| | rust | `crates/plugins/src/rust.rs:317` `emit_pattern_field_reads` | | typescript | `crates/plugins/src/typescript.rs:483` `emit_pattern_property_reads` | | csharp | `crates/plugins/src/csharp.rs:736` | | python | `crates/plugins/src/python.rs:261` | Four unit tests pass (`a_struct_pattern_reads_every_member_it_names`, `an_object_pattern_reads_every_property_its_annotation_anchors`, `a_property_pattern_reads_every_member_it_names`, `a_class_pattern_reads_every_attribute_it_names`) — **EXIT=0**. `precision_gate` 7/7 with `phantoms=0` and recall 1.000 on all seven languages — criterion 4 met. An end-to-end probe with a `552e3a2` binary on the issue's exact motivating shape emits **and resolves** the row. ### Three residuals, all still true byte-for-byte on master 1. **The headline remains literally true for the JS majority case.** `crates/plugins/src/typescript.rs:1663` and `crates/plugins/src/csharp.rs:1656` are `if qualifier.is_none() { return; }`, and the TS unit test *asserts the withholding* — `const { theta } = obj;` and `({ zeta: h } = obj)` must emit no `read`/`write` at all. The trade is well-measured (six corpus phantoms against two correct binds, `typescript.rs:1640-1662`) and is the right call, but an unqualified destructuring use still produces no reference row. 2. **No cross-language fixture** (criterion 2). Grading is four plugin-local unit tests. The only other `#125` mention outside the four plugins is a *comment* in `crates/mcp-server/tests/overview_payload_budget_e2e.rs:1466`. 3. **The PHP/Ruby exemption is not in the tree.** `search_text("#125")` returns 6 files; zero hits in `php.rs` or `ruby.rs`, and `emit_pattern*` exists in neither. The reason — *"their destructuring is by array/hash KEY, not member name — a language fact, not a gap"* — lives only in `2f16e22`'s commit message, which is exactly the "a decision nobody wrote down is indistinguishable from a bug" state this project files issues about. ### Corrected scope Retitle as above; record in the body that qualified patterns in rust/TS/C#/python were closed by `2f16e22`, with the six-phantom measurement as the accepted reason for the unqualified recall loss. Remaining work: **(a)** the PHP/Ruby exemption written at the two sites, **(b)** the cross-language fixture decision — either build it or record why four plugin tests are the right grading. ### NEW DEFECT, and it is the more urgent finding **An extractor change that adds ref rows shipped with no re-heal migration, so no existing index sees this fix — including this repo's own.** On the live dogfood index (installed daemon `0.26.1 (8d90075)`, which *contains* `2f16e22`), the motivating symbol still reports the defect verbatim: ``` search_symbols("tier", crates/package/src/manifest.rs) -> ClaimDecl::tier ref_count: 0 name_fallback_count: 2 find_references(...) -> 2 rows, both name_fallback (manifest.rs:921, corpus/mod.rs:100) -> crates/package/src/digest.rs:376 `let ClaimDecl { language, tier, .. } = claim;` is ABSENT ``` A fresh index of the same shape with the `552e3a2` binary emits and resolves that row, so the difference is **staleness, not code**: `git show --stat 2f16e22` adds no migration, and the series still ends at `m0061_file_refs_rollup.rs`. Indexing skips files by stat/hash, so a plugin upgrade never re-extracts an unchanged file — every pre-existing index keeps its pre-fix rows until each affected file happens to be edited. This repo has fixed exactly this before with a no-invalidation re-heal migration (the m0017/m0019 pattern). Concretely: `safe_delete` and `ref_count` on `ClaimDecl::tier` still read as dead on the daemon a user is running **today**, which is the failure mode this issue was filed about.
buildagent changed title from A struct-destructuring use produces no reference row at all, so it is in neither ref_count nor name_fallback_count to An UNQUALIFIED destructuring use still produces no reference row at all (JS/C#), PHP+Ruby have no written exemption, and there is no cross-language fixture — the qualified case was fixed by 2f16e22 2026-09-06 12:49:51 +02:00
Author
Member

Both close-out items done, plus the NEW DEFECT the last comment flagged — but the headline stays true for untyped JS, so this narrows again rather than closing

Resolver-recall lane, worktree off master fc329a8. Taking the three residuals in the order the previous comment listed them.

(3) The PHP/Ruby exemption is now IN THE TREE, at both sites

crates/plugins/src/php.rs and crates/plugins/src/ruby.rs, on rebind_pattern — the function a reader lands on when asking why that cell is empty. Both carry the same argument in the same words: PHP destructuring names ARRAY KEYS and offsets ([$a, $b] = $pair names 0 and 1; ['id' => $i] = $row names the string 'id'), Ruby's names POSITIONS and HASH KEYS. Neither is a declared member of any type, so no honest read row exists — and a row keyed on id would be a phantom generator against every $obj->id property and every attr_reader :id in the tree. Object member access in those languages is $obj->prop / recv.attr / @ivar, which emit_property_access, emit_member_call and emit_ivar_access already emit.

(2) The cross-language fixture exists, and it keeps the two empty cells APART

crates/plugins/src/lib.rs, destructuring_contract_tests, in the shape of the access_ref_contract_tests seven-language gate next to it. One row per language, (extension, source, expectation) with the pattern always on line 2.

The reason it is not just a table of "emits / does not emit": #125 shipped with two DIFFERENT empty cells and recorded neither, and they are not the same finding. Expect has three arms so they cannot be collapsed:

  • Members(&[(name, qualifier)]) — rust, ts, csharp, python. Exhaustive: an EXTRA row fails too, because an unanchored extra row is a phantom candidate, not a bonus.
  • WithheldUnqualified(reason) — javascript. The language HAS member destructuring; the plugin withholds because nothing names the type. A measured TRADE (six corpus phantoms against two correct binds), and the OPEN half of this issue.
  • NoMembersByLanguage(reason) — php, ruby. Permanent language fact.

The failure message prints the recorded reason and says move the row, do not delete the arm.

Both no-row arms are graded, never asserted away: the fixture must produce binding rows on the pattern line first, which proves the pattern parsed and the walk reached it. Without that, "projects no member read" is satisfied by a fixture holding no pattern — the vacuous (b) cell in an (a) cell's clothes, which is exactly what producer_coverage_matrix exists to prevent.

A second test derives the language list from all_plugins() so a plugin claiming an extension no row covers fails. A hand-copied list is how this issue kept two silent cells in the first place.

Five mutations, all RUN, all RED:

mutation result
delete "struct_pattern" from rust.rs's walk RED — .rs Members arm, Got: []
php.rs's rebind_pattern pushes an access_ref per variable RED — .php projected member reads ... [("a", None), ("b", None)]
drop if qualifier.is_none() { return; } from typescript.rs RED — .js projected ... [("alpha", None), ("beta", None)]
gut the php fixture so no pattern is walked RED — produced 0 binding rows ... is VACUOUS. Fix the fixture, not the assertion
delete the .rb row from the table RED — a plugin claiming ["rb", "rake", "gemspec"] is covered by no destructuring row

The last two grade the guards rather than the emitters, which is the part that would otherwise rot.

THE NEW DEFECT — fixed. m0063, a RE-PARSE

The previous comment is right and it is the more urgent finding: 2f16e22 is an EXTRACTION change that shipped with no migration, so no existing index has those rows, including this repository's own.

crates/indexer/src/migrations/m0063_pattern_reads_and_member_receivers.rs follows the m0040 pattern, not the m0017/m0019/m0062 re-heal one:

UPDATE files SET mtime_ns = -1, hash = zeroblob(16) WHERE kind = 'code'

mtime_ns = -1 fails the stat tier (no real mtime is negative), the zeroed hash fails the content tier, so neither reconcile tier can skip a file. Scoped to kind = 'code' — text rows carry no extraction. #69's resolution change rides on it for free: a re-parse rewrites every ref and resolution runs over the result, which is strictly stronger than the heal Spec 04 requires.

m0063_forces_a_reparse_because_pattern_reads_come_from_the_parser grades it, and it is SCOPED TO m0063 on purpose. m0040's and m0041's versions of this test run the chain to the END, so any later re-parse satisfies them and they no longer grade the migration they are named after — that is a live vacuity in two existing tests, worth its own look. This one runs to 62, stamps every file FRESH, asserts the fixture really is fresh, and only then applies 63.

MUTATION (RUN): replace m0063's body with m0062's re-heal (UPDATE refs SET target_id = NULL + resolve_ref_targets_in_tx). RED: left: 0, right: 2 — a re-heal touches no file, and every pre-2f16e22 index keeps its missing pattern rows forever.

(1) The headline is STILL TRUE for untyped JS

Unchanged, and deliberately so. typescript.rs:1663 and csharp.rs:1656 are still if qualifier.is_none() { return; }, and the new .js row now asserts the withholding with the six-phantom measurement as its recorded reason. That is the right call and I am not touching it; buying it back needs receiver-type evidence, not a wider pattern arm.

Worth noting from the #69 work in this same lane: receiver-type evidence is exactly what tier 1R now supplies for member access, and it produced +4 922 binds with 0 lost — but 0 of them on js-express, because untyped JS has no binding rows to key on. So the mechanism that would close this half is already in the tree and demonstrably does not reach JavaScript. That is a fact about the language, not about the tier.

Where this leaves the issue

Residuals (2) and (3) are closed and graded; the new re-heal defect is fixed and graded. Residual (1) — an unqualified destructuring use produces no reference row — is unchanged and is now the whole of the issue. Either accept it in the body with the six-phantom measurement as the reason and close, or keep it open as the JS recall item; it should not close silently on the two items above.

## Both close-out items done, plus the NEW DEFECT the last comment flagged — but the headline stays true for untyped JS, so this narrows again rather than closing Resolver-recall lane, worktree off master `fc329a8`. Taking the three residuals in the order the previous comment listed them. ### (3) The PHP/Ruby exemption is now IN THE TREE, at both sites `crates/plugins/src/php.rs` and `crates/plugins/src/ruby.rs`, on `rebind_pattern` — the function a reader lands on when asking why that cell is empty. Both carry the same argument in the same words: PHP destructuring names ARRAY KEYS and offsets (`[$a, $b] = $pair` names 0 and 1; `['id' => $i] = $row` names the string `'id'`), Ruby's names POSITIONS and HASH KEYS. Neither is a declared member of any type, so no honest `read` row exists — and a row keyed on `id` would be a phantom generator against every `$obj->id` property and every `attr_reader :id` in the tree. Object member access in those languages is `$obj->prop` / `recv.attr` / `@ivar`, which `emit_property_access`, `emit_member_call` and `emit_ivar_access` already emit. ### (2) The cross-language fixture exists, and it keeps the two empty cells APART `crates/plugins/src/lib.rs`, `destructuring_contract_tests`, in the shape of the `access_ref_contract_tests` seven-language gate next to it. One row per language, `(extension, source, expectation)` with the pattern always on line 2. The reason it is not just a table of "emits / does not emit": **#125 shipped with two DIFFERENT empty cells and recorded neither**, and they are not the same finding. `Expect` has three arms so they cannot be collapsed: * `Members(&[(name, qualifier)])` — rust, ts, csharp, python. Exhaustive: an EXTRA row fails too, because an unanchored extra row is a phantom candidate, not a bonus. * `WithheldUnqualified(reason)` — **javascript**. The language HAS member destructuring; the plugin withholds because nothing names the type. A measured TRADE (six corpus phantoms against two correct binds), and the OPEN half of this issue. * `NoMembersByLanguage(reason)` — php, ruby. Permanent language fact. The failure message prints the recorded reason and says *move the row, do not delete the arm*. Both no-row arms are graded, never asserted away: the fixture must produce `binding` rows on the pattern line first, which proves the pattern parsed and the walk reached it. Without that, "projects no member read" is satisfied by a fixture holding no pattern — the vacuous (b) cell in an (a) cell's clothes, which is exactly what `producer_coverage_matrix` exists to prevent. A second test derives the language list from `all_plugins()` so a plugin claiming an extension no row covers fails. A hand-copied list is how this issue kept two silent cells in the first place. **Five mutations, all RUN, all RED:** | mutation | result | |---|---| | delete `"struct_pattern"` from `rust.rs`'s `walk` | RED — `.rs` Members arm, `Got: []` | | `php.rs`'s `rebind_pattern` pushes an `access_ref` per variable | RED — `.php projected member reads ... [("a", None), ("b", None)]` | | drop `if qualifier.is_none() { return; }` from `typescript.rs` | RED — `.js projected ... [("alpha", None), ("beta", None)]` | | gut the php fixture so no pattern is walked | RED — `produced 0 binding rows ... is VACUOUS. Fix the fixture, not the assertion` | | delete the `.rb` row from the table | RED — `a plugin claiming ["rb", "rake", "gemspec"] is covered by no destructuring row` | The last two grade the guards rather than the emitters, which is the part that would otherwise rot. ### THE NEW DEFECT — fixed. `m0063`, a RE-PARSE The previous comment is right and it is the more urgent finding: `2f16e22` is an EXTRACTION change that shipped with no migration, so no existing index has those rows, including this repository's own. `crates/indexer/src/migrations/m0063_pattern_reads_and_member_receivers.rs` follows the **m0040** pattern, not the m0017/m0019/m0062 re-heal one: ```rust UPDATE files SET mtime_ns = -1, hash = zeroblob(16) WHERE kind = 'code' ``` `mtime_ns = -1` fails the stat tier (no real mtime is negative), the zeroed hash fails the content tier, so neither reconcile tier can skip a file. Scoped to `kind = 'code'` — text rows carry no extraction. #69's resolution change rides on it for free: a re-parse rewrites every ref and resolution runs over the result, which is strictly stronger than the heal Spec 04 requires. `m0063_forces_a_reparse_because_pattern_reads_come_from_the_parser` grades it, and it is **SCOPED TO m0063** on purpose. `m0040`'s and `m0041`'s versions of this test run the chain to the END, so any later re-parse satisfies them and they no longer grade the migration they are named after — that is a live vacuity in two existing tests, worth its own look. This one runs to 62, stamps every file FRESH, asserts the fixture really is fresh, and only then applies 63. **MUTATION (RUN):** replace `m0063`'s body with m0062's re-heal (`UPDATE refs SET target_id = NULL` + `resolve_ref_targets_in_tx`). **RED**: `left: 0, right: 2` — a re-heal touches no file, and every pre-`2f16e22` index keeps its missing pattern rows forever. ### (1) The headline is STILL TRUE for untyped JS Unchanged, and deliberately so. `typescript.rs:1663` and `csharp.rs:1656` are still `if qualifier.is_none() { return; }`, and the new `.js` row now *asserts the withholding* with the six-phantom measurement as its recorded reason. That is the right call and I am not touching it; buying it back needs receiver-type evidence, not a wider pattern arm. Worth noting from the #69 work in this same lane: receiver-type evidence is exactly what tier 1R now supplies for member access, and it produced +4 922 binds with 0 lost — but **0 of them on `js-express`**, because untyped JS has no binding rows to key on. So the mechanism that would close this half is already in the tree and demonstrably does not reach JavaScript. That is a fact about the language, not about the tier. ### Where this leaves the issue Residuals (2) and (3) are closed and graded; the new re-heal defect is fixed and graded. Residual (1) — an unqualified destructuring use produces no reference row — is unchanged and is now the whole of the issue. Either accept it in the body with the six-phantom measurement as the reason and close, or keep it open as the JS recall item; it should not close silently on the two items above.
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#125
No description provided.