- save.test.ts: add regression test proving loop actions do NOT start
or yield during offline catch-up (applyOfflineProgress replays
tickGame directly; maybeStartLoopAction is never called, so an
enabled loop with no active action stays idle and yields 0).
- purity.test.ts: extend FORBIDDEN list to catch environment APIs
(Date.now(), localStorage., indexedDB., idb-keyval import, document.,
window., requestAnimationFrame()) in addition to the existing
React/react-dom/zustand import guards. Patterns are scoped to call
sites and member-access forms so prose comments (e.g. save.ts's
"Date scheduling" doc comment) do not false-positive. lz-string is
intentionally omitted — it is a pure compression library.
- NavRail.tsx: add sr-only "New story" span alongside the aria-hidden
pulse dot so screen readers can perceive the unread-story badge.
Finding 1 (critical): atBootIntro was derived from tree.length === 0 in
StoryView.tsx, but buildStoryTree always returns all topology-root nodes,
so tree.length is always > 0 and the Continue button never rendered.
Fix: add atBootIntro: boolean to StoryView (viewModel), computed by
finding the boot-trigger node id without hardcoding the literal string,
then comparing to the current node. StoryView.tsx reads it from the store.
Also add a viewModel test covering both true and false cases.
Finding 2: onSelect in StoryView.tsx called useGameStore.getState()
directly, bypassing the runtime command layer. Fix: add
GameRuntime.selectStoryNode(id) and wire onSelect through gameRuntime.
Finding 3: aria-pressed on story tree nodes wrongly signals toggle-button
semantics. Fix: replace with aria-current={isSelected ? 'true' : undefined}.
Optional: hoist h-[calc(100dvh-6rem)] to SHELL_HEIGHT const in StoryView.tsx.
- Guard ActionCard click-outside listener behind open state
- Add aria-expanded to ActionGroup collapse toggle
- Use optional chaining and drop non-null assertions in column projection