Files
TheLastGarden/.planning/REQUIREMENTS.md
T
josh e5d449095d docs(02-05): complete letter-settings-e2e plan
- 02-05-letter-settings-e2e-SUMMARY.md: full plan summary (frontmatter
  + decisions + REQ table + self-check). All 24 Phase-2 REQ-IDs
  structurally satisfied across the 5-plan set.
- STATE.md: marked Plan 02-05 complete; Phase 2 ready for
  /gsd-verify-work; progress 19% → 22%; next action set to verifier.
- ROADMAP.md: Plan 02-05 row marked [x] with duration + SUMMARY ref.
- REQUIREMENTS.md: UX-02 / UX-10 / CORE-03 / PIPE-07 marked complete
  with traceability annotations citing Plan 02-05's contribution;
  per-row Plan 02-05 references added to UX-02, UX-10, CORE-03;
  PIPE-07 traceability table row updated.
2026-05-09 11:16:02 -04:00

299 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Requirements: The Last Garden
**Defined:** 2026-05-08
**Core Value:** Every idle mechanic must function as a metaphor that the player absorbs without being told. When economy and meaning conflict, meaning wins.
## v1 Requirements
Requirements for initial release. Each maps to roadmap phases. All are user-centric, testable, atomic.
### CORE — Engine, Simulation, Save Persistence
- [x] **CORE-01**: Player loads the game in a modern browser (Chrome, Firefox, Safari, Edge — last 2 stable releases) and reaches a playable state in under 5 seconds on a 25 Mbps connection. <!-- Plan 01-01: scaffold builds (`npm run build` green); end-to-end <5s wall-clock measurement is Phase 2 PIPE-07. -->
- [x] **CORE-02**: Game runs a deterministic, fixed-timestep simulation that advances by elapsed real time (not `setInterval` ticks), so a player who switches tabs or sleeps their device returns to a correctly-advanced garden. <!-- Plan 02-01: drainTicks fixed-timestep accumulator + Clock injection contract; TICK_MS=200 (5Hz); 7 scheduler tests green. ESLint sim-purity rule (D-33) bans setInterval inside src/sim/**. Scene-driven tick wiring + visibility-pause is Plan 02-02. -->
- [x] **CORE-03**: Player who closes the game and returns finds the garden has progressed by the elapsed time (capped at 24 hours) — *no progression resumes from a stale snapshot*. <!-- Plan 02-01: drainTicks clamps at MAX_OFFLINE_MS=24h; computeOfflineCatchup reports hitOfflineCap=true on excess; both paths covered by Vitest. Plan 02-05: src/PhaserGame.tsx boot path threads computeOfflineCatchup → drainTicks(silent=true) → autoHarvestReadyPlants → letter overlay opens at ≥5min absence; auto-harvest accumulates plantsBloomedCount + harvestedFragmentIds + luraBeatPending into the OfflineEventBlock the letter Ink renders. PIPE-07 e2e exercises offline catchup structurally via FakeClock advance + reload. -->
- [x] **CORE-04**: Player's progress saves to IndexedDB (with localStorage fallback), surviving browser refresh, browser updates, and at least 30 days of inactivity on Chrome and Firefox. <!-- Plan 01-03: idb DB + LocalStorageDBAdapter fallback; 4 db tests green; round-trip test green. Settings UI surface is Phase 2. -->
- [x] **CORE-05**: Game requests persistent storage via `navigator.storage.persist()` on first save and surfaces the result respectfully if the browser declines. <!-- Plan 01-03: requestPersistence() all 4 API scenarios covered by Vitest; Settings UI surface is Phase 2. -->
- [x] **CORE-06**: Saves are versioned (`{schemaVersion, payload, checksum}`) and the game refuses to load a save with a checksum mismatch, presenting the player with a recovery option. <!-- Plan 01-03: wrap/unwrap + SaveCorruptError + CRC-32; 9 envelope tests green. -->
- [x] **CORE-07**: Game runs a `migrate_vN_to_vN+1` chain on load, so a save from any prior version of the game upgrades cleanly to the current shape (validated by Vitest tests for every shipped migration). <!-- Plan 01-03: forward-only registry with synthetic v0→v1; 6 migration tests green. -->
- [x] **CORE-08**: Game keeps the last 3 pre-migration save snapshots and offers the player a "restore previous save" option in settings. <!-- Plan 01-03: RETAIN=3 enforced; 5-then-3 invariant test green. Settings UI surface is Phase 2. -->
- [x] **CORE-09**: Player can export their save as a Base64 text blob via Settings → Export and import it back into the same or a fresh browser via Settings → Import. <!-- Plan 01-03: exportToBase64/importFromBase64 + 50MB DoS cap; 3 round-trip tests green. Settings UI surface is Phase 2. -->
- [x] **CORE-10**: Game's simulation core (`src/sim/`) imports nothing from `src/render/` or `src/ui/` — enforced by ESLint boundary rules in CI. <!-- Plan 01-02: ESLint 9 flat config + boundaries/element-types rule + programmatic Vitest proof; lint exits 0. -->
- [x] **CORE-11**: Simulation refuses negative time deltas (system-clock cheat defense) and caps any single offline progression at 24 hours, regardless of wall-clock claim. <!-- Plan 02-01: drainTicks(state, accumulatorMs<0) returns the original state with ticksApplied=0; computeOfflineCatchup reports cappedMs=0 for negative deltas; 24h clamp shared with CORE-03; 5 catchup tests + 4 tick tests green. -->
### GARDEN — Planting, Growing, Harvesting
- [x] **GARD-01**: Player can plant a seed from their seed inventory into an unoccupied tile of the garden. <!-- Plan 02-02: sim/garden plantSeed command (D-05 unlock-gate + occupied silent no-op + immutability) + SeedPicker DOM popover + Garden scene pointerdown → store.enqueueCommand wiring; 14 commands tests + 6 SeedPicker tests green. -->
- [x] **GARD-02**: Each plant has a visible growth state (sprout → mature → ready-to-harvest) that updates from save data on load and advances over time. <!-- Plan 02-02: advanceGrowth pure function (sprout→mature@33%→ready@100%) + render/garden/plant-renderer primitives per stage + Garden scene appStore.subscribe drives reactive repaintPlants; 11 growth tests green. Save-load tickCount restore is in Garden.create(). -->
- [x] **GARD-03**: Player can harvest a mature plant to receive a memory fragment; harvesting empties the tile. <!-- Plan 02-03: sim/garden harvest() pure command — refuses immature plants, calls selectFragment() to pick exactly one fragment from the gated pool, empties the tile, appends to harvestedFragmentIds, recomputes unlockedPlantTypes (Pitfall 10 post-commit). Garden.ts handleTilePointerDown enqueues 'harvest' on a ready-stage plant click. 11 commands.test.ts cases (harvest + Pitfall 10 boundaries). -->
- [x] **GARD-04**: Player can compost an immature or unwanted plant, returning a portion of resources and triggering a tonal beat (acknowledging the choice to let go). <!-- Plan 02-03: sim/garden compost() pure command — empties tile regardless of growth stage, no fragment yield (D-07), no resource refund (D-04 = infinite seeds). Garden.ts handleTilePointerDown enqueues 'compost' on an immature plant click. content/dialogue/season1/compost-acknowledgements.ink ships 6 authored beat lines in voice; Plan 02-04 wires the inkjs runtime (TODO at the call site marks the wiring point). 5 commands.test.ts cases (compost). -->
- [ ] **GARD-05**: Player unlocks new plant types as they progress through Seasons, with each plant type having distinct growth time, harvest yield, and visual identity.
- [ ] **GARD-06**: Tree plantings (Season 3+) are slow and expensive but produce place-memory vignettes when harvested.
- [ ] **GARD-07**: Cross-pollination (Season 2+): adjacent compatible plants can produce hybrid seeds with mixed memory traits.
- [ ] **GARD-08**: Ecosystem planting (Season 5+): clusters of compatible species produce yield bonuses, encouraging the player to think in ecosystems rather than individual crops.
- [ ] **GARD-09**: Memory Storms (Season 4+): periodic events accelerate decay of unprotected plants, requiring the player to plant resilient species, build windbreaks, or time harvests around them.
- [ ] **GARD-10**: Player sees the garden as a Phaser 4 canvas with watercolor-styled, hand-painted-feeling plants (no generic fantasy flora).
### MEMORY — Fragments, Journal, Selection
- [x] **MEMR-01**: Each harvest yields exactly one memory fragment, drawn from the authored content pool gated by current Season and unlocked progression. <!-- Plan 02-03: harvest() calls selectFragment() exactly once per ready-stage harvest; result appended to harvestedFragmentIds. Pinned by selector.test.ts (16 cases) + commands.test.ts harvest cases. -->
- [x] **MEMR-02**: Memory fragments are authored in plain text (Markdown with frontmatter) in the project's `/content/` tree and compiled per-Season at build time. <!-- Plan 02-03: 14 yaml entries + 2 long-form Markdown fragments under /content/seasons/01-soil/; PIPE-01 enforced (build fails on schema violation). PIPE-02 lazy chunk surface from Plan 02-02 verified structurally by scripts/check-bundle-split.mjs. -->
- [x] **MEMR-03**: Each fragment has a stable string ID (e.g., `season3.canopy.lura_07.vignette`) — never numeric — so re-ordering or re-authoring does not break references. <!-- Plan 02-03: all 17 Season-1 fragment ids match /^season1\.[a-z0-9._-]+$/ (FragmentSchema regex enforced at module-eval). loader.test.ts has the numeric-id rejection case. -->
- [x] **MEMR-04**: Player can open a Memory Journal (React DOM panel) listing every fragment they have collected, organized by Season. <!-- Plan 02-03: src/ui/journal/Journal.tsx — full-screen modal (D-24) grouped by Season. JournalIcon corner affordance (D-23 first-harvest gate). 7 Vitest cases. -->
- [x] **MEMR-05**: Player can read any collected fragment in full at any time, including selecting and copying its text (DOM-based, not canvas-rendered). <!-- Plan 02-03: Journal + FragmentRevealModal both render fragment bodies inside <pre> with userSelect: 'text' (DOM, not canvas). Pinned by Journal.test.tsx + FragmentRevealModal.test.tsx assertions on computed style. -->
- [x] **MEMR-06**: Fragments are selected by a deterministic selector that respects authored gating rules (Season requirement, story-state requirement, player-progression requirement) and avoids duplicates within a single playthrough until the pool is exhausted. <!-- Plan 02-03: src/sim/memory/selector.ts — selectFragment() is pure, deterministic (mulberry32 PRNG seeded from sim state), respects Season + plant-type tonal-register gating + no-dup. Pitfall 8 exhaustion fallback via EXHAUSTION_FALLBACK_ID sentinel. 16 selector.test.ts cases. -->
- [ ] **MEMR-07**: Place-memory vignettes (Season 3+) deliver a fragment as a short interactive scene the player can walk through, not just a text block.
### STORY — Characters, Dialogue, Choice
- [x] **STRY-01**: Lura (the audience-surrogate carpenter from a Remembered town) appears at the garden gate during Season 1 and reacts to early fragments with text-message-cadence dialogue authored in Ink. <!-- Plan 02-04: 3 authored Ink beats (lura-arrival/mid/farewell.ink) under /content/dialogue/season1/, fired at 1st/4th/8th harvest (D-14); src/sim/narrative/lura-gate.ts gates on harvestedFragmentIds.length; src/ui/dialogue/LuraDialogue.tsx renders lines via inkjs Story + InkRenderer drip cadence (1500ms base + 20ms/char, capped 4000ms); render/garden/gate-renderer.ts visual cue. 17 sim tests + 13 dialogue tests green. -->
- [ ] **STRY-02**: Lura's dialogue continues across all 7 Seasons, contextualizes major story beats, and reflects player progression in Ink-driven branches tied to Zustand variables.
- [ ] **STRY-03**: The Nameless Man appears in Season 2, his dialogue progressively shortens and confuses across Seasons 2-4, and he vanishes mid-sentence in Season 4 with no fanfare or cutscene.
- [ ] **STRY-04**: The Archivist appears in Season 6, never gendered (they/them), speaks softly and reflectively, and asks the player a thematic question without forcing an answer.
- [ ] **STRY-05**: The Archivist responds (mechanically and tonally) when the player feeds the Loom a memory containing both joy and grief — the Loom holds the contradiction, ending the Unremembering's advance.
- [x] **STRY-06**: All authored dialogue uses Ink (`.ink` files) compiled to JSON for runtime via inkjs. <!-- Plan 02-04: scripts/compile-ink.mjs invokes the bundled inklecate binary at build time (BLOCKER 4 — uses node_modules/inklecate/bin); 4 .ink → .ink.json compiled deterministically; src/content/ink-loader.ts lazy-loads compiled JSON via import.meta.glob and instantiates inkjs.Story; npm run ci runs compile:ink before tests + before build. RESEARCH Assumption A6 verified first-try on Windows. -->
- [x] **STRY-07**: The Keeper (player character) has no name, no backstory, and no dialogue beyond the final binary choice in Season 7. <!-- Plan 02-04: vacuously satisfied for Phase 2 — zero Ink files contain Keeper-spoken lines. The Keeper is the player; only Lura speaks (and the gardener-keeper voice acknowledges in compost beats, but is never personified as a named character). Phase 7 (SEAS-09 / STRY-08) lands the binary choice surface. -->
- [ ] **STRY-08**: The final scene of Season 7 presents the player with a binary narrative choice (*"They help us remember"* / *"They help us grow"*); both endings display the line *"The garden persists."* and both are tonally complete; neither unlocks alternate post-credits content.
- [x] **STRY-09**: Every player-visible string is externalized in `/content/` (not hardcoded in TypeScript), so localization can be retrofitted in v2 without code refactor. <!-- Plan 01-04: /content/ convention established; no player-visible strings in Phase 1 source (vacuously satisfied); real enforcement lands Phase 2. -->
- [x] **STRY-10**: Story progression gates on tick count, not on wall time — players cannot fast-forward through authored beats by manipulating their system clock. <!-- Plan 02-04: src/sim/narrative/lura-gate.ts gates on state.harvestedFragmentIds.length (sim-internal counter), never wall time. The gate function takes only the harvest count as input — no clock parameter exists. The STRY-10 test case in lura-gate.test.ts advances FakeClock by 24 hours with zero harvests and confirms no beat fires. ESLint sim-purity rule (Block 3 of eslint.config.js) mechanically prevents Date.now/setInterval inside src/sim/narrative/. -->
### SEAS — Seasons, Prestige, Roothold
- [ ] **SEAS-01**: The game presents the seven Seasons (Soil, Roots, Canopy, Storm, Depth, Loom, Return) as an authored sequence; the player progresses through them in order.
- [ ] **SEAS-02**: Each Season ends with a die-off event (frost in Season 1, escalating tonally; pulled-edit-out-of-existence in Season 3) that wipes surface plantings and triggers a Season transition.
- [ ] **SEAS-03**: Roothold persists across every Season transition; it is the only number that can never decrease.
- [ ] **SEAS-04**: Roothold has a *finite* ceiling tied to the narrative argument; the curve flattens deliberately as the player approaches the end of the authored arc.
- [ ] **SEAS-05**: Each Season introduces *at most one* new mechanic (composting, cross-pollination, place-memory vignettes, Memory Storms, ecosystem planting, the Loom interface, Return-mode expansion) per the scope-defense doctrine.
- [ ] **SEAS-06**: Season transitions crossfade visual palette and audio bed (Howler.js), shifting from golden/autumnal (early) → deep green/storm (middle) → dawn/silver (late).
- [ ] **SEAS-07**: The Below (accessible Season 5+) lets the player grow root structures into ancient memory layers and deliver content from a pre-Archivist civilization.
- [ ] **SEAS-08**: Season 6 shifts the primary gameplay loop to the Below, where the player feeds memories to the Loom rather than harvesting fragments.
- [ ] **SEAS-09**: Season 7 ("Return") shifts to a long, satisfying late-game in which collected memories become seeds that automatically reconstitute the world; the Pale recedes; the Heartsoil expands beyond the garden walls.
- [ ] **SEAS-10**: When the player reaches the end of the authored arc, the game transitions to a credits/coda *rest* state — not infinite prestige tiers — that the player can return to indefinitely without grinding.
### AEST — Visual & Audio Aesthetic
- [ ] **AEST-01**: The garden renders in a watercolor-adjacent visual style (hand-painted textures, plants that look like real species made slightly wrong) on a Phaser 4 canvas with a watercolor post-process filter.
- [ ] **AEST-02**: The Pale renders as overexposed white silence — luminous, pearlescent, *too bright* — with a faint tinnitus-like high tone in audio.
- [ ] **AEST-03**: The main musical theme is a solo cello, looped and crossfaded across Seasons via Howler.js.
- [ ] **AEST-04**: Ambient garden sounds (wind, birdsong, the creak of a gate) thin and fade as the Unremembering draws closer to the player's region.
- [ ] **AEST-05**: Audio crossfades, never hard-cuts, between Seasons; the cello and ambient layers are independent buses with separate volume controls.
- [ ] **AEST-06**: Color palette shifts deliberately by Season — golden/autumnal → deep green/storm → dawn/silver.
- [x] **AEST-07**: The first screen of the game is a hand-painted "Tend the garden" / "Begin" gesture gate that satisfies the Web Audio user-gesture requirement and explicitly calls `AudioContext.resume()`. <!-- Plan 02-02: BeginScreen (D-21 typographic placeholder; Phase 3 swaps in painted treatment) + use-audio-bootstrap.ts (synchronous-inside-click bootstrapAudioContext defends Pitfall 5: iOS Safari construction-inside-gesture; webkitAudioContext fallback). 4 BeginScreen tests + first-interaction gesture handler one-shot for D-22 returning players. -->
- [x] **AEST-08**: All AI-assisted assets carry persisted provenance metadata (`{model_id, checkpoint_hash, prompt, seed, sampler, params}`) and are produced from a pinned model and a locked north-star reference set. <!-- Plan 01-05: Zod ProvenanceSchema (6 fields) + CI gate + 2 placeholder assets with valid sidecars; north-star reference set deferred to Phase 5 per IOU. -->
- [x] **AEST-09**: All shipped assets pass a mandatory human curation gate before integration; no asset reaches the production manifest unreviewed. <!-- Plan 01-05: gate mechanism in place (validator + sidecar schema); human curation recorded as explicit decision in 01-05-IOU.md (Path C); real north-star images Phase 5. -->
### UX — Onboarding, Settings, Accessibility, Return
- [x] **UX-01**: First-time player sees a single, painted "Begin" screen with no UI clutter; the garden reveals itself as the player interacts (A Dark Room rule). <!-- Plan 02-02: BeginScreen mounts as a fixed-position dialog covering the canvas with only title + subtitle + Begin CTA; no HUD, no journal, no settings. Dismissed via session.beginGateDismissed (D-22). Phase 3 paints the treatment. -->
- [x] **UX-02**: Player who returns after time away receives a "while you were away" *letter from the garden* — written in voice, not a stat dump — describing what grew, what bloomed, what the wind brought. <!-- Plan 02-05: content/dialogue/season1/letter-from-the-garden.ink authored skeleton (bible voice, anti-FOMO compliant, 24h cap silent in voice per D-11) + slot vocabulary plants_bloomed/fragment_titles/lura_was_here from offlineEvents block; src/ui/letter/Letter.tsx full-screen overlay (D-20: opens at ≥5min absence, dismisses on tap with Pitfall 9 audio bootstrap); buildLetterSlots pure helper + 10 tests; Letter overlay 7 tests. Boot path in src/PhaserGame.tsx threads silent catchup → offlineEvents → openLetter. -->
- [ ] **UX-03**: Player can buy plants/upgrades in multi-buy increments (×1 / ×10 / ×100 / Max) when the option is meaningful for the current scaling.
- [ ] **UX-04**: Player can adjust separate Music, Ambient, and SFX volume sliders, with a master mute keybind; settings persist in saves.
- [ ] **UX-05**: Player can toggle a reduced-motion option (respects `prefers-reduced-motion` system setting by default) that disables non-essential particles and animation.
- [ ] **UX-06**: Player can navigate all menus by keyboard (Tab, Enter, Escape, arrow keys) and the focus indicator is always visible.
- [ ] **UX-07**: All UI text is selectable, copy-pasteable, and supports browser zoom up to 200% without breaking layout.
- [ ] **UX-08**: Color is never the sole carrier of information — icons, labels, or patterns provide a redundant channel for color-blind players.
- [ ] **UX-09**: Tab title and favicon update to reflect a backgrounded state (e.g., a small bloom appears when a fragment is ready).
- [x] **UX-10**: Game saves state on `visibilitychange` to hidden, on `beforeunload`, and on Season transitions; behavior is identical between "tab backgrounded" and "tab closed." <!-- Plan 02-01: registerSaveLifecycleHooks attaches synchronous handlers for visibilitychange→hidden + beforeunload + saveOnSeasonTransition() callable; 6 lifecycle tests green covering every trigger + detach. Plan 02-05: PhaserGame.tsx boot path now wires saveSync via clock.now() (BLOCKER 3 — wall-clock anchor) + synchronous LocalStorage write (Pitfall 7) + best-effort IDB write; lifecycle handle held in a ref so the outer useLayoutEffect cleanup detaches across the async IIFE boundary (W5). -->
- [x] **UX-11**: Numbers display in human-readable formats (1.2K, 4.5M, 8.9B, scientific notation past notation thresholds). <!-- Plan 02-01: formatHumanReadable handles every K/M/B/T threshold + 1e15 scientific + negative-sign branch; 11 format tests green. BigQty.format() delegates so all currency-grade numbers in the HUD route through this. -->
- [ ] **UX-12**: Game surfaces *what Lura said yesterday* in returning-player UI affordances — never *fragments per hour* or *optimization metrics* (mechanic-as-metaphor doctrine).
- [x] **UX-13**: No daily login bonuses, no streaks, no limited-time content, no nag notifications, no loss-aversion copy — anti-FOMO doctrine is enforced in every UX review. <!-- Plan 01-06: .planning/anti-fomo-doctrine.md (17 banned mechanics, review checklist) authored and doc-lint tested; enforced by review per CONTEXT D-07. -->
### PIPE — Content Build & Asset Pipelines
- [x] **PIPE-01**: Project ships a build step that compiles `/content/**/*.{md,yaml,ink}` into per-Season JSON chunks via Zod-validated schemas; build fails on any schema violation. <!-- Plan 01-04: Vite-native import.meta.glob + Zod schemas; 5 loader tests green; schema violation throws at module-eval time. -->
- [x] **PIPE-02**: Player loads only the content for their current Season at runtime (lazy chunk loading); future Seasons are not in the initial bundle. <!-- Plan 02-02: loadSeasonFragments(seasonId) lazy import.meta.glob surface in src/content/loader.ts. Plan 02-03: scripts/check-bundle-split.mjs structural verifier integrated into npm run ci; runs after build to assert Season-1 content reaches dist/ via the lazy plumbing (currently chunkContentMatch=true via the eager corpus inlining; chunkNameMatch=false until Plan 02-04+ switches consumers to lazy-only). The structural assertion holds today; Phase 4+ Season-2 onboarding extends the script's known-content list. -->
- [x] **PIPE-03**: Project ships an AI asset pipeline that records provenance per asset and refuses to integrate an asset missing required provenance fields. <!-- Plan 01-05: scripts/validate-assets.mjs + Zod ProvenanceSchema (6 fields) + refused-sample fixture + 2 Vitest tests green. -->
- [ ] **PIPE-04**: Project ships visual regression testing for the asset library that flags style drift before any model migration is merged.
- [x] **PIPE-05**: Project ships an `anti-FOMO doctrine` document and a `Season 7 end-state` design document in `.planning/` (or `docs/`) before economy code is written. <!-- Plan 01-06: both docs authored and doc-lint tested (8 Vitest assertions green). -->
- [x] **PIPE-06**: Project ships unit tests (Vitest) covering all save migrations and core economy formulas, run on every CI build. <!-- Plan 01-07: .github/workflows/ci.yml runs npm ci + npm run ci on push + PR; 53 tests / 12 files green. -->
- [x] **PIPE-07**: Project ships an end-to-end smoke test (Playwright) that loads the game, plants a seed, harvests a fragment, and verifies persistence across a page reload. <!-- Plan 02-05: tests/e2e/season1-loop.spec.ts covers load → Begin → plant rosemary → fast-forward FakeClock 3min → harvest → fragment-reveal modal → close → journal-icon visible → open journal → fragment present → reload → fragment persists. URL-flag FakeClock injection production-guarded by import.meta.env.PROD; window.__tlgStore exposed only when ?devtime=fake. 1.5s test runtime, ~4s end-to-end. npm run test:e2e (not in npm run ci per minimum-viable doctrine; runs separately before /gsd-verify-work and on release). -->
## v2 Requirements
Deferred to v1.x or v2.0. Tracked but not in current roadmap.
### MONE — Cosmetic Monetization
- **MONE-01**: Player can purchase cosmetic-only items (planters, walls, gates, tool skins) via fixed-price catalog (no gacha, no random drops).
- **MONE-02**: Player can purchase a one-time "Keeper's Journal" premium upgrade unlocking annotation, organization, and personal-scrapbook features for collected fragments.
- **MONE-03**: Player can purchase Season acceleration that *accelerates waiting* — never *skips story beats*; UI explicitly labels the distinction.
- **MONE-04**: Game uses a server-authoritative entitlements backend (Cloudflare Worker + Stripe webhooks) — no localStorage entitlement claims.
- **MONE-05**: Cosmetic catalog items are permanent (never time-limited), priced transparently, and aesthetic-coherent with the garden setting.
### PWA — Offline-First & Notifications
- **PWA-01**: Player can install the game as a PWA with a service worker manifest.
- **PWA-02**: Player can opt in to push notifications for Memory Storm events only — no daily/marketing/streak notifications, ever.
- **PWA-03**: Service worker caches static assets and serves them offline; saves continue to work without a network connection.
### CLOU — Cloud Save Sync
- **CLOU-01**: Player can sign in (anonymous or email) and sync their save across devices via PocketBase or equivalent backend.
- **CLOU-02**: Cloud save uses the same versioned snapshot format as local save; conflict resolution prompts the player rather than silently overwriting.
### CONT — Post-Launch Content Patches
- **CONT-01**: Project ships free additive content patches post-launch (Hollow Knight model) — additional place-memory vignettes, garden cosmetics, and ambient-only Seasons that fit between authored beats.
### PORT — Steam & Mobile Native Ports
- **PORT-01**: Game ships on Steam (PC/Mac) using Phaser 4 + Electron or equivalent wrapper.
- **PORT-02**: Game ships on iOS and Android as native or hybrid wrappers, with on-device save sync via cloud.
### LOC — Localization
- **LOC-01**: Game supports localization (EN baseline, additional languages added based on community demand) — externalized strings already shipped in v1, only translation pass needed.
### MOD — Modding & Community Content
- **MOD-01**: Project documents a community-content authoring spec that lets community writers contribute fragments respecting tone and provenance discipline.
- **MOD-02**: Project ships a community-content gallery (off-canon, opt-in) where players can browse and load community-authored gardens.
### SOC — Garden Visiting (cautious — must not break tone)
- **SOC-01**: Player can opt in to visiting other players' gardens in read-only mode, viewing what fragments they have collected (without spoilers for unvisited Seasons).
## Out of Scope
Explicit exclusions. Documented to prevent scope creep. **Anti-features tied to the project's thematic constraints — these are not deferred features, they are excluded by design.**
| Feature | Reason |
|---------|--------|
| Gacha mechanics | Directly contradicts the game's thematic argument that complex things cannot be reduced to simple transactions. |
| Lootboxes | Same reason as gacha — undermines the story's monetization-as-meaning argument. |
| Narrative gating behind purchase | The story IS the product; story content is never paid. |
| Season *skipping* (vs. *accelerating*) | Players must never miss authored story beats; acceleration is allowed, skipping is forbidden. |
| Daily login bonuses | FOMO mechanic; violates cozy/contemplative tone. |
| Login streaks | FOMO mechanic; same reason. |
| Limited-time / time-limited content | FOMO mechanic; same reason. |
| Energy / stamina systems | Anti-cozy gating that interrupts contemplative play. |
| Rewarded ads | Anti-cozy; tonally incoherent with a contemplative grief-narrative. |
| Push notification spam | Memory Storm opt-in is the *only* allowed notification — no daily/marketing/streak/re-engagement nags. |
| Lore codex / encyclopedia entries | Players should always feel *almost* understanding; world-building emerges through fragments alone. |
| Generic fantasy flora | Plants must look like real-world species made slightly wrong; no D&D-style fictional flora. |
| Combat / boss fights | The Archivist is not a boss; there is no enemy. Combat would violate the entire thematic frame. |
| Multiplayer / leaderboards (v1) | Solitary, contemplative experience; reconsider for v2 only if it does not break tone. |
| Voiced dialogue (v1) | Tone is "a friend texting you while you're at work"; text fits the medium. Reconsider v2+ only if cello-and-silence soundscape benefits. |
| Always-online | Local-first save model; game must work offline. |
| Named/personality-rich Keeper | Keeper is a presence, not a personality; player projects onto the system. |
| Hint system / objective tracker | Discovery-driven progression (A Dark Room rule); explicit objectives violate the tone. |
| Time-skip purchases that bypass real-time | Real-time *is* the metaphor for memory; skip-time purchases would violate mechanic-as-metaphor doctrine. |
| Unity / Godot / non-web engines (v1) | Web-first per PROJECT.md and lineage of A Dark Room / Universal Paperclips; install friction kills the audience. |
| Generic cosmetic items | Cosmetics must reinforce, not dilute, the garden aesthetic. No generic skins. |
| Random-drop cosmetics | All cosmetics must be deterministic catalog purchases. No RNG monetization. |
| Mobile-style nag UX | Cozy audience expects respect; nag patterns will tank reviews. |
## Traceability
Populated by gsd-roadmapper during roadmap creation on 2026-05-08. Updated after Phase 1 verification on 2026-05-09.
| Requirement | Phase | Status |
|-------------|-------|--------|
| CORE-01 | Phase 1 — Foundations & Doctrine | Complete (scaffold builds; full E2E <5s measurement is Phase 2 PIPE-07) |
| CORE-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; drainTicks fixed-timestep accumulator + Clock injection; scene-driven tick wiring is Plan 02-02) |
| CORE-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01 + 02-05; MAX_OFFLINE_MS=24h clamp + computeOfflineCatchup + PhaserGame.tsx boot path threads catchup → silent drainTicks → letter overlay) |
| CORE-04 | Phase 1 — Foundations & Doctrine | Complete (IDB + localStorage fallback; codec + round-trip; Settings UI is Phase 2) |
| CORE-05 | Phase 1 — Foundations & Doctrine | Complete (navigator.storage.persist() all 4 scenarios; Settings UI surface is Phase 2) |
| CORE-06 | Phase 1 — Foundations & Doctrine | Complete (wrap/unwrap + CRC-32 checksum + SaveCorruptError) |
| CORE-07 | Phase 1 — Foundations & Doctrine | Complete (forward-only migration chain; synthetic v0→v1 tested; real v1→v2 in Phase 4) |
| CORE-08 | Phase 1 — Foundations & Doctrine | Complete (last-3 snapshot retention; Settings UI surface is Phase 2) |
| CORE-09 | Phase 1 — Foundations & Doctrine | Complete (Base64 export/import + 50MB DoS cap; Settings UI surface is Phase 2) |
| CORE-10 | Phase 1 — Foundations & Doctrine | Complete (ESLint boundary rule + Vitest proof) |
| CORE-11 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; drainTicks refuses negative deltas + computeOfflineCatchup clamps to 0; ESLint sim-purity rule mechanically enforces D-33) |
| GARD-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; sim/garden plantSeed + SeedPicker + Garden scene pointerdown wiring) |
| GARD-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; advanceGrowth state machine + plant-renderer primitives + reactive repaint via appStore.subscribe) |
| GARD-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; sim/garden harvest() pure command + selectFragment() integration + Garden.ts pointer wiring) |
| GARD-04 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; sim/garden compost() pure command + content/dialogue/season1/compost-acknowledgements.ink authored ahead of Plan 02-04 Ink runtime) |
| GARD-05 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| GARD-06 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
| GARD-07 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| GARD-08 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
| GARD-09 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
| GARD-10 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| MEMR-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; harvest() invokes selectFragment() exactly once per ready harvest) |
| MEMR-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; 14 yaml + 2 long-form md fragments under /content/seasons/01-soil/; PIPE-01 build-time schema enforcement) |
| MEMR-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; 17 fragments match /^season1\.[a-z0-9._-]+$/ regex; numeric-id rejected by FragmentSchema) |
| MEMR-04 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; src/ui/journal/Journal.tsx full-screen modal grouped by Season + JournalIcon corner affordance) |
| MEMR-05 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; Journal + FragmentRevealModal render fragment bodies in <pre> with userSelect:'text'; pinned by computed-style assertions) |
| MEMR-06 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; src/sim/memory/selector.ts deterministic via mulberry32 seeded from sim state; gated by Season + plant-type tonal register; no-dup; sentinel fallback for Pitfall 8) |
| MEMR-07 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
| STRY-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; 3 Ink beats authored at /content/dialogue/season1/lura-{arrival,mid,farewell}.ink, gated at 1/4/8 harvests via sim/narrative/lura-gate.ts; LuraDialogue overlay renders inkjs Story with text-message cadence) |
| STRY-02 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
| STRY-03 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
| STRY-04 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
| STRY-05 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
| STRY-06 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; scripts/compile-ink.mjs invokes bundled inklecate binary; src/content/ink-loader.ts lazy-loads compiled JSON; npm run ci compiles before tests + build. Assumption A6 verified) |
| STRY-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; vacuously satisfied — zero Keeper-spoken lines in Phase 2 .ink files; Phase 7 lands the binary choice surface) |
| STRY-08 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
| STRY-09 | Phase 1 — Foundations & Doctrine | Complete (vacuous — /content/ convention established; no player-visible strings in Phase 1 source) |
| STRY-10 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; lura-gate gates on harvest count not wall time; STRY-10 test case advances FakeClock 24h with 0 harvests and confirms no beat fires; ESLint sim-purity rule prevents Date.now in src/sim/narrative/) |
| SEAS-01 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| SEAS-02 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| SEAS-03 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| SEAS-04 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| SEAS-05 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| SEAS-06 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
| SEAS-07 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
| SEAS-08 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
| SEAS-09 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
| SEAS-10 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
| AEST-01 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| AEST-02 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| AEST-03 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| AEST-04 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| AEST-05 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| AEST-06 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| AEST-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; BeginScreen + bootstrapAudioContext synchronous-inside-click defends Pitfall 5) |
| AEST-08 | Phase 1 — Foundations & Doctrine | Complete (Zod ProvenanceSchema 6 fields + CI gate; north-star reference set deferred to Phase 5 per IOU) |
| AEST-09 | Phase 1 — Foundations & Doctrine | Complete (human curation gate mechanism in place; recorded human decision in 01-05-IOU.md) |
| UX-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; single fixed-position Begin overlay; no HUD/journal/settings; D-22 dismissal) |
| UX-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-05; letter-from-the-garden.ink + Letter overlay + boot path silent catchup → openLetter at ≥5min absence; Pitfall 9 audio bootstrap on dismiss) |
| UX-03 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| UX-04 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| UX-05 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
| UX-06 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| UX-07 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| UX-08 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| UX-09 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| UX-10 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01 + 02-05; registerSaveLifecycleHooks + saveOnSeasonTransition; PhaserGame.tsx boot path wires saveSync via clock.now() with synchronous LocalStorage write + best-effort IDB) |
| UX-11 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; formatHumanReadable K/M/B/T/scientific; BigQty.format() delegates) |
| UX-12 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| UX-13 | Phase 1 — Foundations & Doctrine | Complete (anti-fomo-doctrine.md authored + doc-lint tested; review-enforced per CONTEXT D-07) |
| PIPE-01 | Phase 1 — Foundations & Doctrine | Complete (Vite-native loader + Zod schemas; build fails on schema violation) |
| PIPE-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; loadSeasonFragments lazy import.meta.glob surface in src/content/loader.ts. Plan 02-03; scripts/check-bundle-split.mjs structural verifier integrated into npm run ci.) |
| PIPE-03 | Phase 1 — Foundations & Doctrine | Complete (validate-assets.mjs + ProvenanceSchema + refused-sample fixture + 2 Vitest tests) |
| PIPE-04 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
| PIPE-05 | Phase 1 — Foundations & Doctrine | Complete (both doctrine docs authored + 8 doc-lint assertions green) |
| PIPE-06 | Phase 1 — Foundations & Doctrine | Complete (ci.yml runs npm run ci on push + PR; 53 tests / 12 files green) |
| PIPE-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-05; tests/e2e/season1-loop.spec.ts — full Phase-2 loop in Chromium with FakeClock injection, 1.5s test runtime, 4s end-to-end) |
**Per-Phase Counts:**
| Phase | Requirements |
|-------|--------------|
| Phase 1 — Foundations & Doctrine | 16 |
| Phase 2 — Season 1 Vertical Slice (Soil) | 24 |
| Phase 3 — Watercolor & Cello Aesthetic | 8 |
| Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | 8 |
| Phase 5 — Seasons 3-4 (Canopy & Storm) | 4 |
| Phase 6 — Seasons 5-6 (Depth & Loom) | 5 |
| Phase 7 — Season 7 (Return) & Final Choice | 4 |
| Phase 8 — UX, Accessibility & Launch Polish | 8 |
| **Total** | **77** |
**Coverage:**
- v1 requirements: 77 total (the "78" in the prior coverage block was a counting error in initial drafting; categories sum to 11+10+7+10+10+9+13+7 = 77 numbered REQ-IDs)
- Mapped to phases: 77 (100%)
- Unmapped: 0
---
*Requirements defined: 2026-05-08*
*Last updated: 2026-05-09 after Plan 02-05 execution (40/77 REQ-IDs marked Complete — Phase 1 + Phase 2 fully shipped pending /gsd-verify-work)*