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.
- package/.claude/skills/ucn/SKILL.md +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- 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: '
|
|
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
|
|
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,
|
|
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.',
|