ucn 5.5.3 → 5.6.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 +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
package/core/output/tracing.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* conservation) lines close the arithmetic.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const { testCountText } = require('../test-units');
|
|
11
12
|
const { unverifiedReasonLabel } = require('./shared');
|
|
12
13
|
const { codeUnitCompare } = require('../shared');
|
|
13
14
|
const { formatAccountLines } = require('./analysis');
|
|
@@ -562,23 +563,49 @@ function formatAffectedTests(result, options = {}) {
|
|
|
562
563
|
lines.push('No confirmed static test links found for any affected function.');
|
|
563
564
|
} else {
|
|
564
565
|
const MAX_TEST_FILES = options.all ? Infinity : 30;
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
|
|
566
|
+
const runnable = result.testFiles.filter(tf => tf.role !== 'support');
|
|
567
|
+
const support = result.testFiles.filter(tf => tf.role === 'support');
|
|
568
|
+
const displayFiles = runnable.slice(0, MAX_TEST_FILES);
|
|
569
|
+
const truncatedFiles = runnable.length - displayFiles.length;
|
|
570
|
+
// A count is exact only where every test of the files is listed:
|
|
571
|
+
// "at least" says a file holds tests that are not.
|
|
572
|
+
const total = testCountText(summary.totalTests, summary.moreTests, summary.conditionalTests);
|
|
573
|
+
lines.push(`Test files to run (${runnable.length})${total ? `, ${total}` : ''}:`);
|
|
568
574
|
lines.push('');
|
|
569
575
|
for (const tf of displayFiles) {
|
|
570
|
-
|
|
576
|
+
const counted = testCountText(tf.testCount, tf.moreTests, tf.conditionalTests);
|
|
577
|
+
const count = counted ? ` · ${counted}` : '';
|
|
578
|
+
const refusal = tf.runWithheld ? ` · no command: ${tf.runWithheld}` : '';
|
|
579
|
+
lines.push(` ${tf.file} (links: ${linkList(tf.linkedFunctions, options)})${count}${refusal}`);
|
|
571
580
|
// Show up to 5 key matches per file
|
|
572
581
|
const keyMatches = tf.matches
|
|
573
|
-
.filter(m => m.matchType === 'call' || m.matchType === 'test-case')
|
|
582
|
+
.filter(m => m.matchType === 'call' || m.matchType === 'test-case' || m.matchType === 'inherited-test')
|
|
574
583
|
.slice(0, 5);
|
|
575
584
|
for (const m of keyMatches) {
|
|
576
|
-
|
|
585
|
+
const origin = m.matchType === 'inherited-test' ? ` ${m.name} (from ${m.from})` : '';
|
|
586
|
+
lines.push(` L${m.line}: ${m.content}${origin} [${m.matchType}]`);
|
|
577
587
|
}
|
|
578
588
|
}
|
|
579
589
|
if (truncatedFiles > 0) {
|
|
580
590
|
lines.push(`\n ... ${truncatedFiles} more test files (use file= and exclude= to narrow scope)`);
|
|
581
591
|
}
|
|
592
|
+
if (support.length > 0) {
|
|
593
|
+
lines.push('');
|
|
594
|
+
lines.push(`Test support code reached (${support.length}) — declares no test, runs with the tests that load it:`);
|
|
595
|
+
for (const tf of support.slice(0, options.all ? Infinity : 10)) {
|
|
596
|
+
lines.push(` ${tf.file} (links: ${linkList(tf.linkedFunctions, options)})`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const commands = result.commands || [];
|
|
600
|
+
if (commands.length > 0) {
|
|
601
|
+
lines.push('');
|
|
602
|
+
lines.push('Run:');
|
|
603
|
+
const MAX_COMMANDS = options.all ? Infinity : 5;
|
|
604
|
+
for (const line of commands.slice(0, MAX_COMMANDS)) lines.push(` ${line}`);
|
|
605
|
+
if (commands.length > MAX_COMMANDS) {
|
|
606
|
+
lines.push(` ... ${commands.length - MAX_COMMANDS} more (${options.allHint || 'use --all'})`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
582
609
|
}
|
|
583
610
|
|
|
584
611
|
// Possible band: functions reachable only through unverified chains.
|
|
@@ -592,7 +619,7 @@ function formatAffectedTests(result, options = {}) {
|
|
|
592
619
|
}
|
|
593
620
|
const pat = result.possiblyAffectedTests || [];
|
|
594
621
|
if (pat.length > 0) {
|
|
595
|
-
lines.push(`
|
|
622
|
+
lines.push(` Test files with possible links (${pat.length}):`);
|
|
596
623
|
const MAX_POSSIBLE = options.all ? Infinity : 10;
|
|
597
624
|
for (const tf of pat.slice(0, MAX_POSSIBLE)) {
|
|
598
625
|
lines.push(` ${tf.file} (links: ${linkList(tf.linkedFunctions, options)})`);
|
|
@@ -614,7 +641,8 @@ function formatAffectedTests(result, options = {}) {
|
|
|
614
641
|
const pct = summary.totalAffected > 0
|
|
615
642
|
? Math.round(summary.staticallyLinkedFunctions / summary.totalAffected * 100)
|
|
616
643
|
: 0;
|
|
617
|
-
|
|
644
|
+
const totalTests = testCountText(summary.totalTests, summary.moreTests, summary.conditionalTests);
|
|
645
|
+
let summaryLine = `Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} statically linked test files${totalTests ? ` (${totalTests})` : ''}, ${summary.staticallyLinkedFunctions}/${summary.totalAffected} functions linked (${pct}%)`;
|
|
618
646
|
if (summary.possiblyAffected > 0) {
|
|
619
647
|
summaryLine += ` · ${summary.possiblyAffected} possibly affected (unverified chains)`;
|
|
620
648
|
}
|
package/core/output-budget.js
CHANGED
|
@@ -3,22 +3,27 @@
|
|
|
3
3
|
const {
|
|
4
4
|
BROAD_COMMANDS: BROAD_CANONICAL,
|
|
5
5
|
FLAG_APPLICABILITY,
|
|
6
|
+
paramDefault,
|
|
7
|
+
paramRange,
|
|
6
8
|
resolveCommand,
|
|
7
|
-
toMcpName,
|
|
8
9
|
} = require('./registry');
|
|
10
|
+
const { LINES_COMMANDS, budgetLineListing } = require('./output/lines');
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
...[...BROAD_CANONICAL].map(toMcpName),
|
|
16
|
-
]);
|
|
12
|
+
// The output budget is the parameter table's `maxChars`: its ceiling, its
|
|
13
|
+
// default, and the smaller default of the broad sweep commands.
|
|
14
|
+
const DEFAULT_OUTPUT_CHARS = paramDefault('maxChars');
|
|
15
|
+
const BROAD_OUTPUT_CHARS = paramDefault('maxChars', [...BROAD_CANONICAL][0]);
|
|
16
|
+
const MAX_OUTPUT_CHARS = paramRange('maxChars').max;
|
|
17
17
|
|
|
18
|
-
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|VALUE ACCOUNT|TYPE ACCOUNT|Note)
|
|
18
|
+
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|VALUE ACCOUNT|TYPE ACCOUNT|Note):|TRUST: (?:BLOCKED|REVIEW_REQUIRED|READY_FOR_TOOLCHAIN)\b|\d+ test-file usage\(s\) hidden\b|Found \d+ (?:definitions|fuzzy matches)\b)/;
|
|
19
19
|
// Trust lines: the accounting and contract lines that qualify a caller
|
|
20
20
|
// answer. A truncated answer carries every one of them or none of its body.
|
|
21
|
-
|
|
21
|
+
// The pre-commit check's verdict line is one of them (`repo` prints a
|
|
22
|
+
// TRUST level of its own, which is orientation, not a verdict).
|
|
23
|
+
const TRUST_LINE_RE = /^(?:(?:ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|VALUE ACCOUNT|TYPE ACCOUNT):|TRUST: (?:BLOCKED|REVIEW_REQUIRED|READY_FOR_TOOLCHAIN)\b)/;
|
|
24
|
+
// A count or deletion verdict is the answer the warnings qualify. Preserve
|
|
25
|
+
// it with those warnings, or explicitly withhold the answer when both cannot fit.
|
|
26
|
+
const RESULT_LINE_RE = /^(?:Dead code: \d+ unused symbol\(s\)|No dead code found\.|VERDICT: (?:no-static-dependents|has-static-dependents|review)\b)/;
|
|
22
27
|
const MAX_PRESERVED_CONTRACT_LINES = 24;
|
|
23
28
|
const MAX_PRESERVED_CONTRACT_CHARS = 8000;
|
|
24
29
|
|
|
@@ -43,10 +48,10 @@ function contractCandidates(fullText) {
|
|
|
43
48
|
// Shell-shaped MCP/interactive results prefix disclosures with '# '.
|
|
44
49
|
// Match their contents but retain the prefix in the preserved text.
|
|
45
50
|
const evidenceLine = contractLine.replace(/^\s*# /, '').trim();
|
|
46
|
-
if (!CONTRACT_LINE_RE.test(evidenceLine)) continue;
|
|
51
|
+
if (!CONTRACT_LINE_RE.test(evidenceLine) && !RESULT_LINE_RE.test(evidenceLine)) continue;
|
|
47
52
|
const line = contractLine.trim();
|
|
48
53
|
if (!line) continue;
|
|
49
|
-
const priority = TRUST_LINE_RE.test(evidenceLine)
|
|
54
|
+
const priority = TRUST_LINE_RE.test(evidenceLine) || RESULT_LINE_RE.test(evidenceLine)
|
|
50
55
|
? 0
|
|
51
56
|
: /^\d+ test-file usage\(s\) hidden\b/.test(evidenceLine) ? 1 : 2;
|
|
52
57
|
candidates.push({ line, priority, sourceIndex });
|
|
@@ -139,7 +144,9 @@ function compactNarrowingHint(command, surface, params = {}) {
|
|
|
139
144
|
// (`CALLERS — CONFIRMED (12, ...)`, `CALLERS — RUNTIME DISPATCH (332 sites`,
|
|
140
145
|
// `UNVERIFIED CALL SITES (627) —`, `CALLEES (3):`, `BY FILE:`).
|
|
141
146
|
const SECTION_HEADER_RE = /^(?:[A-Z][A-Z-]*(?: [A-Z][A-Z-]*)*(?: — [A-Z][A-Z -]*)? \(\d[^)]*\)|[A-Z][A-Z ]+:$)/;
|
|
142
|
-
|
|
147
|
+
// `check` groups its items by verdict (`BLOCKED (2) — ...`): a long list of
|
|
148
|
+
// breaks must not push what is ready, or what to review, out of the answer.
|
|
149
|
+
const SECTION_AWARE_COMMANDS = new Set(['show', 'impact', 'check']);
|
|
143
150
|
|
|
144
151
|
/**
|
|
145
152
|
* Cut a relationship answer to `budget` chars keeping a representative head
|
|
@@ -152,7 +159,7 @@ const SECTION_AWARE_COMMANDS = new Set(['show', 'impact']);
|
|
|
152
159
|
* no sections or its fixed parts alone do not fit (caller falls back to a
|
|
153
160
|
* head cut).
|
|
154
161
|
*/
|
|
155
|
-
function sectionAwareCut(text, budget) {
|
|
162
|
+
function sectionAwareCut(text, budget, lineCost = line => line.length) {
|
|
156
163
|
const lines = text.split('\n');
|
|
157
164
|
const blocks = []; // { kind: 'fixed', line } | { kind: 'section', header, body }
|
|
158
165
|
let current = null;
|
|
@@ -177,7 +184,7 @@ function sectionAwareCut(text, budget) {
|
|
|
177
184
|
const gapLine = ' ...';
|
|
178
185
|
let used = 0;
|
|
179
186
|
for (const block of blocks) {
|
|
180
|
-
used += (block.kind === 'fixed' ? block.line
|
|
187
|
+
used += lineCost(block.kind === 'fixed' ? block.line : block.header) + 1;
|
|
181
188
|
}
|
|
182
189
|
// Reserve the worst-case `+N more` line and one gap marker per section.
|
|
183
190
|
for (const section of sections) {
|
|
@@ -212,7 +219,7 @@ function sectionAwareCut(text, budget) {
|
|
|
212
219
|
const state = orders.get(section);
|
|
213
220
|
if (state.next >= state.order.length) continue;
|
|
214
221
|
const index = state.order[state.next];
|
|
215
|
-
const cost = section.body[index]
|
|
222
|
+
const cost = lineCost(section.body[index]) + 1;
|
|
216
223
|
if (used + cost > budget) { state.next = state.order.length; continue; }
|
|
217
224
|
used += cost;
|
|
218
225
|
state.taken.add(index);
|
|
@@ -249,13 +256,31 @@ function applyOutputBudget(text, {
|
|
|
249
256
|
surface = 'cli',
|
|
250
257
|
params = {},
|
|
251
258
|
trailingChars = 0,
|
|
259
|
+
peerText,
|
|
252
260
|
} = {}) {
|
|
253
|
-
const defaultLimit =
|
|
254
|
-
|
|
255
|
-
: DEFAULT_OUTPUT_CHARS;
|
|
261
|
+
const defaultLimit = paramDefault('maxChars',
|
|
262
|
+
resolveCommand(command, 'mcp') || resolveCommand(command, 'cli') || command);
|
|
256
263
|
const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
|
|
257
264
|
const hardLimit = Math.min(requested, MAX_OUTPUT_CHARS);
|
|
258
|
-
|
|
265
|
+
// Native CLI/MCP guidance has different lengths. Both presentations
|
|
266
|
+
// select the same semantic lines using the larger cost at each source
|
|
267
|
+
// line, including room for the CLI's terminal newline. Rendered text
|
|
268
|
+
// keeps its own native guidance and actual full-character count.
|
|
269
|
+
const paired = typeof peerText === 'string';
|
|
270
|
+
const costs = new Map();
|
|
271
|
+
if (paired) {
|
|
272
|
+
const own = String(text || '').split('\n'), peer = peerText.split('\n');
|
|
273
|
+
for (let i = 0; i < own.length; i++) {
|
|
274
|
+
const cost = Math.max(own[i].length, (peer[i] || '').length);
|
|
275
|
+
costs.set(own[i], Math.max(costs.get(own[i]) || 0, cost));
|
|
276
|
+
costs.set(own[i].trim(), Math.max(costs.get(own[i].trim()) || 0,
|
|
277
|
+
Math.max(own[i].trim().length, (peer[i] || '').trim().length)));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const lineCost = line => costs.get(line) ?? line.length;
|
|
281
|
+
const textCost = value => value.split('\n').reduce((sum, line) => sum + lineCost(line) + 1, -1);
|
|
282
|
+
const terminalChars = paired ? Math.max(1, trailingChars) : trailingChars;
|
|
283
|
+
const limit = Math.max(0, hardLimit - terminalChars);
|
|
259
284
|
if (!text) {
|
|
260
285
|
return {
|
|
261
286
|
text: '(no output)'.slice(0, limit),
|
|
@@ -267,7 +292,7 @@ function applyOutputBudget(text, {
|
|
|
267
292
|
};
|
|
268
293
|
}
|
|
269
294
|
|
|
270
|
-
if (text
|
|
295
|
+
if (textCost(text) <= limit) {
|
|
271
296
|
return {
|
|
272
297
|
text,
|
|
273
298
|
truncated: false,
|
|
@@ -280,25 +305,56 @@ function applyOutputBudget(text, {
|
|
|
280
305
|
|
|
281
306
|
const canonical = resolveCommand(command, surface === 'mcp' ? 'mcp' : 'cli') ||
|
|
282
307
|
command;
|
|
308
|
+
// A line listing is cut between records: the rows that fit, the
|
|
309
|
+
// accounting, and one line counting the rows left out.
|
|
310
|
+
if (params.lines && LINES_COMMANDS.has(canonical)) {
|
|
311
|
+
const listing = budgetLineListing(text, limit, surface, { allowNoRows: true });
|
|
312
|
+
if (listing) {
|
|
313
|
+
return {
|
|
314
|
+
text: listing.text,
|
|
315
|
+
truncated: true,
|
|
316
|
+
fullChars: text.length,
|
|
317
|
+
requestedLimit: hardLimit,
|
|
318
|
+
contractMetadata: [],
|
|
319
|
+
contractMetadataComplete: listing.contractMetadataComplete,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
283
323
|
const supportsAll = FLAG_APPLICABILITY[canonical]?.includes('all') &&
|
|
284
324
|
!(canonical === 'deps' && params.cycles);
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
?
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
325
|
+
const noticesFor = (target, fullChars, budget = hardLimit) => {
|
|
326
|
+
const allHint = supportsAll
|
|
327
|
+
? (target === 'mcp'
|
|
328
|
+
? 'Use all=true or max_chars=<n> (100K maximum).'
|
|
329
|
+
: 'Use --all or --max-chars=N (100K maximum).')
|
|
330
|
+
: (target === 'mcp'
|
|
331
|
+
? 'Use max_chars=<n> (100K maximum).'
|
|
332
|
+
: 'Use --max-chars=N (100K maximum).');
|
|
333
|
+
const raiseHint = target === 'mcp' ? 'max_chars=<n>' : '--max-chars=N';
|
|
334
|
+
const compactScope = compactNarrowingHint(command, target, params);
|
|
335
|
+
const compactGuidance = compactScope.includes(raiseHint)
|
|
336
|
+
? `Raise ${raiseHint}.`
|
|
337
|
+
: `Narrow with ${compactScope || raiseHint}; raise ${raiseHint}.`;
|
|
338
|
+
return {
|
|
339
|
+
longNotice: `... OUTPUT TRUNCATED: ${fullChars} chars total; hard limit ${budget}. ` +
|
|
340
|
+
`${narrowingHint(command, target, params)} ${allHint}`,
|
|
341
|
+
compactNotice: `... OUTPUT TRUNCATED (${fullChars}→${budget}). ${compactGuidance}`,
|
|
342
|
+
shortNotice: `... OUTPUT TRUNCATED (${fullChars}→${budget}). Raise ${raiseHint}.`,
|
|
343
|
+
// At the tightest budgets, disclosures outrank the repeated
|
|
344
|
+
// raise-budget guidance; retain the full size and limit.
|
|
345
|
+
minimalNotice: `... OUTPUT TRUNCATED (${fullChars}→${budget}).`,
|
|
346
|
+
raiseHint,
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
const ownNotices = noticesFor(surface, text.length);
|
|
350
|
+
if (paired) {
|
|
351
|
+
const peerNotices = noticesFor(surface === 'mcp' ? 'cli' : 'mcp', peerText.length);
|
|
352
|
+
for (const name of ['longNotice', 'compactNotice', 'shortNotice', 'minimalNotice']) {
|
|
353
|
+
costs.set(ownNotices[name], Math.max(ownNotices[name].length, peerNotices[name].length));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const { longNotice, compactNotice, shortNotice, minimalNotice, raiseHint } = ownNotices;
|
|
292
357
|
const compactBudget = limit < 500;
|
|
293
|
-
const raiseHint = surface === 'mcp' ? 'max_chars=<n>' : '--max-chars=N';
|
|
294
|
-
const compactScope = compactNarrowingHint(command, surface, params);
|
|
295
|
-
const compactGuidance = compactScope.includes(raiseHint)
|
|
296
|
-
? `Raise ${raiseHint}.`
|
|
297
|
-
: `Narrow with ${compactScope || raiseHint}; raise ${raiseHint}.`;
|
|
298
|
-
const longNotice = `... OUTPUT TRUNCATED: ${text.length} chars total; hard limit ${hardLimit}. ` +
|
|
299
|
-
`${narrowingHint(command, surface, params)} ${allHint}`;
|
|
300
|
-
const compactNotice = `... OUTPUT TRUNCATED (${text.length}→${hardLimit}). ${compactGuidance}`;
|
|
301
|
-
const shortNotice = `... OUTPUT TRUNCATED. Raise ${raiseHint}.`;
|
|
302
358
|
|
|
303
359
|
// Trust lines (ACCOUNT/CONTRACT/WARNING/...) qualify the answer: room for
|
|
304
360
|
// every one of them is reserved before any body text. Optional
|
|
@@ -307,7 +363,7 @@ function applyOutputBudget(text, {
|
|
|
307
363
|
const trust = candidates.filter(candidate => candidate.priority === 0);
|
|
308
364
|
const optional = candidates.filter(candidate => candidate.priority > 0)
|
|
309
365
|
.sort((a, b) => a.priority - b.priority || a.sourceIndex - b.sourceIndex);
|
|
310
|
-
const trustChars = trust.reduce((sum, candidate) => sum + candidate.line
|
|
366
|
+
const trustChars = trust.reduce((sum, candidate) => sum + lineCost(candidate.line) + 1, 0);
|
|
311
367
|
const result = (rendered, appended, complete) => ({
|
|
312
368
|
text: rendered,
|
|
313
369
|
truncated: true,
|
|
@@ -321,6 +377,16 @@ function applyOutputBudget(text, {
|
|
|
321
377
|
// number of complete source lines it holds.
|
|
322
378
|
const headCut = (budget) => {
|
|
323
379
|
if (budget <= 0) return { body: '', keptLines: 0 };
|
|
380
|
+
if (paired) {
|
|
381
|
+
const kept = [];
|
|
382
|
+
let used = -1;
|
|
383
|
+
for (const line of text.split('\n')) {
|
|
384
|
+
if (used + lineCost(line) + 1 > budget) break;
|
|
385
|
+
kept.push(line);
|
|
386
|
+
used += lineCost(line) + 1;
|
|
387
|
+
}
|
|
388
|
+
return { body: kept.join('\n'), keptLines: kept.length };
|
|
389
|
+
}
|
|
324
390
|
const prefix = text.slice(0, budget);
|
|
325
391
|
const lastNewline = prefix.lastIndexOf('\n');
|
|
326
392
|
const body = lastNewline > budget * 0.8 ? prefix.slice(0, lastNewline) : prefix;
|
|
@@ -336,18 +402,18 @@ function applyOutputBudget(text, {
|
|
|
336
402
|
// to `optionalShare` chars (priority order) before the body; the body
|
|
337
403
|
// takes the rest. Returns null when the notice and the trust lines alone
|
|
338
404
|
// exceed the limit.
|
|
339
|
-
const costOf = list => list.reduce((sum, candidate) => sum + candidate.line
|
|
405
|
+
const costOf = list => list.reduce((sum, candidate) => sum + lineCost(candidate.line) + 1, 0);
|
|
340
406
|
const layout = (notice, heading, separator, optionalShare) => {
|
|
341
|
-
const reserveFor = metadataChars => notice
|
|
407
|
+
const reserveFor = metadataChars => lineCost(notice) + (metadataChars > 0 ? heading.length + metadataChars : 0);
|
|
342
408
|
if (reserveFor(trustChars) > limit) return null;
|
|
343
409
|
const reservedOptional = [];
|
|
344
410
|
let optionalChars = 0;
|
|
345
411
|
const optionalRoom = Math.min(optionalShare, limit - reserveFor(trustChars) - (trustChars > 0 ? 0 : heading.length));
|
|
346
412
|
for (const candidate of optional) {
|
|
347
413
|
if (reservedOptional.length >= MAX_PRESERVED_CONTRACT_LINES) break;
|
|
348
|
-
if (optionalChars + candidate.line
|
|
414
|
+
if (optionalChars + lineCost(candidate.line) + 1 > optionalRoom) continue;
|
|
349
415
|
reservedOptional.push(candidate);
|
|
350
|
-
optionalChars += candidate.line
|
|
416
|
+
optionalChars += lineCost(candidate.line) + 1;
|
|
351
417
|
}
|
|
352
418
|
const reserved = [...trust, ...reservedOptional];
|
|
353
419
|
const pendingOf = cutState => reserved.filter(candidate => candidate.sourceIndex >= cutState.keptLines);
|
|
@@ -361,12 +427,12 @@ function applyOutputBudget(text, {
|
|
|
361
427
|
}
|
|
362
428
|
const metadata = pendingOf(cut);
|
|
363
429
|
let rendered = cut.body ? `${cut.body}${separator}${notice}` : notice;
|
|
364
|
-
let used = rendered
|
|
430
|
+
let used = textCost(rendered) + (metadata.length > 0 ? heading.length + costOf(metadata) : 0);
|
|
365
431
|
let omitted = 0;
|
|
366
432
|
const taken = new Set(reserved);
|
|
367
433
|
for (const candidate of optional) {
|
|
368
434
|
if (taken.has(candidate) || candidate.sourceIndex < cut.keptLines) continue;
|
|
369
|
-
const cost = candidate.line
|
|
435
|
+
const cost = lineCost(candidate.line) + 1 + (metadata.length === 0 ? heading.length : 0);
|
|
370
436
|
if (metadata.length >= trust.length + MAX_PRESERVED_CONTRACT_LINES || used + cost > limit) {
|
|
371
437
|
omitted++;
|
|
372
438
|
continue;
|
|
@@ -392,28 +458,44 @@ function applyOutputBudget(text, {
|
|
|
392
458
|
// so with the budget that would carry them.
|
|
393
459
|
const withheld = () => {
|
|
394
460
|
const flag = surface === 'mcp' ? 'max_chars' : '--max-chars';
|
|
395
|
-
const noticeFor = (shown, needed
|
|
396
|
-
|
|
461
|
+
const noticeFor = (shown, needed, target = surface, fullChars = text.length) =>
|
|
462
|
+
`... OUTPUT TRUNCATED (${fullChars}→${hardLimit}): answer withheld; ` +
|
|
463
|
+
`${shown} of ${trust.length} trust line(s) shown. Raise ${target === 'mcp' ? 'max_chars' : '--max-chars'} to at least ${needed}.`;
|
|
397
464
|
// Smallest budget whose shortest layout carries every trust line.
|
|
398
|
-
|
|
465
|
+
let needed = terminalChars + lineCost(minimalNotice) + trustChars;
|
|
466
|
+
// Raising the limit can add a digit to the notice itself. Compute
|
|
467
|
+
// the advertised budget using that budget's notice, for both peers.
|
|
468
|
+
for (;;) {
|
|
469
|
+
const noticeChars = Math.max(noticesFor(surface, text.length, needed).minimalNotice.length,
|
|
470
|
+
paired ? noticesFor(surface === 'mcp' ? 'cli' : 'mcp', peerText.length, needed).minimalNotice.length : 0);
|
|
471
|
+
const required = terminalChars + noticeChars + trustChars;
|
|
472
|
+
if (required <= needed) break;
|
|
473
|
+
needed = required;
|
|
474
|
+
}
|
|
399
475
|
let shown = 0;
|
|
400
|
-
|
|
476
|
+
const noticeCost = text => paired ? Math.max(text.length,
|
|
477
|
+
text.replace(surface === 'mcp' ? 'max_chars' : '--max-chars', surface === 'mcp' ? '--max-chars' : 'max_chars').length) : text.length;
|
|
478
|
+
// Native full sizes can have different digit widths. Pair the whole
|
|
479
|
+
// notice before choosing trust lines, just as for ordinary notices.
|
|
480
|
+
const fullNoticeCost = shown => Math.max(noticeFor(shown, needed).length,
|
|
481
|
+
paired ? noticeFor(shown, needed, surface === 'mcp' ? 'cli' : 'mcp', peerText.length).length : 0);
|
|
482
|
+
let used = fullNoticeCost(trust.length);
|
|
401
483
|
const kept = [];
|
|
402
484
|
for (const candidate of trust) {
|
|
403
|
-
if (used + candidate.line
|
|
485
|
+
if (used + lineCost(candidate.line) + 1 > limit) break;
|
|
404
486
|
kept.push(candidate.line);
|
|
405
|
-
used += candidate.line
|
|
487
|
+
used += lineCost(candidate.line) + 1;
|
|
406
488
|
shown++;
|
|
407
489
|
}
|
|
408
490
|
const notice = noticeFor(shown, needed);
|
|
409
|
-
if (
|
|
491
|
+
if (fullNoticeCost(shown) > limit) {
|
|
410
492
|
// Shorter statements of the same fact; a budget number is never
|
|
411
493
|
// cut mid-digit.
|
|
412
494
|
const shorter = [
|
|
413
495
|
`TRUNCATED: answer withheld; raise ${flag} to at least ${needed}.`,
|
|
414
496
|
`Answer withheld; ${flag}>=${needed}`,
|
|
415
497
|
`withheld; >=${needed}`,
|
|
416
|
-
].find(text => text
|
|
498
|
+
].find(text => noticeCost(text) <= limit);
|
|
417
499
|
return result(shorter || '', [], false);
|
|
418
500
|
}
|
|
419
501
|
return result([notice, ...kept].join('\n'), kept, false);
|
|
@@ -440,7 +522,9 @@ function applyOutputBudget(text, {
|
|
|
440
522
|
|
|
441
523
|
if (compactBudget) {
|
|
442
524
|
// Tiny transports: every disclosure outranks body detail.
|
|
443
|
-
return pick(() => layout(compactNotice, '', '\n', limit),
|
|
525
|
+
return pick(() => layout(compactNotice, '', '\n', limit),
|
|
526
|
+
() => layout(shortNotice, '', '\n', limit),
|
|
527
|
+
() => layout(minimalNotice, '', '\n', limit)) ||
|
|
444
528
|
(trust.length > 0 ? withheld() : bare());
|
|
445
529
|
}
|
|
446
530
|
|
|
@@ -448,26 +532,26 @@ function applyOutputBudget(text, {
|
|
|
448
532
|
// Sections keep every non-indented line (trust lines included) in
|
|
449
533
|
// place; trust lines inside a cut section body are appended, with
|
|
450
534
|
// their room taken from the sections.
|
|
451
|
-
let sectionBudget = limit - longNotice
|
|
535
|
+
let sectionBudget = limit - lineCost(longNotice) - 2;
|
|
452
536
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
453
|
-
const sectioned = sectionAwareCut(text, sectionBudget);
|
|
537
|
+
const sectioned = sectionAwareCut(text, sectionBudget, lineCost);
|
|
454
538
|
if (sectioned === null) break;
|
|
455
539
|
const visible = new Set(sectioned.split('\n').map(line => line.trim()));
|
|
456
540
|
const missing = trust.filter(candidate => !visible.has(candidate.line));
|
|
457
|
-
const missingChars = missing.reduce((sum, candidate) => sum + candidate.line
|
|
541
|
+
const missingChars = missing.reduce((sum, candidate) => sum + lineCost(candidate.line) + 1, 0);
|
|
458
542
|
let rendered = `${sectioned}\n\n${longNotice}`;
|
|
459
|
-
if (rendered
|
|
543
|
+
if (textCost(rendered) + missingChars > limit) {
|
|
460
544
|
sectionBudget -= missingChars;
|
|
461
545
|
continue;
|
|
462
546
|
}
|
|
463
547
|
const appended = missing.map(candidate => candidate.line);
|
|
464
548
|
let omitted = 0;
|
|
465
|
-
let used = rendered
|
|
549
|
+
let used = textCost(rendered) + missingChars;
|
|
466
550
|
for (const candidate of optional) {
|
|
467
551
|
if (visible.has(candidate.line)) continue;
|
|
468
|
-
if (used + candidate.line
|
|
552
|
+
if (used + lineCost(candidate.line) + 1 > limit) { omitted++; continue; }
|
|
469
553
|
appended.push(candidate.line);
|
|
470
|
-
used += candidate.line
|
|
554
|
+
used += lineCost(candidate.line) + 1;
|
|
471
555
|
}
|
|
472
556
|
if (appended.length > 0) rendered += '\n' + appended.join('\n');
|
|
473
557
|
return result(rendered, appended, omitted === 0);
|
|
@@ -480,7 +564,8 @@ function applyOutputBudget(text, {
|
|
|
480
564
|
const optionalShare = Math.max(0, Math.min(MAX_PRESERVED_CONTRACT_CHARS, Math.floor(limit * 0.5)) - trustChars);
|
|
481
565
|
return pick(() => layout(longNotice, heading, '\n\n', optionalShare),
|
|
482
566
|
() => layout(compactNotice, '', '\n', optionalShare),
|
|
483
|
-
() => layout(shortNotice, '', '\n', optionalShare)
|
|
567
|
+
() => layout(shortNotice, '', '\n', optionalShare),
|
|
568
|
+
() => layout(minimalNotice, '', '\n', optionalShare)) ||
|
|
484
569
|
(trust.length > 0 ? withheld() : bare());
|
|
485
570
|
}
|
|
486
571
|
|
package/core/parallel-build.js
CHANGED
|
@@ -220,7 +220,7 @@ function parallelBuild(index, files, options = {}) {
|
|
|
220
220
|
{
|
|
221
221
|
for (const result of collected) {
|
|
222
222
|
if (result.error) {
|
|
223
|
-
index.
|
|
223
|
+
index._noteFailedFile(result.filePath);
|
|
224
224
|
if (!options.quiet) {
|
|
225
225
|
console.error(` Warning: Could not index ${result.filePath}: ${result.error}`);
|
|
226
226
|
}
|
|
@@ -236,7 +236,7 @@ function parallelBuild(index, files, options = {}) {
|
|
|
236
236
|
existing.size = result.sizeUpdate;
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
index.
|
|
239
|
+
index._clearFailedFile(result.filePath);
|
|
240
240
|
continue;
|
|
241
241
|
}
|
|
242
242
|
|
|
@@ -267,7 +267,7 @@ function parallelBuild(index, files, options = {}) {
|
|
|
267
267
|
index.callsCacheDirty = true;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
index.
|
|
270
|
+
index._clearFailedFile(result.filePath);
|
|
271
271
|
changed++;
|
|
272
272
|
}
|
|
273
273
|
}
|