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.
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +14 -9
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +2 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +8 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +10 -0
- package/dist/core/ingestion/utils/ast-helpers.js +31 -0
- package/dist/core/ingestion/workers/callable-id.d.ts +12 -4
- package/dist/core/ingestion/workers/callable-id.js +20 -11
- package/dist/core/ingestion/workers/parse-worker.js +73 -22
- package/dist/core/lbug/pool-adapter.d.ts +9 -0
- package/dist/core/lbug/pool-adapter.js +154 -14
- package/dist/mcp/local/local-backend.d.ts +5 -5
- package/dist/mcp/local/local-backend.js +107 -35
- package/dist/storage/parse-cache.js +11 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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.
|
|
29
|
-
*
|
|
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
|
|
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.
|
|
48
|
-
*
|
|
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
|
|
58
|
+
export function boundCallableStartPosition(definitionNode, nodeName, nodeLabel, localDefs, nameNode) {
|
|
59
59
|
if (localDefs === undefined)
|
|
60
|
-
return definitionNode.startPosition
|
|
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 ||
|
|
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 &&
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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
|
-
:
|
|
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
|
|
1694
|
+
const startPosition = definitionNode &&
|
|
1662
1695
|
(nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor')
|
|
1663
|
-
?
|
|
1664
|
-
: definitionNode?.startPosition
|
|
1665
|
-
const startLine =
|
|
1666
|
-
?
|
|
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
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
//
|
|
2198
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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
|
|
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
|
|
787
|
-
*
|
|
788
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2531
|
-
|
|
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
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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",
|