gitnexus 1.6.13-rc.66 → 1.6.13-rc.67

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.
@@ -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);
@@ -14,6 +14,7 @@ export interface ToolDef {
14
14
  filePath: string;
15
15
  description: string;
16
16
  handlerNodeId?: string;
17
+ allowFileFallback?: false;
17
18
  }
18
19
  export interface ToolsOutput {
19
20
  toolDefs: ToolDef[];
@@ -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);
@@ -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
- const SCHEMA_BUMP = 125;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.66",
3
+ "version": "1.6.13-rc.67",
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",