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/output-budget.js
CHANGED
|
@@ -16,18 +16,19 @@ const BROAD_COMMANDS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|Note):|\d+ test-file usage\(s\) hidden\b|Found \d+ (?:definitions|fuzzy matches)\b)/;
|
|
19
|
+
// Trust lines: the accounting and contract lines that qualify a caller
|
|
20
|
+
// answer. A truncated answer carries every one of them or none of its body.
|
|
21
|
+
const TRUST_LINE_RE = /^(?:ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):/;
|
|
19
22
|
const MAX_PRESERVED_CONTRACT_LINES = 24;
|
|
20
23
|
const MAX_PRESERVED_CONTRACT_CHARS = 8000;
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Contract metadata candidates of a text, in source order: trust lines
|
|
27
|
+
* (priority 0), the hidden-test-scope sentence (1), and other disclosures
|
|
28
|
+
* (2: Summary, Note, definition counts).
|
|
29
|
+
*/
|
|
30
|
+
function contractCandidates(fullText) {
|
|
24
31
|
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
32
|
for (const [sourceIndex, rawLine] of fullText.split('\n').entries()) {
|
|
32
33
|
// Execution notes may concatenate several independent disclosures on
|
|
33
34
|
// one physical line. Preserve the actionable test-scope contract as
|
|
@@ -44,13 +45,23 @@ function preservedContractMetadata(fullText, visibleText, options = {}) {
|
|
|
44
45
|
const evidenceLine = contractLine.replace(/^\s*# /, '').trim();
|
|
45
46
|
if (!CONTRACT_LINE_RE.test(evidenceLine)) continue;
|
|
46
47
|
const line = contractLine.trim();
|
|
47
|
-
if (!line
|
|
48
|
-
const priority =
|
|
48
|
+
if (!line) continue;
|
|
49
|
+
const priority = TRUST_LINE_RE.test(evidenceLine)
|
|
49
50
|
? 0
|
|
50
51
|
: /^\d+ test-file usage\(s\) hidden\b/.test(evidenceLine) ? 1 : 2;
|
|
51
52
|
candidates.push({ line, priority, sourceIndex });
|
|
52
53
|
}
|
|
54
|
+
return candidates;
|
|
55
|
+
}
|
|
53
56
|
|
|
57
|
+
function preservedContractMetadata(fullText, visibleText, options = {}) {
|
|
58
|
+
const visible = new Set(visibleText.split('\n').map(line => line.trim()));
|
|
59
|
+
const selected = [];
|
|
60
|
+
let selectedChars = 0;
|
|
61
|
+
let omitted = 0;
|
|
62
|
+
const maxLines = options.maxLines ?? MAX_PRESERVED_CONTRACT_LINES;
|
|
63
|
+
const maxChars = options.maxChars ?? MAX_PRESERVED_CONTRACT_CHARS;
|
|
64
|
+
const candidates = contractCandidates(fullText).filter(candidate => !visible.has(candidate.line));
|
|
54
65
|
for (const { line } of candidates.sort((a, b) =>
|
|
55
66
|
a.priority - b.priority || a.sourceIndex - b.sourceIndex)) {
|
|
56
67
|
if (selected.length >= maxLines ||
|
|
@@ -76,6 +87,7 @@ function narrowingHint(command, surface, params = {}) {
|
|
|
76
87
|
usages: 'Use --file or --in to narrow the result.',
|
|
77
88
|
deps: 'Use --depth=1 or --direction=imports|importers.',
|
|
78
89
|
api: 'Use --limit=N to narrow the result.',
|
|
90
|
+
find: 'Use --compact, --limit=N, --type, --file, --in, or --exclude to narrow the result.',
|
|
79
91
|
};
|
|
80
92
|
const mcp = {
|
|
81
93
|
repo: 'Use sections=, in=, or exclude= to narrow the view.',
|
|
@@ -87,6 +99,7 @@ function narrowingHint(command, surface, params = {}) {
|
|
|
87
99
|
usages: 'Use file= or in= to narrow the result.',
|
|
88
100
|
deps: 'Use depth=1 or direction=imports|importers.',
|
|
89
101
|
api: 'Use limit=<n> to narrow the result.',
|
|
102
|
+
find: 'Use compact=true, limit=<n>, type=, file=, in=, or exclude= to narrow the result.',
|
|
90
103
|
};
|
|
91
104
|
const canonical = String(command).replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
92
105
|
if (canonical === 'deps' && params.cycles) {
|
|
@@ -118,10 +131,113 @@ function compactNarrowingHint(command, surface, params = {}) {
|
|
|
118
131
|
const flags = full.match(/--[a-z-]+(?:=N)?/g) || [];
|
|
119
132
|
return [...new Set(flags)].join('/');
|
|
120
133
|
}
|
|
121
|
-
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) || [];
|
|
134
|
+
const flags = full.match(/\b(?:sections|in|exclude|framework|prefix|method|server_only|client_only|file|limit|depth|direction|max_chars|compact|type)(?:=<n>|=true|=imports\|importers|=1|=)?/g) || [];
|
|
122
135
|
return [...new Set(flags)].join('/');
|
|
123
136
|
}
|
|
124
137
|
|
|
138
|
+
// fix #367g: tier-section headers of the relationship answers
|
|
139
|
+
// (`CALLERS — CONFIRMED (12, ...)`, `CALLERS — RUNTIME DISPATCH (332 sites`,
|
|
140
|
+
// `UNVERIFIED CALL SITES (627) —`, `CALLEES (3):`, `BY FILE:`).
|
|
141
|
+
const SECTION_HEADER_RE = /^(?:[A-Z][A-Z-]*(?: [A-Z][A-Z-]*)*(?: — [A-Z][A-Z -]*)? \(\d[^)]*\)|[A-Z][A-Z ]+:$)/;
|
|
142
|
+
const SECTION_AWARE_COMMANDS = new Set(['show', 'impact']);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Cut a relationship answer to `budget` chars keeping a representative head
|
|
146
|
+
* of EVERY tier section instead of the first sections only: a long CONFIRMED
|
|
147
|
+
* list must not push the UNVERIFIED band (or the excluded summary) out of
|
|
148
|
+
* the answer. Section bodies are the indented lines under a header; lines
|
|
149
|
+
* are handed out round-robin across sections, each cut section ends with a
|
|
150
|
+
* `+N more` line, and the non-indented lines around sections (context,
|
|
151
|
+
* ACCOUNT/CONTRACT/WARNING) stay in place. Returns null when the text has
|
|
152
|
+
* no sections or its fixed parts alone do not fit (caller falls back to a
|
|
153
|
+
* head cut).
|
|
154
|
+
*/
|
|
155
|
+
function sectionAwareCut(text, budget) {
|
|
156
|
+
const lines = text.split('\n');
|
|
157
|
+
const blocks = []; // { kind: 'fixed', line } | { kind: 'section', header, body }
|
|
158
|
+
let current = null;
|
|
159
|
+
let sawSection = false;
|
|
160
|
+
for (const line of lines) {
|
|
161
|
+
if (SECTION_HEADER_RE.test(line)) {
|
|
162
|
+
current = { kind: 'section', header: line, body: [] };
|
|
163
|
+
blocks.push(current);
|
|
164
|
+
sawSection = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (current && /^\s/.test(line) && line.trim()) {
|
|
168
|
+
current.body.push(line);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
current = null;
|
|
172
|
+
blocks.push({ kind: 'fixed', line });
|
|
173
|
+
}
|
|
174
|
+
const sections = blocks.filter(block => block.kind === 'section');
|
|
175
|
+
if (!sawSection || sections.every(section => section.body.length === 0)) return null;
|
|
176
|
+
const moreLine = count => ` ... +${count} more`;
|
|
177
|
+
const gapLine = ' ...';
|
|
178
|
+
let used = 0;
|
|
179
|
+
for (const block of blocks) {
|
|
180
|
+
used += (block.kind === 'fixed' ? block.line.length : block.header.length) + 1;
|
|
181
|
+
}
|
|
182
|
+
// Reserve the worst-case `+N more` line and one gap marker per section.
|
|
183
|
+
for (const section of sections) {
|
|
184
|
+
used += moreLine(section.body.length).length + gapLine.length + 2;
|
|
185
|
+
}
|
|
186
|
+
if (used > budget) return null;
|
|
187
|
+
// Selection order per section: the first numbered site entry and the
|
|
188
|
+
// sub-headers above it come first, so a long preamble list (competing
|
|
189
|
+
// definitions) cannot crowd every site out; then lines in source order.
|
|
190
|
+
const indentOf = line => line.length - line.trimStart().length;
|
|
191
|
+
const orders = new Map();
|
|
192
|
+
for (const section of sections) {
|
|
193
|
+
const first = section.body.findIndex(line => /^\s+\[\d+\]/.test(line));
|
|
194
|
+
const order = [];
|
|
195
|
+
if (first > 0) {
|
|
196
|
+
const chain = [];
|
|
197
|
+
let indent = indentOf(section.body[first]);
|
|
198
|
+
for (let i = first - 1; i >= 0 && indent > 0; i--) {
|
|
199
|
+
const lineIndent = indentOf(section.body[i]);
|
|
200
|
+
if (lineIndent < indent) { chain.unshift(i); indent = lineIndent; }
|
|
201
|
+
}
|
|
202
|
+
order.push(...chain, first);
|
|
203
|
+
}
|
|
204
|
+
const seen = new Set(order);
|
|
205
|
+
for (let i = 0; i < section.body.length; i++) if (!seen.has(i)) order.push(i);
|
|
206
|
+
orders.set(section, { order, next: 0, taken: new Set() });
|
|
207
|
+
}
|
|
208
|
+
let progress = true;
|
|
209
|
+
while (progress) {
|
|
210
|
+
progress = false;
|
|
211
|
+
for (const section of sections) {
|
|
212
|
+
const state = orders.get(section);
|
|
213
|
+
if (state.next >= state.order.length) continue;
|
|
214
|
+
const index = state.order[state.next];
|
|
215
|
+
const cost = section.body[index].length + 1;
|
|
216
|
+
if (used + cost > budget) { state.next = state.order.length; continue; }
|
|
217
|
+
used += cost;
|
|
218
|
+
state.taken.add(index);
|
|
219
|
+
state.next++;
|
|
220
|
+
progress = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const block of blocks) {
|
|
225
|
+
if (block.kind === 'fixed') { out.push(block.line); continue; }
|
|
226
|
+
out.push(block.header);
|
|
227
|
+
const { taken } = orders.get(block);
|
|
228
|
+
let last = -1;
|
|
229
|
+
for (let i = 0; i < block.body.length; i++) {
|
|
230
|
+
if (!taken.has(i)) continue;
|
|
231
|
+
if (i > last + 1 && last >= 0) out.push(gapLine);
|
|
232
|
+
out.push(block.body[i]);
|
|
233
|
+
last = i;
|
|
234
|
+
}
|
|
235
|
+
const omitted = block.body.length - taken.size;
|
|
236
|
+
if (omitted > 0) out.push(moreLine(omitted));
|
|
237
|
+
}
|
|
238
|
+
return out.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
125
241
|
/**
|
|
126
242
|
* Apply the same bounded-output contract to CLI and MCP text. JSON is not
|
|
127
243
|
* passed here: structured consumers receive the complete stable envelope.
|
|
@@ -179,127 +295,193 @@ function applyOutputBudget(text, {
|
|
|
179
295
|
const compactGuidance = compactScope.includes(raiseHint)
|
|
180
296
|
? `Raise ${raiseHint}.`
|
|
181
297
|
: `Narrow with ${compactScope || raiseHint}; raise ${raiseHint}.`;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (compactBudget && notice.length > limit) {
|
|
187
|
-
const emergency = supportsAll
|
|
188
|
-
? (surface === 'mcp'
|
|
189
|
-
? 'all=true/max_chars=<n>'
|
|
190
|
-
: '--all/--max-chars=N')
|
|
191
|
-
: raiseHint;
|
|
192
|
-
notice = emergency.slice(0, limit);
|
|
193
|
-
}
|
|
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}.`;
|
|
194
302
|
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
303
|
+
// Trust lines (ACCOUNT/CONTRACT/WARNING/...) qualify the answer: room for
|
|
304
|
+
// every one of them is reserved before any body text. Optional
|
|
305
|
+
// disclosures (hidden test scope, notes, summaries) fill what is left.
|
|
306
|
+
const candidates = contractCandidates(text);
|
|
307
|
+
const trust = candidates.filter(candidate => candidate.priority === 0);
|
|
308
|
+
const optional = candidates.filter(candidate => candidate.priority > 0)
|
|
309
|
+
.sort((a, b) => a.priority - b.priority || a.sourceIndex - b.sourceIndex);
|
|
310
|
+
const trustChars = trust.reduce((sum, candidate) => sum + candidate.line.length + 1, 0);
|
|
311
|
+
const result = (rendered, appended, complete) => ({
|
|
312
|
+
text: rendered,
|
|
313
|
+
truncated: true,
|
|
314
|
+
fullChars: text.length,
|
|
315
|
+
requestedLimit: hardLimit,
|
|
316
|
+
contractMetadata: appended,
|
|
317
|
+
contractMetadataComplete: complete,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Head cut of the body to `budget` chars; returns the kept text and the
|
|
321
|
+
// number of complete source lines it holds.
|
|
322
|
+
const headCut = (budget) => {
|
|
323
|
+
if (budget <= 0) return { body: '', keptLines: 0 };
|
|
324
|
+
const prefix = text.slice(0, budget);
|
|
325
|
+
const lastNewline = prefix.lastIndexOf('\n');
|
|
326
|
+
const body = lastNewline > budget * 0.8 ? prefix.slice(0, lastNewline) : prefix;
|
|
327
|
+
const newlines = body.split('\n').length - 1;
|
|
328
|
+
// The last line of the kept body is complete only when the cut fell
|
|
329
|
+
// on a line boundary.
|
|
330
|
+
const lastComplete = body.length === text.length || text[body.length] === '\n';
|
|
331
|
+
return { body, keptLines: newlines + (body && lastComplete ? 1 : 0) };
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Render one layout: body, notice, then the metadata the body does not
|
|
335
|
+
// show. Every trust line is reserved first; optional disclosures get up
|
|
336
|
+
// to `optionalShare` chars (priority order) before the body; the body
|
|
337
|
+
// takes the rest. Returns null when the notice and the trust lines alone
|
|
338
|
+
// exceed the limit.
|
|
339
|
+
const costOf = list => list.reduce((sum, candidate) => sum + candidate.line.length + 1, 0);
|
|
340
|
+
const layout = (notice, heading, separator, optionalShare) => {
|
|
341
|
+
const reserveFor = metadataChars => notice.length + (metadataChars > 0 ? heading.length + metadataChars : 0);
|
|
342
|
+
if (reserveFor(trustChars) > limit) return null;
|
|
343
|
+
const reservedOptional = [];
|
|
344
|
+
let optionalChars = 0;
|
|
345
|
+
const optionalRoom = Math.min(optionalShare, limit - reserveFor(trustChars) - (trustChars > 0 ? 0 : heading.length));
|
|
346
|
+
for (const candidate of optional) {
|
|
347
|
+
if (reservedOptional.length >= MAX_PRESERVED_CONTRACT_LINES) break;
|
|
348
|
+
if (optionalChars + candidate.line.length + 1 > optionalRoom) continue;
|
|
349
|
+
reservedOptional.push(candidate);
|
|
350
|
+
optionalChars += candidate.line.length + 1;
|
|
351
|
+
}
|
|
352
|
+
const reserved = [...trust, ...reservedOptional];
|
|
353
|
+
const pendingOf = cutState => reserved.filter(candidate => candidate.sourceIndex >= cutState.keptLines);
|
|
354
|
+
let cut = headCut(limit - reserveFor(costOf(reserved)) - separator.length);
|
|
355
|
+
// Reserved lines the kept body already shows need no room; spend it
|
|
356
|
+
// on the body (one refinement pass; a wider cut only shows more).
|
|
357
|
+
const firstPending = costOf(pendingOf(cut));
|
|
358
|
+
if (firstPending < costOf(reserved)) {
|
|
359
|
+
const wider = headCut(limit - reserveFor(firstPending) - separator.length);
|
|
360
|
+
if (costOf(pendingOf(wider)) <= firstPending) cut = wider;
|
|
361
|
+
}
|
|
362
|
+
const metadata = pendingOf(cut);
|
|
363
|
+
let rendered = cut.body ? `${cut.body}${separator}${notice}` : notice;
|
|
364
|
+
let used = rendered.length + (metadata.length > 0 ? heading.length + costOf(metadata) : 0);
|
|
365
|
+
let omitted = 0;
|
|
366
|
+
const taken = new Set(reserved);
|
|
367
|
+
for (const candidate of optional) {
|
|
368
|
+
if (taken.has(candidate) || candidate.sourceIndex < cut.keptLines) continue;
|
|
369
|
+
const cost = candidate.line.length + 1 + (metadata.length === 0 ? heading.length : 0);
|
|
370
|
+
if (metadata.length >= trust.length + MAX_PRESERVED_CONTRACT_LINES || used + cost > limit) {
|
|
371
|
+
omitted++;
|
|
372
|
+
continue;
|
|
212
373
|
}
|
|
374
|
+
metadata.push(candidate);
|
|
375
|
+
used += cost;
|
|
213
376
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const remainingForMetadata = Math.max(0,
|
|
224
|
-
limit - cleanCut.length - notice.length -
|
|
225
|
-
(cleanCut ? 1 : 0) - 1);
|
|
226
|
-
const contractMetadata = preservedContractMetadata(text, cleanCut, {
|
|
227
|
-
// preservedContractMetadata accounts a trailing newline per item;
|
|
228
|
-
// the renderer joins the final item without one.
|
|
229
|
-
maxChars: remainingForMetadata + 1,
|
|
230
|
-
});
|
|
231
|
-
const pieces = [];
|
|
232
|
-
if (cleanCut) pieces.push(cleanCut);
|
|
233
|
-
pieces.push(notice);
|
|
234
|
-
if (contractMetadata.lines.length > 0) {
|
|
235
|
-
pieces.push(contractMetadata.lines.join('\n'));
|
|
377
|
+
const appended = [];
|
|
378
|
+
if (metadata.length > 0) {
|
|
379
|
+
// Trust lines and optional disclosures keep their source order.
|
|
380
|
+
metadata.sort((x, y) => x.sourceIndex - y.sourceIndex);
|
|
381
|
+
rendered += heading;
|
|
382
|
+
for (const candidate of metadata) {
|
|
383
|
+
rendered += '\n' + candidate.line;
|
|
384
|
+
appended.push(candidate.line);
|
|
385
|
+
}
|
|
236
386
|
}
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
truncated: true,
|
|
240
|
-
fullChars: text.length,
|
|
241
|
-
requestedLimit: hardLimit,
|
|
242
|
-
contractMetadata: contractMetadata.lines,
|
|
243
|
-
contractMetadataComplete: contractMetadata.complete,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
387
|
+
return result(rendered, appended, omitted === 0);
|
|
388
|
+
};
|
|
246
389
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
390
|
+
// The notice and the trust lines alone do not fit: the answer is
|
|
391
|
+
// withheld rather than shown without its contract, and the notice says
|
|
392
|
+
// so with the budget that would carry them.
|
|
393
|
+
const withheld = () => {
|
|
394
|
+
const flag = surface === 'mcp' ? 'max_chars' : '--max-chars';
|
|
395
|
+
const noticeFor = (shown, needed) => `... OUTPUT TRUNCATED (${text.length}→${hardLimit}): answer withheld; ` +
|
|
396
|
+
`${shown} of ${trust.length} trust line(s) shown. Raise ${flag} to at least ${needed}.`;
|
|
397
|
+
// Smallest budget whose shortest layout carries every trust line.
|
|
398
|
+
const needed = trailingChars + shortNotice.length + trustChars;
|
|
399
|
+
let shown = 0;
|
|
400
|
+
let used = noticeFor(trust.length, needed).length;
|
|
401
|
+
const kept = [];
|
|
402
|
+
for (const candidate of trust) {
|
|
403
|
+
if (used + candidate.line.length + 1 > limit) break;
|
|
404
|
+
kept.push(candidate.line);
|
|
405
|
+
used += candidate.line.length + 1;
|
|
406
|
+
shown++;
|
|
407
|
+
}
|
|
408
|
+
const notice = noticeFor(shown, needed);
|
|
409
|
+
if (notice.length > limit) {
|
|
410
|
+
// Shorter statements of the same fact; a budget number is never
|
|
411
|
+
// cut mid-digit.
|
|
412
|
+
const shorter = [
|
|
413
|
+
`TRUNCATED: answer withheld; raise ${flag} to at least ${needed}.`,
|
|
414
|
+
`Answer withheld; ${flag}>=${needed}`,
|
|
415
|
+
`withheld; >=${needed}`,
|
|
416
|
+
].find(text => text.length <= limit);
|
|
417
|
+
return result(shorter || '', [], false);
|
|
418
|
+
}
|
|
419
|
+
return result([notice, ...kept].join('\n'), kept, false);
|
|
420
|
+
};
|
|
277
421
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
422
|
+
// At tiny limits with no trust line to carry, the notice itself is the
|
|
423
|
+
// answer: the flag that raises the budget.
|
|
424
|
+
const bare = () => result((supportsAll
|
|
425
|
+
? (surface === 'mcp' ? 'all=true/max_chars=<n>' : '--all/--max-chars=N')
|
|
426
|
+
: raiseHint).slice(0, limit), [], optional.length === 0);
|
|
427
|
+
|
|
428
|
+
// First layout that carries every disclosure, else the one carrying the
|
|
429
|
+
// most (a shorter notice can leave room for a scope warning).
|
|
430
|
+
const pick = (...attempts) => {
|
|
431
|
+
let best = null;
|
|
432
|
+
for (const attempt of attempts) {
|
|
433
|
+
const rendered = attempt();
|
|
434
|
+
if (!rendered) continue;
|
|
435
|
+
if (rendered.contractMetadataComplete) return rendered;
|
|
436
|
+
if (!best || rendered.contractMetadata.length > best.contractMetadata.length) best = rendered;
|
|
284
437
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
438
|
+
return best;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
if (compactBudget) {
|
|
442
|
+
// Tiny transports: every disclosure outranks body detail.
|
|
443
|
+
return pick(() => layout(compactNotice, '', '\n', limit), () => layout(shortNotice, '', '\n', limit)) ||
|
|
444
|
+
(trust.length > 0 ? withheld() : bare());
|
|
290
445
|
}
|
|
291
446
|
|
|
292
|
-
|
|
293
|
-
|
|
447
|
+
if (SECTION_AWARE_COMMANDS.has(canonical)) {
|
|
448
|
+
// Sections keep every non-indented line (trust lines included) in
|
|
449
|
+
// place; trust lines inside a cut section body are appended, with
|
|
450
|
+
// their room taken from the sections.
|
|
451
|
+
let sectionBudget = limit - longNotice.length - 2;
|
|
452
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
453
|
+
const sectioned = sectionAwareCut(text, sectionBudget);
|
|
454
|
+
if (sectioned === null) break;
|
|
455
|
+
const visible = new Set(sectioned.split('\n').map(line => line.trim()));
|
|
456
|
+
const missing = trust.filter(candidate => !visible.has(candidate.line));
|
|
457
|
+
const missingChars = missing.reduce((sum, candidate) => sum + candidate.line.length + 1, 0);
|
|
458
|
+
let rendered = `${sectioned}\n\n${longNotice}`;
|
|
459
|
+
if (rendered.length + missingChars > limit) {
|
|
460
|
+
sectionBudget -= missingChars;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const appended = missing.map(candidate => candidate.line);
|
|
464
|
+
let omitted = 0;
|
|
465
|
+
let used = rendered.length + missingChars;
|
|
466
|
+
for (const candidate of optional) {
|
|
467
|
+
if (visible.has(candidate.line)) continue;
|
|
468
|
+
if (used + candidate.line.length + 1 > limit) { omitted++; continue; }
|
|
469
|
+
appended.push(candidate.line);
|
|
470
|
+
used += candidate.line.length + 1;
|
|
471
|
+
}
|
|
472
|
+
if (appended.length > 0) rendered += '\n' + appended.join('\n');
|
|
473
|
+
return result(rendered, appended, omitted === 0);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
294
476
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
477
|
+
const heading = '\n\nPRESERVED CONTRACT METADATA (from omitted output):';
|
|
478
|
+
// Optional disclosures share at most half of the transport with the
|
|
479
|
+
// trust lines, as the preservation budget always did.
|
|
480
|
+
const optionalShare = Math.max(0, Math.min(MAX_PRESERVED_CONTRACT_CHARS, Math.floor(limit * 0.5)) - trustChars);
|
|
481
|
+
return pick(() => layout(longNotice, heading, '\n\n', optionalShare),
|
|
482
|
+
() => layout(compactNotice, '', '\n', optionalShare),
|
|
483
|
+
() => layout(shortNotice, '', '\n', optionalShare)) ||
|
|
484
|
+
(trust.length > 0 ? withheld() : bare());
|
|
303
485
|
}
|
|
304
486
|
|
|
305
487
|
module.exports = {
|
|
@@ -308,4 +490,5 @@ module.exports = {
|
|
|
308
490
|
MAX_OUTPUT_CHARS,
|
|
309
491
|
applyOutputBudget,
|
|
310
492
|
preservedContractMetadata,
|
|
493
|
+
sectionAwareCut,
|
|
311
494
|
};
|