Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,39 @@ action must perform one; a page that ships must be reachable.
allowlist (redirect targets / legacy-compat routes). Both run in `verify:cheap` and CI. Mockups
(`src/app/mockups/**`, `*-mockups.tsx`) are design-scratch and exempt from both — **and from
nothing else**. Mockups are compiled like any other source: they are typechecked, and their client
chunks count toward `check:bundle-budget`'s repo-wide total, so a mockup-only PR can still fail
`Build` (PR #1580, `+10.1%` against a 10% tolerance). Do not read "exempt" as "free".
chunks are still weighed by `check:bundle-budget` — against the separate `mockups` scratch budget,
not the `production` one (reconciled 2026-08-09; see "Bundle budget" below). Do not read "exempt"
as "free".
- **Never** add a production page route without either an inbound link or a documented
reachability allowlist entry plus an `/issues` note, and never silence the button-wiring rule
with a blanket disable — wire the control or make it an explicit placeholder.

# Bundle budget

`check:bundle-budget` enforces **two** baselines in `bundle-budget.json`, because one number could
not honestly answer both questions (`#013` vs `#252`, reconciled 2026-08-09):

- **`production`** — every chunk a non-mockup route reaches, plus chunks no route manifest claims
(framework, polyfills, runtime). This is user-facing weight and the real regression guard.
Tolerance 10%. A failure here means find the regression; do not refresh the baseline to clear it.
- **`mockups`** — chunks reachable **only** from `/mockups/**`. Nobody downloads these, so this is a
repo-hygiene ceiling for unbounded accumulation, not a per-mockup gate. Tolerance 25%.

A chunk shared by a mockup and a production route counts as production — it would be built either
way. Attribution comes from the per-route `*_client-reference-manifest.js` files under
`.next/server/app`; if that tree is missing or resolves no routes the check **fails closed** rather
than collapsing the two buckets.

Why the split rather than a raised ceiling: measured on `main` at `af85cbc`, the repo-wide total was
+9.96% of the old single baseline — 576 bytes from failing `Build` — while production-only was
**9.06% below** it. Every byte of the apparent regression was design scratch; production had
actually shrunk since the baseline was captured. Raising the ceiling would have hidden that.

**Measuring:** `npm run build` reuses a cached `.next`, and the check then reads stale output and
reports byte-identical numbers — it will tell you the budget passes when it does not. Always
`rm -rf .next` before measuring, and sanity-check `.next/BUILD_ID`'s mtime against the current
commit before trusting a number.

<!-- END:page-and-button-wiring -->

<!-- BEGIN:search-chrome-behaviour -->
Expand Down
11 changes: 6 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,12 @@ These fail builds, so they are worth knowing before you write code:
- **Mockups are exempt from two gates, not all of them.** `src/app/mockups/**` and `*-mockups.tsx`
are design scratch and 404 in production, so they sit outside the **wiring** and **reachability**
gates — and nothing else. They are still compiled: they are typechecked like any source, and their
client chunks still count toward `check:bundle-budget`, which totals **every** built chunk rather
than the initial production bundle. A mockup-only PR can therefore fail `Build` on bundle budget
(PR #1580: `+10.1% vs baseline`, tolerance 10%) even though the routes never serve a user. Budget
scope vs. the "not an initial production bundle" position in `/issues` `#013` is unreconciled —
see `#252` before assuming either number governs.
client chunks are still weighed by `check:bundle-budget` — but since 2026-08-09 against a separate
`mockups` scratch baseline (tolerance 25%), not the `production` one (tolerance 10%). That split
reconciled `/issues` `#013` and `#252`: the old single total charged design scratch against a
ceiling named as though it were production weight, which is how PR #1580 blocked at `+10.1%` for
chunks no user can load. See the "Bundle budget" section in `AGENTS.md`; a mockup-only PR can still
fail `Build`, just only on genuine runaway growth.

## Repo-specific tooling

Expand Down
14 changes: 11 additions & 3 deletions bundle-budget.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"$comment": "Client JS bundle-size budget captured from a known-good production build. CI fails when total gzip size grows beyond tolerancePct; refresh intentionally with `npm run check:bundle-budget -- --update`.",
"$comment": "Client JS bundle-size budgets captured from a known-good production build. Two numbers, two questions (see scripts/check-bundle-budget.mjs): `production` is what users actually download and is the real regression guard; `mockups` is design scratch that 404s in production and is a repo-hygiene ceiling for unbounded accumulation, not a per-mockup gate. CI fails when either grows beyond its tolerancePct; refresh intentionally with `npm run check:bundle-budget -- --update`.",
"enforce": true,
"production": {
"gzipBytes": 1309772,
"tolerancePct": 10
},
"mockups": {
"gzipBytes": 273873,
"tolerancePct": 25
},
"totalGzipBytes": 1583645,
"tolerancePct": 10,
"totalGzipBytes": 1440201,
"updatedAt": "2026-08-04T09:21:11.952Z"
"updatedAt": "2026-08-09T07:31:53.000Z"
}
29 changes: 15 additions & 14 deletions docs/outstanding-issues.md

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions docs/plans/document-viewer-phase3-handover.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ npm run verify:pr-local

Two gates will move and must not be silenced:

- **`check:bundle-budget`** totals _every_ built chunk against 1,440,201 gzip bytes at 10% tolerance
(`bundle-budget.json`). A virtualization dependency would land straight on it — prefer none.
- **`check:bundle-budget`** weighs production chunks against 1,309,772 gzip bytes at 10% tolerance,
and `/mockups/**`-only chunks against a separate 273,873-byte scratch baseline at 25%
(`bundle-budget.json`; split from the old single 1,440,201-byte total on 2026-08-09). A
virtualization dependency would land straight on the production budget — prefer none.
- **The `document-viewer` visual golden will shift.** Per ledger `#278` that target composites
viewport-pinned chrome over content, and its position tracks total content height, so any
content-height change inflates the diff. Expect it; fix it by narrowing the clip or masking the
Expand Down
23 changes: 22 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,28 @@ export PLAYWRIGHT_KEEP_BUILD_ROOT=true

**Refuted levers (do not revive):** persistent Actions cache for the Next webpack tree (~804 MB, evicts browser cache); splitting `ui-phone-scroll*` to rebalance `--shard` (siblings still co-land); renaming specs to game alphabetical shard order; Playwright `workers > 1` or blocking retries; dropping Production UI from ordinary UI PRs; Firefox/WebKit on every PR (main/weekly matrix only).

**Remote / Cloud browser drift.** When `check:installed-lock-parity` fails on `playwright`, or `check:playwright-browser-revision` reports `/opt/pw-browsers` revision drift, do **not** point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at a mismatched shell. Delegate browser proof to CI Production UI (or refresh the image/install matching browsers). See `#255` and [codex-cloud.md](codex-cloud.md).
**Remote / Cloud browser drift.** When `check:installed-lock-parity` fails on `playwright`, or `check:playwright-browser-revision` reports `/opt/pw-browsers` revision drift, do **not** point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at a mismatched shell and do **not** set `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` to force a run — a browser gate against the wrong revision is not evidence. Delegating browser proof to CI Production UI is always valid. Restoring the gates locally is also possible; the recipe below was verified end to end on 2026-08-09 (`#255`). See also [codex-cloud.md](codex-cloud.md).

Two separate image faults produce this, and the second is why the obvious fix looks impossible:

1. **The baked `node_modules` is stale or incomplete.** Symptoms range from no `node_modules` at all to `playwright: installed 1.62.0 does not match locked 1.62.1` with `tailwind-merge` missing entirely. The lockfile is not wrong — do not re-pin it to the installed version.
2. **The image's Node is too old to run `npm ci`.** `jsdom@30.0.1` requires `^22.22.2 || ^24.15.0 || >=26.0.0`; images have shipped v24.13.0, so `npm ci --include=dev` dies on `EBADENGINE` under `engine-strict=true`. Never bypass with `--force`, `--legacy-peer-deps`, or `--engine-strict=false`.

```bash
# 1. Node >= 24.15.0 (satisfies both the repo's 24.x engine and jsdom's floor).
curl -sSL -o /tmp/node24.tar.xz https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-x64.tar.xz
mkdir -p /root/.node24 && tar -xf /tmp/node24.tar.xz -C /root/.node24/
export PATH=/root/.node24/node-v24.19.0-linux-x64/bin:$PATH # node v24.19.0, npm 11.17.0

# 2. Real install. Expect exit 0; then parity prints all seven pinned packages.
npm ci --include=dev && npm run check:installed-lock-parity

# 3. Browsers. Playwright 1.62.1 wants Chromium 1234; images have shipped only 1194.
unset PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD
npx playwright install chromium chromium-headless-shell # installs into PLAYWRIGHT_BROWSERS_PATH
```

Costs roughly 5 minutes and ~330 MB (184 MB chromium + 115 MB headless shell + 32 MB node), needs a few GB free, and is paid **per session** because the container is ephemeral. The durable fix is still an image that ships Node ≥ 24.15.0, a complete `npm ci`, and the locked Chromium revision.

Codex Cloud agents remain provider-free. Run authenticated Supabase tests through the
manual `.github/workflows/authenticated-live-tests.yml` workflow, which requires the
Expand Down
Loading
Loading