gitnexus 1.6.2-rc.21 → 1.6.2-rc.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/_shared/mro-strategy.d.ts +38 -16
  2. package/dist/_shared/mro-strategy.d.ts.map +1 -1
  3. package/dist/core/ingestion/call-processor.d.ts +1 -1
  4. package/dist/core/ingestion/call-processor.js +172 -42
  5. package/dist/core/ingestion/call-routing.d.ts +8 -12
  6. package/dist/core/ingestion/call-routing.js +13 -34
  7. package/dist/core/ingestion/call-types.d.ts +75 -0
  8. package/dist/core/ingestion/heritage-extractors/configs/go.d.ts +13 -0
  9. package/dist/core/ingestion/heritage-extractors/configs/go.js +20 -0
  10. package/dist/core/ingestion/heritage-extractors/configs/ruby.d.ts +18 -0
  11. package/dist/core/ingestion/heritage-extractors/configs/ruby.js +65 -0
  12. package/dist/core/ingestion/heritage-extractors/generic.d.ts +23 -0
  13. package/dist/core/ingestion/heritage-extractors/generic.js +47 -0
  14. package/dist/core/ingestion/heritage-processor.d.ts +9 -0
  15. package/dist/core/ingestion/heritage-processor.js +120 -85
  16. package/dist/core/ingestion/heritage-types.d.ts +73 -0
  17. package/dist/core/ingestion/heritage-types.js +2 -0
  18. package/dist/core/ingestion/language-provider.d.ts +69 -1
  19. package/dist/core/ingestion/languages/c-cpp.js +3 -0
  20. package/dist/core/ingestion/languages/csharp.js +2 -0
  21. package/dist/core/ingestion/languages/dart.js +2 -0
  22. package/dist/core/ingestion/languages/go.js +3 -0
  23. package/dist/core/ingestion/languages/java.js +2 -0
  24. package/dist/core/ingestion/languages/kotlin.js +2 -0
  25. package/dist/core/ingestion/languages/php.js +2 -0
  26. package/dist/core/ingestion/languages/python.js +2 -0
  27. package/dist/core/ingestion/languages/ruby.js +92 -15
  28. package/dist/core/ingestion/languages/rust.js +2 -0
  29. package/dist/core/ingestion/languages/swift.js +2 -0
  30. package/dist/core/ingestion/languages/typescript.js +3 -0
  31. package/dist/core/ingestion/languages/vue.js +2 -0
  32. package/dist/core/ingestion/model/heritage-map.d.ts +35 -0
  33. package/dist/core/ingestion/model/heritage-map.js +110 -9
  34. package/dist/core/ingestion/model/resolve.d.ts +30 -28
  35. package/dist/core/ingestion/model/resolve.js +105 -25
  36. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +1 -0
  37. package/dist/core/ingestion/pipeline-phases/parse-impl.js +9 -3
  38. package/dist/core/ingestion/pipeline-phases/parse.d.ts +7 -0
  39. package/dist/core/ingestion/pipeline.d.ts +11 -0
  40. package/dist/core/ingestion/pipeline.js +9 -2
  41. package/dist/core/ingestion/utils/ast-helpers.js +19 -2
  42. package/dist/core/ingestion/utils/ruby-self-call.d.ts +52 -0
  43. package/dist/core/ingestion/utils/ruby-self-call.js +59 -0
  44. package/dist/core/ingestion/workers/parse-worker.js +57 -60
  45. package/dist/types/pipeline.d.ts +6 -0
  46. package/package.json +1 -1
@@ -27,6 +27,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
27
27
  import { dartVariableConfig } from '../variable-extractors/configs/dart.js';
28
28
  import { createCallExtractor } from '../call-extractors/generic.js';
29
29
  import { dartCallConfig } from '../call-extractors/configs/dart.js';
30
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
30
31
  /**
31
32
  * Resolve the enclosing function from a `function_body` node by looking at its
32
33
  * previous sibling. In Dart's tree-sitter grammar, function_signature and
@@ -95,6 +96,7 @@ export const dartProvider = defineLanguage({
95
96
  methodExtractor: createMethodExtractor(dartMethodConfig),
96
97
  variableExtractor: createVariableExtractor(dartVariableConfig),
97
98
  classExtractor: createClassExtractor(dartClassConfig),
99
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.Dart),
98
100
  enclosingFunctionFinder: dartEnclosingFunctionFinder,
99
101
  builtInNames: BUILT_INS,
100
102
  });
@@ -25,6 +25,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
25
25
  import { goVariableConfig } from '../variable-extractors/configs/go.js';
26
26
  import { createCallExtractor } from '../call-extractors/generic.js';
27
27
  import { goCallConfig } from '../call-extractors/configs/go.js';
28
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
29
+ import { goHeritageConfig } from '../heritage-extractors/configs/go.js';
28
30
  export const goProvider = defineLanguage({
29
31
  id: SupportedLanguages.Go,
30
32
  extensions: ['.go'],
@@ -38,4 +40,5 @@ export const goProvider = defineLanguage({
38
40
  methodExtractor: createMethodExtractor(goMethodConfig),
39
41
  variableExtractor: createVariableExtractor(goVariableConfig),
40
42
  classExtractor: createClassExtractor(goClassConfig),
43
+ heritageExtractor: createHeritageExtractor(goHeritageConfig),
41
44
  });
@@ -24,6 +24,7 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
24
24
  import { javaMethodConfig } from '../method-extractors/configs/jvm.js';
25
25
  import { createVariableExtractor } from '../variable-extractors/generic.js';
26
26
  import { javaVariableConfig } from '../variable-extractors/configs/jvm.js';
27
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
27
28
  export const javaProvider = defineLanguage({
28
29
  id: SupportedLanguages.Java,
29
30
  extensions: ['.java'],
@@ -39,4 +40,5 @@ export const javaProvider = defineLanguage({
39
40
  methodExtractor: createMethodExtractor(javaMethodConfig),
40
41
  variableExtractor: createVariableExtractor(javaVariableConfig),
41
42
  classExtractor: createClassExtractor(javaClassConfig),
43
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.Java),
42
44
  });
@@ -25,6 +25,7 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
25
25
  import { kotlinMethodConfig } from '../method-extractors/configs/jvm.js';
26
26
  import { createVariableExtractor } from '../variable-extractors/generic.js';
27
27
  import { kotlinVariableConfig } from '../variable-extractors/configs/jvm.js';
28
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
28
29
  /** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
29
30
  * Kotlin grammar uses function_declaration for both top-level functions and class methods.
30
31
  * Returns true when the captured definition node has a class_body ancestor. */
@@ -110,6 +111,7 @@ export const kotlinProvider = defineLanguage({
110
111
  methodExtractor: createMethodExtractor(kotlinMethodConfig),
111
112
  variableExtractor: createVariableExtractor(kotlinVariableConfig),
112
113
  classExtractor: createClassExtractor(kotlinClassConfig),
114
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.Kotlin),
113
115
  builtInNames: BUILT_INS,
114
116
  labelOverride: (functionNode, defaultLabel) => {
115
117
  if (defaultLabel !== 'Function')
@@ -24,6 +24,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
24
24
  import { phpVariableConfig } from '../variable-extractors/configs/php.js';
25
25
  import { createCallExtractor } from '../call-extractors/generic.js';
26
26
  import { phpCallConfig } from '../call-extractors/configs/php.js';
27
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
27
28
  const BUILT_INS = new Set([
28
29
  'echo',
29
30
  'isset',
@@ -232,6 +233,7 @@ export const phpProvider = defineLanguage({
232
233
  methodExtractor: createMethodExtractor(phpMethodConfig),
233
234
  variableExtractor: createVariableExtractor(phpVariableConfig),
234
235
  classExtractor: createClassExtractor(phpClassConfig),
236
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.PHP),
235
237
  descriptionExtractor: phpDescriptionExtractor,
236
238
  isRouteFile: isPhpRouteFile,
237
239
  builtInNames: BUILT_INS,
@@ -27,6 +27,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
27
27
  import { pythonVariableConfig } from '../variable-extractors/configs/python.js';
28
28
  import { createCallExtractor } from '../call-extractors/generic.js';
29
29
  import { pythonCallConfig } from '../call-extractors/configs/python.js';
30
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
30
31
  const BUILT_INS = new Set([
31
32
  'print',
32
33
  'len',
@@ -71,5 +72,6 @@ export const pythonProvider = defineLanguage({
71
72
  methodExtractor: createMethodExtractor(pythonMethodConfig),
72
73
  variableExtractor: createVariableExtractor(pythonVariableConfig),
73
74
  classExtractor: createClassExtractor(pythonClassConfig),
75
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.Python),
74
76
  builtInNames: BUILT_INS,
75
77
  });
@@ -24,6 +24,27 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
24
24
  import { rubyVariableConfig } from '../variable-extractors/configs/ruby.js';
25
25
  import { createCallExtractor } from '../call-extractors/generic.js';
26
26
  import { rubyCallConfig } from '../call-extractors/configs/ruby.js';
27
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
28
+ import { rubyHeritageConfig } from '../heritage-extractors/configs/ruby.js';
29
+ import { maybeRewriteRubyBareCallToSelf } from '../utils/ruby-self-call.js';
30
+ import { findEnclosingClassInfo } from '../utils/ast-helpers.js';
31
+ /**
32
+ * Ruby label override. Applied to:
33
+ * - `definition.module` captures in the structure phase — remaps to `Trait`
34
+ * so Ruby modules are registered in the class-like type registry and are
35
+ * therefore resolvable by `lookupClassByName` during mixin heritage
36
+ * resolution (`include`/`extend`/`prepend`).
37
+ * - `definition.function` captures — Ruby has no bare "function" construct
38
+ * (top-level `def` is a method on `main`); return the default so generic
39
+ * logic continues to apply.
40
+ *
41
+ * Returning `null` means "skip this definition"; we never do that here.
42
+ */
43
+ const rubyLabelOverride = (_node, defaultLabel) => {
44
+ if (defaultLabel === 'Module')
45
+ return 'Trait';
46
+ return defaultLabel;
47
+ };
27
48
  /** Ruby method/singleton_method: extract name from 'name' field, label as Method. */
28
49
  const rubyExtractFunctionName = (node) => {
29
50
  if (node.type !== 'method' && node.type !== 'singleton_method')
@@ -97,6 +118,26 @@ const BUILT_INS = new Set([
97
118
  'flatten',
98
119
  'uniq',
99
120
  ]);
121
+ /**
122
+ * Remaps `class << self` (singleton_class) to its enclosing class/module for
123
+ * receiver inference. A `singleton_class` node is not itself a type — walking
124
+ * up to the real owner lets `inferImplicitReceiver` set `hint='singleton'`.
125
+ * Returns null for orphaned singleton_class (no enclosing class/module found).
126
+ * All other container types are returned as-is.
127
+ */
128
+ const rubyResolveEnclosingOwner = (node) => {
129
+ if (node.type === 'singleton_class') {
130
+ let ancestor = node.parent;
131
+ while (ancestor) {
132
+ if (ancestor.type === 'class' || ancestor.type === 'module') {
133
+ return ancestor;
134
+ }
135
+ ancestor = ancestor.parent;
136
+ }
137
+ return null; // no enclosing class/module — skip
138
+ }
139
+ return node; // use as-is for all other container types
140
+ };
100
141
  export const rubyProvider = defineLanguage({
101
142
  id: SupportedLanguages.Ruby,
102
143
  extensions: ['.rb', '.rake', '.gemspec'],
@@ -107,21 +148,7 @@ export const rubyProvider = defineLanguage({
107
148
  callRouter: routeRubyCall,
108
149
  importSemantics: 'wildcard-leaf',
109
150
  callExtractor: createCallExtractor(rubyCallConfig),
110
- resolveEnclosingOwner(node) {
111
- // Ruby singleton_class (class << self) should resolve to the enclosing
112
- // class or module for owner/container resolution (HAS_METHOD edges, class IDs).
113
- if (node.type === 'singleton_class') {
114
- let ancestor = node.parent;
115
- while (ancestor) {
116
- if (ancestor.type === 'class' || ancestor.type === 'module') {
117
- return ancestor;
118
- }
119
- ancestor = ancestor.parent;
120
- }
121
- return null; // no enclosing class/module — skip
122
- }
123
- return node; // use as-is for all other container types
124
- },
151
+ resolveEnclosingOwner: rubyResolveEnclosingOwner,
125
152
  fieldExtractor: createFieldExtractor(rubyFieldConfig),
126
153
  methodExtractor: createMethodExtractor({
127
154
  ...rubyMethodConfig,
@@ -129,5 +156,55 @@ export const rubyProvider = defineLanguage({
129
156
  }),
130
157
  variableExtractor: createVariableExtractor(rubyVariableConfig),
131
158
  classExtractor: createClassExtractor(rubyClassConfig),
159
+ heritageExtractor: createHeritageExtractor(rubyHeritageConfig),
160
+ labelOverride: rubyLabelOverride,
161
+ // Ruby MRO is kind-aware: prepend providers beat the class's own method,
162
+ // which in turn beats include providers. See `lookupMethodByOwnerWithMRO`
163
+ // in `model/resolve.ts` for the walk order.
164
+ mroStrategy: 'ruby-mixin',
165
+ // ── DAG hooks ────────────────────────────────────────────────────
166
+ //
167
+ // DAG stage 3: rewrite bare calls (e.g. `serialize` in Account#call_serialize)
168
+ // as `self.serialize` so they route through owner-scoped MRO instead of
169
+ // global free-call lookup. `dispatchKind` goes into `hint` for stage 4.
170
+ inferImplicitReceiver: ({ calledName, callForm, receiverName, receiverTypeName, callNode, filePath, }) => {
171
+ // Only fire when no receiver has been resolved already.
172
+ if (receiverName || receiverTypeName)
173
+ return null;
174
+ const enclosing = findEnclosingClassInfo(callNode, filePath, rubyResolveEnclosingOwner);
175
+ const rewrite = maybeRewriteRubyBareCallToSelf(calledName, callForm, callNode, enclosing?.className ?? null, { isBuiltInName: (n) => BUILT_INS.has(n), mroStrategy: 'ruby-mixin' });
176
+ if (!rewrite)
177
+ return null;
178
+ return {
179
+ callForm: rewrite.callForm,
180
+ receiverName: rewrite.receiverName,
181
+ receiverTypeName: rewrite.receiverTypeName,
182
+ receiverSource: 'implicit-self',
183
+ hint: rewrite.dispatchKind, // 'instance' | 'singleton'
184
+ };
185
+ },
186
+ // DAG stage 4: two Ruby dispatch overrides —
187
+ // implicit-self: MRO walk first, fallback to free-arity-narrowed on miss.
188
+ // class-as-receiver: singleton ancestry (extend providers only); miss null-routes.
189
+ selectDispatch: ({ receiverSource, hint }) => {
190
+ if (receiverSource === 'implicit-self') {
191
+ // hint='instance' → instance ancestry (prepend→direct→include, see mro-strategy.ts § 'ruby-mixin')
192
+ // hint='singleton' → singleton ancestry (extend providers only; miss null-routes)
193
+ const ancestryView = hint === 'singleton' ? 'singleton' : 'instance';
194
+ return {
195
+ primary: 'owner-scoped',
196
+ fallback: 'free-arity-narrowed',
197
+ ancestryView,
198
+ };
199
+ }
200
+ if (receiverSource === 'class-as-receiver') {
201
+ // Class constant receiver (e.g. Account.log): singleton ancestry only; miss null-routes.
202
+ return {
203
+ primary: 'owner-scoped',
204
+ ancestryView: 'singleton',
205
+ };
206
+ }
207
+ return null;
208
+ },
132
209
  builtInNames: BUILT_INS,
133
210
  });
@@ -27,6 +27,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
27
27
  import { rustVariableConfig } from '../variable-extractors/configs/rust.js';
28
28
  import { createCallExtractor } from '../call-extractors/generic.js';
29
29
  import { rustCallConfig } from '../call-extractors/configs/rust.js';
30
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
30
31
  /** Rust impl_item: find the function_item child and extract its name as a Method. */
31
32
  const rustExtractFunctionName = (node) => {
32
33
  if (node.type !== 'impl_item')
@@ -126,5 +127,6 @@ export const rustProvider = defineLanguage({
126
127
  }),
127
128
  variableExtractor: createVariableExtractor(rustVariableConfig),
128
129
  classExtractor: createClassExtractor(rustClassConfig),
130
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.Rust),
129
131
  builtInNames: BUILT_INS,
130
132
  });
@@ -26,6 +26,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
26
26
  import { swiftVariableConfig } from '../variable-extractors/configs/swift.js';
27
27
  import { createCallExtractor } from '../call-extractors/generic.js';
28
28
  import { swiftCallConfig } from '../call-extractors/configs/swift.js';
29
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
29
30
  /**
30
31
  * Group Swift files by SPM target for implicit module visibility.
31
32
  * If SwiftPackageConfig is available, use target -> directory mappings.
@@ -237,6 +238,7 @@ export const swiftProvider = defineLanguage({
237
238
  }),
238
239
  variableExtractor: createVariableExtractor(swiftVariableConfig),
239
240
  classExtractor: createClassExtractor(swiftClassConfig),
241
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.Swift),
240
242
  implicitImportWirer: wireSwiftImplicitImports,
241
243
  builtInNames: BUILT_INS,
242
244
  });
@@ -25,6 +25,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
25
25
  import { typescriptVariableConfig, javascriptVariableConfig, } from '../variable-extractors/configs/typescript-javascript.js';
26
26
  import { createCallExtractor } from '../call-extractors/generic.js';
27
27
  import { typescriptCallConfig, javascriptCallConfig, } from '../call-extractors/configs/typescript-javascript.js';
28
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
28
29
  /**
29
30
  * TypeScript/JavaScript: arrow_function and function_expression get their name
30
31
  * from the parent variable_declarator (e.g. `const foo = () => {}`).
@@ -159,6 +160,7 @@ export const typescriptProvider = defineLanguage({
159
160
  }),
160
161
  variableExtractor: createVariableExtractor(typescriptVariableConfig),
161
162
  classExtractor: createClassExtractor(typescriptClassConfig),
163
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.TypeScript),
162
164
  builtInNames: BUILT_INS,
163
165
  });
164
166
  export const javascriptProvider = defineLanguage({
@@ -177,5 +179,6 @@ export const javascriptProvider = defineLanguage({
177
179
  }),
178
180
  variableExtractor: createVariableExtractor(javascriptVariableConfig),
179
181
  classExtractor: createClassExtractor(javascriptClassConfig),
182
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.JavaScript),
180
183
  builtInNames: BUILT_INS,
181
184
  });
@@ -26,6 +26,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
26
26
  import { typescriptVariableConfig } from '../variable-extractors/configs/typescript-javascript.js';
27
27
  import { createCallExtractor } from '../call-extractors/generic.js';
28
28
  import { typescriptCallConfig } from '../call-extractors/configs/typescript-javascript.js';
29
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
29
30
  const VUE_SPECIFIC_BUILT_INS = [
30
31
  'ref',
31
32
  'reactive',
@@ -71,5 +72,6 @@ export const vueProvider = defineLanguage({
71
72
  fieldExtractor: typescriptFieldExtractor,
72
73
  variableExtractor: createVariableExtractor(typescriptVariableConfig),
73
74
  classExtractor: vueClassExtractor,
75
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.TypeScript),
74
76
  builtInNames: VUE_BUILT_INS,
75
77
  });
@@ -43,11 +43,46 @@ export declare const resolveExtendsType: (parentName: string, currentFilePath: s
43
43
  type: "EXTENDS" | "IMPLEMENTS";
44
44
  idPrefix: string;
45
45
  };
46
+ /**
47
+ * Direct parent entry with the heritage kind that produced it. Preserved
48
+ * so kind-aware consumers (Ruby MRO, see `lookupMethodByOwnerWithMRO`) can
49
+ * walk prepend/include providers in the correct order. Flat-string consumers
50
+ * use `getParents` / `getAncestors` and see only the parent nodeIds.
51
+ */
52
+ export interface ParentEntry {
53
+ readonly parentId: string;
54
+ /** 'extends' | 'implements' | 'trait-impl' | 'include' | 'extend' | 'prepend' */
55
+ readonly kind: string;
56
+ }
46
57
  export interface HeritageMap {
47
58
  /** Direct parents of `childNodeId` (extends + implements + trait-impl). */
48
59
  getParents(childNodeId: string): string[];
49
60
  /** Full ancestor chain (BFS, bounded depth, cycle-safe). */
50
61
  getAncestors(childNodeId: string): string[];
62
+ /**
63
+ * Direct parents with heritage kind preserved, insertion-ordered. Used by
64
+ * kind-aware consumers (Ruby MRO) that need to distinguish prepend /
65
+ * include / extend / extends for walk-order decisions.
66
+ *
67
+ * Insertion order mirrors the order `ExtractedHeritage` records were fed
68
+ * into `buildHeritageMap`, which in turn mirrors tree-sitter match order.
69
+ * For Ruby, this matches source declaration order for `prepend` / `include`
70
+ * statements — the MRO walk reverses this (last-declared-first) at the
71
+ * consumer side.
72
+ */
73
+ getParentEntries(childNodeId: string): readonly ParentEntry[];
74
+ /**
75
+ * Ordered ancestry for instance method dispatch (Ruby-aware): includes
76
+ * `extends`, `implements`, `trait-impl`, `include`, `prepend` kinds.
77
+ * Excludes `extend` (singleton-only). Order is caller-determined in Unit 3.
78
+ * For non-Ruby callers (first-wins, c3, etc.), this matches `getAncestors`.
79
+ */
80
+ getInstanceAncestry(childNodeId: string): readonly ParentEntry[];
81
+ /**
82
+ * Ordered ancestry for singleton / class-method dispatch (Ruby-aware):
83
+ * only `extend` kind parents. For non-Ruby languages this is always empty.
84
+ */
85
+ getSingletonAncestry(childNodeId: string): readonly ParentEntry[];
51
86
  /**
52
87
  * File paths of classes that directly implement or extend-as-interface the
53
88
  * given interface/abstract-class **name**. Replaces the standalone
@@ -64,8 +64,12 @@ const DEFAULT_HERITAGE_STRATEGY = { defaultEdge: 'EXTENDS' };
64
64
  * paths) used by interface-dispatch in call resolution.
65
65
  */
66
66
  export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
67
- // childNodeId → Set<parentNodeId> (Set to deduplicate cross-chunk duplicates)
67
+ // childNodeId → insertion-ordered array of { parentId, kind }.
68
+ // Ordered array (not Set) because Ruby MRO walk depends on declaration
69
+ // order. A parallel `seen` map dedupes `(parentId, kind)` pairs without
70
+ // losing order.
68
71
  const directParents = new Map();
72
+ const seenParents = new Map();
69
73
  // interfaceName → Set<filePath> (implementor lookup for interface dispatch)
70
74
  const implementorFiles = new Map();
71
75
  for (const h of heritage) {
@@ -80,10 +84,23 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
80
84
  continue;
81
85
  let parents = directParents.get(child.nodeId);
82
86
  if (!parents) {
83
- parents = new Set();
87
+ parents = [];
84
88
  directParents.set(child.nodeId, parents);
85
89
  }
86
- parents.add(parent.nodeId);
90
+ let seen = seenParents.get(child.nodeId);
91
+ if (!seen) {
92
+ seen = new Set();
93
+ seenParents.set(child.nodeId, seen);
94
+ }
95
+ // Dedup by `parentId + kind` so the same parent under two different
96
+ // kinds (e.g. a module that is both included and prepended — legal
97
+ // Ruby though unusual) is recorded twice; the consumer needs both
98
+ // kinds in the walk. A single (parent, kind) pair is deduped.
99
+ const key = `${parent.nodeId}|${h.kind}`;
100
+ if (!seen.has(key)) {
101
+ seen.add(key);
102
+ parents.push({ parentId: parent.nodeId, kind: h.kind });
103
+ }
87
104
  }
88
105
  }
89
106
  }
@@ -120,9 +137,28 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
120
137
  }
121
138
  }
122
139
  // --- Public API ---------------------------------------------------
140
+ /** Internal helper: return the entries array (may be undefined). */
141
+ const entriesFor = (nodeId) => directParents.get(nodeId);
142
+ const getParentEntries = (childNodeId) => {
143
+ const entries = entriesFor(childNodeId);
144
+ return entries ?? [];
145
+ };
123
146
  const getParents = (childNodeId) => {
124
- const parents = directParents.get(childNodeId);
125
- return parents ? [...parents] : [];
147
+ const entries = entriesFor(childNodeId);
148
+ if (!entries)
149
+ return [];
150
+ // Deduplicate parent ids across kinds so the flat-string contract
151
+ // (used by non-Ruby MRO strategies and by the C3 linearizer) stays
152
+ // identical to its pre-kind-awareness behavior.
153
+ const out = [];
154
+ const seen = new Set();
155
+ for (const e of entries) {
156
+ if (!seen.has(e.parentId)) {
157
+ seen.add(e.parentId);
158
+ out.push(e.parentId);
159
+ }
160
+ }
161
+ return out;
126
162
  };
127
163
  const getAncestors = (childNodeId) => {
128
164
  const result = [];
@@ -139,11 +175,15 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
139
175
  visited.add(parentId);
140
176
  result.push(parentId);
141
177
  // Expand parent's own parents for next level
142
- const grandparents = directParents.get(parentId);
178
+ const grandparents = entriesFor(parentId);
143
179
  if (grandparents) {
180
+ const gpSeen = new Set();
144
181
  for (const gp of grandparents) {
145
- if (!visited.has(gp))
146
- nextFrontier.push(gp);
182
+ if (gpSeen.has(gp.parentId))
183
+ continue;
184
+ gpSeen.add(gp.parentId);
185
+ if (!visited.has(gp.parentId))
186
+ nextFrontier.push(gp.parentId);
147
187
  }
148
188
  }
149
189
  }
@@ -152,8 +192,69 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
152
192
  }
153
193
  return result;
154
194
  };
195
+ /**
196
+ * Lazy-computed per-owner split of direct parents into instance-dispatch
197
+ * (non-`extend`) and singleton-dispatch (`extend`-only) views. Memoized on
198
+ * first request so the `.filter()` pass happens at most once per owner per
199
+ * HeritageMap lifetime, not per call-site dispatch.
200
+ *
201
+ * Shared empty-array sentinels for owners with no entries in a given view
202
+ * avoid per-call allocation when the split is asymmetric (common Ruby case:
203
+ * a class has `include` but no `extend`, so its singleton view is empty).
204
+ */
205
+ const EMPTY_PARENT_ENTRIES = [];
206
+ const splitCache = new Map();
207
+ const splitForOwner = (childNodeId) => {
208
+ let cached = splitCache.get(childNodeId);
209
+ if (cached)
210
+ return cached;
211
+ const entries = entriesFor(childNodeId);
212
+ if (!entries || entries.length === 0) {
213
+ cached = { instance: EMPTY_PARENT_ENTRIES, singleton: EMPTY_PARENT_ENTRIES };
214
+ }
215
+ else {
216
+ const instance = [];
217
+ const singleton = [];
218
+ for (const e of entries) {
219
+ if (e.kind === 'extend')
220
+ singleton.push(e);
221
+ else
222
+ instance.push(e);
223
+ }
224
+ cached = {
225
+ instance: instance.length === 0 ? EMPTY_PARENT_ENTRIES : instance,
226
+ singleton: singleton.length === 0 ? EMPTY_PARENT_ENTRIES : singleton,
227
+ };
228
+ }
229
+ splitCache.set(childNodeId, cached);
230
+ return cached;
231
+ };
232
+ /**
233
+ * Instance-dispatch ancestry walk. Excludes `extend` (singleton-only).
234
+ * For kind-aware consumers (Ruby MRO): walks parents in source-insertion
235
+ * order. The consumer is responsible for interleaving self / reversing
236
+ * prepend order / etc. This method preserves raw declaration order.
237
+ *
238
+ * Result is cached per owner; repeat calls return the same array.
239
+ */
240
+ const getInstanceAncestry = (childNodeId) => splitForOwner(childNodeId).instance;
241
+ /**
242
+ * Singleton-dispatch ancestry walk. Only `extend` parents. For non-Ruby
243
+ * languages this is always empty (no language currently produces `extend`
244
+ * heritage records outside Ruby).
245
+ *
246
+ * Result is cached per owner; repeat calls return the same array.
247
+ */
248
+ const getSingletonAncestry = (childNodeId) => splitForOwner(childNodeId).singleton;
155
249
  const getImplementorFiles = (interfaceName) => {
156
250
  return implementorFiles.get(interfaceName) ?? EMPTY_SET;
157
251
  };
158
- return { getParents, getAncestors, getImplementorFiles };
252
+ return {
253
+ getParents,
254
+ getAncestors,
255
+ getParentEntries,
256
+ getInstanceAncestry,
257
+ getSingletonAncestry,
258
+ getImplementorFiles,
259
+ };
159
260
  };
@@ -28,32 +28,34 @@ declare function gatherAncestors(classId: string, parentMap: Map<string, string[
28
28
  export declare function c3Linearize(classId: string, parentMap: Map<string, string[]>, cache: Map<string, string[] | null>, inProgress?: Set<string>): string[] | null;
29
29
  export { gatherAncestors };
30
30
  /**
31
- * Look up a method on an owner class, walking the parent chain via HeritageMap
32
- * when the method isn't found on the direct owner.
33
- *
34
- * Respects the 5 per-language MRO strategies:
35
- * - `first-wins`: BFS ancestor walk, first match wins (default)
36
- * - `leftmost-base`: BFS ancestor walk, leftmost base in declaration order wins (C++);
37
- * HeritageMap preserves insertion order matching source declaration,
38
- * so BFS order is equivalent to leftmost-base semantics
39
- * - `c3`: C3-linearized ancestor order, first match wins (Python)
40
- * - `implements-split`: BFS ancestor walk, first match wins (Java/C#) —
41
- * full ambiguity detection for multiple interface defaults
42
- * is handled by computeMRO at graph level
43
- * - `qualified-syntax`: No auto-resolution (Rust) — returns undefined
44
- *
45
- * Uses the `c3Linearize` defined in this file (also consumed by
46
- * mro-processor.ts for graph-level MRO emission) for the `c3` strategy.
47
- *
48
- * Depends only on {@link SemanticModel} + {@link HeritageMap} + an
49
- * {@link MroStrategy} literal — NO dependency on SymbolTable, the language
50
- * registry, or resolution-context, which keeps the `model/` module free of
51
- * cross-layer imports. Callers derive the strategy from their language
52
- * provider before invoking this function.
53
- *
54
- * @internal This is the low-level MRO walker. Exported so call-processor's
55
- * higher-level resolvers (and unit tests) can invoke it directly. Callers
56
- * outside `core/ingestion/` should use the higher-level resolvers in
57
- * call-processor.ts instead of depending on this function.
31
+ * DAG stage 5 helper: look up a method on an owner class via MRO walk.
32
+ *
33
+ * Low-level resolver; no dependency on SymbolTable, language registry, or
34
+ * resolution-context (keeps model/ layer free of cross-layer imports).
35
+ * All strategies respect `argCount` for overload narrowing.
36
+ * `ancestryOverride` replaces the default walk; caller must compute it correctly.
37
+ *
38
+ * Strategy summary (full docs in gitnexus-shared/mro-strategy.ts):
39
+ * - `first-wins` / `leftmost-base` / `implements-split`: BFS, first match wins.
40
+ * - `c3`: C3-linearized order; falls back to BFS on cycle/inconsistency.
41
+ * - `qualified-syntax`: returns undefined immediately (Rust requires explicit syntax).
42
+ * - `ruby-mixin`: kind-aware walk see inline comments below.
43
+ *
44
+ * Internal API: exported for call-processor resolvers and tests.
45
+ * External callers should use resolveMemberCall instead.
46
+ *
47
+ * @see gitnexus-shared/mro-strategy.ts § 'ruby-mixin'
48
+ * @see call-processor.ts § resolveMemberCall
49
+ */
50
+ export declare const lookupMethodByOwnerWithMRO: (ownerNodeId: string, methodName: string, heritageMap: HeritageMap, model: SemanticModel, strategy: MroStrategy, argCount?: number,
51
+ /**
52
+ * Optional pre-computed ancestry list. When provided, overrides the default
53
+ * per-strategy ancestry source. Primarily used by Ruby singleton dispatch:
54
+ * the caller supplies `heritageMap.getSingletonAncestry(ownerNodeId)` as
55
+ * node-id array so this walker resolves against `extend` providers only.
56
+ *
57
+ * For `ruby-mixin` strategy, passing an override switches the walker into
58
+ * a no-prepend-no-direct linear scan (the caller has already decided the
59
+ * order), which is the correct semantics for singleton dispatch.
58
60
  */
59
- export declare const lookupMethodByOwnerWithMRO: (ownerNodeId: string, methodName: string, heritageMap: HeritageMap, model: SemanticModel, strategy: MroStrategy, argCount?: number) => SymbolDefinition | undefined;
61
+ ancestryOverride?: readonly string[]) => SymbolDefinition | undefined;