gitnexus 1.6.6-rc.134 → 1.6.6-rc.136

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.
@@ -124,6 +124,13 @@ export function createClassExtractor(config) {
124
124
  extractQualifiedName(node, simpleName) {
125
125
  return extract(node, { name: simpleName })?.qualifiedName ?? null;
126
126
  },
127
+ // #1991: qualify a non-typeDeclaration scope node (e.g. a Ruby `module` → Trait)
128
+ // by the same ancestor-scope walk the node-id path uses, so two same-tail nested
129
+ // mixin modules stay distinct. extract()/extractQualifiedName cannot be reused —
130
+ // they bail on non-typeDeclarations (a module is not in typeDeclarationNodes).
131
+ qualifyScopeName(node, simpleName) {
132
+ return buildQualifiedName(node, simpleName);
133
+ },
127
134
  shouldSkipClassCapture(context) {
128
135
  return config.shouldSkipClassCapture?.(context) ?? false;
129
136
  },
@@ -34,6 +34,14 @@ export interface ClassExtractor {
34
34
  type?: NodeLabel | null;
35
35
  }): ExtractedClassSymbol | null;
36
36
  extractQualifiedName(node: SyntaxNode, simpleName: string): string | null;
37
+ /**
38
+ * #1991: qualify a scope-defining node that maps to a class-like registry label
39
+ * (e.g. a Ruby `module` → Trait) but is NOT a typeDeclaration, so it cannot go
40
+ * through extract()/extractQualifiedName (which bail on non-typeDeclarations).
41
+ * Walks the same ancestor scopes as the node-id path. Optional — only providers
42
+ * that materialize such nodes implement it.
43
+ */
44
+ qualifyScopeName?(node: SyntaxNode, simpleName: string): string;
37
45
  shouldSkipClassCapture?(context: ClassCaptureContext & {
38
46
  nodeLabel: ClassLikeNodeLabel;
39
47
  }): boolean;
@@ -33,7 +33,7 @@ import { getDartParser, getDartScopeQuery } from './query.js';
33
33
  import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
34
34
  import { getTreeSitterBufferSize } from '../../constants.js';
35
35
  import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
36
- import { DART_HERITAGE_PREFIX } from './interpret.js';
36
+ import { encodeMarker } from '../../utils/heritage-marker.js';
37
37
  import { DART_BUILT_INS } from './built-ins.js';
38
38
  const FUNCTION_DECL_TAGS = [
39
39
  '@declaration.function',
@@ -439,7 +439,7 @@ function emitHeritageMarkers(container, kind, className, out) {
439
439
  const c = container.namedChild(i);
440
440
  if (c === null || c.type !== 'type_identifier')
441
441
  continue;
442
- const payload = `${DART_HERITAGE_PREFIX}${kind}:${c.text}:${className}`;
442
+ const payload = encodeMarker('heritage', [kind, c.text, className]);
443
443
  out.push({ '@import.heritage': syntheticCapture('@import.heritage', c, payload) });
444
444
  }
445
445
  }
@@ -15,8 +15,10 @@
15
15
  */
16
16
  import type { CaptureMatch, ParsedImport, ParsedTypeBinding } from '../../../../_shared/index.js';
17
17
  /** Marker prefix carried on a side-effect `ParsedImport.targetRaw` for
18
- * `implements`/`with` heritage, consumed by `emitDartHeritageEdges`. */
19
- export declare const DART_HERITAGE_PREFIX = "__heritage__:";
18
+ * `implements`/`with` heritage, consumed by `emitDartHeritageEdges`. Aliased to
19
+ * the shared codec prefix (#1994) so the Dart wire prefix has a single source of
20
+ * truth and cannot desync from `encodeMarker`/`decodeMarker`. */
21
+ export declare const DART_HERITAGE_PREFIX: string;
20
22
  export declare function interpretDartImport(captures: CaptureMatch): ParsedImport | null;
21
23
  export declare function normalizeDartType(text: string): string;
22
24
  export declare function interpretDartTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null;
@@ -13,9 +13,12 @@
13
13
  * normalizing the Dart type (strip nullable `?`, unwrap single-arg
14
14
  * container generics like `Future<X>`/`List<X>`, drop library prefixes).
15
15
  */
16
+ import { HERITAGE_MARKER_PREFIX } from '../../utils/heritage-marker.js';
16
17
  /** Marker prefix carried on a side-effect `ParsedImport.targetRaw` for
17
- * `implements`/`with` heritage, consumed by `emitDartHeritageEdges`. */
18
- export const DART_HERITAGE_PREFIX = '__heritage__:';
18
+ * `implements`/`with` heritage, consumed by `emitDartHeritageEdges`. Aliased to
19
+ * the shared codec prefix (#1994) so the Dart wire prefix has a single source of
20
+ * truth and cannot desync from `encodeMarker`/`decodeMarker`. */
21
+ export const DART_HERITAGE_PREFIX = HERITAGE_MARKER_PREFIX;
19
22
  function stripQuotes(s) {
20
23
  return s.replace(/^['"]|['"]$/g, '');
21
24
  }
@@ -31,7 +31,8 @@ import { populateClassOwnedMembers, isClassLike } from '../../scope-resolution/s
31
31
  import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
32
32
  import { generateId } from '../../../../lib/utils.js';
33
33
  import { dartProvider } from '../dart.js';
34
- import { dartArityCompatibility, dartMergeBindings, resolveDartImportTarget, DART_HERITAGE_PREFIX, } from './index.js';
34
+ import { dartArityCompatibility, dartMergeBindings, resolveDartImportTarget } from './index.js';
35
+ import { decodeMarker } from '../../utils/heritage-marker.js';
35
36
  import { expandDartWildcardNames } from './expand-wildcards.js';
36
37
  /**
37
38
  * Resolve a class simple name to a graph id with FILE AFFINITY: prefer a
@@ -88,9 +89,12 @@ function emitDartHeritageEdges(graph, parsedFiles, nodeLookup) {
88
89
  for (const parsed of parsedFiles) {
89
90
  for (const imp of parsed.parsedImports) {
90
91
  const raw = imp.targetRaw;
91
- if (typeof raw !== 'string' || !raw.startsWith(DART_HERITAGE_PREFIX))
92
+ if (typeof raw !== 'string')
92
93
  continue;
93
- const parts = raw.slice(DART_HERITAGE_PREFIX.length).split(':');
94
+ const decoded = decodeMarker(raw);
95
+ if (decoded?.kind !== 'heritage')
96
+ continue;
97
+ const parts = decoded.fields;
94
98
  if (parts.length < 3)
95
99
  continue;
96
100
  const [kind, baseName, childName] = parts;
@@ -5,6 +5,7 @@ import { synthesizeRubyReceiverBinding, findEnclosingClassOrModule } from './rec
5
5
  import { getTreeSitterBufferSize } from '../../constants.js';
6
6
  import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
7
7
  import { splitQualifiedName } from '../../utils/qualified-name.js';
8
+ import { encodeMarker } from '../../utils/heritage-marker.js';
8
9
  const FUNCTION_NODE_TYPES = ['method', 'singleton_method'];
9
10
  const HERITAGE_CALL_NAMES = new Set(['include', 'extend', 'prepend']);
10
11
  const ATTR_CALL_NAMES = new Set([
@@ -159,7 +160,7 @@ export function emitRubyScopeCaptures(sourceText, _filePath, cachedTree) {
159
160
  out.push({
160
161
  '@import.statement': grouped['@reference.call.free'],
161
162
  '@import.kind': syntheticCapture('@import.kind', callNode, 'namespace'),
162
- '@import.source': syntheticCapture('@import.source', callNode, `__heritage__:${callName}:${mixinName}:${ownerName}`),
163
+ '@import.source': syntheticCapture('@import.source', callNode, encodeMarker('heritage', [callName, mixinName, ownerName])),
163
164
  '@import.name': syntheticCapture('@import.name', callNode, mixinName),
164
165
  });
165
166
  }
@@ -188,7 +189,7 @@ export function emitRubyScopeCaptures(sourceText, _filePath, cachedTree) {
188
189
  out.push({
189
190
  '@import.statement': grouped['@reference.call.free'],
190
191
  '@import.kind': syntheticCapture('@import.kind', callNode, 'namespace'),
191
- '@import.source': syntheticCapture('@import.source', callNode, `__property__:${callName}:${propName}:${ownerName}`),
192
+ '@import.source': syntheticCapture('@import.source', callNode, encodeMarker('property', [callName, propName, ownerName])),
192
193
  '@import.name': syntheticCapture('@import.name', callNode, propName),
193
194
  });
194
195
  // Emit a property declaration so the property flows into
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { resolveRubyImportInternal } from '../../import-resolvers/ruby.js';
10
10
  import { buildSuffixIndex } from '../../import-resolvers/utils.js';
11
+ import { isHeritageMarker } from '../../utils/heritage-marker.js';
11
12
  // ─── resolveRubyImportTarget ──────────────────────────────────────────────
12
13
  /**
13
14
  * ScopeResolver-shaped adapter:
@@ -25,7 +26,7 @@ import { buildSuffixIndex } from '../../import-resolvers/utils.js';
25
26
  export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, _resolutionConfig) {
26
27
  if (!targetRaw)
27
28
  return null;
28
- if (targetRaw.startsWith('__heritage__:') || targetRaw.startsWith('__property__:'))
29
+ if (isHeritageMarker(targetRaw))
29
30
  return null;
30
31
  const fromNormalized = fromFile.replace(/\\/g, '/');
31
32
  const fromDir = fromNormalized.includes('/')
@@ -1,3 +1,4 @@
1
+ import { isHeritageMarker } from '../../utils/heritage-marker.js';
1
2
  // ─── interpretImport ──────────────────────────────────────────────────────
2
3
  /**
3
4
  * Interpret a pre-decomposed Ruby import capture into a `ParsedImport`.
@@ -18,7 +19,7 @@ export function interpretRubyImport(captures) {
18
19
  return null;
19
20
  // Heritage-encoded imports (__heritage__:include:Serializable:User)
20
21
  // are stored as namespace imports so emitHeritageEdges can read them.
21
- if (source.startsWith('__heritage__:') || source.startsWith('__property__:')) {
22
+ if (isHeritageMarker(source)) {
22
23
  const name = captures['@import.name']?.text ?? source;
23
24
  return { kind: 'namespace', localName: name, importedName: name, targetRaw: source };
24
25
  }
@@ -5,18 +5,34 @@ import { rubyArityCompatibility, rubyMergeBindings, resolveRubyImportTarget } fr
5
5
  import { populateClassOwnedMembers, isClassLike } from '../../scope-resolution/scope/walkers.js';
6
6
  import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
7
7
  import { generateId } from '../../../../lib/utils.js';
8
- const HERITAGE_PREFIX = '__heritage__:';
9
- const PROPERTY_PREFIX = '__property__:';
8
+ import { decodeMarker } from '../../utils/heritage-marker.js';
9
+ /**
10
+ * #1991: resolve a BARE mixin reference (`include Loggable`) to a nested module by
11
+ * the INCLUDING class's lexical scope — Ruby looks up a constant in the innermost
12
+ * enclosing scope first. For owner `App.S`, try `App.Loggable`, then walk outward.
13
+ * Returns undefined if no enclosing-scope-qualified module exists.
14
+ */
15
+ function qualifyMixinByOwnerScope(mixinName, ownerName, graphIdByName) {
16
+ let prefix = ownerName;
17
+ let dot = prefix.lastIndexOf('.');
18
+ while (dot !== -1) {
19
+ prefix = prefix.slice(0, dot);
20
+ const g = graphIdByName.get(`${prefix}.${mixinName}`);
21
+ if (g !== undefined)
22
+ return g;
23
+ dot = prefix.lastIndexOf('.');
24
+ }
25
+ return undefined;
26
+ }
10
27
  function emitRubyMixinEdges(graph, parsedFiles, nodeLookup) {
11
28
  const graphIdByName = new Map();
12
- // Secondary tail -> graphId map (first-wins). The `__heritage__` marker carries
13
- // the mixin TARGET as the bare written name (`arg.text`, e.g. `Loggable`), not
14
- // its full qualifiedName, so a nested mixin module included by its short name
15
- // (`include Loggable` where it is `App::Loggable`) misses the full-qn map and
16
- // its IMPLEMENTS edge is silently dropped (#1982 follow-up). The tail fallback
17
- // recovers it. OWNER (`className`) lookups stay full-qn only, preserving
18
- // same-tail owner disambiguation; only the under-qualified mixin reference
19
- // falls back, and a genuine same-tail mixin tie there resolves first-wins.
29
+ // Secondary tail -> graphId map. The `__heritage__` marker carries the mixin
30
+ // TARGET as the bare written name (`arg.text`, e.g. `Loggable`), not its full
31
+ // qualifiedName, so a nested mixin module included by its short name misses the
32
+ // full-qn map. We first resolve it lexically by the including class's enclosing
33
+ // scope (`qualifyMixinByOwnerScope`); this tail map is the last resort. A genuine
34
+ // same-tail collision is mapped to `null` so we REFUSE to guess (#1991) rather
35
+ // than the old first-wins, which cross-wired App::Loggable / Web::Loggable.
20
36
  const graphIdByTail = new Map();
21
37
  for (const parsed of parsedFiles) {
22
38
  for (const def of parsed.localDefs) {
@@ -34,8 +50,13 @@ function emitRubyMixinEdges(graph, parsedFiles, nodeLookup) {
34
50
  graphIdByName.set(fullName, graphId);
35
51
  const dot = fullName.lastIndexOf('.');
36
52
  const tail = dot === -1 ? fullName : fullName.slice(dot + 1);
37
- if (tail.length > 0 && !graphIdByTail.has(tail))
38
- graphIdByTail.set(tail, graphId);
53
+ if (tail.length > 0) {
54
+ const existingTail = graphIdByTail.get(tail);
55
+ if (existingTail === undefined)
56
+ graphIdByTail.set(tail, graphId);
57
+ else if (existingTail !== null && existingTail !== graphId)
58
+ graphIdByTail.set(tail, null); // same-tail collision — refuse to guess
59
+ }
39
60
  }
40
61
  }
41
62
  }
@@ -49,16 +70,22 @@ function emitRubyMixinEdges(graph, parsedFiles, nodeLookup) {
49
70
  }
50
71
  for (const parsed of parsedFiles) {
51
72
  for (const imp of parsed.parsedImports) {
52
- if (!imp.targetRaw.startsWith(HERITAGE_PREFIX))
73
+ const decoded = decodeMarker(imp.targetRaw);
74
+ if (decoded?.kind !== 'heritage')
53
75
  continue;
54
- const parts = imp.targetRaw.slice(HERITAGE_PREFIX.length).split(':');
76
+ const parts = decoded.fields;
55
77
  if (parts.length < 3)
56
78
  continue;
57
79
  const [kind, mixinName, className] = parts;
58
80
  const classGraphId = graphIdByName.get(className);
59
- // Owner stays full-qn; the mixin target may be written by short name and
60
- // miss the full-qn map, so fall back to the simple-tail map (#1982).
61
- const mixinGraphId = graphIdByName.get(mixinName) ?? graphIdByTail.get(mixinName);
81
+ // Owner stays full-qn. The mixin target may be written by short name and miss
82
+ // the full-qn map; resolve it lexically by the including class's enclosing
83
+ // scope (`App::S` + `Loggable` -> `App::Loggable`), then fall back to the tail
84
+ // map ONLY when unambiguous — never first-wins on a collision (#1982/#1991).
85
+ const mixinGraphId = graphIdByName.get(mixinName) ??
86
+ qualifyMixinByOwnerScope(mixinName, className, graphIdByName) ??
87
+ graphIdByTail.get(mixinName) ??
88
+ undefined;
62
89
  if (classGraphId === undefined || mixinGraphId === undefined)
63
90
  continue;
64
91
  const edgeKey = `${classGraphId}->${mixinGraphId}:${kind}`;
@@ -87,9 +114,10 @@ function emitRubyMixinEdges(graph, parsedFiles, nodeLookup) {
87
114
  }
88
115
  for (const parsed of parsedFiles) {
89
116
  for (const imp of parsed.parsedImports) {
90
- if (!imp.targetRaw.startsWith(PROPERTY_PREFIX))
117
+ const decoded = decodeMarker(imp.targetRaw);
118
+ if (decoded?.kind !== 'property')
91
119
  continue;
92
- const parts = imp.targetRaw.slice(PROPERTY_PREFIX.length).split(':');
120
+ const parts = decoded.fields;
93
121
  if (parts.length < 3)
94
122
  continue;
95
123
  const [_attrKind, propName, className] = parts;
@@ -7,7 +7,7 @@ import { extractVueScript, isVueSetupTopLevel } from './vue-sfc-extractor.js';
7
7
  import { yieldToEventLoop } from './utils/event-loop.js';
8
8
  import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
9
9
  import { isVerboseIngestionEnabled } from './utils/verbose.js';
10
- import { buildConcreteTypedefDefinitionRanges, getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, getLabelFromCaptures, isSuppressedConcreteTypedefDuplicate, qualifyRustImplTargetByModScope, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
10
+ import { buildConcreteTypedefDefinitionRanges, getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, getLabelFromCaptures, isSuppressedConcreteTypedefDuplicate, isQualifiableScopeLabel, qualifyRustImplTargetByModScope, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
11
11
  import { detectFrameworkFromAST } from './framework-detection.js';
12
12
  import { buildTypeEnv } from './type-env.js';
13
13
  import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, parameterShapeIdTag, } from './utils/method-props.js';
@@ -450,7 +450,14 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
450
450
  // the qualified class node id computed below. Gated on the flag, so the
451
451
  // owner walk and its cache entry are byte-identical when the flag is off.
452
452
  const getQualifiedOwnerName = provider.classExtractor?.qualifiedNodeId === true
453
- ? (node, simpleName) => provider.classExtractor.extractQualifiedName(node, simpleName)
453
+ ? (node, simpleName) =>
454
+ // #1991: a Ruby `module` owner is not a typeDeclaration, so
455
+ // extractQualifiedName returns null; fall back to the scope walk so a
456
+ // method inside a nested module owns through the SAME qualified Trait
457
+ // id its node uses (App.Loggable), not a dangling bare id.
458
+ provider.classExtractor.extractQualifiedName(node, simpleName) ??
459
+ provider.classExtractor.qualifyScopeName?.(node, simpleName) ??
460
+ null
454
461
  : undefined;
455
462
  const enclosingClassInfo = needsOwner
456
463
  ? cachedFindEnclosingClassInfo(nameNode || definitionNodeForRange, file.path, provider.resolveEnclosingOwner, getQualifiedOwnerName)
@@ -468,7 +475,16 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
468
475
  const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
469
476
  (classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
470
477
  ? (provider.classExtractor.extractQualifiedName(classNodeForSymbol, nodeName) ?? nodeName)
471
- : undefined);
478
+ : // #1991: a Ruby `module` maps to Trait (class-like registry) but is not a
479
+ // typeDeclaration, so extractQualifiedName bails. Qualify it via the scope
480
+ // walk so two same-tail nested mixin modules get distinct ids. Gated on
481
+ // qualifiedNodeId, so languages without the flag are unaffected.
482
+ isQualifiableScopeLabel(nodeLabel) &&
483
+ provider.classExtractor?.qualifiedNodeId === true &&
484
+ classNodeForSymbol
485
+ ? (provider.classExtractor.qualifyScopeName?.(classNodeForSymbol, nodeName) ??
486
+ undefined)
487
+ : undefined);
472
488
  // Qualify method/property IDs with enclosing class name to avoid collisions
473
489
  // e.g. "Method:animal.dart:Animal.speak" vs "Method:animal.dart:Dog.speak".
474
490
  // Class-like nodes use their own fully-qualified path as the id key when the
@@ -487,13 +503,16 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
487
503
  : undefined;
488
504
  const qualifiedName = rustImplQualifiedName !== undefined
489
505
  ? rustImplQualifiedName
490
- : isClassLikeLabel &&
491
- provider.classExtractor?.qualifiedNodeId === true &&
492
- qualifiedTypeName !== undefined
493
- ? qualifiedTypeName
494
- : enclosingClassInfo
495
- ? `${enclosingClassInfo.className}.${nodeName}`
496
- : nodeName;
506
+ : // #1991: include Trait so a Ruby mixin module's qualified scope id keys
507
+ // the node, mirroring the class-like path (qualifiedTypeName is computed
508
+ // for Trait above).
509
+ (isClassLikeLabel || isQualifiableScopeLabel(nodeLabel)) &&
510
+ provider.classExtractor?.qualifiedNodeId === true &&
511
+ qualifiedTypeName !== undefined
512
+ ? qualifiedTypeName
513
+ : enclosingClassInfo
514
+ ? `${enclosingClassInfo.className}.${nodeName}`
515
+ : nodeName;
497
516
  // Extract method metadata for Function/Method/Constructor nodes BEFORE generating
498
517
  // the node ID — parameterCount is needed to disambiguate overloaded methods.
499
518
  // Use the per-language MethodExtractor for method metadata (isAbstract, isStatic,
@@ -17,6 +17,17 @@ export type SyntaxNode = Parser.SyntaxNode;
17
17
  * parse-worker mirrors this so the owner edge and node id agree byte-for-byte.
18
18
  */
19
19
  export declare const qualifyRustImplTargetByModScope: (implNode: SyntaxNode, rawTargetText: string) => string;
20
+ /**
21
+ * #1991: scope-label predicate that single-sources the `nodeLabel === 'Trait'`
22
+ * checks in parsing-processor.ts / parse-worker.ts. A Ruby `module` maps to the
23
+ * `Trait` registry label but is NOT a typeDeclaration, so `extractQualifiedName`
24
+ * bails on it; these node labels are instead qualified via the scope walk
25
+ * (`qualifyScopeName`) so same-tail nested modules get distinct ids. Keeping the
26
+ * literal in one place stops the four hand-maintained copies (two each in the
27
+ * sequential and worker definition paths) from drifting apart. Pure predicate —
28
+ * value-identical to the inlined `nodeLabel === 'Trait'`.
29
+ */
30
+ export declare const isQualifiableScopeLabel: (nodeLabel: string) => boolean;
20
31
  /**
21
32
  * Ordered list of definition capture keys for tree-sitter query matches.
22
33
  * Used to extract the definition node from a capture map.
@@ -28,6 +28,17 @@ export const qualifyRustImplTargetByModScope = (implNode, rawTargetText) => {
28
28
  }
29
29
  return [...modSegments, ...splitQualifiedName(rawTargetText)].filter(Boolean).join('.');
30
30
  };
31
+ /**
32
+ * #1991: scope-label predicate that single-sources the `nodeLabel === 'Trait'`
33
+ * checks in parsing-processor.ts / parse-worker.ts. A Ruby `module` maps to the
34
+ * `Trait` registry label but is NOT a typeDeclaration, so `extractQualifiedName`
35
+ * bails on it; these node labels are instead qualified via the scope walk
36
+ * (`qualifyScopeName`) so same-tail nested modules get distinct ids. Keeping the
37
+ * literal in one place stops the four hand-maintained copies (two each in the
38
+ * sequential and worker definition paths) from drifting apart. Pure predicate —
39
+ * value-identical to the inlined `nodeLabel === 'Trait'`.
40
+ */
41
+ export const isQualifiableScopeLabel = (nodeLabel) => nodeLabel === 'Trait';
31
42
  /**
32
43
  * Ordered list of definition capture keys for tree-sitter query matches.
33
44
  * Used to extract the definition node from a capture map.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * #1994: shared codec for the synthetic `__heritage__:` / `__property__:` import
3
+ * markers used by the Ruby and Dart scope resolvers to carry side-effect facts
4
+ * (mixin includes, attr_accessor properties) through the import channel. Both
5
+ * languages share the exact `':'`-delimited wire format, so a single encode/decode
6
+ * pair removes the per-site hand-rolled string handling that produced the #1981
7
+ * edge-drop. Language-NEUTRAL — keyed only on the literal prefixes; no provider
8
+ * branching belongs here.
9
+ */
10
+ export type MarkerKind = 'heritage' | 'property';
11
+ export declare const HERITAGE_MARKER_PREFIX: string;
12
+ export declare const PROPERTY_MARKER_PREFIX: string;
13
+ /**
14
+ * Build a marker string `<prefix><field>:<field>:...`. The `':'` delimiter IS the
15
+ * wire format, so a field that itself contains `':'` is structurally invalid and
16
+ * THROWS — callers must pre-normalize colon-bearing values (e.g. a qualified mixin
17
+ * arg `Outer::Mixin` → `Outer.Mixin`). This makes the #1981 silent edge-drop a
18
+ * loud failure instead.
19
+ */
20
+ export declare function encodeMarker(kind: MarkerKind, fields: readonly string[]): string;
21
+ /**
22
+ * Parse a marker string back into its kind + positional fields, or `null` if `raw`
23
+ * is not a marker. Mirrors the historical `slice(PREFIX.length).split(':')`.
24
+ */
25
+ export declare function decodeMarker(raw: string): {
26
+ kind: MarkerKind;
27
+ fields: string[];
28
+ } | null;
29
+ /**
30
+ * True if `raw` is a synthetic heritage/property marker — exactly the prior
31
+ * `startsWith('__heritage__:') || startsWith('__property__:')` pair.
32
+ */
33
+ export declare function isHeritageMarker(raw: string): boolean;
@@ -0,0 +1,41 @@
1
+ const PREFIX_BY_KIND = {
2
+ heritage: '__heritage__:',
3
+ property: '__property__:',
4
+ };
5
+ export const HERITAGE_MARKER_PREFIX = PREFIX_BY_KIND.heritage;
6
+ export const PROPERTY_MARKER_PREFIX = PREFIX_BY_KIND.property;
7
+ /**
8
+ * Build a marker string `<prefix><field>:<field>:...`. The `':'` delimiter IS the
9
+ * wire format, so a field that itself contains `':'` is structurally invalid and
10
+ * THROWS — callers must pre-normalize colon-bearing values (e.g. a qualified mixin
11
+ * arg `Outer::Mixin` → `Outer.Mixin`). This makes the #1981 silent edge-drop a
12
+ * loud failure instead.
13
+ */
14
+ export function encodeMarker(kind, fields) {
15
+ for (const field of fields) {
16
+ if (field.includes(':')) {
17
+ throw new Error(`encodeMarker: field "${field}" contains the ':' delimiter; normalize it before encoding`);
18
+ }
19
+ }
20
+ return PREFIX_BY_KIND[kind] + fields.join(':');
21
+ }
22
+ /**
23
+ * Parse a marker string back into its kind + positional fields, or `null` if `raw`
24
+ * is not a marker. Mirrors the historical `slice(PREFIX.length).split(':')`.
25
+ */
26
+ export function decodeMarker(raw) {
27
+ if (raw.startsWith(PREFIX_BY_KIND.heritage)) {
28
+ return { kind: 'heritage', fields: raw.slice(PREFIX_BY_KIND.heritage.length).split(':') };
29
+ }
30
+ if (raw.startsWith(PREFIX_BY_KIND.property)) {
31
+ return { kind: 'property', fields: raw.slice(PREFIX_BY_KIND.property.length).split(':') };
32
+ }
33
+ return null;
34
+ }
35
+ /**
36
+ * True if `raw` is a synthetic heritage/property marker — exactly the prior
37
+ * `startsWith('__heritage__:') || startsWith('__property__:')` pair.
38
+ */
39
+ export function isHeritageMarker(raw) {
40
+ return raw.startsWith(PREFIX_BY_KIND.heritage) || raw.startsWith(PREFIX_BY_KIND.property);
41
+ }
@@ -38,7 +38,7 @@ try {
38
38
  }
39
39
  catch { }
40
40
  import { getLanguageFromFilename } from '../../../_shared/index.js';
41
- import { buildConcreteTypedefDefinitionRanges, FUNCTION_NODE_TYPES, getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, getLabelFromCaptures, genericFuncName, inferFunctionLabel, isSuppressedConcreteTypedefDuplicate, qualifyRustImplTargetByModScope, CLASS_CONTAINER_TYPES, } from '../utils/ast-helpers.js';
41
+ import { buildConcreteTypedefDefinitionRanges, FUNCTION_NODE_TYPES, getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, getLabelFromCaptures, genericFuncName, inferFunctionLabel, isSuppressedConcreteTypedefDuplicate, isQualifiableScopeLabel, qualifyRustImplTargetByModScope, CLASS_CONTAINER_TYPES, } from '../utils/ast-helpers.js';
42
42
  import { extractCallArgTypes } from '../utils/call-analysis.js';
43
43
  import { buildTypeEnv } from '../type-env.js';
44
44
  import { detectFrameworkFromAST } from '../framework-detection.js';
@@ -1290,7 +1290,14 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1290
1290
  // to the *qualified* class id (Outer.Inner). Gated on the flag → byte-identical
1291
1291
  // when off. Mirrors parsing-processor.ts.
1292
1292
  const getQualifiedOwnerName = provider.classExtractor?.qualifiedNodeId === true
1293
- ? (node, simpleName) => provider.classExtractor.extractQualifiedName(node, simpleName)
1293
+ ? (node, simpleName) =>
1294
+ // #1991: LOCKSTEP — a Ruby `module` owner is not a typeDeclaration, so
1295
+ // extractQualifiedName returns null; fall back to the scope walk so a
1296
+ // method inside a nested module owns through the SAME qualified Trait
1297
+ // id its node uses on the worker path too.
1298
+ provider.classExtractor.extractQualifiedName(node, simpleName) ??
1299
+ provider.classExtractor.qualifyScopeName?.(node, simpleName) ??
1300
+ null
1294
1301
  : undefined;
1295
1302
  const enclosingClassInfo = needsOwner
1296
1303
  ? cachedFindEnclosingClassInfo(nameNode || definitionNode, file.path, provider.resolveEnclosingOwner, getQualifiedOwnerName)
@@ -1306,7 +1313,15 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1306
1313
  const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
1307
1314
  (classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
1308
1315
  ? (provider.classExtractor.extractQualifiedName(classNodeForSymbol, nodeName) ?? nodeName)
1309
- : undefined);
1316
+ : // #1991: LOCKSTEP with parsing-processor.ts — qualify a Ruby `module`
1317
+ // (Trait) via the scope walk so same-tail nested mixin modules get
1318
+ // distinct ids on the worker path too. Gated on qualifiedNodeId.
1319
+ isQualifiableScopeLabel(nodeLabel) &&
1320
+ provider.classExtractor?.qualifiedNodeId === true &&
1321
+ classNodeForSymbol
1322
+ ? (provider.classExtractor.qualifyScopeName?.(classNodeForSymbol, nodeName) ??
1323
+ undefined)
1324
+ : undefined);
1310
1325
  // Qualify method/property IDs with enclosing class name to avoid collisions.
1311
1326
  // Class-like nodes use their own fully-qualified path as the id key when the
1312
1327
  // language enables qualifiedNodeId (#1978); everything else is unchanged.
@@ -1320,13 +1335,15 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1320
1335
  : undefined;
1321
1336
  const qualifiedName = rustImplQualifiedName !== undefined
1322
1337
  ? rustImplQualifiedName
1323
- : isClassLikeLabel &&
1324
- provider.classExtractor?.qualifiedNodeId === true &&
1325
- qualifiedTypeName !== undefined
1326
- ? qualifiedTypeName
1327
- : enclosingClassInfo
1328
- ? `${enclosingClassInfo.className}.${nodeName}`
1329
- : nodeName;
1338
+ : // #1991: LOCKSTEP — include Trait so a Ruby mixin module's qualified
1339
+ // scope id keys the worker-path node, matching the sequential path.
1340
+ (isClassLikeLabel || isQualifiableScopeLabel(nodeLabel)) &&
1341
+ provider.classExtractor?.qualifiedNodeId === true &&
1342
+ qualifiedTypeName !== undefined
1343
+ ? qualifiedTypeName
1344
+ : enclosingClassInfo
1345
+ ? `${enclosingClassInfo.className}.${nodeName}`
1346
+ : nodeName;
1330
1347
  // Extract method metadata BEFORE generating node ID — parameterCount is needed
1331
1348
  // to disambiguate overloaded methods via #<arity> suffix in the ID.
1332
1349
  let declaredType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.134",
3
+ "version": "1.6.6-rc.136",
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",