gitnexus 1.6.13-rc.65 → 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,9 @@
1
+ import type { ParsedFile, ScopeId, SymbolDefinition } from '../../../../_shared/index.js';
2
+ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
3
+ export declare function swiftIsCallableVisibleFromCaller(ctx: {
4
+ readonly candidate: SymbolDefinition;
5
+ readonly callerParsed?: ParsedFile;
6
+ readonly callArity?: number;
7
+ readonly callerScope?: ScopeId;
8
+ readonly scopes?: ScopeResolutionIndexes;
9
+ }): boolean;
@@ -0,0 +1,57 @@
1
+ import { findClassBindingInScope } from '../../scope-resolution/scope/walkers.js';
2
+ export function swiftIsCallableVisibleFromCaller(ctx) {
3
+ const indexes = ctx.scopes;
4
+ if (ctx.callerScope === undefined || indexes === undefined || ctx.callArity !== 0)
5
+ return true;
6
+ const name = ctx.candidate.qualifiedName?.split('.').at(-1);
7
+ if (name === undefined)
8
+ return true;
9
+ let scopeId = ctx.callerScope;
10
+ let selfType;
11
+ while (scopeId !== null) {
12
+ const scope = indexes.scopeTree.getScope(scopeId);
13
+ if (scope === undefined)
14
+ break;
15
+ // A local function selected inside the caller wins before member lookup.
16
+ if (scope.kind !== 'Class' &&
17
+ scope.ownedDefs.some((def) => def.nodeId === ctx.candidate.nodeId))
18
+ return true;
19
+ selfType ??= scope.typeBindings.get('self');
20
+ if (scope.kind === 'Class') {
21
+ const classDef = scope.ownedDefs.find((def) => def.type === 'Class') ??
22
+ (selfType === undefined
23
+ ? undefined
24
+ : findClassBindingInScope(ctx.callerScope, selfType.rawName, indexes, undefined, {
25
+ uniqueQualifiedNameFallback: false,
26
+ }));
27
+ const propertyOnOwner = (ownerId, ownerName) => indexes.qualifiedNames.get(`${ownerName}.${name}`).some((defId) => {
28
+ const def = indexes.defs.get(defId);
29
+ return def?.type === 'Property' && def.ownerId === ownerId;
30
+ });
31
+ const ownProperty = scope.ownedDefs.some((def) => def.type === 'Property' && def.qualifiedName?.split('.').at(-1) === name) ||
32
+ (classDef !== undefined &&
33
+ classDef.qualifiedName !== undefined &&
34
+ propertyOnOwner(classDef.nodeId, classDef.qualifiedName));
35
+ const inheritedProperty = classDef !== undefined &&
36
+ indexes.methodDispatch.mroFor(classDef.nodeId).some((ownerId) => {
37
+ const ownerName = indexes.defs.get(ownerId)?.qualifiedName;
38
+ return ownerName !== undefined && propertyOnOwner(ownerId, ownerName);
39
+ });
40
+ if (!ownProperty && !inheritedProperty)
41
+ return true;
42
+ // Swift's nested function is owned by its own Function scope. A
43
+ // same-file sibling must not count as a nearer lexical binding.
44
+ const declarationScope = ctx.callerParsed?.scopes.find((candidateScope) => candidateScope.ownedDefs.some((def) => def.nodeId === ctx.candidate.nodeId));
45
+ if (declarationScope !== undefined &&
46
+ declarationScope.kind === 'Function' &&
47
+ (declarationScope.id === ctx.callerScope ||
48
+ indexes.scopeTree.getAncestors(declarationScope.id).includes(ctx.callerScope)))
49
+ return true;
50
+ // Scope defs do not carry Swift access modifiers. A method positively
51
+ // selected on this type must not be vetoed by an uncertain ancestor.
52
+ return classDef !== undefined && ctx.candidate.ownerId === classDef.nodeId;
53
+ }
54
+ scopeId = scope.parent;
55
+ }
56
+ return true;
57
+ }
@@ -56,6 +56,7 @@ import { swiftArityCompatibility, swiftMergeBindings, interpretSwiftImport, reso
56
56
  import { stripSwiftTypePreservingDecoration } from './interpret.js';
57
57
  import { groupSwiftFilesByModule } from './target-grouping.js';
58
58
  import { swiftIsGlobalNameFallbackPlausible } from './name-fallback-visibility.js';
59
+ import { swiftIsCallableVisibleFromCaller } from './callable-visibility.js';
59
60
  const ZERO_RANGE = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 };
60
61
  const swiftScopeResolver = {
61
62
  language: SupportedLanguages.Swift,
@@ -123,6 +124,7 @@ const swiftScopeResolver = {
123
124
  // no-`new` constructor + cross-file free-call shape).
124
125
  allowGlobalFreeCallFallback: true,
125
126
  isGlobalNameFallbackPlausible: swiftIsGlobalNameFallbackPlausible,
127
+ isCallableVisibleFromCaller: swiftIsCallableVisibleFromCaller,
126
128
  // Swift's call graph models `Type(...)` as a reference to the type
127
129
  // itself, not its `init` — both the legacy DAG and this test suite link
128
130
  // `Foo()` to the Class node even when an explicit `init` exists.
@@ -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
@@ -1112,6 +1112,9 @@ export interface ScopeResolver {
1112
1112
  readonly isCallableVisibleFromCaller?: (ctx: {
1113
1113
  readonly callerParsed: ParsedFile;
1114
1114
  readonly candidate: SymbolDefinition;
1115
+ /** Arity of the actual call, when known. A visibility veto must not
1116
+ * infer applicability from a name match alone. */
1117
+ readonly callArity?: number;
1115
1118
  /** Caller's enclosing scope id. Languages that gate visibility on
1116
1119
  * caller scope (e.g. C++ two-phase template lookup) consult it;
1117
1120
  * others ignore. Optional so existing implementations stay valid. */
@@ -47,12 +47,7 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
47
47
  * appended to its reason. See `ScopeResolver.markConstructionSites`. */
48
48
  readonly markConstructionSites?: boolean;
49
49
  readonly isFileLocalDef?: (def: SymbolDefinition) => boolean;
50
- readonly isCallableVisibleFromCaller?: (ctx: {
51
- readonly callerParsed: ParsedFile;
52
- readonly candidate: SymbolDefinition;
53
- readonly callerScope?: ScopeId;
54
- readonly scopes?: ScopeResolutionIndexes;
55
- }) => boolean;
50
+ readonly isCallableVisibleFromCaller?: ScopeResolver['isCallableVisibleFromCaller'];
56
51
  readonly resolveAdlCandidates?: (site: {
57
52
  readonly name: string;
58
53
  readonly arity?: number;
@@ -447,6 +447,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
447
447
  ? (candidate) => options.isCallableVisibleFromCaller({
448
448
  callerParsed: parsed,
449
449
  candidate,
450
+ callArity: site.arity,
450
451
  callerScope: site.inScope,
451
452
  scopes,
452
453
  })
@@ -514,6 +515,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
514
515
  !options.isCallableVisibleFromCaller({
515
516
  callerParsed: parsed,
516
517
  candidate: fnDef,
518
+ callArity: site.arity,
517
519
  callerScope: site.inScope,
518
520
  scopes,
519
521
  })) {
@@ -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.65",
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",