Skip to content

fix(ingestion): materialize graph nodes for scoped class/module/impl declarations (#1975)#1977

Merged
magyargergo merged 9 commits into
mainfrom
fix/scoped-declaration-graph-nodes
Jun 2, 2026
Merged

fix(ingestion): materialize graph nodes for scoped class/module/impl declarations (#1975)#1977
magyargergo merged 9 commits into
mainfrom
fix/scoped-declaration-graph-nodes

Conversation

@magyargergo

@magyargergo magyargergo commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Resolves #1975 (the PR #1972 P2 follow-up). Scoped class/module/impl declarations now materialize a real graph node and own their methods through it, instead of leaving dangling HAS_METHOD edges.

What's fixed (all collision-safe)

  • Ruby class Foo::Bar / module Baz::Qux — node keyed by the full scoped name. Foo::BarBaz::Bar (distinct).
  • C++ out-of-line struct/class Outer::Inner { … } — the legacy structure query now materializes a node keyed by the full qualified_identifier text, which matches the HAS_METHOD owner id. Outer::Inner and Other::Inner stay distinct (no merge/mis-attribution); 3-level A::B::C resolves.
  • Rust scoped inherent impl a::Inner { … } — a @definition.impl arm materializes the Impl node keyed by the full scoped text; findEnclosingClassInfo owns the methods through it. impl a::Inner and impl b::Inner stay distinct.

The unifying principle (same as the Ruby decision): key the scoped declaration's node by its full qualified text so node id == owner id and same-tail types in different scopes never collide. This deliberately avoids owner tail-reduction, which an earlier revision used and the self tri-review reproduced as a silent same-tail collision (Outer::Inner + Other::Inner → one merged node).

Tests

A findDanglingEdges graph-integrity helper + per-language pipeline tests with positive owner-identity assertions and same-tail collision fixtures (Outer::Inner+Other::Inner, impl a::Inner+impl b::Inner, Foo::Bar+Baz::Bar) — not just dangle-free checks, so a regression to mis-attribution would fail.

Validation

Ruby 136/136, C++/Rust 434/434 on both resolver legs (371+63-skip legacy); ruby+rust captures goldens 19/19 (additive); bench --check PASS (14 langs); tsc --noEmit clean.

Deferred to #1978

  • Rust trait impls on a scoped struct path (impl T for a::Inner) — references a simply-keyed struct, needs qualified struct-node identity.
  • The pre-existing inline same-tail node collision (struct Outer{struct Inner} + struct Other{struct Inner} already merge on main, independent of this PR).
  • A redundant C++ forward-decl node remains alongside the out-of-line def (same qualifiedName); harmless (no test impact), to be reconciled with qualified node identity.

🤖 Generated with Claude Code

magyargergo and others added 6 commits June 2, 2026 14:24
…oped-declaration nodes (U1, #1975)

Adds findDanglingEdges() and pipeline-level tests asserting that Ruby
namespaced class/module declarations materialize a Class/Trait node with
a resolving HAS_METHOD edge. Red by design on the pre-fix base (5 failing)
— the fix lands in U2 (shared core) + U3 (Ruby enablement).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ule declarations (U2/U3, #1975)

Widen the Ruby legacy structure query so `class Foo::Bar` / `module Baz::Qux`
(name field is a scope_resolution node) match @definition.class/.module as
separate top-level patterns. The node is keyed by its full scoped name, which
matches the HAS_METHOD owner id that findEnclosingClassInfo derives from the
same name field — so the previously-dangling ownership edges now resolve, and
distinct namespaces (Foo::Bar vs Baz::Bar) stay distinct nodes (no collision).

No change to findEnclosingClassInfo (zero call-resolution blast radius) and no
scope-extractor/golden/bench impact — the fix is purely the legacy structure
query gate. Finalizes the U1 target assertions to the qualified-name identity.

Validated: 134/134 Ruby resolver tests pass on BOTH legs; tsc --noEmit clean;
dangling HAS_METHOD edges on the ruby-namespaced fixture drop from 3 to 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rship (U4, #1975)

For an out-of-line `struct Outer::Inner { ... }`, the container name is a
qualified_identifier, so findEnclosingClassInfo derived the owner id from the
full `Outer::Inner` text — but the type is keyed by its in-class declaration
(the nested `Inner` node), leaving the method's HAS_METHOD edge dangling.

Reduce a qualified_identifier container name to its tail segment for the owner
id/name, matching how inline nested definitions are already keyed. Node-type
scoped, so Ruby's scope_resolution names stay full (distinct-by-namespace) and
no language is named in shared code. Only out-of-line-def methods (already
dangling) change behavior — zero impact on bare classes or call resolution.

Validated: C++ 268/268 default leg, 205+63-skip legacy leg, no regression;
2 new target tests pass both legs; Ruby namespaced tests still pass; tsc clean;
scope-capture bench rebaselined (cpp +cpp-out-of-line-class fixture) — --check
PASS (13 langs). Dangling HAS_METHOD on the new fixture: 1 -> 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1975)

`impl path::Type` and `impl Trait for path::Type` name the target with a
scoped_type_identifier. Two coordinated fixes:
- findEnclosingClassInfo: reduce a scoped_type_identifier impl target to its
  trailing type name (both the trait-impl `for` branch and the inherent
  branch), matching the type's own tail-keyed declaration.
- tree-sitter-queries: add a @definition.impl arm for scoped inherent impls so
  the Impl node is materialized (keyed by the same tail) instead of missing.

Together the trait-impl method owns through the real Struct node and the
inherent-impl method owns through a real Impl node — no dangling edges. Rust's
scoped_type_identifier has a name: field, so the tail extraction is exact.

Validated: Rust 163/163 on BOTH legs, no regression; new target test passes;
C++/Ruby suites unaffected; tsc clean; scope-capture bench rebaselined
(rust +rust-scoped-impl fixture) — --check PASS (13 langs). Dangling 1 -> 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t captures goldens (U6, #1975)

- Add ruby-tail-collision fixture + test: Foo::Bar and Baz::Bar share the tail
  'Bar' but must stay two distinct Class nodes (locks the KTD-2 anti-collision
  guarantee from full-scoped-name keying). No dangling, no cross-wiring.
- Regenerate the ruby + rust captures goldens for the fixtures added in U3-U6
  (ruby-tail-collision, rust-scoped-impl). Both diffs are additive-only — a
  single new entry each, existing entries byte-identical (no capture-logic
  drift; the fixes are in the legacy structure query + findEnclosingClassInfo,
  not the scope-extractor).
- Re-baseline the ruby scope-capture fingerprint (81->82 fixtures).

N/A-language verification: C#/Java/PHP have no class-declaration scoped-name
gap and show no regression (606 passed; the 2 C# worker-pool failures are the
known worktree 'parse-worker.js not built' limitation, unrelated to this change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment Jun 2, 2026 5:40pm

Request Review

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

✨ PR Autofix

Found fixable formatting / unused-import issues across 32 changed lines. Comment /autofix on this PR to apply them, or run npm run lint:fix && npm run format locally.

{"schema":"gitnexus.pr-autofix/v2","state":"fixes-available","pr_number":1977,"changed_lines":32,"head_sha":"6cb7406e156c7d9a842655bca02ba65de8764e7f","run_id":"26836887106","apply_command":"/autofix"}

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
10930 10918 0 12 673s

✅ All 10918 tests passed

12 test(s) skipped — expand for details
  • COBOL pipeline benchmark > scales with file count
  • C# pipeline benchmark > scales with file count — namespaces spread across the solution
  • C# pipeline benchmark > scales with file count — all types in one (global) namespace bucket
  • C# pipeline benchmark > scales with file count — all types in one (named) namespace bucket
  • Go pipeline benchmark > scales with file count (workers enabled)
  • Go pipeline benchmark — worker pool (issue Worker idle timeout kills long Go scope extraction and surfaces as Napi::Error during analyze #1848) > does not quarantine the large generated Go file on sub-batch idle timeout
  • Go structural interface detection benchmark > scales linearly with interface × struct count
  • PHP pipeline benchmark > scales with file count (workers enabled)
  • Ruby pipeline benchmark > scales with file count (workers enabled)
  • Rust pipeline benchmark > scales with file count (workers enabled)
  • run.cjs direct-exec entrypoint (fix(cli): steer docs, skills, and hooks through a CLI-neutral project-local runner (#1939) #1945) > resolves a .cmd shim via the Windows shell branch, passing args and exit code
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 80.3% 38218/47593 79.84% 📈 +0.5 🟢 ████████████████░░░░
Branches 68.85% 24298/35288 68.5% 📈 +0.3 🟢 █████████████░░░░░░░
Functions 85.45% 3976/4653 84.94% 📈 +0.5 🟢 █████████████████░░░
Lines 83.9% 34377/40969 83.36% 📈 +0.5 🟢 ████████████████░░░░

📋 View full run · Generated by CI

@magyargergo magyargergo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

PR #1977 Tri-Review — scoped class/module/impl graph nodes (#1975)

Methods & engines. GitNexus risk / test-CI lanes + Compound-Engineering personas (correctness, adversarial, maintainability, testing) — all Claude — plus Codex (gpt-5.5, xhigh) as the one independent engine (live; mostly static GitHub inspection this run), plus claude-mem context (the Rust bareTypeIdentifier tail convention; the separate inheritance-edge path). Full disclosure: this PR was authored by the same coordinator running the review — it's a self-review, and it caught a P1 in the author's own implementation. The P1/P2 below were reproduced by the coordinator and corroborated by Codex + ≥2 Claude lanes.

🔴 P1 · reproduced · ship-blocker — C++/Rust tail-reduction collides same-tail types in one file

To kill the dangling owner edge, the PR reduces a C++ qualified_identifier / Rust scoped_type_identifier container to its bare tail. The owner node id is Struct:<file>:<tail>, so two out-of-line scoped types in the same file sharing a tail collapse:

  • Reproduced: struct Outer::Inner {…} + struct Other::Inner {…} in one file → a single Struct:file:Inner node; both from_outer and from_other own through it (from_other silently mis-attributed; Other::Inner never materialized). With same-named members the two methods merge into one node (symbol loss). DANGLING:0 — so the PR's own findDanglingEdges check cannot detect it.
  • Adversarial reproduced the Rust analogue (a::Inner + b::Inner → one Impl:Inner) and confirmed BASE kept them distinct (dangling, not merged) — so this trades a visible dangle for silent corruption for that case.
  • This is exactly the collision the PR deliberately avoided in Ruby by keeping the full scoped key. Fix: key the C++/Rust owner by the full qualified path (resolve to the type's qualifiedName, e.g. Outer.Inner), not the bare tail — mirror the Ruby decision. (Cross-file same-tail types don't collide — the id is file-scoped — so the blast radius is single-file translation units with sibling nested types, common in C++.)

🟠 P2 · reproduced — C++ reduction is single-hop; 3+ level defs still dangle

struct A::B::C {…} → owner Struct:file:B::C [MISSING] (one childForFieldName('name') hop yields the middle B::C, not tail C; real node C / qn A.B.C). Same root cause as P1 — the bare-tail strategy is the wrong lever. (Rust's scoped_type_identifier is flat on name:, so it isn't hit.)

🟡 P2 · code-read — inaccurate comment / contract drift

The new ast-helpers.ts comment says the reduction "Mirrors the qualified_identifier handling above" — it's below; and it (plus the pre-existing NOTE) points at rust.ts:extractOwnerName, which actually lives at method-extractors/configs/rust.ts:extractOwnerName. That function was not updated and still returns the full scoped text — harmless today (it only feeds method metadata, not the owner edge) but the NOTE asserts a synchrony that no longer holds. Fix the references and note the deliberate divergence.

✅ What's solid (validated)

  • Ruby is correct. Adversarial confirmed Foo::Bar / Baz::Bar stay distinct (full-key works) and a cross-file Foo::Bar.new; obj.x resolves (R4 holds). The Ruby half is sound.
  • No inheritance regression — Codex + correctness confirmed the IMPLEMENTS edge for impl Trait for outer::Inner is emitted via a separate (@heritage / preEmitInheritanceEdges) path the owner-reduction doesn't touch.
  • Golden/bench re-baselines are additive-only; bench --check PASS (14 langs); tsc clean.

🧪 Test gaps (why the P1 shipped green)

  • No same-tail collision fixture for C++/Rust — the exact breaking case (Ruby has ruby-tail-collision; add the parallel). findDanglingEdges == [] is necessary but not sufficient — it passes on the mis-attribution; add positive owner-identity assertions.
  • IMPLEMENTS edge for the scoped trait-impl unasserted; the Rust target test uses plain it (no parity wrapper → no legacy-leg differentiation); cross-file R4, class Foo::Bar < Base heritage, and qux_method ownership untested.

CI / merge

Local validation passed (both legs, unit 1177, bench, tsc) but missed the collision (single-type fixtures only). Recommendation: do not merge as-is — rework the C++/Rust owner to a full-qualified key (the Ruby approach). The Ruby portion is sound and could ship independently.

Automated multi-tool digest (GitNexus + Compound-Engineering + Codex + claude-mem) — a self-review of the author's own PR. The P1/P2 were reproduced by the coordinator; verify before acting.

// `class Foo::Bar` has no in-class declaration, so its node IS keyed by the
// full scoped name (and must stay distinct from a separate `Baz::Bar`).
const ownerNameNode =
nameNode.type === 'qualified_identifier'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🔴 P1 (reproduced) + 🟠 P2 (reproduced) — the bare-tail owner key is the wrong lever.

P1: two out-of-line scoped types sharing a tail in the same file collide. Reproduced — struct Outer::Inner{…} + struct Other::Inner{…} → one Struct:file:Inner node; from_outer AND from_other both own through it (from_other mis-attributed, Other::Inner never created); same-named members merge into one method node. DANGLING:0, so findDanglingEdges can't catch it. BASE kept them distinct → a regression to silent corruption for this case.

P2: childForFieldName('name') is a single hop — struct A::B::C{…} reduces to the middle B::C, not tail C, so it still dangles (Struct:file:B::C MISSING).

Fix: key the owner by the full qualified path (resolve to the type's qualifiedName, e.g. Outer.Inner / A.B.C), not the bare tail — the same full-key decision made for Ruby scope_resolution. [reproduced]

// method owns through the real node instead of a `path::Type` id that was
// never materialized (#1975). Mirrors the qualified_identifier handling
// above and rust.ts:extractOwnerName.
const implTargetName = (n: SyntaxNode): string =>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🔴 P1 (reproduced, Rust analogue) + 🟡 comment nit.

Collision: same bare-tail issue in Rust — impl a::Inner{…} + impl b::Inner{…} in one file → one Impl:file:Inner node, the second impl's methods mis-attributed (adversarial reproduced; BASE emitted no false owner). Key by the full module-qualified path instead. [reproduced]

Nit: this comment says it "Mirrors the qualified_identifier handling above" — that handling is below (the generic nameNode block); and rust.ts:extractOwnerName is actually method-extractors/configs/rust.ts:extractOwnerName, which was not updated (still returns full text — harmless today, metadata-only, but the NOTE implies a synchrony that no longer holds). [code-read]

…ly (#1975)

The self-tri-review of PR #1977 (review 4411683756) found — and reproduced —
that the C++/Rust tail-reduction in findEnclosingClassInfo collides same-tail
types declared in the same file (struct Outer::Inner + struct Other::Inner ->
one Struct:Inner node, methods silently mis-attributed; same-named members
merge). Root cause is pre-existing: GitNexus keys nested-type nodes by their
tail name within a file, so even plain inline same-tail nested types already
merge. A correct fix needs fully-qualified nested-type node identity — a broad
change deferred to #1978.

This reverts the C++ (qualified_identifier) and Rust (scoped_type_identifier
impl) owner reductions in ast-helpers.ts, the Rust @definition.impl scoped arm,
and the cpp/rust fixtures+tests+golden+bench entries. The Ruby fix is unaffected
(it keys the node by the full scoped text — no collision) and stays:
namespaced class/module node materialization + the cross-namespace collision test.

Validated Ruby-only: 136/136 both legs; ruby+rust captures goldens 19/19;
bench --check PASS (14 langs); tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@magyargergo magyargergo changed the title fix(ingestion): materialize graph nodes for scoped class/module/impl declarations (#1975) fix(ingestion): materialize graph nodes for Ruby namespaced class/module declarations (#1975) Jun 2, 2026
…ship (#1975)

Re-introduces the C++/Rust fix the tri-review reverted, using a collision-safe
approach instead of owner tail-reduction (which merged same-tail types in one
file). Key the scoped DECLARATION's node by its full qualified text so it
matches the owner id and stays distinct from a same-tail type elsewhere:

- C++: widen the legacy structure query to materialize a node for out-of-line
  defs (class/struct Outer::Inner — name is qualified_identifier), keyed by the
  full text. No findEnclosingClassInfo change needed — BASE already derives the
  full-text owner, which now matches. Outer::Inner and Other::Inner stay
  distinct; 3-level A::B::C resolves. (A redundant forward-decl node remains.)
- Rust: @definition.impl arm for scoped inherent impls (keyed full) +
  findEnclosingClassInfo inherent-impl branch accepts scoped_type_identifier
  with full text. impl a::Inner and impl b::Inner stay distinct.

Collision-aware fixtures + positive owner-identity assertions (per the
tri-review) replace the single-type fixtures. Deferred to #1978: Rust trait
impls on a scoped struct path (impl T for a::Inner) and the pre-existing inline
same-tail node collision — both need qualified struct-node identity.

Validated: Ruby 136/136, C++/Rust 434/434 both legs (371+63-skip legacy);
ruby+rust captures goldens 19/19 (additive); bench --check PASS (14 langs);
tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@magyargergo magyargergo changed the title fix(ingestion): materialize graph nodes for Ruby namespaced class/module declarations (#1975) fix(ingestion): materialize graph nodes for scoped class/module/impl declarations (#1975) Jun 2, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@magyargergo magyargergo merged commit 5f0d690 into main Jun 2, 2026
31 checks passed
@magyargergo magyargergo deleted the fix/scoped-declaration-graph-nodes branch June 2, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: scoped class/module/impl declarations don't materialize a graph node (dangling HAS_METHOD edges) — PR #1972 P2 follow-up

1 participant