release v0.31.2: a bare Ruby call is a call (#294), check_rename speaks Ruby (#295), minified code ranks last #299

Merged
buildagent merged 5 commits from fix-ruby-recall-294 into master 2026-09-23 21:21:18 +02:00
Member

Fixes from a live Ruby repository's review of code-index. Every item was reproduced here before any code changed.

#294 — a bare zero-argument Ruby call produced no reference at all

forbidden_response — no parentheses, arguments or receiver — is how Ruby normally calls an attribute reader, predicate or memoized helper, and it parses to the same identifier node as a local read. No row was emitted, so a method called only that way showed ref_count: 0, name_fallback_count: 0, name_fallback_shape_excluded: 0 and no name_fallback_unmeasured: three disclosures agreeing that nothing referenced it.

The fix applies Ruby's own lexical rule: a name is a local from the point an assignment, parameter, block parameter, for variable, rescue => e, pattern binding or named regex capture binds it. Otherwise it is a call. Identifiers hung off an ERROR node are recovery debris (the broken.rb.rbx decoy caught that).

What the row may bind to took five measured encodings. Each was diffed bind-for-bind on ruby-sinatra and every new bind outside the call site's own class read at source. precision_gate was 7/7 for all five, because none of these sites is a declared decoy:

encoding new binds wrong
foo()'s shape 767 59+ (tier 2 same-file: route-block params → PatternLookAlike#params)
self.foo everywhere 538 via tier 3
qualified by container path 356 85 (tier 1Q reads a qualifier as a module path)
self.foo in method bodies 114 12 (inherited gem methods taken as the unique project one)
proven, relative-self qualified 105 3, via the documented I023/#194 tier-1b policy

Shipped: a bare call is UNCAPTURED unless its own class defines the name in the same file; a proven call binds only through TIER1Q_REL_SAMEFILE. No existing bind changed. The de.h-dv.ruby wasm package got the same logic, and ruby_package_parity holds builtin and package equal row for row (2966 resolved each). Package 0.6.0 → 0.7.0.

Corpus: ruby-sinatra refs +2091, resolved +105; rust-ripgrep refs +8 (a Homebrew formula). The +11.2% cost was attributed per statement before blessing: it tracks the rows, nothing disproportionate. Full account: _prdoc/records/I070-ruby-bare-calls.md.

#295 — check_rename refused foo? and its text scan went dark

The index stores issued_for_us? with its ?, then refused to rename to such a name, and — the worse half — switched off the text channel that catches unresolved call sites. That left it off exactly where Ruby resolution is weakest. Now one table, lang_profile::IDENTIFIER_SUFFIXES, is read through the profile gate, so the packaged Ruby id gets it too (the #112 trap). A Rust symbol still refuses valid?, with a message naming its language.

Minified code ranked above the project's own symbol

search_symbols("error") put minified public/assets/*.js copies above the project's Tools.error. Rather than a path list, this uses the structural fact already stored: no human-written declaration in the seven corpus repos or this one sits past column 105, and minified code declares in the thousands. Declarations past column 200 rank below the rest. It demotes and never hides.

Precision is not recall

"1.00 precision where ripgrep scored 0.52" is true and read as general superiority. All five copies now say that a reference the index did not record or resolve is simply absent from callers, impact and dead-code answers.

Deferred, with measurements

  • #296 — Auth.resolve_user: the obvious encoding measured ~60 phantoms via tier 40's package anchoring.
  • #297 — autoload: the one-line fix measured 17 phantoms through segment-run require matching. Reverted.
  • #298 — extensionless shebang scripts: claims are path-based by design, so this is an architectural change.

Gates

fmt, clippy, rustdoc; 375 workspace suites / 4017 tests; 840 e2e; guest gates under --locked; corpus ratchet; precision 7/7; ruby_package_parity; the version-bump suites. Mutations run on final code for every new test.

Closes #294, #295.

🤖 Generated with Claude Code

https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu

Fixes from a live Ruby repository's review of code-index. Every item was reproduced here before any code changed. ## #294 — a bare zero-argument Ruby call produced no reference at all `forbidden_response` — no parentheses, arguments or receiver — is how Ruby normally calls an attribute reader, predicate or memoized helper, and it parses to the same `identifier` node as a local read. No row was emitted, so a method called only that way showed `ref_count: 0`, `name_fallback_count: 0`, `name_fallback_shape_excluded: 0` and no `name_fallback_unmeasured`: three disclosures agreeing that nothing referenced it. The fix applies Ruby's own lexical rule: a name is a local from the point an assignment, parameter, block parameter, `for` variable, `rescue => e`, pattern binding or named regex capture binds it. Otherwise it is a call. Identifiers hung off an `ERROR` node are recovery debris (the `broken.rb.rbx` decoy caught that). **What the row may bind to took five measured encodings.** Each was diffed bind-for-bind on ruby-sinatra and every new bind outside the call site's own class read at source. `precision_gate` was 7/7 for all five, because none of these sites is a declared decoy: | encoding | new binds | wrong | |---|---:|---| | `foo()`'s shape | 767 | 59+ (tier 2 same-file: route-block `params` → `PatternLookAlike#params`) | | `self.foo` everywhere | 538 | via tier 3 | | qualified by container path | 356 | 85 (tier 1Q reads a qualifier as a *module path*) | | `self.foo` in method bodies | 114 | 12 (inherited *gem* methods taken as the unique project one) | | **proven, relative-`self` qualified** | **105** | **3**, via the documented I023/#194 tier-1b policy | Shipped: a bare call is `UNCAPTURED` unless its own class defines the name in the same file; a proven call binds only through `TIER1Q_REL_SAMEFILE`. No existing bind changed. The `de.h-dv.ruby` wasm package got the same logic, and `ruby_package_parity` holds builtin and package equal row for row (2966 resolved each). Package **0.6.0 → 0.7.0**. Corpus: ruby-sinatra refs +2091, resolved +105; rust-ripgrep refs +8 (a Homebrew formula). The +11.2% cost was attributed per statement before blessing: it tracks the rows, nothing disproportionate. Full account: `_prdoc/records/I070-ruby-bare-calls.md`. ## #295 — `check_rename` refused `foo?` and its text scan went dark The index stores `issued_for_us?` with its `?`, then refused to rename to such a name, and — the worse half — switched off the text channel that catches unresolved call sites. That left it off exactly where Ruby resolution is weakest. Now one table, `lang_profile::IDENTIFIER_SUFFIXES`, is read through the profile gate, so the packaged Ruby id gets it too (the #112 trap). A Rust symbol still refuses `valid?`, with a message naming its language. ## Minified code ranked above the project's own symbol `search_symbols("error")` put minified `public/assets/*.js` copies above the project's `Tools.error`. Rather than a path list, this uses the structural fact already stored: no human-written declaration in the seven corpus repos or this one sits past column 105, and minified code declares in the thousands. Declarations past column 200 rank below the rest. It demotes and never hides. ## Precision is not recall "1.00 precision where ripgrep scored 0.52" is true and read as general superiority. All five copies now say that a reference the index did not record or resolve is simply absent from callers, impact and dead-code answers. ## Deferred, with measurements - **#296** — `Auth.resolve_user`: the obvious encoding measured ~60 phantoms via tier 40's package anchoring. - **#297** — `autoload`: the one-line fix measured 17 phantoms through segment-run require matching. Reverted. - **#298** — extensionless shebang scripts: claims are path-based by design, so this is an architectural change. ## Gates fmt, clippy, rustdoc; 375 workspace suites / 4017 tests; 840 e2e; guest gates under `--locked`; corpus ratchet; precision 7/7; `ruby_package_parity`; the version-bump suites. Mutations run on final code for every new test. Closes #294, #295. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
`forbidden_response` -- no parentheses, no arguments, no receiver -- is
how Ruby normally calls an attribute reader, predicate or memoized
helper. It parses to the same `identifier` node as a local read, and the
extractor emitted NO ROW for it. A method called only that way then
carried `ref_count: 0`, `name_fallback_count: 0`,
`name_fallback_shape_excluded: 0` and no `name_fallback_unmeasured`:
three disclosures agreeing nothing referenced a symbol with call sites.
Reported from a live Ruby repository and reproduced before any change.

THE FIX IS RUBY'S OWN RULE, APPLIED LEXICALLY. A name is a local from the
point an assignment, parameter, block parameter, `for` variable, `rescue
=> e`, pattern binding or named regex capture binds it; `def`, `class`,
`module` and `class << self` are hard scopes, blocks and lambdas soft.
`identifier_role` reads which slot an identifier sits in -- every slot
measured against a tree-sitter-ruby parse -- and a value that is not a
bound local becomes a row. Implicit block params (`it`, `_1`) are not
calls; an identifier whose parent is `ERROR` is recovery debris (the
`broken.rb.rbx` decoy caught `method_call bad` / `method_call end`).

WHAT THE ROW MAY BIND TO TOOK FIVE MEASURED ENCODINGS. Each was diffed
bind-for-bind against the clean build on ruby-sinatra, every bind outside
the call site's own class read at source. `precision_gate` was 7/7 for
all five -- none of these sites is a declared decoy:

  foo()'s shape (unqualified CALL)   767 new, 59+ wrong (tier 2 bound
                                     route-block `params` to
                                     `PatternLookAlike#params`)
  `self.foo` everywhere              538 new, wrong via tier 3
  qualified by container path        356 new, 85 wrong: tier 1Q reads a
                                     qualifier as a MODULE path
  `self.foo` in method bodies        114 new, 12 wrong: an inherited GEM
                                     method (`Rack::Response#headers`)
                                     was taken as the unique project one
  proven, relative-`self` qualified  105 new, 3 via documented tier 1b

Shipped: every bare call is `UNCAPTURED` (refused by tiers 1a/2/3) unless
self's own class DEFINES the name in the same file -- decided after the
whole file -- and a proven call is qualified by the relative path `self`,
binding only through `TIER1Q_REL_SAMEFILE`. Of the 105: 101 own-class
(six sampled at source), 1 via `include Helpers`, 3 through the I023
same-directory/file-key policy (#194). No existing bind changed. A path
qualifier rather than a receiver because the package ABI can only say
`self` without a source token via `TAG_REF_REL_QUALIFIER`, which
`validate` refuses unless the ref is qualified.

THE PACKAGE MOVED WITH IT. `ruby_package_parity` holds builtin and the
`de.h-dv.ruby` wasm guest equal row for row: the guest got the same
logic in fixed buffers (`LOCALS`, `BARE`), 19 kinds appended to its kind
table (`KIND_TABLE_DIGEST` unchanged -- it is the grammar's), extractor
rebuilt after first reproducing the committed 807e0485... byte for byte.
Both legs 2966 resolved. Package 0.6.0 -> 0.7.0, both digests moved.

CORPUS: ruby-sinatra refs +2091, resolved +105; rust-ripgrep refs +8
(`pkg/brew/ripgrep-bin.rb`'s Homebrew DSL, resolved unchanged). Cost
fired at +11.2% vm_step and was attributed per statement against the
clean tree before blessing: it tracks the rows (INSERT INTO refs 32% of
the delta), nothing disproportionate.

SKILL.md now says both counters count ROWS, not source, and names field
destructuring (#125) as a use still unrecorded. Constant-receiver calls
(`Auth.resolve_user`) stay unresolved-and-disclosed: the naive encoding
measured ~60 phantoms via tier 40's package anchoring -- #296.

Mutations run on the final code, six, each RED on its named test.
Gates: fmt, clippy, rustdoc, 374 workspace suites (the two re-pinned
abi_projection counts are the 5 new fixture rows, each read), 840 e2e,
guest gates, corpus ratchet, precision 7/7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
`issued_for_us?` is an ordinary Ruby method name, and the index stores it
with the `?`. `check_rename` refused to rename TO such a name ("new_name
must be a plain identifier"), and -- the worse half -- its text scan went
DARK on one, because both used the language-blind `[A-Za-z0-9_$]` set.
The text channel is what catches the call sites the index did not
resolve, so it was off on exactly the methods where Ruby resolution is
weakest, and `clean` became unreachable for every predicate, bang and
setter method.

ONE TABLE, READ THROUGH THE PROFILE GATE. `lang_profile::
IDENTIFIER_SUFFIXES` grants Ruby a single trailing `?`, `!` or `=`, and
`is_plain_identifier(lang, name)` reads it through `profile_name` -- so
the packaged `de.h-dv.ruby/ruby` gets Ruby's answer, which a bare
`lang == "ruby"` would silently deny (the #112 trap). Every language
without a row is unchanged.

  * The MCP layer checks SHAPE first (an identifier in SOME language,
    before any index call), then asks the symbol's own language. A Rust
    symbol still refuses `valid?`, now with a message naming the language
    and what it allows.
  * `text_occurrences` accepts the same names. `?`, `!` and `=` are
    literal inside an FTS5 phrase; only `"` is special, and it is still
    refused.

Verified on the reporter's case: `issued_for_us?` -> `mcp_client_token?`
finds both edit sites and every evidence channel reads `clear`, the text
channel included.

Mutations run, each RED on its named test: the bare charset gate
restored in `text_occurrences`; the Ruby row dropped; the table read by
bare language id instead of through the profile gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
Reported from a live Rails repository: `search_symbols("error")` ranked
`error` functions from minified upstream assets (`public/assets/*.js`)
above the project's own `Tools.error`, which missed the top five of 95.
Exact matches are ordered by `ref_count`, and minified code's internal
calls resolve densely inside one enormous line -- so the vendored copy
out-counts the project's.

A PATH RULE WOULD BE A LIST OF CASES (`assets/`, `.min.js`, `vendor/`).
The structural fact is already stored: minified code declares symbols at
huge COLUMN offsets, because the file is a handful of very long lines,
and human-written code does not. MEASURED, max `start_col` over every
non-import symbol: this repository 49 (19,187 symbols), cs-dapper 105,
ruby-sinatra 79, python-flask 66, php-guzzle 35, ts-zod 33, js-express
18 -- none past 200 anywhere. `MINIFIED_DECL_COL = 200` is twice the
widest real declaration measured.

The tier sits UNDER exact match and above everything else, in both
orderings (the SQL fast path and its Rust mirror). It demotes and never
hides: the row stays in `results`, in `total`, and on a later page
through `next_cursor`. Registered in `bounding_site_registry` as
`Intentional` for exactly that reason.

`search_ranking_minified_e2e` reproduces the report against a real
daemon: the project's `error` with no refs, a minified copy past column
400 called three times. Two preconditions pin that the fixture really is
the reported shape. MUTATION (run): drop the tier from both orderings --
RED, the minified `error` (col 421, ref_count 3) ranks first.

Part of the same ranking complaint is downstream of #296: the project's
`Tools.error` had `ref_count: 0` because its constant-receiver calls do
not resolve. Ranking is only as good as resolution.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
A Ruby user's review of this tool put it plainly: the index's precision
held, but it missed most callers, and grep was complete. Our own line --
"a measured 1.00 precision where ripgrep scored 0.52" -- is true and
reads as general superiority, which is the part that was not.

Every copy now says what the figure measures and what it does not: a
call the index did not record or resolve is simply ABSENT from callers,
impact and dead-code answers, so where a language's `resolution` is low,
confirm those with `search_text` before acting on them.

  * crates/core/src/skills/code-index/SKILL.md   the served skill
  * crates/mcp-server/src/server.rs              the MCP instructions
  * crates/cli/src/rules.rs                      the `rules` template,
    and this repository's own generated rule files refreshed from it
  * AGENTS.md                                    hand-maintained, no
    marker block, so `rules` leaves it alone -- updated by hand

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
release: v0.31.2
All checks were successful
CI / cargo fmt (pull_request) Successful in 51s
CI / OSS corpus tier-3 scale (nightly) (pull_request) Has been skipped
CI / Grammar rebuild from source (nightly) (pull_request) Has been skipped
CI / cargo deny (pull_request) Successful in 4m16s
CI / cargo test (abi, 32-bit + wasm32) (pull_request) Successful in 4m47s
CI / cargo check (MSRV 1.98) (pull_request) Successful in 4m58s
CI / cargo clippy (pull_request) Successful in 4m57s
CI / cargo check (windows-gnu) (pull_request) Successful in 5m5s
CI / CI lane wall-clock headroom (pull_request) Successful in 1m18s
CI / guest crates (fmt, clippy, doc) (pull_request) Successful in 1m19s
CI / cargo doc (intra-doc links) (pull_request) Successful in 2m42s
CI / OSS corpus (tier 1) (pull_request) Successful in 24m18s
CI / cargo test (pull_request) Successful in 27m35s
CI / cargo test (daemon transport) (pull_request) Successful in 9m1s
CI / Plugin path cost + pool throughput (nightly) (pull_request) Has been skipped
CI (Windows) / fmt + clippy + build + test (windows) (pull_request) Successful in 55m28s
455de9fa64
Carried by `.forgejo/scripts/version_bump.sh 0.31.2`: Cargo.toml, the
root and four guest lockfiles, README.md, and the catalog's `version`,
`tag` and plugin asset URLs -- including `de.h-dv.ruby`'s new 0.7.0 row,
whose URLs now point at the tag that will actually publish it. The
self-check passed with `rusqlite` still at 0.31.0.

Graded by the suites a bump moves (registry_schema_gate,
startup_beacon_e2e, release_gate, corpus_stage, version_site_registry,
registry_mcp_e2e, answer_provenance_e2e, plugin_add_package_fetch_e2e,
tree_abi_bench) and the guest gates under --locked: all green.

What ships: #294 (a bare Ruby call is a call, builtin and package),
#295 (check_rename accepts a name valid in the symbol's language), the
minified-declaration ranking tier, and the precision-not-recall wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
buildagent deleted branch fix-ruby-recall-294 2026-09-23 21:21:18 +02:00
buildagent referenced this pull request from a commit 2026-09-23 21:21:20 +02:00
Sign in to join this conversation.
No reviewers
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!299
No description provided.