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
+177 -6
View File
@@ -1,10 +1,181 @@
# /content/seasons/01-soil/fragments.yaml
#
# Phase 2 placeholder. Plan 02-03 replaces with the authored Season-1
# fragments (≥10 in voice, MEMR-* coverage). The single placeholder
# fragment here keeps the eager fragment loader green during Plan 02-02
# (Plan 02-03 expands the file).
# Phase 2 Plan 02-03 — Season 1 ("Soil") authored fragment pool.
#
# Bible voice (CLAUDE.md "Tone"): warm, specific, intermittent, sometimes
# funny, sometimes devastating. Lura is the warmth anchor (Plan 02-04);
# Phase 2 Wave 1 ships the gardener-keeper voice — the contrast, not a
# co-griever.
#
# Tag tonal registers (Plan 02-03 extension to FragmentSchema):
# warm — light, mundane, sometimes funny (rosemary pool)
# contemplative — quiet weight, the shape of an absence (yarrow pool)
# heavy — clear-eyed grief; never melodrama (winter-rose pool)
# _meta — selector-only sentinel; the gated pool excludes this tag
#
# Pool depth (Plan W6 fix): a worst-case all-rosemary playthrough must not
# exhaust the warm pool before the 8th harvest (Lura's farewell threshold,
# CONTEXT D-14). The warm pool below ships ≥9 entries for the 1-buffer
# safety margin. The exhaustion sentinel `season1.soil._exhaustion` is a
# defensive fallback (RESEARCH Pitfall 8); under normal Phase-2 play it is
# unreachable.
#
# IDs match /^season1\.[a-z0-9._-]+$/ (FragmentSchema regex; CLAUDE.md
# stable-string-ID rule). IDs are forever — once shipped, only the body
# may change, never the id.
fragments:
- id: season1.soil.placeholder
# ----- WARM tonal register (rosemary pool) -----
- id: season1.soil.first-bloom
season: 1
body: "(placeholder — Plan 02-03 ships authored fragments)"
tags: [warm]
body: |
The first thing that grew was rosemary. The shape of it didn't matter
so much as the smell — sharp, the kind of green that means the air
will warm up by afternoon.
- id: season1.soil.bread-was-easy
season: 1
tags: [warm]
body: |
Someone, in the place this came from, was very good at bread. There
isn't a name attached. There is the shape of an oven door, and a
towel folded a particular way.
- id: season1.soil.the-cat
season: 1
tags: [warm]
body: |
The cat is missing now too. It used to walk along the wall at dusk.
It would not come when called. It came anyway, in its own time. Most
good things were like that.
- id: season1.soil.kettle-on-the-hob
season: 1
tags: [warm]
body: |
A kettle, a little dented on one side, lived on a stove that no
longer exists. It whistled flat — half a step under the note it was
meant to make. Nobody ever fixed it. Nobody ever needed to.
- id: season1.soil.the-wrong-song
season: 1
tags: [warm]
body: |
Someone in the kitchen used to sing a song with the words mostly
wrong. They would commit to the wrong words anyway, full voice. It
was funnier each time. The garden has the rhythm but not the words.
- id: season1.soil.the-jam-summer
season: 1
tags: [warm]
body: |
There was a summer where someone made too much jam. Apricot, mostly.
The cupboards filled. People came over and were given jam. Strangers
were given jam. It became a small embarrassment, and then a joke,
and then a kindness people remembered for a long time after.
- id: season1.soil.boots-by-the-door
season: 1
tags: [warm]
body: |
Two pairs of boots used to sit by a door. One pair larger, one pair
smaller. They were left muddy more often than not. Whoever it was
that minded the mud, in the end, did not really mind it.
- id: season1.soil.the-good-spoon
season: 1
tags: [warm]
body: |
Every kitchen has a good spoon. The one you reach for without
thinking. This one was wooden, with a small burn mark on the handle
from a moment of inattention years ago. It outlasted the inattentive
person. Some objects are like that.
- id: season1.soil.the-laughing-fit
season: 1
tags: [warm]
body: |
A laughing fit at a funeral. The kind that makes things worse and
better at once. It started over something nobody could later
identify. They were all forgiven. Mostly by themselves, after a
decent interval.
# ----- CONTEMPLATIVE tonal register (yarrow pool) -----
- id: season1.soil.what-the-wind-was-for
season: 1
tags: [contemplative]
body: |
The wind used to mean something specific in spring — a person putting
sheets out to dry, the line across two posts, the way it would crack
like a small flag. That meaning has gone soft. The wind still blows.
- id: season1.soil.the-letter-not-sent
season: 1
tags: [contemplative]
body: |
There was a letter someone meant to send. The address is gone, the
ink is gone, the reason is gone. What remains is the silence on the
other side of it — a room, somewhere, that never received the news.
- id: season1.soil.numbers-in-the-margin
season: 1
tags: [contemplative]
body: |
A book had a number written in the margin: 47. Whose age, whose page,
whose count of something — gone. The 47 sits very calmly on the
paper. Numbers are the last to forget. They will outlast all of us.
- id: season1.soil.the-clock-that-stopped
season: 1
tags: [contemplative]
body: |
A clock on a mantel stopped at 4:18. Nobody wound it again. It was
not a meaningful hour. It was the hour the hand happened to be on
when nobody was looking. Now it is the only hour, forever, in that
one small place.
# ----- HEAVY tonal register (winter-rose pool) -----
- id: season1.soil.the-name-she-used
season: 1
tags: [heavy]
body: |
She had a name for him that wasn't his name. He had stopped objecting
to it long before the end. After, the name kept arriving — at the
door, in the post, in the mouths of people who had heard it once and
never been corrected. The garden does not say it. The garden only
grows.
- id: season1.soil.what-the-snow-took
season: 1
tags: [heavy]
body: |
Snow took the orchard one March. The trees were already old. The
orchard had been someone's grandfather's, then someone's father's,
then a row of stumps and a few unrooted sticks pretending. Pretending
is also a kind of remembering, until one day it isn't.
- id: season1.soil.the-quiet-after
season: 1
tags: [heavy]
body: |
There is a quiet that comes after, that is not the same as the quiet
that came before. The room is the same. The light is the same. The
quiet is differently shaped — slightly larger than the room, somehow.
Nobody needs to explain this to anyone who has felt it.
# ----- EXHAUSTION FALLBACK (RESEARCH Pitfall 8) -----
# Returned ONLY when the gated pool is empty. The pool excludes anything
# tagged `_meta`; selector.ts looks this id up explicitly via
# EXHAUSTION_FALLBACK_ID. In normal Phase-2 play this is unreachable
# (the warm pool is sized to outlast the 8th-harvest Lura threshold),
# but the sentinel is the documented "behavior chosen" for the
# gated-pool-exhaustion case and is committed to the corpus so the
# selector has something to return rather than null.
- id: season1.soil._exhaustion
season: 1
tags: [_meta]
body: |
The garden knows this one already. The light comes in the same way it
came yesterday. There will be a new thing tomorrow. There is also
this — the steady part, that does not need re-learning.