feat(02-04): ink compilation pipeline + 4 authored Season-1 Ink files + runtime loader

- scripts/compile-ink.mjs: build-time inklecate runner using bundled binary
  (BLOCKER 4 — uses node_modules/inklecate/bin, not stale -windows/-mac path strings).
  Assumption A6 verified first-try on Windows; the same binary path resolution
  works on macOS + Linux per the wrapper's own getInklecatePath convention.
- scripts/compile-ink.test.mjs: 3 Vitest cases proving the compiler runs +
  emits valid JSON with inkVersion. wipe=false for the test path so it can
  run in parallel with the ink-loader test without racing on the wipe step.
- 4 Season-1 .ink files authored in voice (Lura warmth-anchor, gardener-keeper
  for compost): lura-arrival.ink, lura-mid.ink, lura-farewell.ink,
  compost-acknowledgements.ink (rewrite of Plan 02-03 scaffolded version into
  VAR-driven branch shape consumable by the runtime).
- src/content/ink-loader.ts: loadInkStory + bindGardenStateToInk +
  INK_VARIABLE_MAP. Centralized snake_case slot mapping per Pitfall 4. UTF-8
  BOM stripped before Story instantiation.
- src/content/ink-loader.test.ts: 8 cases — Story instantiation for all 4
  beats, fragment_count binding, Pitfall 4 snake_case enforcement, silent
  skip for stories missing declared vars.
- package.json: build now runs compile:ink first; ci chain runs compile:ink
  before test so ink-loader.test.ts's precondition check passes.
- .gitignore: src/content/compiled-ink/ excluded (regenerated on every build).

npm run ci exits 0; 11 new tests green (228 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 10:24:40 -04:00
parent 348c76a537
commit c90f8f1e5c
11 changed files with 674 additions and 39 deletions
@@ -1,43 +1,42 @@
// content/dialogue/season1/compost-acknowledgements.ink
// Compost acknowledgements — D-07 + GARD-04. Plan 02-03 authored content;
// Plan 02-04 ships the Ink runtime that consumes it.
//
// Plan 02-03 ships the AUTHORED CONTENT for the compost tonal beat
// (CONTEXT D-07 + GARD-04). Plan 02-04 owns the Ink runtime — this file
// is loaded by the Ink runtime (inkjs) at that point and one of these
// short lines is dripped into the dialogue overlay each time the player
// composts an immature plant.
// Phase 2 NOTE — UI WIRING DEFERRED TO PLAN 02-05:
// Plan 02-04 ships the Ink compile pipeline + runtime + LuraDialogue
// overlay. The compost-beat surface is a thinner toast variant (separate
// from the Lura full-screen overlay) and is folded into Plan 02-05's
// persistence-toast UI surface for minimum-viable-bias reasons documented
// in 02-04-SUMMARY.md.
//
// In Plan 02-03 the React surface (Garden.ts handleTilePointerDown's
// compost branch) does NOT yet render these lines — there's a TODO
// comment at the call site marking the Plan 02-04 wiring point. The
// content lives here so the writer can iterate on voice without waiting
// for the runtime to land.
// This file is rewritten in VAR-driven branch form (replacing Plan 02-03's
// choice-list shape) so it matches the runtime contract: one ChoosePathString
// → drip lines → END. The branching uses fragment_count to vary the line
// without requiring the runtime to expose Ink choice points.
//
// Tone (CLAUDE.md): warm, specific, intermittent, sometimes funny,
// sometimes devastating. The gardener-keeper voice. NOT Lura. The garden
// is acknowledging the player's choice to let go — never sentimental,
// never reassuring, never "it's okay." Just the small fact of the choice,
// honored.
//
// Phase 2 ships ~6 short lines so the player rarely hears the same line
// twice in a single session. Plan 02-04 will randomize selection (via
// the same mulberry32 pattern as the fragment selector, or a simple
// weighted pick — implementer's choice).
// Tone (CLAUDE.md): the gardener-keeper voice, NOT Lura. Warm, specific,
// intermittent. Acknowledges the player's choice to let go without making
// it a moral. Never "it's okay." Never reassurance. Just the small fact
// of the choice, honored.
=== compost_beats ===
* The earth takes it back without comment.
->DONE
VAR fragment_count = 0
* Some things are tended into being. Others are tended into not being. Both count.
->DONE
== compost ==
* The space the plant occupied is now space. That is a kind of progress.
->DONE
{ fragment_count == 0:
The earth takes it back without comment.
- else:
{
- fragment_count % 5 == 0:
Some things are tended into being. Others are tended into not being. Both count.
- fragment_count % 4 == 0:
It wasn't ready. That isn't the same as failing.
- fragment_count % 3 == 0:
The space the plant was in is now space. That's a kind of progress.
- fragment_count % 2 == 0:
It returns to the soil. Not poetry — just composting. Mostly.
- else:
You changed your mind. The garden has nothing to say about it.
}
}
* It returns to the soil it came from. Not poetry — just composting. Mostly.
->DONE
* The garden is bigger by one empty tile.
->DONE
* You changed your mind. The garden has nothing to say about it.
->DONE
-> END