d52e35f3ad
- FragmentSchema with stable-string-ID regex /^season\d+\.[a-z0-9._-]+$/ - SeasonContentSchema wraps fragments[] - loader.ts uses import.meta.glob with literal patterns (Pitfall 1) - Throws on schema violation at module-eval time, failing npm run build (PIPE-01) - Test-only loadFragmentsFromGlob helper for unit-test injection - Demo fragment season0.demo.first-light proves end-to-end round-trip - content/README.md documents the convention for Phase 2 writers (STRY-09) - Removes now-redundant src/content/.gitkeep firewall marker
111 lines
3.5 KiB
Markdown
111 lines
3.5 KiB
Markdown
# /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<N>.<id>
|
||
```
|
||
|
||
where `<N>` is `0..7` and `<id>` 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/<slug>/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/<slug>/fragments/<slug>.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.
|