feat(02-03): Season-1 fragments + sim/memory selector + harvest/compost commands

Task 1 of Plan 02-03: ship Season-1 authored content + the deterministic
fragment selector + extend sim/garden/commands.ts with harvest + compost.

Content (≥17 Season-1 fragments under /content/seasons/01-soil/):
- 14 in fragments.yaml (9 warm / 3 contemplative / 2 heavy + 1 _meta sentinel)
- 2 long-form Markdown fragments (lura-first-letter.md, winter-rose-night.md)
- Pool depth (W6): warm pool ≥9 satisfies the worst-case all-rosemary
  playthrough at the 8th-harvest Lura threshold (CONTEXT D-14)
- All ids match /^season1\.[a-z0-9._-]+$/ (FragmentSchema regex; CLAUDE.md
  stable-string-ID rule); bible voice maintained throughout

FragmentSchema extension (back-compat — tags is optional):
- Optional `tags: z.array(z.string()).optional()` for tonal-register gating
- Reserved tag `_meta` excludes the exhaustion sentinel from the normal pool

src/sim/memory/ (new module):
- pool.ts — filterPool() pure helper (Season + tonal-register + no-dup gates)
- selector.ts — selectFragment() deterministic + mulberry32 PRNG +
  EXHAUSTION_FALLBACK_ID for Pitfall 8 fallback
- selector.test.ts — 16 tests covering gating / no-dup / determinism /
  sentinel-fallback / sentinel-exclusion-from-normal-pool
- index.ts — barrel; src/sim/index.ts re-exports

src/sim/garden/commands.ts (extended):
- harvest() pure command — empties tile, appends one fragment id,
  re-computes unlockedPlantTypes (Pitfall 10: thresholds checked AFTER
  the harvest commit). Refuses immature plants and OOR indices.
- compost() pure command — empties tile regardless of stage; no fragment
  yield (D-07); no resource refund (D-04 = infinite seeds).
- SimContext interface — application-layer-injected (fragments, currentSeason)
- simulateOneTick() takes optional ctx (default empty pool); harvest/compost
  branches added to the kind switch.
- BLOCKER 3 invariant preserved — sim writes tickCount, never lastTickAt.

Plant-type unlock thresholds (CONTEXT D-05, plan author's discretion):
- rosemary @ count 0 (start)
- yarrow @ count 3 (third harvest)
- winter-rose @ count 6 (sixth harvest)

commands.test.ts: +18 new cases (harvest / compost / Pitfall 10 boundary
on yarrow + winter-rose / sentinel fallback / immutability). 65/65 tests
green across src/sim/memory + src/sim/garden + src/content; lint exits 0;
build green (Vite parses all 17 fragments without schema violation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 10:00:38 -04:00
parent d052a35478
commit f192e8298c
12 changed files with 926 additions and 14 deletions
@@ -0,0 +1,14 @@
---
id: season1.soil.lura-first-letter
season: 1
tags: [warm]
---
Lura wrote you a letter once, and never sent it. It was about a recipe — the
proportions of vinegar to honey, and how long to let the onions sit. Most of
the letter is the recipe. Two paragraphs at the bottom are about something
else: a bee in a kitchen window, a song you didn't recognize, the shape your
hand made on a glass.
She left the letter in a drawer, decided it sounded too much. Then there was
no drawer, and no letter. The recipe is real. You could find it again, if you
asked.
@@ -0,0 +1,13 @@
---
id: season1.soil.winter-rose-night
season: 1
tags: [heavy]
---
Winter-rose blooms at night. This is, technically, slander — the rose blooms
when it blooms, and the night is when most people are asleep, and so the
night is when most people fail to see things bloom. But the slander stuck.
A flower for the people who couldn't sleep.
Someone, in this place, used to set a chair by the window in February and
wait. The wait was the thing. The flower would bloom in its own time. Most
good things were like that, until they weren't.