gitnexus 1.6.10-rc.221 → 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
  }
@@ -122,6 +122,15 @@ export declare const initLbug: (repoId: string, dbPath: string) => Promise<boole
122
122
  * repoId already injected it), the existing entry is reused.
123
123
  */
124
124
  export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
125
+ /**
126
+ * Lazily load VECTOR for a semantic query.
127
+ *
128
+ * Exact graph reads never call this function, so opening their read pool does
129
+ * not probe or warn about an optional extension they do not use. The promise
130
+ * lives on SharedDB because extension scope is per Database, and also joins
131
+ * concurrent first semantic requests onto one LOAD attempt.
132
+ */
133
+ export declare function ensureVectorExtension(repoId: string): Promise<boolean>;
125
134
  export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
126
135
  /**
127
136
  * Execute a parameterized query on a specific repo's connection pool.
@@ -265,6 +265,7 @@ function closeOne(repoId) {
265
265
  shared.refCount = 0;
266
266
  shared.ftsLoaded = false;
267
267
  shared.vectorLoaded = false;
268
+ shared.vectorLoadPromise = undefined;
268
269
  }
269
270
  else {
270
271
  shared.db.close().catch(() => { });
@@ -692,13 +693,6 @@ async function doInitLbug(repoId, dbPath) {
692
693
  if (!shared.ftsLoaded) {
693
694
  shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
694
695
  }
695
- // VECTOR too — extension load scope is per-Database, so this one load
696
- // makes QUERY_VECTOR_INDEX legal on every pooled connection. Same
697
- // load-only contract as FTS above; on failure the semantic-query lane
698
- // falls back to the exact scan with its own diagnostic (#2623 follow-up).
699
- if (!shared.vectorLoaded) {
700
- shared.vectorLoaded = await loadVectorExtension(available[0], { policy: 'load-only' });
701
- }
702
696
  // Register pool entry only after all connections are pre-warmed and FTS is
703
697
  // loaded. Concurrent executeQuery calls see either "not initialized"
704
698
  // (and throw cleanly) or a fully ready pool — never a half-built one.
@@ -761,11 +755,6 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
761
755
  if (!shared.ftsLoaded) {
762
756
  shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
763
757
  }
764
- // VECTOR too — same per-Database scope and load-only contract as the
765
- // doInitLbug site above (#2623 follow-up).
766
- if (!shared.vectorLoaded) {
767
- shared.vectorLoaded = await loadVectorExtension(available[0], { policy: 'load-only' });
768
- }
769
758
  pool.set(repoId, {
770
759
  db: existingDb,
771
760
  available,
@@ -780,6 +769,143 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
780
769
  ensureIdleTimer();
781
770
  traceRss('init', repoId);
782
771
  }
772
+ /**
773
+ * Lazily load VECTOR for a semantic query.
774
+ *
775
+ * Exact graph reads never call this function, so opening their read pool does
776
+ * not probe or warn about an optional extension they do not use. The promise
777
+ * lives on SharedDB because extension scope is per Database, and also joins
778
+ * concurrent first semantic requests onto one LOAD attempt.
779
+ */
780
+ export async function ensureVectorExtension(repoId) {
781
+ const entry = pool.get(repoId);
782
+ if (!entry) {
783
+ throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
784
+ }
785
+ const shared = dbCache.get(entry.dbPath);
786
+ if (!shared) {
787
+ throw new Error(`LadybugDB shared handle is unavailable for repo "${repoId}".`);
788
+ }
789
+ if (shared.vectorLoaded)
790
+ return true;
791
+ if (shared.vectorLoadPromise)
792
+ return shared.vectorLoadPromise;
793
+ const loadAttempt = (async () => {
794
+ const conn = await checkout(entry);
795
+ try {
796
+ const loaded = await loadVectorExtension(conn, { policy: 'load-only' });
797
+ shared.vectorLoaded = loaded;
798
+ return loaded;
799
+ }
800
+ finally {
801
+ checkin(entry, conn);
802
+ }
803
+ })();
804
+ const cachedAttempt = loadAttempt.catch((err) => {
805
+ // A transient checkout/load failure must not poison this Database for the
806
+ // rest of its lifetime. Keep resolved false cached, but let a later
807
+ // semantic request retry a rejected attempt.
808
+ if (shared.vectorLoadPromise === cachedAttempt) {
809
+ shared.vectorLoadPromise = undefined;
810
+ }
811
+ throw err;
812
+ });
813
+ shared.vectorLoadPromise = cachedAttempt;
814
+ return shared.vectorLoadPromise;
815
+ }
816
+ /**
817
+ * Detect an actual VECTOR procedure call without treating source text stored in
818
+ * Cypher literals or comments as executable syntax.
819
+ */
820
+ function callsVectorIndex(cypher) {
821
+ if (!/QUERY_VECTOR_INDEX/i.test(cypher))
822
+ return false;
823
+ let code = '';
824
+ let state = 'code';
825
+ let backtickIdentifier = '';
826
+ for (let i = 0; i < cypher.length; i++) {
827
+ const ch = cypher[i];
828
+ const next = cypher[i + 1];
829
+ if (state === 'code') {
830
+ if (ch === "'" || ch === '"' || ch === '`') {
831
+ state = ch === "'" ? 'single' : ch === '"' ? 'double' : 'backtick';
832
+ if (state === 'backtick')
833
+ backtickIdentifier = '';
834
+ code += ' ';
835
+ }
836
+ else if (ch === '/' && next === '/') {
837
+ state = 'line-comment';
838
+ code += ' ';
839
+ i++;
840
+ }
841
+ else if (ch === '/' && next === '*') {
842
+ state = 'block-comment';
843
+ code += ' ';
844
+ i++;
845
+ }
846
+ else {
847
+ code += ch;
848
+ }
849
+ continue;
850
+ }
851
+ if (state === 'line-comment') {
852
+ if (ch === '\n' || ch === '\r') {
853
+ state = 'code';
854
+ code += ch;
855
+ }
856
+ else {
857
+ code += ' ';
858
+ }
859
+ continue;
860
+ }
861
+ if (state === 'block-comment') {
862
+ if (ch === '*' && next === '/') {
863
+ state = 'code';
864
+ code += ' ';
865
+ i++;
866
+ }
867
+ else {
868
+ code += ch === '\n' || ch === '\r' ? ch : ' ';
869
+ }
870
+ continue;
871
+ }
872
+ if (state === 'backtick') {
873
+ if (ch === '`' && next === '`') {
874
+ backtickIdentifier += '`';
875
+ code += ' ';
876
+ i++;
877
+ }
878
+ else if (ch === '`') {
879
+ state = 'code';
880
+ code +=
881
+ backtickIdentifier.toUpperCase() === 'QUERY_VECTOR_INDEX' ? 'QUERY_VECTOR_INDEX' : ' ';
882
+ }
883
+ else if (ch === '\\' && next !== undefined) {
884
+ backtickIdentifier += next;
885
+ code += ' ';
886
+ i++;
887
+ }
888
+ else {
889
+ backtickIdentifier += ch;
890
+ code += ch === '\n' || ch === '\r' ? ch : ' ';
891
+ }
892
+ continue;
893
+ }
894
+ if (ch === '\\') {
895
+ code += ' ';
896
+ if (next !== undefined) {
897
+ code += next === '\n' || next === '\r' ? next : ' ';
898
+ i++;
899
+ }
900
+ continue;
901
+ }
902
+ const closesLiteral = (state === 'single' && ch === "'") || (state === 'double' && ch === '"');
903
+ if (closesLiteral)
904
+ state = 'code';
905
+ code += ch === '\n' || ch === '\r' ? ch : ' ';
906
+ }
907
+ return /\bCALL\s+QUERY_VECTOR_INDEX\s*\(/i.test(code);
908
+ }
783
909
  /**
784
910
  * Checkout a connection from the pool.
785
911
  * Returns an available connection, or creates a new one if under the cap.
@@ -870,12 +996,26 @@ export const executeParameterized = async (repoId, cypher, params) => {
870
996
  // a query built by splicing a caller-sized list names itself even when the
871
997
  // repo is not initialized. Never throws (#2915).
872
998
  warnIfQueryTextUnbounded(cypher, `pool executeParameterized (repo "${repoId}")`, (message) => poolSidecarLogger.warn(message));
873
- const entry = pool.get(repoId);
999
+ let entry = pool.get(repoId);
874
1000
  if (!entry) {
875
1001
  throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
876
1002
  }
877
- entry.lastUsed = Date.now();
1003
+ // Exact reads must not pay for VECTOR, but an explicit raw vector procedure
1004
+ // call is a semantic read. Preflight before taking the query connection:
1005
+ // ensureVectorExtension performs its own checkout, so holding one here could
1006
+ // make a saturated pool wait for a connection that every caller is holding.
1007
+ // A load rejection must not replace the query's own diagnostic.
1008
+ if (callsVectorIndex(cypher)) {
1009
+ await ensureVectorExtension(repoId).catch(() => false);
1010
+ // The preflight suspends, so close/re-init may replace the pool entry.
1011
+ // Re-read it before checkout to avoid querying through a stale handle.
1012
+ entry = pool.get(repoId);
1013
+ if (!entry) {
1014
+ throw new Error(`LadybugDB connection pool closed for repo "${repoId}" (re-init/teardown); retry the query.`);
1015
+ }
1016
+ }
878
1017
  const conn = await checkout(entry);
1018
+ entry.lastUsed = Date.now();
879
1019
  silenceStdout();
880
1020
  activeQueryCount++;
881
1021
  let queryResult;
@@ -256,12 +256,12 @@ export declare class LocalBackend {
256
256
  */
257
257
  private warnedSiblingDrift;
258
258
  /**
259
- * One-shot stderr warning for the VECTOR-extension fallback. Without this
260
- * guard the diagnostic would fire on every `semanticSearch()` call on
261
- * platforms where the extension is unsupported (e.g. Windows), making MCP
262
- * stderr noisy per DoD §2.8.
259
+ * One-shot stderr guards for distinct VECTOR load and index-query failures.
260
+ * Keeping them separate preserves both diagnostics across semanticSearch calls
261
+ * without repeating either on hot paths.
263
262
  */
264
- private warnedVectorUnsupported;
263
+ private warnedVectorLoadFailed;
264
+ private warnedVectorQueryFailed;
265
265
  /**
266
266
  * One-shot warning when a pruned or Node-unloadable optional embedding stack
267
267
  * (#2370/#2372) forces semantic search to fall back to BM25 — so the
@@ -8,7 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import { createHash } from 'crypto';
11
- import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
11
+ import { initLbug, executeQuery, executeParameterized, ensureVectorExtension, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
12
12
  import { queryClassBeanMetadata } from './bean-metadata.js';
13
13
  import { querySpringAopMetadata } from './aop-metadata.js';
14
14
  import { queryConvexDispatchMetadata } from './convex-metadata.js';
@@ -783,12 +783,12 @@ export class LocalBackend {
783
783
  */
784
784
  warnedSiblingDrift = new Set();
785
785
  /**
786
- * One-shot stderr warning for the VECTOR-extension fallback. Without this
787
- * guard the diagnostic would fire on every `semanticSearch()` call on
788
- * platforms where the extension is unsupported (e.g. Windows), making MCP
789
- * stderr noisy per DoD §2.8.
786
+ * One-shot stderr guards for distinct VECTOR load and index-query failures.
787
+ * Keeping them separate preserves both diagnostics across semanticSearch calls
788
+ * without repeating either on hot paths.
790
789
  */
791
- warnedVectorUnsupported = false;
790
+ warnedVectorLoadFailed = false;
791
+ warnedVectorQueryFailed = false;
792
792
  /**
793
793
  * One-shot warning when a pruned or Node-unloadable optional embedding stack
794
794
  * (#2370/#2372) forces semantic search to fall back to BM25 — so the
@@ -2518,17 +2518,25 @@ export class LocalBackend {
2518
2518
  this.lastQueryEmbeddingDims.set(repo.lbugPath, dims);
2519
2519
  const queryVecStr = `[${queryVec.join(',')}]`;
2520
2520
  const maxDistance = getVectorMaxDistance(DEFAULT_MCP_VECTOR_MAX_DISTANCE);
2521
- let bestChunks = new Map();
2522
- // Always TRY the vector lane — no platform gate. LadybugDB ships the
2523
- // VECTOR extension for every supported platform, Windows included
2524
- // (#2623 follow-up; the old `platform !== 'win32'` gate was stale), so
2525
- // whether the index is queryable is a per-machine runtime fact. The
2526
- // catch below is the fallback: any failure (extension unloadable, index
2527
- // absent, older DB) degrades to the exact scan with a once-per-backend
2528
- // diagnostic instead of being silently swallowed.
2521
+ let vectorReady = false;
2529
2522
  try {
2530
- bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
2531
- const vectorQuery = `
2523
+ vectorReady = await ensureVectorExtension(repo.lbugPath);
2524
+ }
2525
+ catch (err) {
2526
+ if (!this.warnedVectorLoadFailed) {
2527
+ this.warnedVectorLoadFailed = true;
2528
+ logger.warn({ err }, 'GitNexus [query:vector]: vector extension load failed; using exact scan fallback');
2529
+ }
2530
+ }
2531
+ let bestChunks = new Map();
2532
+ // Try the vector lane only after its lazy load succeeds. An unavailable
2533
+ // extension is already reported by ExtensionManager; an index/query
2534
+ // failure below gets its own once-per-backend diagnostic before the exact
2535
+ // scan fallback.
2536
+ if (vectorReady) {
2537
+ try {
2538
+ bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
2539
+ const vectorQuery = `
2532
2540
  CALL QUERY_VECTOR_INDEX('${EMBEDDING_TABLE_NAME}', '${EMBEDDING_INDEX_NAME}',
2533
2541
  CAST(${queryVecStr} AS FLOAT[${dims}]), ${fetchLimit})
2534
2542
  YIELD node AS emb, distance
@@ -2538,24 +2546,25 @@ export class LocalBackend {
2538
2546
  emb.startLine AS startLine, emb.endLine AS endLine, distance
2539
2547
  ORDER BY distance
2540
2548
  `;
2541
- const embResults = await executeQuery(repo.lbugPath, vectorQuery);
2542
- return embResults.map((row) => ({
2543
- nodeId: row.nodeId ?? row[0],
2544
- chunkIndex: row.chunkIndex ?? row[1] ?? 0,
2545
- startLine: row.startLine ?? row[2] ?? 0,
2546
- endLine: row.endLine ?? row[3] ?? 0,
2547
- distance: row.distance ?? row[4],
2548
- }));
2549
- });
2550
- }
2551
- catch (err) {
2552
- bestChunks = new Map();
2553
- if (!this.warnedVectorUnsupported) {
2554
- // Rare diagnostic: surface why semantic search fell back to the
2555
- // exact scan. Emitted once per `LocalBackend` instance lifetime to
2556
- // avoid noisy stderr on hot semantic-search paths (DoD §2.8).
2557
- this.warnedVectorUnsupported = true;
2558
- logger.warn({ err }, 'GitNexus [query:vector]: vector index query failed; using exact scan fallback');
2549
+ const embResults = await executeQuery(repo.lbugPath, vectorQuery);
2550
+ return embResults.map((row) => ({
2551
+ nodeId: row.nodeId ?? row[0],
2552
+ chunkIndex: row.chunkIndex ?? row[1] ?? 0,
2553
+ startLine: row.startLine ?? row[2] ?? 0,
2554
+ endLine: row.endLine ?? row[3] ?? 0,
2555
+ distance: row.distance ?? row[4],
2556
+ }));
2557
+ });
2558
+ }
2559
+ catch (err) {
2560
+ bestChunks = new Map();
2561
+ if (!this.warnedVectorQueryFailed) {
2562
+ // Rare diagnostic: surface why semantic search fell back to the
2563
+ // exact scan. Emitted once per `LocalBackend` instance lifetime to
2564
+ // avoid noisy stderr on hot semantic-search paths (DoD §2.8).
2565
+ this.warnedVectorQueryFailed = true;
2566
+ logger.warn({ err }, 'GitNexus [query:vector]: vector index query failed; using exact scan fallback');
2567
+ }
2559
2568
  }
2560
2569
  }
2561
2570
  if (bestChunks.size === 0) {
@@ -5149,6 +5158,7 @@ export class LocalBackend {
5149
5158
  summaryOnly: true,
5150
5159
  skipEpistemic: true,
5151
5160
  skipEnrichment: true,
5161
+ hasExplicitRelationTypes,
5152
5162
  });
5153
5163
  }
5154
5164
  catch (e) {
@@ -5385,6 +5395,7 @@ export class LocalBackend {
5385
5395
  limit: Number.isFinite(params.limit) ? params.limit : 100,
5386
5396
  offset: Number.isFinite(params.offset) ? params.offset : 0,
5387
5397
  pdgBridge,
5398
+ hasExplicitRelationTypes,
5388
5399
  });
5389
5400
  return composeUnifiedPdgImpactResult(pdgResult, interproceduralResult);
5390
5401
  }
@@ -5401,6 +5412,7 @@ export class LocalBackend {
5401
5412
  limit: Number.isFinite(params.limit) ? params.limit : 100,
5402
5413
  offset: Number.isFinite(params.offset) ? params.offset : 0,
5403
5414
  summaryOnly: params.summaryOnly,
5415
+ hasExplicitRelationTypes,
5404
5416
  });
5405
5417
  }
5406
5418
  /**
@@ -5738,7 +5750,11 @@ export class LocalBackend {
5738
5750
  const visited = new Set([symId]);
5739
5751
  const pdgBridgeEvidenceById = new Map();
5740
5752
  let frontier = [symId];
5753
+ const objectCallableFrontier = [];
5741
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;
5742
5758
  // Fix #480: For Java (and other JVM) Class/Interface nodes, CALLS edges
5743
5759
  // point to Constructor nodes and IMPORTS edges point to File nodes — not
5744
5760
  // the Class/Interface itself. Seed the frontier with the Constructor(s)
@@ -5798,6 +5814,58 @@ export class LocalBackend {
5798
5814
  }
5799
5815
  catch (e) {
5800
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;
5801
5869
  }
5802
5870
  }
5803
5871
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
@@ -5939,7 +6007,10 @@ export class LocalBackend {
5939
6007
  traversalComplete = false;
5940
6008
  break;
5941
6009
  }
5942
- frontier = nextFrontier;
6010
+ frontier =
6011
+ depth === 1 && objectCallableFrontier.length > 0
6012
+ ? [...new Set([...nextFrontier, ...objectCallableFrontier])]
6013
+ : nextFrontier;
5943
6014
  }
5944
6015
  // Stamp the finalized, order-independent bridge evidence (strongest across
5945
6016
  // all parents) onto each impacted item. Deferred from the BFS loop so a
@@ -6476,6 +6547,7 @@ export class LocalBackend {
6476
6547
  // the #1858 epistemic/boundaries fields — computing them per neighbor is
6477
6548
  // dead work on the highest-volume path, so suppress them here too.
6478
6549
  skipEpistemic: true,
6550
+ hasExplicitRelationTypes: opts.relationTypes.length > 0,
6479
6551
  });
6480
6552
  }
6481
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.221",
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",