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,331 @@
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 a predicate over (relative path, line), or null for "anywhere".
67
+ * An occurrence elsewhere is another name that happens to be spelled the
68
+ * same: it could not have meant this declaration before, and cannot now.
69
+ */
70
+ function reachOf(index, definition) {
71
+ if (!definition || definition.className) return null;
72
+ const entry = index.files.get(definition.file);
73
+ if (!entry) return null;
74
+ const own = entry.relativePath;
75
+ if (definition.lexicalScopeStartLine != null) {
76
+ return (file, line) => file === own &&
77
+ line >= definition.lexicalScopeStartLine && line <= definition.lexicalScopeEndLine;
78
+ }
79
+ const traits = langTraits(entry.language) || {};
80
+ if (traits.packageScope !== 'file' || traits.textualIncludes) return null;
81
+ // A script without module syntax shares one global scope with the
82
+ // others (trait `scriptGlobalScope`).
83
+ if (traits.scriptGlobalScope && (entry.imports || []).length === 0 && (entry.exports || []).length === 0) return null;
84
+ const files = new Set([own]);
85
+ const exported = new Set(entry.exports || []);
86
+ if (traits.moduleNamesImportable) {
87
+ for (const symbol of entry.symbols || []) if (!symbol.className) exported.add(symbol.name);
88
+ }
89
+ // A name the module does not export stays in the module.
90
+ if (!exported.has(definition.name)) return file => file === own;
91
+ for (const other of index.files.values()) {
92
+ const resolved = other.moduleResolved || {};
93
+ const takesAll = (other.importDetails || []).some(detail =>
94
+ resolved[detail.module] === own && (detail.names || []).includes('*'));
95
+ const reExportsAll = (other.exportDetails || []).some(detail =>
96
+ detail && (detail.name === '*' || detail.type === 're-export-all') && resolved[detail.source] === own);
97
+ // Everything the module exports is passed on: who receives the
98
+ // name is no longer written here.
99
+ if (reExportsAll) return null;
100
+ // A binding of one of the module's exports takes that name only.
101
+ // Any other binding of the module holds the module itself.
102
+ const binds = (other.importBindings || []).some(binding => resolved[binding.module] === own &&
103
+ (binding.name === definition.name || !exported.has(binding.name) ||
104
+ binding.kind === 'namespace' || binding.kind === 'default'));
105
+ if (takesAll || binds) files.add(other.relativePath);
106
+ }
107
+ return file => files.has(file);
108
+ }
109
+
110
+ /**
111
+ * Occurrences, in code, of a new declaration's name outside the lines the
112
+ * diff added, among the places where the name could mean the declaration.
113
+ *
114
+ * @param {object} index
115
+ * @param {string} name
116
+ * @param {Map<string, Set<number>>} addedLinesByFile - relative path -> lines
117
+ * @param {object} [definition] - the new declaration (its reach narrows
118
+ * the search; without it every occurrence counts)
119
+ * @returns {{outside: {file, line}[], total: number}}
120
+ */
121
+ function occurrencesOutsideChange(index, name, addedLinesByFile, definition = null) {
122
+ const rows = index.usages(name, { includeTests: true, codeOnly: true }) || [];
123
+ const reaches = reachOf(index, definition);
124
+ const outside = [];
125
+ for (const row of rows) {
126
+ const file = row.relativePath || row.file;
127
+ if (addedLinesByFile.get(file)?.has(row.line)) continue;
128
+ if (reaches && !reaches(file, row.line)) continue;
129
+ outside.push({ file, line: row.line });
130
+ }
131
+ outside.sort((a, b) => (a.file === b.file ? a.line - b.line : (a.file < b.file ? -1 : 1)));
132
+ return { outside, total: rows.length };
133
+ }
134
+
135
+ /** The type definitions that own a member: same file, containing it. */
136
+ function ownersOf(index, definition) {
137
+ return (index.symbols.get(definition.className) || []).filter(type =>
138
+ type.file === definition.file && !type.className &&
139
+ type.startLine <= definition.startLine && (type.endLine || type.startLine) >= definition.startLine);
140
+ }
141
+
142
+ /** One syntax tree per file for the questions below. */
143
+ function treeReader(index) {
144
+ const { parseRevision } = require('./change-kinds');
145
+ const roots = new Map();
146
+ return file => {
147
+ if (!roots.has(file)) {
148
+ let root;
149
+ try { root = parseRevision(index._readFile(file), index.files.get(file)?.language); } catch (_) { root = null; }
150
+ roots.set(file, root);
151
+ }
152
+ return roots.get(file);
153
+ };
154
+ }
155
+
156
+ /**
157
+ * What a new member does to the SHAPE of the type that owns it: the member
158
+ * set existing code is held to, whether or not it names the member.
159
+ *
160
+ * Where classes are compared by shape (traits `classComparedByShape`,
161
+ * `classMemberRole`), a class's members are a contract in two ways. Values
162
+ * used as the class must have them: an object literal or any class, and
163
+ * where the class only INHERITS a private or protected member, any class
164
+ * that inherits the same one. And types computed from its members
165
+ * (`keyof`, mapped types, `Pick`) are held to them even when the class
166
+ * declares a private member of its own and so has no other values.
167
+ *
168
+ * 'required' every value used as the type must now have the member: an
169
+ * instance member of a class that declares no private or
170
+ * protected member of its own. A private one included: no
171
+ * other value can ever have it;
172
+ * 'shape' the type is something else to existing code, with no value
173
+ * that can be shown to lack the member: a public member of a
174
+ * class held by its own declaration (seen by `keyof` only),
175
+ * a static member, hidden or not (the constructor is
176
+ * compared by shape whatever its instances are held to), an
177
+ * optional member, a member whose role cannot be read; and a
178
+ * virtual member where that changes what the type is (trait
179
+ * `virtualMemberChangesShape`);
180
+ * null nothing: a private or protected INSTANCE member of a class
181
+ * that already declares one, which no other code can hold,
182
+ * name or enumerate.
183
+ *
184
+ * "Declares one of its own" is asked of the class AS IT WAS: members on
185
+ * lines the change added are left out (the first private member of a class
186
+ * is what makes every structural value of it stop being one).
187
+ *
188
+ * @param {Map<string, Set<number>>} [addedLinesByFile] by relative path
189
+ */
190
+ function shapeChangeOf(index, definition, addedLinesByFile = null) {
191
+ const entry = index.files.get(definition.file);
192
+ const traits = langTraits(entry && entry.language) || {};
193
+ if (traits.virtualMemberChangesShape && (definition.modifiers || []).includes('virtual')) return 'shape';
194
+ if (typeof traits.classComparedByShape !== 'function' || typeof traits.classMemberRole !== 'function') return null;
195
+ const rootOf = treeReader(index);
196
+ let change = null;
197
+ for (const owner of ownersOf(index, definition)) {
198
+ if (owner.type !== 'class') continue; // an interface member is a requirement, asked about elsewhere
199
+ const root = rootOf(owner.file);
200
+ const node = root && classNodeAt(root, owner);
201
+ const member = node ? traits.classMemberRole(node, definition.name, definition.startLine) : null;
202
+ if (!member) { change = 'shape'; continue; }
203
+ const added = (addedLinesByFile && addedLinesByFile.get(owner.relativePath)) || null;
204
+ const ownDeclaration = !traits.classComparedByShape(node, added);
205
+ // The constructor's type is its own contract: a static member is
206
+ // part of it whatever the instances are held to, private included
207
+ // (anything with a matching construct signature was a `typeof T`).
208
+ if (member.role === 'static') { change = 'shape'; continue; }
209
+ if (member.hidden && ownDeclaration) continue;
210
+ if (member.hidden || (member.role === 'instance' && !ownDeclaration)) return 'required';
211
+ change = 'shape';
212
+ }
213
+ return change;
214
+ }
215
+
216
+ /** The class declaration node a type definition names. */
217
+ function classNodeAt(root, type) {
218
+ let found = null;
219
+ const visit = node => {
220
+ const start = node.startPosition.row + 1;
221
+ const end = node.endPosition.row + 1;
222
+ if (end < type.startLine || start > (type.endLine || type.startLine)) return;
223
+ if (node.childForFieldName('body') && node.childForFieldName('name')?.text === type.name &&
224
+ /class/.test(node.type)) {
225
+ if (!found || node.startIndex < found.startIndex) found = node;
226
+ return;
227
+ }
228
+ for (const child of node.namedChildren) visit(child);
229
+ };
230
+ visit(root);
231
+ return found;
232
+ }
233
+
234
+ // A member name that is exactly what a literal's key would spell.
235
+ const PLAIN_MEMBER_NAME = /^#?[A-Za-z_$][A-Za-z0-9_$]*$/;
236
+
237
+ /**
238
+ * Object literals declared as the type that owns `definition` and that do
239
+ * not have a member of its name: `const box: Box = { size: () => 1 }` once
240
+ * `Box` gains `brandNew()`. This is a claim of a break, so only what can
241
+ * be read is reported:
242
+ * - the member's name is a plain name (a quoted or escaped one may be
243
+ * spelled differently in the literal);
244
+ * - the file declares one type of that name, and the literal's
245
+ * annotation is that name, resolved to that file (a qualified name, a
246
+ * union or an array is another type);
247
+ * - the literal is the declared value itself and every member it has is
248
+ * a key that can be read, counting the members every object has
249
+ * (trait `typedLiteralMembers`: open on a spread, a computed, escaped
250
+ * or numeric key, an assertion or call around the literal, a comment
251
+ * that tells the checker to skip it);
252
+ * - the project does not add members to every object (trait
253
+ * `universalObjectInterface`).
254
+ * Sorted (file, line) of the literal.
255
+ */
256
+ function literalsLackingMember(index, definition) {
257
+ if (!PLAIN_MEMBER_NAME.test(String(definition.name))) return [];
258
+ if (ownersOf(index, definition).length === 0) return [];
259
+ const namesakes = (index.symbols.get(definition.className) || []).filter(type =>
260
+ type.file === definition.file && !type.className && NON_CALLABLE_TYPES.has(type.type));
261
+ if (namesakes.length !== 1) return [];
262
+ const rootOf = treeReader(index);
263
+ const lacking = new Map(); // file\0line -> site
264
+ for (const entry of index.files.values()) {
265
+ const traits = langTraits(entry.language) || {};
266
+ const members = traits.typedLiteralMembers;
267
+ if (typeof members !== 'function') continue;
268
+ if (traits.universalObjectInterface && (index.symbols.get(traits.universalObjectInterface) || []).length > 0) continue;
269
+ for (const symbol of entry.symbols || []) {
270
+ if (!symbol.registryContainerType || symbol.className) continue;
271
+ if (String(symbol.registryContainerType).trim().replace(/<.*$/s, '').trim() !== definition.className) continue;
272
+ const line = symbol.objectLiteralLine || symbol.startLine;
273
+ const key = `${entry.relativePath}\0${line}`;
274
+ if (lacking.has(key)) continue;
275
+ lacking.set(key, null);
276
+ const declaredIn = index._resolveClassFile ? index._resolveClassFile(definition.className, symbol.file) : null;
277
+ if (declaredIn !== definition.file) continue;
278
+ const root = rootOf(symbol.file);
279
+ const literal = root ? members(root, line) : null;
280
+ if (!literal || literal.open || literal.names.has(definition.name)) continue;
281
+ lacking.set(key, { file: entry.relativePath, line, kind: 'literal' });
282
+ }
283
+ }
284
+ return [...lacking.values()].filter(Boolean)
285
+ .sort((a, b) => (a.file === b.file ? a.line - b.line : (a.file < b.file ? -1 : 1)));
286
+ }
287
+
288
+ /**
289
+ * Hazards of adding `definition`, a declaration the index now holds.
290
+ *
291
+ * @param {object} index
292
+ * @param {object} definition - the indexed symbol record
293
+ * @param {object} facts - { ownerExisted: the owner type predates the change,
294
+ * contract: what holds the declaration in place whatever names it
295
+ * (core/deadcode.js contractFactsOf), shield: the reason a deletion
296
+ * assessment of it names, or null, shape: what the member does to its
297
+ * type's shape (shapeChangeOf) }
298
+ * @returns {string[]} hazard codes, in a fixed order
299
+ */
300
+ function captureHazards(index, definition, facts = {}) {
301
+ const hazards = [];
302
+ if (definition.className && facts.ownerExisted) {
303
+ const type = definition.type || definition.memberType;
304
+ if (definition.requiredMember) hazards.push('new-requirement');
305
+ if (definition.isConstructor || type === 'constructor') hazards.push('constructor-of-existing-type');
306
+ // A value member (a field, a constant, an enum variant) is part of
307
+ // what the type IS, whichever list the diff reported it in.
308
+ else if (facts.nonCallable || isValueDefinition(definition) ||
309
+ (NON_CALLABLE_TYPES.has(type) && !definition.isMethod)) hazards.push('field-of-existing-type');
310
+ else if (!definition.requiredMember && facts.shape) hazards.push('shape-of-existing-type');
311
+ let closure;
312
+ try { closure = hierarchySlotClosure(index, definition); } catch (_) { closure = null; }
313
+ if (closure) {
314
+ if ((closure.ancestors || []).some(ancestor => (ancestor.members || []).length > 0)) {
315
+ hazards.push('overrides-inherited');
316
+ }
317
+ if ((closure.descendants || []).some(descendant => (descendant.members || []).length > 0)) {
318
+ hazards.push('overridden-by-descendant');
319
+ }
320
+ }
321
+ }
322
+ const contract = facts.contract || {};
323
+ if (contract.runtimeContract) hazards.push('protocol-member');
324
+ if (contract.registered) hazards.push('registered');
325
+ if (contract.externalContract) hazards.push('overrides-external');
326
+ const fromShield = HAZARD_OF_SHIELD[facts.shield];
327
+ if (fromShield) hazards.push(fromShield);
328
+ return hazards;
329
+ }
330
+
331
+ module.exports = { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, HAZARD_TEXT };