gitnexus 1.6.10-rc.222 → 1.6.10-rc.223

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.
@@ -17,7 +17,7 @@
17
17
  * migrate.
18
18
  */
19
19
  import { generateId } from '../../../../lib/utils.js';
20
- import { AMBIGUOUS_POSITION, localNameKey, positionKey, qualifiedKey, simpleKey, } from '../graph-bridge/node-lookup.js';
20
+ import { AMBIGUOUS_POSITION, exactPositionKey, localNameKey, positionKey, qualifiedKey, simpleKey, } from '../graph-bridge/node-lookup.js';
21
21
  import { isOverloadableCallable, isPositionQualifiedLocalLabel, } from '../../utils/callable-labels.js';
22
22
  import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
23
23
  import { parameterShapeIdTag } from '../../utils/method-props.js';
@@ -143,13 +143,6 @@ function pickCallerCallableDef(scope, scopes, atRange) {
143
143
  * resolution working for languages that don't yet synthesize
144
144
  * qualifiers).
145
145
  */
146
- /**
147
- * Extract the 1-based declaration line from a scope-resolution def id.
148
- * Shape: `def:<filePath>#<line>:<col>:<...>`; `undefined` when it doesn't match.
149
- */
150
- function defStartLine(nodeId, filePath) {
151
- return definitionIdPosition(nodeId, filePath)?.line;
152
- }
153
146
  /**
154
147
  * Trailing segment of a dotted qualified name (`Outer.inner` -> `inner`),
155
148
  * with any function-local `@line:col` identity suffix stripped
@@ -214,9 +207,21 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
214
207
  // AST nodes (outer wrapper vs inner callable), but the graph node's
215
208
  // `startLine` follows the initializer (#2735) so this join matches even
216
209
  // when the binding is split across lines.
217
- const line = defStartLine(def.nodeId, filePath);
210
+ const definitionPosition = definitionIdPosition(def.nodeId, filePath);
211
+ const line = definitionPosition?.line;
218
212
  if (line !== undefined && isPositionQualifiedLocalLabel(def.type)) {
219
213
  const simple = simpleNameOf(qn);
214
+ if (definitionPosition !== undefined) {
215
+ const exactHit = nodeLookup.get(exactPositionKey(filePath, def.type, definitionPosition.line - 1, definitionPosition.column));
216
+ if (exactHit !== undefined && exactHit !== AMBIGUOUS_POSITION)
217
+ return exactHit;
218
+ if (exactHit === undefined && siblingLabel !== undefined) {
219
+ const siblingExactHit = nodeLookup.get(exactPositionKey(filePath, siblingLabel, definitionPosition.line - 1, definitionPosition.column));
220
+ if (siblingExactHit !== undefined && siblingExactHit !== AMBIGUOUS_POSITION) {
221
+ return siblingExactHit;
222
+ }
223
+ }
224
+ }
220
225
  const posHit = nodeLookup.get(positionKey(filePath, def.type, line - 1, simple));
221
226
  if (posHit !== undefined && posHit !== AMBIGUOUS_POSITION)
222
227
  return posHit;
@@ -51,6 +51,8 @@ export declare function simpleKey(filePath: string, name: string): string;
51
51
  * name-based keys rather than picking by source order.
52
52
  */
53
53
  export declare function positionKey(filePath: string, label: NodeLabel, startLine: number, name: string): string;
54
+ /** Exact source-position key used before the legacy line/name join. */
55
+ export declare function exactPositionKey(filePath: string, label: NodeLabel, startLine: number, startColumn: number): string;
54
56
  /**
55
57
  * Key recording that a FUNCTION-LOCAL callable with this simple name exists in the
56
58
  * file (#2699 follow-up).
@@ -79,6 +79,10 @@ export function simpleKey(filePath, name) {
79
79
  export function positionKey(filePath, label, startLine, name) {
80
80
  return `<p>:${filePath}::${label}::${startLine}::${name}`;
81
81
  }
82
+ /** Exact source-position key used before the legacy line/name join. */
83
+ export function exactPositionKey(filePath, label, startLine, startColumn) {
84
+ return `<pc>:${filePath}::${label}::${startLine}:${startColumn}`;
85
+ }
82
86
  /**
83
87
  * Key recording that a FUNCTION-LOCAL callable with this simple name exists in the
84
88
  * file (#2699 follow-up).
@@ -116,6 +120,10 @@ export function buildGraphNodeLookup(graph) {
116
120
  // ambiguous rather than letting source order decide.
117
121
  const startLine = props.startLine;
118
122
  if (startLine !== undefined && isPositionQualifiedLocalLabel(node.label)) {
123
+ if (props.startColumn !== undefined) {
124
+ const exactK = exactPositionKey(props.filePath, node.label, startLine, props.startColumn);
125
+ lookup.set(exactK, lookup.has(exactK) ? AMBIGUOUS_POSITION : node.id);
126
+ }
119
127
  const posK = positionKey(props.filePath, node.label, startLine, props.name);
120
128
  lookup.set(posK, lookup.has(posK) ? AMBIGUOUS_POSITION : node.id);
121
129
  // A local-identity node carries `@<row>:<col>` on its last name segment. Record
@@ -294,6 +294,16 @@ export interface ObjectLiteralBindingInfo {
294
294
  */
295
295
  ownerName?: string;
296
296
  }
297
+ /**
298
+ * True when an object-literal member is contained by an array before reaching
299
+ * another callable or class boundary.
300
+ *
301
+ * An array does not provide a stable named owner for its elements, so members
302
+ * below one cannot use `<binding>.<member>` identity or ownership. They still
303
+ * need distinct graph identities, however; callers use this predicate to opt
304
+ * into source-position qualification while keeping ownership suppressed.
305
+ */
306
+ export declare const isArrayContainedObjectLiteralMember: (node: SyntaxNode) => boolean;
297
307
  /**
298
308
  * Find the file-scope variable that owns an object literal method definition.
299
309
  *
@@ -980,6 +980,31 @@ getQualifiedOwnerName) => {
980
980
  }
981
981
  return null;
982
982
  };
983
+ /**
984
+ * True when an object-literal member is contained by an array before reaching
985
+ * another callable or class boundary.
986
+ *
987
+ * An array does not provide a stable named owner for its elements, so members
988
+ * below one cannot use `<binding>.<member>` identity or ownership. They still
989
+ * need distinct graph identities, however; callers use this predicate to opt
990
+ * into source-position qualification while keeping ownership suppressed.
991
+ */
992
+ export const isArrayContainedObjectLiteralMember = (node) => {
993
+ let current = node;
994
+ let sawObject = false;
995
+ while (current) {
996
+ if (current.type === 'object')
997
+ sawObject = true;
998
+ if (current.type === 'array' && sawObject)
999
+ return true;
1000
+ if (current !== node &&
1001
+ (FUNCTION_NODE_TYPES.has(current.type) || CLASS_CONTAINER_TYPES.has(current.type))) {
1002
+ return false;
1003
+ }
1004
+ current = current.parent;
1005
+ }
1006
+ return false;
1007
+ };
983
1008
  /**
984
1009
  * Block-statement AST types that disqualify an object-literal binding from
985
1010
  * carrying a HAS_METHOD edge. A `const` declared inside one of these is block-
@@ -1119,6 +1144,12 @@ export const findObjectLiteralBindingInfo = (node, filePath, options) => {
1119
1144
  if (current.type === 'object') {
1120
1145
  objectDepth += 1;
1121
1146
  }
1147
+ if (current !== node && current.type === 'array') {
1148
+ // `const handlers = [{ run() {} }]` has no `handlers.run` member.
1149
+ // Crossing the array would mint a confident but false owner edge; keep
1150
+ // the existing conservative under-approximation used for nested objects.
1151
+ return null;
1152
+ }
1122
1153
  if (current.type === 'variable_declarator' && objectDepth >= 1) {
1123
1154
  if (objectDepth > 1) {
1124
1155
  // Method belongs to a nested object literal; safe under-approximation.
@@ -21,12 +21,12 @@
21
21
  import type { NodeLabel, SymbolDefinition } from '../../../_shared/index.js';
22
22
  import type { SyntaxNode } from '../utils/ast-helpers.js';
23
23
  /**
24
- * Zero-based start row that keys the graph-to-scope position join for a bound
25
- * callable (#2735).
24
+ * Zero-based start position that keys the graph-to-scope join for a bound
25
+ * callable (#2735/#3041).
26
26
  *
27
27
  * Graph-node queries may anchor on an outer binding wrapper while the scope
28
- * channel anchors on the inner callable. The join is line-only, so a multi-line
29
- * binding needs the graph node's `startLine` to follow the semantic definition.
28
+ * channel anchors on the inner callable. A bound graph node therefore follows
29
+ * the semantic definition's line and column rather than the outer wrapper.
30
30
  *
31
31
  * `ParsedFile.localDefs` is the language-agnostic source of that position.
32
32
  * Matching uses only the canonical label, name, and source range; shared worker
@@ -36,6 +36,10 @@ import type { SyntaxNode } from '../utils/ast-helpers.js';
36
36
  * Missing or ambiguous semantic matches retain the wrapper row, preserving the
37
37
  * existing fail-closed behavior.
38
38
  */
39
+ export declare function boundCallableStartPosition(definitionNode: SyntaxNode, nodeName: string, nodeLabel: NodeLabel, localDefs: readonly SymbolDefinition[] | undefined, nameNode?: SyntaxNode | null): {
40
+ readonly row: number;
41
+ readonly column: number;
42
+ };
39
43
  export declare function boundCallableStartRow(definitionNode: SyntaxNode, nodeName: string, nodeLabel: NodeLabel, localDefs: readonly SymbolDefinition[] | undefined, nameNode?: SyntaxNode | null): number;
40
44
  /**
41
45
  * A function-local callable's own name segment: its name plus its declaration
@@ -60,6 +64,10 @@ export declare function boundCallableStartRow(definitionNode: SyntaxNode, nodeNa
60
64
  * bare/class-qualified ids, which is what keeps this off the symbols other
61
65
  * files, saved queries and stored references actually address.
62
66
  */
67
+ export declare const positionQualifiedCallableName: (name: string, position: {
68
+ readonly row: number;
69
+ readonly column: number;
70
+ }) => string;
63
71
  export declare const localIdentity: (node: SyntaxNode, name: string) => string;
64
72
  /**
65
73
  * The qualified name of a callable nested inside another callable — THE single
@@ -40,12 +40,12 @@ function containsPosition(node, row, column) {
40
40
  return true;
41
41
  }
42
42
  /**
43
- * Zero-based start row that keys the graph-to-scope position join for a bound
44
- * callable (#2735).
43
+ * Zero-based start position that keys the graph-to-scope join for a bound
44
+ * callable (#2735/#3041).
45
45
  *
46
46
  * Graph-node queries may anchor on an outer binding wrapper while the scope
47
- * channel anchors on the inner callable. The join is line-only, so a multi-line
48
- * binding needs the graph node's `startLine` to follow the semantic definition.
47
+ * channel anchors on the inner callable. A bound graph node therefore follows
48
+ * the semantic definition's line and column rather than the outer wrapper.
49
49
  *
50
50
  * `ParsedFile.localDefs` is the language-agnostic source of that position.
51
51
  * Matching uses only the canonical label, name, and source range; shared worker
@@ -55,15 +55,17 @@ function containsPosition(node, row, column) {
55
55
  * Missing or ambiguous semantic matches retain the wrapper row, preserving the
56
56
  * existing fail-closed behavior.
57
57
  */
58
- export function boundCallableStartRow(definitionNode, nodeName, nodeLabel, localDefs, nameNode) {
58
+ export function boundCallableStartPosition(definitionNode, nodeName, nodeLabel, localDefs, nameNode) {
59
59
  if (localDefs === undefined)
60
- return definitionNode.startPosition.row;
60
+ return definitionNode.startPosition;
61
61
  const origin = nameNode?.startPosition ?? definitionNode.startPosition;
62
62
  let best;
63
63
  let tied = false;
64
64
  for (const def of localDefs) {
65
- if (def.type !== nodeLabel || simpleDefinitionName(def) !== nodeName)
65
+ if (def.type !== nodeLabel ||
66
+ (simpleDefinitionName(def) !== nodeName && def.qualifiedName !== nodeName)) {
66
67
  continue;
68
+ }
67
69
  const position = definitionIdPosition(def.nodeId, def.filePath);
68
70
  if (position === undefined)
69
71
  continue;
@@ -72,14 +74,20 @@ export function boundCallableStartRow(definitionNode, nodeName, nodeLabel, local
72
74
  continue;
73
75
  const distance = Math.abs(row - origin.row) * 1_000_000 + Math.abs(position.column - origin.column);
74
76
  if (best === undefined || distance < best.distance) {
75
- best = { row, distance };
77
+ best = { row, column: position.column, distance };
76
78
  tied = false;
77
79
  }
78
- else if (distance === best.distance && row !== best.row) {
80
+ else if (distance === best.distance &&
81
+ (row !== best.row || position.column !== best.column)) {
79
82
  tied = true;
80
83
  }
81
84
  }
82
- return best !== undefined && !tied ? best.row : definitionNode.startPosition.row;
85
+ return best !== undefined && !tied
86
+ ? { row: best.row, column: best.column }
87
+ : definitionNode.startPosition;
88
+ }
89
+ export function boundCallableStartRow(definitionNode, nodeName, nodeLabel, localDefs, nameNode) {
90
+ return boundCallableStartPosition(definitionNode, nodeName, nodeLabel, localDefs, nameNode).row;
83
91
  }
84
92
  /**
85
93
  * A function-local callable's own name segment: its name plus its declaration
@@ -104,7 +112,8 @@ export function boundCallableStartRow(definitionNode, nodeName, nodeLabel, local
104
112
  * bare/class-qualified ids, which is what keeps this off the symbols other
105
113
  * files, saved queries and stored references actually address.
106
114
  */
107
- export const localIdentity = (node, name) => `${name}@${node.startPosition.row}:${node.startPosition.column}`;
115
+ export const positionQualifiedCallableName = (name, position) => `${name}@${position.row}:${position.column}`;
116
+ export const localIdentity = (node, name) => positionQualifiedCallableName(name, node.startPosition);
108
117
  /**
109
118
  * The qualified name of a callable nested inside another callable — THE single
110
119
  * definition of that rule, shared by all three id-building phases.
@@ -1,5 +1,5 @@
1
1
  import { parentPort, threadId, workerData } from 'node:worker_threads';
2
- import { boundCallableStartRow, localIdentity, nestedCallableQualifiedName, } from './callable-id.js';
2
+ import { boundCallableStartPosition, localIdentity, nestedCallableQualifiedName, positionQualifiedCallableName, } from './callable-id.js';
3
3
  import Parser from 'tree-sitter';
4
4
  import JavaScript from 'tree-sitter-javascript';
5
5
  import TypeScript from 'tree-sitter-typescript';
@@ -59,7 +59,7 @@ try {
59
59
  }
60
60
  catch { }
61
61
  import { getLanguageFromFilename } from '../../../_shared/index.js';
62
- import { buildDefinitionPreScan, FUNCTION_NODE_TYPES, findAncestorBeforeBoundary, findSplitBodyCallableAncestor, SPLIT_SIGNATURE_NODE_TYPES, getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, findReturnShapeOwnerInfo, isReturnShapeProperty, findMemberAssignmentOwnerInfo, isCjsDefaultExportAssignment, getLabelFromCaptures, genericFuncName, inferFunctionLabel, isSuppressedConcreteTypedefDuplicate, isValueDefinitionLabel, isQualifiableScopeLabel, MEMBER_OWNER_NODE_TYPES, qualifyByEnclosingModScope, qualifyRustImplTargetByModScope, CLASS_CONTAINER_TYPES, PARAMETER_LIST_NODE_TYPES, LOCAL_SCOPE_BODY_NODE_TYPES, } from '../utils/ast-helpers.js';
62
+ import { buildDefinitionPreScan, FUNCTION_NODE_TYPES, findAncestorBeforeBoundary, findSplitBodyCallableAncestor, SPLIT_SIGNATURE_NODE_TYPES, getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, isArrayContainedObjectLiteralMember, findReturnShapeOwnerInfo, isReturnShapeProperty, findMemberAssignmentOwnerInfo, isCjsDefaultExportAssignment, getLabelFromCaptures, genericFuncName, inferFunctionLabel, isSuppressedConcreteTypedefDuplicate, isValueDefinitionLabel, isQualifiableScopeLabel, MEMBER_OWNER_NODE_TYPES, qualifyByEnclosingModScope, qualifyRustImplTargetByModScope, CLASS_CONTAINER_TYPES, PARAMETER_LIST_NODE_TYPES, LOCAL_SCOPE_BODY_NODE_TYPES, } from '../utils/ast-helpers.js';
63
63
  import { isPositionQualifiedLocalLabel } from '../utils/callable-labels.js';
64
64
  import { extractCallArgTypes } from '../utils/call-analysis.js';
65
65
  import { buildTypeEnv } from '../type-env.js';
@@ -406,6 +406,11 @@ const CALLABLE_PREFIX_BOUNDARY_TYPES = new Set([
406
406
  'object_literal', // Kotlin: object : Runnable { ... }
407
407
  'anonymous_object_creation_expression', // C#
408
408
  ]);
409
+ /**
410
+ * Object-literal callables use the binding owner in their identity so spelling
411
+ * a member as a property or shorthand method cannot change its graph semantics.
412
+ */
413
+ const shouldObjectOwnerQualifyCallable = (label) => label === 'Function' || label === 'Method';
409
414
  const enclosingCallablePrefix = (node, filePath, provider) => {
410
415
  // Boundary on class-likes: a method's owner is its CLASS, not whatever
411
416
  // function that class happens to sit inside. `CLASS_CONTAINER_TYPES` alone is
@@ -464,18 +469,33 @@ const callableOwnQualifiedName = (fnNode, filePath, provider) => {
464
469
  // `ownName === null` branch below carries the position INSTEAD of a name,
465
470
  // never in addition to one, so the two spellings cannot stack.
466
471
  const ownName = efnResult?.funcName ?? genericFuncName(fnNode) ?? null;
472
+ let finalLabel = efnResult?.label ?? inferFunctionLabel(fnNode.type);
473
+ if (provider.labelOverride) {
474
+ const override = provider.labelOverride(fnNode, finalLabel);
475
+ if (override !== null)
476
+ finalLabel = override;
477
+ }
467
478
  const prefix = enclosingCallablePrefix(fnNode, filePath, provider);
468
479
  const classInfo = prefix === undefined
469
480
  ? cachedFindEnclosingClassInfo(fnNode, filePath, provider.resolveEnclosingOwner)
470
481
  : null;
471
- const owner = prefix ?? classInfo?.className;
482
+ const objectOwner = prefix === undefined && classInfo === null && shouldObjectOwnerQualifyCallable(finalLabel)
483
+ ? findObjectLiteralBindingInfo(fnNode, filePath, { includeOwnerName: true })?.ownerName
484
+ : undefined;
485
+ const owner = prefix ?? classInfo?.className ?? objectOwner;
486
+ const needsArrayPosition = owner === undefined &&
487
+ ownName !== null &&
488
+ shouldObjectOwnerQualifyCallable(finalLabel) &&
489
+ isArrayContainedObjectLiteralMember(fnNode);
472
490
  const result = prefix !== undefined
473
491
  ? nestedCallableQualifiedName(prefix, fnNode, ownName ?? 'fn')
474
492
  : ownName === null
475
493
  ? localIdentity(fnNode, 'fn')
476
494
  : owner
477
495
  ? `${owner}.${ownName}`
478
- : ownName;
496
+ : needsArrayPosition
497
+ ? positionQualifiedCallableName(ownName, fnNode.startPosition)
498
+ : ownName;
479
499
  callableQualifiedNameCache.set(fnNode, result);
480
500
  return result;
481
501
  };
@@ -518,7 +538,18 @@ const findEnclosingFunctionId = (node, filePath, provider) => {
518
538
  // to the METHOD, not directly to the class, and a Go receiver method can
519
539
  // never itself be nested inside another callable.
520
540
  const nestedPrefix = enclosingCallablePrefix(current, filePath, provider);
521
- const ownerName = nestedPrefix ?? classInfo?.className ?? standaloneMethodInfo?.receiverType ?? undefined;
541
+ const objectOwnerName = nestedPrefix === undefined &&
542
+ classInfo === null &&
543
+ shouldObjectOwnerQualifyCallable(finalLabel)
544
+ ? findObjectLiteralBindingInfo(current, filePath, { includeOwnerName: true })?.ownerName
545
+ : undefined;
546
+ const ownerName = nestedPrefix ??
547
+ classInfo?.className ??
548
+ standaloneMethodInfo?.receiverType ??
549
+ objectOwnerName;
550
+ const needsArrayPosition = ownerName === undefined &&
551
+ shouldObjectOwnerQualifyCallable(finalLabel) &&
552
+ isArrayContainedObjectLiteralMember(current);
522
553
  // Lockstep with the other two id-building phases — see
523
554
  // `nestedCallableQualifiedName`, which is the shared rule. When a
524
555
  // nested prefix exists it IS `ownerName`, so this branch and the
@@ -527,7 +558,9 @@ const findEnclosingFunctionId = (node, filePath, provider) => {
527
558
  ? nestedCallableQualifiedName(nestedPrefix, current, funcName)
528
559
  : ownerName
529
560
  ? `${ownerName}.${funcName}`
530
- : funcName;
561
+ : needsArrayPosition
562
+ ? positionQualifiedCallableName(funcName, current.startPosition)
563
+ : funcName;
531
564
  // Include #<arity> suffix to match definition-phase Method/Constructor IDs.
532
565
  // Use the same MethodExtractor (getMethodInfo) as the definition phase.
533
566
  // When same-arity collisions exist, also append ~type1,type2.
@@ -1658,15 +1691,16 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1658
1691
  // wrapper while scope-resolution anchors on the INNER expression. The
1659
1692
  // position join is line-only, so `startLine` must follow the initializer
1660
1693
  // (ids still use `definitionNode` via `localIdentity`).
1661
- const startRow = definitionNode &&
1694
+ const startPosition = definitionNode &&
1662
1695
  (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor')
1663
- ? boundCallableStartRow(definitionNode, nodeName, nodeLabel, parsedFile?.localDefs, nameNode)
1664
- : definitionNode?.startPosition.row;
1665
- const startLine = startRow !== undefined
1666
- ? startRow + lineOffset
1696
+ ? boundCallableStartPosition(definitionNode, nodeName, nodeLabel, parsedFile?.localDefs, nameNode)
1697
+ : definitionNode?.startPosition;
1698
+ const startLine = startPosition !== undefined
1699
+ ? startPosition.row + lineOffset
1667
1700
  : nameNode
1668
1701
  ? nameNode.startPosition.row + lineOffset
1669
1702
  : lineOffset;
1703
+ const startColumn = startPosition?.column ?? nameNode?.startPosition.column ?? 0;
1670
1704
  // Compute enclosing class BEFORE node ID — needed to qualify method IDs
1671
1705
  const needsOwner = nodeLabel === 'Method' ||
1672
1706
  nodeLabel === 'Constructor' ||
@@ -1733,19 +1767,27 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1733
1767
  // and COLLAPSE INTO ONE node — two distinct settings become one symbol,
1734
1768
  // and the merged name then looks workspace-unique to name inference,
1735
1769
  // which resolves reads of it to a node representing both.
1736
- const objectLiteralOwnerInfo = !enclosingClassId && (nodeLabel === 'Method' || nodeLabel === 'Property') && definitionNode
1770
+ const objectLiteralBindingInfo = !enclosingClassId &&
1771
+ (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Property') &&
1772
+ definitionNode
1773
+ ? findObjectLiteralBindingInfo(definitionNode, file.path, {
1774
+ includeOwnerName: shouldObjectOwnerQualifyCallable(nodeLabel) || nodeLabel === 'Property',
1775
+ })
1776
+ : null;
1777
+ const objectLiteralOwnerInfo = !enclosingClassId &&
1778
+ (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Property') &&
1779
+ definitionNode
1737
1780
  ? (findMemberAssignmentOwnerInfo(definitionNode, file.path) ??
1738
- findObjectLiteralBindingInfo(definitionNode, file.path, {
1739
- // Only `Property` opts into the qualifier; `Method` ids must stay
1740
- // byte-identical or every object-literal method in every indexed
1741
- // repo changes id.
1742
- includeOwnerName: nodeLabel === 'Property',
1743
- }) ??
1781
+ objectLiteralBindingInfo ??
1744
1782
  // R3-4: an anonymous literal in return position is owned by the
1745
1783
  // function whose shape it is. Last in the chain so a variable-bound
1746
1784
  // literal keeps its existing owner and its existing id.
1747
1785
  (nodeLabel === 'Property' ? findReturnShapeOwnerInfo(definitionNode, file.path) : null))
1748
1786
  : null;
1787
+ const isArrayContainedObjectCallable = !enclosingClassId &&
1788
+ shouldObjectOwnerQualifyCallable(nodeLabel) &&
1789
+ definitionNode !== undefined &&
1790
+ isArrayContainedObjectLiteralMember(definitionNode);
1749
1791
  // Provenance for narrowing (R3-4). A return shape is a real definition but
1750
1792
  // the weaker one, and the unique-name pass ranks declared anchors above it
1751
1793
  // so indexing these cannot change an answer that already resolved.
@@ -1829,7 +1871,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1829
1871
  // define `bar` stay distinct nodes.
1830
1872
  objectLiteralOwnerInfo?.ownerName !== undefined
1831
1873
  ? `${objectLiteralOwnerInfo.ownerName}.${nodeName}`
1832
- : nodeName;
1874
+ : isArrayContainedObjectCallable
1875
+ ? positionQualifiedCallableName(nodeName, startPosition)
1876
+ : nodeName;
1833
1877
  // #2742: qualify by the enclosing `mod` chain, so two same-named items at
1834
1878
  // different module depths in one file are DISTINCT nodes. Without this,
1835
1879
  // `mod inner { fn dispatch }` and a crate-root `fn dispatch` both keyed
@@ -2133,6 +2177,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
2133
2177
  name: nodeName,
2134
2178
  filePath: file.path,
2135
2179
  startLine,
2180
+ ...(shouldObjectOwnerQualifyCallable(nodeLabel) &&
2181
+ (objectLiteralBindingInfo?.ownerName || isArrayContainedObjectCallable)
2182
+ ? { startColumn }
2183
+ : {}),
2136
2184
  endLine: definitionNode ? definitionNode.endPosition.row + lineOffset : startLine,
2137
2185
  language: language,
2138
2186
  isExported,
@@ -2194,8 +2242,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
2194
2242
  ? { annotations: methodProps.annotations }
2195
2243
  : {}),
2196
2244
  });
2197
- // Only emit File -> Symbol DEFINES for top-level symbols (issue #1944).
2198
- if (ownerId === undefined) {
2245
+ // Object-literal callables remain file definitions as well as members of
2246
+ // their exported binding. Class members still use HAS_METHOD alone.
2247
+ const isTopLevelObjectCallable = objectLiteralBindingInfo?.ownerName !== undefined &&
2248
+ shouldObjectOwnerQualifyCallable(nodeLabel);
2249
+ if (ownerId === undefined || isTopLevelObjectCallable) {
2199
2250
  const fileId = generateId('File', file.path);
2200
2251
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
2201
2252
  result.relationships.push({
@@ -2217,7 +2268,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
2217
2268
  type: memberEdgeType,
2218
2269
  confidence: 1.0,
2219
2270
  reason: objectLiteralOwnerInfo
2220
- ? 'object literal method belongs to exported object binding'
2271
+ ? 'object literal member belongs to exported object binding'
2221
2272
  : '',
2222
2273
  });
2223
2274
  }
@@ -5158,6 +5158,7 @@ export class LocalBackend {
5158
5158
  summaryOnly: true,
5159
5159
  skipEpistemic: true,
5160
5160
  skipEnrichment: true,
5161
+ hasExplicitRelationTypes,
5161
5162
  });
5162
5163
  }
5163
5164
  catch (e) {
@@ -5394,6 +5395,7 @@ export class LocalBackend {
5394
5395
  limit: Number.isFinite(params.limit) ? params.limit : 100,
5395
5396
  offset: Number.isFinite(params.offset) ? params.offset : 0,
5396
5397
  pdgBridge,
5398
+ hasExplicitRelationTypes,
5397
5399
  });
5398
5400
  return composeUnifiedPdgImpactResult(pdgResult, interproceduralResult);
5399
5401
  }
@@ -5410,6 +5412,7 @@ export class LocalBackend {
5410
5412
  limit: Number.isFinite(params.limit) ? params.limit : 100,
5411
5413
  offset: Number.isFinite(params.offset) ? params.offset : 0,
5412
5414
  summaryOnly: params.summaryOnly,
5415
+ hasExplicitRelationTypes,
5413
5416
  });
5414
5417
  }
5415
5418
  /**
@@ -5747,7 +5750,11 @@ export class LocalBackend {
5747
5750
  const visited = new Set([symId]);
5748
5751
  const pdgBridgeEvidenceById = new Map();
5749
5752
  let frontier = [symId];
5753
+ const objectCallableFrontier = [];
5750
5754
  let traversalComplete = true;
5755
+ // Fetch one sentinel row beyond the cap so generated object bindings
5756
+ // degrade visibly instead of allocating an unbounded seed frontier.
5757
+ const OBJECT_CALLABLE_MEMBER_CAP = 5000;
5751
5758
  // Fix #480: For Java (and other JVM) Class/Interface nodes, CALLS edges
5752
5759
  // point to Constructor nodes and IMPORTS edges point to File nodes — not
5753
5760
  // the Class/Interface itself. Seed the frontier with the Constructor(s)
@@ -5807,6 +5814,58 @@ export class LocalBackend {
5807
5814
  }
5808
5815
  catch (e) {
5809
5816
  logQueryError('impact:class-node-expansion', e);
5817
+ traversalComplete = false;
5818
+ }
5819
+ }
5820
+ // Function-valued properties on exported object bindings are represented
5821
+ // as Const/Variable -[:HAS_METHOD]-> Function. HAS_METHOD is intentionally
5822
+ // absent from the default usage traversal, but downstream impact on the
5823
+ // binding still needs to enter its own callable member before following
5824
+ // CALLS.
5825
+ if (direction === 'downstream' &&
5826
+ (symType === 'Const' || symType === 'Variable') &&
5827
+ relationTypes.includes('CALLS') &&
5828
+ !relationTypes.includes('HAS_METHOD') &&
5829
+ !opts.hasExplicitRelationTypes) {
5830
+ try {
5831
+ const memberRows = await executeParameterized(repo.lbugPath, `
5832
+ MATCH (n)-[hm:CodeRelation]->(member:Function)
5833
+ WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
5834
+ RETURN DISTINCT member.id AS id, member.name AS name,
5835
+ 'Function' AS type, member.filePath AS filePath
5836
+ ORDER BY id
5837
+ LIMIT ${OBJECT_CALLABLE_MEMBER_CAP + 1}
5838
+ UNION ALL
5839
+ MATCH (n)-[hm:CodeRelation]->(member:Method)
5840
+ WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
5841
+ RETURN DISTINCT member.id AS id, member.name AS name,
5842
+ 'Method' AS type, member.filePath AS filePath
5843
+ ORDER BY id
5844
+ LIMIT ${OBJECT_CALLABLE_MEMBER_CAP + 1}
5845
+ `, { symId });
5846
+ memberRows.sort((a, b) => compareCodeUnits(String(a.id ?? a[0]), String(b.id ?? b[0])));
5847
+ if (memberRows.length > OBJECT_CALLABLE_MEMBER_CAP)
5848
+ traversalComplete = false;
5849
+ for (const row of memberRows.slice(0, OBJECT_CALLABLE_MEMBER_CAP)) {
5850
+ const memberId = row.id || row[0];
5851
+ if (memberId && !visited.has(memberId)) {
5852
+ visited.add(memberId);
5853
+ objectCallableFrontier.push(memberId);
5854
+ impacted.push({
5855
+ depth: 1,
5856
+ id: memberId,
5857
+ name: row.name || row[1],
5858
+ type: row.type || row[2],
5859
+ filePath: row.filePath || row[3] || '',
5860
+ relationType: 'HAS_METHOD',
5861
+ confidence: 1,
5862
+ });
5863
+ }
5864
+ }
5865
+ }
5866
+ catch (e) {
5867
+ logQueryError('impact:object-callable-expansion', e);
5868
+ traversalComplete = false;
5810
5869
  }
5811
5870
  }
5812
5871
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
@@ -5948,7 +6007,10 @@ export class LocalBackend {
5948
6007
  traversalComplete = false;
5949
6008
  break;
5950
6009
  }
5951
- frontier = nextFrontier;
6010
+ frontier =
6011
+ depth === 1 && objectCallableFrontier.length > 0
6012
+ ? [...new Set([...nextFrontier, ...objectCallableFrontier])]
6013
+ : nextFrontier;
5952
6014
  }
5953
6015
  // Stamp the finalized, order-independent bridge evidence (strongest across
5954
6016
  // all parents) onto each impacted item. Deferred from the BFS loop so a
@@ -6485,6 +6547,7 @@ export class LocalBackend {
6485
6547
  // the #1858 epistemic/boundaries fields — computing them per neighbor is
6486
6548
  // dead work on the highest-volume path, so suppress them here too.
6487
6549
  skipEpistemic: true,
6550
+ hasExplicitRelationTypes: opts.relationTypes.length > 0,
6488
6551
  });
6489
6552
  }
6490
6553
  catch {
@@ -563,7 +563,6 @@ import { fileURLToPath } from 'url';
563
563
  // and the v37/v38 clash it was written for: the next free value above every
564
564
  // IN-FLIGHT claim, not above origin/main. Every open PR touching gitnexus/ was
565
565
  // scanned; #3017 is the only other claimant.
566
- // RE-CHECK AGAINST origin/main AND OPEN PRs IMMEDIATELY BEFORE MERGING.
567
566
  //
568
567
  // 72 -> 74 adds import-proven Convex endpoint metadata to Const/Function worker
569
568
  // output. A warm v72 cache has no convexEndpointFactory property, so the MCP
@@ -573,7 +572,17 @@ import { fileURLToPath } from 'url';
573
572
  // migration needs both guarantees. Version 73 is intentionally skipped because
574
573
  // concurrent PR #3046 (fixes #3041) claims it. Re-check main and open PRs
575
574
  // immediately before merge.
576
- const SCHEMA_BUMP = 74;
575
+ //
576
+ // 74 -> 76 makes object-literal Function and Method members owner-qualified
577
+ // (#3041). A warm v74 cache replays the old collapsed callable ids and omits
578
+ // the new Const/Variable -> callable HAS_METHOD ownership edges, so this bump
579
+ // makes unchanged files re-parse rather than waiting for a source edit. The
580
+ // persisted graph is rebuilt separately when `analyzerRunnerIdentitiesEqual`
581
+ // detects the changed analyzer build in run-analyze.ts. Both guards are
582
+ // required; a parse-cache bump alone must never be read as a graph rebuild.
583
+ // Version 75 is intentionally skipped because concurrent PR #3017 claims it.
584
+ // RE-CHECK AGAINST origin/main AND OPEN PRs IMMEDIATELY BEFORE MERGING.
585
+ const SCHEMA_BUMP = 76;
577
586
  const GITNEXUS_PKG_VERSION = (() => {
578
587
  try {
579
588
  // package.json sits at gitnexus/package.json — two levels up from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.222",
3
+ "version": "1.6.10-rc.223",
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",