ucn 5.4.2 → 5.5.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 (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 +18 -12
  6. package/cli/index.js +40 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  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 +522 -31
  16. package/core/callers.js +12053 -1047
  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 +457 -60
  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 +2113 -0
  62. package/core/rust-modules.js +185 -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 +1161 -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 +2237 -115
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -0,0 +1,256 @@
1
+ /**
2
+ * core/test-name-bindings.js - Test functions bound to a declaration by name
3
+ * (fix #383).
4
+ *
5
+ * Some toolchains bind a test function to a declared identifier through the
6
+ * function's NAME, not through a reference: Go examples
7
+ * `Example<Ident>[_<Member>][_<suffix>]` (the language trait
8
+ * `nameBoundTestFunctions`). go vet's `tests` analyzer, which `go test` runs,
9
+ * rejects an example whose name no longer names a declaration, so a rename of
10
+ * the identifier or member must rename the example with it.
11
+ *
12
+ * Binding follows the checker exactly: the name is split into its parts by
13
+ * the trait's parser, the identifier part is looked up the way vet looks it
14
+ * up (the example's own package scope, then the packages that package
15
+ * imports), and the member part must resolve on that type the way a selector
16
+ * does (own and promoted methods, interface requirements, fields). A name
17
+ * that could resolve to more than one imported package is routed to review,
18
+ * never renamed silently.
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const path = require('path');
24
+ const { langTraits } = require('../languages');
25
+
26
+ /** Name-bound test function candidates of one convention, per operation. */
27
+ function candidatesFor(index, trait) {
28
+ const memo = index._opMemo?.('nameBoundTestCandidates');
29
+ if (memo?.has(trait.kind)) return memo.get(trait.kind);
30
+ const out = [];
31
+ for (const [name, defs] of index.symbols) {
32
+ if (!name.startsWith(trait.prefix)) continue;
33
+ for (const def of defs) {
34
+ if (def.className || def.type !== 'function' || def.lexicalScopeStartLine) continue;
35
+ const rel = def.relativePath || def.file;
36
+ if (!trait.testFile.test(rel)) continue;
37
+ if (langTraits(index.files.get(def.file)?.language)?.nameBoundTestFunctions !== trait) continue;
38
+ const parsed = trait.parse(name);
39
+ if (parsed) out.push({ def, parsed });
40
+ }
41
+ }
42
+ memo?.set(trait.kind, out);
43
+ return out;
44
+ }
45
+
46
+ // ── Go package scopes (vet lookup semantics) ────────────────────────────
47
+
48
+ /** The package clause name of a Go file (recorded by the parser). */
49
+ function goPackageClause(index, file) {
50
+ return index.files.get(file)?.packageName || null;
51
+ }
52
+
53
+ /** Go files per directory, per operation. */
54
+ function goFilesByDir(index) {
55
+ const memo = index._opMemo?.('goFilesByDir', () => ({ map: null }));
56
+ if (memo?.map) return memo.map;
57
+ const map = new Map();
58
+ for (const [file, entry] of index.files) {
59
+ if (entry.language !== 'go') continue;
60
+ const dir = path.dirname(file);
61
+ if (!map.has(dir)) map.set(dir, []);
62
+ map.get(dir).push(file);
63
+ }
64
+ if (memo) memo.map = map;
65
+ return map;
66
+ }
67
+
68
+ /** Files of the Go package `file` belongs to: same directory, same clause. */
69
+ function goPackageFiles(index, file) {
70
+ const clause = goPackageClause(index, file);
71
+ const dir = path.dirname(file);
72
+ return (goFilesByDir(index).get(dir) || []).filter(other =>
73
+ goPackageClause(index, other) === clause);
74
+ }
75
+
76
+ function isGoPackageLevel(def) {
77
+ return !def.className && def.type !== 'method' && def.type !== 'field' &&
78
+ !def.lexicalScopeStartLine;
79
+ }
80
+
81
+ /** Directory of the project package a Go import spec resolves to, or null. */
82
+ function goImportDir(index, file, spec) {
83
+ const memo = index._opMemo?.('goImportDir');
84
+ const key = `${file}\0${spec}`;
85
+ if (memo?.has(key)) return memo.get(key);
86
+ let dir = null;
87
+ const rel = index.files.get(file)?.moduleResolved?.[spec];
88
+ if (rel) {
89
+ dir = path.dirname(path.isAbsolute(rel) ? rel : path.join(index.root, rel));
90
+ } else {
91
+ try {
92
+ const { resolveImport } = require('./imports');
93
+ const resolved = resolveImport(spec, file, { language: 'go', root: index.root });
94
+ if (resolved) dir = path.dirname(resolved);
95
+ } catch {
96
+ dir = null;
97
+ }
98
+ }
99
+ memo?.set(key, dir);
100
+ return dir;
101
+ }
102
+
103
+ /**
104
+ * What `ident` names in the package of `file`, as go vet looks it up: the
105
+ * package's own declarations, else every imported project package that
106
+ * declares it. Returns { own: defs[] } or { imported: [{ dir, defs }] }.
107
+ */
108
+ function goLookupIdent(index, file, ident, testFile) {
109
+ const pkgFiles = goPackageFiles(index, file);
110
+ const pkgSet = new Set(pkgFiles);
111
+ const defs = (index.symbols.get(ident) || []).filter(isGoPackageLevel);
112
+ const own = defs.filter(def => pkgSet.has(def.file));
113
+ if (own.length > 0) return { own };
114
+ const dirs = new Set();
115
+ for (const pkgFile of pkgFiles) {
116
+ for (const binding of index.files.get(pkgFile)?.importBindings || []) {
117
+ if (!binding.module) continue;
118
+ const dir = goImportDir(index, pkgFile, binding.module);
119
+ if (dir) dirs.add(dir);
120
+ }
121
+ }
122
+ const imported = [];
123
+ for (const dir of [...dirs].sort()) {
124
+ // An imported package is its non-test files.
125
+ const found = defs.filter(def => path.dirname(def.file) === dir &&
126
+ !testFile.test(def.relativePath || def.file));
127
+ if (found.length > 0) imported.push({ dir, defs: found });
128
+ }
129
+ return { imported };
130
+ }
131
+
132
+ /**
133
+ * Whether the identifier part of example `example` binds to the package
134
+ * declaration `decl` (a type for member bindings, the renamed declaration
135
+ * otherwise): 'yes', 'no', or 'ambiguous'.
136
+ */
137
+ function goIdentBinding(index, example, ident, decl, testFile) {
138
+ const lookup = goLookupIdent(index, example.file, ident, testFile);
139
+ if (lookup.own) {
140
+ return lookup.own.some(def => def === decl ||
141
+ (def.file === decl.file && def.startLine === decl.startLine)) ? 'yes' : 'no';
142
+ }
143
+ const declDir = path.dirname(decl.file);
144
+ const hit = lookup.imported.some(item => item.dir === declDir &&
145
+ item.defs.some(def => def === decl || (def.file === decl.file && def.startLine === decl.startLine)));
146
+ if (!hit) return 'no';
147
+ return lookup.imported.length > 1 ? 'ambiguous' : 'yes';
148
+ }
149
+
150
+ /** Whether a call record outside the function's own body names it. */
151
+ function referencedElsewhere(index, fn) {
152
+ const files = index.getCalleeFiles?.(fn.name);
153
+ if (!files || files.size === 0) return false;
154
+ const { getCachedCalls } = require('./callers');
155
+ for (const file of files) {
156
+ for (const call of getCachedCalls(index, file) || []) {
157
+ if (call.name !== fn.name) continue;
158
+ const enclosing = call.enclosingFunction;
159
+ // Records inside the function itself (Go's synthetic closure
160
+ // entry markers carry the enclosing name) are not references.
161
+ if (file === fn.file && enclosing?.startLine === fn.startLine) continue;
162
+ return true;
163
+ }
164
+ }
165
+ return false;
166
+ }
167
+
168
+ function sameDef(a, b) {
169
+ return a === b || (a.file === b.file && a.startLine === b.startLine && a.name === b.name);
170
+ }
171
+
172
+ /**
173
+ * Examples (and other name-bound test functions) a rename must carry along.
174
+ * @param {object} index
175
+ * @param {object[]} renamedDefs - declarations the plan renames
176
+ * @param {string} oldName
177
+ * @param {string} newName
178
+ * @returns {Array<{ def, newName, status: 'edit'|'review', reason?, binding }>}
179
+ */
180
+ function nameBoundTestFunctions(index, renamedDefs, oldName, newName) {
181
+ const out = [];
182
+ const byTrait = new Map();
183
+ for (const def of renamedDefs) {
184
+ const trait = langTraits(index.files.get(def.file)?.language)?.nameBoundTestFunctions;
185
+ if (!trait) continue;
186
+ if (!byTrait.has(trait)) byTrait.set(trait, []);
187
+ byTrait.get(trait).push(def);
188
+ }
189
+ for (const [trait, defs] of byTrait) {
190
+ if (trait.kind !== 'go-example') continue;
191
+ const candidates = candidatesFor(index, trait).filter(({ parsed }) =>
192
+ parsed.ident === oldName || parsed.member === oldName);
193
+ if (candidates.length === 0) continue;
194
+ // Bindings the renamed declarations own: the identifier itself for a
195
+ // package-level declaration, `Type_<member>` for every type whose
196
+ // selector reaches a renamed member.
197
+ const identDecls = defs.filter(isGoPackageLevel);
198
+ const memberTypes = [];
199
+ if (defs.some(def => def.className)) {
200
+ const { goMemberProviders } = require('./contract-membership');
201
+ for (const provider of goMemberProviders(index, oldName)) {
202
+ if (!provider.defs.some(member => defs.some(def => sameDef(def, member)))) continue;
203
+ const typeDecl = (index.symbols.get(provider.typeName) || []).find(def =>
204
+ isGoPackageLevel(def) && path.dirname(def.file) === provider.dir &&
205
+ !trait.testFile.test(def.relativePath || def.file)) ||
206
+ (index.symbols.get(provider.typeName) || []).find(def =>
207
+ isGoPackageLevel(def) && path.dirname(def.file) === provider.dir);
208
+ if (typeDecl) memberTypes.push({ typeName: provider.typeName, decl: typeDecl });
209
+ }
210
+ }
211
+ for (const { def: example, parsed } of candidates) {
212
+ let identVerdict = 'no';
213
+ let memberVerdict = 'no';
214
+ if (parsed.ident === oldName) {
215
+ for (const decl of identDecls) {
216
+ const verdict = goIdentBinding(index, example, oldName, decl, trait.testFile);
217
+ if (verdict === 'yes') { identVerdict = 'yes'; break; }
218
+ if (verdict === 'ambiguous') identVerdict = 'ambiguous';
219
+ }
220
+ }
221
+ if (parsed.member === oldName) {
222
+ for (const { typeName, decl } of memberTypes) {
223
+ if (parsed.ident !== typeName) continue;
224
+ const verdict = goIdentBinding(index, example, typeName, decl, trait.testFile);
225
+ if (verdict === 'yes') { memberVerdict = 'yes'; break; }
226
+ if (verdict === 'ambiguous') memberVerdict = 'ambiguous';
227
+ }
228
+ }
229
+ if (identVerdict === 'no' && memberVerdict === 'no') continue;
230
+ const renamed = parsed.rename({
231
+ ...(identVerdict === 'yes' && { ident: newName }),
232
+ ...(memberVerdict === 'yes' && { member: newName }),
233
+ });
234
+ const binding = memberVerdict !== 'no' ? 'member' : 'identifier';
235
+ if (identVerdict === 'ambiguous' || memberVerdict === 'ambiguous') {
236
+ out.push({ def: example, newName: renamed, status: 'review', reason: 'example-binding-ambiguous', binding });
237
+ } else if (referencedElsewhere(index, example)) {
238
+ // The example is also referenced as a function: its other
239
+ // references are not part of this rename.
240
+ out.push({ def: example, newName: renamed, status: 'review', reason: 'example-referenced', binding });
241
+ } else {
242
+ out.push({ def: example, newName: renamed, status: 'edit', binding });
243
+ }
244
+ }
245
+ }
246
+ return out.sort((a, b) => {
247
+ const fa = a.def.relativePath || a.def.file;
248
+ const fb = b.def.relativePath || b.def.file;
249
+ return fa < fb ? -1 : fa > fb ? 1 : a.def.startLine - b.def.startLine;
250
+ });
251
+ }
252
+
253
+ module.exports = {
254
+ nameBoundTestFunctions,
255
+ goPackageClause,
256
+ };