PIPE-01 + STRY-09 satisfied. Vite-native loader with literal import.meta.glob patterns; FragmentSchema regex enforced; demo fragment proves round-trip; 5 Vitest assertions cover schema violations; content/README.md is the writer-facing contract.
18 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundations-and-doctrine | 04 | content-pipeline |
|
|
|
|
|
|
|
|
|
8min | 2026-05-09 |
Phase 1 Plan 04: Content Pipeline Summary
Vite-native build-time content pipeline (src/content/loader.ts) with Zod schemas (FragmentSchema + SeasonContentSchema), the stable-string-ID regex ^season\d+\.[a-z0-9._-]+$ from CLAUDE.md, one demo fragment proving end-to-end round-trip, and 5 Vitest assertions proving schema violations throw at module-eval time (PIPE-01).
Performance
- Duration: ~8 min
- Started: 2026-05-09T03:21:00Z (approx)
- Completed: 2026-05-09T03:30:00Z
- Tasks: 2 (both completed atomically)
- Files created: 8 (5 source + 1 test + 1 demo content + 1 README); 1 deleted (
src/content/.gitkeep)
Accomplishments
- Zod schemas with the stable-string-ID regex.
FragmentSchemarejects numeric IDs and IDs that don't match^season\d+\.[a-z0-9._-]+$.SeasonContentSchemawrapsfragments[]so a YAML file with malformed top-level shape is also rejected. Both schemas are colocated insrc/content/schemas/with a barrel re-export. - Vite-native loader using literal
import.meta.globpatterns (RESEARCH Pitfall 1 honored). Two glob calls — one for/content/seasons/*/fragments.yaml, one for/content/seasons/*/fragments/*.md— both with{ eager: true, query: '?raw', import: 'default' }. Throws on schema violation at module-eval time, which failsnpm run buildnon-zero (PIPE-01). - One demo fragment proves round-trip.
season0.demo.first-lightunder/content/seasons/00-demo/fragments.yamlvalidates and is included in the production bundle.npm run buildis green, which means the loader executed and the schema accepted the demo. - 5 Vitest assertions cover the schema-violation matrix. 2 happy-path (empty globs, valid YAML) + 3 schema-violation throws (numeric id, season out of [0,7] range, Markdown frontmatter missing required id). All 5 pass; the full Phase-1 suite (sentinel + content) is 6 green tests.
content/README.mddocuments the convention for Phase 2 writers. Captures the directory shape, the ID regex, both YAML and Markdown authoring forms, the validation guarantee, and the Phase-2 transition notes (e.g., season range will narrow to [1,7] once the demo is removed). Phase 2 writers can author fragments without reading TypeScript.compile:inkno-op stub from Plan 01 confirmed runnable. Verifiednpm run compile:inkexits 0 with the placeholder echo message, per CONTEXT D-08 (Ink deferred to Phase 2).
Task Commits
Each task was committed atomically:
- Task 1: Vite-native content pipeline + Zod schemas + demo fragment + /content/ README —
d52e35f(feat) - Task 2: PIPE-01 enforcement test — schema violations throw at content load —
c49710e(test)
Note: Plan-level final metadata commit (this SUMMARY.md) is owned by the orchestrator after all parallel-wave agents return.
Files Created/Modified
Created (8):
src/content/schemas/fragment.ts— FragmentSchema (Zod) with id regex^season\d+\.[a-z0-9._-]+$, seasonint [0,7], bodymin(1). ExportsFragmenttype viaz.infer.src/content/schemas/season.ts— SeasonContentSchema wrappingz.array(FragmentSchema). ExportsSeasonContenttype.src/content/schemas/index.ts— Barrel re-export of both schemas + types.src/content/loader.ts— Twoimport.meta.globcalls (yaml + md) with literal patterns;loadYamlFragments+loadMdFragmentshelpers throw on schema violation; flatfragments: Fragment[]export; test-onlyloadFragmentsFromGlob(yamlGlob, mdGlob?)helper for unit-test injection.src/content/index.ts— Public surface re-exportingfragments,loadFragmentsFromGlob, schemas, and types for Phase 2 consumers.src/content/loader.test.ts— 5 Vitest assertions: empty globs, valid YAML round-trip, numeric-id throws, season-99 throws, missing-id-frontmatter throws. All asserting the[content] schema violationerror-message prefix.content/seasons/00-demo/fragments.yaml— Demo fragmentseason0.demo.first-lightproving end-to-end round-trip (removed in Phase 2).content/README.md— Writer-facing documentation of the /content/ convention, ID regex, YAML and Markdown authoring options, validation guarantee, and Phase 2 transition notes.
Deleted (1):
src/content/.gitkeep— Was placed in Plan 01 as a firewall marker; removed now that real source files populate the directory. The wave1_handoff explicitly identifies this as the marker this plan was meant to retire.
Modified: None.
Decisions Made
- Explicit
.tsimport suffixes +import typefor type-only re-exports.tsconfig.app.jsonhasverbatimModuleSyntax: trueandallowImportingTsExtensions: true. Without the.tssuffix on imports the build fails type-checking; withoutimport typeforFragment/SeasonContentre-exports the strict-TS lint blocks the surface. This is a global pattern for the project; future plans should follow it. - Demo fragment uses
season: 0. The schema accepts[0, 7]so the demo can ship in Phase 1 without polluting Season 1's slug space. Phase 2 will narrow the range to[1, 7]when the demo is removed. Both the schema comment and the README flag this transition. - No
content/seasons/00-demo/.gitkeepcreated. The directory has a realfragments.yamlfile. Phase 2 will replace the entire directory anyway. Adding.gitkeepalongside a real file is dead weight. loader.tsships both YAML and Markdown glob handling in Phase 1. Phase 2 should not need to re-editloader.tsto begin authoring per-file Markdown fragments. ThemdFilesglob simply expands to{}until/content/seasons/<slug>/fragments/*.mdfiles exist. The Markdown path is exercised by Test 5 inloader.test.ts.- Build-time error message includes the offending file path. Every throw carries
[content] schema violation in <path>so anpm run buildfailure points the writer at the broken file. Tests assert this prefix via regex, so any future refactor that drops the prefix breaks the suite.
Deviations from Plan
None — plan executed exactly as written. The plan's <action> block was followed verbatim across both tasks; the only minor adjustments were:
- Adding
.tsimport suffixes andimport typefor type-only re-exports (required by the project's existing TS config; not a deviation, just an environmental requirement). The plan's pseudo-code was language-agnostic; the actual file-on-disk respects the project's strict-TS verbatim-module-syntax setting.
If the orchestrator's deviation-detection treats those as adjustments, classify them as Rule 3 (blocking — without them, tsc -b fails). They were applied transparently and required no scope change.
Issues Encountered
None. npm run build passed first try; npx vitest run src/content/loader.test.ts passed all 5 tests first try; npm test (full suite) is 6 green.
Authentication Gates
None — content pipeline is build-time only; no external services.
Threat Flags
None. Per the plan's <threat_model>: "content pipeline threats are minimal. Path traversal via import.meta.glob (a malicious content file with ../../ in frontmatter) is not exploitable: Vite glob expansion is at build time; the validator step never resolves paths from frontmatter values." This plan introduced no new network endpoints, auth paths, file-access patterns, or schema changes at trust boundaries.
Known Stubs
compile:inkis a no-op echo +exit 0. Inherited from Plan 01; not introduced by this plan. Per CONTEXT D-08 / RESEARCH § "Pattern 4 — Ink files in Phase 1": Phase 2 will replace this withinklecate -o src/content/compiled-ink/ content/dialogue/*.inkonce authored Ink files exist./content/seasons/00-demo/fragments.yamlitself is a stub. Intentional. It exists only to prove the loader round-trips end-to-end. Phase 2 deletes/content/seasons/00-demo/and creates/content/seasons/01-soil/with real fragments. The README and schema comment both document this transition.- Markdown glob expands to
{}in Phase 1. No per-file Markdown fragments exist yet. The wiring is in place so Phase 2 can author them without re-editingloader.ts. Test 5 inloader.test.tsexercises the Markdown validation path via the test helper.
These are all intentional Phase-1 stubs; none block production at the Phase-1 boundary.
Next Plan / Next Phase Readiness
Plan 06 (doctrine docs): Ready. npm run build succeeds, so the doctrine docs can reference this content-pipeline implementation as a working example of the build-as-validator pattern.
Plan 07 (CI workflow): Ready. The ci script (npm run lint && npm run test && npm run validate:assets && npm run build) now has a green npm run build and a green npm test (sentinel + 5 PIPE-01 assertions). Plan 07's CI workflow YAML can call npm ci && npm run ci and rely on the build step exercising the content pipeline.
Phase 2 (Season 1 vertical slice): The loader is the contract Phase 2 writes against. When Phase 2 begins authoring /content/seasons/01-soil/:
- Follow
content/README.mdSection "Adding fragments" for the YAML and Markdown authoring forms. - Use the ID convention
season1.<slug>per the regex^season1\.[a-z0-9._-]+$. - The test in
src/content/loader.test.tsproves any deviation from the schema fails the build. - Delete
/content/seasons/00-demo/when real Season 1 content lands. - Narrow the schema range from
[0, 7]to[1, 7]insrc/content/schemas/fragment.tsonce the demo is removed. - Begin authoring
.inkfiles under/content/dialogue/and replace thecompile:inkno-op with the realinklecateinvocation.
For Phase 2's writer (writer-facing summary):
- Fragment ID regex:
^season\d+\.[a-z0-9._-]+$ - Demo fragment path (delete in Phase 2):
content/seasons/00-demo/fragments.yaml compile:inkis currently a no-op echo (per CONTEXT D-08); Phase 2 swaps it for a real Ink compile step.- When authoring real fragments, follow
content/README.md"Adding fragments" — the test insrc/content/loader.test.tsproves any deviation from the schema fails the build.
Self-Check
src/content/schemas/fragment.tsexists — verified.src/content/schemas/season.tsexists — verified.src/content/schemas/index.tsexists — verified.src/content/loader.tsexists — verified.src/content/index.tsexists — verified.src/content/loader.test.tsexists — verified.content/seasons/00-demo/fragments.yamlexists withseason0.demofragment — verified.content/README.mdexists, documentsseason<N>convention, says "Never use numeric IDs" — verified.content/dialogue/.gitkeepexists (inherited from Plan 01) — verified.- FragmentSchema enforces the regex
^season\d+\.[a-z0-9._-]+$— verified by inspection ofsrc/content/schemas/fragment.ts. loader.tscallsimport.meta.globwith literal patterns (2 calls — yaml + md) — verified by inspection.loader.tsthrows on schema violation (throw new Error("[content] schema violation ...) — verified by inspection.npm run buildexits 0 — verified.npm run compile:inkexits 0 — verified.npx vitest run src/content/loader.test.tspasses 5 tests — verified.npm testpasses the entire Phase-1 suite (6 tests) — verified.- Task 1 commit
d52e35fexists — verified ingit log. - Task 2 commit
c49710eexists — verified ingit log.
## Self-Check: PASSED
Phase: 01-foundations-and-doctrine Plan: 04 of 7 Completed: 2026-05-09