feat(01-04): Vite-native content pipeline + Zod schemas + demo fragment + /content/ README
- 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
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# /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.
|
||||
Reference in New Issue
Block a user