gitnexus 1.6.13-rc.66 → 1.6.13-rc.68
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/language-provider.d.ts +4 -1
- package/dist/core/ingestion/languages/typescript/tool-definitions.d.ts +4 -0
- package/dist/core/ingestion/languages/typescript/tool-definitions.js +355 -0
- package/dist/core/ingestion/languages/typescript.js +3 -0
- package/dist/core/ingestion/pipeline-phases/processes.js +2 -0
- package/dist/core/ingestion/pipeline-phases/tools.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/tools.js +1 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +2 -0
- package/dist/core/ingestion/workers/parse-worker.js +21 -0
- package/dist/core/lbug/lbug-adapter.d.ts +2 -1
- package/dist/core/lbug/lbug-adapter.js +3 -2
- package/dist/core/search/bm25-index.d.ts +2 -0
- package/dist/core/search/bm25-index.js +15 -7
- package/dist/core/search/fts-indexes.d.ts +5 -8
- package/dist/core/search/fts-indexes.js +9 -10
- package/dist/mcp/local/local-backend.js +17 -7
- package/dist/storage/parse-cache.js +8 -1
- package/package.json +1 -1
|
@@ -26,7 +26,7 @@ import type { ExtractedRoute } from './route-extractors/laravel.js';
|
|
|
26
26
|
import type { SharedSpringType } from './route-extractors/spring-shared.js';
|
|
27
27
|
import type { ModuleConstants, Operand, RepoConstants } from './route-extractors/constant-resolver.js';
|
|
28
28
|
import type Parser from 'tree-sitter';
|
|
29
|
-
import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
|
|
29
|
+
import type { ExtractedDecoratorRoute, ExtractedToolDef } from './workers/parse-worker.js';
|
|
30
30
|
import type { SemanticModel } from './model/semantic-model.js';
|
|
31
31
|
/** What a provider's {@link LanguageProviderConfig.resolveRouteHandler} can see. */
|
|
32
32
|
export interface RouteHandlerResolutionHookContext {
|
|
@@ -430,6 +430,9 @@ interface LanguageProviderConfig {
|
|
|
430
430
|
* Default: undefined (no text-route extraction).
|
|
431
431
|
*/
|
|
432
432
|
readonly extractTextRoutes?: (filePath: string, content: string) => ExtractedRoute[];
|
|
433
|
+
/** Extract tool registrations after captures, using only emitted callable identities.
|
|
434
|
+
* The map keys are declaration-name AST node IDs, local to this parsed tree. */
|
|
435
|
+
readonly extractToolDefinitions?: (tree: Parser.Tree, filePath: string, lineOffset: number, callableBindings: ReadonlyMap<number, string>) => ExtractedToolDef[];
|
|
433
436
|
/**
|
|
434
437
|
* Extract routes that a parsed file declares in its own AST.
|
|
435
438
|
*
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type Parser from 'tree-sitter';
|
|
2
|
+
import type { ExtractedToolDef } from '../../workers/parse-worker.js';
|
|
3
|
+
/** Direct SDK registrations only; no wrapper, alias-chain or runtime-value inference. */
|
|
4
|
+
export declare function extractToolDefinitions(tree: Parser.Tree, filePath: string, lineOffset?: number, callableBindings?: ReadonlyMap<number, string>): ExtractedToolDef[];
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { plainString, propertyName } from '../../route-extractors/data-route-table.js';
|
|
2
|
+
const FUNCTIONS = new Set([
|
|
3
|
+
'function_declaration',
|
|
4
|
+
'generator_function_declaration',
|
|
5
|
+
'function_expression',
|
|
6
|
+
'generator_function',
|
|
7
|
+
'arrow_function',
|
|
8
|
+
'method_definition',
|
|
9
|
+
]);
|
|
10
|
+
const BLOCKS = new Set([
|
|
11
|
+
'statement_block',
|
|
12
|
+
'for_statement',
|
|
13
|
+
'for_in_statement',
|
|
14
|
+
'switch_body',
|
|
15
|
+
'catch_clause',
|
|
16
|
+
'class_body',
|
|
17
|
+
]);
|
|
18
|
+
const SDK_MODULES = new Set([
|
|
19
|
+
'@modelcontextprotocol/sdk/server/mcp.js',
|
|
20
|
+
'@modelcontextprotocol/sdk/server/mcp',
|
|
21
|
+
]);
|
|
22
|
+
function lookup(scope, name) {
|
|
23
|
+
for (let current = scope; current; current = current.parent) {
|
|
24
|
+
const binding = current.bindings.get(name);
|
|
25
|
+
if (binding)
|
|
26
|
+
return binding;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Only binding/assignment patterns: never descend into keys, types or defaults.
|
|
30
|
+
* Member assignment targets are reported separately from binding names. */
|
|
31
|
+
function patternNames(pattern, onMember) {
|
|
32
|
+
const names = [];
|
|
33
|
+
const pending = [pattern];
|
|
34
|
+
while (pending.length) {
|
|
35
|
+
const node = pending.pop();
|
|
36
|
+
if (node.type === 'identifier' || node.type === 'shorthand_property_identifier_pattern') {
|
|
37
|
+
names.push(node);
|
|
38
|
+
}
|
|
39
|
+
else if (node.type === 'member_expression' || node.type === 'subscript_expression') {
|
|
40
|
+
onMember?.(node);
|
|
41
|
+
}
|
|
42
|
+
else if (node.type === 'pair_pattern') {
|
|
43
|
+
const value = node.childForFieldName('value');
|
|
44
|
+
if (value)
|
|
45
|
+
pending.push(value);
|
|
46
|
+
}
|
|
47
|
+
else if (node.type === 'assignment_pattern' || node.type === 'object_assignment_pattern') {
|
|
48
|
+
const left = node.childForFieldName('left');
|
|
49
|
+
if (left)
|
|
50
|
+
pending.push(left);
|
|
51
|
+
}
|
|
52
|
+
else if (node.type === 'array_pattern' ||
|
|
53
|
+
node.type === 'object_pattern' ||
|
|
54
|
+
node.type === 'rest_pattern') {
|
|
55
|
+
pending.push(...node.namedChildren);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return names;
|
|
59
|
+
}
|
|
60
|
+
function declare(scope, name, details = {}) {
|
|
61
|
+
const previous = scope.bindings.get(name.text);
|
|
62
|
+
if (previous) {
|
|
63
|
+
previous.invalid = true;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
scope.bindings.set(name.text, { name, scope, kind: 'unknown', ...details });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function variableScope(scope) {
|
|
70
|
+
while (!scope.functionScope && scope.parent)
|
|
71
|
+
scope = scope.parent;
|
|
72
|
+
return scope;
|
|
73
|
+
}
|
|
74
|
+
function collectBindings(root) {
|
|
75
|
+
const moduleScope = { functionScope: true, bindings: new Map() };
|
|
76
|
+
const scopes = new Map();
|
|
77
|
+
const calls = [];
|
|
78
|
+
const writes = [];
|
|
79
|
+
const pending = [{ node: root, scope: moduleScope }];
|
|
80
|
+
while (pending.length) {
|
|
81
|
+
const entry = pending.pop();
|
|
82
|
+
const node = entry.node;
|
|
83
|
+
let scope = entry.scope;
|
|
84
|
+
const isFunction = FUNCTIONS.has(node.type);
|
|
85
|
+
const name = node.childForFieldName('name');
|
|
86
|
+
if (node.type === 'function_declaration' || node.type === 'generator_function_declaration') {
|
|
87
|
+
if (name)
|
|
88
|
+
declare(scope, name, { kind: 'function', value: node, immutable: true });
|
|
89
|
+
}
|
|
90
|
+
else if ([
|
|
91
|
+
'class_declaration',
|
|
92
|
+
'interface_declaration',
|
|
93
|
+
'type_alias_declaration',
|
|
94
|
+
'enum_declaration',
|
|
95
|
+
].includes(node.type)) {
|
|
96
|
+
if (name)
|
|
97
|
+
declare(scope, name);
|
|
98
|
+
}
|
|
99
|
+
if (isFunction ||
|
|
100
|
+
BLOCKS.has(node.type) ||
|
|
101
|
+
node.type === 'class' ||
|
|
102
|
+
node.type === 'class_declaration') {
|
|
103
|
+
scope = { parent: scope, functionScope: isFunction, bindings: new Map() };
|
|
104
|
+
}
|
|
105
|
+
scopes.set(node.id, scope);
|
|
106
|
+
if (node.type === 'class' && name)
|
|
107
|
+
declare(scope, name);
|
|
108
|
+
if (isFunction) {
|
|
109
|
+
if (name && (node.type === 'function_expression' || node.type === 'generator_function')) {
|
|
110
|
+
declare(scope, name, { kind: 'function', value: node, immutable: true });
|
|
111
|
+
}
|
|
112
|
+
const parameters = node.childForFieldName('parameters');
|
|
113
|
+
const single = node.childForFieldName('parameter');
|
|
114
|
+
for (const parameter of parameters?.namedChildren ?? (single ? [single] : [])) {
|
|
115
|
+
const pattern = parameter.childForFieldName('pattern') ?? parameter;
|
|
116
|
+
const type = parameter.childForFieldName('type')?.namedChildren[0];
|
|
117
|
+
for (const bindingName of patternNames(pattern)) {
|
|
118
|
+
declare(scope, bindingName, {
|
|
119
|
+
kind: 'parameter',
|
|
120
|
+
...(pattern.type === 'identifier' && type ? { type } : {}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (node.type === 'import_statement') {
|
|
126
|
+
const source = node.childForFieldName('source');
|
|
127
|
+
const sdk = source !== null && SDK_MODULES.has(plainString(source) ?? '');
|
|
128
|
+
const typeOnly = node.children.some((child) => child.type === 'type');
|
|
129
|
+
const clause = node.namedChildren.find((child) => child.type === 'import_clause');
|
|
130
|
+
for (const child of clause?.namedChildren ?? []) {
|
|
131
|
+
if (child.type === 'identifier')
|
|
132
|
+
declare(scope, child);
|
|
133
|
+
else if (child.type === 'namespace_import') {
|
|
134
|
+
const local = child.namedChildren[0];
|
|
135
|
+
if (local)
|
|
136
|
+
declare(scope, local, { kind: sdk ? 'sdk-namespace' : 'unknown', typeOnly });
|
|
137
|
+
}
|
|
138
|
+
else if (child.type === 'named_imports') {
|
|
139
|
+
for (const specifier of child.namedChildren) {
|
|
140
|
+
const imported = specifier.childForFieldName('name');
|
|
141
|
+
const local = specifier.childForFieldName('alias') ?? imported;
|
|
142
|
+
if (local)
|
|
143
|
+
declare(scope, local, {
|
|
144
|
+
kind: sdk && imported?.text === 'McpServer' ? 'sdk' : 'unknown',
|
|
145
|
+
typeOnly: typeOnly || specifier.children.some((part) => part.type === 'type'),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (node.type === 'variable_declarator') {
|
|
152
|
+
if (name) {
|
|
153
|
+
const target = node.parent?.type === 'variable_declaration' ? variableScope(scope) : scope;
|
|
154
|
+
for (const bindingName of patternNames(name)) {
|
|
155
|
+
declare(target, bindingName, {
|
|
156
|
+
kind: 'variable',
|
|
157
|
+
immutable: node.parent?.childForFieldName('kind')?.type === 'const',
|
|
158
|
+
...(name.type === 'identifier'
|
|
159
|
+
? { value: node.childForFieldName('value') ?? undefined }
|
|
160
|
+
: {}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (node.type === 'catch_clause') {
|
|
166
|
+
const parameter = node.childForFieldName('parameter');
|
|
167
|
+
if (parameter)
|
|
168
|
+
for (const bindingName of patternNames(parameter))
|
|
169
|
+
declare(scope, bindingName);
|
|
170
|
+
}
|
|
171
|
+
else if (node.type === 'for_in_statement') {
|
|
172
|
+
const left = node.childForFieldName('left');
|
|
173
|
+
const kind = node.childForFieldName('kind');
|
|
174
|
+
if (left && kind) {
|
|
175
|
+
const target = kind.type === 'var' ? variableScope(scope) : scope;
|
|
176
|
+
for (const bindingName of patternNames(left))
|
|
177
|
+
declare(target, bindingName);
|
|
178
|
+
}
|
|
179
|
+
else if (left)
|
|
180
|
+
writes.push(left);
|
|
181
|
+
}
|
|
182
|
+
else if (node.type === 'type_parameter') {
|
|
183
|
+
if (name)
|
|
184
|
+
declare(scope, name);
|
|
185
|
+
}
|
|
186
|
+
if (node.type === 'call_expression')
|
|
187
|
+
calls.push(node);
|
|
188
|
+
if (node.type === 'assignment_expression' || node.type === 'augmented_assignment_expression') {
|
|
189
|
+
const left = node.childForFieldName('left');
|
|
190
|
+
if (left)
|
|
191
|
+
writes.push(left);
|
|
192
|
+
}
|
|
193
|
+
else if (node.type === 'update_expression' ||
|
|
194
|
+
(node.type === 'unary_expression' && node.children.some((child) => child.type === 'delete'))) {
|
|
195
|
+
const argument = node.childForFieldName('argument');
|
|
196
|
+
if (argument)
|
|
197
|
+
writes.push(argument);
|
|
198
|
+
}
|
|
199
|
+
for (let index = node.namedChildCount - 1; index >= 0; index--) {
|
|
200
|
+
pending.push({ node: node.namedChild(index), scope });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Resolve writes after declarations so later declarations also shadow outer names.
|
|
204
|
+
while (writes.length) {
|
|
205
|
+
let target = writes.pop();
|
|
206
|
+
const scope = scopes.get(target.id);
|
|
207
|
+
const members = [];
|
|
208
|
+
while (target.type === 'member_expression' || target.type === 'subscript_expression') {
|
|
209
|
+
const object = target.childForFieldName('object');
|
|
210
|
+
if (!object)
|
|
211
|
+
break;
|
|
212
|
+
const property = target.childForFieldName('property');
|
|
213
|
+
const index = target.childForFieldName('index');
|
|
214
|
+
members.push(property ? propertyName(property) : index ? plainString(index) : null);
|
|
215
|
+
target = object;
|
|
216
|
+
}
|
|
217
|
+
// Nested member targets must pass the same guard as direct property writes.
|
|
218
|
+
for (const name of patternNames(target, (member) => writes.push(member))) {
|
|
219
|
+
const binding = lookup(scope, name.text);
|
|
220
|
+
// Lifecycle callbacks and other known properties do not replace the receiver
|
|
221
|
+
// or its registration methods. Unknown keys and constructor mutations remain unsafe.
|
|
222
|
+
if (binding?.kind !== 'sdk' &&
|
|
223
|
+
binding?.kind !== 'sdk-namespace' &&
|
|
224
|
+
members.length > 0 &&
|
|
225
|
+
members.every((member) => member !== null) &&
|
|
226
|
+
!['tool', 'registerTool', '__proto__'].includes(members[members.length - 1]))
|
|
227
|
+
continue;
|
|
228
|
+
if (binding)
|
|
229
|
+
binding.invalid = true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { scopes, calls };
|
|
233
|
+
}
|
|
234
|
+
function sdkBinding(node, scope, forType = false) {
|
|
235
|
+
let kind = 'sdk';
|
|
236
|
+
if (node.type === (forType ? 'nested_type_identifier' : 'member_expression')) {
|
|
237
|
+
const namespace = node.childForFieldName(forType ? 'module' : 'object');
|
|
238
|
+
const member = node.childForFieldName(forType ? 'name' : 'property');
|
|
239
|
+
if (namespace?.type !== 'identifier' || member?.text !== 'McpServer')
|
|
240
|
+
return false;
|
|
241
|
+
node = namespace;
|
|
242
|
+
kind = 'sdk-namespace';
|
|
243
|
+
}
|
|
244
|
+
if (node.type !== 'identifier' && node.type !== 'type_identifier')
|
|
245
|
+
return false;
|
|
246
|
+
const binding = lookup(scope, node.text);
|
|
247
|
+
return binding?.kind === kind && !binding.invalid && (forType || !binding.typeOnly);
|
|
248
|
+
}
|
|
249
|
+
function sdkReceiver(node, scope, scopes) {
|
|
250
|
+
if (node.type !== 'identifier')
|
|
251
|
+
return false;
|
|
252
|
+
const binding = lookup(scope, node.text);
|
|
253
|
+
if (!binding || binding.invalid)
|
|
254
|
+
return false;
|
|
255
|
+
if (binding.kind === 'parameter' && binding.type) {
|
|
256
|
+
return sdkBinding(binding.type, binding.scope, true);
|
|
257
|
+
}
|
|
258
|
+
const value = binding.value;
|
|
259
|
+
if (binding.kind !== 'variable' || value?.type !== 'new_expression')
|
|
260
|
+
return false;
|
|
261
|
+
if (value.endIndex > node.startIndex && variableScope(binding.scope) === variableScope(scope))
|
|
262
|
+
return false;
|
|
263
|
+
const constructor = value.childForFieldName('constructor');
|
|
264
|
+
return constructor !== null && sdkBinding(constructor, scopes.get(value.id));
|
|
265
|
+
}
|
|
266
|
+
/** An unknown later property can replace description; a later explicit property restores proof. */
|
|
267
|
+
function descriptionFromConfig(config) {
|
|
268
|
+
let description = '';
|
|
269
|
+
if (config.type !== 'object')
|
|
270
|
+
return description;
|
|
271
|
+
for (const child of config.namedChildren) {
|
|
272
|
+
if (child.type === 'comment')
|
|
273
|
+
continue;
|
|
274
|
+
const key = child.childForFieldName('key') ?? child.childForFieldName('name');
|
|
275
|
+
const name = child.type === 'shorthand_property_identifier' ? child.text : key && propertyName(key);
|
|
276
|
+
if (child.type === 'pair' && name === 'description') {
|
|
277
|
+
const value = child.childForFieldName('value');
|
|
278
|
+
description = value ? (plainString(value) ?? '') : '';
|
|
279
|
+
}
|
|
280
|
+
else if (!name || name === 'description') {
|
|
281
|
+
description = '';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return description;
|
|
285
|
+
}
|
|
286
|
+
function handlerNodeId(node, scope, callableBindings) {
|
|
287
|
+
if (node.type !== 'identifier')
|
|
288
|
+
return undefined;
|
|
289
|
+
const binding = lookup(scope, node.text);
|
|
290
|
+
if (!binding || binding.invalid)
|
|
291
|
+
return undefined;
|
|
292
|
+
if (binding.kind === 'variable') {
|
|
293
|
+
const value = binding.value;
|
|
294
|
+
if (!binding.immutable ||
|
|
295
|
+
!value ||
|
|
296
|
+
(value.type !== 'arrow_function' && value.type !== 'function_expression'))
|
|
297
|
+
return undefined;
|
|
298
|
+
if (value.endIndex > node.startIndex && variableScope(binding.scope) === variableScope(scope))
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
else if (binding.kind !== 'function')
|
|
302
|
+
return undefined;
|
|
303
|
+
return callableBindings?.get(binding.name.id);
|
|
304
|
+
}
|
|
305
|
+
/** Direct SDK registrations only; no wrapper, alias-chain or runtime-value inference. */
|
|
306
|
+
export function extractToolDefinitions(tree, filePath, lineOffset = 0, callableBindings) {
|
|
307
|
+
// Ordinary files need no lexical walk; every supported receiver originates here.
|
|
308
|
+
const importsSdk = tree.rootNode.namedChildren.some((node) => {
|
|
309
|
+
if (node.type !== 'import_statement')
|
|
310
|
+
return false;
|
|
311
|
+
const source = node.childForFieldName('source');
|
|
312
|
+
return source !== null && SDK_MODULES.has(plainString(source) ?? '');
|
|
313
|
+
});
|
|
314
|
+
if (!importsSdk)
|
|
315
|
+
return [];
|
|
316
|
+
const { scopes, calls } = collectBindings(tree.rootNode);
|
|
317
|
+
const definitions = [];
|
|
318
|
+
for (const call of calls) {
|
|
319
|
+
const callee = call.childForFieldName('function');
|
|
320
|
+
if (call.hasError || callee?.type !== 'member_expression')
|
|
321
|
+
continue;
|
|
322
|
+
const receiver = callee.childForFieldName('object');
|
|
323
|
+
const method = callee.childForFieldName('property');
|
|
324
|
+
if (!receiver ||
|
|
325
|
+
method?.type !== 'property_identifier' ||
|
|
326
|
+
(method.text !== 'registerTool' && method.text !== 'tool') ||
|
|
327
|
+
!sdkReceiver(receiver, scopes.get(call.id), scopes))
|
|
328
|
+
continue;
|
|
329
|
+
const args = call
|
|
330
|
+
.childForFieldName('arguments')
|
|
331
|
+
?.namedChildren.filter((child) => child.type !== 'comment') ?? [];
|
|
332
|
+
if (args.some((arg) => arg.type === 'spread_element'))
|
|
333
|
+
continue;
|
|
334
|
+
if (method.text === 'registerTool' ? args.length !== 3 : args.length < 2 || args.length > 5)
|
|
335
|
+
continue;
|
|
336
|
+
const toolName = plainString(args[0]);
|
|
337
|
+
if (toolName === null)
|
|
338
|
+
continue;
|
|
339
|
+
const description = method.text === 'registerTool'
|
|
340
|
+
? descriptionFromConfig(args[1])
|
|
341
|
+
: args.length > 2
|
|
342
|
+
? (plainString(args[1]) ?? '')
|
|
343
|
+
: '';
|
|
344
|
+
const handler = handlerNodeId(args[args.length - 1], scopes.get(call.id), callableBindings);
|
|
345
|
+
definitions.push({
|
|
346
|
+
filePath,
|
|
347
|
+
toolName,
|
|
348
|
+
description,
|
|
349
|
+
lineNumber: call.startPosition.row + 1 + lineOffset,
|
|
350
|
+
...(handler !== undefined ? { handlerNodeId: handler } : {}),
|
|
351
|
+
allowFileFallback: false,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return definitions;
|
|
355
|
+
}
|
|
@@ -77,6 +77,7 @@ import { extractDataRouteTableRoutes } from '../route-extractors/data-route-tabl
|
|
|
77
77
|
import { extractNestRoutes } from '../route-extractors/nest.js';
|
|
78
78
|
import { extractTrpcRoutes, shouldScanForTrpcRoutes } from '../route-extractors/trpc.js';
|
|
79
79
|
import { extractConvexEndpointProperties } from './typescript/convex-endpoint-metadata.js';
|
|
80
|
+
import { extractToolDefinitions } from './typescript/tool-definitions.js';
|
|
80
81
|
const extractJsTsRoutes = (...args) => [
|
|
81
82
|
...extractDispatchGuardRoutes(...args),
|
|
82
83
|
...extractDataRouteTableRoutes(...args),
|
|
@@ -415,6 +416,7 @@ export const typescriptProvider = defineLanguage({
|
|
|
415
416
|
// Content-based (not AST): tRPC procedure routers are scanned from source text.
|
|
416
417
|
// Path-gate lives here (language provider), not in the shared parse worker.
|
|
417
418
|
extractTextRoutes: extractJsTsTextRoutes,
|
|
419
|
+
extractToolDefinitions,
|
|
418
420
|
});
|
|
419
421
|
export const javascriptProvider = defineLanguage({
|
|
420
422
|
id: SupportedLanguages.JavaScript,
|
|
@@ -494,4 +496,5 @@ export const javascriptProvider = defineLanguage({
|
|
|
494
496
|
extractDecoratorRoutes: extractJsTsRoutes,
|
|
495
497
|
// Content-based (not AST): tRPC procedure routers are scanned from source text.
|
|
496
498
|
extractTextRoutes: extractJsTsTextRoutes,
|
|
499
|
+
extractToolDefinitions,
|
|
497
500
|
});
|
|
@@ -261,6 +261,8 @@ export const processesPhase = {
|
|
|
261
261
|
const toolsByHandlerId = new Map();
|
|
262
262
|
const toolsWithoutHandlerByFile = new Map();
|
|
263
263
|
for (const td of toolDefs) {
|
|
264
|
+
if (!td.handlerNodeId && td.allowFileFallback === false)
|
|
265
|
+
continue;
|
|
264
266
|
const key = td.handlerNodeId ?? td.filePath;
|
|
265
267
|
const targetMap = td.handlerNodeId ? toolsByHandlerId : toolsWithoutHandlerByFile;
|
|
266
268
|
let list = targetMap.get(key);
|
|
@@ -30,6 +30,7 @@ export const toolsPhase = {
|
|
|
30
30
|
filePath: td.filePath,
|
|
31
31
|
description: td.description,
|
|
32
32
|
...(handlerNodeId !== undefined ? { handlerNodeId } : {}),
|
|
33
|
+
...(td.allowFileFallback === false ? { allowFileFallback: false } : {}),
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
// TS tool definition arrays — require inputSchema nearby
|
|
@@ -196,6 +196,8 @@ export interface ExtractedToolDef {
|
|
|
196
196
|
description: string;
|
|
197
197
|
lineNumber: number;
|
|
198
198
|
handlerNodeId?: string;
|
|
199
|
+
/** Unresolved registrations must not inherit unrelated same-file flows. */
|
|
200
|
+
allowFileFallback?: false;
|
|
199
201
|
}
|
|
200
202
|
export interface ExtractedORMQuery {
|
|
201
203
|
filePath: string;
|
|
@@ -1087,6 +1087,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1087
1087
|
// node id → graph node id for classes THIS file's capture loop materialized.
|
|
1088
1088
|
// Keyed by in-memory AST identity (never persisted); filled below.
|
|
1089
1089
|
const classOwnersByNodeId = new Map();
|
|
1090
|
+
const callableBindings = new Map();
|
|
1090
1091
|
// #2687: ONE pass over `matches` yields both suppression sets — the
|
|
1091
1092
|
// definition-name claims by rank (callable > Property > value), so the dedup
|
|
1092
1093
|
// below cannot depend on tree-sitter's match order, and the concrete-typedef
|
|
@@ -2328,6 +2329,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
2328
2329
|
...(returnShapeProperty ? { fromReturnShape: true, isDetail: true } : {}),
|
|
2329
2330
|
}),
|
|
2330
2331
|
});
|
|
2332
|
+
// Keep actual emitted identities; providers must not reconstruct graph IDs.
|
|
2333
|
+
if (nameNode && (nodeLabel === 'Function' || nodeLabel === 'Method')) {
|
|
2334
|
+
callableBindings.set(nameNode.id, nodeId);
|
|
2335
|
+
}
|
|
2331
2336
|
// enclosingClassId already computed above (before nodeId generation)
|
|
2332
2337
|
const ownerId = enclosingClassId ?? objectLiteralOwnerInfo?.ownerId;
|
|
2333
2338
|
result.symbols.push({
|
|
@@ -2419,6 +2424,22 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
2419
2424
|
}
|
|
2420
2425
|
}
|
|
2421
2426
|
}
|
|
2427
|
+
if (provider.extractToolDefinitions) {
|
|
2428
|
+
// Distinct lexical declarations can share a graph ID (for example, sibling
|
|
2429
|
+
// block-scoped functions). Such IDs cannot prove which handler owns a tool.
|
|
2430
|
+
const seenCallableIds = new Set();
|
|
2431
|
+
const ambiguousCallableIds = new Set();
|
|
2432
|
+
for (const nodeId of callableBindings.values()) {
|
|
2433
|
+
if (seenCallableIds.has(nodeId))
|
|
2434
|
+
ambiguousCallableIds.add(nodeId);
|
|
2435
|
+
seenCallableIds.add(nodeId);
|
|
2436
|
+
}
|
|
2437
|
+
for (const [bindingId, nodeId] of callableBindings) {
|
|
2438
|
+
if (ambiguousCallableIds.has(nodeId))
|
|
2439
|
+
callableBindings.delete(bindingId);
|
|
2440
|
+
}
|
|
2441
|
+
result.toolDefs.push(...provider.extractToolDefinitions(tree, file.path, lineOffset, callableBindings));
|
|
2442
|
+
}
|
|
2422
2443
|
// Extract framework routes via provider detection (e.g., Laravel routes.php)
|
|
2423
2444
|
if (provider.isRouteFile?.(file.path)) {
|
|
2424
2445
|
const extractedRoutes = extractLaravelRoutes(tree, file.path);
|
|
@@ -893,9 +893,10 @@ export declare const buildFtsQueryCypher: (tableName: string, indexName: string,
|
|
|
893
893
|
* @param query - Search query string
|
|
894
894
|
* @param limit - Maximum results
|
|
895
895
|
* @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR)
|
|
896
|
+
* @param missingIndex - Preserve the empty-result default, or propagate missing indexes for diagnostics
|
|
896
897
|
* @returns Array of { node properties, score }
|
|
897
898
|
*/
|
|
898
|
-
export declare const queryFTS: (tableName: string, indexName: string, query: string, limit?: number, conjunctive?: boolean) => Promise<Array<{
|
|
899
|
+
export declare const queryFTS: (tableName: string, indexName: string, query: string, limit?: number, conjunctive?: boolean, missingIndex?: 'empty' | 'throw') => Promise<Array<{
|
|
899
900
|
nodeId: string;
|
|
900
901
|
name: string;
|
|
901
902
|
filePath: string;
|
|
@@ -3504,9 +3504,10 @@ export const buildFtsQueryCypher = (tableName, indexName, limit, conjunctive = f
|
|
|
3504
3504
|
* @param query - Search query string
|
|
3505
3505
|
* @param limit - Maximum results
|
|
3506
3506
|
* @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR)
|
|
3507
|
+
* @param missingIndex - Preserve the empty-result default, or propagate missing indexes for diagnostics
|
|
3507
3508
|
* @returns Array of { node properties, score }
|
|
3508
3509
|
*/
|
|
3509
|
-
export const queryFTS = async (tableName, indexName, query, limit = 20, conjunctive = false) => {
|
|
3510
|
+
export const queryFTS = async (tableName, indexName, query, limit = 20, conjunctive = false, missingIndex = 'empty') => {
|
|
3510
3511
|
if (!conn) {
|
|
3511
3512
|
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
3512
3513
|
}
|
|
@@ -3532,7 +3533,7 @@ export const queryFTS = async (tableName, indexName, query, limit = 20, conjunct
|
|
|
3532
3533
|
// NEW-6 — this used to be a bare `.includes('does not exist')` check
|
|
3533
3534
|
// that could not tell the two apart).
|
|
3534
3535
|
const message = e instanceof Error ? e.message : String(e);
|
|
3535
|
-
if (classifyFtsQueryError(message) === 'missing-index') {
|
|
3536
|
+
if (missingIndex === 'empty' && classifyFtsQueryError(message) === 'missing-index') {
|
|
3536
3537
|
return [];
|
|
3537
3538
|
}
|
|
3538
3539
|
throw e;
|
|
@@ -29,6 +29,8 @@ export interface FTSSearchResponse {
|
|
|
29
29
|
* which only does so when every table failed).
|
|
30
30
|
*/
|
|
31
31
|
nonBenignErrors?: string[];
|
|
32
|
+
/** Configured table.index names that could not be queried because their index is missing. */
|
|
33
|
+
missingIndexes?: string[];
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Search using LadybugDB's built-in FTS (fresh, reads from disk)
|
|
@@ -86,6 +86,7 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId, disabledReaso
|
|
|
86
86
|
const resultsByIndex = [];
|
|
87
87
|
let queriesSucceeded = 0;
|
|
88
88
|
const nonBenignErrors = [];
|
|
89
|
+
const missingIndexes = [];
|
|
89
90
|
const ftsExtension = getExtensionCapabilities().find((c) => c.name === 'fts');
|
|
90
91
|
if (ftsExtension && !ftsExtension.loaded) {
|
|
91
92
|
// tri-review NEW-4 (applies to BOTH the MCP pool path and the CLI/pipeline
|
|
@@ -114,26 +115,32 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId, disabledReaso
|
|
|
114
115
|
queriesSucceeded++;
|
|
115
116
|
resultsByIndex.push(outcome.rows);
|
|
116
117
|
}
|
|
117
|
-
else if (
|
|
118
|
+
else if (outcome.benign) {
|
|
119
|
+
missingIndexes.push(`${table}.${indexName}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
118
122
|
nonBenignErrors.push(redactPaths(outcome.message ?? 'Unknown FTS query error'));
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
else {
|
|
123
127
|
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// missing-table or real query error rethrows here — track it the same way
|
|
127
|
-
// the MCP pool path does instead of a bare `catch {}` that dropped it.
|
|
128
|
+
// Opt into missing-index propagation so absent indexes cannot masquerade
|
|
129
|
+
// as successful zero-match queries. Keep core/pool classification identical.
|
|
128
130
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
129
131
|
try {
|
|
130
|
-
const result = await queryFTS(table, indexName, searchQuery, limit, false);
|
|
132
|
+
const result = await queryFTS(table, indexName, searchQuery, limit, false, 'throw');
|
|
131
133
|
queriesSucceeded++;
|
|
132
134
|
resultsByIndex.push(result);
|
|
133
135
|
}
|
|
134
136
|
catch (e) {
|
|
135
137
|
const message = e instanceof Error ? e.message : String(e);
|
|
136
|
-
|
|
138
|
+
if (classifyFtsQueryError(message) === 'missing-index') {
|
|
139
|
+
missingIndexes.push(`${table}.${indexName}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
nonBenignErrors.push(redactPaths(message));
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
}
|
|
139
146
|
}
|
|
@@ -174,5 +181,6 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId, disabledReaso
|
|
|
174
181
|
})),
|
|
175
182
|
ftsAvailable,
|
|
176
183
|
...(nonBenignErrors.length > 0 && { nonBenignErrors }),
|
|
184
|
+
...(missingIndexes.length > 0 && { missingIndexes }),
|
|
177
185
|
};
|
|
178
186
|
};
|
|
@@ -48,15 +48,12 @@ export interface FtsWarningContext {
|
|
|
48
48
|
*/
|
|
49
49
|
export declare const ftsDegradedWarning: (context?: FtsWarningContext, disabledReason?: FtsDisabledReason) => string;
|
|
50
50
|
/**
|
|
51
|
-
* Warning
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* so this deliberately does NOT suggest it: reusing the missing-index
|
|
56
|
-
* message here would reproduce, for this cause, the exact misleading
|
|
57
|
-
* "run --repair-fts" guidance #2767 itself was about (tri-review NEW-1).
|
|
51
|
+
* Warning when no FTS query succeeded and at least one failed for a real,
|
|
52
|
+
* non-benign reason (timeout, connection reset, native fault). `--repair-fts`
|
|
53
|
+
* will not fix those errors. If indexes are also missing, the caller composes
|
|
54
|
+
* their repair guidance separately; do not deny that additional failure cause.
|
|
58
55
|
*/
|
|
59
|
-
export declare const ftsQueryFailedWarning: (context: FtsWarningContext) => string;
|
|
56
|
+
export declare const ftsQueryFailedWarning: (context: FtsWarningContext, hasMissingIndexes?: boolean) => string;
|
|
60
57
|
export declare const SUPPORTED_FTS_STEMMERS: ReadonlySet<string>;
|
|
61
58
|
export interface CreateSearchFTSIndexesOptions {
|
|
62
59
|
indexes?: readonly FTSIndexDefinition[];
|
|
@@ -72,18 +72,17 @@ export const ftsDegradedWarning = (context, disabledReason) => {
|
|
|
72
72
|
suffix);
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
|
-
* Warning
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* so this deliberately does NOT suggest it: reusing the missing-index
|
|
80
|
-
* message here would reproduce, for this cause, the exact misleading
|
|
81
|
-
* "run --repair-fts" guidance #2767 itself was about (tri-review NEW-1).
|
|
75
|
+
* Warning when no FTS query succeeded and at least one failed for a real,
|
|
76
|
+
* non-benign reason (timeout, connection reset, native fault). `--repair-fts`
|
|
77
|
+
* will not fix those errors. If indexes are also missing, the caller composes
|
|
78
|
+
* their repair guidance separately; do not deny that additional failure cause.
|
|
82
79
|
*/
|
|
83
|
-
export const ftsQueryFailedWarning = (context) => 'FTS keyword search failed — every configured index query returned an error' +
|
|
80
|
+
export const ftsQueryFailedWarning = (context, hasMissingIndexes = false) => 'FTS keyword search failed — every configured index query returned an error' +
|
|
84
81
|
(context.lastErrorRedacted ? ` (${context.lastErrorRedacted})` : '') +
|
|
85
|
-
'; results do not include keyword matches.
|
|
86
|
-
|
|
82
|
+
'; results do not include keyword matches. ' +
|
|
83
|
+
(hasMissingIndexes
|
|
84
|
+
? 'See server logs for query error details.'
|
|
85
|
+
: 'This is not a missing-index condition — see server logs for details.') +
|
|
87
86
|
` (resolved: ${formatResolvedSuffix(context)})`;
|
|
88
87
|
// Stemmers shipped by the LadybugDB FTS extension. Mirrors the lowercase token
|
|
89
88
|
// set in the extension bundled with @ladybugdb/core 0.18.x (see package.json).
|
|
@@ -2413,6 +2413,7 @@ export class LocalBackend {
|
|
|
2413
2413
|
// regardless of whether OTHER tables succeeded — previously a real error
|
|
2414
2414
|
// on N-1 of N tables while one succeeded left zero diagnostic trail.
|
|
2415
2415
|
const ftsQueryErrors = bm25SearchResult?.nonBenignErrors;
|
|
2416
|
+
const ftsMissingIndexes = bm25SearchResult?.missingIndexes;
|
|
2416
2417
|
if (ftsQueryErrors) {
|
|
2417
2418
|
// tri-review NEW-5: these strings are already classified non-benign by
|
|
2418
2419
|
// classifyFtsQueryError — do NOT route them through logQueryError,
|
|
@@ -2847,12 +2848,11 @@ export class LocalBackend {
|
|
|
2847
2848
|
branch: repo.branch,
|
|
2848
2849
|
indexedAt: this.lastObservedPoolState.get(repo.lbugPath)?.indexedAt ?? repo.indexedAt,
|
|
2849
2850
|
};
|
|
2850
|
-
//
|
|
2851
|
-
//
|
|
2852
|
-
//
|
|
2853
|
-
// instead of burying the real cause as a trailing suffix on bad advice.
|
|
2851
|
+
// Real errors (timeout, connection reset) need their own diagnosis.
|
|
2852
|
+
// When some indexes are also missing, preserve both causes and append
|
|
2853
|
+
// their repair guidance below even though no FTS query succeeded.
|
|
2854
2854
|
warnings.push(ftsQueryErrors
|
|
2855
|
-
? ftsQueryFailedWarning({ ...warningContext, lastErrorRedacted: ftsQueryErrors[0] })
|
|
2855
|
+
? ftsQueryFailedWarning({ ...warningContext, lastErrorRedacted: ftsQueryErrors[0] }, !!ftsMissingIndexes?.length)
|
|
2856
2856
|
: ftsDegradedWarning(warningContext, ftsDisabledReason));
|
|
2857
2857
|
}
|
|
2858
2858
|
else if (ftsQueryErrors) {
|
|
@@ -2863,6 +2863,10 @@ export class LocalBackend {
|
|
|
2863
2863
|
// that convention instead of only logging server-side.
|
|
2864
2864
|
warnings.push(`FTS keyword search partially failed — ${ftsQueryErrors.length} of the configured indexes hit a query error and were skipped; results may be missing matches from those node types (see server logs).`);
|
|
2865
2865
|
}
|
|
2866
|
+
if (ftsMissingIndexes?.length && (ftsUsed || ftsQueryErrors)) {
|
|
2867
|
+
warnings.push(`FTS keyword search is incomplete: missing configured indexes (${ftsMissingIndexes.join(', ')}). ` +
|
|
2868
|
+
'Results may be missing matches from those node types. Run `gitnexus analyze --repair-fts`.');
|
|
2869
|
+
}
|
|
2866
2870
|
// #2331: a CJK query against a server process resolving
|
|
2867
2871
|
// GITNEXUS_FTS_CJK_SEGMENTATION to 'none' silently misses sub-phrase
|
|
2868
2872
|
// matches with no other signal — this is the only place an agent driving
|
|
@@ -2979,7 +2983,7 @@ export class LocalBackend {
|
|
|
2979
2983
|
// #2767: a partial FTS failure (some tables ok, one or more real errors)
|
|
2980
2984
|
// is as much a "results may be incomplete" signal as enrichmentDegraded —
|
|
2981
2985
|
// flag it the same way rather than only via the warning string.
|
|
2982
|
-
const ftsPartial = ftsUsed && !!ftsQueryErrors;
|
|
2986
|
+
const ftsPartial = ftsUsed && (!!ftsQueryErrors || !!ftsMissingIndexes?.length);
|
|
2983
2987
|
return {
|
|
2984
2988
|
processes,
|
|
2985
2989
|
process_symbols,
|
|
@@ -3023,6 +3027,7 @@ export class LocalBackend {
|
|
|
3023
3027
|
const bm25Results = ftsResponse?.results ?? [];
|
|
3024
3028
|
const ftsUsed = ftsResponse?.ftsAvailable ?? false;
|
|
3025
3029
|
const nonBenignErrors = ftsResponse?.nonBenignErrors;
|
|
3030
|
+
const missingIndexes = ftsResponse?.missingIndexes;
|
|
3026
3031
|
const results = [];
|
|
3027
3032
|
for (const bm25Result of bm25Results) {
|
|
3028
3033
|
const fullPath = bm25Result.filePath;
|
|
@@ -3087,7 +3092,12 @@ export class LocalBackend {
|
|
|
3087
3092
|
});
|
|
3088
3093
|
}
|
|
3089
3094
|
}
|
|
3090
|
-
return {
|
|
3095
|
+
return {
|
|
3096
|
+
results,
|
|
3097
|
+
ftsUsed,
|
|
3098
|
+
...(nonBenignErrors && { nonBenignErrors }),
|
|
3099
|
+
...(missingIndexes && { missingIndexes }),
|
|
3100
|
+
};
|
|
3091
3101
|
}
|
|
3092
3102
|
/**
|
|
3093
3103
|
* Keyword-only indexes are the default. Do not put this on `query.warning`
|
|
@@ -817,7 +817,14 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|
|
817
817
|
// `handlerReceiver` hint. Warm v123 Go worker results carry no routes.
|
|
818
818
|
// v125 (#3402): Go route hints now honor lexical declarations and captured writes;
|
|
819
819
|
// namespace imports retain whether their local name comes from the package clause.
|
|
820
|
-
|
|
820
|
+
// v126 (#3446): SDK positional tool registrations now emit tool definitions,
|
|
821
|
+
// exact handler identities, and an opt-out from unrelated file-level flows.
|
|
822
|
+
// Warm v125 worker results omit these definitions and must be re-extracted.
|
|
823
|
+
// v127 (#3450): Destructured member writes invalidate SDK registration evidence.
|
|
824
|
+
// Warm v126 worker results can retain false tools after a method replacement.
|
|
825
|
+
// v128 (#3450): SDK namespace imports now prove positional tool receivers.
|
|
826
|
+
// Warm v127 worker results omit these definitions and must be re-extracted.
|
|
827
|
+
const SCHEMA_BUMP = 128;
|
|
821
828
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
822
829
|
try {
|
|
823
830
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED