ucn 5.5.3 → 5.6.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 (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  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 +485 -0
  6. package/README.md +66 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
@@ -0,0 +1,363 @@
1
+ /**
2
+ * core/capture.js - Can an ADDED declaration change what existing code means?
3
+ *
4
+ * Adding code is inert when nothing that existed before can reach it. Two
5
+ * questions decide that:
6
+ *
7
+ * novelty does the name occur anywhere outside the lines the diff
8
+ * added? A name with no earlier occurrence cannot have been
9
+ * called, imported, shadowed or looked up before, so nothing
10
+ * that existed binds to the new declaration.
11
+ * capture even under a new name, a member added to a type that already
12
+ * existed can take part in something existing code relies on:
13
+ * it can override or be overridden, become a requirement every
14
+ * implementer must now supply, change how the type is
15
+ * constructed or laid out, be called by the runtime or a
16
+ * framework without being named.
17
+ *
18
+ * Each hazard is named. None of them is a proven break: they are the
19
+ * reasons an addition is review and not ready.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const { hierarchySlotClosure } = require('./contract-membership');
25
+ const { NON_CALLABLE_TYPES, isValueDefinition } = require('./shared');
26
+ const { langTraits } = require('../languages');
27
+
28
+ const HAZARD_TEXT = Object.freeze({
29
+ 'name-in-use': 'the name already occurs outside this change: existing code may now bind to the new declaration',
30
+ 'novelty-unknown': 'part of the project could not be searched for the name, so it cannot be shown to be new',
31
+ 'new-requirement': 'a new requirement of an existing contract: every implementer must now supply it',
32
+ 'overrides-inherited': 'overrides a member the type inherited: existing calls on this type now reach it',
33
+ 'overridden-by-descendant': 'a descendant already declares this member: it now overrides the new one',
34
+ 'constructor-of-existing-type': 'changes how an existing type is constructed',
35
+ 'field-of-existing-type': 'a new member of an existing type: construction, positional initialisation and layout can depend on its members',
36
+ 'shape-of-existing-type': 'a new member of a type whose members are compared and enumerated by shape: values used as the type or as its constructor, and types computed from its members, are held to it',
37
+ 'literal-lacks-new-member': 'an object literal declared as this type does not have the new member',
38
+ 'protocol-member': 'a member the language runtime or the compiler invokes by name',
39
+ 'registered': 'decorated or registered with a framework, which may invoke it without naming it',
40
+ 'overrides-external': 'implements or overrides something defined outside the project',
41
+ 'reflection-reachable': 'reflection in the project can reach this name',
42
+ 'syntax-invoked': 'invoked by operator, conversion or indexer syntax that never contains its name: expressions that exist may now resolve to it',
43
+ 'registry-member': 'member of a registry that is called through a computed key',
44
+ });
45
+
46
+ // Name-level reasons a deletion assessment withholds a claim, read as a
47
+ // property of an addition: what can reach a name can reach a new one.
48
+ const HAZARD_OF_SHIELD = Object.freeze({
49
+ 'reflection': 'reflection-reachable',
50
+ 'dynamic-dispatch': 'registry-member',
51
+ 'operator-surface': 'syntax-invoked',
52
+ });
53
+
54
+ /**
55
+ * Where an occurrence of a new declaration's name could mean it.
56
+ *
57
+ * a function-local declaration the lines of its enclosing scope;
58
+ * a module-level declaration of its own file, the files that import the
59
+ * a module (a file with its own name from it, and the files that take
60
+ * scope) everything the module has (a namespace
61
+ * or module import, a star import);
62
+ * anything else (a member, a the whole project: a receiver, a
63
+ * name in a shared scope) package, an inclusion can reach it
64
+ * from anywhere.
65
+ *
66
+ * Returns candidate files and a predicate over (relative path, line), or
67
+ * null for "anywhere". Candidate files bound the scan before parsing.
68
+ * An occurrence elsewhere is another name that happens to be spelled the
69
+ * same: it could not have meant this declaration before, and cannot now.
70
+ */
71
+ function reachOf(index, definition) {
72
+ if (!definition || definition.className) return null;
73
+ const entry = index.files.get(definition.file);
74
+ if (!entry) return null;
75
+ const own = entry.relativePath;
76
+ if (definition.lexicalScopeStartLine != null) {
77
+ return { files: new Set([own]), includes: (file, line) => file === own &&
78
+ line >= definition.lexicalScopeStartLine && line <= definition.lexicalScopeEndLine };
79
+ }
80
+ const traits = langTraits(entry.language) || {};
81
+ if (traits.packageScope !== 'file' || traits.textualIncludes) return null;
82
+ // A script without module syntax shares one global scope with the
83
+ // others (trait `scriptGlobalScope`).
84
+ if (traits.scriptGlobalScope && (entry.imports || []).length === 0 && (entry.exports || []).length === 0) return null;
85
+ const files = new Set([own]);
86
+ const exported = new Set(entry.exports || []);
87
+ if (traits.moduleNamesImportable) {
88
+ for (const symbol of entry.symbols || []) if (!symbol.className) exported.add(symbol.name);
89
+ }
90
+ // A name the module does not export stays in the module.
91
+ if (!exported.has(definition.name)) return { files, includes: file => file === own };
92
+ for (const other of index.files.values()) {
93
+ const resolved = other.moduleResolved || {};
94
+ const takesAll = (other.importDetails || []).some(detail =>
95
+ resolved[detail.module] === own && (detail.names || []).includes('*'));
96
+ const reExportsAll = (other.exportDetails || []).some(detail =>
97
+ detail && (detail.name === '*' || detail.type === 're-export-all') && resolved[detail.source] === own);
98
+ // Everything the module exports is passed on: who receives the
99
+ // name is no longer written here.
100
+ if (reExportsAll) return null;
101
+ // A binding of one of the module's exports takes that name only.
102
+ // Any other binding of the module holds the module itself.
103
+ const binds = (other.importBindings || []).some(binding => resolved[binding.module] === own &&
104
+ (binding.name === definition.name || !exported.has(binding.name) ||
105
+ binding.kind === 'namespace' || binding.kind === 'default'));
106
+ if (takesAll || binds) files.add(other.relativePath);
107
+ }
108
+ return { files, includes: file => files.has(file) };
109
+ }
110
+
111
+ /**
112
+ * Occurrences, in code, of a new declaration's name outside the lines the
113
+ * diff added, among the places where the name could mean the declaration.
114
+ *
115
+ * @param {object} index
116
+ * @param {string} name
117
+ * @param {Map<string, Set<number>>} addedLinesByFile - relative path -> lines
118
+ * @param {object} [definition] - the new declaration (its reach narrows
119
+ * the search; without it every occurrence counts)
120
+ * @returns {{outside: {file, line}[], total: number}}
121
+ */
122
+ function occurrencesOutsideChange(index, name, addedLinesByFile, definition = null) {
123
+ const reaches = reachOf(index, definition);
124
+ const options = { includeTests: true, codeOnly: true };
125
+ // Use the same proven module/lexical reach that filters the answer to
126
+ // avoid parsing unrelated files for every added declaration. The caller
127
+ // still assesses the full source-universe account before claiming novelty.
128
+ const rows = reaches
129
+ ? [...reaches.files].sort().flatMap(file => index.usages(name, { ...options, file }) || [])
130
+ : index.usages(name, options) || [];
131
+ const outside = [];
132
+ for (const row of rows) {
133
+ const file = row.relativePath || row.file;
134
+ if (addedLinesByFile.get(file)?.has(row.line)) continue;
135
+ if (reaches && !reaches.includes(file, row.line)) continue;
136
+ outside.push({ file, line: row.line });
137
+ }
138
+ outside.sort((a, b) => (a.file === b.file ? a.line - b.line : (a.file < b.file ? -1 : 1)));
139
+ return { outside, total: rows.length };
140
+ }
141
+
142
+ /** The type definitions that own a member: same file, containing it. */
143
+ function ownersOf(index, definition) {
144
+ return (index.symbols.get(definition.className) || []).filter(type =>
145
+ type.file === definition.file && !type.className &&
146
+ type.startLine <= definition.startLine && (type.endLine || type.startLine) >= definition.startLine);
147
+ }
148
+
149
+ /** A member of a wholly new, unreachable owner cannot capture a namesake. */
150
+ function newOwnerUnreached(index, definition, addedLinesByFile) {
151
+ if (!definition.className) return false;
152
+ const owners = ownersOf(index, definition);
153
+ if (owners.length !== 1) return false;
154
+ const owner = owners[0];
155
+ // An impl block, namespace or out-of-class member group can be newly
156
+ // written while its type existed all along (including outside source).
157
+ if (!['class', 'struct', 'record', 'enum', 'interface', 'trait', 'type', 'union'].includes(owner.type)) return false;
158
+ const memo = index._opMemo?.('checkNewOwnerReach', () => new WeakMap());
159
+ if (memo?.has(owner)) return memo.get(owner);
160
+ let unreachable = false;
161
+ const added = addedLinesByFile.get(owner.relativePath);
162
+ if (added?.has(owner.nameLine || owner.startLine) && !owner.extends && !owner.implements &&
163
+ !(owner.decorators || []).length && !(owner.attributes || []).length) {
164
+ // Name reach is the module/import scope of THIS owner, not the
165
+ // global spelling of a field. Bases, registrations and reflection
166
+ // remain separately guarded by the ordinary capture assessment.
167
+ const occurrences = occurrencesOutsideChange(index, owner.name, addedLinesByFile, owner);
168
+ unreachable = occurrences.outside.length === 0;
169
+ }
170
+ memo?.set(owner, unreachable);
171
+ return unreachable;
172
+ }
173
+
174
+ /** One syntax tree per file for the questions below. */
175
+ function treeReader(index) {
176
+ const { parseRevision } = require('./change-kinds');
177
+ const roots = new Map();
178
+ return file => {
179
+ if (!roots.has(file)) {
180
+ let root;
181
+ try { root = parseRevision(index._readFile(file), index.files.get(file)?.language); } catch (_) { root = null; }
182
+ roots.set(file, root);
183
+ }
184
+ return roots.get(file);
185
+ };
186
+ }
187
+
188
+ /**
189
+ * What a new member does to the SHAPE of the type that owns it: the member
190
+ * set existing code is held to, whether or not it names the member.
191
+ *
192
+ * Where classes are compared by shape (traits `classComparedByShape`,
193
+ * `classMemberRole`), a class's members are a contract in two ways. Values
194
+ * used as the class must have them: an object literal or any class, and
195
+ * where the class only INHERITS a private or protected member, any class
196
+ * that inherits the same one. And types computed from its members
197
+ * (`keyof`, mapped types, `Pick`) are held to them even when the class
198
+ * declares a private member of its own and so has no other values.
199
+ *
200
+ * 'required' every value used as the type must now have the member: an
201
+ * instance member of a class that declares no private or
202
+ * protected member of its own. A private one included: no
203
+ * other value can ever have it;
204
+ * 'shape' the type is something else to existing code, with no value
205
+ * that can be shown to lack the member: a public member of a
206
+ * class held by its own declaration (seen by `keyof` only),
207
+ * a static member, hidden or not (the constructor is
208
+ * compared by shape whatever its instances are held to), an
209
+ * optional member, a member whose role cannot be read; and a
210
+ * virtual member where that changes what the type is (trait
211
+ * `virtualMemberChangesShape`);
212
+ * null nothing: a private or protected INSTANCE member of a class
213
+ * that already declares one, which no other code can hold,
214
+ * name or enumerate.
215
+ *
216
+ * "Declares one of its own" is asked of the class AS IT WAS: members on
217
+ * lines the change added are left out (the first private member of a class
218
+ * is what makes every structural value of it stop being one).
219
+ *
220
+ * @param {Map<string, Set<number>>} [addedLinesByFile] by relative path
221
+ */
222
+ function shapeChangeOf(index, definition, addedLinesByFile = null) {
223
+ const entry = index.files.get(definition.file);
224
+ const traits = langTraits(entry && entry.language) || {};
225
+ if (traits.virtualMemberChangesShape && (definition.modifiers || []).includes('virtual')) return 'shape';
226
+ if (typeof traits.classComparedByShape !== 'function' || typeof traits.classMemberRole !== 'function') return null;
227
+ const rootOf = treeReader(index);
228
+ let change = null;
229
+ for (const owner of ownersOf(index, definition)) {
230
+ if (owner.type !== 'class') continue; // an interface member is a requirement, asked about elsewhere
231
+ const root = rootOf(owner.file);
232
+ const node = root && classNodeAt(root, owner);
233
+ const member = node ? traits.classMemberRole(node, definition.name, definition.startLine) : null;
234
+ if (!member) { change = 'shape'; continue; }
235
+ const added = (addedLinesByFile && addedLinesByFile.get(owner.relativePath)) || null;
236
+ const ownDeclaration = !traits.classComparedByShape(node, added);
237
+ // The constructor's type is its own contract: a static member is
238
+ // part of it whatever the instances are held to, private included
239
+ // (anything with a matching construct signature was a `typeof T`).
240
+ if (member.role === 'static') { change = 'shape'; continue; }
241
+ if (member.hidden && ownDeclaration) continue;
242
+ if (member.hidden || (member.role === 'instance' && !ownDeclaration)) return 'required';
243
+ change = 'shape';
244
+ }
245
+ return change;
246
+ }
247
+
248
+ /** The class declaration node a type definition names. */
249
+ function classNodeAt(root, type) {
250
+ let found = null;
251
+ const visit = node => {
252
+ const start = node.startPosition.row + 1;
253
+ const end = node.endPosition.row + 1;
254
+ if (end < type.startLine || start > (type.endLine || type.startLine)) return;
255
+ if (node.childForFieldName('body') && node.childForFieldName('name')?.text === type.name &&
256
+ /class/.test(node.type)) {
257
+ if (!found || node.startIndex < found.startIndex) found = node;
258
+ return;
259
+ }
260
+ for (const child of node.namedChildren) visit(child);
261
+ };
262
+ visit(root);
263
+ return found;
264
+ }
265
+
266
+ // A member name that is exactly what a literal's key would spell.
267
+ const PLAIN_MEMBER_NAME = /^#?[A-Za-z_$][A-Za-z0-9_$]*$/;
268
+
269
+ /**
270
+ * Object literals declared as the type that owns `definition` and that do
271
+ * not have a member of its name: `const box: Box = { size: () => 1 }` once
272
+ * `Box` gains `brandNew()`. This is a claim of a break, so only what can
273
+ * be read is reported:
274
+ * - the member's name is a plain name (a quoted or escaped one may be
275
+ * spelled differently in the literal);
276
+ * - the file declares one type of that name, and the literal's
277
+ * annotation is that name, resolved to that file (a qualified name, a
278
+ * union or an array is another type);
279
+ * - the literal is the declared value itself and every member it has is
280
+ * a key that can be read, counting the members every object has
281
+ * (trait `typedLiteralMembers`: open on a spread, a computed, escaped
282
+ * or numeric key, an assertion or call around the literal, a comment
283
+ * that tells the checker to skip it);
284
+ * - the project does not add members to every object (trait
285
+ * `universalObjectInterface`).
286
+ * Sorted (file, line) of the literal.
287
+ */
288
+ function literalsLackingMember(index, definition) {
289
+ if (!PLAIN_MEMBER_NAME.test(String(definition.name))) return [];
290
+ if (ownersOf(index, definition).length === 0) return [];
291
+ const namesakes = (index.symbols.get(definition.className) || []).filter(type =>
292
+ type.file === definition.file && !type.className && NON_CALLABLE_TYPES.has(type.type));
293
+ if (namesakes.length !== 1) return [];
294
+ const rootOf = treeReader(index);
295
+ const lacking = new Map(); // file\0line -> site
296
+ for (const entry of index.files.values()) {
297
+ const traits = langTraits(entry.language) || {};
298
+ const members = traits.typedLiteralMembers;
299
+ if (typeof members !== 'function') continue;
300
+ if (traits.universalObjectInterface && (index.symbols.get(traits.universalObjectInterface) || []).length > 0) continue;
301
+ for (const symbol of entry.symbols || []) {
302
+ if (!symbol.registryContainerType || symbol.className) continue;
303
+ if (String(symbol.registryContainerType).trim().replace(/<.*$/s, '').trim() !== definition.className) continue;
304
+ const line = symbol.objectLiteralLine || symbol.startLine;
305
+ const key = `${entry.relativePath}\0${line}`;
306
+ if (lacking.has(key)) continue;
307
+ lacking.set(key, null);
308
+ const declaredIn = index._resolveClassFile ? index._resolveClassFile(definition.className, symbol.file) : null;
309
+ if (declaredIn !== definition.file) continue;
310
+ const root = rootOf(symbol.file);
311
+ const literal = root ? members(root, line) : null;
312
+ if (!literal || literal.open || literal.names.has(definition.name)) continue;
313
+ lacking.set(key, { file: entry.relativePath, line, kind: 'literal' });
314
+ }
315
+ }
316
+ return [...lacking.values()].filter(Boolean)
317
+ .sort((a, b) => (a.file === b.file ? a.line - b.line : (a.file < b.file ? -1 : 1)));
318
+ }
319
+
320
+ /**
321
+ * Hazards of adding `definition`, a declaration the index now holds.
322
+ *
323
+ * @param {object} index
324
+ * @param {object} definition - the indexed symbol record
325
+ * @param {object} facts - { ownerExisted: the owner type predates the change,
326
+ * contract: what holds the declaration in place whatever names it
327
+ * (core/deadcode.js contractFactsOf), shield: the reason a deletion
328
+ * assessment of it names, or null, shape: what the member does to its
329
+ * type's shape (shapeChangeOf) }
330
+ * @returns {string[]} hazard codes, in a fixed order
331
+ */
332
+ function captureHazards(index, definition, facts = {}) {
333
+ const hazards = [];
334
+ if (definition.className && facts.ownerExisted) {
335
+ const type = definition.type || definition.memberType;
336
+ if (definition.requiredMember) hazards.push('new-requirement');
337
+ if (definition.isConstructor || type === 'constructor') hazards.push('constructor-of-existing-type');
338
+ // A value member (a field, a constant, an enum variant) is part of
339
+ // what the type IS, whichever list the diff reported it in.
340
+ else if (facts.nonCallable || isValueDefinition(definition) ||
341
+ (NON_CALLABLE_TYPES.has(type) && !definition.isMethod)) hazards.push('field-of-existing-type');
342
+ else if (!definition.requiredMember && facts.shape) hazards.push('shape-of-existing-type');
343
+ let closure;
344
+ try { closure = hierarchySlotClosure(index, definition); } catch (_) { closure = null; }
345
+ if (closure) {
346
+ if ((closure.ancestors || []).some(ancestor => (ancestor.members || []).length > 0)) {
347
+ hazards.push('overrides-inherited');
348
+ }
349
+ if ((closure.descendants || []).some(descendant => (descendant.members || []).length > 0)) {
350
+ hazards.push('overridden-by-descendant');
351
+ }
352
+ }
353
+ }
354
+ const contract = facts.contract || {};
355
+ if (contract.runtimeContract) hazards.push('protocol-member');
356
+ if (contract.registered) hazards.push('registered');
357
+ if (contract.externalContract) hazards.push('overrides-external');
358
+ const fromShield = HAZARD_OF_SHIELD[facts.shield];
359
+ if (fromShield) hazards.push(fromShield);
360
+ return hazards;
361
+ }
362
+
363
+ module.exports = { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, newOwnerUnreached, HAZARD_TEXT };