A bare zero-arg Ruby call emits no reference row, so an idiomatically-called method ships a TRIPLE-earned zero — and SKILL.md now tells agents to trust it #294
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#294
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?
Reported from a live Ruby project (a Zammad MCP server), and reproduced here before filing.
The measurement
search_symbolson that file:ref_countname_fallback_countname_fallback_shape_excludedname_fallback_unmeasuredparen_calledparen_called(1)forbidden_responseresource_metadata_urlissuer_hostallowed_app_idsThe index resolved exactly 1 reference in the file — the control.
Why this is the catastrophic reading, in our own words
SymbolRow::unmeasured's doc already describes this defect class exactly:That is this, in Ruby. And all three disclosure fields agree it did not happen:
name_fallback_count: 0— documented as "no unresolved ref matches this symbol's name, language and call shape"name_fallback_shape_excluded: 0— so the zero is not an artefact of the call-shape filter eithername_fallback_unmeasuredabsent — which the contract says means the zero was EARNEDsearch_symbols' own description states the contract: "A0IS EARNED: it ships only where the index holds a USE-BEARING row bearing this name … Where none does, the field is ABSENT andname_fallback_unmeasurednames why IN THE ROW." There is no use-bearing row here. The field should have been absent with a reason. It shipped a zero instead.The shape is NARROWER than "Ruby without parentheses"
The reporter said parenthesis-free calls produce no reference. Measured, that is not quite it, and the real boundary matters for the fix:
Ruby's command syntax (
foo "x") is unambiguously a call and is handled. The invisible shape is the bare, receiver-less, zero-argument identifier, which is syntactically identical to a local variable read and needs scope analysis to disambiguate.That shape is not an edge case in Ruby. It is how attribute readers, predicates and memoized helpers are normally called.
Why
NO_USE_REFERENCE_CHANNELdoes not fireThe earned-zero test is evaluated per (language, kind) — "does a Ruby method have a use channel?" It does: both
foo(x)andfoo "x"produce rows. So the kind-level test passes and the zero is declared earned, while the specific call shape actually used in the codebase is invisible.The blindness is per call shape; the test is per kind. That is the gap.
Why it is urgent now
v0.31.0 shipped
skill://code-index/SKILL.md, which tells agents in its claim table thatref_count: 0withname_fallback_count: 0licenses "nothing references this". We are now actively instructing agents to trust a value that is wrong for idiomatic Ruby.safe_deleteinherits it directly, andchange_impactunderstates reach.The reporter measured 3.3% of unqualified method calls resolving on their repo, and
review_diffproduced six false "untested change" findings because of it.What is NOT broken, said explicitly
Auth.resolve_user(t)gaveref_count: 0withname_fallback_shape_excluded: 2— the filter disclosure working exactly as designed, andsearch_symbolsdocuments that a0beside a non-zero there means "cannot tell unused from unrecorded".find_callersalready flagssymbol_shape_excluded_refs, and the reporter confirms it never claimed "no callers".So the disclosure machinery is right everywhere it has a row to reason about. The failure is confined to the shape that produces no row at all — which is precisely why it is the dangerous half, exactly as #125 argues for destructuring.
What closing it needs
name_fallback_countcan see it. Scope analysis decides call-vs-local-variable; where it cannot, an unresolved row is the honest output and the resolver's existing tiers already refuse to bind what they cannot justify.name_fallback_countmust go ABSENT with a reason code — a new one, since neitherNO_USE_REFERENCE_CHANNEL(kind-level) norUSES_MAY_BE_GENERATEDdescribes it.precision_gate(7/7,phantom_count == 0). New rows are new resolution opportunities and must not manufacture phantoms.search_symbols' description must say so. An agent following our own documented rule reaches a wrong conclusion on any Ruby repository today.Related
Reproduction
The fixture above,
code-index index, thencode-index query search_symbols '{"query":"","limit":30}'. Measured on0.31.1.check_renamerefuses a Ruby predicate/bang name it indexes happily:new_name must be a plain identifierrejectsfoo?#295Rack::MockRequest.newwould bind toRack::Protection::new— so Ruby constant-receiver calls stay unresolved #296rack/protection/basemakeslib/sinatra/base.rbreachable — which blocks treatingautoloadas the dependency it is #297#!/usr/bin/env rubyshebang is indexed as text only, because every language claim is path-based #298