Task 3 of Plan 02-03: ship the PIPE-02 structural assertion that Season-1
content reaches the build output. Three structural checks (any one
sufficient): chunk filename slug match (fragments / season1 / 01-soil),
chunk-contents reference to /content/seasons/01-soil/ source path or to
known fragment ids, and a future-extension hook for index.html manifest
inspection.
Phase 2 ships eager-corpus loading alongside the lazy
loadSeasonFragments surface, so currently chunkContentMatch=true via
inlined ?raw content. When Plan 02-04+ switches consumers to lazy-only
and Vite emits a separate Season-1 chunk, chunkNameMatch will also
start passing — at which point either path satisfies the assertion. The
plumbing is structurally proven now; the chunk-naming side is documented
as the path of least resistance for the Phase-4 Season-2 onboarding.
scripts/check-bundle-split.mjs:
- Refactored body into export function runCheck() returning a structured
result; the CLI invocation guard wraps process.exit so Vitest can
import the module without termination (verified by the test file).
scripts/check-bundle-split.test.mjs:
- 3 cases: file exists, parses + imports without process.exit firing,
runCheck() returns the documented {ok, message, chunkNameMatch,
chunkContentMatch, files} shape. The on-disk dist/-required happy path
fires via the package.json scripts.ci chain (`npm run build &&
npm run check:bundle-split`).
package.json:
- New `check:bundle-split` script.
- `ci` chain extended: lint → test → validate:assets → build →
check:bundle-split. dist/ is populated by build before the bundle-split
assertion runs.
`npm run ci` exits 0 end-to-end. 217/217 tests green (was 214; +3 new
this task). The PIPE-02 verification step now refuses any future change
that breaks the lazy-content plumbing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 2 of Plan 02-03: ship the Memory Journal UI surfaces (D-23/D-24/D-25)
and wire harvest + compost pointer events through the Garden scene to the
sim → store → React reveal flow.
src/ui/journal/ (new module):
- Journal.tsx — full-screen modal (D-24); fragments grouped by Season;
DOM-rendered text with userSelect: text per MEMR-05; reads
harvestedFragmentIds from the store; resolves ids against the eager
`fragments` corpus (defensive — unresolvable ids skip silently).
- FragmentRevealModal.tsx — D-25 active-play reveal modal; backdrop click
+ inner Close button dismiss; event.stopPropagation on the article
body so clicking inside the text doesn't dismiss; defensive silent
dismiss on unresolvable id.
- journal-icon.tsx — D-23 + D-29 corner affordance; gated by
selectJournalRevealed (`harvestedFragmentIds.length > 0`); local open
state (no store pollution); 'j' hotkey deferred to Plan 02-05.
- index.ts — barrel.
- 16 new Vitest cases across 3 test files (Journal: 7 / FragmentRevealModal:
6 / journal-icon: 3); all green.
src/App.tsx — mount FragmentRevealModal + JournalIcon as DOM siblings of
PhaserGame.
src/ui/index.ts — re-export ./journal.
src/game/scenes/Garden.ts — harvest/compost pointer flow:
- create() builds a SimContext from the eager `fragments` corpus filtered
to Season 1; passed to every simulateOneTick call (Phase 4+ should swap
to await loadSeasonFragments(currentSeason) when Season transitions land).
- handleTilePointerDown branches on tile state: empty → seed picker
event; ready plant → enqueue 'harvest' command; immature plant → enqueue
'compost' command (TODO Plan 02-04: render the Ink-authored compost
acknowledgement beat from compost-acknowledgements.ink).
- update() detects newly-appended harvestedFragmentIds and sets
fragmentRevealId so the reveal modal pops with the new fragment text.
- BLOCKER 3 invariant preserved — sim writes tickCount, never lastTickAt.
content/dialogue/season1/compost-acknowledgements.ink — authored content
for the GARD-04 + D-07 compost beat. 6 short lines in the gardener-keeper
voice (NOT Lura — she's the warmth anchor; the garden's voice is the
contrast). Plan 02-04 wires the inkjs runtime; Plan 02-03 ships the
content so the writer can iterate independently.
214/214 tests green (was 163; +51 new this plan); npm run lint exits 0;
npm run ci exits 0; npm run build exits 0 with the expected
INEFFECTIVE_DYNAMIC_IMPORT warnings (eager `fragments` export still
imports the Season-1 yaml/md statically alongside the lazy
loadSeasonFragments path — documented in 02-02-SUMMARY.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- eslint.config.js block 3: no-restricted-syntax bans Date.now() and
setInterval() inside src/sim/**, with src/sim/scheduler/clock.ts as
the single allowed wall-clock owner (CONTEXT D-33, RESEARCH Pitfall 1)
- date-now-violator.ts deliberate-violation fixture (excluded from
default lint by Block 1's top-level ignores; the programmatic ESLint
test passes ignore: false to override)
- lint-firewall.test.ts gains 2 new cases: positive (rule fires on
violator) + negative (rule does NOT fire on clock.ts the one exception)
- Existing CORE-10 firewall test left untouched and remains green
- Zustand 5 vanilla createStore composes 4 slices (garden / memory /
narrative / session); useAppStore React hook re-renders on selector
change; getState() works without React (Phaser ↔ React bridge per D-32)
- simAdapter exposes drainCommands / applyTilesAndUnlocks /
applyHarvestedFragments / applyLuraProgress / applyTickCount; sim
never imports the store (CORE-10)
- V1Payload extended in place per D-34: tickCount (BLOCKER 3 monotonic
counter), unlockedPlantTypes, luraBeatProgress, offlineEvents,
settings.persistenceToastShown — CURRENT_SCHEMA_VERSION stays 1, no
migrations[2] sneaked in (regression-defense test pins this)
- migrations[1] populates all new field defaults; tickCount: 0 means
fresh sims always start at sim-tick 0
- registerSaveLifecycleHooks (UX-10): visibilitychange→hidden,
beforeunload, plus saveOnSeasonTransition() — Vitest covers all three
- Phaser EventBus singleton seeded per the Phaser 4 React-template pattern
- Install @testing-library/react as devDep so the React-hook test can
exercise the real renderHook surface
- 27 new tests across store / migrations / lifecycle all green; full
npm run ci is 126/126
Decision-coverage gate found three CONTEXT.md decisions structurally
implemented but not literally cited by their D-NN tags. Added one-line
must_haves entries citing each:
- D-12 (Lura as discrete gate visits, 3 beats this Season) → 02-04
- D-16 (all Lura dialogue authored in Ink, runtime via inkjs) → 02-04
- D-32 (Zustand 5 store as the Phaser↔React bridge; sim never imports
store, CORE-10 enforced) → 02-01
STATE.md flipped from in_progress (context gathered) to ready_to_execute
with the planning summary in stopped_at.
All 24 REQ-IDs + 34 D-XX decisions now covered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BLOCKER 3 — cross-plan regression: Plans 02-03 and 02-05 BOTH re-author
src/sim/garden/commands.ts but had reverted simulateOneTick to the old
defective return shape (`return { ...next, lastTickAt: currentTick };`).
Wave 1's execution of 02-03 would overwrite 02-02's correct version,
breaking the invariant for the entire phase.
- 02-03: simulateOneTick return now matches 02-02 line 457 exactly:
`return { ...next, tickCount: next.tickCount + 1 };`
- 02-05: same fix for the silent-mode update (Step 6).
- 02-03 acceptance_criteria: add negative grep
(`! grep -E "lastTickAt:\s*(this|currentTick)" src/sim/garden/commands.ts`)
and positive grep (`grep -q "tickCount: next.tickCount" ...`).
- 02-05 acceptance_criteria: add the same two greps for commands.ts so
02-05's silent-mode edits cannot silently re-introduce the regression.
W1 — App.tsx import: 02-05 Step 11 used `useEffect` without importing it.
Combined `import { useState }` and `import { useRef }` into a single
`import { useState, useEffect, useRef } from 'react';` line.
W2 — helper arity divergence: Settings.tsx (one-arg, Date.now() inline)
and PhaserGame.tsx (two-arg, clock.now() injected) had two parallel
definitions of buildPayloadFromStore / hydrateStoreFromPayload. Fix:
- New Step 3.5 introduces `src/save/payload.ts` with the unified
two-arg signature: `buildPayloadFromStore(state, nowMs)` and
`hydrateStoreFromPayload(state, payload)`.
- `src/save/index.ts` re-exports both.
- Settings.tsx imports from save barrel; passes Date.now() at the
call site (no clock injection on hand).
- PhaserGame.tsx imports from save barrel; passes clock.now() (the
injected wallClock or FakeClock).
- Inline duplicate definitions in both files removed; replaced with
a comment pointing to the shared module.
- files_modified updated to include src/save/payload.ts.
- acceptance_criteria asserts: shared file exists, both helpers
exported, both consumers import from save barrel, no inline
duplicate definitions remain.
VALIDATION.md not updated — no `<automated>` verify command changed;
the new greps live inside `<acceptance_criteria>` (executor-checked
per task), and VALIDATION.md is not present in the phase dir.
All iteration-1 + iteration-2 fixes preserved; no regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- W1: 02-RESEARCH.md Open Questions section now flagged (RESOLVED) and each
Recommendation prefixed with RESOLVED + a pointer to the artifact that
codified the resolution.
- W8: 02-02 Plan example moves `import type { GrowthStage }` to the top of
commands.ts (alongside the other type-only imports) and drops the trailing
parenthetical apology — the executor doesn't need to fix anything.
Two distinct fields with strict separation:
- lastTickAt: wall-clock milliseconds. Written ONLY at saveSync time by
the application layer. The sim NEVER writes this field.
computeOfflineCatchup uses it as the wall-clock anchor.
- tickCount: monotonic sim-internal counter (one per simulate() call).
Used for STRY-10 narrative gating that must be immune to wall-clock
manipulation. The sim writes this field; the application layer reads
it via simAdapter.applyTickCount.
Changes:
02-01: SimState + V1Payload gain `tickCount: number`; migrations[1]
defaults to 0; GardenSlice exposes tickCount + lastTickAt + setters;
simAdapter exposes applyTickCount; tests assert the round-trip.
02-02: simulateOneTick increments next.tickCount + 1 (not lastTickAt:
currentTick); Garden scene's SimState snapshot reads lastTickAt
through from store and writes tickCount: this.currentTick locally;
acceptance_criteria forbids `lastTickAt: this.*` in the sim and scene.
02-05: buildPayloadFromStore now persists tickCount (from store);
hydrateStoreFromPayload restores it via state.setTickCount.
This unblocks the offline-catchup math: computeOfflineCatchup(payload.lastTickAt,
nowMs) now reliably reads wall-clock ms because the sim never overwrites it
with a tick counter.
- BLOCKER 1: PhaserGame.tsx boot path now runs unwrap(env) → migrate(raw, env.schemaVersion).
Casting unwrap(record.envelope) directly to V1Payload silently accepted any
future-shape payload as the current shape; only migrate() walks the schema
version chain.
- BLOCKER 2: Settings.tsx onImport now correctly orders importFromBase64 →
unwrap (CRC verify) → migrate. Previous code discarded migrate's result
and then read v1.payload as if unwrap returned an envelope rather than
the payload itself — runtime crash on every import.
- BLOCKER 3: documented the lastTickAt invariant as wall-clock milliseconds,
written ONLY at saveSync time (never by the sim). Added acceptance_criteria
greps proving (a) saveSync writes clock.now(), (b) Garden scene does not
overwrite lastTickAt with a tick counter, (c) sim/garden/Garden.ts (if it
exists; the Garden scene actually lives at src/game/scenes/Garden.ts)
contains no lastTickAt: this.* writes.
- W2: D-29 keyboard shortcut wired in App.tsx — comma toggles Settings,
'j' dispatches a window CustomEvent the JournalIcon picks up.
- W5: lifecycle handle now stored in useRef and detached in the OUTER
useLayoutEffect cleanup (the previous IIFE-internal return was a closure
return, never reaching React's effect cleanup contract).
- W6: warm-tagged pool depth raised to ≥9 (8th-harvest threshold + 1 buffer)
so a worst-case all-rosemary playthrough never exhausts. Total per-pool
targets: ≥9 warm, ≥3 contemplative, ≥3 heavy, plus the sentinel.
- W2: JournalIcon now listens for the 'tlg:toggle-journal' window event so
App.tsx can wire a 'j' hotkey without lifting open/close state into the
store. Hotkey is gated on the same revealed selector as the icon itself.
Per W9: invoking the compiler from inside ink-loader.test.ts's beforeAll
creates a filesystem race against other concurrent tests because the
script wipes src/content/compiled-ink/ at start. Compile is already part
of the npm run ci chain (via npm run build); the test should only verify
the artefact exists and fail loudly with a fix-it message otherwise.
- BLOCKER 4: inklecateBinary() now resolves node_modules/inklecate/bin/inklecate{,.exe};
the previous path (inklecate-windows/, inklecate-mac/, inklecate-linux/) does not
exist in the package — fallback verification of Assumption A6 would have masked
the wrapper-API failure.
- W3: removed lura-greeting-template.ink from files_modified (never authored).
- W4: added last_fragment_title to INK_VARIABLE_MAP (extracts first sentence of the
most-recently-harvested fragment) so the must_haves slot promise is fulfilled.
54 file targets classified (49 new, 5 modified) with 87% analog coverage.
Key patterns: V1Payload extension (not v1→v2 migration), per-layer
public barrel pattern, test colocation, Zustand vanilla store + Phaser
EventBus singleton as the dual sim↔React bridge, ESLint sim-purity rule
proposed as a defended option (not auto-locked, per minimum-viable bias).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nyquist VALIDATION.md scaffold for Phase 2. Defines test infrastructure
(Vitest + Playwright already wired by Phase 1), sampling rates (npm test
after each commit, npm run ci after each wave), Wave-0 dependency surface
(BigQty + scheduler + Zustand store + V1Payload extension), and three
manual-only verifications (AudioContext cross-browser, letter voice review,
cozy-pace playtest). The per-task verification map is intentionally empty —
the planner fills it during plan generation; nyquist_compliant flips to
true once it's complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5-plan MVP slice proposal across 3 waves: Wave 0 lands the three
deferred foundations (BigQty wrapper around break_eternity.js, Zustand
5 store wiring, tick scheduler / monotonic clock). Wave 1 ships two
parallel vertical slices (Begin+Plant+Grow, Harvest+Journal+Fragments).
Wave 2 ships the Lura gate-visit slice and the offline-letter slice
including Playwright PIPE-07 e2e. All 24 REQ-IDs addressed in the
coverage map; 10 architectural patterns enumerated; tick rate locked at
5Hz with 24h offline cap; AudioContext.resume() bootstrap pattern
documented for first-run + returning-player paths; V1Payload extension
shape locked per CONTEXT D-34 (no migrate_v1_to_v2 added). 10
assumptions logged, 8 are LOW risk; 2 are MEDIUM (canvas-DOM coord
mapping under FIT scale, inklecate Windows binary invocation) and
flagged for early verification in Plans 02-02 and 02-04.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 discuss-phase complete; STATE.md now reflects context-gathered
status, updated stopped_at narrative, and next-action pointer to
/gsd-plan-phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 (Season 1 Vertical Slice — Soil) discuss-phase output. 02-CONTEXT.md
captures 34 implementation decisions across 8 gray areas (garden geometry &
input · time density · Lura's arc · letter composition · Begin screen ·
Memory Journal · plant placeholders · Phase-2 Settings UI scope). Locks the
4×4 grid + click+inline seed picker, 2–5min growth band per plant with
auto-harvest-while-offline, 3 Lura gate visits gated by 1st/4th/8th harvest,
authored Ink letter skeleton with templated slots, full-screen letter on
≥5min absence, tasteful placeholder Begin screen (Phase 3 paints), full-
screen Journal modal revealing after first harvest, simple Phaser-primitive
plants with subtle ready-state pulse, save-management-only Settings.
Phase 2's first commits: BigQty wrapper, Zustand 5 store, tick scheduler.
Save schema is a v1 *extension*, not v1→v2.
02-DISCUSSION-LOG.md preserves the alternatives considered for human review.
Next: /gsd-plan-phase 2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User invoked planning-doctrine pushback principle on the 10-20 north-star
curation step. Two 1x1 transparent-PNG placeholders ship with provenance
sidecars marked model_id: 'placeholder' so the validator exercises at >0
assets. Full deferral rationale and resolution path in
.planning/phases/01-foundations-and-doctrine/01-05-IOU.md — to be revisited
at Phase 5 entry (curate then, or amend CONTEXT D-01 if still ceremonial).
- 01-07-ci-workflow-SUMMARY.md: structural enforcement map, Phase 2/8 handoff notes, threat T-01-08 mitigation confirmed
- 01-VALIDATION.md: per-task table populated (12/13 green, 01-05-T2 partial — checkpoint:human-verify awaiting north-star image curation); status flipped to executed
- ROADMAP.md: progress table marks Phase 1 as 7/7 with 01-05 partial annotation
- STATE.md: position advanced to Plan 7 of 7 complete; performance trend; Plan 01-05 Task 2 explicitly tracked as the only outstanding deliverable; next action = human curation pass then /gsd-verify-work
- REQUIREMENTS.md: PIPE-06 marked complete (CI workflow runs Vitest on every push/PR)
- Single-job workflow at .github/workflows/ci.yml (~49 lines including load-bearing comments)
- runs-on: ubuntu-latest, timeout-minutes: 10
- Uses actions/setup-node@v4 with cache: 'npm' (per RESEARCH CI Pitfall A — never cache node_modules/)
- Node 22 (per RESEARCH § Environment Availability)
- Triggers on push to main and pull_request to main
- Steps: checkout → setup-node → npm ci (lockfile-strict) → npm run ci (lint + test + validate-assets + build)
- Per CONTEXT user pushback: NO matrix, NO test reporters, NO Codecov, NO release automation
- Local npm run ci exits 0 (53 tests passing across 12 files); workflow will be green on push
- Structurally enforces every Phase 1 success criterion on every commit going forward
7 commits across 3 TDD tasks (RED + GREEN per task) + .gitkeep cleanup;
36 Vitest tests across 7 test files green; npm run build clean under
TypeScript strict; all 6 CORE requirements (CORE-04 through CORE-09)
covered by at least one assertion.
Key structural decision documented in SUMMARY: SaveDB is a single
common-contract interface, not a union of IDBPDatabase | LocalStorageDBAdapter.
The union shape failed TypeScript-strict at the build gate; the interface
refactor isolates the type-system cast to one location at openSaveDB().
src/save/ now contains 7 production files + 7 test files. The .gitkeep
firewall marker exists only to make empty directories trackable in git;
it can be retired once the directory has real content (per Plan 01-01
SUMMARY's pattern — 'firewall-as-directory pattern').
- codec.ts: exportToBase64 / importFromBase64 via lz-string with
MAX_IMPORT_BYTES=50MB DoS cap (T-01-02 in plan threat model); import
validates against SaveEnvelopeSchema before returning. lz-string sync
caveat documented per RESEARCH Pitfall 5 (Web Worker mitigation deferred
to Phase 8 per CONTEXT D-09)
- index.ts: 14 public re-exports — the only entry point Phase 2 should
import from. Includes the LocalStorageDBAdapter class so consumers can
type-check the fallback path explicitly if needed
[Rule 1 - Bug] Build was failing because the original SaveDB type was a
union (IDBPDatabase<SaveDBSchema> | LocalStorageDBAdapter) — TypeScript
cannot resolve method calls through a union when each branch has
differently-shaped overloads ('no compatible signature' on every db.put).
Fixed by:
- Defining SaveDB as a single common-contract interface that both
backends MUST satisfy (get/put/delete/getAll/transaction with
conditional-type RecordOf<S> return values)
- Hoisting the canonical SavedRecord/SnapshotRecord/StoreName types
into db-localstorage-adapter.ts (lower-level module) and re-exporting
them from db.ts to avoid a circular import
- Casting the idb-returned IDBPDatabase to SaveDB at the open-call
boundary (the casts are isolated to openSaveDB; Phase 2 only sees
the SaveDB interface)
- Promoting SnapshotEntry to a type-alias of SnapshotRecord so
snapshots.ts no longer redeclares the shape and can rely on
canonical types
Tests: 36/36 pass under 'npx vitest run src/save/' (full suite incl
sentinel: 37/37). 'npm run build' exits 0 under TypeScript strict.
'npm run lint' is not invoked here because Plan 02 (eslint-firewall) has
not landed yet — the lint script will fail until it does, by design per
the Plan 01-01 SUMMARY ('Plan 02 owns it').
SUMMARY documents the ESLint flat config, the 9 element types, the
single CORE-10 rule, the deliberate-violation fixture, the Vitest test
that runs ESLint programmatically against the violator, and the four
auto-fixed deviations (typescript-eslint parser-only integration,
real render target file for the violator import, eslint-import-resolver-
typescript wiring, tsconfig.app.json test-file exclusion).
Verifies: npm run lint -> 0 errors / 0 warnings; npm test -> 2/2 pass;
npm run build -> green; eslint --no-ignore on violator -> exits 1.
Self-Check: PASSED.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- round-trip.test.ts (3 tests): full pipeline EXPORT -> IMPORT -> MIGRATE
-> WRAP -> UNWRAP -> IDB PUT -> IDB GET exercising every save layer
file end-to-end (CORE-09 + CORE-04 + CORE-06 + CORE-07); plus DoS-cap
rejection at MAX_IMPORT_BYTES + 1; plus malformed-Base64 rejection
RED phase per TDD plan-level gate. Tests fail because codec.ts does not
exist yet.
- db.ts: openSaveDB() opens IndexedDB ('tlg-save', v1) with two object
stores (saves singleton + save_snapshots keyed); on openDB rejection
(private mode, blocked, quota exceeded) falls back to LocalStorageDBAdapter
per CORE-04 contract
- db-localstorage-adapter.ts: ~110-LoC adapter exposing the same minimal
get/put/delete/getAll/transaction surface as idb's IDBPDatabase, namespaced
under tlg.saves.<id> and tlg.save_snapshots.<id>; transaction() shim
proxies straight through (localStorage has no real transactions)
- snapshots.ts: snapshot(envelope) writes to save_snapshots and prunes to
RETAIN=3 newest by savedAt descending (CORE-08); listSnapshots() returns
newest-first; entropy suffix on snapshot IDs avoids same-ms collisions
- persist.ts: requestPersistence() returns {granted, apiAvailable} for all
4 navigator.storage scenarios per CORE-05 + RESEARCH Pitfall 2
Test infra fixes: snapshots.test.ts and db.test.ts cannot deleteDatabase
between tests because openSaveDB leaves an open connection that idb caches
(deleteDatabase blocks indefinitely). beforeEach instead clears store
contents directly. The fallback test calls vi.resetModules() BEFORE
vi.doMock('idb') so the freshly-imported db.ts picks up the rejecting
openDB stub, and re-imports LocalStorageDBAdapter from the same module
graph so instanceof checks against the same class identity.
Tests: 12/12 pass (npx vitest run src/save/db.test.ts
src/save/snapshots.test.ts src/save/persist.test.ts).
Full save suite: 33/33 pass (Task 1 + Task 2 combined).
TypeScript-strict; no 'any' in production code (CLAUDE.md).
- src/sim/__test_violation__/violator.ts deliberately imports from
src/render/__firewall_target__.ts to trigger the firewall rule.
- src/sim/__test_violation__/lint-firewall.test.ts runs ESLint
programmatically (with ignore: false) against the violator and
asserts boundaries/element-types fires with severity=error and the
message mentions both 'sim' and 'render'.
- src/render/__firewall_target__.ts is a minimal export so the
boundaries plugin can resolve the import to a real path on disk.
Without a real target, the plugin marks the import as isUnknown
and silently skips the rule (verified empirically; see SUMMARY).
- eslint.config.js gains an import/resolver: typescript block so the
TS-aware resolver follows extension-less imports
('../../render/foo' -> src/render/foo.ts). Required by the
boundaries plugin's element classification of import targets.
- tsconfig.app.json excludes *.test.ts and src/sim/__test_violation__/
so 'tsc -b' does not try to typecheck Node-API-using test code with
the DOM-only project's lib settings; vitest still discovers them
via its own include glob.
- Added eslint-import-resolver-typescript as devDep.
Verifies green:
npm run lint -> 0 errors, 0 warnings (violator excluded)
npm test -> 2/2 pass (sentinel + firewall)
npm run build -> tsc -b clean, vite build clean
npx eslint --no-ignore src/sim/__test_violation__/violator.ts
-> exits 1 with the expected
boundaries/element-types error
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the asset-provenance gate that landed (validator script, Zod sidecar
schema with the 6 CLAUDE.md fields + optional provenance_schema_version, refused-
sample PNG, tmpdir-isolated Vitest enforcement test) and the resume protocol for
Task 2 (Path A AI-generate / Path B hand-painted-or-licensed-photograph fallback /
Path C defer with explicit IOU). Per plan's autonomous: false flag and orchestrator
spawn instructions, the human curates the 10–20 north-star reference images.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- .planning/season-7-end-state.md answers principle-level the three questions
per CONTEXT D-08: (a) what rest state means, (b) what the finite Roothold
ceiling is tied to (count of authored fragments + Seasons), (c) the coda's
tonal register (warm/quiet/specific/final). Cites SEAS-04, SEAS-09, SEAS-10,
STRY-08; ROADMAP Phase 7; PITFALLS #1.
- Includes the explicit 'What this document is NOT' boundary section so
treatment-level scope creep is structurally rejected (binary-choice scene
text, ending paragraph text, Lura's final line, credits screen — all
authored Phase 7, not Phase 1).
- scripts/doctrine.test.ts is the only automated enforcement of both Phase-1
doctrine docs (per CONTEXT D-07: no UX-string lint rule). Asserts file
existence + required H2 sections + required source citations + boundary
disclaimer. 8 assertions / 2 doc files.
- vitest.config.ts include glob extended to scripts/**/*.test.ts so
doctrine.test.ts is discovered by 'npm test'.
- tsconfig.node.json include extended to scripts/**/*.ts so the strict-TS
gate covers the new doc-lint test alongside the existing build configs.
- 'npm test' green: 2 test files, 9 tests passing (sentinel + doctrine).
- Per CONTEXT D-09: lives in .planning/, not docs/.
Rule 3 [Blocking]: vitest.config.ts and tsconfig.node.json include globs
extended to discover the new TypeScript test file (existing globs only
covered .mjs scripts and src/ tests).
- db.test.ts (4 tests): IDB-primary path opens both stores + round-trips
saves and save_snapshots; localStorage-fallback path via vi.doMock('idb')
asserts LocalStorageDBAdapter is returned and tlg.saves.main is written
- snapshots.test.ts (4 tests): basic put + listSnapshots, empty store
returns [], CORE-08 5-then-3 retention with newest-first ordering, and
pruned entries are oldest by savedAt
- persist.test.ts (4 tests): all 4 navigator.storage scenarios per
CORE-05 + RESEARCH Pitfall 2 (granted true / false / throws / missing)
RED phase per TDD plan-level gate. Tests fail because db.ts / snapshots.ts /
persist.ts / db-localstorage-adapter.ts do not exist yet.
- 2 happy-path tests: empty globs, valid YAML round-trip
- 3 throw assertions covering the schema-violation matrix:
* numeric id (violates stable-string-ID rule)
* season out of [0,7] range
* Markdown frontmatter missing required id
- All 5 tests pass; full Phase-1 suite remains green
- Proves the throws that fail npm run build at module-eval time
- scripts/validate-assets.mjs: walks ASSETS_DIR (default 'assets'), requires every
non-sidecar non-.gitkeep non-README file to carry a sibling <name>.provenance.json
validating against Zod ProvenanceSchema (6 required fields per CLAUDE.md / AEST-08
+ optional provenance_schema_version per RESEARCH Open Question #2). Excludes
assets/__samples__/refused/ so the proof-of-gate fixture passes the gate.
- assets/__samples__/refused/no-provenance.png: 1x1 transparent PNG with no sidecar;
the gate-proof artifact per CONTEXT D-03.
- scripts/validate-assets.test.ts: Vitest integration test covering both cases.
Positive: real /assets/ tree must exit 0. Negative: per-test-run mkdtemp under
os.tmpdir() with one orphan PNG; runs validator with ASSETS_DIR pointing at the
tmpdir; asserts exit 1 + clear error message + cleanup in afterAll. No risk of
polluting the real /assets/ tree (BLOCKER 2 fix).
- vitest.config.ts: extend include glob to also pick up scripts/**/*.test.ts (Rule 3
blocking fix — without this the new test file is invisible to vitest).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>