@codedology/code-forge 0.7.2 → 0.7.4
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/CHANGELOG.md +20 -0
- package/README.md +15 -0
- package/docs/reference/blocks.json +33 -1
- package/package.json +1 -1
- package/skill/references/review.md +2 -2
- package/skill/scripts/forge +1 -1
- package/src/autopilot/binnacle.mjs +29 -1
- package/src/review/engine.mjs +135 -33
- package/src/review/fixloop.mjs +146 -33
- package/src/review/moved.mjs +399 -0
- package/src/review/packet.mjs +32 -23
- package/src/review/schema-fallback.mjs +351 -0
- package/src/review/sections.mjs +5 -4
- package/src/review/validate-review.mjs +11 -0
- package/src/session/spawn.mjs +3 -86
- package/src/util/diagnostic-tail.mjs +95 -0
- package/src/worker/engine.mjs +78 -8
package/src/review/fixloop.mjs
CHANGED
|
@@ -59,6 +59,19 @@
|
|
|
59
59
|
* findings get ids `S<section>.<id>`; any section session unavailable ⇒ `retry` of the whole
|
|
60
60
|
* round. A section still over the budget alone ⇒ the terminal stop above (`split_required`).
|
|
61
61
|
*
|
|
62
|
+
* Moved code (B56): when `deps.peerDiffs` (or `deps.peers`) names the block's other changed files,
|
|
63
|
+
* every recheck and patch_check packet carries the file's `## moved code` section — computed from
|
|
64
|
+
* the file's diff against the block base (`deps.base`), never from the fix hunks, so a stale
|
|
65
|
+
* "feature deleted" finding on code that moved to another file meets the same hint as round 1. A
|
|
66
|
+
* Markdown section packet lists only the moves in its section. Sizing (`recheckTokens`,
|
|
67
|
+
* `packRecheck`) uses the same section text. The peers are read once per deps object (one ticket):
|
|
68
|
+
* the caller's `deps.peerDiffs` reader, else one memoised reader per deps (`recheckPeerDiffs`). Any
|
|
69
|
+
* failure there means no hint, never a failed recheck.
|
|
70
|
+
*
|
|
71
|
+
* Schema misses (B55): a recheck (or patch_check) session whose answer fails the schema at its
|
|
72
|
+
* packet's second miss is tried once on `review.second_levels.L2` when configured
|
|
73
|
+
* (`schema-fallback.mjs`); only when that fails too is the round `unavailable` as below.
|
|
74
|
+
*
|
|
62
75
|
* A session that fails the stub guard is `unavailable`, and a failed write of a gate-relevant row
|
|
63
76
|
* (`review.late_finding`, `review.round`, `review.cap`) is `ledger_write_failed`: either way the
|
|
64
77
|
* round is not counted, nothing closes, and `next.action: 'retry'` with `pending_kind` set —
|
|
@@ -75,10 +88,12 @@ import { decisionProblem, grantCoverProblem } from './gate-check.mjs';
|
|
|
75
88
|
import { contentHash, gitChildEnv } from '../worker/ticket.mjs';
|
|
76
89
|
import { parseDiff } from './context.mjs';
|
|
77
90
|
import { assemblePacket, assertPacketPath, diffOnlyTokens, readFileDiff } from './packet.mjs';
|
|
91
|
+
import { movedText, movesFor, peerDiffReader } from './moved.mjs';
|
|
78
92
|
import { isMarkdownFile, packSections, sectionFindings, sectionGroups, sectionOf } from './sections.mjs';
|
|
79
93
|
import { cameFromJudge, resolvedBand, triageFindings } from './triage.mjs';
|
|
80
94
|
import { FINDING_SCHEMA, minTokensOut, validateReview } from './validate-review.mjs';
|
|
81
95
|
import { spawnWithTimeoutRetry } from './session-retry.mjs';
|
|
96
|
+
import { afterSchemaMiss, logSecondLevelMiss } from './schema-fallback.mjs';
|
|
82
97
|
|
|
83
98
|
/** @typedef {import('./triage.mjs').Finding} Finding */
|
|
84
99
|
/** @typedef {import('./context.mjs').Hunk} Hunk */
|
|
@@ -123,9 +138,12 @@ import { spawnWithTimeoutRetry } from './session-retry.mjs';
|
|
|
123
138
|
* @property {(row: Record<string, any>) => Promise<unknown>} [writeRow]
|
|
124
139
|
* @property {(req: {file: string, round: number, level: string, open: Finding[], l3_patch: boolean}) => Promise<unknown>} [fix] - a fresh coder session at `level`; `l3_patch` = the block's L3 patch is in the tree (§3.6).
|
|
125
140
|
* @property {(req: {file: string, level: 'L3', open: Finding[]}) => Promise<unknown>} [patch] - the L3 patch rung (≤ 80 lines, owned files only).
|
|
126
|
-
* @property {string | null} [base] - the block base (
|
|
141
|
+
* @property {string | null} [base] - the block base (`recheck_scope: file`, and the B56 moved-code diff).
|
|
142
|
+
* @property {ReadonlyArray<string>} [peers] - B56: the block's other changed files (moved-code hints).
|
|
143
|
+
* @property {() => Promise<import('./moved.mjs').PeerDiff[]>} [peerDiffs] - B56: a memoised reader of their diffs (`peerDiffReader`); wins over `peers`.
|
|
127
144
|
* @property {string} [rulesDigest] @property {string} [factsExcerpt]
|
|
128
145
|
* @property {number} [extraRounds] - B47: rounds past `review.max_rounds_per_file` granted by autopilot (`extraRoundsFor`).
|
|
146
|
+
* @property {import('./schema-fallback.mjs').SchemaHistory} [schemaHistory] - B55: the packet's signed schema-miss history; absent ⇒ no second-level try.
|
|
129
147
|
*/
|
|
130
148
|
|
|
131
149
|
/** @param {LoopDeps} deps @param {Record<string, any>} row */
|
|
@@ -164,11 +182,14 @@ export function mergePacketInfo(info, packet) {
|
|
|
164
182
|
* or hunks that do not parse one to one) ⇒ `split_required` at the whole size, never sectioned;
|
|
165
183
|
* a finding no packet holds is listed in the LAST packet — every open finding is listed once.
|
|
166
184
|
* @param {import('./packet.mjs').FileDiff} diff @param {Finding[]} open @param {number} budget
|
|
185
|
+
* @param {import('./moved.mjs').Moves | null} [moves] - the file's moves (B56): each candidate is sized
|
|
186
|
+
* with the `## moved code` text of ITS sections (`movedText`), the whole size with all of them.
|
|
167
187
|
* @returns {{status: 'ok', sections: Array<import('./sections.mjs').Section & {open: Finding[]}>} | {status: 'split_required', tokensIn: number, budget: number, section: string | null}}
|
|
168
188
|
*/
|
|
169
|
-
export function packRecheck(diff, open, budget) {
|
|
189
|
+
export function packRecheck(diff, open, budget, moves = null) {
|
|
190
|
+
const moved = movedText(moves, diff.content, null);
|
|
170
191
|
const groups = sectionGroups(diff);
|
|
171
|
-
if (groups === null || groups.length === 0) return { status: 'split_required', tokensIn: recheckTokens(diff, open), budget, section: null };
|
|
192
|
+
if (groups === null || groups.length === 0) return { status: 'split_required', tokensIn: recheckTokens(diff, open, moved), budget, section: null };
|
|
172
193
|
/** @type {Map<Finding, number>} */
|
|
173
194
|
const keyOf = new Map();
|
|
174
195
|
for (const f of open) {
|
|
@@ -177,10 +198,10 @@ export function packRecheck(diff, open, budget) {
|
|
|
177
198
|
}
|
|
178
199
|
/** @param {ReadonlyArray<number>} keys @returns {Finding[]} the open findings of those groups, in order. */
|
|
179
200
|
const openIn = (keys) => open.filter((f) => keyOf.has(f) && keys.includes(/** @type {number} */ (keyOf.get(f))));
|
|
180
|
-
const packed = packSections({ diff, budget, measure: (part, set) => recheckTokens(part, openIn(set.map((g) => g.key))) });
|
|
201
|
+
const packed = packSections({ diff, budget, measure: (part, set) => (set === null ? recheckTokens(part, open, moved) : recheckTokens(part, openIn(set.map((g) => g.key)), movedText(moves, diff.content, set.map((g) => g.key)))) });
|
|
181
202
|
if (packed.status !== 'ok') return packed;
|
|
182
203
|
const sections = packed.sections.map((sec) => ({ ...sec, open: openIn(sec.keys) }));
|
|
183
|
-
if (sections.length === 0) return { status: 'split_required', tokensIn: recheckTokens(diff, open), budget, section: null };
|
|
204
|
+
if (sections.length === 0) return { status: 'split_required', tokensIn: recheckTokens(diff, open, moved), budget, section: null };
|
|
184
205
|
// defensive: a finding no packet holds goes to the last packet (it is sized there by the check
|
|
185
206
|
// every packet passes before any session) — never dropped, never closed unseen
|
|
186
207
|
const listed = new Set(sections.flatMap((sec) => sec.open));
|
|
@@ -326,24 +347,77 @@ const openFindingsText = (open) => `${OPEN_FINDINGS_HEAD}${open.length > 0 ? ope
|
|
|
326
347
|
* The size a recheck packet is held to (B54): its diff-only `recheck` packet (the measure behind
|
|
327
348
|
* `split_required`) plus the open-findings list `buildRecheckPacket` appends. Section packing and
|
|
328
349
|
* the check before any session use this ONE function.
|
|
329
|
-
* @param {import('./packet.mjs').FileDiff} diff @param {ReadonlyArray<Finding>} open
|
|
350
|
+
* @param {import('./packet.mjs').FileDiff} diff @param {ReadonlyArray<Finding>} open
|
|
351
|
+
* @param {string} [moved] - the `## moved code` section the packet carries (B56).
|
|
352
|
+
* @returns {number}
|
|
330
353
|
*/
|
|
331
|
-
export function recheckTokens(diff, open) {
|
|
332
|
-
return diffOnlyTokens({ diff, lens: 'recheck' }) + Math.ceil(Buffer.byteLength(openFindingsText(open)) / 4);
|
|
354
|
+
export function recheckTokens(diff, open, moved = '') {
|
|
355
|
+
return diffOnlyTokens({ diff, lens: 'recheck', moved }) + Math.ceil(Buffer.byteLength(openFindingsText(open)) / 4);
|
|
333
356
|
}
|
|
334
357
|
|
|
335
358
|
/**
|
|
336
359
|
* The recheck packet: the `recheck` lens on the fix hunks (hunk form, never the whole file), then
|
|
337
360
|
* the open findings by id. Over budget ⇒ `minimal` (see the module doc).
|
|
338
|
-
* @param {{diff: import('./packet.mjs').FileDiff, open: Finding[], cfg?: Record<string, any>, contextMode?: 'auto' | 'recheck', rulesDigest?: string, factsExcerpt?: string}} opts
|
|
361
|
+
* @param {{diff: import('./packet.mjs').FileDiff, open: Finding[], cfg?: Record<string, any>, contextMode?: 'auto' | 'recheck', rulesDigest?: string, factsExcerpt?: string, moved?: string}} opts -
|
|
362
|
+
* `moved`: the file's `## moved code` section (B56), '' for none.
|
|
339
363
|
*/
|
|
340
|
-
export function buildRecheckPacket({ diff, open, cfg, contextMode = 'recheck', rulesDigest, factsExcerpt }) {
|
|
341
|
-
const packet = assemblePacket({ diff, lens: 'recheck', cfg, contextMode, rulesDigest, factsExcerpt });
|
|
364
|
+
export function buildRecheckPacket({ diff, open, cfg, contextMode = 'recheck', rulesDigest, factsExcerpt, moved = '' }) {
|
|
365
|
+
const packet = assemblePacket({ diff, lens: 'recheck', cfg, contextMode, rulesDigest, factsExcerpt, moved });
|
|
342
366
|
if (packet.status !== 'ok') return packet;
|
|
343
367
|
const text = `${packet.text}${openFindingsText(open)}`;
|
|
344
368
|
return { ...packet, text, tokensIn: packet.tokensIn + Math.ceil(Buffer.byteLength(text.slice(packet.text.length)) / 4) };
|
|
345
369
|
}
|
|
346
370
|
|
|
371
|
+
/**
|
|
372
|
+
* B56: the peer readers built from `deps.peers` (no `deps.peerDiffs`): per deps object, one reader
|
|
373
|
+
* per (repoRoot, base, file, peers) key, so A → B → A on one deps reads A's peers once.
|
|
374
|
+
* @type {WeakMap<object, Map<string, () => Promise<import('./moved.mjs').PeerDiff[]>>>}
|
|
375
|
+
*/
|
|
376
|
+
const peerReaders = new WeakMap();
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The peers' diffs reader of a recheck: `deps.peerDiffs` when the caller gave one, else ONE
|
|
380
|
+
* memoised `peerDiffReader` per deps object and (repoRoot, base, file, peers) key, so the peers are
|
|
381
|
+
* read once however many rechecks run with the same deps (one ticket), whatever the order.
|
|
382
|
+
* @param {LoopDeps} deps @param {string} base @param {string} rel
|
|
383
|
+
* @returns {() => Promise<import('./moved.mjs').PeerDiff[]>}
|
|
384
|
+
*/
|
|
385
|
+
function recheckPeerDiffs(deps, base, rel) {
|
|
386
|
+
if (deps.peerDiffs) return deps.peerDiffs;
|
|
387
|
+
const key = JSON.stringify([deps.repoRoot, base, rel, deps.peers ?? null]);
|
|
388
|
+
let readers = peerReaders.get(deps);
|
|
389
|
+
if (!readers) {
|
|
390
|
+
readers = new Map();
|
|
391
|
+
peerReaders.set(deps, readers);
|
|
392
|
+
}
|
|
393
|
+
const known = readers.get(key);
|
|
394
|
+
if (known) return known;
|
|
395
|
+
const reader = peerDiffReader({ repoRoot: deps.repoRoot, base, file: rel, peers: deps.peers });
|
|
396
|
+
readers.set(key, reader);
|
|
397
|
+
return reader;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The recheck's moves (B56): from the file's diff against the block base (the one already read
|
|
402
|
+
* for `recheck_scope: file`, else read here), never from the fix hunks; the peers' diffs from the
|
|
403
|
+
* memoised reader (`recheckPeerDiffs`). No base, no peer, or ANY failure (building the reader
|
|
404
|
+
* included) ⇒ null: no hint, the recheck goes on.
|
|
405
|
+
* @param {LoopDeps} deps @param {string} rel @param {import('./packet.mjs').FileDiff | null} baseDiff
|
|
406
|
+
* @returns {Promise<import('./moved.mjs').Moves | null>}
|
|
407
|
+
*/
|
|
408
|
+
async function recheckMoves(deps, rel, baseDiff) {
|
|
409
|
+
try {
|
|
410
|
+
if (typeof deps.base !== 'string') return null;
|
|
411
|
+
const peerDiffs = recheckPeerDiffs(deps, deps.base, rel);
|
|
412
|
+
// nothing to read when there is no peer: the base diff is read only for a real peer set
|
|
413
|
+
if ((await peerDiffs()).length === 0) return null;
|
|
414
|
+
const diff = baseDiff ?? (await readFileDiff({ repoRoot: deps.repoRoot, file: rel, base: deps.base }));
|
|
415
|
+
return await movesFor({ diff, peerDiffs });
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
347
421
|
/** @param {Finding} f @param {ReadonlyArray<Hunk>} hunks @returns {boolean} */
|
|
348
422
|
export function insideHunks(f, hunks) {
|
|
349
423
|
return hunks.some((h) => {
|
|
@@ -353,7 +427,8 @@ export function insideHunks(f, hunks) {
|
|
|
353
427
|
}
|
|
354
428
|
|
|
355
429
|
/**
|
|
356
|
-
* One recheck session through the stub guard
|
|
430
|
+
* One recheck session through the stub guard — and, on a `schema` miss, the B55 second-level try
|
|
431
|
+
* on the same packet (`schema-fallback.mjs`; rechecks and the patch_check alike).
|
|
357
432
|
* @param {{text: string, hunkHeaders: string[], tokensIn: number, contextMode: string}} packet
|
|
358
433
|
* @param {LoopDeps} deps
|
|
359
434
|
*/
|
|
@@ -361,26 +436,62 @@ async function recheckSession(packet, deps) {
|
|
|
361
436
|
mkdirSync(deps.workDir, { recursive: true, mode: 0o700 });
|
|
362
437
|
const promptPath = path.join(deps.workDir, `recheck-${randomBytes(6).toString('hex')}.md`);
|
|
363
438
|
writeFileSync(promptPath, packet.text, { mode: 0o600 });
|
|
439
|
+
const min = minTokensOut(deps.cfg);
|
|
440
|
+
const session = { lens: 'recheck', role: 'reviewer', level: 'L2' };
|
|
441
|
+
/** @param {Record<string, any> | undefined} cfg */
|
|
442
|
+
const attempt = async (cfg) =>
|
|
443
|
+
deps.spawn
|
|
444
|
+
? // a `timeout` is spawned once more on the same packet and level (B30)
|
|
445
|
+
spawnWithTimeoutRetry(
|
|
446
|
+
deps.spawn,
|
|
447
|
+
{ level: 'L2', role: 'reviewer', promptPath, schema: FINDING_SCHEMA, rowExtra: { lens: 'recheck', context_mode: packet.contextMode, ctx_tokens_in: packet.tokensIn }, ...(cfg ? { cfg } : {}) },
|
|
448
|
+
(row) => note(deps, row),
|
|
449
|
+
)
|
|
450
|
+
: { res: null, attempts: 0 };
|
|
364
451
|
/** @type {Record<string, any> | null} */
|
|
365
452
|
let res = null;
|
|
453
|
+
/** @type {import('./validate-review.mjs').Verdict} */
|
|
454
|
+
let verdict = { ok: false, reason: 'exit', detail: 'no session result' };
|
|
455
|
+
/** @type {number | null} the round's tokens: the first session's, plus a second-level try's */
|
|
456
|
+
let tokensIn = null;
|
|
457
|
+
/** @type {number | null} */
|
|
458
|
+
let tokensOut = null;
|
|
459
|
+
// the packet file is removed on every path, and only after the second-level try (which reads it)
|
|
366
460
|
try {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
461
|
+
try {
|
|
462
|
+
res = (await attempt(undefined)).res;
|
|
463
|
+
} catch {
|
|
464
|
+
res = null; // the helper never throws; if it ever did, it is still an `exit` failure
|
|
465
|
+
}
|
|
466
|
+
tokensIn = res?.usage?.tokens_in ?? null;
|
|
467
|
+
tokensOut = res?.usage?.tokens_out ?? null;
|
|
468
|
+
verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
469
|
+
if (!verdict.ok && verdict.reason === 'schema') {
|
|
470
|
+
try {
|
|
471
|
+
// no ledger writer ⇒ nothing logged, nothing tried; every row goes through the same writer
|
|
472
|
+
// (each helper handles its own failed write)
|
|
473
|
+
const write = deps.writeRow ? (/** @type {Record<string, any>} */ row) => /** @type {NonNullable<LoopDeps['writeRow']>} */ (deps.writeRow)(row) : undefined;
|
|
474
|
+
const second = await afterSchemaMiss({ res, session, packetText: packet.text, cfg: deps.cfg ?? {}, allowed: true /* rechecks are never consensus; the budget gate is inside the spawn */, history: deps.schemaHistory, writeRow: write, spawnAt: attempt });
|
|
475
|
+
if (second) {
|
|
476
|
+
// compute everything first, then assign together, so a throw never leaves a mixed state
|
|
477
|
+
const secondVerdict = validateReview(second.res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
478
|
+
const sumIn = sum(tokensIn, second.res?.usage?.tokens_in);
|
|
479
|
+
const sumOut = sum(tokensOut, second.res?.usage?.tokens_out);
|
|
480
|
+
res = second.res;
|
|
481
|
+
verdict = secondVerdict;
|
|
482
|
+
tokensIn = sumIn;
|
|
483
|
+
tokensOut = sumOut;
|
|
484
|
+
if (!verdict.ok && verdict.reason === 'schema') await logSecondLevelMiss({ res, session, packetText: packet.text, writeRow: write });
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
// defensive only: the helpers never throw. If they ever did, the first session's result
|
|
488
|
+
// and its `schema` verdict stand (never turned into `exit`, never approval)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
379
491
|
} finally {
|
|
380
492
|
rmSync(promptPath, { force: true });
|
|
381
493
|
}
|
|
382
|
-
|
|
383
|
-
return { verdict, tokens_in: res?.usage?.tokens_in ?? null, tokens_out: res?.usage?.tokens_out ?? null };
|
|
494
|
+
return { verdict, tokens_in: tokensIn, tokens_out: tokensOut };
|
|
384
495
|
}
|
|
385
496
|
|
|
386
497
|
/**
|
|
@@ -525,16 +636,17 @@ export async function runRound(state, deps, opts = {}) {
|
|
|
525
636
|
draft.open = [];
|
|
526
637
|
if (pending.length > 0) {
|
|
527
638
|
const contextMode = s.scope === 'file' ? 'auto' : 'recheck';
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
639
|
+
const moves = await recheckMoves(deps, current.rel, s.scope === 'file' ? diff : null);
|
|
640
|
+
const whole = buildRecheckPacket({ diff, open: pending, cfg: deps.cfg, contextMode, rulesDigest: deps.rulesDigest, factsExcerpt: deps.factsExcerpt, moved: movedText(moves, diff.content, null) });
|
|
641
|
+
/** @type {Array<{index: number | null, heading: string | null, diff: import('./packet.mjs').FileDiff, open: Finding[], keys: number[] | null}>} */
|
|
642
|
+
let parts = [{ index: null, heading: null, diff, open: pending, keys: null }];
|
|
531
643
|
if (whole.status !== 'ok') {
|
|
532
644
|
// deterministic (the same hunks build the same packet): terminal, never a retry — except
|
|
533
645
|
// a Markdown file whose packet is `split_required`, rechecked section by section (B54);
|
|
534
646
|
// any other status stops as it always did
|
|
535
|
-
const packed = whole.status === 'split_required' && isMarkdownFile(current.rel) ? packRecheck(diff, pending, whole.budget) : null;
|
|
647
|
+
const packed = whole.status === 'split_required' && isMarkdownFile(current.rel) ? packRecheck(diff, pending, whole.budget, moves) : null;
|
|
536
648
|
if (!packed || packed.status !== 'ok') return stopBeforeRound(state, deps, kind, current.rel, packed ?? whole);
|
|
537
|
-
parts = packed.sections.map((sec) => ({ index: sec.index, heading: sec.headings[0], diff: sec.diff, open: sec.open }));
|
|
649
|
+
parts = packed.sections.map((sec) => ({ index: sec.index, heading: sec.headings[0], diff: sec.diff, open: sec.open, keys: sec.keys }));
|
|
538
650
|
}
|
|
539
651
|
// EVERY packet is built and held to the budget BEFORE any session runs: a section over it
|
|
540
652
|
// stops the round as split_required (with its heading) before anything is spent
|
|
@@ -545,10 +657,11 @@ export async function runRound(state, deps, opts = {}) {
|
|
|
545
657
|
built.push(whole);
|
|
546
658
|
continue;
|
|
547
659
|
}
|
|
548
|
-
const
|
|
660
|
+
const moved = movedText(moves, diff.content, part.keys);
|
|
661
|
+
const packet = buildRecheckPacket({ diff: part.diff, open: part.open, cfg: deps.cfg, contextMode, rulesDigest: deps.rulesDigest, factsExcerpt: deps.factsExcerpt, moved });
|
|
549
662
|
// a packet that fails to build stops with ITS status (and size, when it has one)
|
|
550
663
|
if (packet.status !== 'ok') return stopBeforeRound(state, deps, kind, current.rel, { ...packet, section: part.heading });
|
|
551
|
-
const size = recheckTokens(part.diff, part.open);
|
|
664
|
+
const size = recheckTokens(part.diff, part.open, moved);
|
|
552
665
|
if (size > whole.budget) return stopBeforeRound(state, deps, kind, current.rel, { status: 'split_required', tokensIn: size, budget: whole.budget, section: part.heading });
|
|
553
666
|
built.push(packet);
|
|
554
667
|
}
|