- 02-01-foundations-SUMMARY.md authored with frontmatter dependency graph, key-files manifest, decisions log, patterns established, test-count breakdown (72 new tests), TICK_MS=200 (no drift), and ESLint sim-purity rule landed (defended-option clause did not trigger) - STATE.md: Phase 2 progress 1/5 plans (Wave 0 complete); velocity table updated with Plan 02-01 ~12min entry; decisions log cites BLOCKER 3 split, V1Payload extension, ESLint rule - ROADMAP.md: Phase 2 row updated to 1/5; 02-01 plan marked [x] with duration + summary backlink - REQUIREMENTS.md: CORE-02, CORE-03, CORE-11, UX-10, UX-11 marked complete with annotations; traceability table updated Plan execution metrics: - 3 atomic commits (58db532,fe99058,2a8d354) - 72 new tests across 9 test files (cushion above plan estimate of 54) - Total test count: 128/128 green - npm run ci exits 0 - Duration: ~12 min (sequential mode)
18 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-season-1-vertical-slice-soil | 01 | foundations |
|
|
|
|
|
|
|
|
|
12min | 2026-05-09 |
Phase 2 Plan 01: Foundations Summary
One-liner
Wave-0 foundations for the Season-1 vertical slice — BigQty number wrapper around break_eternity.js, Zustand 5 vanilla store with 4 composed slices and a slim sim adapter, fixed-timestep tick scheduler with negative-delta refusal and 24h offline cap, V1Payload extended in place per D-34 with 5 new fields, save lifecycle hooks for UX-10, Phaser EventBus singleton, and an ESLint sim-purity rule that mechanically prevents future regressions of the Date.now/setInterval ban inside src/sim/**.
What Landed
Task 1 (commit 58db532) — feat(02-01): BigQty + scheduler + sim foundations
- Installed zustand@^5.0.0 (resolved 5.0.13) + break_eternity.js@^2.1.3 as runtime dependencies
- src/sim/numbers/: BigQty immutable wrapper, formatHumanReadable for UX-11 thresholds, barrel
- src/sim/scheduler/: Clock interface + wallClock + FakeClock (D-33), drainTicks (CORE-02/03/11), computeOfflineCatchup pure descriptor, barrel
- src/sim/state.ts: SimState root type with the BLOCKER 3 lastTickAt/tickCount split documented in a docblock
- src/sim/index.ts: top-level sim barrel
- 47 new tests across big-qty / format / clock / tick / catchup all green (52 reported by the runner because Phase-1's sentinel test runs alongside)
Task 2 (commit fe99058) — feat(02-01): Zustand store + V1Payload extension + save lifecycle hooks
- src/store/: 4 slices + composed appStore (zustand/vanilla createStore) + useAppStore React hook + simAdapter + 4 named selectors + barrel
- src/save/migrations.ts: V1Payload extended in place per D-34 with tickCount + unlockedPlantTypes + luraBeatProgress + offlineEvents + settings.persistenceToastShown; OfflineEventBlock declared inline (save layer stays a leaf, no upward sim dependency); migrations[1] populates all defaults; CURRENT_SCHEMA_VERSION stays at 1
- src/save/migrations.test.ts: 6 new tests pinning each Phase-2 default + 1 regression-defense test asserting only migrations[1] exists
- src/save/lifecycle.ts: registerSaveLifecycleHooks (visibilitychange→hidden + beforeunload) + saveOnSeasonTransition() — UX-10
- src/save/lifecycle.test.ts: 6 tests covering all three triggers + the visibility→visible no-op + detach()
- src/save/index.ts: re-exports lifecycle + OfflineEventBlock
- src/game/event-bus.ts: Phaser.Events.EventEmitter singleton per the Phaser 4 React-template pattern
- 27 new tests across store / migrations / lifecycle all green
Task 3 (commit 2a8d354) — chore(02-01): eslint sim-purity rule + Date.now violator fixture
- eslint.config.js Block 3: no-restricted-syntax bans Date.now() and setInterval() inside src/sim/** with src/sim/scheduler/clock.ts as the single exception
- src/sim/test_violation/date-now-violator.ts: deliberate-violation fixture (excluded from default lint by Block 1's top-level ignores; the programmatic ESLint test overrides via ignore: false)
- src/sim/test_violation/lint-firewall.test.ts: 2 new tests — positive (rule fires on violator with the D-33 message) + negative (rule does NOT fire on clock.ts)
- 2 new tests; existing CORE-10 firewall test left untouched and still green
Test Count Breakdown
| File | Tests |
|---|---|
| src/sim/numbers/big-qty.test.ts | 18 |
| src/sim/numbers/format.test.ts | 11 |
| src/sim/scheduler/clock.test.ts | 6 |
| src/sim/scheduler/tick.test.ts | 7 |
| src/sim/scheduler/catchup.test.ts | 5 |
| src/store/store.test.ts | 10 |
| src/save/migrations.test.ts (additions) | 7 |
| src/save/lifecycle.test.ts | 6 |
| src/sim/test_violation/lint-firewall.test.ts (additions) | 2 |
| Total new tests | 72 |
Pre-existing Phase-1 tests (53) + 75 new tests this plan = 128 total (full vitest run reports 128/128 green).
The plan's verification block estimated ≥54 new tests; actual count was 72 (the additional cushion came from extra immutability-guard tests on each BigQty operation and an explicit visibility→visible no-op test on the lifecycle hook).
TICK_MS
TICK_MS = 200 (5Hz), unchanged from RESEARCH Pattern 1 line 440. No drift during implementation.
ESLint Sim-Purity Rule
Landed. The defended-option clause did NOT trigger — the rule integrated cleanly into the existing flat-config layout with one small adjustment from the plan text (per-block ignores does NOT exclude src/sim/__test_violation__/**; see key-decisions above for why). All three ways the rule is exercised — npm run lint clean, programmatic positive test on the violator, programmatic negative test on clock.ts — pass.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 — Blocking] Initial Block 3 ignores accidentally excluded the violator fixture, masking the rule from its own test
- Found during: Task 3 (first run of the new lint-firewall.test.ts cases)
- Issue: The plan's eslint.config.js snippet listed
src/sim/__test_violation__/**in Block 3's per-blockignores. ESLint'signore: falseAPI flag overrides Block 1's top-level ignores but does NOT override per-block file matching, so the rule simply didn't apply to the violator fixture. Test reported 0 violations and failed. - Fix: Removed
src/sim/__test_violation__/**from Block 3's per-blockignores(kept clock.ts as the lone exception). Block 1's top-level ignores still keep the violator out ofnpm run lint. Added a docblock explaining the asymmetry so future readers don't re-introduce the bug. - Files modified: eslint.config.js
- Commit:
2a8d354
2. [Rule 3 — Blocking] BigQty.format() initial draft used require('./format') to dodge a non-existent cycle
- Found during: Task 1 (immediately on first read of the file I'd just written)
- Issue: I'd hedged against a hypothetical cycle between BigQty and formatHumanReadable by using
require(). But (a) the project istype: "module"so CommonJSrequiredoesn't work, and (b) there's no cycle: format.ts only imports Decimal, never BigQty. - Fix: Replaced with a static
import { formatHumanReadable } from './format'. Removed the apologetic docblock. - Files modified: src/sim/numbers/big-qty.ts
- Commit:
58db532(caught and fixed before commit)
Acceptance-Criteria Footnote
The plan's Task 1 acceptance criterion grep -c "Date.now" src/sim/scheduler/clock.ts reports 1 exactly is overly literal — it counts every occurrence of the literal string "Date.now" in the file, including the two doc-comment mentions ("Per CLAUDE.md ... no Date.now() ..."). The actual call count is 1, which is what matters for the rule. Doc comments quoting CLAUDE.md were left intact because they're load-bearing references for readers; the test that DOES enforce the constraint mechanically is the Task 3 lint-firewall test. The same is true for the Task 1 grep that asserts src/sim/scheduler/tick.ts lacks Date.now — that file ALSO has a docblock quoting CLAUDE.md but no actual call site. The intent of both grep checks (single call site under src/sim/) is satisfied; the literal-string count is not.
Self-Check: PASSED
Verification before this section was added:
- src/sim/numbers/big-qty.ts: FOUND
- src/sim/numbers/format.ts: FOUND
- src/sim/scheduler/clock.ts: FOUND
- src/sim/scheduler/tick.ts: FOUND
- src/sim/scheduler/catchup.ts: FOUND
- src/sim/state.ts: FOUND
- src/sim/index.ts: FOUND
- src/store/store.ts: FOUND
- src/store/sim-adapter.ts: FOUND
- src/save/migrations.ts (modified, V1Payload extended): FOUND
- src/save/lifecycle.ts: FOUND
- src/game/event-bus.ts: FOUND
- src/sim/test_violation/date-now-violator.ts: FOUND
- eslint.config.js (Block 3 added): FOUND
- Commit
58db532(Task 1): FOUND ingit log --oneline -5 - Commit
fe99058(Task 2): FOUND ingit log --oneline -5 - Commit
2a8d354(Task 3): FOUND ingit log --oneline -5 npm run ciexits 0: VERIFIED- 128/128 tests pass: VERIFIED