ucn 3.8.23 → 3.8.26

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.
Files changed (44) hide show
  1. package/.claude/skills/ucn/SKILL.md +127 -12
  2. package/README.md +152 -156
  3. package/cli/index.js +363 -37
  4. package/core/analysis.js +936 -32
  5. package/core/bridge.js +1095 -0
  6. package/core/brief.js +408 -0
  7. package/core/cache.js +105 -5
  8. package/core/callers.js +72 -18
  9. package/core/check.js +200 -0
  10. package/core/discovery.js +57 -34
  11. package/core/entrypoints.js +638 -4
  12. package/core/execute.js +304 -5
  13. package/core/git-enrich.js +130 -0
  14. package/core/graph.js +24 -2
  15. package/core/output/analysis.js +157 -25
  16. package/core/output/brief.js +100 -0
  17. package/core/output/check.js +79 -0
  18. package/core/output/doctor.js +85 -0
  19. package/core/output/endpoints.js +239 -0
  20. package/core/output/extraction.js +2 -0
  21. package/core/output/find.js +126 -39
  22. package/core/output/graph.js +48 -15
  23. package/core/output/refactoring.js +103 -5
  24. package/core/output/reporting.js +63 -23
  25. package/core/output/search.js +110 -17
  26. package/core/output/shared.js +56 -2
  27. package/core/output.js +4 -0
  28. package/core/parser.js +8 -2
  29. package/core/project.js +39 -3
  30. package/core/registry.js +30 -14
  31. package/core/reporting.js +465 -2
  32. package/core/search.js +130 -52
  33. package/core/shared.js +101 -5
  34. package/core/tracing.js +16 -6
  35. package/core/verify.js +982 -95
  36. package/languages/go.js +91 -6
  37. package/languages/html.js +10 -0
  38. package/languages/java.js +151 -35
  39. package/languages/javascript.js +290 -33
  40. package/languages/python.js +78 -11
  41. package/languages/rust.js +267 -12
  42. package/languages/utils.js +315 -3
  43. package/mcp/server.js +91 -16
  44. package/package.json +9 -1
@@ -11,7 +11,8 @@ const {
11
11
  nodeToLocation,
12
12
  extractParams,
13
13
  parseStructuredParams,
14
- extractJSDocstring
14
+ extractJSDocstring,
15
+ buildTypeAnnotations
15
16
  } = require('./utils');
16
17
  const { PARSE_OPTIONS, safeParse } = require('./index');
17
18
 
@@ -95,19 +96,138 @@ function extractModifiers(text) {
95
96
  */
96
97
  function extractDecorators(node) {
97
98
  const decorators = [];
99
+ const consume = (n) => {
100
+ if (n.type !== 'decorator') return;
101
+ let text = n.text.replace(/^@/, '');
102
+ const parenIdx = text.indexOf('(');
103
+ if (parenIdx > 0) text = text.substring(0, parenIdx);
104
+ decorators.push(text);
105
+ };
106
+
107
+ // 1. Direct children — covers most class/method decorators.
98
108
  for (let i = 0; i < node.namedChildCount; i++) {
99
- const child = node.namedChild(i);
100
- if (child.type === 'decorator') {
101
- let text = child.text.replace(/^@/, '');
102
- // Strip arguments: @Component({...}) Component
103
- const parenIdx = text.indexOf('(');
104
- if (parenIdx > 0) text = text.substring(0, parenIdx);
105
- decorators.push(text);
109
+ consume(node.namedChild(i));
110
+ }
111
+
112
+ // 2. When a class/function is wrapped in `export class …`, tree-sitter
113
+ // wraps it in an `export_statement`. The decorator becomes a sibling
114
+ // of the inner declaration *inside* that export_statement. Walk the
115
+ // wrapper's children for any decorator preceding the inner node.
116
+ if (node.parent && node.parent.type === 'export_statement') {
117
+ const wrapper = node.parent;
118
+ let myIdx = -1;
119
+ for (let i = 0; i < wrapper.namedChildCount; i++) {
120
+ if (wrapper.namedChild(i).id === node.id) { myIdx = i; break; }
121
+ }
122
+ for (let i = myIdx - 1; i >= 0; i--) {
123
+ const sib = wrapper.namedChild(i);
124
+ if (sib.type === 'decorator') consume(sib);
125
+ else break;
126
+ }
127
+ }
128
+
129
+ // 3. Some grammars place decorators as preceding siblings of the
130
+ // declaration itself (rather than wrapping in export_statement).
131
+ // Walk back from this node within its parent.
132
+ if (node.parent && node.parent.type !== 'export_statement') {
133
+ const parent = node.parent;
134
+ let myIdx = -1;
135
+ for (let i = 0; i < parent.namedChildCount; i++) {
136
+ if (parent.namedChild(i).id === node.id) { myIdx = i; break; }
137
+ }
138
+ for (let i = myIdx - 1; i >= 0; i--) {
139
+ const sib = parent.namedChild(i);
140
+ if (sib.type === 'decorator') consume(sib);
141
+ else break;
106
142
  }
107
143
  }
108
144
  return decorators;
109
145
  }
110
146
 
147
+ /**
148
+ * Extract decorators along with their string-literal first argument.
149
+ * Returns array of { name, args, firstStringArg } where:
150
+ * - name is the decorator name (no @)
151
+ * - args is the raw argument text (without outer parens), or null
152
+ * - firstStringArg is the literal value of the first string-literal argument, or null
153
+ *
154
+ * @Get(':id') → { name: 'Get', args: "':id'", firstStringArg: ':id' }
155
+ * @Controller('/api/users') → { name: 'Controller', args: "'/api/users'", firstStringArg: '/api/users' }
156
+ * @Injectable → { name: 'Injectable', args: null, firstStringArg: null }
157
+ *
158
+ * Used by route extraction (NestJS, etc.) — only the firstStringArg is currently
159
+ * consumed by core/bridge.js, but `args` is preserved for future structural-search use.
160
+ */
161
+ function extractDecoratorsWithArgs(node) {
162
+ const result = [];
163
+ const { extractStringArg } = require('./utils');
164
+
165
+ const consume = (n) => {
166
+ if (n.type !== 'decorator') return;
167
+ // tree-sitter-javascript: decorator has a single 'expression' child.
168
+ // Look for call_expression vs identifier vs member_expression.
169
+ let inner = null;
170
+ for (let i = 0; i < n.namedChildCount; i++) {
171
+ const c = n.namedChild(i);
172
+ if (c.type !== 'comment') { inner = c; break; }
173
+ }
174
+ if (!inner) return;
175
+
176
+ if (inner.type === 'call_expression') {
177
+ const fn = inner.childForFieldName('function');
178
+ const argsNode = inner.childForFieldName('arguments');
179
+ if (!fn || !argsNode) return;
180
+ const name = fn.text;
181
+ // Get raw arg text without the surrounding parens
182
+ const argsText = argsNode.text.replace(/^\(|\)$/g, '');
183
+ // Find first string-literal arg
184
+ let firstStringArg = null;
185
+ for (let j = 0; j < argsNode.namedChildCount; j++) {
186
+ const arg = argsNode.namedChild(j);
187
+ if (arg.type === 'comment') continue;
188
+ const s = extractStringArg(arg);
189
+ if (s && !s.interp) { firstStringArg = s.value; break; }
190
+ if (s) { firstStringArg = s.value; break; }
191
+ break;
192
+ }
193
+ result.push({ name, args: argsText, firstStringArg });
194
+ } else if (inner.type === 'identifier' || inner.type === 'member_expression') {
195
+ // Plain decorator: @Injectable
196
+ result.push({ name: inner.text, args: null, firstStringArg: null });
197
+ }
198
+ };
199
+
200
+ // Same traversal as extractDecorators
201
+ for (let i = 0; i < node.namedChildCount; i++) {
202
+ consume(node.namedChild(i));
203
+ }
204
+ if (node.parent && node.parent.type === 'export_statement') {
205
+ const wrapper = node.parent;
206
+ let myIdx = -1;
207
+ for (let i = 0; i < wrapper.namedChildCount; i++) {
208
+ if (wrapper.namedChild(i).id === node.id) { myIdx = i; break; }
209
+ }
210
+ for (let i = myIdx - 1; i >= 0; i--) {
211
+ const sib = wrapper.namedChild(i);
212
+ if (sib.type === 'decorator') consume(sib);
213
+ else break;
214
+ }
215
+ }
216
+ if (node.parent && node.parent.type !== 'export_statement') {
217
+ const parent = node.parent;
218
+ let myIdx = -1;
219
+ for (let i = 0; i < parent.namedChildCount; i++) {
220
+ if (parent.namedChild(i).id === node.id) { myIdx = i; break; }
221
+ }
222
+ for (let i = myIdx - 1; i >= 0; i--) {
223
+ const sib = parent.namedChild(i);
224
+ if (sib.type === 'decorator') consume(sib);
225
+ else break;
226
+ }
227
+ }
228
+ return result;
229
+ }
230
+
111
231
  // --- Single-pass helpers: extracted from find* callbacks ---
112
232
 
113
233
  /**
@@ -131,22 +251,28 @@ function _processFunction(node, functions, processedRanges, lines) {
131
251
  const generics = extractGenerics(node);
132
252
  const docstring = extractJSDocstring(lines, startLine);
133
253
  const isGen = isGenerator(node);
254
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
255
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
134
256
  // Check parent for export status (function_declaration inside export_statement)
135
257
  const modifiers = node.parent && node.parent.type === 'export_statement'
136
258
  ? extractModifiers(node.parent.text)
137
259
  : extractModifiers(node.text);
260
+ // Feature B: explicit isAsync flag (auditAsync needs to know whether
261
+ // the fn was declared `async function`).
262
+ const isAsync = modifiers.includes('async');
138
263
 
139
264
  functions.push({
140
265
  name: nameNode.text,
141
266
  params: extractParams(paramsNode),
142
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
267
+ paramsStructured,
143
268
  startLine,
144
269
  endLine,
145
270
  indent,
146
271
  isArrow: false,
147
272
  isGenerator: isGen,
273
+ isAsync,
148
274
  modifiers,
149
- ...(returnType && { returnType }),
275
+ ...typeAnno,
150
276
  ...(generics && { generics }),
151
277
  ...(docstring && { docstring })
152
278
  });
@@ -167,11 +293,13 @@ function _processFunction(node, functions, processedRanges, lines) {
167
293
  const returnType = extractReturnType(node);
168
294
  const generics = extractGenerics(node);
169
295
  const docstring = extractJSDocstring(lines, startLine);
296
+ const paramsStructured = parseStructuredParams(paramsNode, 'typescript');
297
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
170
298
 
171
299
  functions.push({
172
300
  name: nameNode.text,
173
301
  params: extractParams(paramsNode),
174
- paramsStructured: parseStructuredParams(paramsNode, 'typescript'),
302
+ paramsStructured,
175
303
  startLine,
176
304
  endLine,
177
305
  indent,
@@ -179,7 +307,7 @@ function _processFunction(node, functions, processedRanges, lines) {
179
307
  isGenerator: false,
180
308
  isSignature: true,
181
309
  modifiers: [],
182
- ...(returnType && { returnType }),
310
+ ...typeAnno,
183
311
  ...(generics && { generics }),
184
312
  ...(docstring && { docstring })
185
313
  });
@@ -210,22 +338,31 @@ function _processFunction(node, functions, processedRanges, lines) {
210
338
  const generics = extractGenerics(valueNode);
211
339
  const docstring = extractJSDocstring(lines, startLine);
212
340
  const isGen = isGenerator(valueNode);
341
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
342
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
213
343
  // Check parent for export status (lexical_declaration inside export_statement)
214
344
  const modifiers = node.parent && node.parent.type === 'export_statement'
215
345
  ? extractModifiers(node.parent.text)
216
346
  : extractModifiers(node.text);
347
+ // Feature B: detect async — for arrow/fn-expressions the `async`
348
+ // keyword precedes the parameter list on the value node, NOT on
349
+ // the lexical_declaration text. extractModifiers walked the full
350
+ // declaration text, so we double-check the value node directly.
351
+ const valueIsAsync = valueNode.text.trimStart().startsWith('async ');
352
+ const isAsync = valueIsAsync || modifiers.includes('async');
217
353
 
218
354
  functions.push({
219
355
  name: nameNode.text,
220
356
  params: extractParams(paramsNode),
221
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
357
+ paramsStructured,
222
358
  startLine,
223
359
  endLine,
224
360
  indent,
225
361
  isArrow,
226
362
  isGenerator: isGen,
363
+ isAsync,
227
364
  modifiers,
228
- ...(returnType && { returnType }),
365
+ ...typeAnno,
229
366
  ...(generics && { generics }),
230
367
  ...(docstring && { docstring })
231
368
  });
@@ -255,6 +392,8 @@ function _processFunction(node, functions, processedRanges, lines) {
255
392
  const returnType = extractReturnType(innerFn);
256
393
  const generics = extractGenerics(innerFn);
257
394
  const docstring = extractJSDocstring(lines, startLine);
395
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
396
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
258
397
  const modifiers = node.parent && node.parent.type === 'export_statement'
259
398
  ? extractModifiers(node.parent.text)
260
399
  : extractModifiers(node.text);
@@ -262,14 +401,14 @@ function _processFunction(node, functions, processedRanges, lines) {
262
401
  functions.push({
263
402
  name: nameNode.text,
264
403
  params: extractParams(paramsNode),
265
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
404
+ paramsStructured,
266
405
  startLine,
267
406
  endLine,
268
407
  indent,
269
408
  isArrow: innerFn.type === 'arrow_function',
270
409
  isGenerator: false,
271
410
  modifiers,
272
- ...(returnType && { returnType }),
411
+ ...typeAnno,
273
412
  ...(generics && { generics }),
274
413
  ...(docstring && { docstring })
275
414
  });
@@ -330,18 +469,20 @@ function _processFunction(node, functions, processedRanges, lines) {
330
469
  const generics = extractGenerics(rightNode);
331
470
  const docstring = extractJSDocstring(lines, startLine);
332
471
  const isGen = isGenerator(rightNode);
472
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
473
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
333
474
 
334
475
  functions.push({
335
476
  name,
336
477
  params: extractParams(paramsNode),
337
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
478
+ paramsStructured,
338
479
  startLine,
339
480
  endLine,
340
481
  indent,
341
482
  isArrow,
342
483
  isGenerator: isGen,
343
484
  modifiers: [],
344
- ...(returnType && { returnType }),
485
+ ...typeAnno,
345
486
  ...(generics && { generics }),
346
487
  ...(docstring && { docstring })
347
488
  });
@@ -368,18 +509,20 @@ function _processFunction(node, functions, processedRanges, lines) {
368
509
  const generics = extractGenerics(child);
369
510
  const docstring = extractJSDocstring(lines, startLine);
370
511
  const isGen = isGenerator(child);
512
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
513
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
371
514
 
372
515
  functions.push({
373
516
  name: 'default',
374
517
  params: extractParams(paramsNode),
375
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
518
+ paramsStructured,
376
519
  startLine,
377
520
  endLine,
378
521
  indent,
379
522
  isArrow: child.type === 'arrow_function',
380
523
  isGenerator: isGen,
381
524
  modifiers: ['export', 'default'],
382
- ...(returnType && { returnType }),
525
+ ...typeAnno,
383
526
  ...(generics && { generics }),
384
527
  ...(docstring && { docstring })
385
528
  });
@@ -428,6 +571,7 @@ function _processClass(node, classes, processedRanges, lines) {
428
571
  const extendsInfo = extractExtends(node);
429
572
  const implementsInfo = extractImplements(node);
430
573
  const decorators = extractDecorators(node);
574
+ const decoratorsWithArgs = extractDecoratorsWithArgs(node);
431
575
 
432
576
  const isAbstract = node.type === 'abstract_class_declaration';
433
577
  classes.push({
@@ -441,7 +585,8 @@ function _processClass(node, classes, processedRanges, lines) {
441
585
  ...(generics && { generics }),
442
586
  ...(extendsInfo && { extends: extendsInfo }),
443
587
  ...(implementsInfo.length > 0 && { implements: implementsInfo }),
444
- ...(decorators.length > 0 && { decorators })
588
+ ...(decorators.length > 0 && { decorators }),
589
+ ...(decoratorsWithArgs.some(d => d.firstStringArg) && { decoratorsWithArgs })
445
590
  });
446
591
  }
447
592
  return true;
@@ -656,15 +801,17 @@ function extractInterfaceMembers(interfaceNode, code) {
656
801
  if (nameNode) {
657
802
  const { startLine, endLine } = nodeToLocation(child, code);
658
803
  const returnType = extractReturnType(child);
804
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
805
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
659
806
  members.push({
660
807
  name: nameNode.text,
661
808
  params: extractParams(paramsNode),
662
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
809
+ paramsStructured,
663
810
  startLine,
664
811
  endLine,
665
812
  memberType: 'method',
666
813
  isMethod: true,
667
- ...(returnType && { returnType })
814
+ ...typeAnno
668
815
  });
669
816
  }
670
817
  } else if (child.type === 'property_signature') {
@@ -773,20 +920,24 @@ function extractClassMembers(classNode, codeOrLines) {
773
920
  const isAsync = text.match(/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:override\s+)?async\s/) !== null;
774
921
  const returnType = extractReturnType(child);
775
922
  const docstring = extractJSDocstring(code, startLine);
923
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
924
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
776
925
 
926
+ const decoratorsWithArgs = extractDecoratorsWithArgs(child);
777
927
  members.push({
778
928
  name,
779
929
  params: extractParams(paramsNode),
780
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
930
+ paramsStructured,
781
931
  startLine,
782
932
  endLine,
783
933
  memberType,
784
934
  isAsync,
785
935
  isGenerator: isGen,
786
936
  isMethod: true, // Mark as method for context() lookups
787
- ...(returnType && { returnType }),
937
+ ...typeAnno,
788
938
  ...(docstring && { docstring }),
789
- ...(decorators.length > 0 && { decorators })
939
+ ...(decorators.length > 0 && { decorators }),
940
+ ...(decoratorsWithArgs.length > 0 && { decoratorsWithArgs })
790
941
  });
791
942
  }
792
943
  }
@@ -799,6 +950,8 @@ function extractClassMembers(classNode, codeOrLines) {
799
950
  const { startLine, endLine } = nodeToLocation(child, code);
800
951
  const returnType = extractReturnType(child);
801
952
  const docstring = extractJSDocstring(code, startLine);
953
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
954
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
802
955
  // Collect decorators from preceding siblings
803
956
  const decorators = [];
804
957
  for (let j = i - 1; j >= 0; j--) {
@@ -813,12 +966,12 @@ function extractClassMembers(classNode, codeOrLines) {
813
966
  members.push({
814
967
  name: nameNode.text,
815
968
  params: extractParams(paramsNode),
816
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
969
+ paramsStructured,
817
970
  startLine,
818
971
  endLine,
819
972
  memberType: 'abstract',
820
973
  isMethod: true,
821
- ...(returnType && { returnType }),
974
+ ...typeAnno,
822
975
  ...(docstring && { docstring }),
823
976
  ...(decorators.length > 0 && { decorators })
824
977
  });
@@ -862,16 +1015,18 @@ function extractClassMembers(classNode, codeOrLines) {
862
1015
  if (isArrow) {
863
1016
  const paramsNode = valueNode.childForFieldName('parameters');
864
1017
  const returnType = extractReturnType(valueNode);
1018
+ const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
1019
+ const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
865
1020
  members.push({
866
1021
  name,
867
1022
  params: extractParams(paramsNode),
868
- paramsStructured: parseStructuredParams(paramsNode, 'javascript'),
1023
+ paramsStructured,
869
1024
  startLine,
870
1025
  endLine,
871
1026
  memberType: name.startsWith('#') ? 'private' : 'field',
872
1027
  isArrow: true,
873
1028
  isMethod: true, // Arrow fields are callable like methods
874
- ...(returnType && { returnType }),
1029
+ ...typeAnno,
875
1030
  ...(fieldDecorators.length > 0 && { decorators: fieldDecorators })
876
1031
  });
877
1032
  } else {
@@ -1003,6 +1158,76 @@ function findCallsInCode(code, parser) {
1003
1158
  const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
1004
1159
  const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
1005
1160
 
1161
+ // Helper: extract first string-arg literal from a call_expression node.
1162
+ // Used by route extraction to capture path arg of fetch('/path'), app.get('/path', handler) etc.
1163
+ const { extractStringArg: _extractStringArg } = require('./utils');
1164
+ const getFirstStringArg = (callNode) => {
1165
+ const argsNode = callNode.childForFieldName('arguments');
1166
+ if (!argsNode) return null;
1167
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
1168
+ const arg = argsNode.namedChild(i);
1169
+ if (arg.type === 'comment') continue;
1170
+ return _extractStringArg(arg);
1171
+ }
1172
+ return null;
1173
+ };
1174
+
1175
+ // Helper: count the number of (non-comment) arguments in a call_expression.
1176
+ // Used to disambiguate dual-purpose Express APIs (BUG M5):
1177
+ // app.get('/users', handler) → 2 args → route registration
1178
+ // app.get('env') → 1 arg → config getter, NOT a route
1179
+ const getArgCount = (callNode) => {
1180
+ const argsNode = callNode.childForFieldName('arguments');
1181
+ if (!argsNode) return 0;
1182
+ let count = 0;
1183
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
1184
+ const arg = argsNode.namedChild(i);
1185
+ if (arg.type === 'comment') continue;
1186
+ count++;
1187
+ }
1188
+ return count;
1189
+ };
1190
+
1191
+ // MEDIUM-5: extract HTTP method from `fetch(url, { method: 'POST' })`
1192
+ // and similar XHR/Request-init shapes. Returns the upper-cased method
1193
+ // string or null. Looks at argument index `argIdx` (default 1, the
1194
+ // options object after the URL).
1195
+ const getOptionsMethod = (callNode, argIdx = 1) => {
1196
+ const argsNode = callNode.childForFieldName('arguments');
1197
+ if (!argsNode) return null;
1198
+ // Walk named children and pick the argIdx-th non-comment node.
1199
+ let idx = 0;
1200
+ let target = null;
1201
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
1202
+ const arg = argsNode.namedChild(i);
1203
+ if (arg.type === 'comment') continue;
1204
+ if (idx === argIdx) { target = arg; break; }
1205
+ idx++;
1206
+ }
1207
+ if (!target || target.type !== 'object') return null;
1208
+ for (let i = 0; i < target.namedChildCount; i++) {
1209
+ const prop = target.namedChild(i);
1210
+ if (prop.type !== 'pair') continue;
1211
+ const keyNode = prop.childForFieldName('key');
1212
+ const valNode = prop.childForFieldName('value');
1213
+ if (!keyNode || !valNode) continue;
1214
+ // Key may be `method`, `'method'`, or `"method"`.
1215
+ let keyName = keyNode.text;
1216
+ if (keyNode.type === 'string' || keyNode.type === 'property_identifier') {
1217
+ keyName = keyName.replace(/^['"`]|['"`]$/g, '');
1218
+ }
1219
+ if (keyName !== 'method') continue;
1220
+ // Value must be a literal string. Skip variables / expressions
1221
+ // (we can't statically resolve those).
1222
+ const v = _extractStringArg(valNode);
1223
+ if (v && !v.interp && typeof v.value === 'string' && v.value.length > 0) {
1224
+ return v.value.toUpperCase();
1225
+ }
1226
+ return null;
1227
+ }
1228
+ return null;
1229
+ };
1230
+
1006
1231
  // Helper to check if a node is a non-callable literal
1007
1232
  const isNonCallableInit = (node) => {
1008
1233
  // Primitive literals
@@ -1219,6 +1444,11 @@ function findCallsInCode(code, parser) {
1219
1444
  const alias = aliases.get(funcNode.text);
1220
1445
  const resolvedName = typeof alias === 'string' ? alias : undefined;
1221
1446
  const resolvedNames = Array.isArray(alias) ? alias : undefined;
1447
+ const firstArg = getFirstStringArg(node);
1448
+ // MEDIUM-5: capture explicit method for fetch(url, { method }).
1449
+ const optionsMethod = funcNode.text === 'fetch'
1450
+ ? getOptionsMethod(node, 1)
1451
+ : null;
1222
1452
  calls.push({
1223
1453
  name: funcNode.text,
1224
1454
  ...(resolvedName && { resolvedName }),
@@ -1226,7 +1456,9 @@ function findCallsInCode(code, parser) {
1226
1456
  line: node.startPosition.row + 1,
1227
1457
  isMethod: false,
1228
1458
  enclosingFunction,
1229
- uncertain
1459
+ uncertain,
1460
+ ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
1461
+ ...(optionsMethod && { optionsMethod })
1230
1462
  });
1231
1463
  } else if (funcNode.type === 'member_expression') {
1232
1464
  // Method call: obj.foo() or foo.call/apply/bind()
@@ -1271,6 +1503,8 @@ function findCallsInCode(code, parser) {
1271
1503
  }
1272
1504
  }
1273
1505
  const receiverType = receiver ? localVarTypes.get(receiver) : undefined;
1506
+ const firstArg = getFirstStringArg(node);
1507
+ const argCount = getArgCount(node);
1274
1508
  calls.push({
1275
1509
  name: propName,
1276
1510
  line: node.startPosition.row + 1,
@@ -1278,7 +1512,9 @@ function findCallsInCode(code, parser) {
1278
1512
  receiver,
1279
1513
  ...(receiverType && { receiverType }),
1280
1514
  enclosingFunction,
1281
- uncertain
1515
+ uncertain,
1516
+ ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
1517
+ argCount
1282
1518
  });
1283
1519
  }
1284
1520
  }
@@ -2165,12 +2401,32 @@ const _JS_LIFECYCLE_METHODS = new Set([
2165
2401
  'connectedCallback', 'disconnectedCallback', 'attributeChangedCallback', 'adoptedCallback'
2166
2402
  ]);
2167
2403
 
2404
+ /**
2405
+ * Classify a JS/TS symbol as a runtime entry point of a specific kind.
2406
+ * Returns 'framework' | null.
2407
+ *
2408
+ * - 'framework': React lifecycle methods (componentDidMount, etc.) and Web
2409
+ * Components callbacks (connectedCallback, etc.) — invoked by
2410
+ * the framework, not user code.
2411
+ *
2412
+ * Note: in JS/TS, test cases are framework calls (`it`, `test`, `describe`)
2413
+ * not function definitions, so they aren't classified as test entry points
2414
+ * here — `_addAffectedTestCases` in core/tracing.js handles them via call
2415
+ * detection rather than this predicate.
2416
+ *
2417
+ * Used by tracing/search so `affectedTests` only tags genuine test cases.
2418
+ */
2419
+ function getEntryPointKind(symbol) {
2420
+ if (symbol.isMethod && _JS_LIFECYCLE_METHODS.has(symbol.name)) return 'framework';
2421
+ return null;
2422
+ }
2423
+
2168
2424
  /**
2169
2425
  * Check if a symbol is a JS/TS-convention entry point.
2170
2426
  * These are framework lifecycle methods invoked by React or Web Components.
2171
2427
  */
2172
2428
  function isEntryPoint(symbol) {
2173
- return !!(symbol.isMethod && _JS_LIFECYCLE_METHODS.has(symbol.name));
2429
+ return getEntryPointKind(symbol) !== null;
2174
2430
  }
2175
2431
 
2176
2432
  module.exports = {
@@ -2184,5 +2440,6 @@ module.exports = {
2184
2440
  findExportsInCode,
2185
2441
  findUsagesInCode,
2186
2442
  isEntryPoint,
2443
+ getEntryPointKind,
2187
2444
  parse
2188
2445
  };