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
@@ -0,0 +1,18 @@
1
+ import type { HeritageExtractionConfig } from '../../heritage-types.js';
2
+ /**
3
+ * Ruby heritage extraction config.
4
+ *
5
+ * Ruby expresses inheritance in two ways, and only one of them has
6
+ * dedicated tree-sitter heritage captures:
7
+ *
8
+ * 1. Class inheritance (`class A < B`) produces standard
9
+ * `@heritage.extends` captures and flows through the generic
10
+ * capture-based `extract` hook (not defined here — the factory
11
+ * handles it).
12
+ * 2. Mixin calls (`include`/`extend`/`prepend`) have no dedicated
13
+ * heritage captures; they surface as ordinary call sites. The
14
+ * `callBasedHeritage` hook below intercepts them before the call
15
+ * router, absorbing the mixin routing logic that previously lived
16
+ * in call-routing.ts (routeRubyCall).
17
+ */
18
+ export declare const rubyHeritageConfig: HeritageExtractionConfig;
@@ -0,0 +1,65 @@
1
+ // gitnexus/src/core/ingestion/heritage-extractors/configs/ruby.ts
2
+ import { SupportedLanguages } from '../../../../_shared/index.js';
3
+ /**
4
+ * Maximum parent depth for enclosing class/module walk.
5
+ * Prevents runaway walks on malformed/deeply-nested ASTs.
6
+ */
7
+ const MAX_PARENT_DEPTH = 50;
8
+ /**
9
+ * Walk up the AST from a call node to find the enclosing class or module name.
10
+ * Ruby include/extend/prepend calls must be inside a class or module body.
11
+ */
12
+ function findEnclosingClassName(callNode) {
13
+ let current = callNode.parent;
14
+ let depth = 0;
15
+ while (current && ++depth <= MAX_PARENT_DEPTH) {
16
+ if (current.type === 'class' || current.type === 'module') {
17
+ const nameNode = current.childForFieldName?.('name');
18
+ if (nameNode)
19
+ return nameNode.text;
20
+ }
21
+ current = current.parent;
22
+ }
23
+ return null;
24
+ }
25
+ /** Ruby heritage call names that express mixin inclusion. */
26
+ const RUBY_HERITAGE_CALL_NAMES = new Set(['include', 'extend', 'prepend']);
27
+ /**
28
+ * Ruby heritage extraction config.
29
+ *
30
+ * Ruby expresses inheritance in two ways, and only one of them has
31
+ * dedicated tree-sitter heritage captures:
32
+ *
33
+ * 1. Class inheritance (`class A < B`) produces standard
34
+ * `@heritage.extends` captures and flows through the generic
35
+ * capture-based `extract` hook (not defined here — the factory
36
+ * handles it).
37
+ * 2. Mixin calls (`include`/`extend`/`prepend`) have no dedicated
38
+ * heritage captures; they surface as ordinary call sites. The
39
+ * `callBasedHeritage` hook below intercepts them before the call
40
+ * router, absorbing the mixin routing logic that previously lived
41
+ * in call-routing.ts (routeRubyCall).
42
+ */
43
+ export const rubyHeritageConfig = {
44
+ language: SupportedLanguages.Ruby,
45
+ callBasedHeritage: {
46
+ callNames: RUBY_HERITAGE_CALL_NAMES,
47
+ extract(calledName, callNode, _filePath) {
48
+ const enclosingClass = findEnclosingClassName(callNode);
49
+ if (!enclosingClass)
50
+ return [];
51
+ const results = [];
52
+ const argList = callNode.childForFieldName?.('arguments');
53
+ for (const arg of argList?.children ?? []) {
54
+ if (arg.type === 'constant' || arg.type === 'scope_resolution') {
55
+ results.push({
56
+ className: enclosingClass,
57
+ parentName: arg.text,
58
+ kind: calledName, // 'include' | 'extend' | 'prepend'
59
+ });
60
+ }
61
+ }
62
+ return results;
63
+ },
64
+ },
65
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Generic table-driven heritage extractor factory.
3
+ *
4
+ * Follows the same config+factory pattern as method-extractors/generic.ts,
5
+ * field-extractors/generic.ts, call-extractors/generic.ts, and
6
+ * variable-extractors/generic.ts.
7
+ *
8
+ * Languages with custom extraction hooks (Go: shouldSkipExtends, Ruby:
9
+ * callBasedHeritage) pass a full HeritageExtractionConfig. Languages
10
+ * that use the default capture-based extraction can pass just the
11
+ * SupportedLanguages enum value — no per-language config file needed.
12
+ */
13
+ import type { SupportedLanguages } from '../../../_shared/index.js';
14
+ import type { HeritageExtractionConfig, HeritageExtractor } from '../heritage-types.js';
15
+ /**
16
+ * Create a HeritageExtractor from a declarative config or a language enum.
17
+ *
18
+ * When a full HeritageExtractionConfig is provided, custom hooks
19
+ * (shouldSkipExtends, callBasedHeritage) drive the extraction.
20
+ * When only a SupportedLanguages value is provided, the factory produces
21
+ * a default extractor that handles the standard @heritage.* captures.
22
+ */
23
+ export declare function createHeritageExtractor(config: HeritageExtractionConfig | SupportedLanguages): HeritageExtractor;
@@ -0,0 +1,47 @@
1
+ // gitnexus/src/core/ingestion/heritage-extractors/generic.ts
2
+ /**
3
+ * Create a HeritageExtractor from a declarative config or a language enum.
4
+ *
5
+ * When a full HeritageExtractionConfig is provided, custom hooks
6
+ * (shouldSkipExtends, callBasedHeritage) drive the extraction.
7
+ * When only a SupportedLanguages value is provided, the factory produces
8
+ * a default extractor that handles the standard @heritage.* captures.
9
+ */
10
+ export function createHeritageExtractor(config) {
11
+ const actualConfig = typeof config === 'string' ? { language: config } : config;
12
+ const callNameSet = actualConfig.callBasedHeritage?.callNames;
13
+ return {
14
+ language: actualConfig.language,
15
+ extract(captureMap, context) {
16
+ const classNode = captureMap['heritage.class'];
17
+ if (!classNode)
18
+ return [];
19
+ const className = classNode.text;
20
+ const results = [];
21
+ const extendsNode = captureMap['heritage.extends'];
22
+ if (extendsNode) {
23
+ if (!actualConfig.shouldSkipExtends?.(extendsNode)) {
24
+ results.push({ className, parentName: extendsNode.text, kind: 'extends' });
25
+ }
26
+ }
27
+ const implementsNode = captureMap['heritage.implements'];
28
+ if (implementsNode) {
29
+ results.push({ className, parentName: implementsNode.text, kind: 'implements' });
30
+ }
31
+ const traitNode = captureMap['heritage.trait'];
32
+ if (traitNode) {
33
+ results.push({ className, parentName: traitNode.text, kind: 'trait-impl' });
34
+ }
35
+ return results;
36
+ },
37
+ ...(callNameSet
38
+ ? {
39
+ extractFromCall(calledName, callNode, context) {
40
+ if (!callNameSet.has(calledName))
41
+ return null;
42
+ return actualConfig.callBasedHeritage.extract(calledName, callNode, context.filePath);
43
+ },
44
+ }
45
+ : {}),
46
+ };
47
+ }
@@ -38,6 +38,15 @@ export declare const processHeritageFromExtracted: (graph: KnowledgeGraph, extra
38
38
  * {@link ExtractedHeritage} rows without mutating the graph. Used on the
39
39
  * sequential pipeline path so `buildHeritageMap(..., ctx)` can run before
40
40
  * `processCalls` (worker path defers calls until heritage from all chunks exists).
41
+ *
42
+ * This prepass extracts BOTH capture-based heritage (`@heritage.*` — extends /
43
+ * implements / trait-impl) AND call-based heritage (`@call.name` routed through
44
+ * `heritageExtractor.extractFromCall` — Ruby `include` / `extend` / `prepend`).
45
+ * Without the second pass, sequential-mode `sequentialHeritageMap` would not
46
+ * know about Ruby mixin ancestry before `processCalls` resolves calls against
47
+ * it, silently dropping mixed-in methods from the graph. This function stays
48
+ * read-only — `processCalls` still owns emission of heritage graph edges via
49
+ * its `rubyHeritage` return path.
41
50
  */
42
51
  export declare function extractExtractedHeritageFromFiles(files: {
43
52
  path: string;
@@ -54,6 +54,69 @@ const resolveHeritageId = (name, filePath, ctx, fallbackLabel, fallbackKey) => {
54
54
  confidence: TIER_CONFIDENCE['global'],
55
55
  };
56
56
  };
57
+ /**
58
+ * Resolve a single HeritageInfo to a graph edge, using the same resolution
59
+ * logic as processHeritageFromExtracted. This bridges the heritage extractor
60
+ * output format to the graph-resolution side.
61
+ */
62
+ const resolveAndAddHeritageEdge = (graph, item, filePath, language, ctx) => {
63
+ if (item.kind === 'extends') {
64
+ const { type: relType, idPrefix } = resolveExtendsType(item.parentName, filePath, ctx, getHeritageStrategyForLanguage(language));
65
+ const child = resolveHeritageId(item.className, filePath, ctx, 'Class', `${filePath}:${item.className}`);
66
+ const parent = resolveHeritageId(item.parentName, filePath, ctx, idPrefix);
67
+ if (child.id && parent.id && child.id !== parent.id) {
68
+ graph.addRelationship({
69
+ id: generateId(relType, `${child.id}->${parent.id}`),
70
+ sourceId: child.id,
71
+ targetId: parent.id,
72
+ type: relType,
73
+ confidence: Math.sqrt(child.confidence * parent.confidence),
74
+ reason: '',
75
+ });
76
+ }
77
+ }
78
+ else if (item.kind === 'implements') {
79
+ const cls = resolveHeritageId(item.className, filePath, ctx, 'Class', `${filePath}:${item.className}`);
80
+ const iface = resolveHeritageId(item.parentName, filePath, ctx, 'Interface');
81
+ if (cls.id && iface.id) {
82
+ graph.addRelationship({
83
+ id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
84
+ sourceId: cls.id,
85
+ targetId: iface.id,
86
+ type: 'IMPLEMENTS',
87
+ confidence: Math.sqrt(cls.confidence * iface.confidence),
88
+ reason: '',
89
+ });
90
+ }
91
+ }
92
+ else if (item.kind === 'trait-impl' ||
93
+ item.kind === 'include' ||
94
+ item.kind === 'extend' ||
95
+ item.kind === 'prepend') {
96
+ // Fallback label for an unresolved child name. Rust `trait-impl` children
97
+ // are structs; Ruby mixin children are classes or modules (Trait). For
98
+ // Ruby mixin kinds the common case resolves through the type registry
99
+ // post-plan-001, so the fallback only fires for true-unresolved references
100
+ // (e.g. mixin inside a singleton_class). `Class` is strictly better than
101
+ // `Struct` there because it matches the label the structure phase would
102
+ // emit for a Ruby `class` — the dominant shape. Ruby modules that fail
103
+ // to resolve still lose their `Trait` label in the synthesized id, but
104
+ // they fail to resolve rarely and the tradeoff is documented.
105
+ const childFallbackLabel = item.kind === 'trait-impl' ? 'Struct' : 'Class';
106
+ const strct = resolveHeritageId(item.className, filePath, ctx, childFallbackLabel, `${filePath}:${item.className}`);
107
+ const trait = resolveHeritageId(item.parentName, filePath, ctx, 'Trait');
108
+ if (strct.id && trait.id) {
109
+ graph.addRelationship({
110
+ id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}:${item.kind}`),
111
+ sourceId: strct.id,
112
+ targetId: trait.id,
113
+ type: 'IMPLEMENTS',
114
+ confidence: Math.sqrt(strct.confidence * trait.confidence),
115
+ reason: item.kind,
116
+ });
117
+ }
118
+ }
119
+ };
57
120
  export const processHeritage = async (graph, files, astCache, ctx, onProgress) => {
58
121
  const parser = await loadParser();
59
122
  const logSkipped = isVerboseIngestionEnabled();
@@ -98,78 +161,31 @@ export const processHeritage = async (graph, files, astCache, ctx, onProgress) =
98
161
  let query;
99
162
  let matches;
100
163
  try {
101
- const language = parser.getLanguage();
102
- query = new Parser.Query(language, queryStr);
164
+ const treeSitterLang = parser.getLanguage();
165
+ query = new Parser.Query(treeSitterLang, queryStr);
103
166
  matches = query.matches(tree.rootNode);
104
167
  }
105
168
  catch (queryError) {
106
169
  console.warn(`Heritage query error for ${file.path}:`, queryError);
107
170
  continue;
108
171
  }
109
- // 4. Process heritage matches
172
+ // 4. Process heritage matches via provider heritage extractor
173
+ const heritageExtractor = provider.heritageExtractor;
110
174
  matches.forEach((match) => {
111
175
  const captureMap = {};
112
176
  match.captures.forEach((c) => {
113
177
  captureMap[c.name] = c.node;
114
178
  });
115
- // EXTENDS or IMPLEMENTS: resolve via symbol table for languages where
116
- // the tree-sitter query can't distinguish classes from interfaces (C#, Java)
117
- if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
118
- // Go struct embedding: skip named fields (only anonymous fields are embedded)
119
- const extendsNode = captureMap['heritage.extends'];
120
- const fieldDecl = extendsNode.parent;
121
- if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name')) {
122
- return; // Named field, not struct embedding
123
- }
124
- const className = captureMap['heritage.class'].text;
125
- const parentClassName = captureMap['heritage.extends'].text;
126
- const { type: relType, idPrefix } = resolveExtendsType(parentClassName, file.path, ctx, getHeritageStrategyForLanguage(language));
127
- const child = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
128
- const parent = resolveHeritageId(parentClassName, file.path, ctx, idPrefix);
129
- if (child.id && parent.id && child.id !== parent.id) {
130
- graph.addRelationship({
131
- id: generateId(relType, `${child.id}->${parent.id}`),
132
- sourceId: child.id,
133
- targetId: parent.id,
134
- type: relType,
135
- confidence: Math.sqrt(child.confidence * parent.confidence),
136
- reason: '',
137
- });
138
- }
139
- }
140
- // IMPLEMENTS: Class implements Interface (TypeScript only)
141
- if (captureMap['heritage.class'] && captureMap['heritage.implements']) {
142
- const className = captureMap['heritage.class'].text;
143
- const interfaceName = captureMap['heritage.implements'].text;
144
- const cls = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
145
- const iface = resolveHeritageId(interfaceName, file.path, ctx, 'Interface');
146
- if (cls.id && iface.id) {
147
- graph.addRelationship({
148
- id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
149
- sourceId: cls.id,
150
- targetId: iface.id,
151
- type: 'IMPLEMENTS',
152
- confidence: Math.sqrt(cls.confidence * iface.confidence),
153
- reason: '',
154
- });
155
- }
156
- }
157
- // IMPLEMENTS (Rust): impl Trait for Struct
158
- if (captureMap['heritage.trait'] && captureMap['heritage.class']) {
159
- const structName = captureMap['heritage.class'].text;
160
- const traitName = captureMap['heritage.trait'].text;
161
- const strct = resolveHeritageId(structName, file.path, ctx, 'Struct', `${file.path}:${structName}`);
162
- const trait = resolveHeritageId(traitName, file.path, ctx, 'Trait');
163
- if (strct.id && trait.id) {
164
- graph.addRelationship({
165
- id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}`),
166
- sourceId: strct.id,
167
- targetId: trait.id,
168
- type: 'IMPLEMENTS',
169
- confidence: Math.sqrt(strct.confidence * trait.confidence),
170
- reason: 'trait-impl',
171
- });
172
- }
179
+ if (!captureMap['heritage.class'])
180
+ return;
181
+ if (!heritageExtractor)
182
+ return;
183
+ const heritageItems = heritageExtractor.extract(captureMap, {
184
+ filePath: file.path,
185
+ language,
186
+ });
187
+ for (const item of heritageItems) {
188
+ resolveAndAddHeritageEdge(graph, item, file.path, language, ctx);
173
189
  }
174
190
  });
175
191
  // Tree is now owned by the LRU cache — no manual delete needed
@@ -228,7 +244,11 @@ export const processHeritageFromExtracted = async (graph, extractedHeritage, ctx
228
244
  h.kind === 'include' ||
229
245
  h.kind === 'extend' ||
230
246
  h.kind === 'prepend') {
231
- const strct = resolveHeritageId(h.className, h.filePath, ctx, 'Struct', `${h.filePath}:${h.className}`);
247
+ // See the per-item call above (processHeritageFromExtractedItem) for
248
+ // rationale: `Class` is the correct fallback for Ruby mixin kinds,
249
+ // `Struct` stays the Rust `trait-impl` default.
250
+ const childFallbackLabel = h.kind === 'trait-impl' ? 'Struct' : 'Class';
251
+ const strct = resolveHeritageId(h.className, h.filePath, ctx, childFallbackLabel, `${h.filePath}:${h.className}`);
232
252
  const trait = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');
233
253
  if (strct.id && trait.id) {
234
254
  graph.addRelationship({
@@ -249,6 +269,15 @@ export const processHeritageFromExtracted = async (graph, extractedHeritage, ctx
249
269
  * {@link ExtractedHeritage} rows without mutating the graph. Used on the
250
270
  * sequential pipeline path so `buildHeritageMap(..., ctx)` can run before
251
271
  * `processCalls` (worker path defers calls until heritage from all chunks exists).
272
+ *
273
+ * This prepass extracts BOTH capture-based heritage (`@heritage.*` — extends /
274
+ * implements / trait-impl) AND call-based heritage (`@call.name` routed through
275
+ * `heritageExtractor.extractFromCall` — Ruby `include` / `extend` / `prepend`).
276
+ * Without the second pass, sequential-mode `sequentialHeritageMap` would not
277
+ * know about Ruby mixin ancestry before `processCalls` resolves calls against
278
+ * it, silently dropping mixed-in methods from the graph. This function stays
279
+ * read-only — `processCalls` still owns emission of heritage graph edges via
280
+ * its `rubyHeritage` return path.
252
281
  */
253
282
  export async function extractExtractedHeritageFromFiles(files, astCache) {
254
283
  const parser = await loadParser();
@@ -283,40 +312,46 @@ export async function extractExtractedHeritageFromFiles(files, astCache) {
283
312
  catch {
284
313
  continue;
285
314
  }
315
+ const callBasedEnabled = !!provider.heritageExtractor?.extractFromCall;
286
316
  for (const match of matches) {
287
317
  const captureMap = {};
288
318
  match.captures.forEach((c) => {
289
319
  captureMap[c.name] = c.node;
290
320
  });
291
321
  if (captureMap['heritage.class']) {
292
- if (captureMap['heritage.extends']) {
293
- const extendsNode = captureMap['heritage.extends'];
294
- const fieldDecl = extendsNode.parent;
295
- const isNamedField = fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name');
296
- if (!isNamedField) {
322
+ if (provider.heritageExtractor) {
323
+ const heritageItems = provider.heritageExtractor.extract(captureMap, {
324
+ filePath: file.path,
325
+ language,
326
+ });
327
+ for (const item of heritageItems) {
297
328
  out.push({
298
329
  filePath: file.path,
299
- className: captureMap['heritage.class'].text,
300
- parentName: captureMap['heritage.extends'].text,
301
- kind: 'extends',
330
+ className: item.className,
331
+ parentName: item.parentName,
332
+ kind: item.kind,
302
333
  });
303
334
  }
304
335
  }
305
- if (captureMap['heritage.implements']) {
306
- out.push({
307
- filePath: file.path,
308
- className: captureMap['heritage.class'].text,
309
- parentName: captureMap['heritage.implements'].text,
310
- kind: 'implements',
311
- });
312
- }
313
- if (captureMap['heritage.trait']) {
314
- out.push({
315
- filePath: file.path,
316
- className: captureMap['heritage.class'].text,
317
- parentName: captureMap['heritage.trait'].text,
318
- kind: 'trait-impl',
319
- });
336
+ continue;
337
+ }
338
+ // Call-based heritage (e.g. Ruby include/extend/prepend). Matches the
339
+ // routing the worker path performs inline in parse-worker.ts — see the
340
+ // `provider.heritageExtractor?.extractFromCall` branch there. We only
341
+ // need call-based records here; other @call captures are consumed by
342
+ // processCalls later in the sequential loop.
343
+ if (callBasedEnabled && captureMap['call'] && captureMap['call.name']) {
344
+ const calledName = captureMap['call.name'].text;
345
+ const heritageItems = provider.heritageExtractor.extractFromCall(calledName, captureMap['call'], { filePath: file.path, language });
346
+ if (heritageItems) {
347
+ for (const item of heritageItems) {
348
+ out.push({
349
+ filePath: file.path,
350
+ className: item.className,
351
+ parentName: item.parentName,
352
+ kind: item.kind,
353
+ });
354
+ }
320
355
  }
321
356
  }
322
357
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Types for the language-agnostic heritage extraction pipeline.
3
+ *
4
+ * Follows the same pattern as call-types.ts, variable-types.ts, and
5
+ * method-types.ts: defines the domain interfaces consumed by
6
+ * createHeritageExtractor() and the per-language configs.
7
+ *
8
+ * Heritage extraction handles extends/implements/trait-impl captures from
9
+ * tree-sitter queries, plus call-based heritage for languages like Ruby
10
+ * (include/extend/prepend expressed as method calls).
11
+ */
12
+ import type { SupportedLanguages } from '../../_shared/index.js';
13
+ import type { SyntaxNode } from './utils/ast-helpers.js';
14
+ import type { CaptureMap } from './language-provider.js';
15
+ /**
16
+ * Per-match heritage extraction result. The parse worker adds filePath to
17
+ * produce the final {@link ExtractedHeritage} that enters the resolution
18
+ * pipeline (heritage-processor.ts / heritage-map.ts).
19
+ */
20
+ export interface HeritageInfo {
21
+ className: string;
22
+ parentName: string;
23
+ /** 'extends' | 'implements' | 'trait-impl' | 'include' | 'extend' | 'prepend' */
24
+ kind: string;
25
+ }
26
+ export interface HeritageExtractorContext {
27
+ filePath: string;
28
+ language: SupportedLanguages;
29
+ }
30
+ export interface HeritageExtractor {
31
+ readonly language: SupportedLanguages;
32
+ /**
33
+ * Extract heritage records from tree-sitter @heritage.* captures.
34
+ *
35
+ * @param captureMap The capture map from a single tree-sitter match
36
+ * @param context File path and language context
37
+ * @returns Array of heritage records (may be empty if captures don't match)
38
+ */
39
+ extract(captureMap: CaptureMap, context: HeritageExtractorContext): HeritageInfo[];
40
+ /**
41
+ * Extract heritage from a call node (for languages where heritage is
42
+ * expressed as method calls, e.g., Ruby include/extend/prepend).
43
+ *
44
+ * @param calledName The method name (e.g. 'include', 'extend', 'prepend')
45
+ * @param callNode The tree-sitter call AST node
46
+ * @param context File path and language context
47
+ * @returns Heritage records if the call is heritage-related, or null to
48
+ * fall through to the call router / normal call handling.
49
+ */
50
+ extractFromCall?(calledName: string, callNode: SyntaxNode, context: HeritageExtractorContext): HeritageInfo[] | null;
51
+ }
52
+ export interface HeritageExtractionConfig {
53
+ language: SupportedLanguages;
54
+ /**
55
+ * Called for heritage.extends captures. Return true to skip this extends
56
+ * capture. Used by Go to skip named struct fields that match the
57
+ * field_declaration pattern but are not anonymous embeddings.
58
+ *
59
+ * Default: never skip (all extends captures are valid).
60
+ */
61
+ shouldSkipExtends?: (extendsNode: SyntaxNode) => boolean;
62
+ /**
63
+ * Call-based heritage extraction for languages where heritage is expressed
64
+ * as method calls (e.g., Ruby include/extend/prepend).
65
+ *
66
+ * callNames: set of method names that trigger heritage extraction.
67
+ * extract: extract heritage items from the call node + method name.
68
+ */
69
+ callBasedHeritage?: {
70
+ readonly callNames: ReadonlySet<string>;
71
+ extract(calledName: string, callNode: SyntaxNode, filePath: string): HeritageInfo[];
72
+ };
73
+ }
@@ -0,0 +1,2 @@
1
+ // gitnexus/src/core/ingestion/heritage-types.ts
2
+ export {};
@@ -11,10 +11,11 @@
11
11
  import type { SupportedLanguages, MroStrategy } from '../../_shared/index.js';
12
12
  import type { LanguageTypeConfig } from './type-extractors/types.js';
13
13
  import type { CallRouter } from './call-routing.js';
14
- import type { CallExtractor } from './call-types.js';
14
+ import type { CallExtractor, DispatchDecision, ImplicitReceiverOverride, ReceiverEnriched } from './call-types.js';
15
15
  import type { ClassExtractor } from './class-types.js';
16
16
  import type { ExportChecker } from './export-detection.js';
17
17
  import type { FieldExtractor } from './field-extractor.js';
18
+ import type { HeritageExtractor } from './heritage-types.js';
18
19
  import type { MethodExtractor } from './method-types.js';
19
20
  import type { VariableExtractor } from './variable-types.js';
20
21
  import type { ImportResolverFn } from './import-resolvers/types.js';
@@ -143,6 +144,13 @@ interface LanguageProviderConfig {
143
144
  * Uses the same provider-driven strategy pattern as method/field extraction so
144
145
  * namespace/package/module rules stay language-specific. */
145
146
  readonly classExtractor?: ClassExtractor;
147
+ /** Heritage extractor for extracting extends/implements/trait-impl relationships
148
+ * from tree-sitter @heritage.* captures and call-based heritage (e.g., Ruby
149
+ * include/extend/prepend). Produced by createHeritageExtractor() — pass a
150
+ * SupportedLanguages value for default behaviour or a full
151
+ * HeritageExtractionConfig for languages with custom hooks (Go, Ruby).
152
+ * All tree-sitter providers MUST supply this. */
153
+ readonly heritageExtractor?: HeritageExtractor;
146
154
  /** Extract a semantic description for a definition node (e.g., PHP Eloquent
147
155
  * property arrays, relation method descriptions).
148
156
  * Default: undefined (no description extraction). */
@@ -151,6 +159,66 @@ interface LanguageProviderConfig {
151
159
  * When true, the worker extracts routes via the language's route extraction logic.
152
160
  * Default: undefined (no route files). */
153
161
  readonly isRouteFile?: (filePath: string) => boolean;
162
+ /**
163
+ * DAG stage 3 hook: synthesize an implicit receiver when the call site omits one.
164
+ *
165
+ * Runs after shared inference (TypeEnv → constructor-map → class-as-receiver →
166
+ * mixed-chain). Return an `ImplicitReceiverOverride` to overlay all fields onto
167
+ * `ReceiverEnriched`; return null to keep current state and proceed to stage 4.
168
+ *
169
+ * Constraints: MUST return null when an explicit receiver is already set, at
170
+ * top-level scope, or for built-in methods. Do not mutate input params.
171
+ * `hint` is opaque to shared stages; consumed by this language's `selectDispatch`.
172
+ *
173
+ * Ruby example: bare `serialize` in `Account#call_serialize` →
174
+ * `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
175
+ * receiverSource: 'implicit-self', hint: 'instance' }`
176
+ *
177
+ * @see call-types.ts § ImplicitReceiverOverride
178
+ * @see selectDispatch (stage 4, reads the hint)
179
+ *
180
+ * Default: undefined (no implicit-receiver inference).
181
+ */
182
+ readonly inferImplicitReceiver?: (params: {
183
+ readonly calledName: string;
184
+ readonly callForm: 'free' | 'member' | 'constructor' | undefined;
185
+ readonly receiverName: string | undefined;
186
+ readonly receiverTypeName: string | undefined;
187
+ readonly callNode: SyntaxNode;
188
+ readonly filePath: string;
189
+ }) => ImplicitReceiverOverride | null;
190
+ /**
191
+ * DAG stage 4 hook: decide dispatch strategy (primary path, fallback, MRO view).
192
+ *
193
+ * Runs after stage 3. Return a `DispatchDecision` to override shared defaults;
194
+ * return null to use `defaultDispatchDecision` (constructor→`'constructor'`,
195
+ * member→`'owner-scoped'`, free→`'free'`). Most languages return null.
196
+ *
197
+ * The hook is responsible for its own gating. `ancestryView` only affects
198
+ * `'ruby-mixin'` strategy. Singleton-ancestry miss NEVER falls through to
199
+ * file-scoped fallback in stage 5 (enforced in resolveCallTarget).
200
+ *
201
+ * Ruby examples:
202
+ * - `receiverSource='implicit-self', hint='instance'` →
203
+ * `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}`
204
+ * - `receiverSource='class-as-receiver'` →
205
+ * `{primary: 'owner-scoped', ancestryView: 'singleton'}` (miss null-routes)
206
+ * - `receiverSource='implicit-self', hint='singleton'` →
207
+ * `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'singleton'}`
208
+ *
209
+ * @see call-types.ts § DispatchDecision
210
+ * @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
211
+ *
212
+ * Default: undefined (use `defaultDispatchDecision`).
213
+ */
214
+ readonly selectDispatch?: (params: {
215
+ readonly calledName: string;
216
+ readonly callForm: 'free' | 'member' | 'constructor' | undefined;
217
+ readonly receiverName: string | undefined;
218
+ readonly receiverTypeName: string | undefined;
219
+ readonly receiverSource: ReceiverEnriched['receiverSource'];
220
+ readonly hint: string | undefined;
221
+ }) => DispatchDecision | null;
154
222
  /** Built-in/stdlib names that should be filtered from the call graph for this language.
155
223
  * Default: undefined (no language-specific filtering). */
156
224
  readonly builtInNames?: ReadonlySet<string>;
@@ -35,6 +35,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
35
35
  import { cVariableConfig, cppVariableConfig } from '../variable-extractors/configs/c-cpp.js';
36
36
  import { createCallExtractor } from '../call-extractors/generic.js';
37
37
  import { cCallConfig, cppCallConfig } from '../call-extractors/configs/c-cpp.js';
38
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
38
39
  const C_BUILT_INS = new Set([
39
40
  'printf',
40
41
  'fprintf',
@@ -301,6 +302,7 @@ export const cProvider = defineLanguage({
301
302
  }),
302
303
  variableExtractor: createVariableExtractor(cVariableConfig),
303
304
  classExtractor: cClassExtractor,
305
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.C),
304
306
  labelOverride: cppLabelOverride,
305
307
  builtInNames: C_BUILT_INS,
306
308
  });
@@ -321,6 +323,7 @@ export const cppProvider = defineLanguage({
321
323
  }),
322
324
  variableExtractor: createVariableExtractor(cppVariableConfig),
323
325
  classExtractor: cppClassExtractor,
326
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.CPlusPlus),
324
327
  labelOverride: cppLabelOverride,
325
328
  builtInNames: C_BUILT_INS,
326
329
  });
@@ -23,6 +23,7 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
23
23
  import { csharpMethodConfig } from '../method-extractors/configs/csharp.js';
24
24
  import { createVariableExtractor } from '../variable-extractors/generic.js';
25
25
  import { csharpVariableConfig } from '../variable-extractors/configs/csharp.js';
26
+ import { createHeritageExtractor } from '../heritage-extractors/generic.js';
26
27
  const BUILT_INS = new Set([
27
28
  'Console',
28
29
  'WriteLine',
@@ -132,5 +133,6 @@ export const csharpProvider = defineLanguage({
132
133
  methodExtractor: createMethodExtractor(csharpMethodConfig),
133
134
  variableExtractor: createVariableExtractor(csharpVariableConfig),
134
135
  classExtractor: createClassExtractor(csharpClassConfig),
136
+ heritageExtractor: createHeritageExtractor(SupportedLanguages.CSharp),
135
137
  builtInNames: BUILT_INS,
136
138
  });