gitnexus 1.6.6-rc.134 → 1.6.6-rc.135

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;
@@ -7,16 +7,33 @@ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
7
7
  import { generateId } from '../../../../lib/utils.js';
8
8
  const HERITAGE_PREFIX = '__heritage__:';
9
9
  const PROPERTY_PREFIX = '__property__:';
10
+ /**
11
+ * #1991: resolve a BARE mixin reference (`include Loggable`) to a nested module by
12
+ * the INCLUDING class's lexical scope — Ruby looks up a constant in the innermost
13
+ * enclosing scope first. For owner `App.S`, try `App.Loggable`, then walk outward.
14
+ * Returns undefined if no enclosing-scope-qualified module exists.
15
+ */
16
+ function qualifyMixinByOwnerScope(mixinName, ownerName, graphIdByName) {
17
+ let prefix = ownerName;
18
+ let dot = prefix.lastIndexOf('.');
19
+ while (dot !== -1) {
20
+ prefix = prefix.slice(0, dot);
21
+ const g = graphIdByName.get(`${prefix}.${mixinName}`);
22
+ if (g !== undefined)
23
+ return g;
24
+ dot = prefix.lastIndexOf('.');
25
+ }
26
+ return undefined;
27
+ }
10
28
  function emitRubyMixinEdges(graph, parsedFiles, nodeLookup) {
11
29
  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.
30
+ // Secondary tail -> graphId map. The `__heritage__` marker carries the mixin
31
+ // TARGET as the bare written name (`arg.text`, e.g. `Loggable`), not its full
32
+ // qualifiedName, so a nested mixin module included by its short name misses the
33
+ // full-qn map. We first resolve it lexically by the including class's enclosing
34
+ // scope (`qualifyMixinByOwnerScope`); this tail map is the last resort. A genuine
35
+ // same-tail collision is mapped to `null` so we REFUSE to guess (#1991) rather
36
+ // than the old first-wins, which cross-wired App::Loggable / Web::Loggable.
20
37
  const graphIdByTail = new Map();
21
38
  for (const parsed of parsedFiles) {
22
39
  for (const def of parsed.localDefs) {
@@ -34,8 +51,13 @@ function emitRubyMixinEdges(graph, parsedFiles, nodeLookup) {
34
51
  graphIdByName.set(fullName, graphId);
35
52
  const dot = fullName.lastIndexOf('.');
36
53
  const tail = dot === -1 ? fullName : fullName.slice(dot + 1);
37
- if (tail.length > 0 && !graphIdByTail.has(tail))
38
- graphIdByTail.set(tail, graphId);
54
+ if (tail.length > 0) {
55
+ const existingTail = graphIdByTail.get(tail);
56
+ if (existingTail === undefined)
57
+ graphIdByTail.set(tail, graphId);
58
+ else if (existingTail !== null && existingTail !== graphId)
59
+ graphIdByTail.set(tail, null); // same-tail collision — refuse to guess
60
+ }
39
61
  }
40
62
  }
41
63
  }
@@ -56,9 +78,14 @@ function emitRubyMixinEdges(graph, parsedFiles, nodeLookup) {
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}`;
@@ -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.
@@ -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.135",
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",