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
@@ -1,19 +1,41 @@
1
1
  /**
2
- * MRO (Method Resolution Order) strategy — shared between CLI and any
3
- * future consumer that reasons about multiple-inheritance semantics.
4
- *
5
- * Lives in `gitnexus-shared` so the low-level resolution module
6
- * (`core/ingestion/model/resolve.ts`) does not need to import from
7
- * `languages/` — keeping the `model/` layer free of language-registry
8
- * coupling.
9
- *
10
- * Strategy semantics:
11
- * - `first-wins`: BFS ancestor walk, first match wins (default).
12
- * - `leftmost-base`: BFS ancestor walk, leftmost base wins (C++).
13
- * - `c3`: C3-linearized ancestor order, first match wins (Python).
14
- * - `implements-split`: BFS walk, first match wins (Java/C#/Kotlin) — full
15
- * interface-default ambiguity is handled at graph level.
16
- * - `qualified-syntax`: No auto-resolution (Rust — requires `<T as Trait>::m`).
2
+ * MRO (Method Resolution Order) strategy — shared canonical definition.
3
+ *
4
+ * Lives in `gitnexus-shared` so `model/resolve.ts` and `mro-processor.ts` share
5
+ * the type without importing the language registry (avoids circular coupling).
6
+ *
7
+ * `first-wins` (default, Java/C#/Kotlin/Go/Swift/Dart):
8
+ * BFS ancestor walk in declaration order; first match wins.
9
+ *
10
+ * `leftmost-base` (C++):
11
+ * BFS walk; HeritageMap preserves source insertion order, so BFS naturally
12
+ * picks the leftmost base in diamond inheritance.
13
+ *
14
+ * `c3` (Python):
15
+ * C3-linearization; falls back to BFS on cyclic/inconsistent hierarchy.
16
+ * See model/resolve.ts § c3Linearize.
17
+ *
18
+ * `implements-split` (Java/C#/Kotlin):
19
+ * Low-level lookup is BFS; graph-level mro-processor detects and warns on
20
+ * interface-default method ambiguity.
21
+ *
22
+ * `qualified-syntax` (Rust):
23
+ * No auto-resolution — `lookupMethodByOwnerWithMRO` returns undefined immediately.
24
+ * Rust requires explicit `<Type as Trait>::method` syntax.
25
+ *
26
+ * `ruby-mixin` (Ruby):
27
+ * Kind-aware walk that does NOT short-circuit on direct owner first (`prepend`
28
+ * must beat the class's own method). Walk order:
29
+ * 1. Prepend providers (reverse declaration — last-prepended wins)
30
+ * 2. Direct owner's own methods
31
+ * 3. Include providers (reverse declaration)
32
+ * 4. Transitive ancestors (BFS fallback)
33
+ * Singleton dispatch: caller passes `ancestryOverride` (extend providers only);
34
+ * becomes a simple left-to-right scan. Miss NEVER falls through to file-scoped
35
+ * lookup — null-routes or honors `fallback`.
36
+ *
37
+ * @see model/resolve.ts § lookupMethodByOwnerWithMRO
38
+ * @see languages/ruby.ts § selectDispatch
17
39
  */
18
- export type MroStrategy = 'first-wins' | 'c3' | 'leftmost-base' | 'implements-split' | 'qualified-syntax';
40
+ export type MroStrategy = 'first-wins' | 'c3' | 'leftmost-base' | 'implements-split' | 'qualified-syntax' | 'ruby-mixin';
19
41
  //# sourceMappingURL=mro-strategy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mro-strategy.d.ts","sourceRoot":"","sources":["../src/mro-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,IAAI,GACJ,eAAe,GACf,kBAAkB,GAClB,kBAAkB,CAAC"}
1
+ {"version":3,"file":"mro-strategy.d.ts","sourceRoot":"","sources":["../src/mro-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,IAAI,GACJ,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,YAAY,CAAC"}
@@ -123,7 +123,7 @@ export declare const _resolveCallTargetForTesting: (call: Pick<ExtractedCall, "c
123
123
  * @param ctx - Resolution context
124
124
  * @param heritageMap - Optional heritage map for MRO-aware ancestor walking
125
125
  */
126
- export declare const resolveMemberCall: (ownerType: string, methodName: string, currentFile: string, ctx: ResolutionContext, heritageMap?: HeritageMap, argCount?: number) => ResolveResult | null;
126
+ export declare const resolveMemberCall: (ownerType: string, methodName: string, currentFile: string, ctx: ResolutionContext, heritageMap?: HeritageMap, argCount?: number, ancestryView?: "instance" | "singleton") => ResolveResult | null;
127
127
  /**
128
128
  * Resolve a free-function call using `lookupExact` (same-file) + import-scoped
129
129
  * resolution via `ctx.resolve()`.
@@ -1,4 +1,25 @@
1
1
  import { CLASS_TYPES, CALL_TARGET_TYPES, lookupMethodByOwnerWithMRO } from './model/index.js';
2
+ /**
3
+ * DAG stage 4 fallback: used when `selectDispatch` is absent or returns null.
4
+ * Preserves pre-DAG dispatch semantics:
5
+ * - 'constructor' → constructor branch
6
+ * - 'free' → free branch (admits Swift/Kotlin class-target fast path)
7
+ * - 'member' or undefined → owner-scoped branch
8
+ *
9
+ * `undefined` callForm MUST route through owner-scoped (not free) so bare
10
+ * identifiers without a classified shape do NOT trigger `resolveFreeCall`'s
11
+ * class-target fast path. Without a `receiverTypeName`, the owner-scoped
12
+ * branch falls through to `resolveModuleAliasedCall` + `singleCandidate`,
13
+ * matching legacy behavior where non-callable symbols (Class, Interface)
14
+ * null-route instead of producing spurious Constructor edges.
15
+ */
16
+ const defaultDispatchDecision = (callForm) => {
17
+ if (callForm === 'constructor')
18
+ return { primary: 'constructor' };
19
+ if (callForm === 'free')
20
+ return { primary: 'free' };
21
+ return { primary: 'owner-scoped' };
22
+ };
2
23
  import Parser from 'tree-sitter';
3
24
  import { TIER_CONFIDENCE } from './model/resolution-context.js';
4
25
  import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
@@ -609,23 +630,27 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
609
630
  // Extract heritage from query matches to build parentMap for buildTypeEnv.
610
631
  // Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
611
632
  const fileParentMap = new Map();
612
- for (const match of matches) {
613
- const captureMap = {};
614
- match.captures.forEach((c) => (captureMap[c.name] = c.node));
615
- if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
616
- const className = captureMap['heritage.class'].text;
617
- const parentName = captureMap['heritage.extends'].text;
618
- const extendsNode = captureMap['heritage.extends'];
619
- const fieldDecl = extendsNode.parent;
620
- if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
621
- continue;
622
- let parents = fileParentMap.get(className);
623
- if (!parents) {
624
- parents = [];
625
- fileParentMap.set(className, parents);
633
+ if (provider.heritageExtractor) {
634
+ for (const match of matches) {
635
+ const captureMap = {};
636
+ match.captures.forEach((c) => (captureMap[c.name] = c.node));
637
+ if (captureMap['heritage.class']) {
638
+ const heritageItems = provider.heritageExtractor.extract(captureMap, {
639
+ filePath: file.path,
640
+ language,
641
+ });
642
+ for (const item of heritageItems) {
643
+ if (item.kind === 'extends') {
644
+ let parents = fileParentMap.get(item.className);
645
+ if (!parents) {
646
+ parents = [];
647
+ fileParentMap.set(item.className, parents);
648
+ }
649
+ if (!parents.includes(item.parentName))
650
+ parents.push(item.parentName);
651
+ }
652
+ }
626
653
  }
627
- if (!parents.includes(parentName))
628
- parents.push(parentName);
629
654
  }
630
655
  }
631
656
  const parentMap = fileParentMap;
@@ -786,22 +811,29 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
786
811
  if (!nameNode)
787
812
  return;
788
813
  const calledName = nameNode.text;
814
+ // Check heritage extractor for call-based heritage (e.g., Ruby include/extend/prepend)
815
+ if (provider.heritageExtractor?.extractFromCall) {
816
+ const heritageItems = provider.heritageExtractor.extractFromCall(calledName, captureMap['call'], { filePath: file.path, language });
817
+ if (heritageItems !== null) {
818
+ for (const item of heritageItems) {
819
+ collectedHeritage.push({
820
+ filePath: file.path,
821
+ className: item.className,
822
+ parentName: item.parentName,
823
+ kind: item.kind,
824
+ });
825
+ }
826
+ return;
827
+ }
828
+ }
829
+ // Dispatch: route language-specific calls (properties, imports)
830
+ // Heritage routing is handled by heritageExtractor.extractFromCall above.
789
831
  const routed = callRouter?.(calledName, captureMap['call']);
790
832
  if (routed) {
791
833
  switch (routed.kind) {
792
834
  case 'skip':
793
835
  case 'import':
794
836
  return;
795
- case 'heritage':
796
- for (const item of routed.items) {
797
- collectedHeritage.push({
798
- filePath: file.path,
799
- className: item.enclosingClass,
800
- parentName: item.mixinName,
801
- kind: item.heritageKind,
802
- });
803
- }
804
- return;
805
837
  case 'properties': {
806
838
  const fileId = generateId('File', file.path);
807
839
  const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
@@ -852,9 +884,16 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
852
884
  }
853
885
  if (provider.isBuiltInName(calledName))
854
886
  return;
855
- const callForm = inferCallForm(callNode, nameNode);
856
- const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
887
+ // --- DAG stage 2-3: classify-form + infer-receiver (shared defaults) ---
888
+ // These stages run the shared inference chain. Language providers can
889
+ // customize infer-receiver (stage 3) via the inferImplicitReceiver hook
890
+ // which runs AFTER this default chain (typed-binding → constructor-map →
891
+ // module-alias → class-as-receiver → mixed-chain), and selectDispatch
892
+ // (stage 4) which picks the resolver branch.
893
+ let callForm = inferCallForm(callNode, nameNode);
894
+ let receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
857
895
  let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
896
+ let receiverSource = receiverTypeName ? 'typed-binding' : 'none';
858
897
  // Phase P: virtual dispatch override — when the declared type is a base class but
859
898
  // the constructor created a known subclass, prefer the more specific type.
860
899
  // Checks per-file parentMap first, then falls back to globalParentMap for
@@ -890,6 +929,7 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
890
929
  (ctx.model.types.lookupClassByName(ctorType).length > 0 &&
891
930
  ctx.model.types.lookupClassByName(receiverTypeName).length > 0)) {
892
931
  receiverTypeName = ctorType;
932
+ receiverSource = 'constructor-map';
893
933
  }
894
934
  }
895
935
  }
@@ -898,18 +938,25 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
898
938
  const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx, provider);
899
939
  const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
900
940
  receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName);
941
+ if (receiverTypeName)
942
+ receiverSource = 'constructor-map';
901
943
  }
902
- // Fall back to class-as-receiver for static method calls (e.g. UserService.find_user()).
903
- // When the receiver name is not a variable in TypeEnv but resolves to a Class/Struct/Interface
904
- // through the standard tiered resolution, use it directly as the receiver type.
944
+ // Fall back to class-as-receiver for static method calls (e.g. UserService.find_user(),
945
+ // Greetable.format()). When the receiver name is not a variable in TypeEnv but
946
+ // resolves to a class-like symbol (Class / Interface / Struct / Enum / Trait) via
947
+ // tiered resolution, use it directly as the receiver type. `Trait` is included so
948
+ // Ruby module class-method calls flow through the class-as-receiver path and reach
949
+ // the `selectDispatch` hook's singleton branch.
905
950
  if (!receiverTypeName && receiverName && callForm === 'member') {
906
951
  const typeResolved = ctx.resolve(receiverName, file.path);
907
952
  if (typeResolved &&
908
953
  typeResolved.candidates.some((d) => d.type === 'Class' ||
909
954
  d.type === 'Interface' ||
910
955
  d.type === 'Struct' ||
911
- d.type === 'Enum')) {
956
+ d.type === 'Enum' ||
957
+ d.type === 'Trait')) {
912
958
  receiverTypeName = receiverName;
959
+ receiverSource = 'class-as-receiver';
913
960
  }
914
961
  }
915
962
  // Hoist sourceId so it's available for ACCESSES edge emission during chain walk.
@@ -941,10 +988,48 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
941
988
  }
942
989
  if (currentType) {
943
990
  receiverTypeName = walkMixedChain(extracted.chain, currentType, file.path, ctx, makeAccessEmitter(graph, sourceId), heritageMap);
991
+ if (receiverTypeName)
992
+ receiverSource = 'mixed-chain';
944
993
  }
945
994
  }
946
995
  }
947
996
  }
997
+ // --- DAG stage 3: infer-receiver (provider hook) ---
998
+ // Synthesize implicit receivers for languages that omit them (e.g., Ruby bare-call).
999
+ // This hook runs AFTER the shared inference chain so explicit receivers /
1000
+ // typed bindings always take precedence. Output (if non-null) overlays onto
1001
+ // the ReceiverEnriched for the next stage.
1002
+ let dispatchHint;
1003
+ if (provider.inferImplicitReceiver) {
1004
+ const override = provider.inferImplicitReceiver({
1005
+ calledName,
1006
+ callForm,
1007
+ receiverName,
1008
+ receiverTypeName,
1009
+ callNode,
1010
+ filePath: file.path,
1011
+ });
1012
+ if (override) {
1013
+ callForm = override.callForm;
1014
+ receiverName = override.receiverName;
1015
+ receiverTypeName = override.receiverTypeName;
1016
+ receiverSource = override.receiverSource;
1017
+ dispatchHint = override.hint;
1018
+ }
1019
+ }
1020
+ // --- DAG stage 4: select-dispatch (provider hook + default fallback) ---
1021
+ // Decide which resolver path to try first (primary) and fallback strategy.
1022
+ // Language providers can customize dispatch via selectDispatch hook; all
1023
+ // others use the shared defaultDispatchDecision. Always non-null after this
1024
+ // block so downstream resolvers are table-driven.
1025
+ const dispatchDecision = provider.selectDispatch?.({
1026
+ calledName,
1027
+ callForm,
1028
+ receiverName,
1029
+ receiverTypeName,
1030
+ receiverSource,
1031
+ hint: dispatchHint,
1032
+ }) ?? defaultDispatchDecision(callForm);
948
1033
  // Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
949
1034
  // Only used when multiple candidates survive arity filtering — ~1-3% of calls.
950
1035
  const langConfig = provider.typeConfig;
@@ -957,7 +1042,7 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
957
1042
  callForm,
958
1043
  receiverTypeName,
959
1044
  receiverName,
960
- }, file.path, ctx, hints, widenCache, undefined, heritageMap);
1045
+ }, file.path, ctx, hints, widenCache, undefined, heritageMap, dispatchDecision);
961
1046
  if (!resolved)
962
1047
  return;
963
1048
  const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
@@ -1359,26 +1444,45 @@ const singleCandidate = (tiered, argCount, callForm) => {
1359
1444
  };
1360
1445
  /** @internal Exported for unit tests. Do not use outside tests. */
1361
1446
  export const _resolveCallTargetForTesting = (call, currentFile, ctx, opts) => resolveCallTarget(call, currentFile, ctx, opts?.overloadHints, opts?.widenCache, opts?.preComputedArgTypes, opts?.heritageMap);
1362
- const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, preComputedArgTypes, heritageMap) => {
1447
+ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, preComputedArgTypes, heritageMap, dispatchDecision) => {
1363
1448
  const tiered = ctx.resolve(call.calledName, currentFile);
1364
1449
  if (!tiered)
1365
1450
  return null;
1366
- if (call.callForm === 'free') {
1451
+ // DAG dispatch: use decision.primary to pick the resolver branch.
1452
+ // Callers that own the DAG (processCalls + crossFile deferred paths)
1453
+ // pass a decision; other callers use the shared default ladder.
1454
+ // Language-specific primary / fallback / ancestryView overrides come from
1455
+ // the provider's `selectDispatch` hook.
1456
+ const decision = dispatchDecision ?? defaultDispatchDecision(call.callForm);
1457
+ const primary = decision.primary;
1458
+ if (primary === 'free') {
1367
1459
  return resolveFreeCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes);
1368
1460
  }
1369
- if (call.callForm === 'constructor') {
1461
+ if (primary === 'constructor') {
1370
1462
  return (resolveStaticCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes) ?? singleCandidate(tiered, call.argCount, 'constructor'));
1371
1463
  }
1464
+ // primary === 'owner-scoped'
1372
1465
  if (call.receiverTypeName) {
1373
1466
  // Skip the owner-scoped MRO path when the tiered pool has genuine
1374
1467
  // overload ambiguity that needs D1-D4+E handling, not D0.
1375
1468
  const skipMember = (!!overloadHints || !!preComputedArgTypes) &&
1376
1469
  countCallableCandidates(tiered.candidates, call.argCount, call.callForm) > 1;
1377
1470
  // Try owner-scoped (resolveMemberCall) then file-scoped (resolveMemberCallByFile).
1471
+ // DAG: dispatchDecision.ancestryView selects instance vs singleton ancestry
1472
+ // for kind-aware MRO strategies. Ruby `Account.log` flows via 'singleton'.
1473
+ //
1474
+ // Singleton-ancestry miss MUST NOT degrade to the file-scoped fallback:
1475
+ // resolveMemberCallByFile matches by ownerId and would happily pick an
1476
+ // instance method defined on the same class, leaking instance dispatch
1477
+ // onto what was declared a class-method call. For singleton dispatch,
1478
+ // a miss either null-routes or falls through to `decision.fallback`.
1479
+ const singletonDispatch = decision.ancestryView === 'singleton';
1378
1480
  const memberResult = (!skipMember
1379
- ? resolveMemberCall(call.receiverTypeName, call.calledName, currentFile, ctx, heritageMap, call.argCount)
1481
+ ? resolveMemberCall(call.receiverTypeName, call.calledName, currentFile, ctx, heritageMap, call.argCount, decision.ancestryView)
1380
1482
  : null) ??
1381
- resolveMemberCallByFile(call.calledName, call.receiverTypeName, currentFile, ctx, call.argCount, call.callForm, overloadHints, preComputedArgTypes);
1483
+ (singletonDispatch
1484
+ ? null
1485
+ : resolveMemberCallByFile(call.calledName, call.receiverTypeName, currentFile, ctx, call.argCount, call.callForm, overloadHints, preComputedArgTypes));
1382
1486
  if (memberResult)
1383
1487
  return memberResult;
1384
1488
  // Module-alias narrowing runs as a FALLBACK, after owner/file-scoped
@@ -1411,7 +1515,19 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, pr
1411
1515
  // hierarchy. When the type is NOT in the index (PHP `mixed`, dynamic
1412
1516
  // types, unresolvable aliases), the scoped resolvers had nothing to
1413
1517
  // work with and singleCandidate is the correct last resort.
1518
+ //
1519
+ // DAG fallback override: when `select-dispatch` returned
1520
+ // `fallback: 'free-arity-narrowed'` (today: Ruby implicit-self bare
1521
+ // calls whose enclosing class doesn't define the method), fall through
1522
+ // to free-call resolution instead of null-routing. This preserves
1523
+ // existing free-call arity-narrowing heuristics for bare calls that
1524
+ // happen to target methods on unrelated classes.
1414
1525
  if (typeResolves && typeResolves.candidates.length > 0) {
1526
+ if (decision.fallback === 'free-arity-narrowed') {
1527
+ const free = resolveFreeCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes);
1528
+ if (free)
1529
+ return free;
1530
+ }
1415
1531
  return null; // null-route: type resolved, no candidate matched
1416
1532
  }
1417
1533
  return singleCandidate(tiered, call.argCount, call.callForm);
@@ -1567,7 +1683,14 @@ const resolveFieldOwnership = (receiverName, fieldName, filePath, ctx) => {
1567
1683
  * Threaded out here so callers don't need a second `ctx.resolve(ownerType, ...)` call —
1568
1684
  * this decouples callers from `ctx.resolve`'s per-file caching contract.
1569
1685
  */
1570
- const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, heritageMap, argCount) => {
1686
+ const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, heritageMap, argCount,
1687
+ /**
1688
+ * DAG-sourced ancestry selector. `'singleton'` routes through
1689
+ * `heritageMap.getSingletonAncestry(owner)` for class-method dispatch
1690
+ * (Ruby `Account.log` via `extend LoggerMixin`). Default / undefined
1691
+ * uses the walker's instance-dispatch behavior.
1692
+ */
1693
+ ancestryView) => {
1571
1694
  const typeResolved = ctx.resolve(receiverTypeName, filePath);
1572
1695
  if (!typeResolved)
1573
1696
  return undefined;
@@ -1595,8 +1718,15 @@ const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, herit
1595
1718
  for (const candidate of typeResolved.candidates) {
1596
1719
  if (!CLASS_LIKE_TYPES.has(candidate.type))
1597
1720
  continue;
1721
+ // Singleton dispatch: when the DAG decision requested the singleton
1722
+ // ancestry view, pass `heritageMap.getSingletonAncestry` as the walker's
1723
+ // ancestry override. Kind-aware strategies (e.g. MroStrategy 'ruby-mixin')
1724
+ // honor the override by scanning it linearly in place of their default walk.
1725
+ const singletonOverride = ancestryView === 'singleton' && canWalkMRO && heritageMap
1726
+ ? heritageMap.getSingletonAncestry(candidate.nodeId).map((e) => e.parentId)
1727
+ : undefined;
1598
1728
  const def = canWalkMRO
1599
- ? lookupMethodByOwnerWithMRO(candidate.nodeId, methodName, heritageMap, ctx.model, mroStrategy, argCount)
1729
+ ? lookupMethodByOwnerWithMRO(candidate.nodeId, methodName, heritageMap, ctx.model, mroStrategy, argCount, singletonOverride)
1600
1730
  : ctx.model.methods.lookupMethodByOwner(candidate.nodeId, methodName, argCount);
1601
1731
  if (!def)
1602
1732
  continue;
@@ -1643,8 +1773,8 @@ const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, herit
1643
1773
  * @param ctx - Resolution context
1644
1774
  * @param heritageMap - Optional heritage map for MRO-aware ancestor walking
1645
1775
  */
1646
- export const resolveMemberCall = (ownerType, methodName, currentFile, ctx, heritageMap, argCount) => {
1647
- const resolved = resolveMethodByOwner(ownerType, methodName, currentFile, ctx, heritageMap, argCount);
1776
+ export const resolveMemberCall = (ownerType, methodName, currentFile, ctx, heritageMap, argCount, ancestryView) => {
1777
+ const resolved = resolveMethodByOwner(ownerType, methodName, currentFile, ctx, heritageMap, argCount, ancestryView);
1648
1778
  if (!resolved)
1649
1779
  return null;
1650
1780
  return toResolveResult(resolved.def, resolved.tier);
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Shared Ruby call routing logic.
3
3
  *
4
- * Ruby expresses imports, heritage (mixins), and property definitions as
5
- * method calls rather than syntax-level constructs. This module provides a
6
- * routing function used by the CLI call-processor, CLI parse-worker, and
7
- * the web call-processor so that the classification logic lives in one place.
4
+ * Ruby expresses imports and property definitions as method calls rather
5
+ * than syntax-level constructs. This module provides a routing function
6
+ * used by the CLI call-processor, CLI parse-worker, and the web
7
+ * call-processor so that the classification logic lives in one place.
8
+ *
9
+ * Heritage (mixins: include/extend/prepend) was previously routed here
10
+ * but is now handled by heritageExtractor.extractFromCall before the
11
+ * call router runs. The router still returns 'skip' for these calls.
8
12
  *
9
13
  * NOTE: This file is intentionally duplicated in gitnexus-web/ because the
10
14
  * two packages have separate build targets (Node native vs WASM/browser).
@@ -24,9 +28,6 @@ export type RubyCallRouting = {
24
28
  kind: 'import';
25
29
  importPath: string;
26
30
  isRelative: boolean;
27
- } | {
28
- kind: 'heritage';
29
- items: RubyHeritageItem[];
30
31
  } | {
31
32
  kind: 'properties';
32
33
  items: RubyPropertyItem[];
@@ -35,11 +36,6 @@ export type RubyCallRouting = {
35
36
  } | {
36
37
  kind: 'skip';
37
38
  };
38
- export interface RubyHeritageItem {
39
- enclosingClass: string;
40
- mixinName: string;
41
- heritageKind: 'include' | 'extend' | 'prepend';
42
- }
43
39
  export type RubyAccessorType = 'attr_accessor' | 'attr_reader' | 'attr_writer';
44
40
  export interface RubyPropertyItem {
45
41
  propName: string;
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Shared Ruby call routing logic.
3
3
  *
4
- * Ruby expresses imports, heritage (mixins), and property definitions as
5
- * method calls rather than syntax-level constructs. This module provides a
6
- * routing function used by the CLI call-processor, CLI parse-worker, and
7
- * the web call-processor so that the classification logic lives in one place.
4
+ * Ruby expresses imports and property definitions as method calls rather
5
+ * than syntax-level constructs. This module provides a routing function
6
+ * used by the CLI call-processor, CLI parse-worker, and the web
7
+ * call-processor so that the classification logic lives in one place.
8
+ *
9
+ * Heritage (mixins: include/extend/prepend) was previously routed here
10
+ * but is now handled by heritageExtractor.extractFromCall before the
11
+ * call router runs. The router still returns 'skip' for these calls.
8
12
  *
9
13
  * NOTE: This file is intentionally duplicated in gitnexus-web/ because the
10
14
  * two packages have separate build targets (Node native vs WASM/browser).
@@ -13,8 +17,6 @@
13
17
  // ── Pre-allocated singletons for common return values ────────────────────────
14
18
  const CALL_RESULT = { kind: 'call' };
15
19
  const SKIP_RESULT = { kind: 'skip' };
16
- /** Max depth for parent-walking loops to prevent pathological AST traversals */
17
- const MAX_PARENT_DEPTH = 50;
18
20
  // ── Routing function ────────────────────────────────────────────────────────
19
21
  /**
20
22
  * Classify a Ruby call node and extract its semantic payload.
@@ -42,35 +44,12 @@ export function routeRubyCall(calledName, callNode) {
42
44
  }
43
45
  return { kind: 'import', importPath, isRelative };
44
46
  }
45
- // ── include / extend / prepend heritage (mixin) ──────────────────────
47
+ // ── include / extend / prepend heritage (now handled by heritageExtractor)
48
+ // Call-based heritage is intercepted by heritageExtractor.extractFromCall
49
+ // before the call router runs. Return SKIP_RESULT so these calls don't
50
+ // fall through to normal call processing.
46
51
  if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
47
- let enclosingClass = null;
48
- let current = callNode.parent;
49
- let depth = 0;
50
- while (current && ++depth <= MAX_PARENT_DEPTH) {
51
- if (current.type === 'class' || current.type === 'module') {
52
- const nameNode = current.childForFieldName?.('name');
53
- if (nameNode) {
54
- enclosingClass = nameNode.text;
55
- break;
56
- }
57
- }
58
- current = current.parent;
59
- }
60
- if (!enclosingClass)
61
- return SKIP_RESULT;
62
- const items = [];
63
- const argList = callNode.childForFieldName?.('arguments');
64
- for (const arg of argList?.children ?? []) {
65
- if (arg.type === 'constant' || arg.type === 'scope_resolution') {
66
- items.push({
67
- enclosingClass,
68
- mixinName: arg.text,
69
- heritageKind: calledName,
70
- });
71
- }
72
- }
73
- return items.length > 0 ? { kind: 'heritage', items } : SKIP_RESULT;
52
+ return SKIP_RESULT;
74
53
  }
75
54
  // ── attr_accessor / attr_reader / attr_writer → property definitions ───
76
55
  if (calledName === 'attr_accessor' ||
@@ -58,3 +58,78 @@ export interface CallExtractionConfig {
58
58
  */
59
59
  typeAsReceiverHeuristic?: boolean;
60
60
  }
61
+ /**
62
+ * DAG stage 3 output: call record with receiver type and source discriminant.
63
+ *
64
+ * `receiverTypeName` is resolved via TypeEnv → constructor-map → class-as-receiver →
65
+ * mixed-chain, or synthesized by `inferImplicitReceiver`. `receiverSource` tags
66
+ * which path won and drives MRO strategy selection in stage 4.
67
+ *
68
+ * Invariants:
69
+ * - `receiverSource` MUST match how `receiverTypeName` was resolved; every
70
+ * discriminant must have a live reader and writer.
71
+ * - `hint` is opaque to shared stages; only the same provider's `selectDispatch` reads it.
72
+ *
73
+ * @see language-provider.ts § inferImplicitReceiver, selectDispatch
74
+ */
75
+ export interface ReceiverEnriched {
76
+ readonly calledName: string;
77
+ readonly callForm: 'free' | 'member' | 'constructor' | undefined;
78
+ readonly receiverName: string | undefined;
79
+ readonly receiverTypeName: string | undefined;
80
+ readonly receiverSource: 'none' | 'typed-binding' | 'constructor-map' | 'class-as-receiver' | 'mixed-chain' | 'implicit-self';
81
+ /** Free-form hint from the provider hook; opaque to shared stages. */
82
+ readonly hint?: string;
83
+ }
84
+ /**
85
+ * Provider hook output for `LanguageProvider.inferImplicitReceiver` (DAG stage 3).
86
+ *
87
+ * Overlay applied to `ReceiverEnriched` when an implicit receiver is synthesized.
88
+ * Ruby example: bare `serialize` inside `Account#call_serialize` →
89
+ * `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
90
+ * receiverSource: 'implicit-self', hint: 'instance' }`
91
+ *
92
+ * Invariants:
93
+ * - `receiverSource` is always `'implicit-self'` — the only variant this type produces.
94
+ * - `callForm` is always `'member'` — the rewrite converts bare-call to method invocation.
95
+ * - `hint` is opaque to shared stages; consumed by the same language's `selectDispatch`.
96
+ */
97
+ export interface ImplicitReceiverOverride {
98
+ readonly callForm: 'free' | 'member' | 'constructor';
99
+ readonly receiverName: string;
100
+ readonly receiverTypeName: string;
101
+ readonly receiverSource: Extract<ReceiverEnriched['receiverSource'], 'implicit-self'>;
102
+ /** Free-form language tag (e.g. Ruby sets 'singleton' for `def self.foo`
103
+ * method bodies). Consumed by the same language's `selectDispatch` hook. */
104
+ readonly hint?: string;
105
+ }
106
+ /**
107
+ * DAG stage 4 output: dispatch strategy for resolving the target method.
108
+ *
109
+ * Encodes which resolver branch to try first and an optional fallback.
110
+ * Stage 5 delegates to `resolveMemberCall`, `resolveFreeCall`, or
111
+ * `resolveStaticCall` based on `primary`.
112
+ *
113
+ * - `primary`: `'owner-scoped'` = MRO walk, `'free'` = arity-tiered global lookup,
114
+ * `'constructor'` = type instantiation.
115
+ * - `fallback`: Only `'free-arity-narrowed'` exists; used by Ruby implicit-self
116
+ * to degrade to arity-tiered free lookup when the MRO walk misses.
117
+ * - `ancestryView`: Ruby `'ruby-mixin'` only. `'singleton'` walks extend providers
118
+ * only; a miss NEVER falls through to file-scoped lookup (enforced in
119
+ * resolveCallTarget). `'instance'` is the default.
120
+ *
121
+ * Common patterns:
122
+ * - `{primary: 'constructor'}` — constructor call
123
+ * - `{primary: 'owner-scoped'}` — member call with known type
124
+ * - `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}` — Ruby implicit-self
125
+ * - `{primary: 'owner-scoped', ancestryView: 'singleton'}` — Ruby class-method call
126
+ *
127
+ * @see language-provider.ts § selectDispatch
128
+ * @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
129
+ */
130
+ export interface DispatchDecision {
131
+ readonly primary: 'owner-scoped' | 'free' | 'constructor';
132
+ readonly fallback?: 'free-arity-narrowed';
133
+ readonly ancestryView?: 'instance' | 'singleton';
134
+ readonly hint?: string;
135
+ }
@@ -0,0 +1,13 @@
1
+ import type { HeritageExtractionConfig } from '../../heritage-types.js';
2
+ /**
3
+ * Go heritage extraction config.
4
+ *
5
+ * Go struct embedding: the tree-sitter query matches ALL field_declarations
6
+ * with type_identifier, but only anonymous fields (no name) are embedded.
7
+ * Named fields like `Breed string` also match — skip them.
8
+ *
9
+ * The shouldSkipExtends hook checks if the extends node's parent is a
10
+ * field_declaration with a named field child, indicating a regular
11
+ * (non-embedded) field that should not produce a heritage record.
12
+ */
13
+ export declare const goHeritageConfig: HeritageExtractionConfig;
@@ -0,0 +1,20 @@
1
+ // gitnexus/src/core/ingestion/heritage-extractors/configs/go.ts
2
+ import { SupportedLanguages } from '../../../../_shared/index.js';
3
+ /**
4
+ * Go heritage extraction config.
5
+ *
6
+ * Go struct embedding: the tree-sitter query matches ALL field_declarations
7
+ * with type_identifier, but only anonymous fields (no name) are embedded.
8
+ * Named fields like `Breed string` also match — skip them.
9
+ *
10
+ * The shouldSkipExtends hook checks if the extends node's parent is a
11
+ * field_declaration with a named field child, indicating a regular
12
+ * (non-embedded) field that should not produce a heritage record.
13
+ */
14
+ export const goHeritageConfig = {
15
+ language: SupportedLanguages.Go,
16
+ shouldSkipExtends(extendsNode) {
17
+ const fieldDecl = extendsNode.parent;
18
+ return fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName?.('name') != null;
19
+ },
20
+ };