ucn 4.2.3 → 5.0.2
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
BROAD_COMMANDS: BROAD_CANONICAL,
|
|
5
|
+
FLAG_APPLICABILITY,
|
|
6
|
+
resolveCommand,
|
|
7
|
+
toMcpName,
|
|
8
|
+
} = require('./registry');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_OUTPUT_CHARS = 10000;
|
|
11
|
+
const BROAD_OUTPUT_CHARS = 3000;
|
|
12
|
+
const MAX_OUTPUT_CHARS = 100000;
|
|
13
|
+
const BROAD_COMMANDS = new Set([
|
|
14
|
+
...BROAD_CANONICAL,
|
|
15
|
+
...[...BROAD_CANONICAL].map(toMcpName),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):|\d+ test-file usage\(s\) hidden\b|(?:Note:\s*)?Found \d+ (?:definitions|fuzzy matches)\b)/;
|
|
19
|
+
const MAX_PRESERVED_CONTRACT_LINES = 24;
|
|
20
|
+
const MAX_PRESERVED_CONTRACT_CHARS = 8000;
|
|
21
|
+
|
|
22
|
+
function preservedContractMetadata(fullText, visibleText, options = {}) {
|
|
23
|
+
const visible = new Set(visibleText.split('\n').map(line => line.trim()));
|
|
24
|
+
const candidates = [];
|
|
25
|
+
const selected = [];
|
|
26
|
+
let selectedChars = 0;
|
|
27
|
+
let omitted = 0;
|
|
28
|
+
const maxLines = options.maxLines ?? MAX_PRESERVED_CONTRACT_LINES;
|
|
29
|
+
const maxChars = options.maxChars ?? MAX_PRESERVED_CONTRACT_CHARS;
|
|
30
|
+
|
|
31
|
+
for (const [sourceIndex, rawLine] of fullText.split('\n').entries()) {
|
|
32
|
+
// Execution notes may concatenate several independent disclosures on
|
|
33
|
+
// one physical line. Preserve the actionable test-scope contract as
|
|
34
|
+
// its own sentence so a later parse-failure note cannot make the
|
|
35
|
+
// whole metadata item too large for a small transport budget.
|
|
36
|
+
const firstSentenceEnd = /^\s*\d+ test-file usage\(s\) hidden\b/.test(rawLine)
|
|
37
|
+
? rawLine.indexOf('. ')
|
|
38
|
+
: -1;
|
|
39
|
+
const contractLine = firstSentenceEnd >= 0
|
|
40
|
+
? rawLine.slice(0, firstSentenceEnd + 1)
|
|
41
|
+
: rawLine;
|
|
42
|
+
if (!CONTRACT_LINE_RE.test(contractLine)) continue;
|
|
43
|
+
const line = contractLine.trim();
|
|
44
|
+
if (!line || visible.has(line)) continue;
|
|
45
|
+
const priority = /^(?:ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):/.test(line)
|
|
46
|
+
? 0
|
|
47
|
+
: /^\d+ test-file usage\(s\) hidden\b/.test(line) ? 1 : 2;
|
|
48
|
+
candidates.push({ line, priority, sourceIndex });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const { line } of candidates.sort((a, b) =>
|
|
52
|
+
a.priority - b.priority || a.sourceIndex - b.sourceIndex)) {
|
|
53
|
+
if (selected.length >= maxLines ||
|
|
54
|
+
selectedChars + line.length + 1 > maxChars) {
|
|
55
|
+
omitted++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
selected.push(line);
|
|
59
|
+
selectedChars += line.length + 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { lines: selected, omitted, complete: omitted === 0 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function narrowingHint(command, surface, params = {}) {
|
|
66
|
+
const cli = {
|
|
67
|
+
repo: 'Use --sections, --in, or --exclude to narrow the view.',
|
|
68
|
+
entrypoints: 'Use --framework or --exclude to narrow the result.',
|
|
69
|
+
endpoints: 'Use --prefix, --method, --server-only, or --client-only.',
|
|
70
|
+
impact: 'Use --file or --limit=N to narrow the result.',
|
|
71
|
+
tests: 'Use --file or --exclude to narrow the result.',
|
|
72
|
+
deadcode: 'Use --file, --in, or --exclude to narrow the result.',
|
|
73
|
+
usages: 'Use --file or --in to narrow the result.',
|
|
74
|
+
deps: 'Use --depth=1 or --direction=imports|importers.',
|
|
75
|
+
api: 'Use --limit=N to narrow the result.',
|
|
76
|
+
};
|
|
77
|
+
const mcp = {
|
|
78
|
+
repo: 'Use sections=, in=, or exclude= to narrow the view.',
|
|
79
|
+
entrypoints: 'Use framework= or exclude= to narrow the result.',
|
|
80
|
+
endpoints: 'Use prefix=, method=, server_only=true, or client_only=true.',
|
|
81
|
+
impact: 'Use file= or limit=<n> to narrow the result.',
|
|
82
|
+
tests: 'Use file= or exclude= to narrow the result.',
|
|
83
|
+
deadcode: 'Use file=, in=, or exclude= to narrow the result.',
|
|
84
|
+
usages: 'Use file= or in= to narrow the result.',
|
|
85
|
+
deps: 'Use depth=1 or direction=imports|importers.',
|
|
86
|
+
api: 'Use limit=<n> to narrow the result.',
|
|
87
|
+
};
|
|
88
|
+
const canonical = String(command).replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
89
|
+
if (canonical === 'deps' && params.cycles) {
|
|
90
|
+
return surface === 'mcp'
|
|
91
|
+
? 'Use max_chars=<n> to raise the output budget for cycle results.'
|
|
92
|
+
: 'Use --max-chars=N to raise the output budget for cycle results.';
|
|
93
|
+
}
|
|
94
|
+
if (surface === 'mcp' && (mcp[command] || mcp[canonical])) {
|
|
95
|
+
return mcp[command] || mcp[canonical];
|
|
96
|
+
}
|
|
97
|
+
if (surface !== 'mcp' && (cli[canonical] || cli[command])) {
|
|
98
|
+
return cli[canonical] || cli[command];
|
|
99
|
+
}
|
|
100
|
+
const applicable = new Set(FLAG_APPLICABILITY[canonical] || []);
|
|
101
|
+
const candidates = ['file', 'in', 'exclude'].filter(flag => applicable.has(flag));
|
|
102
|
+
if (candidates.length > 0) {
|
|
103
|
+
return surface === 'mcp'
|
|
104
|
+
? `Use ${candidates.map(flag => `${flag}=`).join(', ')} to narrow scope.`
|
|
105
|
+
: `Use ${candidates.map(flag => `--${flag}`).join(', ')} to narrow scope.`;
|
|
106
|
+
}
|
|
107
|
+
return surface === 'mcp'
|
|
108
|
+
? 'Use max_chars=<n> to raise the explicit output budget.'
|
|
109
|
+
: 'Use --max-chars=N to raise the explicit output budget.';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function compactNarrowingHint(command, surface, params = {}) {
|
|
113
|
+
const full = narrowingHint(command, surface, params);
|
|
114
|
+
if (surface !== 'mcp') {
|
|
115
|
+
const flags = full.match(/--[a-z-]+(?:=N)?/g) || [];
|
|
116
|
+
return [...new Set(flags)].join('/');
|
|
117
|
+
}
|
|
118
|
+
const flags = full.match(/\b(?:sections|in|exclude|framework|prefix|method|server_only|client_only|file|limit|depth|direction|max_chars)(?:=<n>|=true|=imports\|importers|=1|=)?/g) || [];
|
|
119
|
+
return [...new Set(flags)].join('/');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Apply the same bounded-output contract to CLI and MCP text. JSON is not
|
|
124
|
+
* passed here: structured consumers receive the complete stable envelope.
|
|
125
|
+
*/
|
|
126
|
+
function applyOutputBudget(text, {
|
|
127
|
+
command,
|
|
128
|
+
maxChars,
|
|
129
|
+
all = false,
|
|
130
|
+
surface = 'cli',
|
|
131
|
+
params = {},
|
|
132
|
+
} = {}) {
|
|
133
|
+
if (!text) {
|
|
134
|
+
return {
|
|
135
|
+
text: '(no output)',
|
|
136
|
+
truncated: false,
|
|
137
|
+
fullChars: 0,
|
|
138
|
+
requestedLimit: maxChars || null,
|
|
139
|
+
contractMetadata: [],
|
|
140
|
+
contractMetadataComplete: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const defaultLimit = BROAD_COMMANDS.has(command)
|
|
145
|
+
? BROAD_OUTPUT_CHARS
|
|
146
|
+
: DEFAULT_OUTPUT_CHARS;
|
|
147
|
+
const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
|
|
148
|
+
const limit = Math.min(requested, MAX_OUTPUT_CHARS);
|
|
149
|
+
if (text.length <= limit) {
|
|
150
|
+
return {
|
|
151
|
+
text,
|
|
152
|
+
truncated: false,
|
|
153
|
+
fullChars: text.length,
|
|
154
|
+
requestedLimit: limit,
|
|
155
|
+
contractMetadata: [],
|
|
156
|
+
contractMetadataComplete: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const canonical = resolveCommand(command, surface === 'mcp' ? 'mcp' : 'cli') ||
|
|
161
|
+
command;
|
|
162
|
+
const supportsAll = FLAG_APPLICABILITY[canonical]?.includes('all') &&
|
|
163
|
+
!(canonical === 'deps' && params.cycles);
|
|
164
|
+
const allHint = supportsAll
|
|
165
|
+
? (surface === 'mcp'
|
|
166
|
+
? 'Use all=true or max_chars=<n> (100K maximum).'
|
|
167
|
+
: 'Use --all or --max-chars=N (100K maximum).')
|
|
168
|
+
: (surface === 'mcp'
|
|
169
|
+
? 'Use max_chars=<n> (100K maximum).'
|
|
170
|
+
: 'Use --max-chars=N (100K maximum).');
|
|
171
|
+
const compactBudget = limit < 500;
|
|
172
|
+
const raiseHint = surface === 'mcp' ? 'max_chars=<n>' : '--max-chars=N';
|
|
173
|
+
const compactScope = compactNarrowingHint(command, surface, params);
|
|
174
|
+
const compactGuidance = compactScope.includes(raiseHint)
|
|
175
|
+
? `Raise ${raiseHint}.`
|
|
176
|
+
: `Narrow with ${compactScope || raiseHint}; raise ${raiseHint}.`;
|
|
177
|
+
let notice = compactBudget
|
|
178
|
+
? `... OUTPUT TRUNCATED (${text.length}→${limit}). ${compactGuidance}`
|
|
179
|
+
: `... OUTPUT TRUNCATED: ${text.length} chars total; hard limit ${limit}. ` +
|
|
180
|
+
`${narrowingHint(command, surface, params)} ${allHint}`;
|
|
181
|
+
if (compactBudget && notice.length > limit) {
|
|
182
|
+
const emergency = supportsAll
|
|
183
|
+
? (surface === 'mcp'
|
|
184
|
+
? 'all=true/max_chars=<n>'
|
|
185
|
+
: '--all/--max-chars=N')
|
|
186
|
+
: raiseHint;
|
|
187
|
+
notice = emergency.slice(0, limit);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// At very small explicit budgets, the verbose preservation heading alone
|
|
191
|
+
// can consume most of the transport. Trust/account lines take precedence
|
|
192
|
+
// over body detail and are appended directly after a compact notice.
|
|
193
|
+
if (compactBudget) {
|
|
194
|
+
const metadataCapacity = Math.max(0, limit - notice.length - 1);
|
|
195
|
+
const candidate = preservedContractMetadata(text, '', {
|
|
196
|
+
maxChars: metadataCapacity,
|
|
197
|
+
});
|
|
198
|
+
const candidateText = candidate.lines.join('\n');
|
|
199
|
+
const separatorChars = candidateText ? 2 : 1;
|
|
200
|
+
const bodyBudget = Math.max(0,
|
|
201
|
+
limit - notice.length - candidateText.length - separatorChars);
|
|
202
|
+
const prefix = text.slice(0, bodyBudget);
|
|
203
|
+
const lastNewline = prefix.lastIndexOf('\n');
|
|
204
|
+
const cleanCut = lastNewline > bodyBudget * 0.8
|
|
205
|
+
? prefix.slice(0, lastNewline)
|
|
206
|
+
: prefix;
|
|
207
|
+
const remainingForMetadata = Math.max(0,
|
|
208
|
+
limit - cleanCut.length - notice.length -
|
|
209
|
+
(cleanCut ? 1 : 0) - 1);
|
|
210
|
+
const contractMetadata = preservedContractMetadata(text, cleanCut, {
|
|
211
|
+
// preservedContractMetadata accounts a trailing newline per item;
|
|
212
|
+
// the renderer joins the final item without one.
|
|
213
|
+
maxChars: remainingForMetadata + 1,
|
|
214
|
+
});
|
|
215
|
+
const pieces = [];
|
|
216
|
+
if (cleanCut) pieces.push(cleanCut);
|
|
217
|
+
pieces.push(notice);
|
|
218
|
+
if (contractMetadata.lines.length > 0) {
|
|
219
|
+
pieces.push(contractMetadata.lines.join('\n'));
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
text: pieces.join('\n').slice(0, limit),
|
|
223
|
+
truncated: true,
|
|
224
|
+
fullChars: text.length,
|
|
225
|
+
requestedLimit: limit,
|
|
226
|
+
contractMetadata: contractMetadata.lines,
|
|
227
|
+
contractMetadataComplete: contractMetadata.complete,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// max_chars is a transport ceiling, not merely a body target. Reserve a
|
|
232
|
+
// bounded share for omitted trust/account lines, then spend the remainder
|
|
233
|
+
// on the original body and truncation notice. Tiny limits may be too small
|
|
234
|
+
// for any metadata; the returned completeness bit records that fact.
|
|
235
|
+
const metadataBudget = Math.min(
|
|
236
|
+
MAX_PRESERVED_CONTRACT_CHARS,
|
|
237
|
+
Math.max(0, Math.floor(limit * 0.50)),
|
|
238
|
+
);
|
|
239
|
+
const candidateMetadata = preservedContractMetadata(text, '', {
|
|
240
|
+
maxChars: metadataBudget,
|
|
241
|
+
});
|
|
242
|
+
const provisionalMetadata = candidateMetadata.lines.length > 0
|
|
243
|
+
? '\n\nPRESERVED CONTRACT METADATA (from omitted output):\n' +
|
|
244
|
+
candidateMetadata.lines.join('\n')
|
|
245
|
+
: '';
|
|
246
|
+
const bodyBudget = Math.max(0,
|
|
247
|
+
limit - notice.length - provisionalMetadata.length - 2);
|
|
248
|
+
const truncated = text.substring(0, bodyBudget);
|
|
249
|
+
const lastNewline = truncated.lastIndexOf('\n');
|
|
250
|
+
const cleanCut = lastNewline > bodyBudget * 0.8
|
|
251
|
+
? truncated.substring(0, lastNewline)
|
|
252
|
+
: truncated;
|
|
253
|
+
const metadataHeading = '\n\nPRESERVED CONTRACT METADATA (from omitted output):';
|
|
254
|
+
const remainingForMetadata = Math.max(0,
|
|
255
|
+
limit - cleanCut.length - notice.length - (cleanCut ? 2 : 0) -
|
|
256
|
+
metadataHeading.length);
|
|
257
|
+
const contractMetadata = preservedContractMetadata(text, cleanCut, {
|
|
258
|
+
maxChars: Math.min(metadataBudget, remainingForMetadata),
|
|
259
|
+
});
|
|
260
|
+
let rendered = `${cleanCut}${cleanCut ? '\n\n' : ''}${notice}`;
|
|
261
|
+
|
|
262
|
+
if (contractMetadata.lines.length > 0) {
|
|
263
|
+
const heading = metadataHeading;
|
|
264
|
+
if (rendered.length + heading.length <= limit) rendered += heading;
|
|
265
|
+
for (const line of contractMetadata.lines) {
|
|
266
|
+
if (rendered.length + line.length + 1 > limit) break;
|
|
267
|
+
rendered += '\n' + line;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (contractMetadata.omitted > 0) {
|
|
271
|
+
const warning = `\nWARNING: ${contractMetadata.omitted} additional contract line(s) ` +
|
|
272
|
+
'could not fit the preservation budget; narrow scope before acting.';
|
|
273
|
+
if (rendered.length + warning.length <= limit) rendered += warning;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Defensive final ceiling for very small limits and future notice edits.
|
|
277
|
+
rendered = rendered.slice(0, limit);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
text: rendered,
|
|
281
|
+
truncated: true,
|
|
282
|
+
fullChars: text.length,
|
|
283
|
+
requestedLimit: limit,
|
|
284
|
+
contractMetadata: contractMetadata.lines,
|
|
285
|
+
contractMetadataComplete: contractMetadata.complete,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
DEFAULT_OUTPUT_CHARS,
|
|
291
|
+
BROAD_OUTPUT_CHARS,
|
|
292
|
+
MAX_OUTPUT_CHARS,
|
|
293
|
+
applyOutputBudget,
|
|
294
|
+
preservedContractMetadata,
|
|
295
|
+
};
|
package/core/output.js
CHANGED
package/core/parallel-build.js
CHANGED
|
@@ -9,9 +9,32 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const os = require('os');
|
|
12
|
+
const fs = require('fs');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const { Worker, MessageChannel, receiveMessageOnPort } = require('worker_threads');
|
|
14
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;
|
|
22
|
+
if (!Number.isFinite(bytes)) {
|
|
23
|
+
try { bytes = fs.statSync(file).size; } catch (_) { bytes = 0; }
|
|
24
|
+
}
|
|
25
|
+
return { file, bytes, order };
|
|
26
|
+
}).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
|
+
}
|
|
37
|
+
|
|
15
38
|
/**
|
|
16
39
|
* Build index in parallel using worker threads.
|
|
17
40
|
*
|
|
@@ -28,23 +51,33 @@ function parallelBuild(index, files, options = {}) {
|
|
|
28
51
|
: os.cpus().length;
|
|
29
52
|
const autoWorkers = Math.max(availableCpus - 1, 1);
|
|
30
53
|
const maxWorkers = (options.workerCount > 0) ? options.workerCount : autoWorkers;
|
|
54
|
+
const workerCap = options.maxWorkers > 0 ? options.maxWorkers : 8;
|
|
55
|
+
const minFilesPerWorker = options.minFilesPerWorker > 0
|
|
56
|
+
? options.minFilesPerWorker : 100;
|
|
31
57
|
const workerCount = Math.min(
|
|
32
58
|
maxWorkers,
|
|
33
|
-
|
|
34
|
-
Math.ceil(files.length /
|
|
59
|
+
workerCap,
|
|
60
|
+
Math.ceil(files.length / minFilesPerWorker)
|
|
35
61
|
);
|
|
36
62
|
|
|
37
|
-
if (workerCount < 2)
|
|
63
|
+
if (workerCount < 2) {
|
|
64
|
+
index.lastBuildWorkerCount = 1;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
index.lastBuildWorkerCount = workerCount;
|
|
38
68
|
|
|
39
69
|
if (!options.quiet) {
|
|
40
70
|
console.error(`Parallel build: ${workerCount} workers for ${files.length} files`);
|
|
41
71
|
}
|
|
42
72
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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);
|
|
48
81
|
|
|
49
82
|
// Synchronization: one Int32 per worker in SharedArrayBuffer
|
|
50
83
|
const sab = new SharedArrayBuffer(4 * workerCount);
|
|
@@ -59,7 +92,7 @@ function parallelBuild(index, files, options = {}) {
|
|
|
59
92
|
|
|
60
93
|
// Build per-worker hash subset (each worker only needs hashes for its chunk)
|
|
61
94
|
const workerHashes = Object.create(null);
|
|
62
|
-
for (const fp of chunks[i]) {
|
|
95
|
+
for (const fp of chunks[i].files) {
|
|
63
96
|
const entry = index.files.get(fp);
|
|
64
97
|
if (entry) {
|
|
65
98
|
workerHashes[fp] = { mtime: entry.mtime, size: entry.size, hash: entry.hash };
|
|
@@ -68,7 +101,7 @@ function parallelBuild(index, files, options = {}) {
|
|
|
68
101
|
|
|
69
102
|
const worker = new Worker(path.join(__dirname, 'build-worker.js'), {
|
|
70
103
|
workerData: {
|
|
71
|
-
files: chunks[i],
|
|
104
|
+
files: chunks[i].files,
|
|
72
105
|
rootDir: index.root,
|
|
73
106
|
existingHashes: workerHashes,
|
|
74
107
|
signal: sab,
|
|
@@ -165,4 +198,4 @@ function parallelBuild(index, files, options = {}) {
|
|
|
165
198
|
return changed;
|
|
166
199
|
}
|
|
167
200
|
|
|
168
|
-
module.exports = { parallelBuild };
|
|
201
|
+
module.exports = { parallelBuild, partitionFiles };
|
package/core/parser.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
|
-
const { detectLanguage, getParser,
|
|
9
|
+
const { detectLanguage, getParser, getLanguageAdapter, isSupported } = require('../languages');
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @typedef {Object} FunctionDef
|
|
@@ -80,9 +80,9 @@ function parse(code, language) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const parser = getParser(language);
|
|
83
|
-
const
|
|
83
|
+
const adapter = getLanguageAdapter(language);
|
|
84
84
|
|
|
85
|
-
return
|
|
85
|
+
return adapter.parse(code, parser);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|