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
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ // Request verbs and receiver names discover candidates; only the value's
4
+ // origin can confirm HTTP. Shared by positional and configuration requests.
5
+ const rg = require('./route-graph');
6
+ const { RequestHelperProof, PY_HTTP_MODULES, pyImportedFunction } = require('./request-helpers');
7
+ const { PythonClientResolver } = require('./inprocess-clients');
8
+ const { field, namedChildren, unwrap, findDecl } = rg;
9
+
10
+ const PY_CLIENT_MODULES = [...PY_HTTP_MODULES, 'fastapi.testclient',
11
+ 'starlette.testclient', 'flask.testing', 'werkzeug.test', 'django.test'];
12
+ const PY_FACTORIES = new Set(['Session', 'Client', 'AsyncClient', 'TestClient',
13
+ 'FlaskClient', 'ClientSession', 'AsyncSession', 'HTTPConnection', 'HTTPSConnection',
14
+ 'PoolManager']);
15
+ const NOMINAL_CLIENTS = {
16
+ go: ['net/http.Client', 'net/http'],
17
+ rust: ['reqwest.Client', 'reqwest.blocking.Client', 'reqwest'],
18
+ java: ['org.springframework.web.client.RestTemplate',
19
+ 'org.springframework.web.reactive.function.client.WebClient', 'java.net.http.HttpClient'],
20
+ csharp: ['System.Net.Http.HttpClient'],
21
+ };
22
+ const HTTP_FRAMEWORK = { go: 'go-http', rust: 'reqwest', java: 'spring-client', csharp: 'dotnet-httpclient' };
23
+ const MAX_DEPTH = 12;
24
+
25
+ class ClientReceiverProof {
26
+ constructor(index, sess) {
27
+ this.index = index;
28
+ this.sess = sess;
29
+ this.helpers = new RequestHelperProof(index, sess);
30
+ this.python = new PythonClientResolver(index, sess, () => new Set());
31
+ this.memo = new Map();
32
+ }
33
+
34
+ classify(file, call) {
35
+ const lang = this.sess.lang(file);
36
+ if (['javascript', 'typescript', 'tsx'].includes(lang)) {
37
+ if (!this.helpers.classify(file, call).proven) return null;
38
+ const binding = this.helpers.jsBinding(file, call.receiver);
39
+ const decl = call.receiver && findDecl(this.sess, file, call.receiver,
40
+ this.sess.root(file)?.descendantForIndex(call.callStart, call.callStart));
41
+ const module = binding?.binding.module || this.helpers.requireSpecOf(decl);
42
+ return ['axios', 'ky', 'got'].includes(module) ? module : 'http-client';
43
+ }
44
+ if (lang === 'python') {
45
+ const root = this.sess.root(file);
46
+ const start = call.callSite?.start;
47
+ if (!root || start == null) return null;
48
+ let node = root.descendantForIndex(start, start);
49
+ while (node && node.type !== 'call') node = node.parent;
50
+ const fn = node && field(node, 'function');
51
+ const receiver = fn?.type === 'attribute' ? unwrap(field(fn, 'object')) : null;
52
+ if (!receiver) return null;
53
+ if (PY_HTTP_MODULES.includes(this.pyModule(file, receiver))) return 'requests';
54
+ const values = this.python.clientValues(file, call.line, call.receiver, call.name);
55
+ if (!values?.length || !values.every(v => this.pyValue(v.file, v.expr, 0))) return null;
56
+ return findDecl(this.sess, file, receiver.text, receiver)?.kind === 'param'
57
+ ? 'pytest-client-fixture' : 'python-client';
58
+ }
59
+ return this.nominal(file, call, lang);
60
+ }
61
+
62
+ pyModule(file, expr) {
63
+ if (expr?.type === 'identifier' && !findDecl(this.sess, file, expr.text, expr)) {
64
+ const entry = this.sess.entry(file);
65
+ const candidates = (entry?.importBindings || []).filter(b => (b.alias || b.name) === expr.text);
66
+ const visible = candidates.filter(binding => {
67
+ if (!binding.deferred) return true;
68
+ const row = binding.line - 1;
69
+ const imports = this.sess.root(file)?.descendantsOfType(
70
+ ['import_statement', 'import_from_statement'],
71
+ { row, column: 0 }, { row, column: 1 << 20 }) || [];
72
+ return imports.some(node => {
73
+ let scope = node.parent;
74
+ while (scope && scope.type !== 'function_definition' && scope.type !== 'class_definition') scope = scope.parent;
75
+ return scope?.type === 'function_definition' && scope.startIndex <= expr.startIndex &&
76
+ scope.endIndex >= expr.endIndex;
77
+ });
78
+ });
79
+ if (!visible.length || new Set(visible.map(b => `${b.kind}:${b.module}:${b.name}`)).size !== 1) return null;
80
+ const binding = visible[0];
81
+ if (entry.moduleResolved?.[binding.module]) return null;
82
+ const module = binding.module;
83
+ return PY_CLIENT_MODULES.includes(module)
84
+ ? (binding.kind === 'from' ? `${module}.${binding.name}` : module) : null;
85
+ }
86
+ if (expr?.type === 'attribute') {
87
+ const parent = this.pyModule(file, unwrap(field(expr, 'object')));
88
+ return parent ? `${parent}.${field(expr, 'attribute')?.text}` : null;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ pyValue(file, expr, depth) {
94
+ const e = unwrap(expr);
95
+ if (!e || depth > MAX_DEPTH) return false;
96
+ const key = `${file}:${e.startIndex}:${e.endIndex}`;
97
+ if (this.memo.has(key)) return this.memo.get(key);
98
+ this.memo.set(key, false); // cycles cannot prove an origin
99
+ let proven = false;
100
+ if (e.type === 'identifier') {
101
+ let fn = e.parent;
102
+ while (fn && fn.type !== 'function_definition') fn = fn.parent;
103
+ const value = this.python.followBinding(file, fn, e, 0);
104
+ if (value && (value.file !== file || value.expr?.startIndex !== e.startIndex)) {
105
+ proven = this.pyValue(value.file, value.expr, depth + 1);
106
+ } else {
107
+ const decl = findDecl(this.sess, file, e.text, e);
108
+ if (decl?.count === 1 && decl.valueNode) proven = this.pyValue(file, decl.valueNode, depth + 1);
109
+ }
110
+ } else if (e.type === 'call') {
111
+ const callee = unwrap(field(e, 'function'));
112
+ const identity = this.pyModule(file, callee);
113
+ if (identity && PY_FACTORIES.has(identity.split('.').pop())) proven = true;
114
+ if (!proven) {
115
+ // A helper must RETURN a proven client; merely performing
116
+ // HTTP somewhere in its body does not type its return value.
117
+ const decl = callee?.type === 'identifier' && findDecl(this.sess, file, callee.text, callee);
118
+ let target = decl?.kind === 'function'
119
+ ? { kind: 'function', file, node: decl.valueNode }
120
+ : !decl && callee?.type === 'identifier' ? pyImportedFunction(this.sess, file, callee.text, 0) : null;
121
+ if (decl?.kind === 'param') {
122
+ const fixture = this.python.fixtureDef(file, callee.text, decl.scope);
123
+ const provided = fixture && this.python.fixtureValue(fixture.file, fixture.fnNode, 0);
124
+ if (provided?.expr?.type === 'function_definition') {
125
+ target = { kind: 'function', file: provided.file, node: provided.expr };
126
+ } else if (provided?.expr?.type === 'call') {
127
+ const fn = field(provided.expr, 'function');
128
+ // functools.partial(Client) still constructs the
129
+ // imported client when the fixture is called.
130
+ const binding = fn?.type === 'attribute' &&
131
+ this.helpers.pyBinding(provided.file, field(fn, 'object')?.text);
132
+ if (binding?.module === 'functools' && field(fn, 'attribute')?.text === 'partial') {
133
+ const ctor = namedChildren(field(provided.expr, 'arguments'))[0];
134
+ const identity = this.pyModule(provided.file, ctor);
135
+ proven = !!identity && PY_FACTORIES.has(identity.split('.').pop());
136
+ }
137
+ }
138
+ }
139
+ if (target?.kind === 'function') {
140
+ const fnNode = target.node || target.decl?.valueNode;
141
+ const value = fnNode && this.python.fixtureValue(target.file, fnNode, 0);
142
+ if (value) proven = this.pyValue(value.file, value.expr, depth + 1);
143
+ }
144
+ }
145
+ }
146
+ this.memo.set(key, proven);
147
+ return proven;
148
+ }
149
+
150
+ nominal(file, call, lang) {
151
+ const known = NOMINAL_CLIENTS[lang];
152
+ if (!known || call.localShadow || (call.isPathCall && call.receiverScopeBinding)) return null;
153
+ const entry = this.sess.entry(file);
154
+ const bindings = entry?.importBindings || [];
155
+ const qualifies = name => {
156
+ if (!name) return false;
157
+ const normalized = name.replace(/::/g, '.').replace(/^[*&]+/, '');
158
+ if (known.includes(normalized)) return true;
159
+ return bindings.some(b => {
160
+ const local = b.alias || b.name;
161
+ const module = String(b.module || '').replace(/::/g, '.');
162
+ return (normalized === local && (known.includes(module) || known.includes(`${module}.${b.name}`))) ||
163
+ (normalized.startsWith(local + '.') && known.includes(module + normalized.slice(local.length)));
164
+ });
165
+ };
166
+ if (!call.receiverLocalBinding && !call.isMethod && qualifies(call.receiver)) return HTTP_FRAMEWORK[lang];
167
+ if (call.isPathCall && qualifies(call.receiver)) return HTTP_FRAMEWORK[lang];
168
+ const qualifier = call.receiverTypeQualifier || call.receiverTypeNamespace;
169
+ const type = qualifier ? `${qualifier}.${call.receiverType}` : call.receiverType;
170
+ if (qualifies(type)) return HTTP_FRAMEWORK[lang];
171
+ // Java field receivers carry the enclosing owner rather than the
172
+ // field type in the call record. Read the declared field's AST type.
173
+ if (lang === 'java' && call.receiverRootType && call.receiverField) {
174
+ const root = this.sess.root(file);
175
+ for (const declaration of root?.descendantsOfType('field_declaration') || []) {
176
+ let owner = declaration.parent;
177
+ while (owner && owner.type !== 'class_declaration') owner = owner.parent;
178
+ if (field(owner, 'name')?.text !== call.receiverRootType) continue;
179
+ if (namedChildren(declaration).some(n => n.type === 'variable_declarator' &&
180
+ field(n, 'name')?.text === call.receiverField) && qualifies(field(declaration, 'type')?.text)) {
181
+ return HTTP_FRAMEWORK[lang];
182
+ }
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ }
188
+
189
+ module.exports = { ClientReceiverProof };
@@ -183,9 +183,9 @@ const COMMAND_CONTRACTS = Object.freeze({
183
183
  target: 'Optional symbol/handle. Omitting it selects diff/precommit mode.',
184
184
  modes: [
185
185
  { name: 'symbol', when: 'A symbol target is supplied; `base` names the revision whose declaration its call sites were written against.', answer: 'Argument-count compatibility, uncertain sites, and the caller account.' },
186
- { name: 'diff', when: 'No target is supplied; `base` or `staged` selects changes.', answer: 'Composed diff impact, signature checks, and affected tests.' },
186
+ { name: 'diff', when: 'No target is supplied; `base` or `staged` selects changes.', answer: 'A verdict per changed symbol by what the change did: blocked (a break was observed, every site listed), review (can reach existing code, reason named) or ready (nothing that existed depends on it); TRUST is the worst of them. Affected tests.' },
187
187
  ],
188
- defaults: ['Symbol mode checks static arity, not compiler type compatibility.', '`base` defaults to HEAD in both modes.'],
188
+ defaults: ['Symbol mode checks static arity, not compiler type compatibility.', '`base` defaults to HEAD in both modes.', 'Diff mode: `staged` reads the staged tree from the git index, never the worktree; `strict` makes REVIEW_REQUIRED fail as BLOCKED does.'],
189
189
  truth: 'Findings are static diagnostics with explicit uncertainty; a clean result is not a compiler/test pass.',
190
190
  nonGoals: ['Full type checking.', 'Linting or executing project tests.'],
191
191
  invalidCombinations: [
@@ -193,24 +193,25 @@ const COMMAND_CONTRACTS = Object.freeze({
193
193
  '`base` and `staged` cannot both select the diff.',
194
194
  ],
195
195
  examples: ['ucn check publishOrderCreated', 'ucn check publishOrderCreated --base=main', 'ucn check --staged'],
196
- jsonData: 'Symbol mode returns signature, valid/mismatched/uncertain sites and accounting; diff mode returns composed diagnostics.',
196
+ jsonData: 'Symbol mode returns signature, valid/mismatched/uncertain sites and accounting; diff mode returns each changed symbol with `verdict` and `reasons`, and `trust` with `status` and `passes`.',
197
197
  output: 'Broad diagnostic; scope to a symbol or focused Git diff.',
198
198
  next: ['Run the language compiler/type checker and selected tests.', 'Use `plan` before a signature refactor.'],
199
199
  benchmark: 'A10',
200
200
  }),
201
201
  plan: contract({
202
202
  question: 'What exact indexed edits would this proposed refactor require?',
203
- purpose: 'Preview rename or parameter-shape edits without mutating files.',
203
+ purpose: 'Preview rename or parameter-shape edits, or assess a deletion, without mutating files.',
204
204
  target: 'Required symbol name or stable handle plus one refactor operation.',
205
205
  modes: [
206
206
  { name: 'rename', when: '`renameTo` is supplied.', answer: 'Selected declaration plus indexed call/reference/import/export edit previews and source-text review items.' },
207
207
  { name: 'add parameter', when: '`addParam` is supplied.', answer: 'Selected declaration and call-site preview, optionally with a default.' },
208
208
  { name: 'remove parameter', when: '`removeParam` is supplied.', answer: 'Selected declaration and affected call-site preview.' },
209
+ { name: 'delete', when: '`delete` is supplied.', answer: 'A deletion assessment of the selected declaration: `no-static-dependents`, `has-static-dependents` or `review`, with the reason. The same rule the dead-code audit applies to every name.' },
209
210
  ],
210
211
  defaults: ['Preview only; no file writes.'],
211
212
  truth: 'Changes are derived from indexed definitions/usages, include the selected declaration, and retain unverified/blocked or needs-review evidence separately; non-source files receive an explicit exact-text search handoff.',
212
- nonGoals: ['Applying edits.', 'Guaranteeing the preview compiles.'],
213
- invalidCombinations: ['Exactly one of rename, add-parameter, or remove-parameter is required.', '`defaultValue` only applies to add-parameter.'],
213
+ nonGoals: ['Applying edits.', 'Guaranteeing the preview compiles.', 'Guaranteeing a deletion is safe: a deletion assessment is static evidence.'],
214
+ invalidCombinations: ['Exactly one of rename, add-parameter, remove-parameter, or delete is required.', '`defaultValue` only applies to add-parameter.'],
214
215
  examples: ['ucn plan parseRequest --rename-to=parseIncoming', 'ucn plan parseRequest --add-param=context --default-value=null'],
215
216
  jsonData: 'Before/after signatures, concrete declaration/call/reference/import/export previews, changeSummary, reviewItems, non-source handoff, unverified sites, and account metadata.',
216
217
  output: 'Targeted refactor preview; inspect every unverified or warning entry.',