ruby: private_class_method :name marks the INSTANCE method private, not the class method #102

Closed
opened 2026-09-04 13:09:42 +02:00 by buildagent · 3 comments
Member

Found while writing exhaustive expectations against the builtin Ruby plugin for #84 Phase 3, and verified in the source. The expectations lane recorded it as an observation rather than "correcting" it, which is why it is a report and not a silent edit — an expectation encoding an opinion instead of the reference makes the parity gate grade the wrong thing.

The defect

crates/plugins/src/ruby.rs::visibility_marker takes a class_method_form: bool and has three arms. Two honour it and one does not.

// BARE form — honours it
let Some(args) = node.child_by_field_name("arguments") else {
    if !class_method_form {              // <- consulted
        if let Some(top) = self.section.last_mut() { *top = vis; }
    }
    return;
};

for arg in args.named_children(&mut cursor) {
    match arg.kind() {
        // INLINE form — honours it
        "method" | "singleton_method" => {
            self.pending_def_vis = Some((vis, class_method_form));   // <- carried
        }
        // TARGETED form — DOES NOT
        "simple_symbol" | "string" => {
            ...
            if let Some(s) = self.symbols.iter_mut().rev().find(|s| {
                s.parent == parent
                    && s.name == name
                    && (s.kind == SymbolKind::METHOD || s.kind == SymbolKind::FUNCTION)
            }) {
                s.visibility = vis;       // <- class_method_form never read
            }
        }

So for:

class C
  def theta; end            # instance method
  def self.theta; end       # class method
  private_class_method :theta
end

the search finds a symbol by (parent, name, kind ∈ {METHOD, FUNCTION}) and marks whichever it reaches. private_class_method is supposed to privatise self.theta and leave the instance method alone. Both directions are wrong: the instance method is demoted to file visibility when it should stay exported, and the class method stays exported when it should be private.

.rev() means the LAST matching symbol wins, so which one is hit depends on declaration order rather than on the form the user wrote.

Why it matters beyond a wrong field

Visibility is not cosmetic here — it gates candidate-pool membership. A method wrongly marked file is excluded from cross-file resolution, so this can silently cost real edges on any Ruby codebase using the targeted form. And a class method wrongly left exported is offered to pools it should not be in.

Confirmed adjacent, and already documented as out of scope

visibility_marker's own doc says: "Not handled (v1): module_function, private_constant, visibility inside class << self bodies." That list is honest and this is not on it — the targeted private_class_method arm looks handled and is not.

Suggested fix

The search needs the class/instance distinction the other two arms already have. Whatever field distinguishes a class method from an instance method on RawSymbol should join the find predicate, and the case where no matching symbol exists should stay a no-op (the doc already says a name targeted before its def is skipped).

Test shape

The fixture already exists: tests/packages/ruby/fixtures/visibility.rb.rbx covers section flips, public :x, inline private def, the private def self. gotcha, private_class_method in both forms, the targeted string form, protected, attr-in-private-section and nested-body reset — 21 symbols. It currently PINS the wrong behaviour as the reference, because that is what the builtin does.

When this is fixed, that expectation must be regenerated and the change reviewed as an intentional delta — COSI_BLESS_RUBY_EXPECT=1 rewrites it, and the diff is the proof. A mutation restoring the class_method_form-blind predicate must go red.

Both directions need asserting: the class method becomes private AND the instance method of the same name stays exported. A fix that privatises both would pass a one-sided test.

Found via #84 Phase 3. Not a blocker for the migration proof — the package must reproduce the builtin's behaviour whatever it is, so this affects both legs identically and the parity gate stays valid. Fixing it changes what BOTH must produce.

Found while writing exhaustive expectations against the builtin Ruby plugin for #84 Phase 3, and verified in the source. The expectations lane recorded it as an observation rather than "correcting" it, which is why it is a report and not a silent edit — an expectation encoding an opinion instead of the reference makes the parity gate grade the wrong thing. ## The defect `crates/plugins/src/ruby.rs::visibility_marker` takes a `class_method_form: bool` and has three arms. **Two honour it and one does not.** ```rust // BARE form — honours it let Some(args) = node.child_by_field_name("arguments") else { if !class_method_form { // <- consulted if let Some(top) = self.section.last_mut() { *top = vis; } } return; }; for arg in args.named_children(&mut cursor) { match arg.kind() { // INLINE form — honours it "method" | "singleton_method" => { self.pending_def_vis = Some((vis, class_method_form)); // <- carried } // TARGETED form — DOES NOT "simple_symbol" | "string" => { ... if let Some(s) = self.symbols.iter_mut().rev().find(|s| { s.parent == parent && s.name == name && (s.kind == SymbolKind::METHOD || s.kind == SymbolKind::FUNCTION) }) { s.visibility = vis; // <- class_method_form never read } } ``` So for: ```ruby class C def theta; end # instance method def self.theta; end # class method private_class_method :theta end ``` the search finds a symbol by `(parent, name, kind ∈ {METHOD, FUNCTION})` and marks whichever it reaches. `private_class_method` is supposed to privatise `self.theta` and leave the instance method alone. **Both directions are wrong**: the instance method is demoted to `file` visibility when it should stay exported, and the class method stays exported when it should be private. `.rev()` means the LAST matching symbol wins, so which one is hit depends on declaration order rather than on the form the user wrote. ## Why it matters beyond a wrong field Visibility is not cosmetic here — it gates candidate-pool membership. A method wrongly marked `file` is excluded from cross-file resolution, so this can silently cost real edges on any Ruby codebase using the targeted form. And a class method wrongly left `exported` is offered to pools it should not be in. ## Confirmed adjacent, and already documented as out of scope `visibility_marker`'s own doc says: *"Not handled (v1): `module_function`, `private_constant`, visibility inside `class << self` bodies."* That list is honest and this is **not** on it — the targeted `private_class_method` arm looks handled and is not. ## Suggested fix The search needs the class/instance distinction the other two arms already have. Whatever field distinguishes a class method from an instance method on `RawSymbol` should join the `find` predicate, and the case where no matching symbol exists should stay a no-op (the doc already says a name targeted before its `def` is skipped). ## Test shape The fixture already exists: `tests/packages/ruby/fixtures/visibility.rb.rbx` covers section flips, `public :x`, inline `private def`, the `private def self.` gotcha, `private_class_method` in both forms, the targeted string form, `protected`, attr-in-private-section and nested-body reset — 21 symbols. It currently PINS the wrong behaviour as the reference, because that is what the builtin does. **When this is fixed, that expectation must be regenerated and the change reviewed as an intentional delta** — `COSI_BLESS_RUBY_EXPECT=1` rewrites it, and the diff is the proof. A mutation restoring the `class_method_form`-blind predicate must go red. Both directions need asserting: the class method becomes private AND the instance method of the same name stays exported. A fix that privatises both would pass a one-sided test. ## Related Found via #84 Phase 3. Not a blocker for the migration proof — the package must reproduce the builtin's behaviour whatever it is, so this affects both legs identically and the parity gate stays valid. Fixing it changes what BOTH must produce.
Author
Member

The packaged Ruby half is NOT optional, and it is not silent either — ruby_package_parity is the gate that catches it

Recording the coupling, because the builtin fix is in flight and its packaged twin is not.

This issue already says the right thing and it is worth pulling out of the last paragraph:

the package must reproduce the builtin's behaviour whatever it is … Fixing it changes what BOTH must produce.

crates/indexer/tests/ruby_package_parity.rs compares crates/plugins/src/ruby.rs against tests/packages/ruby row for row on every symbol column, and since #112 closed the qualified_name delta there is no pinned mechanism left on the symbol table at all — the loop now asserts diff.is_empty() and panics with both rows on anything else. visibility is one of those columns.

So the day the builtin fix lands alone, that suite goes red with

an UNEXPLAINED symbol difference at columns [7]. #84 permits two normalisations and no
third, and since #112 NO pinned mechanism touches a symbol column

That is the correct outcome and it is worth saying out loud: a divergence between the two Ruby implementations cannot be silent in this tree. #84's anti-vacuity claim rests on the two agreeing, and the gate enforces it.

The three ways out, and only one is right

  1. Land both halves together. Correct.
  2. Land the builtin fix and add a Mechanism variant pinning the divergence. Wrong, and specifically forbidden: a Mechanism is a host-side gate a package cannot close. This would be a port defect wearing a pin, which is the "widen a Mechanism predicate without re-measuring" move that file's own doc refuses.
  3. Hold the builtin fix until the guest is ported. Acceptable; delays a real defect.

What the packaged half costs, concretely

The guest's visibility_marker (crates/guest/ruby/src/extract.rs:710) has the identical three-arm shape and the identical blind spot: the bare arm at 712 consults class_method_form, the inline arm at 720 carries it into pending_def_vis, and the targeted arm does not. The builtin's in-flight fix threads a singleton_symbols set and joins self.singleton_symbols.contains(&i) == class_method_form onto the find predicate; the guest needs the same rule against its own index space.

Six coordinated artifacts, and none of them is optional:

  1. crates/guest/ruby/src/extract.rs — the predicate.
  2. crates/guest/ruby/build.sh — rebuild extractor.wasm (gen-kinds.sh, cargo build --target wasm32-unknown-unknown, --strip-name-section, --inject-globals).
  3. crates/plugin-host/tests/grammar_provenance.rs::WASM_ARTIFACTS — the artifact sha256 lives there and only there.
  4. tests/packages/ruby/fixtures/visibility.expected — regenerate with COSI_BLESS_RUBY_EXPECT=1; the diff is the review.
  5. tests/packages/ruby/plugin.toml — [package] version 0.3.0 → 0.4.0. The facts change, so the version must.
  6. tests/packages/ruby.digest — re-record. This moves extraction_identity, which re-extracts every claimed file on every project that enabled the package. That is the CORRECT consequence and not a cost to avoid.

Plus the test discipline this issue already specifies: both directions asserted (the class method becomes private AND the instance method of the same name stays exported — a fix that privatises both passes a one-sided test), and a mutation restoring the class_method_form-blind predicate must go red on the guest side too.

Status

Recorded deferral, sequenced after the builtin fix, not abandoned. Porting it now would mean building against an uncommitted ruby.rs whose singleton_symbols design is still under review, and a guest rebuilt against a design that then changes is a wasted extraction_identity move charged to every operator who enabled the package.

The sequencing that works: land the builtin fix and the guest port in one change, with ruby_package_parity green at the end of it and COSI_BLESS_RUBY_EXPECT=1 run once, not twice.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

## The packaged Ruby half is NOT optional, and it is not silent either — `ruby_package_parity` is the gate that catches it Recording the coupling, because the builtin fix is in flight and its packaged twin is not. This issue already says the right thing and it is worth pulling out of the last paragraph: > the package must reproduce the builtin's behaviour whatever it is … **Fixing it changes what BOTH must produce.** `crates/indexer/tests/ruby_package_parity.rs` compares `crates/plugins/src/ruby.rs` against `tests/packages/ruby` **row for row on every symbol column**, and since #112 closed the `qualified_name` delta there is **no pinned mechanism left on the symbol table at all** — the loop now asserts `diff.is_empty()` and panics with both rows on anything else. `visibility` is one of those columns. So the day the builtin fix lands alone, that suite goes red with ``` an UNEXPLAINED symbol difference at columns [7]. #84 permits two normalisations and no third, and since #112 NO pinned mechanism touches a symbol column ``` That is the correct outcome and it is worth saying out loud: **a divergence between the two Ruby implementations cannot be silent in this tree.** #84's anti-vacuity claim rests on the two agreeing, and the gate enforces it. ### The three ways out, and only one is right 1. **Land both halves together.** Correct. 2. Land the builtin fix and add a `Mechanism` variant pinning the divergence. **Wrong**, and specifically forbidden: a `Mechanism` is a host-side gate a package *cannot* close. This would be a port defect wearing a pin, which is the "widen a Mechanism predicate without re-measuring" move that file's own doc refuses. 3. Hold the builtin fix until the guest is ported. Acceptable; delays a real defect. ### What the packaged half costs, concretely The guest's `visibility_marker` (`crates/guest/ruby/src/extract.rs:710`) has the identical three-arm shape and the identical blind spot: the bare arm at 712 consults `class_method_form`, the inline arm at 720 carries it into `pending_def_vis`, and the targeted arm does not. The builtin's in-flight fix threads a `singleton_symbols` set and joins `self.singleton_symbols.contains(&i) == class_method_form` onto the `find` predicate; the guest needs the same rule against its own index space. Six coordinated artifacts, and none of them is optional: 1. `crates/guest/ruby/src/extract.rs` — the predicate. 2. `crates/guest/ruby/build.sh` — rebuild `extractor.wasm` (`gen-kinds.sh`, `cargo build --target wasm32-unknown-unknown`, `--strip-name-section`, `--inject-globals`). 3. `crates/plugin-host/tests/grammar_provenance.rs::WASM_ARTIFACTS` — the artifact sha256 lives there and **only** there. 4. `tests/packages/ruby/fixtures/visibility.expected` — regenerate with `COSI_BLESS_RUBY_EXPECT=1`; the diff is the review. 5. `tests/packages/ruby/plugin.toml` — `[package] version` 0.3.0 → 0.4.0. The facts change, so the version must. 6. `tests/packages/ruby.digest` — re-record. This moves `extraction_identity`, which re-extracts every claimed file on every project that enabled the package. That is the CORRECT consequence and not a cost to avoid. Plus the test discipline this issue already specifies: **both directions asserted** (the class method becomes private AND the instance method of the same name stays exported — a fix that privatises both passes a one-sided test), and a mutation restoring the `class_method_form`-blind predicate must go red **on the guest side too**. ### Status **Recorded deferral, sequenced after the builtin fix, not abandoned.** Porting it now would mean building against an uncommitted `ruby.rs` whose `singleton_symbols` design is still under review, and a guest rebuilt against a design that then changes is a wasted `extraction_identity` move charged to every operator who enabled the package. The sequencing that works: land the builtin fix and the guest port **in one change**, with `ruby_package_parity` green at the end of it and `COSI_BLESS_RUBY_EXPECT=1` run once, not twice. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

The packaged half LANDED, and the fixture that was supposed to grade this graded nothing — measured, not argued

f3fceed on wip/rubypkg, based on integration at a33210d. Both halves are now in one tree, which is option 1 of the three the previous comment listed.

The port

The guest had the identical three-arm shape and the identical blind spot. Sym gains a singleton field — never encoded, scratch for visibility_marker exactly as the builtin's singleton_symbols set is — threaded true through the def self. construction site and false through the other four, and the backward scan now requires s.singleton == class_method_form. false is the safe default because it can only withhold a demotion, never invent one.

THE FIXTURE NEVER GRADED THE DEFECT, AND THAT IS PROVEN RATHER THAN SUSPECTED

This issue said "The fixture already exists" and "a mutation restoring the class_method_form-blind predicate must go red." The first was true and the second was not, and the reason is this issue's own §Test shape being one case short.

visibility.rb.rbx's theta sits inside an enclosing bare private section, so it was file for an unrelated reason, and the file declares no def self.theta at all — so the marker had one candidate and no way to pick wrong. That is why COSI_BLESS_RUBY_EXPECT=1 wrote zero bytes after the builtin fix.

Measured, with the guest predicate reverted and the OLD fixture in place:

verdict             passed
  fixtures/core.rb.rbx        facts  match
  fixtures/members.rb.rbx     facts  match
  fixtures/visibility.rb.rbx  facts  match
  fixtures/rails.rb.rbx       facts  match
  fixtures/broken.rb.rbx      facts  match

The defect fully present, the grader silent. Shipping the port against that fixture would have been a green that means nothing.

The fixture that does grade it

Api::Markers and Api::Mirrors, appended so no existing line number moves and the regenerated .expected is a readable diff. One class per marker form; each declares an instance AND a class method of the same name; and the declaration order is INVERTED between them, so a form-blind predicate — which searches backwards in both implementations — reaches the WRONG method in both rather than being accidentally right in one. That is the trap the builtin's own unit tests hit first (.rev() reaching the right symbol by luck, mutation surviving twice).

class Markers
  def self.nu; end
  def nu; end
  private_class_method :nu     # class -> file, instance stays exported
end

class Mirrors
  def xi; end
  def self.xi; end
  private :xi                  # instance -> file, class stays exported
end

MUTATION (RUN): drop && s.singleton == class_method_form from the guest, rebuild the wasm with build.sh, repack, re-sign, re-install and re-check — deliberately through a locally-signed package, so that the digest pins cannot be what fails and the FIXTURE is what grades.

verdict             FAILED
  fixtures/visibility.rb.rbx  de.h-dv.ruby/ruby [28 symbols, 14 refs]
    facts             MISMATCH (4 total) against fixtures/visibility.expected
      symbol method nu@85:5: expected visibility = file, got exported
      symbol method nu@88:5: expected visibility = exported, got file
      symbol method xi@95:5: expected visibility = file, got exported
      symbol method xi@98:5: expected visibility = exported, got file

Four rows: both directions, both forms. A fix that privatised both, or neither, fails.

The six coordinated artifacts, each verified

# artifact before after
1 crates/guest/ruby/src/extract.rs + src/lib.rs — the predicate + the ZERO_SYM initialiser
2 tests/packages/ruby/extractor.wasm 24,125 B 8c366129… 24,218 B 7b1c3167…
3 grammar_provenance.rs::WASM_ARTIFACTS 8c366129… 7b1c3167…
4 fixtures/visibility.rb.rbx + .expected 21 symbols 28, regenerated by COSI_BLESS_RUBY_EXPECT=1 from the BUILTIN
5 tests/packages/ruby/plugin.toml [package] version 0.3.0 0.4.0
6 tests/packages/ruby.digest 6cede75e… / abe3ee0b… / 2,187,848 B 281dd11a… / d9c1592f… / 2,191,714 B

extraction_identity moved, which re-extracts every claimed file on every project that enabled the package. That is the correct consequence, and it is recorded in ruby.digest rather than left for an operator to discover as a digest_mismatch.

Reproducibility was proven BEFORE the change, not assumed after it. build.sh was run on the UNMODIFIED tree first and produced 24,125 bytes / 8c366129… byte-for-byte — so this box builds 0.3.0 identically, which is what makes 0.4.0's digest a measurement rather than a local artefact. build.sh's container provenance block is annotated, not overwritten: that measurement was 0.3.0's, the container leg was not re-run for 0.4.0, and the file now says so.

Gates

crates/daemon/tests/ruby_package_e2e.rs — 5 passed, 0 failed, including the_shipped_package_is_the_artifact_that_was_recorded (the digest pin) and an_operator_can_grant_derived_names_and_the_rails_file_then_indexes (which runs plugin check --derived-names and asserts all five fixtures pass). plugin-host's grammar_provenance 7/7 and ruby_guest_wire 5/5 with COSI_GUEST_REBUILD=1, so the artifact is bound to the source that builds it.

One thing this landing could NOT verify, and it is not this change

ruby_package_parity — the suite this issue's previous comment correctly names as the gate that catches a one-sided landing — is RED at integration HEAD for an unrelated, pre-existing reason, bisected to 2f16e22's #134 tier-3 origin gate and filed as #167 (green at e724819, red at a33210d, unmodified trees both times; 17 unexplained ref deltas in two opposite directions).

So the claim "the two Ruby implementations agree row for row" is currently unavailable, and this landing does not assert it. What it does assert is what plugin check grades directly: the guest reproduces the builtin's expectations, fixture for fixture, including the four new rows — which is the property this issue actually asks for.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

## The packaged half LANDED, and the fixture that was supposed to grade this graded nothing — measured, not argued `f3fceed` on `wip/rubypkg`, based on `integration` at `a33210d`. Both halves are now in one tree, which is option 1 of the three the previous comment listed. ### The port The guest had the identical three-arm shape and the identical blind spot. `Sym` gains a `singleton` field — **never encoded**, scratch for `visibility_marker` exactly as the builtin's `singleton_symbols` set is — threaded `true` through the `def self.` construction site and `false` through the other four, and the backward scan now requires `s.singleton == class_method_form`. `false` is the safe default because it can only withhold a demotion, never invent one. ### THE FIXTURE NEVER GRADED THE DEFECT, AND THAT IS PROVEN RATHER THAN SUSPECTED This issue said *"The fixture already exists"* and *"a mutation restoring the `class_method_form`-blind predicate must go red."* The first was true and the second was not, and the reason is this issue's own §Test shape being one case short. `visibility.rb.rbx`'s `theta` sits inside an enclosing bare `private` section, so it was `file` for an unrelated reason, and the file declares no `def self.theta` at all — so the marker had one candidate and no way to pick wrong. That is why `COSI_BLESS_RUBY_EXPECT=1` wrote **zero bytes** after the builtin fix. **Measured, with the guest predicate reverted and the OLD fixture in place:** ``` verdict passed fixtures/core.rb.rbx facts match fixtures/members.rb.rbx facts match fixtures/visibility.rb.rbx facts match fixtures/rails.rb.rbx facts match fixtures/broken.rb.rbx facts match ``` The defect fully present, the grader silent. Shipping the port against that fixture would have been a green that means nothing. ### The fixture that does grade it `Api::Markers` and `Api::Mirrors`, appended so no existing line number moves and the regenerated `.expected` is a readable diff. One class per marker form; each declares an instance AND a class method of the same name; and **the declaration order is INVERTED between them**, so a form-blind predicate — which searches backwards in both implementations — reaches the WRONG method in both rather than being accidentally right in one. That is the trap the builtin's own unit tests hit first (`.rev()` reaching the right symbol by luck, mutation surviving twice). ```ruby class Markers def self.nu; end def nu; end private_class_method :nu # class -> file, instance stays exported end class Mirrors def xi; end def self.xi; end private :xi # instance -> file, class stays exported end ``` **MUTATION (RUN):** drop `&& s.singleton == class_method_form` from the guest, rebuild the wasm with `build.sh`, repack, re-sign, re-install and re-check — deliberately through a locally-signed package, so that the digest pins cannot be what fails and the FIXTURE is what grades. ``` verdict FAILED fixtures/visibility.rb.rbx de.h-dv.ruby/ruby [28 symbols, 14 refs] facts MISMATCH (4 total) against fixtures/visibility.expected symbol method nu@85:5: expected visibility = file, got exported symbol method nu@88:5: expected visibility = exported, got file symbol method xi@95:5: expected visibility = file, got exported symbol method xi@98:5: expected visibility = exported, got file ``` Four rows: both directions, both forms. A fix that privatised both, or neither, fails. ### The six coordinated artifacts, each verified | # | artifact | before | after | |---|---|---|---| | 1 | `crates/guest/ruby/src/extract.rs` + `src/lib.rs` | — | the predicate + the `ZERO_SYM` initialiser | | 2 | `tests/packages/ruby/extractor.wasm` | 24,125 B `8c366129…` | **24,218 B `7b1c3167…`** | | 3 | `grammar_provenance.rs::WASM_ARTIFACTS` | `8c366129…` | `7b1c3167…` | | 4 | `fixtures/visibility.rb.rbx` + `.expected` | 21 symbols | **28**, regenerated by `COSI_BLESS_RUBY_EXPECT=1` from the BUILTIN | | 5 | `tests/packages/ruby/plugin.toml` `[package] version` | 0.3.0 | **0.4.0** | | 6 | `tests/packages/ruby.digest` | `6cede75e…` / `abe3ee0b…` / 2,187,848 B | **`281dd11a…` / `d9c1592f…` / 2,191,714 B** | `extraction_identity` moved, which re-extracts every claimed file on every project that enabled the package. That is the correct consequence, and it is recorded in `ruby.digest` rather than left for an operator to discover as a `digest_mismatch`. **Reproducibility was proven BEFORE the change, not assumed after it.** `build.sh` was run on the UNMODIFIED tree first and produced 24,125 bytes / `8c366129…` byte-for-byte — so this box builds 0.3.0 identically, which is what makes 0.4.0's digest a measurement rather than a local artefact. `build.sh`'s container provenance block is **annotated, not overwritten**: that measurement was 0.3.0's, the container leg was not re-run for 0.4.0, and the file now says so. ### Gates `crates/daemon/tests/ruby_package_e2e.rs` — **5 passed, 0 failed**, including `the_shipped_package_is_the_artifact_that_was_recorded` (the digest pin) and `an_operator_can_grant_derived_names_and_the_rails_file_then_indexes` (which runs `plugin check --derived-names` and asserts all five fixtures pass). `plugin-host`'s `grammar_provenance` 7/7 and `ruby_guest_wire` 5/5 with `COSI_GUEST_REBUILD=1`, so the artifact is bound to the source that builds it. ### One thing this landing could NOT verify, and it is not this change `ruby_package_parity` — the suite this issue's previous comment correctly names as the gate that catches a one-sided landing — is **RED at `integration` HEAD for an unrelated, pre-existing reason**, bisected to `2f16e22`'s #134 tier-3 origin gate and filed as **#167** (green at `e724819`, red at `a33210d`, unmodified trees both times; 17 unexplained ref deltas in two opposite directions). So the claim *"the two Ruby implementations agree row for row"* is currently **unavailable**, and this landing does not assert it. What it does assert is what `plugin check` grades directly: the guest reproduces the builtin's expectations, fixture for fixture, including the four new rows — which is the property this issue actually asks for. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

Triage 2026-09-06: CLOSING. Both halves are now in one tree — the builtin fix and the packaged fix — with a fixture that actually grades the defect and six coordinated artifacts moved together.

Verified against master. Builtin half in 2f16e22; packaged half in c54a060 (it was built, verified under wasmtime, then reverted in 2f16e22 because landing it moves package_digest, and re-landed once the version bump, ruby.digest re-record and WASM_ARTIFACTS sha could move with it).

Builtin half

singleton_symbols: HashSet<usize> — crates/plugins/src/ruby.rs:119 (doc at :107-118: "ABSENCE MEANS INSTANCE, which is the safe default"), threaded at :138 and :439. The targeted arm's predicate, ruby.rs:501-507:

(s.parent == parent
    && s.name == name
    && (s.kind == SymbolKind::METHOD || s.kind == SymbolKind::FUNCTION)
    && self.singleton_symbols.contains(&i) == class_method_form)

Packaged half

pub(crate) singleton: bool — crates/guest/ruby/src/extract.rs:159, documented as "Never encoded. It is scratch for visibility_marker, exactly as the builtin's singleton_symbols set is" — the same shape on both legs rather than two rules that must be kept in step. Predicate at extract.rs:762-765.

The declaration-order trap is encoded deliberately

ruby.rs:1477-1533: the class method is declared first so .rev() offers the wrong one first, and the mirror test inverts it. That matters because a mutation ledger elsewhere in this tree records exactly this failure — declaration order let .rev() reach the right symbol by luck and a mutation survived twice. Both directions are asserted here.

The old fixture genuinely graded nothing — confirmed

2f16e22 reports that COSI_BLESS_RUBY_EXPECT=1 wrote nothing, because the fixture's theta already sat inside an enclosing bare private section and was file for an unrelated reason. That is the sharpest kind of finding: the fixture that was supposed to cover this defect was passing for a reason that had nothing to do with it.

The replacement grades it: tests/packages/ruby/fixtures/visibility.rb.rbx:84-98 — class Markers (def self.nu first) and class Mirrors (def xi first), order inverted between them — with 4 new rows at visibility.expected:244-289.

Six artifacts verified as moving together

extractor.wasm 24,218 B, sha256 7b1c3167020eebe93a5187c2c70478b9d59bbd3f3d569ca9554ea794693fefac (matches the recorded claim); WASM_ARTIFACTS at crates/plugin-host/tests/grammar_provenance.rs:182; plugin.toml version = "0.4.0"; ruby.digest re-recorded; the fixture; the .expected.

Runs (exit 0)

cargo test -p code-index-plugins --lib visibility_private_class_method_spares_the_instance_method → 1 passed
cargo test -p code-index-plugins --lib plain_private_spares_the_class_method                      → 1 passed
cargo test -p code-index-daemon  --test ruby_package_e2e                                          → 5 passed

incl. the_shipped_package_is_the_artifact_that_was_recorded (the digest pin) and an_operator_can_grant_derived_names_and_the_rails_file_then_indexes, which runs plugin check over all five fixtures.

Residual — read this before assuming parity is proven

The gate this issue's own first comment names as the one that would catch a one-sided landing — crates/indexer/tests/ruby_package_parity.rs — was not run as part of this verification, and it has been red on the integration branch for a pre-existing unrelated reason (#167 / #170, owned elsewhere).

So the claim "the two Ruby implementations agree row for row" is currently unavailable, not established. What is proven is that plugin check grades the guest against builtin-generated expectations, fixture for fixture, including the four new rows — which is what this issue actually asked for. Stating it that way rather than implying more.

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

## Triage 2026-09-06: CLOSING. **Both halves are now in one tree** — the builtin fix and the packaged fix — with a fixture that actually grades the defect and six coordinated artifacts moved together. Verified against master. Builtin half in `2f16e22`; packaged half in `c54a060` (it was built, verified under wasmtime, then **reverted** in `2f16e22` because landing it moves `package_digest`, and re-landed once the version bump, `ruby.digest` re-record and `WASM_ARTIFACTS` sha could move with it). ### Builtin half `singleton_symbols: HashSet<usize>` — `crates/plugins/src/ruby.rs:119` (doc at `:107-118`: *"ABSENCE MEANS INSTANCE, which is the safe default"*), threaded at `:138` and `:439`. The targeted arm's predicate, `ruby.rs:501-507`: ```rust (s.parent == parent && s.name == name && (s.kind == SymbolKind::METHOD || s.kind == SymbolKind::FUNCTION) && self.singleton_symbols.contains(&i) == class_method_form) ``` ### Packaged half `pub(crate) singleton: bool` — `crates/guest/ruby/src/extract.rs:159`, documented as *"Never encoded. It is scratch for `visibility_marker`, exactly as the builtin's `singleton_symbols` set is"* — the same shape on both legs rather than two rules that must be kept in step. Predicate at `extract.rs:762-765`. ### The declaration-order trap is encoded deliberately `ruby.rs:1477-1533`: the class method is declared **first** so `.rev()` offers the wrong one first, and the mirror test inverts it. That matters because a mutation ledger elsewhere in this tree records exactly this failure — declaration order let `.rev()` reach the right symbol by luck and a mutation survived twice. Both directions are asserted here. ### The old fixture genuinely graded nothing — confirmed `2f16e22` reports that `COSI_BLESS_RUBY_EXPECT=1` wrote **nothing**, because the fixture's `theta` already sat inside an enclosing bare `private` section and was `file` for an unrelated reason. That is the sharpest kind of finding: the fixture that was supposed to cover this defect was passing for a reason that had nothing to do with it. The replacement grades it: `tests/packages/ruby/fixtures/visibility.rb.rbx:84-98` — `class Markers` (`def self.nu` first) and `class Mirrors` (`def xi` first), order inverted between them — with 4 new rows at `visibility.expected:244-289`. ### Six artifacts verified as moving together `extractor.wasm` 24,218 B, sha256 `7b1c3167020eebe93a5187c2c70478b9d59bbd3f3d569ca9554ea794693fefac` (matches the recorded claim); `WASM_ARTIFACTS` at `crates/plugin-host/tests/grammar_provenance.rs:182`; `plugin.toml` `version = "0.4.0"`; `ruby.digest` re-recorded; the fixture; the `.expected`. ### Runs (exit 0) ``` cargo test -p code-index-plugins --lib visibility_private_class_method_spares_the_instance_method → 1 passed cargo test -p code-index-plugins --lib plain_private_spares_the_class_method → 1 passed cargo test -p code-index-daemon --test ruby_package_e2e → 5 passed ``` incl. `the_shipped_package_is_the_artifact_that_was_recorded` (the digest pin) and `an_operator_can_grant_derived_names_and_the_rails_file_then_indexes`, which runs `plugin check` over all five fixtures. ### Residual — read this before assuming parity is proven The gate this issue's own first comment names as the one that would catch a **one-sided landing** — `crates/indexer/tests/ruby_package_parity.rs` — was **not** run as part of this verification, and it has been red on the integration branch for a pre-existing unrelated reason (#167 / #170, owned elsewhere). So the claim *"the two Ruby implementations agree row for row"* is currently **unavailable**, not established. What **is** proven is that `plugin check` grades the guest against builtin-generated expectations, fixture for fixture, including the four new rows — which is what this issue actually asked for. Stating it that way rather than implying more. 🤖 Triage lane, 2026-09-06, master `45cf6e4`
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#102
No description provided.