fix(explore): pay every admitted file on every render path (CG-26)
The invariant this closes: every admitted file receives at least its reservation before any file draws on carry-forward slack. CG-30 bounded an oversize cluster member and CG-31 gave the cluster path a displacement guard; three holes were left, and each one starved a file that had been admitted, reserved and — in the worst case — rendered. 1. The whole-file arms had no displacement guard. BUY's fit test read `renderCeiling - totalChars` (everyone's room) while its source-space sibling refused the same trade, and GRACE was not fit-tested at all. okhttp's CallServerInterceptor.kt shipped 8,499 chars on a 5,964 funded ceiling and the rank-6 file below it delivered nothing. Both arms now test the render they actually produce against `fundedHeadroom`, and a whole render that does not fit falls through to clustering instead of skipping the file. 2. Every section was charged a flat 200 chars while a real header runs 300-500. The loop believed it had room it did not have — okhttp allocated 26,601 against a 24,400 ceiling — so the final truncation threw a fully-rendered section away. Sections are charged their real cost now, the owed-below arithmetic uses a per-file overhead estimated from the file's own symbols, and a marginal overrun trims the weakest cluster (or windows the last one into the room that is left) rather than skipping the file over a rounding difference. 3. `owedPayableBelow` held all-or-nothing. When the last admitted file's FULL reservation no longer fit, nothing was held for it: on the precise-query fixture the rank-5 file took 4,134 chars against a 2,948 reservation while rank 6 — admitted, reserved 2,539 — was left 4 chars and skipped. It now holds the remainder while that remainder is still worth a section (MIN_CHARS). And the epilogue is budgeted instead of discarded. The flat 600-char margin was neither the epilogue's size (1,064 gin, 1,788 django, 2,231 excalidraw) nor a bound on it, so four of six suite repos shipped with no pointer list and no reminders at all. The loop now reserves the epilogue's FLOOR — the one line that says an uncovered area exists, plus a pointer for every file whose bytes were deliberately withheld (CG-12) — and the rest is fitted to the room that actually remains, in priority order, entry by entry. Sized from the real strings; no constant was swept against the suite. Deterministic, same clean-rebuilt indexes, baseline = CG-31 tip: repo base source new source files ceiling django 20,791 20,878 6 -> 6 was discarding its epilogue tokio 21,521 21,607 5 -> 5 was discarding its epilogue okhttp 19,034 18,870 5 -> 6 +1 file delivered excalidraw 20,204 19,652 8 -> 8 keeps its pointer list gin 10,776 10,776 4 -> 4 byte-identical alamofire 11,662 11,662 2 -> 2 byte-identical No repo truncates any more and none loses a file. okhttp and excalidraw trade 164 and 552 source chars on their LAST-ranked file for the pointer list naming what the response could not cover — bytes the CG-31 tip only had because it over-filled a ceiling it mis-measured and then discarded the epilogue whole. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c54e0080c2
commit
7cbde95ce2
@@ -150,6 +150,28 @@ function evaluate(fixture, report, text) {
|
||||
`answer ${pct(share('answer'))} delivered (${pct(allocated.get('answer') ?? 0)} allocated)`,
|
||||
);
|
||||
}
|
||||
// Same question against the SOURCE the response delivered rather than the
|
||||
// whole envelope (CG-26). The envelope-denominated gate above moves whenever
|
||||
// the response's prose does — the epilogue surviving instead of being
|
||||
// discarded costs it a point, and every additional admitted file that gets
|
||||
// paid dilutes it further — so it cannot tell "the answer was starved" from
|
||||
// "everything else was also delivered". Allocation is about source bytes;
|
||||
// measure it in source bytes.
|
||||
if (want.answerShareOfSourceAtLeast !== undefined) {
|
||||
const sourceBy = new Map();
|
||||
let totalSource = 0;
|
||||
for (const f of report.files) {
|
||||
const g = groupOf(f.path, groups);
|
||||
sourceBy.set(g, (sourceBy.get(g) ?? 0) + f.finalChars);
|
||||
totalSource += f.finalChars;
|
||||
}
|
||||
const answerSource = totalSource > 0 ? (sourceBy.get('answer') ?? 0) / totalSource : 0;
|
||||
add(
|
||||
`answer group takes >= ${pct(want.answerShareOfSourceAtLeast)} of DELIVERED SOURCE`,
|
||||
answerSource >= want.answerShareOfSourceAtLeast,
|
||||
`answer ${num(sourceBy.get('answer') ?? 0)} of ${num(totalSource)} source chars (${pct(answerSource)})`,
|
||||
);
|
||||
}
|
||||
if (want.incidentalShareAtMost !== undefined) {
|
||||
add(
|
||||
`incidental group takes <= ${pct(want.incidentalShareAtMost)} of the envelope`,
|
||||
|
||||
Reference in New Issue
Block a user