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.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/parallel-build.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
99
|
+
throw new UcnError(`Cannot detect language for: ${filePath}`);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
const result = parse(code, language);
|