fix(ingestion): use Class fallback for unresolved Ruby mixin owners, not Struct

Ruby `include`/`extend`/`prepend` records share a processHeritageFromExtracted
branch with Rust `trait-impl`. When the mixin child name cannot be resolved
through the type registry, the fallback synthesized a `Struct:filepath:X` id
for both. Rust structs emit under the `Struct` label -- correct. Ruby classes
emit under `Class` and Ruby modules emit under `Trait` (since plan 001's
module relabel), so the synthesized `Struct:` id would never match any real
node and the IMPLEMENTS edge would dangle off a phantom.

The resolved case -- the common path now that Ruby modules register
normally -- is unaffected; only the fallback for truly unresolved references
changes. `Class` is the right default for Ruby mixin kinds because the
dominant shape is `class X; include M; end` where X is a Class.

Split is kind-aware: Rust `trait-impl` keeps `Struct`, Ruby mixin kinds get
`Class`. Applied at both call sites (processHeritageFromExtractedItem +
processHeritageFromExtracted).
This commit is contained in:
Gergo Magyar 2026-04-17 11:18:31 +01:00
parent fbac43815b
commit b6afb4bbef

View file

@ -19,7 +19,7 @@ import { ASTCache } from './ast-cache.js';
import Parser from 'tree-sitter';
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, type SupportedLanguages } from 'gitnexus-shared';
import { getLanguageFromFilename, type NodeLabel, type SupportedLanguages } from 'gitnexus-shared';
import { isVerboseIngestionEnabled } from './utils/verbose.js';
import { yieldToEventLoop } from './utils/event-loop.js';
import { getProvider } from './languages/index.js';
@ -149,11 +149,21 @@ const resolveAndAddHeritageEdge = (
item.kind === 'extend' ||
item.kind === 'prepend'
) {
// Fallback label for an unresolved child name. Rust `trait-impl` children
// are structs; Ruby mixin children are classes or modules (Trait). For
// Ruby mixin kinds the common case resolves through the type registry
// post-plan-001, so the fallback only fires for true-unresolved references
// (e.g. mixin inside a singleton_class). `Class` is strictly better than
// `Struct` there because it matches the label the structure phase would
// emit for a Ruby `class` — the dominant shape. Ruby modules that fail
// to resolve still lose their `Trait` label in the synthesized id, but
// they fail to resolve rarely and the tradeoff is documented.
const childFallbackLabel: NodeLabel = item.kind === 'trait-impl' ? 'Struct' : 'Class';
const strct = resolveHeritageId(
item.className,
filePath,
ctx,
'Struct',
childFallbackLabel,
`${filePath}:${item.className}`,
);
const trait = resolveHeritageId(item.parentName, filePath, ctx, 'Trait');
@ -339,11 +349,15 @@ export const processHeritageFromExtracted = async (
h.kind === 'extend' ||
h.kind === 'prepend'
) {
// See the per-item call above (processHeritageFromExtractedItem) for
// rationale: `Class` is the correct fallback for Ruby mixin kinds,
// `Struct` stays the Rust `trait-impl` default.
const childFallbackLabel: NodeLabel = h.kind === 'trait-impl' ? 'Struct' : 'Class';
const strct = resolveHeritageId(
h.className,
h.filePath,
ctx,
'Struct',
childFallbackLabel,
`${h.filePath}:${h.className}`,
);
const trait = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');