Ruby's require_relative creates no file edge — temp.import_rel selects module GLOB '.*', and Ruby carries relativity in the KEYWORD #194

Closed
opened 2026-09-06 14:36:35 +02:00 by buildagent · 1 comment
Member

Found while measuring #189's receiver gate. It is the reason Ruby is not in lang_profile::RECEIVER_LOCALITY_BLIND_PROFILES, so it is a blocker for a real precision win, not a curiosity.

Measured

temp.import_rel — the relation behind tier 1b's tier1b_resolved_relative arm and tier 3's third reachability edge, the only edge that RESOLVES a specifier to a file rather than guessing at it — is built from exactly one selector:

-- crates/indexer/src/index.rs, the import_rel build
SELECT i.file_id, f.path, i.module
  FROM imports i JOIN files f ON f.id = i.file_id
 WHERE i.module GLOB '.*'

and relative_import_paths accepts two dialects: a leading ./ or ../ (JS/TS), or Python's leading dots.

A Ruby require_relative "lib/greeter" stores imports.module = 'lib/greeter'. No leading dot, so it matches neither, so no import_rel row exists and no file edge is created — even though require_relative is by definition relative to the requiring file, more unambiguously than ./x is in JS.

Reproduced on tests/fixtures/ruby/project:

$ sqlite3 idx.db "SELECT f.path, i.module FROM imports i JOIN files f ON f.id=i.file_id WHERE f.path='main.rb'"
main.rb|lib/greeter
main.rb|json

$ sqlite3 idx.db "SELECT r.name, r.qualifier, r.resolved_by FROM refs r ... WHERE f.path='main.rb' AND r.name='greet'"
greet|greeter|30      <- tier3_import_boost

greeter.greet("everyone") in main.rb resolves to Greeter#greet in lib/greeter.rb — correctly — but it gets there through tier 3's file-KEY edge (the import segment greeter matching the basename stem of lib/greeter.rb), not through the relative edge. precision_gate's ruby oracle declares that bind as ground truth.

Why it matters beyond tidiness

The file-key edge is a guess: it matches an import's segments against basename stems, which is how #134's cross-package phantom happened and why that edge carries an origin gate. The relative edge is a fact. In Ruby every one of those facts is currently thrown away and the guess is doing the work.

#189 refuses a captured, non-self method-call receiver at tier 2 and at tier 3's two guessing edges. Including Ruby was built and measured:

  • -239 binds on ruby-sinatra, nearly all core-method phantoms read at source — options.delete(:locals) (Hash#delete) binding Sinatra's DELETE route DSL at lib/sinatra/base.rb:1543, response.body ×27 binding a test helper at test/test_helper.rb:86, to_s / respond_to? / inspect binding Object's;
  • and RED on precision_gate's ruby oracle, because greeter.greet loses its only evidence:
ruby: probe greet/method@lib/greeter.rb container=Some("Greeter") callers RECALL miss
  — expected site "main.rb::" to be resolution==resolved. Got [main.rb::=name_fallback]

With a real import_rel edge for require_relative, greeter.greet would resolve through the edge #189 leaves open, and Ruby could join the registry.

Shape of a fix (not prescriptive)

The selector and relative_import_paths both key on the SPELLING of the specifier. Ruby's relativity is in the keyword, so either:

  1. the plugin records it — emit require_relative "lib/greeter" as a module the resolver already recognises as relative, or carry a flag; or
  2. the resolver treats a Ruby import as a relative-path candidate and lets the path arithmetic decide. require "json" resolves to nothing and creates no edge, so a miss is free; require 'sinatra/base' from lib/sinatra.rb would resolve to lib/sinatra/base.rb, which is what Ruby's load path actually does.

Option 2 needs no extraction change and no re-parse; option 1 is a wire/plugin change.

What must NOT be done

  • Do not measure this by the corpus count. New import_rel rows create new tier-1b tier1b_resolved_relative binds AND new tier-3 edge-3 reachability, in both directions. Report correct/wrong per rule, adjudicated at source, joined on (path, line, col, kind, occurrence) — position alone fans out.
  • Do not fold it into #189. They move the same corpus rows in opposite directions and the attribution would be unrecoverable. #189 leaves Ruby untouched on purpose (ruby-sinatra is byte-identical: 0 lost, 0 gained, 0 retargeted).
  • Do not assume the guess and the fact agree. Where a Ruby file today reaches a candidate through a basename-stem match that a resolved require_relative would NOT reach, this change removes a bind. Those are the interesting rows and they need reading, not counting.

Measured vs inferred

The selector, the missing import_rel row, the resolved_by = 30 attribution of the fixture probe, the -239 and the precision_gate RED are measured on the pinned corpus and the fixture. That option 2 is safe because a miss creates no edge is inferred from the build loop (hit is only set inside by_path.get(&cand)), and the ruby-sinatra blast radius of either option is not measured — that is the work.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

Found while measuring #189's receiver gate. It is the reason Ruby is **not** in `lang_profile::RECEIVER_LOCALITY_BLIND_PROFILES`, so it is a blocker for a real precision win, not a curiosity. ## Measured `temp.import_rel` — the relation behind tier 1b's `tier1b_resolved_relative` arm and tier 3's **third** reachability edge, the only edge that RESOLVES a specifier to a file rather than guessing at it — is built from exactly one selector: ```sql -- crates/indexer/src/index.rs, the import_rel build SELECT i.file_id, f.path, i.module FROM imports i JOIN files f ON f.id = i.file_id WHERE i.module GLOB '.*' ``` and `relative_import_paths` accepts two dialects: a leading `./` or `../` (JS/TS), or Python's leading dots. A Ruby `require_relative "lib/greeter"` stores `imports.module = 'lib/greeter'`. No leading dot, so it matches neither, so **no `import_rel` row exists and no file edge is created** — even though `require_relative` is *by definition* relative to the requiring file, more unambiguously than `./x` is in JS. Reproduced on `tests/fixtures/ruby/project`: ``` $ sqlite3 idx.db "SELECT f.path, i.module FROM imports i JOIN files f ON f.id=i.file_id WHERE f.path='main.rb'" main.rb|lib/greeter main.rb|json $ sqlite3 idx.db "SELECT r.name, r.qualifier, r.resolved_by FROM refs r ... WHERE f.path='main.rb' AND r.name='greet'" greet|greeter|30 <- tier3_import_boost ``` `greeter.greet("everyone")` in `main.rb` resolves to `Greeter#greet` in `lib/greeter.rb` — correctly — but it gets there through tier 3's **file-KEY** edge (the import segment `greeter` matching the basename stem of `lib/greeter.rb`), not through the relative edge. `precision_gate`'s ruby oracle declares that bind as ground truth. ## Why it matters beyond tidiness The file-key edge is a *guess*: it matches an import's segments against basename stems, which is how #134's cross-package phantom happened and why that edge carries an origin gate. The relative edge is a *fact*. In Ruby every one of those facts is currently thrown away and the guess is doing the work. #189 refuses a captured, non-self method-call receiver at tier 2 and at tier 3's two guessing edges. Including Ruby was built and measured: * **-239 binds on ruby-sinatra**, nearly all core-method phantoms read at source — `options.delete(:locals)` (`Hash#delete`) binding Sinatra's **DELETE route DSL** at `lib/sinatra/base.rb:1543`, `response.body` ×27 binding a test helper at `test/test_helper.rb:86`, `to_s` / `respond_to?` / `inspect` binding `Object`'s; * and **RED on `precision_gate`'s ruby oracle**, because `greeter.greet` loses its only evidence: ``` ruby: probe greet/method@lib/greeter.rb container=Some("Greeter") callers RECALL miss — expected site "main.rb::" to be resolution==resolved. Got [main.rb::=name_fallback] ``` With a real `import_rel` edge for `require_relative`, `greeter.greet` would resolve through the edge #189 leaves open, and Ruby could join the registry. ## Shape of a fix (not prescriptive) The selector and `relative_import_paths` both key on the SPELLING of the specifier. Ruby's relativity is in the keyword, so either: 1. the plugin records it — emit `require_relative "lib/greeter"` as a module the resolver already recognises as relative, or carry a flag; or 2. the resolver treats a Ruby import as a relative-path candidate and lets the path arithmetic decide. `require "json"` resolves to nothing and creates no edge, so a miss is free; `require 'sinatra/base'` from `lib/sinatra.rb` would resolve to `lib/sinatra/base.rb`, which is what Ruby's load path actually does. Option 2 needs no extraction change and no re-parse; option 1 is a wire/plugin change. ## What must NOT be done - **Do not measure this by the corpus count.** New `import_rel` rows create new tier-1b `tier1b_resolved_relative` binds AND new tier-3 edge-3 reachability, in both directions. Report correct/wrong per rule, adjudicated at source, joined on `(path, line, col, kind, occurrence)` — position alone fans out. - **Do not fold it into #189.** They move the same corpus rows in opposite directions and the attribution would be unrecoverable. #189 leaves Ruby untouched on purpose (ruby-sinatra is byte-identical: 0 lost, 0 gained, 0 retargeted). - **Do not assume the guess and the fact agree.** Where a Ruby file today reaches a candidate through a basename-stem match that a resolved `require_relative` would NOT reach, this change removes a bind. Those are the interesting rows and they need reading, not counting. ## Measured vs inferred The selector, the missing `import_rel` row, the `resolved_by = 30` attribution of the fixture probe, the -239 and the `precision_gate` RED are **measured** on the pinned corpus and the fixture. That option 2 is safe because a miss creates no edge is **inferred** from the build loop (`hit` is only set inside `by_path.get(&cand)`), and the ruby-sinatra blast radius of either option is **not measured** — that is the work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

REFUSED WITH NUMBERS — measured in 38ccbd2, and I am posting it here because the reasoning has been sitting in a commit message where nobody reading the tracker could find it. That is its own small defect: a resolution that exists only in git history is invisible to the person deciding whether to work on this.

The measurement

Option 2 alone, on ruby-sinatra:

0 gained, 0 lost, 0 retargeted
byte-identical across 18,450 ref rows
...despite 67 new import_rel edges

Sixty-seven new file edges produced zero change in resolution. The edges are real and they bind nothing.

Option 2 combined with #189's ruby row:

+37 binds, 0 correct
27 of the 37 are the exact `response.body` phantoms that #189 REMOVES

So the change does not merely fail to help — it restores what #189 deletes. Shipping it would undo a phantom removal we already paid for, and it would do so silently, because the binds land in the same places.

The part worth keeping beyond this issue

precision_gate reported 7/7 with phantom_count == 0 on that change.

That is the second demonstration in one week that the gate's green is not evidence about corpus binds. It stayed green across a change that admits 37 wrong binds on a pinned corpus repo, exactly as it stayed 7/7 across the change that admitted 34 wrong binds on cs-dapper. The reason is structural and already documented: the gate never indexes a corpus repo, and a phantom only scores against a declared decoy, so an unpredicted wrong bind is invisible by construction.

Keep requiring the gate — it is cheap and has caught real defects — but this issue is now a second citation for never reading its green as a statement about corpus-scale resolution. When a change moves corpus binds, measure the binds: diff against the prior binary joined on (path, line, col, kind, occurrence), and split by resolved_by.

Disposition

Closing as refuted. The diagnosis in the issue body is correct — Ruby does carry relativity in the keyword, and temp.import_rel does select module GLOB '.*'. What is refuted is that fixing it buys anything: the edges materialise, and resolution does not move except to reintroduce known phantoms.

If someone wants to reopen this, the bar is a measurement showing binds that are correct, inspected at the source of each new site — not a count that went up. Three cuts earlier this year raised the resolved count and every extra bind was a phantom; that is why the bar is bind inspection rather than a delta.

**REFUSED WITH NUMBERS** — measured in `38ccbd2`, and I am posting it here because the reasoning has been sitting in a commit message where nobody reading the tracker could find it. That is its own small defect: a resolution that exists only in git history is invisible to the person deciding whether to work on this. ## The measurement **Option 2 alone, on `ruby-sinatra`:** ``` 0 gained, 0 lost, 0 retargeted byte-identical across 18,450 ref rows ...despite 67 new import_rel edges ``` Sixty-seven new file edges produced **zero** change in resolution. The edges are real and they bind nothing. **Option 2 combined with #189's ruby row:** ``` +37 binds, 0 correct 27 of the 37 are the exact `response.body` phantoms that #189 REMOVES ``` So the change does not merely fail to help — **it restores what #189 deletes.** Shipping it would undo a phantom removal we already paid for, and it would do so silently, because the binds land in the same places. ## The part worth keeping beyond this issue `precision_gate` reported **7/7 with `phantom_count == 0`** on that change. That is the second demonstration in one week that the gate's green is **not evidence about corpus binds**. It stayed green across a change that admits 37 wrong binds on a pinned corpus repo, exactly as it stayed 7/7 across the change that admitted 34 wrong binds on `cs-dapper`. The reason is structural and already documented: the gate never indexes a corpus repo, and a phantom only scores against a *declared* decoy, so an unpredicted wrong bind is invisible by construction. Keep requiring the gate — it is cheap and has caught real defects — but this issue is now a second citation for **never reading its green as a statement about corpus-scale resolution**. When a change moves corpus binds, measure the binds: diff against the prior binary joined on `(path, line, col, kind, occurrence)`, and split by `resolved_by`. ## Disposition Closing as refuted. The diagnosis in the issue body is *correct* — Ruby does carry relativity in the keyword, and `temp.import_rel` does select `module GLOB '.*'`. What is refuted is that fixing it buys anything: the edges materialise, and resolution does not move except to reintroduce known phantoms. If someone wants to reopen this, the bar is a measurement showing binds that are **correct**, inspected at the source of each new site — not a count that went up. Three cuts earlier this year raised the resolved count and every extra bind was a phantom; that is why the bar is bind inspection rather than a delta.
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#194
No description provided.