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
64 changes: 64 additions & 0 deletions apps/web/docs/BUDGETS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Bundle Budgets β€” MBD Web App

Source of truth for ceilings lives in [`apps/web/src/build/bundleConfig.ts`](../src/build/bundleConfig.ts). This file documents the *why* behind each lift and the policy.

## Current ceilings

| Chunk class | Raw | Gzip |
| --- | --- | --- |
| Main-thread chunk | 304 KB | 81 KB |
| Worker chunk | 443 KB | 143 KB |
| Chart vendor (lazy `vendor-charts`) | 430 KB | 120 KB |

The main-thread chunk caps have not moved since launch β€” `MAIN_THREAD_CHUNK_BUDGET_BYTES` / `MAIN_THREAD_CHUNK_GZIP_BUDGET_BYTES` are deliberately frozen so any new app code that lands on the main thread surfaces as a regression in `bundleBudget.test.ts`.

## Lift policy

1. **Smallest safe lift.** A slice ships with the minimum cap move that restores headroom. No round-number gifting.
2. **Raw and gzip move independently.** A copy-heavy story slice usually only needs gzip; a wire-only worker slice usually only needs raw.
3. **Chunk routing first, ceiling lift second.** Before lifting a cap, check whether routing the new module into a different chunk (e.g. moving narrative prose into `game-engine-story` or `game-engine-capstone`) keeps `game-engine-core` lean.
4. **CI vs. local terser drift.** Local builds typically emit `game-engine-core` ~250–500 bytes smaller than CI. Lifts must include headroom for that drift; otherwise the budget test goes red only in CI.
5. **Document the rationale in the PR.** This file gets the *why*; the journal below records milestones.

## Worker chunk lift timeline

| Cap (raw / gzip) | Slice | Cause |
| --- | --- | --- |
| 406 / 124 KB | (pre-arbitration baseline) | β€” |
| 408 / 125 KB | Arbitration broadcast | Arbitration press-conference templates + moment descriptions. Smallest safe gzip lift after copy trim. |
| 410 / 126 KB | Holdout briefings + trade-deadline press | `generateHoldoutResolutionBriefing` and trade-deadline press copy. Raw-only lift first, gzip moved a tick later when trade-deadline copy crossed the cap by 471 bytes. |
| 411 / 126 KB | Team-moment store (v22) | `appendTeamMoments` + `teamMoments` map on `FullGameState` + identityMoments seam through `buildTradeBroadcastCoverage`. Wire-only. |
| 412 / 126 KB | Chase Watch query | `getChaseWatch()` assembling milestone alerts + pace chases. Pure aggregation. |
| 414 / 126 KB | Pennant-race-heat query | `getPennantRaces` with division race + wildcard bubble. |
| 417 / 127 KB | Team-moment types (v23) | `detectSeasonIdentityMoments` + `championship_run` / `contention_collapse` enum + description templates. |
| 420 / 127 KB | Career Retrospective query | `buildCareerRetrospective` unifying titles, beats, story arcs, awards, top rivalry. |
| 423 / 127 KB | Season Story Reel query | `buildSeasonStoryReel` per-season deep-dive. |
| 425 / 127 KB | Pennant Race Detail query | `getPennantRaceDetail` all-six-divisions + top-5 wildcard. |
| 425 / 128 KB | Team-identity wave 2 (v25) | `rebuild_begun` + `breakout_season` + `contention_window_opens` detectors. Gzip-only lift. |
| 428 / 131 KB | Narrative depth wave 3 (v26) | Additive detector/template expansion. Sprint hard cap. |
| 428 / 132 KB | CI terser drift recovery | One KB gzip lift to absorb a +534-byte CI vs. local minifier drift on an unchanged commit. |
| 432 / 134 KB | Narrative depth wave 4 (v27) | Persisted-state fidelity + playoff comeback tracking. |
| 433 / 134 KB | Career Retrospective season-history | `seasonHistory` derivation. Raw-only. |
| 434 / 135 KB | Codex narrative-prose reservation | Headroom reserved for parallel `codex/narrative-prose-expansion` branch (deterministic prose variant pools). |
| 434 / 137 KB | Codex narrative-prose landing | Lift confirmed after rivalry-wave1 (PR #50) stacked with prose pools in `game-engine-core`. |
| 436 / 137 KB | This Week in History query | `getThisWeekInHistory` over persisted moments maps. Raw-only. |
| 438 / 137 KB | Player Arcs of the Season query | `getPlayerArcsOfSeason` filtering redemption_arc / late_career_peak / rookie_breakout. Raw-only. |
| 439 / 139 KB | Narrative depth wave 6 | Dynasty-marker worker wiring + new prose pools. |
| 439 / 141 KB | Narrative depth wave 7 | Position-group moment detectors (`dominant_rotation` / `bullpen_collapse` / `lineup_of_era`). Gzip-only. |
| 440 / 143 KB | Narrative depth wave 8 | Player micro-arc worker source plumbing. |
| 442 / 143 KB | Narrative depth wave 9 | Weekly cadence detectors split into `game-engine-weekly` to protect `game-engine-core`. |
| 443 / 143 KB | Narrative depth wave 10 (current) | Capstone prose split into `game-engine-capstone`. |

## Notes on routing

Routing changes that have paid off:

- **`game-engine-onboarding`** isolates the Day-1 flow so it never lands in `game-engine-core`.
- **`game-engine-story`** absorbs holdout coverage and worker-only narrative payload, leaving the deterministic core lean.
- **`game-engine-capstone`** (Wave 10) splits award / HOF / retirement / stable prose variant pools from the core.
- **`game-engine-weekly`** (Wave 9) splits weekly cadence detectors + prose.
- **`game-engine-shell`** holds only the Comlink entry point and the sim-core root barrel, preventing circular chunk edges between `game-engine-core` and `game-engine-onboarding`.

Routing changes that did *not* pay off:

- Routing `tradeDeadlinePressConferences.ts` into `game-engine-story` pushed story over its own cap (421,762 raw / 125,530 gzip). The smaller ceiling lift on core was the safer fix.
167 changes: 2 additions & 165 deletions apps/web/src/build/bundleConfig.ts
Original file line number Diff line number Diff line change
@@ -1,170 +1,7 @@
export const MAIN_THREAD_CHUNK_BUDGET_BYTES = 304 * 1024;
export const MAIN_THREAD_CHUNK_GZIP_BUDGET_BYTES = 81 * 1024;
// WORKER raw: bumped 406 -> 408 KB to cover the arbitration payload carried across
// the arbitration schema/broadcast slices.
// WORKER raw: bumped 408 -> 410 KB for the holdout resolution news slice. Adds
// generateHoldoutResolutionBriefing + worker wire-in alongside the existing
// opening-beat briefings in game-engine-story. Copy was trimmed aggressively but
// the function signature + constants still landed ~1.3 KB over the prior raw
// ceiling. Gzip stayed at ~125 KB (well under budget), so the actual wire cost
// is unchanged β€” this is a raw-ceiling-only concession for the new helper.
// WORKER gzip: bumped 124 -> 125 KB for the arbitration broadcast slice. The
// arbitration press-conference templates + moment descriptions compressed less
// than projected (actual ~+0.4 KB over prior ceiling), so lift the gzip roof by
// one KB to restore headroom. Holdout briefings already live in the story chunk.
// WORKER gzip: bumped 125 -> 126 KB for the trade deadline broadcast slice after
// the new trade press-conference copy left game-engine-core 471 bytes over the
// prior gzip cap. Attempted routing tradeDeadlinePressConferences.ts into
// game-engine-story first, but that pushed story far higher overall (421,762
// raw / 125,530 gzip), so the smallest ceiling lift was the safer fix. Raw
// ceiling already at 410 KB from the holdout briefing slice accommodates the
// trade deadline worker growth (+613 bytes over the 408 baseline).
// WORKER raw: bumped 410 -> 411 KB for the team-moment store slice (v22). Adds
// appendTeamMoments, the teamMoments Map on FullGameState, the identityMoments
// seam through buildTradeBroadcastCoverage, and snapshot serialization of
// teamMoments. Story chunk landed ~206 bytes over the 410 KB ceiling after the
// wiring; gzip held well under cap (~125 KB actual vs 126 KB ceiling) so only
// raw moves. No new template copy β€” just wire-in code.
// WORKER raw: bumped 411 -> 412 KB for the Chase Watch query slice. Adds
// getChaseWatch() to sim.worker.queries.ts β€” a league-wide view assembling
// career milestone alerts (via existing buildMilestoneAlertsForPlayers) and
// pace chases (via existing projectSeasonStats + findNotableProjections) into
// a single dashboard payload. Pure wiring of existing helpers, no new sim
// logic. Story chunk landed 547 bytes over the 411 KB ceiling; gzip held
// under cap (~125 KB actual vs 126 KB ceiling) so only raw moves.
// WORKER raw: bumped 412 -> 414 KB for the pennant-race-heat query slice.
// Adds getPennantRaces with division race + wildcard bubble computations
// plus inline DivisionRace/WildcardRace types. Story chunk landed ~1 KB
// over the 413 KB stack when combined with Chase Watch growth; gzip held
// well under cap (~126 KB actual) β€” wire cost unchanged, raw ceiling only.
// No new sim-core changes.
// WORKER raw: award race boards slice β€” adds getAwardRaceBoards
// (league-split + sample-size-filtered enrichment around the existing
// calculateAwardRaces scorer) into the story chunk. Absorbed within the
// current 414 KB ceiling from the pennant-race stack; gzip held well under
// cap (~126 KB actual vs 126 KB ceiling) β€” no new sim-core changes.
// WORKER raw: bumped 414 -> 417 KB for the team-moment types slice (v23).
// Adds detectSeasonIdentityMoments + championship_run / contention_collapse
// enum values + MOMENT_TYPE_ORDER and description template entries. When
// stacked on top of the dashboard query slices (chase/pennant/award), the
// story chunk landed ~1.6 KB over the 414 KB ceiling β€” raw lift with a
// small headroom buffer. Gzip also bumped 126 -> 127 KB; copy compressed
// ~296 bytes over the prior cap.
// WORKER raw: bumped 417 -> 420 KB for the Career Retrospective query slice.
// Adds buildCareerRetrospective + getCareerRetrospective to sim.worker.queries
// β€” unifies titles (WS/pennants/division/playoffs derived from seasonArchive +
// archivedSeasons), team-moment beats, completed story arcs, awards-shelf
// counts, and top rivalry into one dashboard payload. Story chunk landed
// ~2.5 KB over the 417 KB ceiling; gzip held under cap (~125 KB actual vs
// 127 KB ceiling).
// WORKER raw: bumped 420 -> 423 KB for the Season Story Reel query slice.
// Adds buildSeasonStoryReel + getSeasonStoryReel β€” per-season deep-dive
// assembling record/rank, playoff path, storylines, timeline events, signature
// beats filtered by season, key transactions, awards, and stat-leader
// highlights from seasonArchive + archivedSeasons. Story chunk landed ~2.2 KB
// over the 420 KB ceiling; gzip held under cap (~125.8 KB actual vs 127 KB
// ceiling). Raw-only lift.
// WORKER raw: bumped 423 -> 425 KB for the Pennant Race Detail query slice.
// Adds getPennantRaceDetail β€” all-six-divisions full standings + top-5
// wildcard picture with projectedWins via winning-percentage pace. Story
// chunk landed ~1.5 KB over the 423 KB ceiling; gzip held under cap
// (~126.4 KB actual vs 127 KB ceiling). Raw-only lift.
// WORKER gzip: bumped 127 -> 128 KB for team-identity expansion wave 2 (v25).
// Adds rebuild_begun + breakout_season + contention_window_opens detectors with
// their MOMENT_TYPE_ORDER entries and description templates. game-engine-core
// landed at 130,174 gzip bytes vs the prior 130,048 cap (+126 bytes), so the
// smallest safe lift is one KB. Raw ceiling already at 425 KB from PR #44's
// Pennant Race detail query absorbs the wave-2 detector code with headroom.
// WORKER raw/gzip: bumped 425 -> 428 KB raw and 128 -> 131 KB gzip for
// narrative depth wave 3 (v26). The additive detector/template expansion kept
// game-engine-core raw under the existing ceiling (~399 KB) but pushed gzip to
// 133,864 bytes; game-engine-story stayed under gzip but landed at 437,490 raw
// bytes. This is the maximum sprint-allowed lift (+3 KB raw / +3 KB gzip),
// preserving the hard stop at 430 KB raw / 131 KB gzip.
// WORKER gzip: bumped 131 -> 132 KB for CI/local terser-output drift. Local
// verify showed game-engine-core at 133,864 gzip bytes; CI minifier emitted
// 134,398 bytes for the identical commit (+534 byte environmental drift),
// which tripped the 131 KB cap (134,144) by 254 bytes. One KB lift restores
// headroom for the already-landed wave-3 payload without touching any code.
// WORKER raw/gzip: bumped 428 -> 432 KB raw and 132 -> 134 KB gzip for
// narrative depth wave 4 (v27). The persisted-state fidelity sprint added
// worker-side snapshot/state plumbing plus playoff comeback tracking, pushing
// the measured worker chunks to 415,354 raw / 136,054 gzip for
// game-engine-core and 442,235 raw / 131,714 gzip for game-engine-story.
// This lands exactly at the sprint hard cap and restores bundleBudget headroom
// without exceeding the approved +4 KB raw / +2 KB gzip ceiling.
// WORKER raw: bumped 432 -> 433 KB for the Career Retrospective season-history
// slice. Extends buildCareerRetrospective with seasonHistory derivation (user
// team per-season win% from seasonArchive + archivedSeasons, deduped by
// season). Story chunk landed 442,727 raw bytes (+359 over the 432 KB cap);
// gzip held at 131,941 (well under the 134 KB ceiling) so only raw moves.
// Pure aggregation of existing persisted state β€” no sim-core changes.
// WORKER raw/gzip: bumped 433 -> 434 KB raw and 134 -> 135 KB gzip to reserve
// headroom for the parallel codex/narrative-prose-expansion branch landing
// alongside this branch. That branch adds deterministic prose variant pools
// to holdoutCoverage / consequences / narrativeState / storyArcs with no
// threshold/ID/schema changes (+4 sim-core tests, determinism sweep clean).
// Measured combined story chunk (Claude season-history + Codex prose) at
// 443,379 raw / 131,595 gzip local; CI-buffered gzip measurement via zlib
// gzipSync lands at 137,334 (+118 over the prior 134 KB cap, matches the
// known +250-500 byte game-engine-core terser drift pattern in patterns.md).
// Smallest safe simultaneous lift: +1 KB raw / +1 KB gzip, giving ~1 KB
// headroom on both ceilings for either-or-both-branch merge paths. If the
// Codex branch is not ultimately merged, this reservation becomes slack but
// is bounded to one KB on each axis.
// WORKER gzip: bumped 135 -> 137 KB for the codex/narrative-prose-expansion
// landing after codex/rivalry-narrative-wave1 already merged its rivalry
// prose pools into game-engine-core (PR #50, commit 2806d37). The original
// 135 reservation was measured against rivalry-wave1 NOT being on main; once
// that landed, rivalry prose + narrative-prose-expansion's owner/consequences/
// storyArcs pools stacked in the same chunk. Post-rebase measurement on this
// branch: game-engine-core gzip 138,815 (+575 over 135 KB cap). +2 KB lift
// covers the stack plus the established +250-500 byte CI terser drift. Raw
// stayed comfortably under: game-engine-core raw 423,076 vs 444,416 cap.
// WORKER raw: bumped 434 -> 436 KB for the This Week in History query slice.
// Adds getThisWeekInHistory to sim.worker.queries β€” computes yearsAgo and
// strict-prior-season Β±dayWindow filtering over persisted playerMoments /
// teamMoments Maps for the dashboard tile. Pure read wiring, no sim-core
// touch. Story chunk landed 445,537 raw bytes (+1,121 over the 434 KB cap);
// gzip held well under cap (132,609 actual vs 140,288 ceiling). +2 KB raw
// lift gives ~900 bytes headroom against the established +250-500 byte
// CI terser drift. Raw-only; no gzip ceiling move needed.
// WORKER raw: bumped 436 -> 438 KB for the Player Arcs of the Season query
// slice. Adds getPlayerArcsOfSeason to sim.worker.queries β€” filters persisted
// playerMoments for redemption_arc / late_career_peak / rookie_breakout and
// surfaces the most recent season's arcs for the dashboard tile. Pure read
// wiring of existing player-arc moment state (PR #55), no sim-core touch.
// Story chunk landed 446,889 raw bytes (+425 over the 436 KB cap); gzip held
// well under cap (132,865 actual vs 140,288 ceiling). +2 KB raw lift gives
// ~1,599 bytes headroom for CI terser drift. Raw-only; no gzip move needed.
// WORKER raw/gzip: bumped 438 -> 439 KB raw and 137 -> 139 KB gzip for
// Narrative Depth Wave 6. The dynasty-marker worker wiring moved enough
// season-summary/detector orchestration into game-engine-story to leave the
// chunk 66 bytes over the prior raw ceiling (448,578 vs 448,512), while the
// new prose pools and detector exports expand the worker-compressed footprint.
// Smallest safe lift: +1 KB raw / +2 KB gzip, preserving sub-1 KB local
// headroom on story while leaving the broader worker budget effectively flat.
// WORKER gzip: bumped 139 -> 141 KB for Narrative Depth Wave 7. Position-group
// moment detectors add team aggregate helpers plus deterministic prose pools
// for dominant_rotation / bullpen_collapse / lineup_of_era. Local budget test
// measured game-engine-core at 142,927 gzip bytes, 591 bytes over the 139 KB
// cap. Raw stayed under the existing 439 KB ceiling (440,242 vs 449,536), so
// only the pre-authorized +2 KB gzip lift is applied.
// WORKER raw/gzip: bumped 439 -> 440 KB raw and 141 -> 143 KB gzip for
// Narrative Depth Wave 8. Player micro-arc worker source plumbing was trimmed
// before lifting the cap; local build lands game-engine-story just under the
// 440 KB raw ceiling and game-engine-core at 145,118 gzip bytes, covered by
// the pre-authorized +2 KB gzip headroom.
// WORKER raw: bumped 440 -> 442 KB for Narrative Depth Wave 9. Weekly cadence
// detector/prose modules are split into game-engine-weekly to protect
// game-engine-core; the remaining worker checkpoint wiring leaves
// game-engine-story ~0.6 KB over the Wave 8 raw cap. +2 KB raw stays within
// the pre-authorized Wave 9 lift and leaves modest minifier-drift headroom.
// WORKER raw: bumped 442 -> 443 KB for Narrative Depth Wave 10. The capstone
// prose variant pools are split into game-engine-capstone to keep
// game-engine-core under budget; the remaining award/HOF/retirement worker
// context wiring leaves game-engine-story at 452,887 raw bytes (+279 over the
// Wave 9 cap). Gzip stayed under the existing 143 KB ceiling (134,439 bytes).

// Worker ceilings: see apps/web/docs/BUDGETS.md for the lift policy and per-slice rationale.
export const WORKER_CHUNK_BUDGET_BYTES = 443 * 1024;
export const WORKER_CHUNK_GZIP_BUDGET_BYTES = 143 * 1024;

Expand Down
22 changes: 0 additions & 22 deletions apps/web/src/features/feedback/FeedbackButton.tsx

This file was deleted.

Loading
Loading