# /content/ — authored content tree All player-visible strings, memory fragments, and dialogue live here, never in `src/`. The build pipeline (`src/content/loader.ts`) reads this tree at build time, validates against Zod schemas, and emits typed values into the runtime bundle. This is the contract. Phase 2's writer can author against it without reading any TypeScript. ## Directory shape ``` /content/ ├── seasons/ │ ├── 00-demo/ # Phase 1 only; removed in Phase 2 │ │ └── fragments.yaml │ ├── 01-soil/ # Phase 2 fills this │ │ ├── fragments.yaml # bulk-authored fragments │ │ └── fragments/ # one-per-file long-form fragments (.md with frontmatter) │ │ └── lura-first-letter.md │ ├── 02-roots/ # Phase 4 │ └── ... # Seasons 3–7 added in Phase 5+ ├── dialogue/ # Phase 2+ Ink (.ink) files │ └── (empty in Phase 1) └── README.md (this file) ``` ## Fragment ID convention (locked — see CLAUDE.md) Fragment IDs are stable strings of the shape: ``` season. ``` where `` is `0..7` and `` matches `[a-z0-9._-]+`. Examples: - `season1.soil.first-bloom` - `season3.canopy.lura_07.vignette` **Never use numeric IDs.** Renames are forbidden once a fragment ships; re-authoring an existing fragment changes its body, never its ID. The exact regex enforced by `src/content/schemas/fragment.ts` is: ``` ^season\d+\.[a-z0-9._-]+$ ``` ## Adding fragments ### Option A — bulk YAML (preferred for short fragments) Add an entry to `/content/seasons//fragments.yaml`: ```yaml fragments: - id: season1.soil.first-bloom season: 1 body: | Multi-line text here. ``` ### Option B — one-per-file Markdown with frontmatter (for longer pieces) Create `/content/seasons//fragments/.md`: ```markdown --- id: season1.soil.lura-first-letter season: 1 --- The body of the fragment goes here as Markdown. Frontmatter holds the structured fields; the body is everything after the closing `---`. ``` The loader (`src/content/loader.ts`) merges frontmatter + body into the same `Fragment` shape as the YAML form. ## Validation (PIPE-01) Every fragment is validated by the Zod schema in `src/content/schemas/fragment.ts`. A schema violation throws at module-eval time, which fails `npm run build`. Test coverage in `src/content/loader.test.ts` proves the schema rejects: - numeric IDs (violates the stable-string rule) - season values outside `[0, 7]` - Markdown frontmatter missing required fields If your edit causes the build or tests to fail with a `[content] schema violation` error, the message includes the offending file path. ## Ink dialogue Phase 1 installs `inkjs` + `inklecate` and ships a no-op `npm run compile:ink` script. Phase 2 begins authoring `.ink` files under `/content/dialogue/` and replaces the no-op with `inklecate -o src/content/compiled-ink/ content/dialogue/*.ink`. ## Deferred (Phase 2+) - **Per-Season lazy loading:** Phase 2 switches to `{ eager: false }` for Seasons 2–7 so the initial bundle contains only Season 1 (PIPE-02). - **Tag/keyword indices:** Phase 5+ may add fragment tagging if the Memory Storm UI needs filtered queries. - **Season-range narrowing:** Phase 2 narrows the `season` field to `[1, 7]` when the demo fragment is removed.