ucn 5.4.2 → 5.5.1

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 +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  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 +529 -31
  16. package/core/callers.js +12228 -921
  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 +472 -66
  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 +3026 -0
  62. package/core/rust-modules.js +203 -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 +1185 -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 +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Where a Rust call's value goes, from the syntax tree alone (fix #372).
3
+ *
4
+ * Shared by the Rust parser, which records per call record whether the
5
+ * value can possibly be lost or misused (`valueConsumed` / `consumingMethod`),
6
+ * and by audit-async, which classifies a resolved future's flow. The one
7
+ * decision that needs project facts (a method called on the value) is left
8
+ * to the caller: with no producer it is reported as kind 'member', otherwise
9
+ * `memberVerdict(future, member, isMethod, exprNode)` decides.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const { sameNode } = require('./utils');
15
+
16
+ function fieldIs(parent, field, node) {
17
+ const child = parent.childForFieldName(field);
18
+ return !!child && sameNode(child, node);
19
+ }
20
+
21
+ function hasChildToken(node, tokenType) {
22
+ for (let i = 0; i < node.childCount; i++) {
23
+ if (node.child(i).type === tokenType) return true;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ const RUST_FUNCTION_BOUNDARY = new Set(['function_item', 'closure_expression']);
29
+
30
+ function rustPatternBinds(pattern, name) {
31
+ if (!pattern) return false;
32
+ if (pattern.type === 'identifier') return pattern.text === name;
33
+ if (pattern.type === 'field_identifier' || pattern.type === 'type_identifier' ||
34
+ pattern.type === 'scoped_identifier') return false;
35
+ for (let i = 0; i < pattern.namedChildCount; i++) {
36
+ const child = pattern.namedChild(i);
37
+ // `Some(x)`: the variant path is not a binding.
38
+ if (sameNode(pattern.childForFieldName('type'), child)) continue;
39
+ if (rustPatternBinds(child, name)) return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function rustLetPattern(letNode) {
45
+ return letNode.childForFieldName('pattern');
46
+ }
47
+
48
+ function rustMatchDestructures(matchNode) {
49
+ const body = matchNode.childForFieldName('body');
50
+ for (const arm of body?.namedChildren || []) {
51
+ if (arm.type !== 'match_arm') continue;
52
+ const pattern = arm.childForFieldName('pattern');
53
+ const inner = pattern?.type === 'match_pattern' ? pattern.namedChild(0) : pattern;
54
+ if (inner && inner.type !== 'identifier' && !(pattern.namedChildCount > 1 && inner.type === 'identifier')) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Where an unpolled Rust future value goes. Returns
63
+ * { kind: 'awaited' | 'flow' } polled or handed on
64
+ * { kind: 'discarded' } statement position
65
+ * { kind: 'dropped' } `let _ = fut;`
66
+ * { kind: 'stored', holder, binding, declared } plain local
67
+ * { kind: 'used-as-value', member? } treated as its Output
68
+ */
69
+ function rustValueFlow(exprNode, future, memberVerdict) {
70
+ let expr = exprNode;
71
+ for (let depth = 0; depth < 32; depth++) {
72
+ const parent = expr.parent;
73
+ if (!parent) return { kind: 'flow' };
74
+ switch (parent.type) {
75
+ case 'parenthesized_expression':
76
+ case 'unsafe_block':
77
+ case 'else_clause':
78
+ case 'match_arm':
79
+ case 'match_block':
80
+ expr = parent;
81
+ continue;
82
+ case 'block': {
83
+ // Only the tail expression is the block's value.
84
+ let tail = parent.namedChildCount - 1;
85
+ while (tail >= 0 && ['line_comment', 'block_comment'].includes(parent.namedChild(tail).type)) tail--;
86
+ if (tail < 0 || !sameNode(parent.namedChild(tail), expr)) return { kind: 'flow' };
87
+ expr = parent;
88
+ continue;
89
+ }
90
+ case 'if_expression':
91
+ if (fieldIs(parent, 'condition', expr)) return { kind: 'used-as-value' };
92
+ expr = parent;
93
+ continue;
94
+ case 'match_expression':
95
+ if (fieldIs(parent, 'value', expr)) {
96
+ return rustMatchDestructures(parent) ? { kind: 'used-as-value' } : { kind: 'flow' };
97
+ }
98
+ expr = parent;
99
+ continue;
100
+ case 'let_condition': {
101
+ if (!fieldIs(parent, 'value', expr)) return { kind: 'flow' };
102
+ const pattern = parent.childForFieldName('pattern');
103
+ return pattern && pattern.type !== 'identifier' ? { kind: 'used-as-value' } : { kind: 'flow' };
104
+ }
105
+ case 'await_expression':
106
+ return { kind: 'awaited' };
107
+ case 'expression_statement': {
108
+ // A block-like expression (`if`/`match`) without `;` at the
109
+ // end of a block is that block's value, not a statement.
110
+ const block = parent.parent;
111
+ if (block?.type === 'block' && !hasChildToken(parent, ';')) {
112
+ let tail = block.namedChildCount - 1;
113
+ while (tail >= 0 && ['line_comment', 'block_comment'].includes(block.namedChild(tail).type)) tail--;
114
+ if (tail >= 0 && sameNode(block.namedChild(tail), parent)) {
115
+ expr = block;
116
+ continue;
117
+ }
118
+ }
119
+ return { kind: 'discarded' };
120
+ }
121
+ case 'let_declaration': {
122
+ if (!fieldIs(parent, 'value', expr)) return { kind: 'flow' };
123
+ const pattern = rustLetPattern(parent);
124
+ if (!pattern || pattern.text === '_') return { kind: 'dropped' };
125
+ if (pattern.type === 'identifier') {
126
+ return { kind: 'stored', holder: parent, binding: pattern.text, declared: true };
127
+ }
128
+ return { kind: 'used-as-value' };
129
+ }
130
+ case 'assignment_expression': {
131
+ if (!fieldIs(parent, 'right', expr)) return { kind: 'flow' };
132
+ const left = parent.childForFieldName('left');
133
+ if (left?.type === 'identifier') {
134
+ return { kind: 'stored', holder: parent, binding: left.text, declared: false };
135
+ }
136
+ return left?.text === '_' ? { kind: 'dropped' } : { kind: 'flow' };
137
+ }
138
+ case 'try_expression':
139
+ case 'binary_expression':
140
+ case 'unary_expression':
141
+ case 'compound_assignment_expr':
142
+ case 'range_expression':
143
+ return { kind: 'used-as-value' };
144
+ case 'index_expression':
145
+ return sameNode(parent.namedChild(0), expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
146
+ case 'for_expression':
147
+ return fieldIs(parent, 'value', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
148
+ case 'while_expression':
149
+ return fieldIs(parent, 'condition', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
150
+ case 'call_expression':
151
+ return fieldIs(parent, 'function', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
152
+ case 'field_expression': {
153
+ if (!fieldIs(parent, 'value', expr)) return { kind: 'flow' };
154
+ const member = parent.childForFieldName('field')?.text;
155
+ const call = parent.parent;
156
+ const isMethod = call?.type === 'call_expression' && fieldIs(call, 'function', parent);
157
+ // Unresolved producer (a first, cheap pass): undecided.
158
+ if (!future) return { kind: 'member', member, node: expr };
159
+ return memberVerdict(future, member, isMethod, expr);
160
+ }
161
+ default:
162
+ // Arguments, collections, struct fields, returns, closures,
163
+ // references, casts, `Box::pin(..)`, macro token trees.
164
+ return { kind: 'flow' };
165
+ }
166
+ }
167
+ return { kind: 'flow' };
168
+ }
169
+
170
+ /**
171
+ * Follow a local that received a future: the first use that takes it for its
172
+ * Output, or whether it is used at all before it is shadowed, reassigned or
173
+ * goes out of scope. Returns { read: bool, misuse: node|null }.
174
+ */
175
+ function rustStoredFutureUse(holder, name, future, memberVerdict) {
176
+ let statement = holder;
177
+ while (statement.parent && statement.parent.type !== 'block') statement = statement.parent;
178
+ const declBlock = statement.parent;
179
+ if (!declBlock) return { read: true, misuse: null };
180
+ let read = false;
181
+ let misuse = null;
182
+ let stop = false;
183
+ // A loop around an assignment can read the value on the next iteration.
184
+ if (holder.type === 'assignment_expression') {
185
+ for (let up = holder.parent; up && !RUST_FUNCTION_BOUNDARY.has(up.type); up = up.parent) {
186
+ if (['loop_expression', 'while_expression', 'for_expression'].includes(up.type)) return { read: true, misuse: null };
187
+ }
188
+ }
189
+ // Only nodes whose text holds `name` can read, misuse, rebind or
190
+ // overwrite the local (fix #388): the walk skips every other subtree.
191
+ // Offsets of every substring occurrence in the region the walk can
192
+ // reach (the declaring block, or the enclosing function for an
193
+ // assignment that may be read after its block).
194
+ let region = declBlock;
195
+ if (holder.type === 'assignment_expression') {
196
+ region = null;
197
+ for (let up = declBlock; up; up = up.parent) {
198
+ region = up;
199
+ if (RUST_FUNCTION_BOUNDARY.has(up.type)) break;
200
+ }
201
+ }
202
+ const occurrences = [];
203
+ {
204
+ const text = region.text;
205
+ const base = region.startIndex;
206
+ for (let at = name ? text.indexOf(name) : -1; at >= 0; at = text.indexOf(name, at + 1)) {
207
+ occurrences.push(base + at);
208
+ }
209
+ }
210
+ const mayHoldName = node => {
211
+ const start = node.startIndex;
212
+ let lo = 0;
213
+ let hi = occurrences.length;
214
+ while (lo < hi) {
215
+ const mid = (lo + hi) >> 1;
216
+ if (occurrences[mid] < start) lo = mid + 1;
217
+ else hi = mid;
218
+ }
219
+ return lo < occurrences.length && occurrences[lo] + name.length <= node.endIndex;
220
+ };
221
+ const visit = node => {
222
+ if (stop || misuse) return;
223
+ if (!mayHoldName(node)) return;
224
+ switch (node.type) {
225
+ case 'function_item':
226
+ return;
227
+ case 'closure_expression':
228
+ if (rustPatternBinds(node.childForFieldName('parameters'), name)) return;
229
+ break;
230
+ case 'match_arm':
231
+ if (rustPatternBinds(node.childForFieldName('pattern'), name)) return;
232
+ break;
233
+ case 'token_tree':
234
+ // Macro arguments (`join!`, `select!`, `println!`) consume it.
235
+ if (tokenTreeNames(node, name)) read = true;
236
+ return;
237
+ case 'identifier':
238
+ if (node.text === name) {
239
+ read = true;
240
+ const flow = rustValueFlow(node, future, memberVerdict);
241
+ if (flow.kind === 'used-as-value' || flow.kind === 'member') misuse = node;
242
+ }
243
+ return;
244
+ case 'block': {
245
+ for (let i = 0; i < node.namedChildCount && !stop && !misuse; i++) {
246
+ const child = node.namedChild(i);
247
+ if (child.type === 'let_declaration' && rustPatternBinds(rustLetPattern(child), name)) {
248
+ const value = child.childForFieldName('value');
249
+ if (value) visit(value);
250
+ return; // shadowed for the rest of this block
251
+ }
252
+ visit(child);
253
+ }
254
+ return;
255
+ }
256
+ case 'assignment_expression': {
257
+ const left = node.childForFieldName('left');
258
+ if (left?.type === 'identifier' && left.text === name) {
259
+ const right = node.childForFieldName('right');
260
+ if (right) visit(right);
261
+ stop = true; // overwritten: the old future is dropped
262
+ return;
263
+ }
264
+ break;
265
+ }
266
+ default:
267
+ break;
268
+ }
269
+ for (let i = 0; i < node.namedChildCount && !stop && !misuse; i++) visit(node.namedChild(i));
270
+ };
271
+ let started = false;
272
+ for (let i = 0; i < declBlock.namedChildCount && !stop && !misuse; i++) {
273
+ const child = declBlock.namedChild(i);
274
+ if (!started) {
275
+ if (sameNode(child, statement)) {
276
+ started = true;
277
+ // `x = fut` inside an expression statement: nothing else in it.
278
+ }
279
+ continue;
280
+ }
281
+ if (child.type === 'let_declaration' && rustPatternBinds(rustLetPattern(child), name)) {
282
+ const value = child.childForFieldName('value');
283
+ if (value) visit(value);
284
+ break;
285
+ }
286
+ visit(child);
287
+ }
288
+ // An assignment to an outer local stays readable after its block ends.
289
+ if (!read && !stop && !misuse && holder.type === 'assignment_expression') {
290
+ let inner = declBlock;
291
+ for (let outer = inner.parent; outer && !stop && !misuse; inner = outer, outer = outer.parent) {
292
+ if (RUST_FUNCTION_BOUNDARY.has(outer.type)) break;
293
+ if (outer.type !== 'block') continue;
294
+ let after = false;
295
+ for (let i = 0; i < outer.namedChildCount && !stop && !misuse; i++) {
296
+ const child = outer.namedChild(i);
297
+ if (!after) {
298
+ if (child.startIndex <= inner.startIndex && child.endIndex >= inner.endIndex) after = true;
299
+ continue;
300
+ }
301
+ visit(child);
302
+ }
303
+ }
304
+ }
305
+ return { read: read || !!misuse, misuse };
306
+ }
307
+
308
+ function tokenTreeNames(node, name) {
309
+ for (let i = 0; i < node.namedChildCount; i++) {
310
+ const child = node.namedChild(i);
311
+ if (child.type === 'identifier' && child.text === name) return true;
312
+ if (child.type === 'token_tree' && tokenTreeNames(child, name)) return true;
313
+ }
314
+ return false;
315
+ }
316
+
317
+ /**
318
+ * Parser fact (fix #372): can this call's value be lost or used as a
319
+ * resolved value, judged before knowing what the call returns? Mirrors
320
+ * audit-async's pre-resolution test exactly: a value that is awaited or
321
+ * flows on is consumed; a method called on it (directly or on the local it
322
+ * is stored in) is recorded as `consumingMethod` for the name-level Output
323
+ * test; anything else (discarded, dropped, stored and never read, used as
324
+ * a value, a field read) stays open.
325
+ * @param {object} callNode - call_expression
326
+ * @returns {{ valueConsumed: string|null, consumingMethod: string|null }}
327
+ */
328
+ function rustValueFacts(callNode) {
329
+ const open = { valueConsumed: null, consumingMethod: null };
330
+ let expr = callNode;
331
+ while (expr.parent?.type === 'parenthesized_expression') expr = expr.parent;
332
+ const parent = expr.parent;
333
+ if (parent?.type === 'await_expression') return { valueConsumed: 'awaited', consumingMethod: null };
334
+ if (parent?.type === 'arguments') return { valueConsumed: 'argument', consumingMethod: null };
335
+ const methodOf = flow => {
336
+ const member = flow.node?.parent;
337
+ const call = member?.parent;
338
+ return call?.type === 'call_expression' && fieldIs(call, 'function', member) ? flow.member || null : null;
339
+ };
340
+ const flow = rustValueFlow(callNode, null, null);
341
+ if (flow.kind === 'awaited') return { valueConsumed: 'awaited', consumingMethod: null };
342
+ if (flow.kind === 'flow') return { valueConsumed: 'flow', consumingMethod: null };
343
+ if (flow.kind === 'member') {
344
+ const method = methodOf(flow);
345
+ return method ? { valueConsumed: null, consumingMethod: method } : open;
346
+ }
347
+ if (flow.kind !== 'stored') return open;
348
+ const use = rustStoredFutureUse(flow.holder, flow.binding, null, null);
349
+ if (!use.read) return open;
350
+ if (!use.misuse) return { valueConsumed: 'stored', consumingMethod: null };
351
+ const misuseFlow = rustValueFlow(use.misuse, null, null);
352
+ if (misuseFlow.kind !== 'member') return open;
353
+ const method = methodOf(misuseFlow);
354
+ return method ? { valueConsumed: null, consumingMethod: method } : open;
355
+ }
356
+
357
+ // Standard-library path roots: a `std::..::f()` call never reaches a
358
+ // project definition.
359
+ const STD_ROOTS = new Set(['std', 'core', 'alloc']);
360
+
361
+ /**
362
+ * Compact file-level shape of a call record whose value is not consumed
363
+ * where produced (fix #372), persisted per file so audit-async decides
364
+ * whether a file can hold a lost future without loading its call records:
365
+ * name bare call
366
+ * name.Type method call (Type = receiverType, may be empty)
367
+ * name:Last path call (Last = last receiver segment, 'std' for std roots)
368
+ * ...~member a method is called on the value
369
+ * @param {object} record - call record
370
+ */
371
+ function openCallShape(record) {
372
+ let shape = record.name;
373
+ if (record.isPathCall) {
374
+ const parts = String(record.receiver || '').split('::').filter(Boolean);
375
+ shape += ':' + (STD_ROOTS.has(parts[0]) ? 'std' : (parts[parts.length - 1] || ''));
376
+ } else if (record.isMethod) {
377
+ shape += '.' + (record.receiverType || '');
378
+ }
379
+ return record.consumingMethod ? `${shape}~${record.consumingMethod}` : shape;
380
+ }
381
+
382
+ /**
383
+ * The call-record fields an openCallShape keeps, as a record-shaped object
384
+ * (a superset test: fields the shape drops only ever narrow a match).
385
+ */
386
+ function parseOpenCallShape(shape) {
387
+ const text = String(shape);
388
+ const tilde = text.indexOf('~');
389
+ const head = tilde < 0 ? text : text.slice(0, tilde);
390
+ const record = tilde < 0 ? {} : { consumingMethod: text.slice(tilde + 1) };
391
+ const colon = head.indexOf(':');
392
+ const dot = head.indexOf('.');
393
+ if (colon >= 0) {
394
+ const last = head.slice(colon + 1);
395
+ return { ...record, name: head.slice(0, colon), isMethod: true, isPathCall: true,
396
+ receiver: last === 'std' ? 'std::_' : last };
397
+ }
398
+ if (dot >= 0) {
399
+ const type = head.slice(dot + 1);
400
+ return { ...record, name: head.slice(0, dot), isMethod: true, ...(type && { receiverType: type }) };
401
+ }
402
+ return { ...record, name: head };
403
+ }
404
+
405
+ module.exports = {
406
+ openCallShape,
407
+ parseOpenCallShape,
408
+ RUST_FUNCTION_BOUNDARY,
409
+ rustPatternBinds,
410
+ rustLetPattern,
411
+ rustMatchDestructures,
412
+ rustValueFlow,
413
+ rustStoredFutureUse,
414
+ tokenTreeNames,
415
+ rustValueFacts,
416
+ };