ucn 5.5.2 → 5.6.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 (74) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -12
  2. package/.claude/skills/ucn/references/commands.md +21 -17
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +474 -0
  6. package/README.md +71 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +138 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +273 -107
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +29 -65
  13. package/core/cache.js +101 -16
  14. package/core/callers.js +352 -70
  15. package/core/capture.js +331 -0
  16. package/core/change-kinds.js +888 -0
  17. package/core/check.js +1117 -139
  18. package/core/client-receivers.js +189 -0
  19. package/core/command-contracts.js +7 -6
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +388 -85
  23. package/core/deletion.js +401 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoints.js +12 -6
  26. package/core/evidence-bands.js +68 -0
  27. package/core/evidence-bands.json +44 -0
  28. package/core/execute.js +172 -161
  29. package/core/hot-summary.js +241 -0
  30. package/core/index-ir.js +3 -1
  31. package/core/inprocess-clients.js +41 -15
  32. package/core/ir.js +5 -1
  33. package/core/member-signatures.js +789 -0
  34. package/core/output/analysis-ext.js +20 -10
  35. package/core/output/analysis.js +98 -60
  36. package/core/output/check.js +98 -37
  37. package/core/output/doctor.js +34 -1
  38. package/core/output/find.js +2 -1
  39. package/core/output/lines.js +63 -3
  40. package/core/output/public.js +30 -11
  41. package/core/output/refactoring.js +32 -0
  42. package/core/output/reporting.js +21 -2
  43. package/core/output/search.js +11 -2
  44. package/core/output/tracing.js +35 -7
  45. package/core/output-budget.js +124 -57
  46. package/core/parallel-build.js +3 -3
  47. package/core/project.js +106 -23
  48. package/core/provenance-type.js +12 -5
  49. package/core/public-command.js +35 -9
  50. package/core/python-modules.js +6 -0
  51. package/core/reflection.js +166 -23
  52. package/core/registry.js +623 -72
  53. package/core/reporting.js +69 -34
  54. package/core/request-helpers.js +63 -7
  55. package/core/review-items.js +75 -0
  56. package/core/search.js +89 -128
  57. package/core/shared.js +0 -45
  58. package/core/snapshot.js +350 -0
  59. package/core/source-universe.js +707 -0
  60. package/core/test-units.js +3624 -0
  61. package/core/tracing.js +150 -96
  62. package/core/verify.js +27 -11
  63. package/languages/adapter.js +8 -3
  64. package/languages/c-family.js +12 -0
  65. package/languages/csharp.js +634 -0
  66. package/languages/go.js +120 -0
  67. package/languages/index.js +422 -4
  68. package/languages/java.js +712 -0
  69. package/languages/javascript.js +1460 -2
  70. package/languages/lexical-scope.js +68 -0
  71. package/languages/python.js +1672 -1
  72. package/languages/rust.js +91 -0
  73. package/mcp/server.js +44 -110
  74. package/package.json +11 -6
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { CANONICAL_COMMANDS } = require('./registry');
3
+ const { CANONICAL_COMMANDS, GLOBAL_PARAMS } = require('./registry');
4
4
 
5
5
  const NAME_COMMANDS = new Set(['show', 'find', 'usages', 'trace', 'tests', 'plan']);
6
6
 
@@ -34,14 +34,16 @@ function sourceRangeTarget(params) {
34
34
  /** Build normalized execute() params from the shared CLI-style positional arg. */
35
35
  function buildPublicParams(command, arg, params = {}) {
36
36
  const clean = { ...params };
37
- if (clean.top === 0 && params.topRaw == null) delete clean.top;
38
- if (clean.maxLines == null) delete clean.maxLines;
39
- // Surface-only values never belong in execute params.
40
- for (const key of ['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'includeBundled', 'interactive',
41
- 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'maxChars', 'maxCharsRaw',
42
- 'depthRaw', 'contextRaw', 'workersRaw',
43
- '_fileFromFileMode']) delete clean[key];
37
+ // Process flags and the build/transport parameters every command accepts
38
+ // never belong in execute params: the surface applies them itself.
39
+ for (const key of ['json', 'quiet', 'cache', 'clearCache', 'interactive', 'workers', '_fileFromFileMode',
40
+ ...GLOBAL_PARAMS]) delete clean[key];
44
41
 
42
+ return withoutUnset(subjectParams(command, arg, clean));
43
+ }
44
+
45
+ /** The params with the positional argument placed as the command's subject. */
46
+ function subjectParams(command, arg, clean) {
45
47
  if (NAME_COMMANDS.has(command)) return { ...clean, name: arg || clean.name };
46
48
  switch (command) {
47
49
  case 'search': return { ...clean, term: arg || clean.term };
@@ -58,8 +60,32 @@ function buildPublicParams(command, arg, params = {}) {
58
60
  }
59
61
  }
60
62
 
63
+ /** A parameter that was not given has no key: the same object MCP arguments make. */
64
+ function withoutUnset(params) {
65
+ for (const key of Object.keys(params)) {
66
+ if (params[key] === undefined) delete params[key];
67
+ }
68
+ return params;
69
+ }
70
+
71
+ /**
72
+ * What to say about a parameter a command accepts and that changes nothing,
73
+ * or less than its name suggests, there. One text for every surface, in CLI
74
+ * spelling: the surface renders it with formatSurfaceMessage.
75
+ */
76
+ function noOpNotes(command, params = {}) {
77
+ if (!params.includeMethods) return [];
78
+ if (['impact', 'trace', 'tests', 'check'].includes(command)) {
79
+ return [`--include-methods has no effect on '${command}' — method calls are always tiered by receiver evidence.`];
80
+ }
81
+ if (command === 'show') {
82
+ return [`--include-methods on '${command}' affects only method-callee display for standalone-function targets — caller tiers are always evidence-based, and method targets analyze method calls by default.`];
83
+ }
84
+ return [];
85
+ }
86
+
61
87
  function isPublicCommand(command) {
62
88
  return CANONICAL_COMMANDS.includes(command);
63
89
  }
64
90
 
65
- module.exports = { buildPublicParams, isPublicCommand, parseSourceTarget, sourceRangeTarget };
91
+ module.exports = { buildPublicParams, isPublicCommand, noOpNotes, parseSourceTarget, sourceRangeTarget };
@@ -141,6 +141,12 @@ function pythonModuleScope(index, file, receiver, site, memo = new Map()) {
141
141
  for (const child of current.namedChildren) stack.push(child);
142
142
  }
143
143
  }
144
+ // The name token of `module.Name`: the module root is read where
145
+ // the whole attribute is.
146
+ const { sameNode } = require('../languages/utils');
147
+ if (node?.parent?.type === 'attribute' && sameNode(node.parent.childForFieldName('attribute'), node)) {
148
+ node = node.parent;
149
+ }
144
150
  if (node) {
145
151
  const { referenceScope } = require('../languages/lexical-scope');
146
152
  const scopesKey = 'py-module-scopes';
@@ -730,8 +730,31 @@ const MEMBER_CALLEE_FIELDS = {
730
730
  member_expression: ['property', 'object'], // JS/TS
731
731
  member_access_expression: ['name', 'expression'], // C#
732
732
  attribute: ['attribute', 'object'], // Python
733
+ field_access: ['field', 'object'], // Java qualified namespace
733
734
  };
734
735
 
736
+ /** Parentheses change neither a callee nor a namespace qualifier. */
737
+ function _calleeNode(node) {
738
+ while (node?.type === 'parenthesized_expression') {
739
+ node = node.namedChildren.find(child => child.type !== 'comment');
740
+ }
741
+ return node;
742
+ }
743
+
744
+ /** A written identifier/member path, from fields rather than intervening text. */
745
+ function _calleeName(node) {
746
+ node = _calleeNode(node);
747
+ if (!node) return '';
748
+ if (['identifier', 'property_identifier', 'field_identifier', 'package_identifier'].includes(node.type)) {
749
+ return node.text;
750
+ }
751
+ const fields = MEMBER_CALLEE_FIELDS[node.type];
752
+ if (!fields) return '';
753
+ const owner = _calleeName(node.childForFieldName(fields[1]));
754
+ const member = _calleeName(node.childForFieldName(fields[0]));
755
+ return owner && member ? `${owner}.${member}` : '';
756
+ }
757
+
735
758
  /** Callee text, member name and receiver of a call node (no arguments). */
736
759
  function _calleeParts(node, wantMembers, wantQualified) {
737
760
  if (node.type === 'method_invocation') {
@@ -739,7 +762,7 @@ function _calleeParts(node, wantMembers, wantQualified) {
739
762
  const name = node.childForFieldName('name')?.text || '';
740
763
  return { calleeText: name, member: name, object: node.childForFieldName('object') };
741
764
  }
742
- const fn = node.childForFieldName('function') || node.namedChild(0);
765
+ const fn = _calleeNode(node.childForFieldName('function') || node.namedChild(0));
743
766
  if (!fn) return null;
744
767
  if (fn.type === 'identifier') {
745
768
  return { calleeText: fn.text || '', member: fn.text, object: null, bare: true };
@@ -762,18 +785,58 @@ function _callArgs(node) {
762
785
  return argsNode ? argsNode.namedChildren.filter(c => c.type !== 'comment') : null;
763
786
  }
764
787
 
788
+ // Construction nodes: `new Function(..)` builds a callable from a string.
789
+ const NEW_NODE_TYPES = ['new_expression', 'object_creation_expression'];
790
+
765
791
  /**
766
- * Reflection sites of one parsed file.
792
+ * Local names a file's imports bind to the modules of `{ package }` rules:
793
+ * `reflect` for `import "reflect"`, `r` for `import r "reflect"`. An import
794
+ * that binds no qualifier (Go `.` and `_`) names none.
795
+ * @returns {Map<string, string>} local name -> module
796
+ */
797
+ function _packageQualifiers(rules, imports) {
798
+ const modules = new Set(rules.filter(rule => rule.package).map(rule => rule.package));
799
+ const qualifiers = new Map();
800
+ if (modules.size === 0) return qualifiers;
801
+ for (const item of imports || []) {
802
+ if (!item || !modules.has(item.module)) continue;
803
+ for (const name of item.names || []) {
804
+ // The parser already supplies valid import names, including
805
+ // Unicode identifiers. Dot and blank imports bind no qualifier.
806
+ if (name && name !== '_' && name !== '.') qualifiers.set(name, item.module);
807
+ }
808
+ }
809
+ return qualifiers;
810
+ }
811
+
812
+ /**
813
+ * Reflection facts of one parsed file: { sites, reflectionUses, evalUses }.
814
+ *
767
815
  * Each site: { kind, line, expression, dynamic, name?, patterns?, receiver?,
768
- * membersOnly? }.
816
+ * membersOnly? }: a reflective access that names its member by a string.
817
+ *
818
+ * The two counts are how often the file uses the language's reflection and
819
+ * code-execution vocabulary, whether or not a name can be read from the use.
820
+ * `reflectionUses`: the call-shaped sites (a `functions` or `members` call
821
+ * that passes its name argument) plus calls of the `unnamed` rules.
822
+ * `evalUses`: bare calls of `namespaceLookups.calls` that pass an argument,
823
+ * plus calls and constructions of the `codeExecution` rules. They count call
824
+ * and construction nodes of the tree, so a spelling inside a comment or a
825
+ * string is no use, and neither is a method or a definition that shares a
826
+ * name with a builtin (`self.exec(..)`, `def eval(..)`), nor a same-named
827
+ * method called without the name (`request.getMethod()`).
828
+ *
829
+ * `options.imports` are the file's import records: a `{ package }` rule
830
+ * matches a call qualified by a local name they bind to that module.
769
831
  */
770
- function reflectionSitesInTree(tree, language, code = null) {
832
+ function reflectionFactsInTree(tree, language, code = null, options = {}) {
833
+ const facts = { sites: [], reflectionUses: 0, evalUses: 0 };
771
834
  const traits = langTraits(language) || {};
772
835
  const api = traits.reflectionApi;
773
- if (!api || !tree) return [];
836
+ if (!api || !tree) return facts;
774
837
  const resolve = makeResolver();
775
838
  const ctx = { formatCalls: api.formatCalls || null, resolve };
776
- const sites = [];
839
+ const sites = facts.sites;
777
840
  const push = (node, kind, nameNode, receiverNode, receiverFact) => {
778
841
  const cls = classifyNameExpression(nameNode, ctx);
779
842
  const expression = String(node.text || '');
@@ -796,22 +859,37 @@ function reflectionSitesInTree(tree, language, code = null) {
796
859
  });
797
860
  };
798
861
  const ns = api.namespaceLookups;
862
+ // Vocabulary that names no member: counted, never a site.
863
+ const unnamed = api.unnamed || [];
864
+ const codeExecution = api.codeExecution || [];
865
+ const constructs = codeExecution.filter(rule => rule.construct);
866
+ const qualifiers = _packageQualifiers([...unnamed, ...codeExecution], options.imports);
867
+ const scopeMemo = new Map();
799
868
  // Candidate nodes are located natively: reflective calls from the
800
869
  // vocabulary's spelling positions (descendantForIndex, walking up to the
801
870
  // enclosing call), computed member keys by node type. No JS-side walk of
802
871
  // the whole tree.
803
- const hintPattern = api.hintPattern || null;
804
- const useHints = code != null && hintPattern;
872
+ const hintSources = [];
873
+ if (api.hintPattern) hintSources.push(api.hintPattern.source);
874
+ // A package's qualifier is the file's own choice (`r "reflect"`).
875
+ if (qualifiers.size > 0) {
876
+ const { literalNameRegex } = require('./shared');
877
+ hintSources.push(...[...qualifiers.keys()].map(name => literalNameRegex(name).source));
878
+ }
879
+ const useHints = code != null && hintSources.length > 0;
880
+ const callTypes = constructs.length > 0 ? [...CALL_NODE_TYPES, ...NEW_NODE_TYPES] : CALL_NODE_TYPES;
805
881
  const types = [];
806
882
  if (!useHints) {
807
- types.push(...CALL_NODE_TYPES);
883
+ types.push(...callTypes);
808
884
  if (ns?.subscriptOf) types.push('subscript');
809
885
  }
810
886
  if (api.computedMembers) types.push('subscript_expression');
811
887
  const wantMembers = (api.members || []).length > 0;
812
- // Qualified function callees (`Reflect.get`) need the callee text of
813
- // member-shaped calls; bare vocabularies (`getattr`) never do.
814
- const wantQualified = (api.functions || []).some(rule => rule.qualified);
888
+ // Qualified callees (`Reflect.get`, a package's functions) need the
889
+ // callee text of member-shaped calls; bare vocabularies (`getattr`)
890
+ // never do.
891
+ const wantQualified = qualifiers.size > 0 ||
892
+ [...(api.functions || []), ...unnamed, ...codeExecution].some(rule => rule.qualified);
815
893
  const visitNode = node => {
816
894
  if (node.type === 'subscript_expression') {
817
895
  // obj["on" + evt] / this[`handle${t}`] (JS/TS computed member
@@ -843,15 +921,23 @@ function reflectionSitesInTree(tree, language, code = null) {
843
921
  }
844
922
  return;
845
923
  }
924
+ if (NEW_NODE_TYPES.includes(node.type)) {
925
+ // `new Function(code)`: the constructed class decides.
926
+ const constructed = node.childForFieldName('constructor') || node.childForFieldName('type');
927
+ const text = _calleeName(constructed);
928
+ if (constructs.some(rule => rule.construct.test(text))) facts.evalUses++;
929
+ return;
930
+ }
846
931
  const call = _calleeParts(node, wantMembers, wantQualified);
847
932
  if (!call) return;
848
933
  // Match on the callee spelling first; arguments are read only for a
849
934
  // vocabulary hit (most calls in a hinted file are unrelated).
850
- const calleeText = () => (call.calleeText ??= call.fn.text || '');
935
+ const calleeText = () => (call.calleeText ??= _calleeName(call.fn));
851
936
  for (const rule of api.functions || []) {
852
937
  if (!rule.callee.test(calleeText())) continue;
853
938
  const args = _callArgs(node);
854
939
  if (args && args[rule.nameArg]) {
940
+ facts.reflectionUses++;
855
941
  push(node, rule.kind || calleeText().split('.').pop(),
856
942
  args[rule.nameArg], args[rule.receiverArg]);
857
943
  }
@@ -861,7 +947,10 @@ function reflectionSitesInTree(tree, language, code = null) {
861
947
  for (const rule of api.members || []) {
862
948
  if (!rule.member.test(call.member)) continue;
863
949
  const args = _callArgs(node);
864
- if (args && args[rule.nameArg]) push(node, call.member, args[rule.nameArg], call.object);
950
+ if (args && args[rule.nameArg]) {
951
+ facts.reflectionUses++;
952
+ push(node, call.member, args[rule.nameArg], call.object);
953
+ }
865
954
  return;
866
955
  }
867
956
  }
@@ -869,25 +958,58 @@ function reflectionSitesInTree(tree, language, code = null) {
869
958
  // so even a fully dynamic name is bounded to it.
870
959
  if (ns?.calls && call.bare && ns.calls.test(call.calleeText)) {
871
960
  const args = _callArgs(node);
872
- if (args && args[0]) push(node, call.calleeText, args[0], null, MODULE_RECEIVER);
961
+ if (args && args[0]) {
962
+ facts.evalUses++;
963
+ push(node, call.calleeText, args[0], null, MODULE_RECEIVER);
964
+ }
965
+ return;
873
966
  }
967
+ if (unnamed.length === 0 && codeExecution.length === 0) return;
968
+ // The callee as written, receiver included (`Class.forName`,
969
+ // `reflect.ValueOf`); a bare callee is its own text.
970
+ let written = null;
971
+ const writtenCallee = () => (written ??= (node.type === 'method_invocation'
972
+ ? (call.object ? `${_calleeName(call.object)}.${call.member}` : call.member || '')
973
+ : calleeText()));
974
+ const matches = rule => {
975
+ if (rule.construct) return false;
976
+ if (rule.callee) return rule.callee.test(writtenCallee());
977
+ if (rule.package) {
978
+ const qualifier = _calleeNode(call.object);
979
+ if (qualifier?.type !== 'identifier' || qualifiers.get(qualifier.text) !== rule.package) return false;
980
+ // The file import owns the spelling only outside scopes
981
+ // that bind another value to it (parameters, locals, etc.).
982
+ return require('../languages/lexical-scope').referenceScope(
983
+ qualifier, language, scopeMemo) === 'module';
984
+ }
985
+ return false;
986
+ };
987
+ if (unnamed.some(matches)) facts.reflectionUses++;
988
+ else if (codeExecution.some(matches)) facts.evalUses++;
874
989
  };
875
990
  try {
876
991
  const candidates = [];
877
992
  if (useHints) {
993
+ // One candidate per node: nested calls can begin at one offset
994
+ // (`a.getMethod(x).getField(y)`), so the end is part of the key.
878
995
  const seen = new Set();
879
- const re = new RegExp(hintPattern.source, 'g');
996
+ const once = (kind, node) => {
997
+ const key = `${kind}${node.startIndex}:${node.endIndex}`;
998
+ if (seen.has(key)) return false;
999
+ seen.add(key);
1000
+ return true;
1001
+ };
1002
+ const re = new RegExp(hintSources.join('|'), 'gu');
880
1003
  let m;
881
1004
  while ((m = re.exec(code)) !== null) {
1005
+ if (m[0] === '') { re.lastIndex++; continue; }
882
1006
  let node = tree.rootNode.descendantForIndex(m.index);
883
- for (let i = 0; node && i < 6; i++, node = node.parent) {
884
- if (!CALL_NODE_TYPES.includes(node.type)) continue;
885
- if (!seen.has(node.startIndex)) { seen.add(node.startIndex); candidates.push(node); }
1007
+ for (; node; node = node.parent) {
1008
+ if (!callTypes.includes(node.type)) continue;
1009
+ if (once('c', node)) candidates.push(node);
886
1010
  // `globals()[name]`: the lookup is the enclosing subscript.
887
1011
  const parent = node.parent;
888
- if (ns?.subscriptOf && parent?.type === 'subscript' &&
889
- !seen.has(-parent.startIndex - 1)) {
890
- seen.add(-parent.startIndex - 1);
1012
+ if (ns?.subscriptOf && parent?.type === 'subscript' && once('s', parent)) {
891
1013
  candidates.push(parent);
892
1014
  }
893
1015
  break;
@@ -898,7 +1020,12 @@ function reflectionSitesInTree(tree, language, code = null) {
898
1020
  candidates.sort((a, b) => a.startIndex - b.startIndex || (a.endIndex - b.endIndex));
899
1021
  for (const node of candidates) visitNode(node);
900
1022
  } catch (_) { /* partial inventory is disclosed as-is */ }
901
- return sites;
1023
+ return facts;
1024
+ }
1025
+
1026
+ /** The reflection sites of one parsed file (see reflectionFactsInTree). */
1027
+ function reflectionSitesInTree(tree, language, code = null) {
1028
+ return reflectionFactsInTree(tree, language, code).sites;
902
1029
  }
903
1030
 
904
1031
  // ---------------------------------------------------------------------------
@@ -1113,11 +1240,26 @@ function resetReflectionIndex(index) {
1113
1240
  index._reflectionPatternIndex = null;
1114
1241
  }
1115
1242
 
1243
+ /** Exact AST string literals, for conservative same-file dynamic dispatch.
1244
+ * This does not prove that a literal flows to a reflection call. */
1245
+ function reflectionLiteralNames(tree) {
1246
+ const names = new Map();
1247
+ if (!tree) return names;
1248
+ for (const node of tree.rootNode.descendantsOfType([...STRING_NODES])) {
1249
+ const value = classifyNameExpression(node);
1250
+ if (value.name && !value.dynamic && !names.has(value.name)) {
1251
+ names.set(value.name, node.startPosition.row + 1);
1252
+ }
1253
+ }
1254
+ return names;
1255
+ }
1256
+
1116
1257
  module.exports = {
1117
1258
  WILD,
1118
1259
  stringAlternatives,
1119
1260
  classifyNameExpression,
1120
1261
  classifyReceiver,
1262
+ reflectionFactsInTree,
1121
1263
  reflectionSitesInTree,
1122
1264
  globMatches,
1123
1265
  patternEntriesFor,
@@ -1126,5 +1268,6 @@ module.exports = {
1126
1268
  reflectionPatternIndex,
1127
1269
  patternIsAttributable,
1128
1270
  resetReflectionIndex,
1271
+ reflectionLiteralNames,
1129
1272
  resolveKeyParameter,
1130
1273
  };