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
@@ -233,35 +233,112 @@ const buildParentMapFromHeritage = (startNodeId, heritageMap) => {
233
233
  // MRO-aware method lookup
234
234
  // ---------------------------------------------------------------------------
235
235
  /**
236
- * Look up a method on an owner class, walking the parent chain via HeritageMap
237
- * when the method isn't found on the direct owner.
236
+ * DAG stage 5 helper: look up a method on an owner class via MRO walk.
238
237
  *
239
- * Respects the 5 per-language MRO strategies:
240
- * - `first-wins`: BFS ancestor walk, first match wins (default)
241
- * - `leftmost-base`: BFS ancestor walk, leftmost base in declaration order wins (C++);
242
- * HeritageMap preserves insertion order matching source declaration,
243
- * so BFS order is equivalent to leftmost-base semantics
244
- * - `c3`: C3-linearized ancestor order, first match wins (Python)
245
- * - `implements-split`: BFS ancestor walk, first match wins (Java/C#) —
246
- * full ambiguity detection for multiple interface defaults
247
- * is handled by computeMRO at graph level
248
- * - `qualified-syntax`: No auto-resolution (Rust) — returns undefined
238
+ * Low-level resolver; no dependency on SymbolTable, language registry, or
239
+ * resolution-context (keeps model/ layer free of cross-layer imports).
240
+ * All strategies respect `argCount` for overload narrowing.
241
+ * `ancestryOverride` replaces the default walk; caller must compute it correctly.
249
242
  *
250
- * Uses the `c3Linearize` defined in this file (also consumed by
251
- * mro-processor.ts for graph-level MRO emission) for the `c3` strategy.
243
+ * Strategy summary (full docs in gitnexus-shared/mro-strategy.ts):
244
+ * - `first-wins` / `leftmost-base` / `implements-split`: BFS, first match wins.
245
+ * - `c3`: C3-linearized order; falls back to BFS on cycle/inconsistency.
246
+ * - `qualified-syntax`: returns undefined immediately (Rust requires explicit syntax).
247
+ * - `ruby-mixin`: kind-aware walk — see inline comments below.
252
248
  *
253
- * Depends only on {@link SemanticModel} + {@link HeritageMap} + an
254
- * {@link MroStrategy} literal NO dependency on SymbolTable, the language
255
- * registry, or resolution-context, which keeps the `model/` module free of
256
- * cross-layer imports. Callers derive the strategy from their language
257
- * provider before invoking this function.
249
+ * Internal API: exported for call-processor resolvers and tests.
250
+ * External callers should use resolveMemberCall instead.
258
251
  *
259
- * @internal This is the low-level MRO walker. Exported so call-processor's
260
- * higher-level resolvers (and unit tests) can invoke it directly. Callers
261
- * outside `core/ingestion/` should use the higher-level resolvers in
262
- * call-processor.ts instead of depending on this function.
252
+ * @see gitnexus-shared/mro-strategy.ts § 'ruby-mixin'
253
+ * @see call-processor.ts § resolveMemberCall
263
254
  */
264
- export const lookupMethodByOwnerWithMRO = (ownerNodeId, methodName, heritageMap, model, strategy, argCount) => {
255
+ export const lookupMethodByOwnerWithMRO = (ownerNodeId, methodName, heritageMap, model, strategy, argCount,
256
+ /**
257
+ * Optional pre-computed ancestry list. When provided, overrides the default
258
+ * per-strategy ancestry source. Primarily used by Ruby singleton dispatch:
259
+ * the caller supplies `heritageMap.getSingletonAncestry(ownerNodeId)` as
260
+ * node-id array so this walker resolves against `extend` providers only.
261
+ *
262
+ * For `ruby-mixin` strategy, passing an override switches the walker into
263
+ * a no-prepend-no-direct linear scan (the caller has already decided the
264
+ * order), which is the correct semantics for singleton dispatch.
265
+ */
266
+ ancestryOverride) => {
267
+ // ── Ruby mixin strategy ───────────────────────────────────────────
268
+ // Kind-aware walk — does NOT short-circuit on direct owner first (prepend beats direct).
269
+ // Instance dispatch: prepend (reverse) → direct → include (reverse) → transitive BFS.
270
+ // Singleton dispatch: caller supplies ancestryOverride (extend providers only);
271
+ // simple left-to-right scan. Miss NEVER falls through to file-scoped fallback.
272
+ // See gitnexus-shared/mro-strategy.ts § 'ruby-mixin' for full strategy docs.
273
+ if (strategy === 'ruby-mixin') {
274
+ if (ancestryOverride) {
275
+ // Singleton dispatch: scan pre-computed ancestry only. Miss null-routes.
276
+ for (const ancestorId of ancestryOverride) {
277
+ const method = model.methods.lookupMethodByOwner(ancestorId, methodName, argCount);
278
+ if (method)
279
+ return method;
280
+ }
281
+ return undefined;
282
+ }
283
+ // Instance dispatch — kind-aware walk per the pseudocode above.
284
+ const instanceEntries = heritageMap.getInstanceAncestry(ownerNodeId);
285
+ // Partition into prepend parents vs other parents (extends / include /
286
+ // implements / trait-impl), preserving declaration order within each.
287
+ const prependParents = [];
288
+ const otherParents = [];
289
+ for (const e of instanceEntries) {
290
+ if (e.kind === 'prepend')
291
+ prependParents.push(e.parentId);
292
+ else
293
+ otherParents.push(e.parentId);
294
+ }
295
+ // Step 1: Walk prepend parents in REVERSE declaration order (last-prepended wins).
296
+ for (let i = prependParents.length - 1; i >= 0; i--) {
297
+ const method = model.methods.lookupMethodByOwner(prependParents[i], methodName, argCount);
298
+ if (method)
299
+ return method;
300
+ }
301
+ // Step 2: Direct owner lookup (the class's own method).
302
+ // This is the only difference from other strategies — prepend beats direct.
303
+ const direct = model.methods.lookupMethodByOwner(ownerNodeId, methodName, argCount);
304
+ if (direct)
305
+ return direct;
306
+ // Step 3: Walk extends + include parents in REVERSE declaration order.
307
+ // (Ruby `include A; include B` puts B ahead of A in MRO.)
308
+ for (let i = otherParents.length - 1; i >= 0; i--) {
309
+ const method = model.methods.lookupMethodByOwner(otherParents[i], methodName, argCount);
310
+ if (method)
311
+ return method;
312
+ }
313
+ // Step 4: Transitive ancestors (a mixin that itself mixes in another module).
314
+ // Fall back to the BFS ancestor walk for depth > 1. Order is best-effort;
315
+ // Ruby's actual MRO for transitive mixins is rare and under-specified
316
+ // (documented in architecture docs as deferred work).
317
+ //
318
+ // O(1) skip-check via Sets:
319
+ // - `walkedDirect` covers parents already visited in steps 1-3.
320
+ // - `singletonOnly` covers direct `extend` providers: they belong to
321
+ // the singleton MRO and must NEVER appear in instance dispatch.
322
+ // Building Sets once before the BFS loop avoids O(n²) `Array.includes`
323
+ // on large mixin hierarchies.
324
+ const walkedDirect = new Set(prependParents);
325
+ for (const id of otherParents)
326
+ walkedDirect.add(id);
327
+ const singletonOnly = new Set(heritageMap.getSingletonAncestry(ownerNodeId).map((e) => e.parentId));
328
+ for (const ancestorId of heritageMap.getAncestors(ownerNodeId)) {
329
+ if (ancestorId === ownerNodeId)
330
+ continue;
331
+ if (walkedDirect.has(ancestorId))
332
+ continue;
333
+ if (singletonOnly.has(ancestorId))
334
+ continue;
335
+ const method = model.methods.lookupMethodByOwner(ancestorId, methodName, argCount);
336
+ if (method)
337
+ return method;
338
+ }
339
+ return undefined;
340
+ }
341
+ // ── Non-Ruby strategies: direct-owner-first short-circuit ─────────
265
342
  // Direct lookup first (child override — no walk needed).
266
343
  // argCount is threaded through so arity-differing overloads on the direct
267
344
  // owner can be disambiguated before the MRO walk starts.
@@ -274,7 +351,10 @@ export const lookupMethodByOwnerWithMRO = (ownerNodeId, methodName, heritageMap,
274
351
  // Determine ancestor walk order based on MRO strategy.
275
352
  // readonly to accept the cached (frozen) c3 linearization without copying.
276
353
  let ancestors;
277
- if (strategy === 'c3') {
354
+ if (ancestryOverride) {
355
+ ancestors = ancestryOverride;
356
+ }
357
+ else if (strategy === 'c3') {
278
358
  // C3 linearization (memoized per HeritageMap
279
359
  // so repeated calls for the same owner within an ingestion run reuse the
280
360
  // linearization instead of rebuilding the parent map and re-running C3).
@@ -43,5 +43,6 @@ export declare function runChunkedParseAndResolve(graph: KnowledgeGraph, scanned
43
43
  allORMQueries: ExtractedORMQuery[];
44
44
  bindingAccumulator: BindingAccumulator;
45
45
  resolutionContext: ReturnType<typeof createResolutionContext>;
46
+ usedWorkerPool: boolean;
46
47
  }>;
47
48
  export {};
@@ -98,9 +98,11 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
98
98
  message: `Parsing ${totalParseable} files in ${numChunks} chunk${numChunks !== 1 ? 's' : ''}...`,
99
99
  stats: { filesProcessed: 0, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
100
100
  });
101
- // Don't spawn workers for tiny repos — overhead exceeds benefit
102
- const MIN_FILES_FOR_WORKERS = 15;
103
- const MIN_BYTES_FOR_WORKERS = 512 * 1024;
101
+ // Don't spawn workers for tiny repos — overhead exceeds benefit.
102
+ // Test suites may lower the thresholds via `options.workerThresholdsForTest`
103
+ // to exercise the worker-pool path with small fixtures; see PipelineOptions.
104
+ const MIN_FILES_FOR_WORKERS = options?.workerThresholdsForTest?.minFiles ?? 15;
105
+ const MIN_BYTES_FOR_WORKERS = options?.workerThresholdsForTest?.minBytes ?? 512 * 1024;
104
106
  const totalBytes = parseableScanned.reduce((s, f) => s + f.size, 0);
105
107
  // Create worker pool once, reuse across chunks
106
108
  let workerPool;
@@ -433,5 +435,9 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
433
435
  allORMQueries,
434
436
  bindingAccumulator,
435
437
  resolutionContext: ctx,
438
+ // Whether a worker pool was actually live for this run. False means the
439
+ // sequential fallback handled every chunk (either due to `skipWorkers`,
440
+ // the file-count/byte thresholds, or a pool-creation failure).
441
+ usedWorkerPool: workerPool !== undefined,
436
442
  };
437
443
  }
@@ -45,5 +45,12 @@ export interface ParseOutput {
45
45
  readonly allPathSet: ReadonlySet<string>;
46
46
  /** Pass-through: total file count for progress reporting. */
47
47
  totalFiles: number;
48
+ /**
49
+ * True if the parse phase spawned a live worker pool for this run.
50
+ * False means every chunk ran through the sequential fallback (skipWorkers,
51
+ * thresholds not met, or pool-creation failure). Primarily a test affordance:
52
+ * see `PipelineOptions.workerThresholdsForTest`.
53
+ */
54
+ readonly usedWorkerPool: boolean;
48
55
  }
49
56
  export declare const parsePhase: PipelinePhase<ParseOutput>;
@@ -21,5 +21,16 @@ export interface PipelineOptions {
21
21
  skipGraphPhases?: boolean;
22
22
  /** Force sequential parsing (no worker pool). Useful for testing the sequential path. */
23
23
  skipWorkers?: boolean;
24
+ /**
25
+ * @internal Test-only override for worker-pool gating thresholds.
26
+ * When unset, production defaults apply (15 files OR 512 KB total bytes).
27
+ * Setting either field lowers the corresponding threshold so small test
28
+ * fixtures can still exercise the worker-pool path. Do not use from
29
+ * production call sites.
30
+ */
31
+ workerThresholdsForTest?: {
32
+ minFiles?: number;
33
+ minBytes?: number;
34
+ };
24
35
  }
25
36
  export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void, options?: PipelineOptions) => Promise<PipelineResult>;
@@ -58,7 +58,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
58
58
  pipelineStart,
59
59
  });
60
60
  // Extract final results for the PipelineResult contract
61
- const { totalFiles } = getPhaseOutput(results, 'parse');
61
+ const { totalFiles, usedWorkerPool } = getPhaseOutput(results, 'parse');
62
62
  let communityResult;
63
63
  let processResult;
64
64
  if (!options?.skipGraphPhases) {
@@ -77,5 +77,12 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
77
77
  nodesCreated: graph.nodeCount,
78
78
  },
79
79
  });
80
- return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult };
80
+ return {
81
+ graph,
82
+ repoPath,
83
+ totalFileCount: totalFiles,
84
+ communityResult,
85
+ processResult,
86
+ usedWorkerPool,
87
+ };
81
88
  };
@@ -141,7 +141,14 @@ export const CONTAINER_TYPE_TO_LABEL = {
141
141
  mixin_declaration: 'Mixin',
142
142
  extension_declaration: 'Extension',
143
143
  class: 'Class',
144
- module: 'Module',
144
+ // Ruby `module` declarations map to `Trait` so they participate in the
145
+ // class-like type registry used by `lookupClassByName` / `buildHeritageMap`.
146
+ // This lets `include` / `extend` / `prepend` mixin heritage resolve to
147
+ // the providing module. Safe for non-Ruby languages: the only supported
148
+ // grammar that uses the bare `module` AST node type as a container is
149
+ // Ruby (Rust uses `mod_item`). Any new language adding a `module` node
150
+ // type must explicitly reclassify here.
151
+ module: 'Trait',
145
152
  singleton_class: 'Class', // Ruby: class << self inherits enclosing class name
146
153
  object_declaration: 'Class',
147
154
  companion_object: 'Class',
@@ -177,8 +184,18 @@ export function getLabelFromCaptures(captureMap, provider) {
177
184
  return 'Enum';
178
185
  if (captureMap['definition.namespace'])
179
186
  return 'Namespace';
180
- if (captureMap['definition.module'])
187
+ if (captureMap['definition.module']) {
188
+ // Let providers reclassify module captures (e.g. Ruby remaps `Module`→`Trait`
189
+ // so mixin heritage resolves through `lookupClassByName`). Returning null
190
+ // from labelOverride means "skip this symbol"; treat it as a no-op here so
191
+ // we keep the default label rather than dropping a real definition.
192
+ if (provider.labelOverride) {
193
+ const override = provider.labelOverride(captureMap['definition.module'], 'Module');
194
+ if (override && override !== 'Module')
195
+ return override;
196
+ }
181
197
  return 'Module';
198
+ }
182
199
  if (captureMap['definition.trait'])
183
200
  return 'Trait';
184
201
  if (captureMap['definition.impl'])
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Ruby bare-call self-inference helper.
3
+ *
4
+ * Ruby makes `self` implicit for method calls inside instance and class bodies:
5
+ * `serialize` inside `Account#call_serialize` means `self.serialize`. Other
6
+ * supported languages make the receiver explicit in source (`this.x`, `self.x`),
7
+ * so tree-sitter produces a member call directly. Ruby's bare identifier
8
+ * produces either `callForm === 'free'` or `callForm === undefined` (body_statement
9
+ * identifier captures where the @call node IS the @call.name node), and
10
+ * `resolveFreeCall` does a global tiered name lookup — no MRO walk.
11
+ *
12
+ * This helper is a pure decision function consumed by the Ruby language
13
+ * provider's `inferImplicitReceiver` hook. Shared pipeline code never imports
14
+ * it directly — only `languages/ruby.ts` does.
15
+ */
16
+ import type { SyntaxNode } from './ast-helpers.js';
17
+ import type { LanguageProvider } from '../language-provider.js';
18
+ /**
19
+ * Rewrite suggestion returned by `maybeRewriteRubyBareCallToSelf`.
20
+ *
21
+ * `callForm` is always `'member'`; `receiverName` is always `'self'`.
22
+ * `dispatchKind` controls the stage-4 ancestry view:
23
+ * - `'instance'` → prepend → direct → include (normal MRO)
24
+ * - `'singleton'` → extend providers only, no file-scoped fallback
25
+ *
26
+ * Consumed by `languages/ruby.ts § inferImplicitReceiver` (wraps into
27
+ * `ImplicitReceiverOverride`; `dispatchKind` becomes the `hint` field).
28
+ */
29
+ export interface SelfCallRewrite {
30
+ readonly callForm: 'member';
31
+ readonly receiverName: 'self';
32
+ readonly receiverTypeName: string;
33
+ /** `'singleton'` when the enclosing method is `def self.foo` / inside a
34
+ * `singleton_class` body; `'instance'` otherwise. Controls MRO ancestry
35
+ * view selection in stage-4 dispatch. */
36
+ readonly dispatchKind: 'instance' | 'singleton';
37
+ }
38
+ /**
39
+ * Pure decision function: should a bare Ruby call be rewritten as `self.method`?
40
+ *
41
+ * Returns a `SelfCallRewrite` when all gates pass; null otherwise.
42
+ * Gates (all required): `callForm` is `'free'` or `undefined`, strategy is
43
+ * `'ruby-mixin'`, `enclosingClassName` is non-null, name is not `'super'`,
44
+ * name is not a built-in.
45
+ *
46
+ * Note: Ruby body-statement identifiers produce `callForm === undefined` because
47
+ * the @call node IS the @call.name node in tree-sitter-ruby.
48
+ *
49
+ * Example: `calledName='serialize'` in `Account` instance method →
50
+ * `{callForm:'member', receiverName:'self', receiverTypeName:'Account', dispatchKind:'instance'}`
51
+ */
52
+ export declare function maybeRewriteRubyBareCallToSelf(calledName: string, callForm: 'free' | 'member' | 'constructor' | undefined, callNode: SyntaxNode, enclosingClassName: string | null, provider: Pick<LanguageProvider, 'isBuiltInName' | 'mroStrategy'>): SelfCallRewrite | null;
@@ -0,0 +1,59 @@
1
+ // gitnexus/src/core/ingestion/utils/ruby-self-call.ts
2
+ /** Maximum parent-walk depth to prevent runaway traversal. */
3
+ const MAX_PARENT_DEPTH = 50;
4
+ /**
5
+ * Returns true if `callNode` is inside a `singleton_method` or `singleton_class`.
6
+ * Stops at `class`/`module` boundary or MAX_PARENT_DEPTH (50) to bound traversal.
7
+ */
8
+ function isInsideSingletonMethod(callNode) {
9
+ let current = callNode.parent;
10
+ let depth = 0;
11
+ while (current && depth++ < MAX_PARENT_DEPTH) {
12
+ if (current.type === 'singleton_method')
13
+ return true;
14
+ if (current.type === 'singleton_class')
15
+ return true;
16
+ if (current.type === 'class' || current.type === 'module')
17
+ return false;
18
+ current = current.parent;
19
+ }
20
+ return false;
21
+ }
22
+ /**
23
+ * Pure decision function: should a bare Ruby call be rewritten as `self.method`?
24
+ *
25
+ * Returns a `SelfCallRewrite` when all gates pass; null otherwise.
26
+ * Gates (all required): `callForm` is `'free'` or `undefined`, strategy is
27
+ * `'ruby-mixin'`, `enclosingClassName` is non-null, name is not `'super'`,
28
+ * name is not a built-in.
29
+ *
30
+ * Note: Ruby body-statement identifiers produce `callForm === undefined` because
31
+ * the @call node IS the @call.name node in tree-sitter-ruby.
32
+ *
33
+ * Example: `calledName='serialize'` in `Account` instance method →
34
+ * `{callForm:'member', receiverName:'self', receiverTypeName:'Account', dispatchKind:'instance'}`
35
+ */
36
+ export function maybeRewriteRubyBareCallToSelf(calledName, callForm, callNode, enclosingClassName, provider) {
37
+ // Body-statement bare identifiers produce `callForm === undefined` because
38
+ // the @call node IS the @call.name node in tree-sitter-ruby. Treat both
39
+ // undefined and 'free' as qualifying.
40
+ if (callForm !== 'free' && callForm !== undefined)
41
+ return null;
42
+ if (provider.mroStrategy !== 'ruby-mixin')
43
+ return null;
44
+ if (!enclosingClassName)
45
+ return null;
46
+ if (calledName === 'super')
47
+ return null;
48
+ if (provider.isBuiltInName(calledName))
49
+ return null;
50
+ const dispatchKind = isInsideSingletonMethod(callNode)
51
+ ? 'singleton'
52
+ : 'instance';
53
+ return {
54
+ callForm: 'member',
55
+ receiverName: 'self',
56
+ receiverTypeName: enclosingClassName,
57
+ dispatchKind,
58
+ };
59
+ }
@@ -1021,33 +1021,36 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1021
1021
  // Heritage edges (EXTENDS/IMPLEMENTS) are created by heritage-processor which runs
1022
1022
  // in PARALLEL with call-processor, so the graph edges don't exist when buildTypeEnv
1023
1023
  // runs. This pre-pass makes parent class information available for type resolution.
1024
+ const provider = getProvider(language);
1024
1025
  const fileParentMap = new Map();
1025
- for (const match of matches) {
1026
- const captureMap = {};
1027
- for (const c of match.captures) {
1028
- captureMap[c.name] = c.node;
1029
- }
1030
- if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
1031
- const className = captureMap['heritage.class'].text;
1032
- const parentName = captureMap['heritage.extends'].text;
1033
- // Skip Go named fields (only anonymous fields are struct embedding)
1034
- const extendsNode = captureMap['heritage.extends'];
1035
- const fieldDecl = extendsNode.parent;
1036
- if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
1037
- continue;
1038
- let parents = fileParentMap.get(className);
1039
- if (!parents) {
1040
- parents = [];
1041
- fileParentMap.set(className, parents);
1026
+ if (provider.heritageExtractor) {
1027
+ for (const match of matches) {
1028
+ const captureMap = {};
1029
+ for (const c of match.captures) {
1030
+ captureMap[c.name] = c.node;
1031
+ }
1032
+ if (captureMap['heritage.class']) {
1033
+ const heritageItems = provider.heritageExtractor.extract(captureMap, {
1034
+ filePath: file.path,
1035
+ language,
1036
+ });
1037
+ for (const item of heritageItems) {
1038
+ if (item.kind === 'extends') {
1039
+ let parents = fileParentMap.get(item.className);
1040
+ if (!parents) {
1041
+ parents = [];
1042
+ fileParentMap.set(item.className, parents);
1043
+ }
1044
+ if (!parents.includes(item.parentName))
1045
+ parents.push(item.parentName);
1046
+ }
1047
+ }
1042
1048
  }
1043
- if (!parents.includes(parentName))
1044
- parents.push(parentName);
1045
1049
  }
1046
1050
  }
1047
1051
  // Build per-file type environment + constructor bindings in a single AST walk.
1048
1052
  // Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
1049
1053
  const parentMap = fileParentMap;
1050
- const provider = getProvider(language);
1051
1054
  const typeEnv = buildTypeEnv(tree, language, {
1052
1055
  parentMap,
1053
1056
  enclosingFunctionFinder: provider?.enclosingFunctionFinder,
@@ -1291,7 +1294,23 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1291
1294
  // ── Path 2: Generic extraction via @call.name ────────────────
1292
1295
  if (callNameNode) {
1293
1296
  const calledName = callNameNode.text;
1294
- // Dispatch: route language-specific calls (heritage, properties, imports)
1297
+ // Check heritage extractor for call-based heritage (e.g., Ruby include/extend/prepend)
1298
+ if (provider.heritageExtractor?.extractFromCall) {
1299
+ const heritageItems = provider.heritageExtractor.extractFromCall(calledName, callNode, { filePath: file.path, language });
1300
+ if (heritageItems !== null) {
1301
+ for (const item of heritageItems) {
1302
+ result.heritage.push({
1303
+ filePath: file.path,
1304
+ className: item.className,
1305
+ parentName: item.parentName,
1306
+ kind: item.kind,
1307
+ });
1308
+ }
1309
+ continue;
1310
+ }
1311
+ }
1312
+ // Dispatch: route language-specific calls (properties, imports)
1313
+ // Heritage routing is handled by heritageExtractor.extractFromCall above.
1295
1314
  const routed = callRouter?.(calledName, captureMap['call']);
1296
1315
  if (routed) {
1297
1316
  if (routed.kind === 'skip')
@@ -1304,17 +1323,6 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1304
1323
  });
1305
1324
  continue;
1306
1325
  }
1307
- if (routed.kind === 'heritage') {
1308
- for (const item of routed.items) {
1309
- result.heritage.push({
1310
- filePath: file.path,
1311
- className: item.enclosingClass,
1312
- parentName: item.mixinName,
1313
- kind: item.heritageKind,
1314
- });
1315
- }
1316
- continue;
1317
- }
1318
1326
  if (routed.kind === 'properties') {
1319
1327
  const propEnclosingInfo = cachedFindEnclosingClassInfo(captureMap['call'], file.path, provider.resolveEnclosingOwner);
1320
1328
  const propEnclosingClassId = propEnclosingInfo?.classId ?? null;
@@ -1453,40 +1461,29 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1453
1461
  }
1454
1462
  continue;
1455
1463
  }
1456
- // Extract heritage (extends/implements)
1464
+ // Extract heritage (extends/implements) via provider heritage extractor
1457
1465
  if (captureMap['heritage.class']) {
1458
- if (captureMap['heritage.extends']) {
1459
- // Go struct embedding: the query matches ALL field_declarations with
1460
- // type_identifier, but only anonymous fields (no name) are embedded.
1461
- // Named fields like `Breed string` also match — skip them.
1462
- const extendsNode = captureMap['heritage.extends'];
1463
- const fieldDecl = extendsNode.parent;
1464
- const isNamedField = fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name');
1465
- if (!isNamedField) {
1466
+ if (provider.heritageExtractor) {
1467
+ const heritageItems = provider.heritageExtractor.extract(captureMap, {
1468
+ filePath: file.path,
1469
+ language,
1470
+ });
1471
+ for (const item of heritageItems) {
1466
1472
  result.heritage.push({
1467
1473
  filePath: file.path,
1468
- className: captureMap['heritage.class'].text,
1469
- parentName: captureMap['heritage.extends'].text,
1470
- kind: 'extends',
1474
+ className: item.className,
1475
+ parentName: item.parentName,
1476
+ kind: item.kind,
1471
1477
  });
1472
1478
  }
1479
+ // When the extractor consumes the match, skip symbol processing below.
1480
+ if (heritageItems.length > 0) {
1481
+ continue;
1482
+ }
1473
1483
  }
1474
- if (captureMap['heritage.implements']) {
1475
- result.heritage.push({
1476
- filePath: file.path,
1477
- className: captureMap['heritage.class'].text,
1478
- parentName: captureMap['heritage.implements'].text,
1479
- kind: 'implements',
1480
- });
1481
- }
1482
- if (captureMap['heritage.trait']) {
1483
- result.heritage.push({
1484
- filePath: file.path,
1485
- className: captureMap['heritage.class'].text,
1486
- parentName: captureMap['heritage.trait'].text,
1487
- kind: 'trait-impl',
1488
- });
1489
- }
1484
+ // Fallback: the extractor returned [] (or is absent), but the match still
1485
+ // carries a heritage-specific capture. The match belongs to a heritage
1486
+ // clause and must not fall through to generic symbol processing.
1490
1487
  if (captureMap['heritage.extends'] ||
1491
1488
  captureMap['heritage.implements'] ||
1492
1489
  captureMap['heritage.trait']) {
@@ -9,4 +9,10 @@ export interface PipelineResult {
9
9
  totalFileCount: number;
10
10
  communityResult?: CommunityDetectionResult;
11
11
  processResult?: ProcessDetectionResult;
12
+ /**
13
+ * True if the parse phase spawned a worker pool for this run. False means
14
+ * the sequential fallback handled every chunk. Primarily a test affordance
15
+ * so regression suites can prove which path executed.
16
+ */
17
+ usedWorkerPool: boolean;
12
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.2-rc.21",
3
+ "version": "1.6.2-rc.22",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",