diff --git a/scripts/size/README.md b/scripts/size/README.md index a20b2746a..b39e216a3 100644 --- a/scripts/size/README.md +++ b/scripts/size/README.md @@ -20,28 +20,43 @@ monotonic in the input: a change of a few minified bytes moves a scenario's brotli by ±50–90 B, so a brotli-only gate failed PRs for noise and every "fix" either raised a cap (permanently) or golfed the code until the layout came out lucky. Minified bytes are deterministic, so the gate (`gate.mjs`) -uses them to tell noise from growth. Per scenario: - -| brotli vs cap | minified vs base | verdict | -| ------------- | ------------------------------------- | ------------------------------ | -| at or under | anything | **pass** | -| over | grew by ≤ `MINIFIED_ALLOWANCE` (20 B) | **pass with a warning** | -| over | grew by more | **fail** | -| over | no base measurement | **fail** (the cap is absolute) | - -"Base" is the PR's base commit (`pull_request.base.sha`, the first parent -of the merge commit CI measures as the head), measured in the same run -with the head's harness; on a push to `next` it is the commit before the -push. The warning — in the job summary, the PR size comment and as an -annotation — reads _over brotli cap by N B, minified +M B — layout noise; -cap will be re-based at the next ratchet_. The allowance is one constant, -`MINIFIED_ALLOWANCE` in `gate.mjs`; a scenario may set its own with -`minifiedAllowance` in `scenarios.js` (none does). +uses them to tell noise from growth. + +Every cap records the **minified size measured when it was set**: +`capMinified` beside an inline `limit` in `scenarios.js`, and +`{ "cap", "minified" }` entries in `floor-caps.json`. Per scenario: + +| brotli vs cap | head minified | verdict | +| ------------- | ---------------------------------------------------- | ----------------------- | +| at or under | anything | **pass** | +| over | ≤ recorded minified + `MINIFIED_ALLOWANCE` (20 B) | **pass with a warning** | +| over | more | **fail** | +| over | no recorded minified: grew ≤ 20 B over the PR's base | pass with a warning | +| over | no recorded minified: grew more, or no base | **fail** | + +The allowance is measured against the recorded size, not per PR, so growth +cannot creep across PRs: on an over-cap scenario, two successive +15 B PRs +do not both pass — the second is +30 B over the recorded size. The warning +(job summary, PR size comment, annotation) states the headroom left: +_over brotli cap by N B; minified M B vs R B recorded with the cap (+D B) — +H B of the 20 B minified allowance left; +P B minified over this PR's base_. +A failure carries the same numbers, so a PR whose own change is small can +see that earlier growth used the allowance up. + +The last two rows are the fail-safe for a cap without a recorded minified (a +new scenario that did not record one): the gate compares with the PR's base +(`pull_request.base.sha`, the first parent of the merge commit CI measures +as the head; on a push to `next`, the commit before the push), measured in +the same run with the head's harness, and the summary says so. The +allowance is one constant, `MINIFIED_ALLOWANCE` in `gate.mjs`; a scenario +may set its own with `minifiedAllowance` in `scenarios.js` (none does). Real growth is still a decision made in the PR: lower the bytes, or raise -the scenario's cap in the same PR with a dated reason in its ledger. Raising -a frozen floor cap (below) additionally needs a `Size-Exception:` line in -the PR body — the override for growth the maintainer has accepted. +the scenario's cap **and its recorded minified** in the same PR, measured +together by CI, with a dated reason in its ledger. A new scenario records +both. Raising a frozen floor's cap or its recorded minified (below) +additionally needs a `Size-Exception:` line in the PR body — the override +for growth the maintainer has accepted. Locally, `npm run size` is the absolute gate (any scenario over its cap fails); `node gate.mjs head.json base.json` applies the PR rule to two @@ -49,14 +64,22 @@ fails); `node gate.mjs head.json base.json` applies the PR rule to two ## The ratchet -Noise that passed with a warning leaves a scenario over its cap; savings -leave caps loose. `npm run ratchet` re-bases every cap on what the tree -measures now — measured + 10 B rounded up to 0.01 KB — and **only ever -lowers** a cap. It rewrites the inline caps in `scenarios.js` and the frozen -floors in `floor-caps.json`, adds a dated ledger line above each lowered -cap, and prints the lowered inline caps and the lowered **frozen floors** as -separate tables, so a floor change is seen as one. A scenario still over its -cap is listed and left alone: the ratchet does not raise. +Savings leave caps loose. `npm run ratchet` re-bases every cap on what the +tree measures now — measured + 10 B rounded up to 0.01 KB — and **only ever +lowers** a cap. A lowered cap records the minified measured with it (one +measurement; if that minified is higher, brotli still shrank, and the +ratchet lists it). A cap it does not lower only ever has its recorded +minified **lowered**, or recorded for the first time; it is never raised — +that loosens the gate like a cap raise, so it takes a PR (and, on a floor, +a `Size-Exception:`). The ratchet rewrites `scenarios.js` and +`floor-caps.json`, adds a dated ledger line above each cap it changes, and +prints the inline caps and the **frozen floors** as separate tables, so a +floor change is seen as one. Scenarios still over their cap, or above their +recorded minified, are listed and left alone: the ratchet does not raise, +so a scenario that landed over its cap on noise stays over it, held to its +recorded minified + 20 B, until code shrinks or a PR re-bases it. +`--minified-only` leaves every cap alone and only records or lowers +minified sizes. Run it once per RC, on `next`, from CI's numbers — local and CI artifacts differ by tens of brotli bytes: @@ -124,8 +147,9 @@ maintainer accepted the cost (read from the PR body when the run starts — edit the body, then re-run). Ten weeks of individually justified 10–300 B bumps took the signals floor from 7.1 to 9.9 KB; the freeze makes the next one a decision, not a paragraph. See -`documentation/plans/size-reduction-audit.md`. The ratchet may lower a -frozen floor like any cap. +`documentation/plans/size-reduction-audit.md`. Each floor's recorded +minified is frozen the same way (raising it, or removing it, needs the +exception). The ratchet may lower a frozen floor like any cap. ## Attribution @@ -176,3 +200,12 @@ breaks. `compare` job (which ran after `check`) into a parallel `base` job that `check` waits on, and pushes to `next` now compare against the previous commit instead of the absolute cap. No cap changed. +- **2026-10-06 — recorded minified (#3822).** Measured per PR, the + allowance let an over-cap scenario creep: each +15 B PR passed against its + own base. Every cap now records the minified size measured when it was + set, and the allowance is measured against that. The warning states the + headroom left instead of promising a re-base (the lower-only ratchet + cannot re-base an over-cap scenario). Seeded by the ratchet's + `--minified-only` mode from CI's measurement of `next` @ fff1615ee (Size + run 37443080193); every cap unchanged. The base comparison remains the fail-safe + for a cap without a recorded minified. diff --git a/scripts/size/check-floor-caps.mjs b/scripts/size/check-floor-caps.mjs index 416036bb2..f39383b50 100644 --- a/scripts/size/check-floor-caps.mjs +++ b/scripts/size/check-floor-caps.mjs @@ -12,7 +12,11 @@ // Compares floor-caps.json at HEAD with the same file at . Exits // non-zero if any cap increased, unless SIZE_EXCEPTION (the PR body, in CI) // contains a line starting with "Size-Exception:" that names the reason. -// A cap absent at the base (a new floor scenario) is allowed. +// A cap absent at the base (a new floor scenario) is allowed. The minified +// size recorded with each cap (`{ cap, minified }` entries; the gate +// measures minified growth against it) is frozen the same way: raising it +// loosens the gate exactly as raising the cap does. A recorded minified +// absent at the base (an entry still in the old string form) is allowed. import { readFileSync } from "node:fs"; import { execFileSync } from "node:child_process"; @@ -54,12 +58,15 @@ try { } const exception = /^\s*Size-Exception:\s*\S/m.test(process.env.SIZE_EXCEPTION ?? ""); +const entry = e => (typeof e === "string" ? { cap: e } : e); let raised = []; -for (const [name, cap] of Object.entries(head)) { +for (const [name, value] of Object.entries(head)) { if (!(name in baseCaps)) continue; - const before = toBytes(baseCaps[name]); - const after = toBytes(cap); - if (after > before) raised.push(` ${name}: ${baseCaps[name]} -> ${cap}`); + const was = entry(baseCaps[name]); + const now = entry(value); + if (toBytes(now.cap) > toBytes(was.cap)) raised.push(` ${name}: ${was.cap} -> ${now.cap}`); + if (typeof was.minified === "number" && !(now.minified <= was.minified)) + raised.push(` ${name}: recorded minified ${was.minified} B -> ${now.minified ?? "removed"}`); } if (raised.length === 0) { diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 0fb36068d..acb9d32b8 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -1,9 +1,30 @@ { - "signals: core floor (createSignal/Memo/Effect/Root/flush)": "7.35 KB", - "app: render + one signal (the simple-app floor)": "9.86 KB", - "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": "17.71 KB", - "page: base server components (hydrating + dynamic + frames + sf reference)": "44.84 KB", - "page: live server components (base + live/GET + action + isPending/latest)": "48.51 KB", - "server: floor (getRequestEvent + isServer)": "1.34 KB", - "server: renderToString (the server-render floor)": "20.42 KB" + "signals: core floor (createSignal/Memo/Effect/Root/flush)": { + "cap": "7.35 KB", + "minified": 20133 + }, + "app: render + one signal (the simple-app floor)": { + "cap": "9.86 KB", + "minified": 27687 + }, + "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": { + "cap": "17.71 KB", + "minified": 52567 + }, + "page: base server components (hydrating + dynamic + frames + sf reference)": { + "cap": "44.84 KB", + "minified": 145599 + }, + "page: live server components (base + live/GET + action + isPending/latest)": { + "cap": "48.51 KB", + "minified": 157562 + }, + "server: floor (getRequestEvent + isServer)": { + "cap": "1.34 KB", + "minified": 3324 + }, + "server: renderToString (the server-render floor)": { + "cap": "20.42 KB", + "minified": 71813 + } } diff --git a/scripts/size/gate.mjs b/scripts/size/gate.mjs index 61bf5cc7e..d75105fee 100644 --- a/scripts/size/gate.mjs +++ b/scripts/size/gate.mjs @@ -3,14 +3,19 @@ // // Caps are brotli bytes, and brotli is what ships, so a scenario under its // cap passes whatever its minified size did. Over the cap is not enough to -// fail: brotli's layout moves ±50–90 B on minified changes of a few bytes, so -// a PR fails a scenario only when it is over its brotli cap AND its minified -// growth over the PR's base exceeds MINIFIED_ALLOWANCE (or the scenario's -// `minifiedAllowance`). Over the cap within the allowance passes with a -// warning — layout noise, left for the next ratchet (ratchet.mjs) to re-base. -// The base is the PR's base commit, or on a push to next the commit before -// it. Without a base measurement (a new scenario, a base the head's harness -// could not measure, a local run) the cap is absolute, as it always was. +// fail: brotli's layout moves ±50–90 B on minified changes of a few bytes. +// Each cap records the minified size measured when it was set +// (`capMinified`), and a scenario fails only when it is over its brotli cap +// AND its minified size exceeds that recorded size by more than +// MINIFIED_ALLOWANCE (or the scenario's `minifiedAllowance`). Over the cap +// within the allowance passes with a warning stating the headroom left. +// Measuring against the recorded size, not the PR's base, bounds growth +// across PRs: two +15 B PRs on an over-cap scenario cannot both pass. +// +// Fail-safe: a cap with no recorded minified falls back to the minified +// growth over the PR's base (the PR's base commit, or on a push to next the +// commit before it). Without that either (a new scenario, a base the head's +// harness could not measure, a local run) the cap is absolute. // // Usage: node gate.mjs [base.json] // Exits non-zero when any scenario fails. In GitHub Actions each warning @@ -23,10 +28,17 @@ import { fileURLToPath } from "node:url"; // counts as real. Minified is the deterministic unit; brotli is not. export const MINIFIED_ALLOWANCE = 20; +const signed = d => `${d >= 0 ? "+" : "−"}${Math.abs(d)} B`; +const bytes = n => `${n.toLocaleString("en-US")} B`; +const REAL_GROWTH = + "real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need `Size-Exception:`)"; + /** - * One scenario's verdict. - * @returns {{ name: string, verdict: "pass" | "warn" | "fail", overBy?: number, - * minDelta?: number, allowance?: number, message: string }} + * One scenario's verdict. `against` says what the minified size was judged + * against: the minified recorded with the cap, or (fail-safe) the base. + * @returns {{ name: string, verdict: "pass" | "warn" | "fail", + * against?: "recorded" | "base" | "none", overBy?: number, minDelta?: number, + * baseDelta?: number, headroom?: number, allowance?: number, message: string }} */ export function decide(head, base) { const { name } = head; @@ -34,32 +46,58 @@ export function decide(head, base) { if (head.size <= head.limit) return { name, verdict: "pass", message: "within its cap" }; const overBy = head.size - head.limit; const allowance = head.minifiedAllowance ?? MINIFIED_ALLOWANCE; - if (!base || base.error || typeof base.minified !== "number") + const hasBase = base && !base.error && typeof base.minified === "number"; + const baseDelta = hasBase ? head.minified - base.minified : undefined; + const thisPR = hasBase ? `; ${signed(baseDelta)} minified over this PR's base` : ""; + + if (typeof head.capMinified === "number") { + const minDelta = head.minified - head.capMinified; + const headroom = allowance - minDelta; + const vs = `minified ${bytes(head.minified)} vs ${bytes(head.capMinified)} recorded with the cap (${signed(minDelta)})`; + const common = { name, against: "recorded", overBy, minDelta, baseDelta, headroom, allowance }; + if (headroom >= 0) + return { + ...common, + verdict: "warn", + message: `over brotli cap by ${overBy} B; ${vs} — ${headroom} B of the ${allowance} B minified allowance left${thisPR}` + }; + return { + ...common, + verdict: "fail", + message: `over brotli cap by ${overBy} B; ${vs} — ${-headroom} B past the ${allowance} B minified allowance${thisPR}. ${REAL_GROWTH}` + }; + } + + if (!hasBase) return { name, verdict: "fail", + against: "none", overBy, allowance, - message: `over brotli cap by ${overBy} B, no base measurement to compare minified growth against` + message: `over brotli cap by ${overBy} B; no minified recorded with the cap and no base measurement to compare against` }; - const minDelta = head.minified - base.minified; - const signed = `${minDelta >= 0 ? "+" : "−"}${Math.abs(minDelta)} B`; - if (minDelta <= allowance) + const headroom = allowance - baseDelta; + const common = { + name, + against: "base", + overBy, + minDelta: baseDelta, + baseDelta, + headroom, + allowance + }; + const vs = `no minified recorded with the cap, so judged against this PR's base: minified ${signed(baseDelta)}`; + if (headroom >= 0) return { - name, + ...common, verdict: "warn", - overBy, - minDelta, - allowance, - message: `over brotli cap by ${overBy} B, minified ${signed} — layout noise; cap will be re-based at the next ratchet` + message: `over brotli cap by ${overBy} B; ${vs} — ${headroom} B of the ${allowance} B minified allowance left` }; return { - name, + ...common, verdict: "fail", - overBy, - minDelta, - allowance, - message: `over brotli cap by ${overBy} B, minified ${signed} (allowance ${allowance} B) — real growth: reduce it, or raise the cap in this PR with a reason (frozen floors need \`Size-Exception:\`)` + message: `over brotli cap by ${overBy} B; ${vs} — ${-headroom} B past the ${allowance} B minified allowance. ${REAL_GROWTH}` }; } @@ -101,6 +139,12 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { `::${v.verdict === "warn" ? "warning" : "error"} title=${prop(`size: ${v.name}`)}::${data(v.message)}` ); } + const fallback = verdicts.filter(v => v.against === "base" || v.against === "none"); + if (fallback.length) + console.log( + `\ngate: ${fallback.length} over-cap scenario(s) have no minified recorded with the cap; ` + + "judged against the base instead (fail-safe)." + ); const failed = verdicts.some(v => v.verdict === "fail"); const warned = verdicts.filter(v => v.verdict === "warn").length; console.log( diff --git a/scripts/size/gate.test.mjs b/scripts/size/gate.test.mjs index 301bd88b0..e3f3b7ad9 100644 --- a/scripts/size/gate.test.mjs +++ b/scripts/size/gate.test.mjs @@ -3,46 +3,100 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { decide, decideAll, MINIFIED_ALLOWANCE } from "./gate.mjs"; -const head = (size, minified, extra = {}) => ({ name: "s", size, minified, limit: 1000, ...extra }); +// A scenario capped at 1000 B brotli, with 4000 B minified recorded with the cap. +const head = (size, minified, extra = {}) => ({ + name: "s", + size, + minified, + limit: 1000, + capMinified: 4000, + ...extra +}); +const unrecorded = (size, minified, extra = {}) => { + const h = head(size, minified, extra); + delete h.capMinified; + return h; +}; const base = minified => ({ name: "s", size: 990, minified }); test("under the cap passes whatever minified did", () => { assert.equal(decide(head(1000, 5000), base(4000)).verdict, "pass"); - assert.equal(decide(head(990, 5000)).verdict, "pass"); + assert.equal(decide(unrecorded(990, 5000)).verdict, "pass"); }); -test("over the cap with minified growth within the allowance warns", () => { - const v = decide(head(1050, 4000 + MINIFIED_ALLOWANCE), base(4000)); +test("over the cap within the allowance of the recorded minified warns with the headroom left", () => { + const v = decide(head(1050, 4012), base(4000)); assert.equal(v.verdict, "warn"); + assert.equal(v.against, "recorded"); assert.equal(v.overBy, 50); - assert.equal(v.minDelta, MINIFIED_ALLOWANCE); + assert.equal(v.minDelta, 12); + assert.equal(v.headroom, MINIFIED_ALLOWANCE - 12); assert.equal( v.message, - `over brotli cap by 50 B, minified +${MINIFIED_ALLOWANCE} B — layout noise; cap will be re-based at the next ratchet` + `over brotli cap by 50 B; minified 4,012 B vs 4,000 B recorded with the cap (+12 B) — ${MINIFIED_ALLOWANCE - 12} B of the ${MINIFIED_ALLOWANCE} B minified allowance left; +12 B minified over this PR's base` ); + assert.doesNotMatch(v.message, /re-base/); +}); + +test("exactly at the allowance warns; one byte past fails", () => { + assert.equal(decide(head(1001, 4000 + MINIFIED_ALLOWANCE)).verdict, "warn"); + const v = decide(head(1001, 4000 + MINIFIED_ALLOWANCE + 1), base(4000)); + assert.equal(v.verdict, "fail"); + assert.equal(v.headroom, -1); + assert.match(v.message, /1 B past the 20 B minified allowance/); +}); + +test("creep: two successive +15 B PRs on an over-cap scenario — the second fails", () => { + // PR 1 on a base at the recorded size: +15 B, over the cap on layout noise. + const pr1 = decide(head(1030, 4015), base(4000)); + assert.equal(pr1.verdict, "warn"); + assert.equal(pr1.headroom, 5); + // PR 1 merged; PR 2 adds another +15 B on top of it. Against its own base + // that is +15 B again, but against the recorded minified it is +30 B. + const pr2 = decide(head(1030, 4030), base(4015)); + assert.equal(pr2.verdict, "fail"); + assert.equal(pr2.baseDelta, 15); + assert.equal(pr2.minDelta, 30); + assert.match(pr2.message, /\+15 B minified over this PR's base/); + // The per-PR comparison alone (no recorded minified) would have let it through. + assert.equal(decide(unrecorded(1030, 4030), base(4015)).verdict, "warn"); }); -test("over the cap after shrinking minified warns", () => { +test("under the cap again after creep passes; the recorded minified does not move", () => { + assert.equal(decide(head(995, 4030), base(4015)).verdict, "pass"); +}); + +test("shrinking minified over the cap warns with more headroom", () => { const v = decide(head(1003, 3946), base(4000)); assert.equal(v.verdict, "warn"); - assert.match(v.message, /minified −54 B/); + assert.equal(v.headroom, MINIFIED_ALLOWANCE + 54); + assert.match(v.message, /\(−54 B\)/); }); -test("over the cap with minified growth past the allowance fails", () => { - const v = decide(head(1001, 4000 + MINIFIED_ALLOWANCE + 1), base(4000)); - assert.equal(v.verdict, "fail"); - assert.equal(v.minDelta, MINIFIED_ALLOWANCE + 1); +test("a scenario's minifiedAllowance overrides the default", () => { + assert.equal(decide(head(1001, 4030, { minifiedAllowance: 40 })).verdict, "warn"); + assert.equal(decide(head(1001, 4010, { minifiedAllowance: 0 })).verdict, "fail"); }); -test("a scenario's minifiedAllowance overrides the default", () => { - assert.equal(decide(head(1001, 4030, { minifiedAllowance: 40 }), base(4000)).verdict, "warn"); - assert.equal(decide(head(1001, 4010, { minifiedAllowance: 0 }), base(4000)).verdict, "fail"); +test("fail-safe: no recorded minified falls back to the base and says so", () => { + const warn = decide(unrecorded(1050, 4020), base(4000)); + assert.equal(warn.verdict, "warn"); + assert.equal(warn.against, "base"); + assert.match(warn.message, /no minified recorded with the cap, so judged against this PR's base/); + const fail = decide(unrecorded(1050, 4021), base(4000)); + assert.equal(fail.verdict, "fail"); + assert.equal(fail.against, "base"); +}); + +test("no recorded minified and no usable base: the cap is absolute", () => { + assert.equal(decide(unrecorded(1001, 4000)).verdict, "fail"); + assert.equal(decide(unrecorded(1001, 4000), { name: "s", error: "boom" }).verdict, "fail"); + assert.equal(decide(unrecorded(1000, 4000)).verdict, "pass"); }); -test("without a usable base the cap is absolute", () => { - assert.equal(decide(head(1001, 4000)).verdict, "fail"); - assert.equal(decide(head(1001, 4000), { name: "s", error: "boom" }).verdict, "fail"); - assert.equal(decide(head(1000, 4000)).verdict, "pass"); +test("a recorded minified does not need a base", () => { + assert.equal(decide(head(1001, 4010)).verdict, "warn"); + assert.equal(decide(head(1001, 4021)).verdict, "fail"); }); test("a scenario that failed to bundle fails", () => { diff --git a/scripts/size/ratchet.mjs b/scripts/size/ratchet.mjs index 9f0c20494..79299e223 100644 --- a/scripts/size/ratchet.mjs +++ b/scripts/size/ratchet.mjs @@ -5,18 +5,28 @@ // Run per RC, on the release candidate's next, from CI's measurement // (`--from`): local and CI artifacts differ by tens of brotli bytes. // +// Each cap carries the minified size recorded when it was set +// (`capMinified`; gate.mjs measures minified growth against it). A lowered +// cap records the minified measured with it — the two are one measurement. +// A cap that is not lowered only ever has its recorded minified LOWERED (or +// recorded for the first time when it has none): raising it would loosen the +// gate exactly as raising the cap would, so that stays a PR decision too. +// // Inline caps are rewritten in scenarios.js with a dated ledger line above // each; the frozen floors in floor-caps.json are rewritten too, with their // ledger line on the scenario in scenarios.js, and are listed in their own -// table so a floor change is seen as one. Scenarios still over their cap -// after re-measuring are listed and left alone: the ratchet does not raise. +// tables so a floor change is seen as one. Scenarios still over their cap, +// or above their recorded minified, are listed and left alone. // -// Usage: node ratchet.mjs [--from ] [--note ] [--dry-run] -// --from ratchet from a size.mjs --json measurement (CI's -// size-head artifact of a push to next) instead of -// measuring this checkout -// --note recorded in each ledger line (e.g. "RC.7, next @ abc1234") -// --dry-run print the tables, write nothing +// Usage: node ratchet.mjs [--from ] [--note ] +// [--minified-only] [--dry-run] +// --from ratchet from a size.mjs --json measurement (CI's +// size-head artifact of a push to next) instead of +// measuring this checkout +// --note recorded in each ledger line (e.g. "RC.7, next @ abc1234") +// --minified-only leave every cap alone; only record missing minified +// sizes and lower recorded ones +// --dry-run print the tables, write nothing import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -27,6 +37,21 @@ const HEADROOM = 10; /** Measured brotli + headroom, rounded up to the next 10 B (0.01 KB). */ const capFor = br => Math.ceil((br + HEADROOM) / 10) * 10; const formatCap = bytes => `${(bytes / 1000).toFixed(2)} KB`; +const fmt = n => n.toLocaleString("en-US"); + +/** `text` as `//` comment lines of at most 80 columns at `indent`. */ +const comment = (text, indent) => { + const lines = []; + let line = ""; + for (const word of text.split(" ")) { + if (line && indent.length + 3 + line.length + 1 + word.length > 80) { + lines.push(line); + line = word; + } else line = line ? `${line} ${word}` : word; + } + lines.push(line); + return lines.map(l => `${indent}// ${l}\n`).join(""); +}; const args = process.argv.slice(2); const flag = name => { @@ -36,6 +61,7 @@ const flag = name => { const fromFile = flag("--from"); const note = flag("--note"); const dryRun = args.includes("--dry-run"); +const minifiedOnly = args.includes("--minified-only"); let measured; if (fromFile) { @@ -54,7 +80,12 @@ if (fromFile) { const floorFile = join(here, "floor-caps.json"); const scenariosFile = join(here, "scenarios.js"); -const floorCaps = JSON.parse(readFileSync(floorFile, "utf8")); +const floorCaps = Object.fromEntries( + Object.entries(JSON.parse(readFileSync(floorFile, "utf8"))).map(([name, e]) => [ + name, + typeof e === "string" ? { cap: e } : e + ]) +); let source = readFileSync(scenariosFile, "utf8"); const errors = measured.filter(m => m.error); @@ -67,28 +98,41 @@ if (errors.length) { const date = new Date().toLocaleDateString("en-CA"); const origin = fromFile ? "CI-measured" : `measured locally (${process.platform})`; -const lowered = { inline: [], floor: [] }; +const changed = { inline: [], floor: [] }; const stillOver = []; +const aboveRecorded = []; for (const scenario of scenarios) { const m = measured.find(x => x.name === scenario.name); if (!m) { console.error(`ratchet: no measurement for "${scenario.name}"; nothing written.`); process.exit(1); } - const cap = toBytes(scenario.limit); - const next = capFor(m.size); - if (m.size > cap) - stillOver.push({ name: scenario.name, cap, size: m.size, minified: m.minified }); - if (next >= cap) continue; const floor = scenario.name in floorCaps; + const cap = toBytes(scenario.limit); + const recorded = scenario.capMinified; + if (m.size > cap) stillOver.push({ name: scenario.name, cap, size: m.size }); + + let newCap = cap; + let newRecorded = recorded; + if (!minifiedOnly && capFor(m.size) < cap) { + newCap = capFor(m.size); + newRecorded = m.minified; + } else if (typeof recorded !== "number" || m.minified < recorded) { + newRecorded = m.minified; + } else if (m.minified > recorded) { + aboveRecorded.push({ name: scenario.name, recorded, minified: m.minified }); + } + if (newCap === cap && newRecorded === recorded) continue; + const row = { name: scenario.name, from: formatCap(cap), - to: formatCap(next), + to: formatCap(newCap), size: m.size, - minified: m.minified + recFrom: recorded, + recTo: newRecorded }; - (floor ? lowered.floor : lowered.inline).push(row); + (floor ? changed.floor : changed.inline).push(row); // The scenario's block runs from its `name:` to the next scenario's. const start = source.indexOf(`name: ${JSON.stringify(scenario.name)},`); @@ -99,41 +143,91 @@ for (const scenario of scenarios) { const at = block.indexOf(limitText); if (at < 0) throw new Error(`ratchet: no \`${limitText}\` line for "${scenario.name}"`); const lineStart = block.lastIndexOf("\n", at) + 1; + const lineEnd = block.indexOf("\n", at); const indent = block.slice(lineStart, at); - const ledger = - `${indent}// Ratchet (${date}${note ? `, ${note}` : ""}): ${row.from} -> ${row.to}, ${origin} at\n` + - `${indent}// ${m.size.toLocaleString("en-US")} B (${m.minified.toLocaleString("en-US")} B minified). ` + - `Lower only: measured + ${HEADROOM} B rounded up to 0.01 KB.\n`; - const rest = floor - ? block.slice(lineStart) - : block.slice(lineStart).replace(limitText, `limit: ${JSON.stringify(row.to)},`); - if (floor) floorCaps[scenario.name] = row.to; - source = source.slice(0, start) + block.slice(0, lineStart) + ledger + rest + source.slice(end); + const recordedText = + typeof recorded === "number" + ? `recorded minified ${fmt(recorded)} -> ${fmt(newRecorded)} B` + : `recorded minified ${fmt(newRecorded)} B (first record)`; + const what = + newCap !== cap + ? `${row.from} -> ${row.to}, ${recordedText}` + : `cap unchanged at ${row.from}, ${recordedText}`; + const ledger = comment( + `Ratchet (${date}${note ? `, ${note}` : ""}): ${what}; ${origin} at ${fmt(m.size)} B ` + + `(${fmt(m.minified)} B minified). Lower only: cap at measured + ${HEADROOM} B rounded up ` + + `to 0.01 KB; recorded minified never raised.`, + indent + ); + + let limitLine = block.slice(lineStart, lineEnd); + let after = block.slice(lineEnd); + if (floor) { + floorCaps[scenario.name] = { cap: row.to, minified: newRecorded }; + } else { + if (newCap !== cap) + limitLine = limitLine.replace(limitText, `limit: ${JSON.stringify(row.to)},`); + const recordedLine = /\n *capMinified: \d+,/; + after = recordedLine.test(after) + ? after.replace(recordedLine, `\n${indent}capMinified: ${newRecorded},`) + : `\n${indent}capMinified: ${newRecorded},` + after; + } + source = + source.slice(0, start) + + block.slice(0, lineStart) + + ledger + + limitLine + + after + + source.slice(end); } const table = rows => - ["| scenario | cap | measured | new cap |", "| --- | ---: | ---: | ---: |"] - .concat(rows.map(r => `| ${r.name} | ${r.from} | ${r.size} B | ${r.to} |`)) + [ + "| scenario | cap | new cap | measured | recorded minified | new recorded |", + "| --- | ---: | ---: | ---: | ---: | ---: |" + ] + .concat( + rows.map( + r => + `| ${r.name} | ${r.from} | ${r.to === r.from ? "—" : r.to} | ${r.size} B | ${r.recFrom ?? "none"} | ${r.recTo} |` + ) + ) .join("\n"); -console.log(`ratchet: ${origin}${note ? `, ${note}` : ""}${dryRun ? " (dry run)" : ""}\n`); -console.log(`Inline caps lowered (scenarios.js): ${lowered.inline.length}`); -if (lowered.inline.length) console.log(table(lowered.inline)); -console.log(`\nFROZEN FLOOR caps lowered (floor-caps.json): ${lowered.floor.length}`); -if (lowered.floor.length) console.log(table(lowered.floor)); -if (stillOver.length) { +console.log( + `ratchet: ${origin}${note ? `, ${note}` : ""}${minifiedOnly ? " (minified only)" : ""}${dryRun ? " (dry run)" : ""}\n` +); +console.log(`Inline caps changed (scenarios.js): ${changed.inline.length}`); +if (changed.inline.length) console.log(table(changed.inline)); +console.log(`\nFROZEN FLOOR caps changed (floor-caps.json): ${changed.floor.length}`); +if (changed.floor.length) console.log(table(changed.floor)); +const pairedUp = [...changed.inline, ...changed.floor].filter( + r => r.to !== r.from && typeof r.recFrom === "number" && r.recTo > r.recFrom +); +if (pairedUp.length) + console.log( + `\nRecorded minified went UP with a lowered cap (one measurement, brotli shrank): ${pairedUp.length}\n` + + pairedUp.map(r => ` ${r.name}: ${r.recFrom} -> ${r.recTo} B`).join("\n") + ); +if (stillOver.length) console.log( `\nStill over their cap (not raised — the ratchet only lowers): ${stillOver.length}\n` + stillOver .map(s => ` ${s.name}: ${s.size} B > ${formatCap(s.cap)} (+${s.size - s.cap} B)`) .join("\n") ); -} +if (aboveRecorded.length) + console.log( + `\nAbove their recorded minified (not raised): ${aboveRecorded.length}\n` + + aboveRecorded + .map(s => ` ${s.name}: ${s.minified} B > ${s.recorded} B (+${s.minified - s.recorded} B)`) + .join("\n") + ); -if (!dryRun && (lowered.inline.length || lowered.floor.length)) { +if (!dryRun && (changed.inline.length || changed.floor.length)) { writeFileSync(scenariosFile, source); writeFileSync(floorFile, JSON.stringify(floorCaps, null, 2) + "\n"); console.log("\nratchet: scenarios.js and floor-caps.json rewritten."); } else if (!dryRun) { - console.log("\nratchet: no cap to lower."); + console.log("\nratchet: nothing to change."); } diff --git a/scripts/size/report.mjs b/scripts/size/report.mjs index fdc879942..0429f02df 100644 --- a/scripts/size/report.mjs +++ b/scripts/size/report.mjs @@ -1,7 +1,7 @@ // Renders the PR size comment: head vs base per scenario, from two size.mjs // --json files, with gate.mjs's verdict for each. Base entries may be missing // (a new scenario, or a base checkout the head's scenarios cannot bundle); -// those rows show "—" and their cap is absolute. +// those rows show "—". // // Usage: node report.mjs [base.json] > comment.md @@ -31,24 +31,34 @@ const rows = head.map((h, i) => { const comparable = !h.error && b && !b.error; const change = comparable ? delta(h.size, b.size) : "—"; const minChange = comparable ? signed(h.minified - b.minified) : "—"; + const minRecorded = + !h.error && typeof h.capMinified === "number" ? signed(h.minified - h.capMinified) : "—"; const cap = toKB(h.limit); const status = h.error ? "❌ error" : v.verdict === "pass" ? "✅" : v.verdict === "warn" - ? `⚠️ over by ${v.overBy} B, noise` + ? `⚠️ over by ${v.overBy} B, ${v.headroom} B minified headroom` : `❌ over by ${v.overBy} B`; const lazy = h.lazy?.length ? h.lazy.map(c => `${c.name} ${toKB(c.br)}`).join(", ") : ""; - return `| ${h.name} | ${size} | ${change} | ${minChange} | ${cap} | ${status} | ${lazy} |`; + return `| ${h.name} | ${size} | ${change} | ${minChange} | ${minRecorded} | ${cap} | ${status} | ${lazy} |`; }); console.log(""); console.log("## Size (brotli, eager entry chunk)\n"); -console.log("| scenario | head | vs base | minified vs base | cap | | lazy chunks (not counted) |"); -console.log("| --- | ---: | ---: | ---: | ---: | --- | --- |"); +console.log( + "| scenario | head | vs base | minified vs base | minified vs recorded | cap | | lazy chunks (not counted) |" +); +console.log("| --- | ---: | ---: | ---: | ---: | ---: | --- | --- |"); console.log(rows.join("\n")); +const fallback = verdicts.filter(v => v.against === "base" || v.against === "none"); +if (fallback.length) + console.log( + `\n> **Fail-safe:** ${fallback.length} over-cap scenario(s) have no minified recorded with the cap and were judged against this PR's base instead: ${fallback.map(v => v.name).join("; ")}.` + ); + const warned = verdicts.filter(v => v.verdict === "warn"); const failed = verdicts.filter(v => v.verdict === "fail"); if (warned.length) { @@ -60,5 +70,5 @@ if (failed.length) { for (const v of failed) console.log(`- **${v.name}**: ${v.message}`); } console.log( - `\nBundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap **and** grew more than ${MINIFIED_ALLOWANCE} B minified over the base; over the cap within that allowance is brotli layout noise and passes with a warning. Caps in \`scripts/size/scenarios.js\`; the floor and page caps in \`floor-caps.json\` are frozen (lower only, or \`Size-Exception:\` in the PR body). Caps are re-based downward by \`npm run ratchet\` (scripts/size/README.md).` + `\nBundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap **and** its minified size is more than ${MINIFIED_ALLOWANCE} B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in \`scripts/size/scenarios.js\`; the floor and page caps in \`floor-caps.json\` are frozen (lower only, or \`Size-Exception:\` in the PR body). \`npm run ratchet\` lowers caps per RC; it never raises one (scripts/size/README.md).` ); diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 45fab4d25..f5e503529 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -18,6 +18,14 @@ // overrides it per scenario. Limits are lowered (never raised) per RC by // ratchet.mjs, which writes a dated `Ratchet` line above each one it moves. // +// Recorded minified (2026-10-06): `capMinified` (floor-caps.json's +// `minified` for the floors) is the minified size measured when the limit +// was set; the allowance is measured against it rather than per PR, so +// growth cannot creep across PRs. Set it with the limit, from the same CI +// measurement. Seeded for every scenario from CI on next @ fff1615ee (Size run +// 37443080193), limits unchanged; the base comparison is the fail-safe for a +// limit without one. +// // Bundler switch (2026-09-26): the harness measured with esbuild through // size-limit until this date; it now measures with Rolldown, the bundler Vite // ships, pinned exactly in package.json. Every cap was re-based on that day @@ -44,7 +52,15 @@ const alias = { // without a `Size-Exception:` line in its body. Lowering is always allowed. // The dated notes on each scenario below remain the ledger of how the // floor got here. -const floorCaps = require("./floor-caps.json"); +// Each entry is { cap, minified }: the brotli cap and the minified size +// measured when it was set (the gate measures minified growth against it). +const floorFile = require("./floor-caps.json"); +const floorCaps = Object.fromEntries( + Object.entries(floorFile).map(([name, e]) => [name, typeof e === "string" ? e : e.cap]) +); +const floorMinified = Object.fromEntries( + Object.entries(floorFile).map(([name, e]) => [name, e.minified]) +); // Server-component PAGES (audit §1): everything such a page ships eagerly, // nothing external — the frames client and the server-function transport @@ -476,7 +492,12 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 7.35 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 7.35 KB, recorded minified 20,133 B (first record); + // CI-measured at 7,361 B (20,133 B minified). Lower only: cap at measured + + // 10 B rounded up to 0.01 KB; recorded minified never raised. limit: floorCaps["signals: core floor (createSignal/Memo/Effect/Root/flush)"], + capMinified: floorMinified["signals: core floor (createSignal/Memo/Effect/Root/flush)"], alias }, { @@ -894,7 +915,12 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 14.56 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 14.56 KB, recorded minified 44,396 B (first record); + // CI-measured at 14,540 B (44,396 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "14.56 KB", + capMinified: 44396, alias }, { @@ -1135,7 +1161,12 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 9.49 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 9.49 KB, recorded minified 26,828 B (first record); + // CI-measured at 9,513 B (26,828 B minified). Lower only: cap at measured + + // 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "9.49 KB", + capMinified: 26828, alias }, { @@ -1325,7 +1356,12 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 9.86 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 9.86 KB, recorded minified 27,687 B (first record); + // CI-measured at 9,856 B (27,687 B minified). Lower only: cap at measured + + // 10 B rounded up to 0.01 KB; recorded minified never raised. limit: floorCaps["app: render + one signal (the simple-app floor)"], + capMinified: floorMinified["app: render + one signal (the simple-app floor)"], alias }, { @@ -1646,7 +1682,12 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 17.71 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 17.71 KB, recorded minified 52,567 B (first record); + // CI-measured at 17,672 B (52,567 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: floorCaps["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"], + capMinified: floorMinified["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"], alias }, { @@ -2070,7 +2111,12 @@ module.exports = [ // render-effect mark in laneDirty (+4). Cap set at measured + 10 B // rounded up to 0.01 KB. Accepted by the maintainer (2026-10-05). The // cap is frozen again at 28.87 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 28.87 KB, recorded minified 91,625 B (first record); + // CI-measured at 28,888 B (91,625 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "28.87 KB", + capMinified: 91625, alias }, { @@ -2292,7 +2338,12 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 12.86 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 12.86 KB, recorded minified 36,568 B (first record); + // CI-measured at 12,905 B (36,568 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "12.86 KB", + capMinified: 36568, alias }, { @@ -2564,7 +2615,12 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 14.46 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 14.46 KB, recorded minified 41,091 B (first record); + // CI-measured at 14,449 B (41,091 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "14.46 KB", + capMinified: 41091, alias: observeAlias }, { @@ -2955,7 +3011,12 @@ module.exports = [ // core only; brotli layout over the engine's bundle. Cap set at // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // (2026-10-05). The cap is frozen again at 28.66 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 28.66 KB, recorded minified 86,419 B (first record); + // CI-measured at 28,639 B (86,419 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "28.66 KB", + capMinified: 86419, alias: observeAlias }, // Compiled-template scenarios (2026-10-05): the four `app:` fixtures above @@ -2988,7 +3049,12 @@ module.exports = [ // CI-measured + 10 B rounded up to 0.01 KB. path: "fixtures/compiled/floor.jsx", compile: { hydratable: false }, + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 10.05 KB, recorded minified 28,218 B (first record); + // CI-measured at 10,031 B (28,218 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "10.05 KB", + capMinified: 28218, alias }, { @@ -3024,7 +3090,12 @@ module.exports = [ // Cap at CI-measured + 10 B rounded up to 0.01 KB. path: "fixtures/compiled/csr.jsx", compile: { hydratable: false }, + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 25.13 KB, recorded minified 78,898 B (first record); + // CI-measured at 25,129 B (78,898 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "25.13 KB", + capMinified: 78898, alias }, { @@ -3055,7 +3126,12 @@ module.exports = [ // web=16,591 app=3,058). Cap at CI-measured + 10 B rounded up to 0.01 KB. path: "fixtures/compiled/hydrating.jsx", compile: { hydratable: true }, + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 30.93 KB, recorded minified 99,198 B (first record); + // CI-measured at 30,957 B (99,198 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "30.93 KB", + capMinified: 99198, alias }, { @@ -3263,7 +3339,12 @@ module.exports = [ // reads the new response's refs before the commit. Accepted by the // maintainer (2026-10-04). The cap is frozen again at 13.78 KB (head + // 10 B). + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 13.78 KB, recorded minified 43,310 B (first record); + // CI-measured at 13,770 B (43,310 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: "13.78 KB", + capMinified: 43310, alias: framesAlias, external: framesExternal }, @@ -3422,7 +3503,13 @@ module.exports = [ // +16, observeFlight first-observer hold +17); brotli layout. Cap set at // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // (2026-10-05). The cap is frozen again at 44.84 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 44.84 KB, recorded minified 145,599 B (first record); + // CI-measured at 44,864 B (145,599 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"], + capMinified: + floorMinified["page: base server components (hydrating + dynamic + frames + sf reference)"], alias: pageAlias }, { @@ -3529,7 +3616,13 @@ module.exports = [ // lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded // up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is // frozen again at 48.51 KB. + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 48.51 KB, recorded minified 157,562 B (first record); + // CI-measured at 48,527 B (157,562 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: floorCaps["page: live server components (base + live/GET + action + isPending/latest)"], + capMinified: + floorMinified["page: live server components (base + live/GET + action + isPending/latest)"], alias: pageAlias }, { @@ -3552,7 +3645,12 @@ module.exports = [ // shaking. path: "../../packages/web/dist/server.js", import: "{ getRequestEvent, isServer }", + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 1.34 KB, recorded minified 3,324 B (first record); + // CI-measured at 1,331 B (3,324 B minified). Lower only: cap at measured + + // 10 B rounded up to 0.01 KB; recorded minified never raised. limit: floorCaps["server: floor (getRequestEvent + isServer)"], + capMinified: floorMinified["server: floor (getRequestEvent + isServer)"], alias: serverAlias, platform: "node", conditions: serverConditions @@ -3584,7 +3682,12 @@ module.exports = [ // maintainer. The cap is frozen again at 20.42 KB. path: "../../packages/web/dist/server.js", import: "{ renderToString }", + // Ratchet (2026-10-06, seed, next @ fff1615ee, Size run 37443080193): cap + // unchanged at 20.42 KB, recorded minified 71,813 B (first record); + // CI-measured at 20,412 B (71,813 B minified). Lower only: cap at measured + // + 10 B rounded up to 0.01 KB; recorded minified never raised. limit: floorCaps["server: renderToString (the server-render floor)"], + capMinified: floorMinified["server: renderToString (the server-render floor)"], alias: serverAlias, platform: "node", conditions: serverConditions diff --git a/scripts/size/size.mjs b/scripts/size/size.mjs index eb21957e8..a6feaa86c 100644 --- a/scripts/size/size.mjs +++ b/scripts/size/size.mjs @@ -65,6 +65,7 @@ for (const scenario of scenarios) { minified: r.min, limit: cap, passed: !over, + ...(typeof scenario.capMinified === "number" && { capMinified: scenario.capMinified }), ...(scenario.minifiedAllowance !== undefined && { minifiedAllowance: scenario.minifiedAllowance }),