ucn 5.4.2 → 5.5.0

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 (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +40 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +457 -60
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +2113 -0
  62. package/core/rust-modules.js +185 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1161 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2237 -115
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -8,7 +8,7 @@
8
8
  * with stable tree-sitter facts.
9
9
  */
10
10
 
11
- const { getParser, safeParse } = require('../languages');
11
+ const { getParser, safeParse, langTraits } = require('../languages');
12
12
 
13
13
  const CALLABLE_NODES = new Set([
14
14
  // JavaScript / TypeScript
@@ -167,9 +167,13 @@ function isDefaultBranch(node) {
167
167
  function findCallableForRange(root, startLine, endLine) {
168
168
  const candidates = [];
169
169
  walkNamed(root, node => {
170
- if (!CALLABLE_NODES.has(node.type)) return true;
171
170
  const start = node.startPosition.row + 1;
172
171
  const end = node.endPosition.row + 1;
172
+ // A subtree lies within its root's rows: one disjoint from the
173
+ // range holds no candidate (fix #365 - the walk used to visit every
174
+ // node of the file for each measured callable).
175
+ if (end < startLine || start > endLine) return false;
176
+ if (!CALLABLE_NODES.has(node.type)) return true;
173
177
  if (start < startLine || end > endLine) return true;
174
178
  candidates.push({
175
179
  node,
@@ -193,10 +197,8 @@ function findCallableForRange(root, startLine, endLine) {
193
197
  * indexed callable. Formatting, comments, strings, optional chaining, and
194
198
  * nullish coalescing cannot affect these values.
195
199
  */
196
- function computeAstComplexity(content, language, {
197
- startLine = 1,
198
- endLine = startLine,
199
- } = {}) {
200
+ function computeAstComplexity(content, language, options = {}) {
201
+ const { startLine = 1, endLine = startLine } = options;
200
202
  const lineCount = Math.max(0, endLine - startLine + 1);
201
203
  try {
202
204
  const parser = getParser(language);
@@ -208,7 +210,7 @@ function computeAstComplexity(content, language, {
208
210
  measuredBy: 'unavailable-no-parser',
209
211
  };
210
212
  }
211
- const tree = safeParse(parser, content);
213
+ const tree = options.tree || safeParse(parser, content);
212
214
  const callable = findCallableForRange(tree.rootNode, startLine, endLine);
213
215
  if (!callable) {
214
216
  return {
@@ -276,99 +278,22 @@ function computedReceiver(callee) {
276
278
  return node?.type === 'identifier' ? node.text : null;
277
279
  }
278
280
 
279
- const STRING_LITERAL_NODES = new Set([
280
- 'string', 'string_literal', 'raw_string_literal',
281
- 'interpreted_string_literal', 'verbatim_string_literal',
282
- ]);
283
-
284
- function literalStringValue(node) {
285
- if (!node || !STRING_LITERAL_NODES.has(node.type)) return null;
286
- const content = (node.namedChildren || []).find(child =>
287
- child.type === 'string_content' || child.type === 'interpreted_string_literal_content');
288
- if (content) return content.text;
289
- const raw = String(node.text || '');
290
- const first = raw.search(/["']/);
291
- if (first < 0) return null;
292
- const quote = raw[first];
293
- const triple = raw.slice(first, first + 3) === quote.repeat(3);
294
- const width = triple ? 3 : 1;
295
- if (!raw.endsWith(quote.repeat(width))) return null;
296
- const value = raw.slice(first + width, -width);
297
- // Escaped/interpolated member names are not a stable static spelling.
298
- if (value.includes('\\') || value.includes('{') || value.includes('$')) return null;
299
- return value;
300
- }
301
-
302
- function callShape(node) {
303
- if (!['call', 'call_expression', 'invocation_expression',
304
- 'method_invocation'].includes(node.type)) return null;
305
- const callee = node.childForFieldName('function') ||
306
- node.childForFieldName('name') || node.namedChild(0);
307
- const args = node.childForFieldName('arguments') ||
308
- (node.namedChildren || []).find(child =>
309
- ['argument_list', 'arguments', 'bracketed_argument_list'].includes(child.type));
310
- return callee && args ? { callee, args: args.namedChildren || [] } : null;
311
- }
312
-
313
281
  /**
314
- * Extract recognized reflection operations and classify whether their member
315
- * target is a stable literal. Literal targets are positive liveness evidence;
316
- * dynamic targets cannot identify one member but must still be disclosed by
317
- * deletion-oriented commands.
282
+ * Extract recognized reflection operations and classify their member target:
283
+ * a stable literal (`name`), literal fragments around a runtime part
284
+ * (`patterns`, fix #363), or dynamic. Literal targets and specific patterns
285
+ * are positive liveness evidence; dynamic targets cannot identify a member
286
+ * but must still be disclosed by deletion-oriented commands.
318
287
  */
319
- function reflectionSites(content, language) {
288
+ function reflectionSites(content, language, tree = null) {
320
289
  try {
321
- const syntaxHints = {
322
- python: ['getattr', 'setattr', 'hasattr', 'delattr'],
323
- javascript: ['Reflect.'],
324
- typescript: ['Reflect.'],
325
- tsx: ['Reflect.'],
326
- go: ['MethodByName', 'FieldByName'],
327
- java: ['getMethod', 'getDeclaredMethod', 'getField', 'getDeclaredField'],
328
- csharp: ['GetMethod', 'GetProperty', 'GetField'],
329
- }[language];
330
- if (!syntaxHints || !syntaxHints.some(hint => content.includes(hint))) {
331
- return [];
290
+ if (!langTraits(language)?.reflectionApi) return [];
291
+ if (!tree) {
292
+ const parser = getParser(language);
293
+ if (!parser) return [];
294
+ tree = safeParse(parser, content);
332
295
  }
333
- const parser = getParser(language);
334
- if (!parser) return [];
335
- const tree = safeParse(parser, content);
336
- const sites = [];
337
- walkNamed(tree.rootNode, node => {
338
- const call = callShape(node);
339
- if (!call) return true;
340
- const callee = String(call.callee.text || '');
341
- let argIndex = null;
342
- let kind = null;
343
- if (language === 'python' &&
344
- ['getattr', 'setattr', 'hasattr', 'delattr'].includes(callee)) {
345
- argIndex = 1;
346
- kind = callee;
347
- } else if (['javascript', 'typescript', 'tsx'].includes(language) &&
348
- /^(?:globalThis\.)?Reflect\.(?:get|set|has|deleteProperty)$/.test(callee)) {
349
- argIndex = 1;
350
- kind = callee.split('.').pop();
351
- } else if (language === 'go' &&
352
- /\.(?:MethodByName|FieldByName)$/.test(callee)) {
353
- argIndex = 0;
354
- kind = callee.split('.').pop();
355
- } else if (['java', 'csharp'].includes(language) &&
356
- /(?:^|\.)(?:getMethod|getDeclaredMethod|getField|getDeclaredField|GetMethod|GetProperty|GetField)$/.test(callee)) {
357
- argIndex = 0;
358
- kind = callee.split('.').pop();
359
- }
360
- if (argIndex == null || !call.args[argIndex]) return true;
361
- const name = literalStringValue(call.args[argIndex]);
362
- sites.push({
363
- ...(name && /^[A-Za-z_$][\w$]*$/.test(name) && { name }),
364
- kind,
365
- line: node.startPosition.row + 1,
366
- expression: node.text,
367
- dynamic: !name || !/^[A-Za-z_$][\w$]*$/.test(name),
368
- });
369
- return true;
370
- });
371
- return sites;
296
+ return require('./reflection').reflectionSitesInTree(tree, language, content);
372
297
  } catch (_) {
373
298
  return [];
374
299
  }
@@ -382,11 +307,19 @@ function literalReflectionSites(content, language) {
382
307
  * Find direct computed dispatch calls such as handlers[name](). Literal keys
383
308
  * are excluded because they retain a statically visible member name.
384
309
  */
385
- function computedDispatchSites(content, language) {
310
+ const CALL_NODE_TYPES = new Set(['call_expression', 'call', 'invocation_expression']);
311
+ const BINDING_NODE_TYPES = new Set(['variable_declarator', 'assignment_expression', 'assignment']);
312
+ const COMPUTED_INDEX_NODE_TYPES = [
313
+ 'subscript_expression', 'index_expression', 'subscript', 'element_access_expression',
314
+ ];
315
+
316
+ function computedDispatchSites(content, language, tree = null) {
386
317
  try {
387
- const parser = getParser(language);
388
- if (!parser) return [];
389
- const tree = safeParse(parser, content);
318
+ if (!tree) {
319
+ const parser = getParser(language);
320
+ if (!parser) return [];
321
+ tree = safeParse(parser, content);
322
+ }
390
323
  const sites = [];
391
324
  const seen = new Set();
392
325
  const selectedByLocal = new Map();
@@ -410,35 +343,45 @@ function computedDispatchSites(content, language) {
410
343
  expression: expression || node.text,
411
344
  });
412
345
  };
413
- walkNamed(tree.rootNode, node => {
414
- let callee = null;
415
- if (node.type === 'call_expression' || node.type === 'call' ||
416
- node.type === 'invocation_expression') {
417
- callee = node.childForFieldName('function');
418
- }
419
- record(node, callee);
420
-
421
- if (callee?.type === 'identifier') {
422
- calledLocals.add(`${scopeKey(node)}\0${callee.text}`);
346
+ // Every site's callee (or bound value) is an index expression, so
347
+ // the native query starts from those (fix #365: the named-node walk
348
+ // touched every node of every file). A callee is its call's first
349
+ // child, so index-expression order is the calls' document order.
350
+ const sameRange = (a, b) => !!a && !!b &&
351
+ a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type;
352
+ for (const indexExpr of tree.rootNode.descendantsOfType(COMPUTED_INDEX_NODE_TYPES)) {
353
+ if (!indexExpr.isNamed) continue;
354
+ const node = indexExpr.parent;
355
+ if (!node || !node.isNamed) continue;
356
+ if (CALL_NODE_TYPES.has(node.type)) {
357
+ const callee = node.childForFieldName('function');
358
+ if (sameRange(callee, indexExpr)) record(node, callee);
359
+ continue;
423
360
  }
424
-
425
361
  // Two-step dispatch: `const h = handlers[key]; h()`. Record the
426
362
  // dynamic access only when its bound local is actually invoked in
427
363
  // the same callable scope. Ordinary indexing (`xs[i]`) is a value
428
364
  // read and says nothing about runtime-selected call targets.
429
- if (node.type === 'variable_declarator' ||
430
- node.type === 'assignment_expression' ||
431
- node.type === 'assignment') {
365
+ if (BINDING_NODE_TYPES.has(node.type)) {
432
366
  const left = node.childForFieldName('name') ||
433
367
  node.childForFieldName('left');
434
368
  const right = node.childForFieldName('value') ||
435
369
  node.childForFieldName('right');
436
- if (left?.type === 'identifier' && indexNodeForComputedCallee(right)) {
370
+ if (sameRange(right, indexExpr) && left?.type === 'identifier' &&
371
+ indexNodeForComputedCallee(right)) {
437
372
  selectedByLocal.set(`${scopeKey(node)}\0${left.text}`, { node, right });
438
373
  }
439
374
  }
440
- return true;
441
- });
375
+ }
376
+ if (selectedByLocal.size > 0) {
377
+ for (const node of tree.rootNode.descendantsOfType([...CALL_NODE_TYPES])) {
378
+ if (!node.isNamed) continue;
379
+ const callee = node.childForFieldName('function');
380
+ if (callee?.type === 'identifier') {
381
+ calledLocals.add(`${scopeKey(node)}\0${callee.text}`);
382
+ }
383
+ }
384
+ }
442
385
  for (const [key, selection] of selectedByLocal) {
443
386
  if (calledLocals.has(key)) record(selection.node, selection.right);
444
387
  }
@@ -469,11 +412,27 @@ function projectComputedDispatch(index) {
469
412
  return byFile;
470
413
  }
471
414
 
415
+ /**
416
+ * Project reflection inventory (fix #363): per-file sites are extracted at
417
+ * index time from the file's own parse (fileEntry.reflectionSites), so this
418
+ * is a view, never a re-parse.
419
+ */
420
+ function projectReflectionSites(index) {
421
+ const byFile = new Map();
422
+ for (const [filePath, fileEntry] of index.files) {
423
+ if (Array.isArray(fileEntry.reflectionSites) && fileEntry.reflectionSites.length > 0) {
424
+ byFile.set(filePath, fileEntry.reflectionSites);
425
+ }
426
+ }
427
+ return byFile;
428
+ }
429
+
472
430
  module.exports = {
473
431
  declarationSnapshots,
474
432
  computeAstComplexity,
475
433
  computedDispatchSites,
476
434
  projectComputedDispatch,
435
+ projectReflectionSites,
477
436
  reflectionSites,
478
437
  literalReflectionSites,
479
438
  };