ucn 5.4.1 → 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 (87) 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 +42 -16
  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-ext.js +6 -6
  38. package/core/output/analysis.js +18 -2
  39. package/core/output/brief.js +3 -0
  40. package/core/output/doctor.js +4 -0
  41. package/core/output/endpoints.js +15 -6
  42. package/core/output/find.js +11 -2
  43. package/core/output/lines.js +8 -2
  44. package/core/output/refactoring.js +121 -14
  45. package/core/output/reporting.js +31 -3
  46. package/core/output/shared.js +14 -0
  47. package/core/output-budget.js +316 -122
  48. package/core/parallel-build.js +124 -47
  49. package/core/parser.js +3 -2
  50. package/core/project.js +457 -60
  51. package/core/provenance-facts.js +156 -12
  52. package/core/provenance-overload.js +30 -3
  53. package/core/provenance.js +104 -30
  54. package/core/pytest-fixtures.js +434 -0
  55. package/core/python-fixture-flow.js +17 -3
  56. package/core/python-modules.js +173 -0
  57. package/core/reflection.js +877 -0
  58. package/core/registry.js +1 -1
  59. package/core/reporting.js +234 -52
  60. package/core/request-helpers.js +561 -0
  61. package/core/route-graph.js +1922 -0
  62. package/core/route-lists.js +2414 -0
  63. package/core/rust-macro-expansion.js +2113 -0
  64. package/core/rust-modules.js +185 -0
  65. package/core/search.js +121 -29
  66. package/core/shared.js +19 -1
  67. package/core/test-name-bindings.js +256 -0
  68. package/core/type-denotation.js +1161 -0
  69. package/core/type-references.js +3229 -0
  70. package/core/verify.js +3360 -672
  71. package/languages/adapter.js +31 -5
  72. package/languages/c-family.js +4061 -212
  73. package/languages/c-macro-invocations.js +380 -0
  74. package/languages/c-preprocessor.js +254 -0
  75. package/languages/csharp.js +1406 -49
  76. package/languages/go.js +1243 -70
  77. package/languages/index.js +728 -6
  78. package/languages/java.js +473 -61
  79. package/languages/javascript.js +1270 -197
  80. package/languages/lexical-scope.js +1280 -0
  81. package/languages/python.js +1557 -72
  82. package/languages/rust-macro-rules.js +1020 -0
  83. package/languages/rust-value-flow.js +416 -0
  84. package/languages/rust.js +2237 -115
  85. package/languages/utils.js +298 -4
  86. package/mcp/server.js +25 -11
  87. package/package.json +1 -1
@@ -13,26 +13,26 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { Worker, MessageChannel, receiveMessageOnPort } = require('worker_threads');
15
15
 
16
- function partitionFiles(index, files, workerCount) {
17
- const chunks = Array.from({ length: workerCount }, () => ({
18
- files: [], bytes: 0,
19
- }));
20
- const weightedFiles = files.map((file, order) => {
21
- let bytes = index.files.get(file)?.size;
16
+ // Young-generation cap of a parse worker's heap (see parallelBuild).
17
+ const WORKER_YOUNG_GENERATION_MB = 32;
18
+
19
+ /**
20
+ * Files in scheduling order: largest first (source size drives parse cost),
21
+ * discovery order breaking ties. Workers pull the next file from a shared
22
+ * counter, so a worker that drew an expensive file (a heavily recovered C++
23
+ * header, a generated source) no longer leaves its statically assigned
24
+ * backlog waiting while peers idle (fix #365). The first files in flight are
25
+ * the largest ones, as with the former longest-processing-time split, which
26
+ * keeps peak native-tree memory bounded the same way.
27
+ */
28
+ function scheduleFiles(index, files, sizes = null) {
29
+ return files.map((file, order) => {
30
+ let bytes = sizes?.get(file) ?? index.files.get(file)?.size;
22
31
  if (!Number.isFinite(bytes)) {
23
32
  try { bytes = fs.statSync(file).size; } catch (_) { bytes = 0; }
24
33
  }
25
34
  return { file, bytes, order };
26
35
  }).sort((a, b) => b.bytes - a.bytes || a.order - b.order);
27
- for (const weighted of weightedFiles) {
28
- let target = chunks[0];
29
- for (let i = 1; i < chunks.length; i++) {
30
- if (chunks[i].bytes < target.bytes) target = chunks[i];
31
- }
32
- target.files.push(weighted.file);
33
- target.bytes += weighted.bytes;
34
- }
35
- return chunks;
36
36
  }
37
37
 
38
38
  /**
@@ -43,6 +43,8 @@ function partitionFiles(index, files, workerCount) {
43
43
  * @param {object} options
44
44
  * @param {number} [options.workerCount] - Number of workers (auto-detect if omitted)
45
45
  * @param {boolean} [options.quiet] - Suppress output
46
+ * @param {number} [options.retainWorkers] - Parse workers kept for the build's
47
+ * next phase (fix #388); `options.onParsed(pool)` receives them
46
48
  * @returns {number|false} Number of changed files, or false if too few workers
47
49
  */
48
50
  function parallelBuild(index, files, options = {}) {
@@ -70,49 +72,79 @@ function parallelBuild(index, files, options = {}) {
70
72
  console.error(`Parallel build: ${workerCount} workers for ${files.length} files`);
71
73
  }
72
74
 
73
- // Parsing cost is driven much more by source size than file count. A
74
- // round-robin split can put several giant generated/template headers in
75
- // one worker while peers finish tiny files, making the whole build wait
76
- // on one straggler and retaining multiple large native ASTs together.
77
- // Longest-processing-time scheduling by byte size is deterministic and
78
- // gives a substantially tighter upper bound on both wall time and peak
79
- // memory. Canonical index ordering after the merge remains unchanged.
80
- const chunks = partitionFiles(index, files, workerCount);
81
-
82
- // Synchronization: one Int32 per worker in SharedArrayBuffer
83
- const sab = new SharedArrayBuffer(4 * workerCount);
75
+ // Parsing cost is driven much more by source size than file count, and
76
+ // per-file cost varies further by language and recovery work; see
77
+ // scheduleFiles. Results merge in discovery order whatever worker parsed
78
+ // them, and canonical index ordering after the merge is unchanged.
79
+ const scheduled = scheduleFiles(index, files, options.sizes);
80
+ const scheduledFiles = scheduled.map(item => item.file);
81
+
82
+ // Synchronization: one Int32 per worker in SharedArrayBuffer, plus the
83
+ // shared next-file counter at index workerCount.
84
+ const sab = new SharedArrayBuffer(4 * (workerCount + 1));
84
85
  const signal = new Int32Array(sab);
86
+ // Parse workers kept for the next phase wait on a control slot once
87
+ // their queue is empty (fix #388): 2 = a job was posted to their port,
88
+ // anything else = exit.
89
+ const retainCount = Math.min(Math.max(options.retainWorkers | 0, 0), workerCount);
90
+ const control = retainCount > 0 ? new SharedArrayBuffer(4 * workerCount) : null;
85
91
 
86
92
  const ports = [];
87
93
  const workers = [];
94
+ // A dictionary given as a function (fix #396: the project's C/C++ macro
95
+ // definitions) is computed while the workers parse and posted to them;
96
+ // a worker waits for it only when a file's recovery consults it.
97
+ const lazyDictionary = typeof options.macroDictionary === 'function';
98
+ const dictionarySignal = lazyDictionary ? new SharedArrayBuffer(4) : null;
99
+
100
+ const workerHashes = Object.create(null);
101
+ for (const fp of scheduledFiles) {
102
+ const entry = index.files.get(fp);
103
+ if (entry) {
104
+ workerHashes[fp] = { mtime: entry.mtime, size: entry.size, hash: entry.hash };
105
+ }
106
+ }
88
107
 
89
108
  for (let i = 0; i < workerCount; i++) {
90
109
  const { port1, port2 } = new MessageChannel();
91
110
  ports.push(port1);
92
111
 
93
- // Build per-worker hash subset (each worker only needs hashes for its chunk)
94
- const workerHashes = Object.create(null);
95
- for (const fp of chunks[i].files) {
96
- const entry = index.files.get(fp);
97
- if (entry) {
98
- workerHashes[fp] = { mtime: entry.mtime, size: entry.size, hash: entry.hash };
99
- }
100
- }
101
-
102
112
  const worker = new Worker(path.join(__dirname, 'build-worker.js'), {
103
113
  workerData: {
104
- files: chunks[i].files,
114
+ files: scheduledFiles,
105
115
  rootDir: index.root,
106
116
  existingHashes: workerHashes,
107
117
  signal: sab,
108
118
  workerIndex: i,
119
+ queueIndex: workerCount,
109
120
  port: port2,
121
+ control,
122
+ macroDictionary: lazyDictionary ? null : (options.macroDictionary || null),
123
+ dictionarySignal,
110
124
  },
111
125
  transferList: [port2],
126
+ // Parse garbage (the native trees behind it included, which the
127
+ // JS heap does not see) is collected by young-generation
128
+ // scavenges; a bounded young generation collects it sooner
129
+ // (fix #388: build peak RSS -15 to -25% on fmt, tokio, django,
130
+ // ripgrep, clap, at equal CPU and wall within noise).
131
+ resourceLimits: { maxYoungGenerationSizeMb: WORKER_YOUNG_GENERATION_MB },
112
132
  });
113
133
  workers.push(worker);
114
134
  }
115
135
 
136
+ if (lazyDictionary) {
137
+ let dictionary = null;
138
+ try {
139
+ dictionary = options.macroDictionary();
140
+ } finally {
141
+ for (const port of ports) port.postMessage({ macroDictionary: dictionary || null });
142
+ const flag = new Int32Array(dictionarySignal);
143
+ Atomics.store(flag, 0, 1);
144
+ Atomics.notify(flag, 0);
145
+ }
146
+ }
147
+
116
148
  // Block main thread until all workers finish (with timeout)
117
149
  const TIMEOUT_MS = 300_000; // 5 minutes
118
150
  const deadline = Date.now() + TIMEOUT_MS;
@@ -128,15 +160,65 @@ function parallelBuild(index, files, options = {}) {
128
160
  }
129
161
  }
130
162
 
131
- // Collect and merge results from each worker
163
+ // Collect results from each worker, then merge them in discovery order.
132
164
  let changed = 0;
133
-
165
+ const discoveryOrder = new Map(files.map((file, order) => [file, order]));
166
+ const collected = [];
134
167
  for (let i = 0; i < workerCount; i++) {
135
- const msg = receiveMessageOnPort(ports[i]);
168
+ for (let msg = receiveMessageOnPort(ports[i]); msg; msg = receiveMessageOnPort(ports[i])) {
169
+ for (const result of msg.message) collected.push(result);
170
+ }
171
+ }
172
+ // Every result is in hand: the parse workers can go, except those kept
173
+ // for the build's next phase (fix #388: Rust macro expansion runs in
174
+ // parse workers whose parser code is already optimized, instead of in
175
+ // fresh threads that start cold). Only a worker that finished its queue
176
+ // normally (signal 1) is kept.
177
+ const controlArray = control ? new Int32Array(control) : null;
178
+ const kept = [];
179
+ for (let i = 0; i < workerCount; i++) {
180
+ if (kept.length < retainCount && Atomics.load(signal, i) === 1) {
181
+ kept.push(i);
182
+ // A kept worker never holds the process open.
183
+ workers[i].unref();
184
+ continue;
185
+ }
186
+ if (controlArray) {
187
+ Atomics.store(controlArray, i, 1);
188
+ Atomics.notify(controlArray, i);
189
+ }
136
190
  ports[i].close();
137
- if (!msg) continue;
191
+ workers[i].terminate();
192
+ }
193
+ if (kept.length > 0) {
194
+ const live = new Set(kept);
195
+ const retire = i => {
196
+ if (!live.delete(i)) return;
197
+ Atomics.store(controlArray, i, 1);
198
+ Atomics.notify(controlArray, i);
199
+ ports[i].close();
200
+ workers[i].terminate();
201
+ };
202
+ options.onParsed?.({
203
+ available: [...kept],
204
+ ports,
205
+ post(i, job) {
206
+ ports[i].postMessage(job);
207
+ Atomics.store(controlArray, i, 2);
208
+ Atomics.notify(controlArray, i);
209
+ },
210
+ retire,
211
+ dispose() {
212
+ for (const i of [...live]) retire(i);
213
+ },
214
+ });
215
+ } else {
216
+ options.onParsed?.(null);
217
+ }
218
+ collected.sort((a, b) => discoveryOrder.get(a.filePath) - discoveryOrder.get(b.filePath));
138
219
 
139
- for (const result of msg.message) {
220
+ {
221
+ for (const result of collected) {
140
222
  if (result.error) {
141
223
  index.failedFiles.add(result.filePath);
142
224
  if (!options.quiet) {
@@ -190,12 +272,7 @@ function parallelBuild(index, files, options = {}) {
190
272
  }
191
273
  }
192
274
 
193
- // Terminate workers
194
- for (const w of workers) {
195
- w.terminate();
196
- }
197
-
198
275
  return changed;
199
276
  }
200
277
 
201
- module.exports = { parallelBuild, partitionFiles };
278
+ module.exports = { parallelBuild, scheduleFiles };
package/core/parser.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const fs = require('fs');
9
+ const { UcnError } = require('./errors');
9
10
  const { detectLanguage, getParser, getLanguageAdapter, isSupported } = require('../languages');
10
11
 
11
12
  /**
@@ -76,7 +77,7 @@ function parse(code, language) {
76
77
  }
77
78
 
78
79
  if (!language || !isSupported(language)) {
79
- throw new Error(`Unsupported language: ${language}`);
80
+ throw new UcnError(`Unsupported language: ${language}`);
80
81
  }
81
82
 
82
83
  const parser = getParser(language);
@@ -95,7 +96,7 @@ function parseFile(filePath) {
95
96
  const language = detectLanguage(filePath);
96
97
 
97
98
  if (!language) {
98
- throw new Error(`Cannot detect language for: ${filePath}`);
99
+ throw new UcnError(`Cannot detect language for: ${filePath}`);
99
100
  }
100
101
 
101
102
  const result = parse(code, language);