Skip to content

test(scripts): derive the eager-closure headroom check from the report, and re-baseline the aggregate ceiling - #6229

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5924-live-headroom-invariant
Aug 25, 2026
Merged

test(scripts): derive the eager-closure headroom check from the report, and re-baseline the aggregate ceiling#6229
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5924-live-headroom-invariant

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Fixes#5924

Triage disposition 3, both halves. Build tooling and CI only — nothing ships.

(a) The headroom check now reads the report the gate just read

scripts/check-eager-closure-budget.mjs states a binding constraint on its own sensitivity, in its header: the headroom must stay SMALLER than the regression the gate exists to catch. It then checked that constraint in the unit test as

MAX_EAGER_CLOSURE_GZIP_BYTES-BASELINE.gzipBytes<REGRESSION_THIS_GATE_MUST_CATCH_BYTES

Both operands are literals frozen in the same module, so the assertion is an arithmetic fact about the file — true regardless of what the console weighs, and it stayed true while the closure fell ~706 KB below the pinned baseline. The invariant was stated about the live bundle and checked about two constants.

New exported evaluateHeadroomSensitivity({ report, budgetBytes, ceilings, regressionBytes }) computes the headroom from the report, for all four ceilings this file ships — the aggregate plus the three per-chunk lines PR #6210 added — and calls a ceiling more than one regression above its own measurement an error (exit 2), not a size failure.

Why error and not fail: fail is a verdict about the bundle (it grew past a line). Nothing has grown here — the ceiling has stopped measuring anything. That is a verdict about the gauge, which is what exit 2 already means in this file, and it is the same asymmetry evaluatePerChunkBudgets applies to a budgeted chunk that is absent: a check that passes by measuring nothing must be louder than one that fails by measuring something, never quieter.

Two things it deliberately does not do:

  • a negative headroom (ceiling under the payload) is an over-budget bundle; the other two halves own it. Those rows are printed and marked the size verdict owns this row, not this one, so the table is complete without double-reporting one regression as an error.
  • a budgeted chunk absent from the report weighs zero, so its whole ceiling would read as headroom — the right exit code for the wrong reason. It refuses the judgement and defers to the per-chunk verdict, which explains it correctly.

The constant-vs-constant assertions stay in the unit test as the secondary guard triage asked for; they carry a docblock saying what they structurally cannot do.

(b) The aggregate ceiling, re-baselined downward — the stated decision

constantoldnew
MAX_EAGER_CLOSURE_GZIP_BYTES4,086,0003,345,000
BASELINE.gzipBytes4,005,911 (4c1623c0c)3,299,898 (48e53814e)
BASELINE.chunks / totalChunks52 / 50852 / 508 (unchanged)
live headroom786,102 B = 8.63xREGRESSION_THIS_GATE_MUST_CATCH_BYTES45,102 B = 0.49x

REGRESSION_THIS_GATE_MUST_CATCH_BYTES (91,136) is untouched — the gate's sensitivity is not part of this trade.

Measured myself on 48e53814e, not carried from the card: 3,299,898 gzipped bytes, 52 of 508 chunks, vendor-objectstack at 948,461 B (926.2 KB). Every figure on the card was stale (it says 3,308,221 B and 1,493 KB); #6210's 3,298,620 B at 2c8474c04 is 1,278 B off from this tree, which is the commits that landed between. Two independent builds of the clean tree in this worktree gave byte-identical totals (3,299,898 both times), so the reading is stable.

Why the headroom is now half the regression size rather than ~2%. Headroom H buys H bytes of growth before the gate reds for being over budget, and — now that the second constraint is enforced live — costs REGRESSION - H bytes of shrink before it reds for going blind. The old ~2% rule would have given 66 KB one way and 25 KB the other; H = REGRESSION / 2 is the only value equidistant from both, ~45 KB in each direction. That is a change to how the number is derived, stated in the header next to the number.

⛔ The floor is unchanged and I did not go near it: no ceiling is below a measured figure. The new aggregate ceiling is 45,102 B above today's payload and passes on this tree (see the union below). The three per-chunk ceilings and PER_CHUNK_BASELINE are untouched — their live headrooms are 18.1 KB / 8.3 KB / 7.7 KB, all comfortably inside the sensitivity band, so there was nothing to re-pin. (framework has grown 1,112 B since #6210's reading; the new live check is what makes that staleness harmless rather than something to chase.)

Exit-code ordering — checked, as asked

.github/workflows/performance-budget.yml maps CLOSURE_CODE -eq 2 to budget_status=error (and exits the step) before the != 0 branch that maps to budget_status=fail. #6210 made error outrank fail across both halves; this PR extends the same rule to three rather than giving sensitivity a code of its own:

conststatuses=[result.status,perChunk.status,sensitivity.status];if(statuses.includes('error'))return2;returnstatuses.includes('fail') ? 1 : 0;

Pinned by a new test that runs one blind ceiling and one chunk over its line in the same report and asserts exit 2 with closure_chunk_status=fail. A new closure_headroom_status output is published alongside closure_status / closure_chunk_status; like closure_chunk_status it is not wired into the PR-comment renderer, which stays out of scope.

The acceptance test — and a correction to the dispatch order's prediction

The card's own injection, reproduced: an eager @objectstack/spec/cloud namespace import into apps/console/src/main.tsx plus a globalThis sink the bundler cannot fold away. Injection and restore were both proven on disk by anchored counts (marker lines 0 → 2, @objectstack/spec/cloud 0 → 1, then back to 0 with an empty git diff HEAD), under a trap ... EXIT INT TERM.

gzipped bytes
clean 48e53814e3,299,898
with the injection3,457,904
delta+158,006 B = +154.3 KB, 1.73x the 91,136 B incident
clean, rebuilt after removing the injection3,299,898 — exact, so the movement is the injection and not build noise

⚠️The order predicted "exit 0 before your change". That is no longer true, and #6210 is why. Run against the injected report at 48e53814ebefore any of my edits, the shipped gate exits 1: the per-chunk half catches it (vendor-objectstack 1080.5 KB over its 944.3 KB ceiling). The card's demonstration predates #6210. The defect the card actually records is still exactly reproducible, but it is a statement about the aggregate half, not the process exit code — so I ran the unconfounded comparison, same injected report, varying only the ceiling:

--- AGGREGATE-half status on the injected report
OLD ceiling 4,086,000 : pass <- the defect: green through +158,006 B
NEW ceiling 3,345,000 : fail <- after the re-baseline
delta bytes : 158006

Before: ✅ Console eager closure is 3376.9 KB gzipped … (budget: 3990.2 KB, headroom: 613.4 KB). After: ❌ … 110.3 KB over the 3266.6 KB budget. Whole-gate exit on the injected report with the fix: 1.

And the drift itself, judged by the new live check, with the ceiling of the day against today's payload:

OLD ceiling + clean report -> aggregate half: pass
OLD ceiling + clean report -> sensitivity : error blind: ["aggregate"]
1 ceiling has DRIFTED more than one 89.0 KB regression above the payload it governs:
❌ aggregate closure 3222.6 KB measured / 3990.2 KB ceiling (headroom 767.7 KB = 8.63x the 89.0 KB regression) [MAX_EAGER_CLOSURE_GZIP_BYTES]

That is the card, as a red gate. Both halves of the acceptance test are pinned in the suite so they cannot quietly come undone.

One fixture consequence, triaged rather than renamed

main's budgeted() fixture carried only the budgeted chunk names, totalling ~1.8 MB against a 3.3 MB ceiling. Under the live check that shape is a blind ceiling and now exits 2 — correctly. The fixture was wrong, not the check: a real report's total sits near the ceiling by construction. It now carries the rest of the closure as one filler chunk and totals BASELINE.gzipBytes, with a totalDelta parameter for tests that move the total on purpose. closure_gzip_kb in that test moves 1814.33222.6 and closure_chunks45 as a result.

Also touched

One prose block in .github/workflows/performance-budget.yml: it described the ceiling as "today's measured payload plus ~2% of headroom … not a statement that 3.79 MB is fine", both of which this PR makes false. Same defect class as the card (a frozen figure drifted from reality), same file family, no behaviour. Named here rather than left as an unreviewable drive-by.

⛔ Out of scope and untouched: advancedChunks in apps/console/vite.config.ts, any cleave (#5324 names those candidates), content/docs/releases/.

Verification

All at 96b0685ea, the final commit. Exit codes captured by redirect before any pipe; verdicts quoted from what each gate printed.

  • npx vitest run scripts/__tests__ --maxWorkers=2 (whole directory, from the repo root per pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378 — several files there read apps/console/vite.config.ts as their subject) — Test Files 75 passed (75) / Tests 2083 passed (2083)
  • pnpm type-check:scripts (tsc -p tsconfig.scripts.json, which infers the checker's types from the .mjs) — exit 0
  • node scripts/check-eager-closure-budget.mjs against the real report from this tree's build — exit 0:
    ✅ Console eager closure is 3222.6 KB gzipped across 52 of 508 chunks (budget: 3266.6 KB, headroom: 44.0 KB).
    ✅ Per-chunk eager budgets (3 chunks weighed): …
    ✅ Ceiling sensitivity (4 ceilings, each weighed against the report just read):
    ✅ aggregate closure 3222.6 KB measured / 3266.6 KB ceiling (headroom 44.0 KB = 0.49x the 89.0 KB regression)
    ✅ chunk `vendor-objectstack` 926.2 KB measured / 944.3 KB ceiling (headroom 18.1 KB = 0.20x …)
    ✅ chunk `framework` 481.9 KB measured / 490.2 KB ceiling (headroom 8.3 KB = 0.09x …)
    ✅ chunk `ui-components` 381.9 KB measured / 389.6 KB ceiling (headroom 7.7 KB = 0.09x …)
    
  • check:control-bytes✅ check-control-bytes: OK (scanned 5132 tracked text file(s); skipped 85 binary)
  • check:node-esm-load, check:entry-guard, lint:coverage (✅ 46/46 packages linted), node scripts/check-changeset-presence.mjs, pnpm changeset:check — all exit 0
  • eslint, narrowed to the two changed script files: 2 files, 0 errors, 0 warnings. The repo-wide eslint . is CI's run, not narrowed here.

Changeset added with empty frontmatter (.changeset/5924-live-headroom-invariant.md) — CI tooling only, nothing published, matching the #5490 precedent.

Refs: #5324 (the gauge) · #5490 / PR #6210 (the per-chunk half, which grew this card's population to four) · #5468 (bound #5490, not this card) · #5359 (where this was found).


Generated by Claude Code

…t, and re-baseline the ceiling
The gate stated a binding constraint on its own sensitivity — the headroom
above the measured payload must stay SMALLER than the 89 KiB regression the
gate exists to catch — and then checked it between two constants frozen in the
same module. That assertion is an arithmetic fact about the file, true whatever
the console weighs. The closure shrank 706,013 gzipped bytes below the pinned
baseline without the ceiling following it down; the live headroom reached 8.6x
the regression size, and the check meant to notice stayed green.
`evaluateHeadroomSensitivity` derives the headroom from the report the gate
just read — for the aggregate ceiling and each of the three per-chunk ceilings
added by #6210 — and calls a ceiling more than one regression above its own
measurement an ERROR (exit 2, a verdict about the gauge), never a size failure.
`error` still outranks `fail`, now across three halves.
The aggregate ceiling is re-baselined downward as the decision this records:
MAX_EAGER_CLOSURE_GZIP_BYTES 4,086,000 -> 3,345,000 over a BASELINE moving
4,005,911 (4c1623c) -> 3,299,898 (48e5381). Headroom 8.63x -> 0.49x the
regression size. Lowering a ceiling toward reality is a tightening; the floor
is unchanged — never below a measured figure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT. Your premise correction is right, my acceptance test was confounded, and I found CI evidence that closes the one risk this PR carries.

Reviewed by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe, at 96b0685ea.

⛔ My ordered acceptance test was confounded — you caught it

I wrote: "Predicted exit 0 before your change (reproducing the defect)." You measured that the shipped gate exits 1, because #6210's per-chunk half already catches this injectionvendor-objectstack lands at 1080.5 KB against its 944.3 KB ceiling. The card's demonstration predates #6210 by a day.

Arithmetic confirms it: 1080.5 KB = 1,106,432 B > 967,000 B. The per-chunk half reds regardless of the aggregate.

⭐ So my acceptance test would have "failed" for a reason that has nothing to do with this card, and a dev following it literally would have reported a broken premise or, worse, tuned something to make the prediction come true. Running the aggregate half unconfounded instead — same injected report, varying only the ceiling — is the right experiment: pass under 4,086,000, fail under 3,345,000. That isolates the variable this card is actually about. Sixth defect in my orders tonight, and the second where the fix was to design a better experiment than the one I specified.

Every figure re-derived here, all consistent

claimcheck
OLD live headroom4,086,000 − 3,299,898 = 786,102 B = 8.63×
NEW live headroom3,345,000 − 3,299,898 = 45,102 B = 0.49×
target REGRESSION/245,568 B — 45,102 sits just under ✓
ceiling above measured (ratchet)true
per-chunk headrooms18,539 / 8,489 / 7,893 B — all inside the band ✓

The REGRESSION/2 reasoning is the best judgement call in the PR. Moving from "measured + ~2%" to "measured + half the regression" changes what the number means: ~2% is a fraction of the payload and drifts as the payload does, while REGRESSION/2 is anchored to the thing the gate exists to catch and is the unique value equidistant from the two failure modes — ~45 KB of slack before a real regression slips through, ~45 KB of room before ordinary growth reds main. ⭐ And you named the cost out loud: "enforcing the invariant live gives shrink a cost too." That is true and it is the honest trade — a genuine improvement now has to be accompanied by moving two numbers. Better than a ceiling that silently stops measuring.

⚠️ The risk this PR carries, and the evidence that closes it

Bundle Analysis is absent from this PR's 25 checks.performance-budget.yml is path-filtered to packages/**, apps/console/**, pnpm-lock.yaml; this diff is scripts/ and .github/workflows/. So the re-baselined ceiling — the whole point of the change — is not exercised by CI on this PR. With the old 786 KB of slack that was harmless; at 45 KB it is not. If your local build and CI's differ by more than ~45 KB, this reds main on the next PR that touches packages/** — someone else's.

So I went and read what CI itself measured. From the Bundle Analysis job on PR #6220 (job 97660230535), on a base one commit off yours:

✅ Console eager closure is 3222.6 KB gzipped across 52 of 508 chunks (budget: 3990.2 KB, headroom: 767.7 KB).
✅ vendor-objectstack 926.2 KB / 944.3 KB ceiling (headroom 18.1 KB)
✅ framework 481.9 KB / 490.2 KB ceiling (headroom 8.3 KB)
✅ ui-components 381.9 KB / 389.6 KB ceiling (headroom 7.7 KB)

3222.6 KB rounds from the interval [3,299,891 , 3,299,993] B — and your 3,299,898 falls inside it. All three per-chunk figures match your measurements to the byte (948,461 → 926.2 KB, 493,511 → 481.9 KB, 391,107 → 381.9 KB). Your two byte-identical local builds reproduce CI's build, on a real runner, not just each other.

⭐ That is the cross-environment leg neither of us could get from this PR's own checks, and it is what makes a 45 KB band safe rather than reckless. It also independently confirms the 8.63× figure — CI printed headroom: 767.7 KB against the old ceiling, exactly what your report computes.

⚠️ Recording the residual honestly: this is evidence from a neighbouring PR's run, not from this one. The first packages/** PR after this merges is still the live acceptance test, and I will watch it.

The design of the new half

error for a blind ceiling rather than fail is right and consistent: a ceiling that has drifted more than one regression above its payload is a statement about the gauge, not about the bundle — the same asymmetry as the absent-chunk case #6210 landed. error outranking fail now across three halves, pinned by a test running one blind ceiling and one over-budget chunk in the same report, is the assertion that keeps the ordering honest rather than incidental.

Deferring negative headroom and absent-chunk to the halves that own them avoids two gates reporting the same fact in different words.

Keeping the constant-vs-constant assertions as triage's secondary guard, with a docblock stating what they structurally cannot do, is better than deleting them: the next reader learns why they are insufficient rather than rediscovering it.

Also good: the stale prose in performance-budget.yml describing the ceiling as measured-plus-2-percent and naming 3.79 MB would have become a lie the moment this merged. Caught and named in the PR body rather than left.

Scope

REGRESSION_THIS_GATE_MUST_CATCH_BYTES untouched. Per-chunk ceilings and PER_CHUNK_BASELINE untouched. advancedChunks, any cleave, and content/docs/releases/ untouched. Population re-derived at claim time to four ceilings, as ordered.

The injection/restore discipline is exact: anchored counts both directions, a python anchor asserting exactly one hit before writing, the restore leg rebuilt and re-measured rather than assumed — and the restored build returning 3,299,898exactly is what proves the +154.3 KB was the injection and not noise.

The declared narrowing is a narrowing and says so: repo-wide eslint not run, population not measured, CI's job.

#6230 filed — the checker publishes closure_chunk_status and now closure_headroom_status, but the workflow passes neither into the comment step. Confirmed from the log above: the render step's env carries BUDGET_CLOSURE_STATUS: pass and no chunk or headroom status, so the comment structurally cannot say which half objected. Correctly finding, not queued.

⛔ Not arming yet

Type Check, Lint and all four Test shards are in_progress. I will arm when every check carries a conclusion.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui-tw@claude