Skip to content
5 changes: 3 additions & 2 deletions gitnexus/bench/scope-capture/baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04)."
},
"ruby": {
"fingerprint": "bdc7dbfbe5ce7b1e98292f88b404071b4a4b5566f6e756cd637e36a2214967e1",
"fingerprint": "c3e9eec6ed152eae1f7d9759c08041d7d6230a4c532ec07d33f3c9f1ff7b9588",
"scaling_budget": 1.5,
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.)"
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.)",
"_note": "F62: + scope_resolution class/module declaration captures — fixture count 78→81, fingerprint drift expected."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 · CI blocker] The captures golden wasn't regenerated — a separate artifact from the bench baseline. [reproduced]

This _note correctly flags bench-fingerprint drift, but the scope-captures golden — test/fixtures/ruby-captures-golden/expected-captures.json (not in this diff) — is a different file and was not regenerated. ruby-captures-golden.test.ts globs every ruby-* fixture, so it now collects ruby-namespaced/namespaced.rb and fails: expected {…(81)} to deeply equal {…(80)} (exit 1). Confirmed failing in CI (tests / ubuntu / coverage → fail; CI Gate fails downstream).

Fix: UPDATE_GOLDEN=1 npx vitest run test/unit/scope-resolution/ruby/ruby-captures-golden.test.ts and commit the updated expected-captures.json.

},
"swift": {
"fingerprint": "53325c6345161c5a495f997297af5a24fb718fd3e6647040160f8ab2a2c8e4c0",
Expand Down
10 changes: 10 additions & 0 deletions gitnexus/src/core/ingestion/languages/ruby/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,21 @@ const RUBY_SCOPE_QUERY = `
(class
name: (constant) @declaration.name) @declaration.class

;; class Foo::Bar — namespaced class definition
(class
name: (scope_resolution
name: (constant) @declaration.name)) @declaration.class

;; ── Declarations — module (labeled Trait for class-like registry lookup) ─

(module
name: (constant) @declaration.name) @declaration.trait

;; module Baz::Qux — namespaced module definition
(module
name: (scope_resolution
name: (constant) @declaration.name)) @declaration.trait

;; ── Declarations — method (instance) ─────────────────────────────────────

(method
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Foo::Bar
def bar_method; end
end

module Baz::Qux
def qux_method; end
end

class Outer::Middle::Inner
def inner_method; end
end
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@
"captureGroups": 12,
"digest": "c10f36dbbbe2be16fc3fccbebb7ee79668ec7a77b75adbd5281ded31893d49de"
},
"ruby-namespaced/namespaced.rb": {
"captureGroups": 16,
"digest": "34e07387fece6c1d2deb49c39fc2bfe0badfe8015dd1f7ae956d57ac98322a1d"
},
"ruby-overload-dispatch/lib/app.rb": {
"captureGroups": 10,
"digest": "288d5386cf37fb76b52a94bc7da6bf8e7843830ebbb01fcd8100d1590c0e3f72"
Expand Down
82 changes: 82 additions & 0 deletions gitnexus/test/integration/resolvers/ruby-namespaced.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Regression tests for Ruby namespaced class/module definitions (issue #1933 F62).
*
* The existing query captures (class) and (module) with name: (constant) only —
* missing namespaced forms like class Foo::Bar and module Baz::Qux where the
* name field is a scope_resolution node.
*/
// NOTE: Tests are capture-level only. Graph-node modeling for namespaced
// class/module definitions is a tracked follow-up — the scope-extractor
// doesn't yet handle scope_resolution names end-to-end.
import { describe, it, expect } from 'vitest';
import { emitRubyScopeCaptures } from '../../../src/core/ingestion/languages/ruby/index.js';
import type { CaptureMatch } from 'gitnexus-shared';

describe('Ruby namespaced class/module definitions (F62) — capture-level', () => {
it('class Foo::Bar captures @declaration.class with tail constant (Bar)', () => {
const src = `class Foo::Bar
def bar_method; end
end
`;
const matches = emitRubyScopeCaptures(src, 'test.rb') as CaptureMatch[];
const classDecls = matches.filter((m) => m['@declaration.class']);
expect(classDecls.length).toBe(1);
expect(classDecls[0]['@declaration.name'].text).toBe('Bar');
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 · incomplete fix] These assertions verify capture output only — the graph node is never created. [reproduced]

All 5 tests assert emitRubyScopeCaptures output (@declaration.name === 'Bar') but never run the pipeline. Running the full pipeline on this PR's own fixture (runPipelineFromRepo, both resolver legs) shows class Foo::Bar / module Baz::Qux produce no Class/Trait node, and their HAS_METHOD edges dangle to a missing owner (Class:namespaced.rb:Foo::Bar → MISSING); a bare class Plain in the same file does get a node. So the capture lands, but the namespaced declaration still isn't modeled as a graph node.

This is pre-existing (byte-identical at BASE), so not a regression — but the tests pass while the F62 goal isn't met end-to-end. Consider a runPipelineFromRepo assertion that the Class/Trait node exists and HAS_METHOD resolves; the root cause is the def keyed on tail Bar vs the method-owner scope keyed on full Foo::Bar.


it('module Baz::Qux captures @declaration.trait with tail constant (Qux)', () => {
const src = `module Baz::Qux
def qux_method; end
end
`;
const matches = emitRubyScopeCaptures(src, 'test.rb') as CaptureMatch[];
const moduleDecls = matches.filter((m) => m['@declaration.trait']);
expect(moduleDecls.length).toBe(1);
expect(moduleDecls[0]['@declaration.name'].text).toBe('Qux');
});

it('nested chain Outer::Middle::Inner resolves to tail constant (Inner)', () => {
const src = `class Outer::Middle::Inner
def inner_method; end
end
`;
const matches = emitRubyScopeCaptures(src, 'test.rb') as CaptureMatch[];
const classDecls = matches.filter((m) => m['@declaration.class']);
expect(classDecls.length).toBe(1);
expect(classDecls[0]['@declaration.name'].text).toBe('Inner');
});

it('bare class Foo still works alongside namespaced class', () => {
const src = `
class Foo
def foo_method; end
end

class Foo::Bar
def bar_method; end
end
`;
const matches = emitRubyScopeCaptures(src, 'test.rb') as CaptureMatch[];
const classDecls = matches.filter((m) => m['@declaration.class']);
expect(classDecls.length).toBe(2);
const names = classDecls.map((m) => m['@declaration.name'].text).sort();
expect(names).toEqual(['Bar', 'Foo']);
});

it('bare module Baz still works alongside namespaced module', () => {
const src = `
module Baz
def baz_method; end
end

module Baz::Qux
def qux_method; end
end
`;
const matches = emitRubyScopeCaptures(src, 'test.rb') as CaptureMatch[];
const moduleDecls = matches.filter((m) => m['@declaration.trait']);
expect(moduleDecls.length).toBe(2);
const names = moduleDecls.map((m) => m['@declaration.name'].text).sort();
expect(names).toEqual(['Baz', 'Qux']);
});
});
Loading