1# Runtime architecture review232026-09-23. Scope: browser runtime, deterministic engine, mod/share data, test and authoring boundaries. This review records the baseline and the migration contract for [the archived runtime-boundaries OpenSpec change](../openspec/changes/archive/2026-09-24-modularize-runtime-boundaries/proposal.md). The visual design and world files have separate owners.45## Baseline67The project has useful seams already: `createGame()` does not import DOM or canvas; mod data is validated before registration; sharing carries data rather than CommonJS source; engine snapshots support Undo and the solver; `render_game_to_text()` exposes playable state to browser tests. At review start, 170 Node tests and the production build passed.89The source tree's file sizes did not match its responsibility boundaries:1011| File | Baseline lines | Responsibilities combined | Risk |12| --- | ---: | --- | --- |13| `src/stage.js` | 1,526 | Scene state and transitions, all scene drawing, layout, keyboard/pointer/intent routing, DOM overlays | High |14| `src/render/board.js` | 660 | Geometry, words, entities, sentence bars, facing, effects, loss state | High |15| `src/juice.js` | 572 | Event interpretation, tweens, facing/Undo history, rule and automata effects | High |16| `src/engine.js` | 427 | Validation, parsing, movement, automata, contacts, snapshots, event assembly | High |17| `src/main.js` | 278 | Bootstrap, catalog and packs, DOM view, game lifecycle, second developer renderer, audio | High despite line count |18| `src/scenes/map.js` | 335 | Progress storage, map layout, navigation | Medium |19| `src/mods.js` | 275 | CommonJS evaluation, validation, normalization | Medium, security-sensitive |20| `src/input.js` | 258 | Keyboard, repeat, swipe, gamepad, listener lifecycle | Medium |21| `src/style.css` | 1 minified line | Most base application styling | Medium for maintainability |2223Raw line count is a signal, not a target. `main.js` demonstrates the sharper problem: dense one-line lifecycle handlers hide branches and make changes difficult to review. Large files should be split at ownership seams rather than sliced into arbitrary line ranges.2425## Intended boundaries2627```mermaid28flowchart LR29 Input[Keyboard touch gamepad] --> Stage[Stage action router]30 Stage --> App[Application session]31 App --> Engine[Deterministic engine]32 Engine --> State[Settled state and events]33 State --> App34 App --> View[Stage projection]35 View --> Render[Canvas renderers and animation]36 Packs[Trusted local CommonJS or shared data] --> Validate[Data validation and normalization]37 Validate --> App38 State --> Solver[Replay and solver state key]39```4041The session owns the game instance and authoritative state. A stage view is a projection of that state plus animation frames. A renderer never changes gameplay state. Named actions cross from input to app once. Mod loading and sharing feed normalized data to the same catalog boundary. The engine has no dependency on application, rendering, audio, or browser APIs.4243## Findings and concrete changes4445### 1. Duplicate stage action paths4647`stage.js` has overlapping keyboard, pointer, and `handleIntent` branches. Pause and Index behavior appears in multiple branches; the Index also has both a capture listener and document listener. This is the most likely source of one platform missing a new action or accepting it twice. Introduce one scene action dispatcher; have device adapters decode to named actions. Let renderers return named hit regions as the pointer contract. Verify a key, a tap, and a programmatic intent each cause exactly one equivalent action.4849### 2. Mirrored and ambiguous runtime state5051`main.js` owns `game`, while `stage.js` also stores board state, game handle, palette, glyphs, and render frame. `setBoard()` is an implicit synchronization point. Keep the engine snapshot authoritative and make the stage projection explicitly read-only. `juice.js` owns visual history, including facing, that can diverge during Undo or scene transitions; future mechanics must not add hidden state there. Refactor command lifecycle so move, wait, Undo, restart, and next level use the same projection refresh contract.5253### 3. Engine complexity and cost5455Rule parsing, push chains, automata, transformation, contacts, and settled event construction share one closure. Movement repeatedly filters the object array or finds IDs, and property queries scan rules repeatedly. Current tutorial boards are small, but larger mods and several automata will multiply these scans. Add turn-local maps for IDs, cells, and subject properties; rebuild at explicit mutation phases. Preserve the existing player → parse/transform → automata → parse/transform → contacts behavior and event ordering. A later change should split turn phases into named modules and specify simultaneous automata conflicts before changing their semantics.5657### 4. Data and validation drift5859Catalog, pack library, pack service, share, Gift, and Workshop repeat JSON clone helpers. A shared data-only clone helper should make ownership and mutability clear. Engine and mod loader also validate overlapping automata fields differently; that schema should be consolidated in a separate change before additional mechanics fields are accepted. Trusted local CommonJS executes via `new Function`; shared links must stay data-only. Any future community/remote executable modules need an explicit isolation model rather than reusing this trusted-local path.6061### 5. Scaling contracts for future mechanics6263Gates, switches, and one-step tiles introduce persistent state and turn-order questions. Every stateful mechanic must define: normalized authored data, collision/contact phase, deterministic conflict order, settled events, snapshot/restore representation, solver key, replay behavior, and Undo/reset behavior. The existing solver stores full snapshots in BFS nodes and keys only objects plus automata phase; its state codec will need to include any new mechanic counters or gate states. Do not add a mechanics field only to rendering or mod data.6465## Sequence and acceptance gates66671. **Current refactor:** stage action dispatch, app session seam, engine lookup seam, shared data clone semantics, focused tests, and this architecture contract. Preserve public APIs and current level behavior.682. **Presentation follow-up:** extract stage scene renderers and layout/hit geometry; split board passes and `juice` event interpretation from animation timelines. Keep designer-owned visual behavior and mobile layout verified.693. **Engine follow-up:** version a mechanics snapshot envelope, centralize pack/engine validation, define automata collision policy, and introduce an effect interface for switches/gates/crumble. Extend solver state codec before shipping authored examples.704. **Project hygiene:** format and split minified CSS as a mechanical commit, separate map progress storage from drawing, add a supported solver command and CI authoring gate when the world-authoring OpenSpec change lands, and watch bundle size and import cycles.7172Acceptance gates for each behavior-facing step: Node fixtures remain green; production build succeeds; desktop and mobile browser flows cover navigation, move/wait, rule changes, Undo, and imported CommonJS/URL data; screenshots and `render_game_to_text()` agree; no new console errors. Performance work should measure representative larger boards rather than assert timing from one device. No feature is complete if Undo, restore, replay, or solver identity omits its state.7374## Implemented in this pass7576| Boundary | Result |77| --- | --- |78| Stage | `stage.js` fell from 1,526 to 648 lines. HUD, Proof, Pause, and Win drawing live in focused render modules. Overlay keyboard, pointer, and programmatic actions share one dispatcher; pause actions have stable IDs separate from labels. |79| App | Turn lifecycle, scene intent routing, and the developer canvas view moved out of `main.js`. Its line count is similar because the controller wiring is explicit; the dense gameplay and drawing branches are gone. |80| Engine | Pure rule parsing and turn-local ID/cell/property indexes moved out of `engine.js`. An Undo browser regression exposed a stale index after restoring history; Undo now rebuilds the indexes. |81| Presentation | `board.js` fell from 660 to 432 lines with a cached render model and entity pass. `juice.js` delegates visual tween projection and effect construction to pure helpers. |82| Data and structure | Pack/share cloning has one data boundary. INDEX progress persistence is separate from map layout. Base and stage CSS are formatted for review. |8384The final architecture pass passed 183 Node tests, the production build, and browser flows for the built-in chapter, imported CommonJS packs, data-only sharing, Gift/Workshop, Win/next proof, mobile controls, automata WAIT, and death followed by Undo. The first browser run caught a missing stage renderer import; the second caught the stale engine index after Undo. Both were corrected and rerun. Screenshots of Proof, Win, mobile Gift, mobile automata, and restored MIAU were visually inspected, with no console errors in the final runs.8586Remaining work is specific rather than a reason to keep expanding this refactor: stage pointer targets still use viewport fractions on mobile; `juice.consume()` remains a large event reducer; mod and engine automata validation overlap; the solver stores full BFS snapshots and needs a mechanics-aware state codec; automata conflict ordering is sequential. Those should be handled in separate changes with authored scenarios and regression fixtures before gates, switches, or crumble tiles join the default campaign.8788The worlds team's inbox supplied concrete next gates. **W8 was independently reproduced and fixed** in `c3537ca`: SINK could remove the `STOP` word while settled rules still reported `ROCK IS STOP`, blocking the next move. The rule refresh change now reparses after contact removals and passed the exact reproduction, reset/Undo/restore fixtures, all seven Tidewater solver paths, and browser regression flows. Tidewater remains with the world team for an independent recheck before bundling. W11 unknown-predicate validation is next in the agreed content order, followed by W6 precedence and AND parsing and W3 solver throughput. Those require separate specs and differential fixtures. The `createGame()` API and snapshot shape remain compatible so the changes can land one at a time.8990### W6 scalability follow-up9192W11 proof-local diagnostics and feature validation are implemented locally, with Gift and Workshop presentation awaiting designer review before the next public revision. W6 now has a strict OpenSpec contract for stacked-cell reading and contact/transform precedence. The parser enumerates every valid one-token-per-cell sentence path in spatial order. It has explicit path and span budgets and an indexed maximal-span pass; an over-budget board fails with a typed error instead of silently dropping rules. The CommonJS loader rejects an over-budget initial board with a structured proof error. During play, rule parsing is part of the turn transaction: an over-budget arrangement rejects the move and restores board, history, move count, indexes, and automata phase. Restore also preserves the prior settled game when a supplied snapshot exceeds the budget.9394This closes a new combinatorial risk introduced by stacked alternatives. It does not replace W3's solver state codec: the current breadth-first solver still stores full snapshots and hit the 300,000-state check limit on two Crossword drafts after W6. The worlds team is rechecking those proofs and the Word Lab lesson whose old STOP puzzle now has a direct PUSH route.9596## Review boundary9798The concurrent designer controls `docs/design.md` and `docs/design.html`; the world team controls `docs/worlds/`, `mods/worlds/`, and its OpenSpec change. This review does not rewrite those files. Design or level-authoring consequences should be sent through the existing inbox files. The current architecture work retains the public nzip URL and existing mod format.99100## Current follow-through1011022026-09-23. The initial refactor is complete. The active work now has separate OpenSpec tracks, so engine semantics, content, and presentation can be accepted independently:1031041. **D22 motion:** the designer accepted Pause, Win, keycap press, clipped odometer, ledger stamp, and pushed-word skid. Title, INDEX, Gift, and Workshop now have scene entry/exit motion. The public release remains gated on the full browser scene/share flow and the designer's two captured P1 regressions: default-player chrome and the DEFEAT ledger color. `src/stage/events.js` owns DOM input wiring outside `stage.js`; input stays blocked during menu exit while a later external import can retarget its destination.1052. **W3 solver and W2 world handoff:** the compact, fast solver includes object nouns and automata phases in its versioned state key and reuses authoritative turn phases. W3 is archived after a 23/23 sequential gate. Core proof 02 now teaches removing `ROCK IS PUSH` to clear a jam, verified by exact solver and browser replay. The worlds team subsequently reworked Tidewater `shell-game`; its 11-step solution and 623,612-state forbidden-rule check pass, and the complete 23-proof gate is being repeated on that revision.1063. **W6 content convergence:** rule precedence and stacked parsing are implemented and tested. The worlds team owns the Crossword rework. An authoring `mustForm` claim requires an UNSOLVED forbidden-rule search; a `LIMIT` alone is inconclusive.1074. **Mechanic stressors:** review the worlds team's channel-linked gate/plate and step-off FRAIL drafts as data contracts first. For each mechanic, require authored validation, phase order, complete snapshot/restore, compact state-key treatment, replay/Undo, settled events, and browser presentation before shipping a default proof. The designer chose a memoryless gate: after contacts, its open state derives from current plate occupancy or another thing occupying its cell, so the solver can derive it from object state rather than storing a hidden jam bit. A FRAIL leftover is represented by a noun change; a level-scoped map selects the next noun from the object's current noun. Confirm these formulas with differential scenarios and record final choices in focused OpenSpec changes before implementation.108109The next structural cleanup should stay bounded: unify rendered hit-region geometry beyond Pause and Win, split the remaining `juice.consume()` event reducer by effect family, and consolidate overlapping mod/engine validation. Each slice needs a behavioral fixture tied to an observed risk; file length alone is not a success metric.110111### Next mechanic delivery sequence112113The three active OpenSpec changes (`add-declarative-mechanic-state-contract`, `add-channel-linked-gates`, and `add-frail-leftover-tiles`) form one ordered delivery path after the public motion release:1141151. Add a validated, versioned mechanic declaration and lifecycle adapter outside `engine.js`. Preserve it through CommonJS import/export, data-only links, catalog cloning, Undo, reset, restore, and complexity rollback. Reject unsupported declarations before installation.1162. Register mechanic state with the compact solver codec before adding a concrete mechanic. Two boards with identical entities but different mechanic scalars must get different keys, while no-mechanic levels keep their current routes.1173. Implement channel plates/gates through shared movement blocker and after-contact projection hooks. Keep openness derived from the current board rather than a hidden bit; compare public and fast transitions, then replay the Crate Works gate scenarios.1184. Implement FRAIL departures and the level-scoped noun map through the same lifecycle. Its future is carried by entity noun, existence, position, and the static map. Check handovers, word/automaton departures, SINK contacts, Undo, share, and solver replay against Tidewater fixtures.119120Gate and FRAIL presentation and default-world bundling follow differential engine fixtures and designer review. This order keeps mechanic-specific branches out of the turn coordinator and preserves the single authoritative transition path.121122### World integration and state boundary update1231242026-09-24. The first 23-proof gate and the revised seven-proof Crossword check passed on settled W6/W8 semantics. The authored world packs and bibles are tracked in Git, and `manifest.rules: 2` now distinguishes the current rule semantics from legacy imported packs. D20 changes the runtime catalog shape: each pack owns a world and its own INDEX sheets, with `packId/levelId` progress and registration order as stable identity. The map model now represents these chapters; input, cards, and public bundling remain separate acceptance slices. Clockwork stays outside the default bundle until W12 custom-property presentation is implemented and reviewed.125126The mechanic foundation was narrowed after the designer chose memoryless gates and stable-ID FRAIL leftovers. These are **derived** mechanics: gate openness, jams, and pressed plates recompute from settled objects and current nouns; FRAIL existence and noun live in `state.objects`, while `leaves` is static authored metadata. Persisting extra `open`, `jammed`, or `crumbled` bits would duplicate board truth and could diverge across Undo, restore, and solver replay. A host-owned lifecycle adapter still provides phase order, validation, and settled events. Only a future mechanic with real memory needs `state.mechanics` scalars and explicit compact-codec registration. The three active mechanic OpenSpec changes now state this split and require public/fast and restore parity without hidden bits.127128The next integration gates are concrete: complete D20 world input/card behavior, validate optional world identity as a nonblocking note with a safe fallback, then bundle core, Crossword, Crate Works, and Tidewater in the approved order. Run the sequential world solver, full Node/build suite, and desktop/mobile Gift→INDEX→Proof→Win browser flow before updating the public alias. The older Garden Basics and Word Lab packs remain importable examples; Word Lab's W6 lesson copy is being corrected so its direct PUSH route matches its hint.129130### Post-D20 architecture status1311322026-09-24. D20 passed designer review and is archived as133`2026-09-24-add-world-index-presentation`. Core, Crossword, Crate Works, and134Tidewater are playable in that order at public revision 13. The complete135sequential world gate solved 30 proofs, including the six Clockwork proofs that136remain outside the default INDEX. The full 373-test Node suite, production137build, local Gift/Workshop/share flow, and public desktop/mobile D20 flow138passed without page errors.139140The active proof now uses `${packId}:${localId}` rather than a retained array141position (`50e4d68`). The catalog keeps a direct ID map and invalidates142derived pack summaries on mutation. In a 64-level local comparison, 2,000143last-level reads took 16.55 ms with the direct lookup and 1,079.7 ms via the144old full-list clone and scan. Delayed Win behavior during pack replacement and145removal remains a focused OpenSpec regression gate; its normal browser paths146already pass.147148The next engine seam is canonical level validation, now specified in149`consolidate-level-schema-boundary` (`73084e6`). The direct engine and150CommonJS loader currently disagree on automaton shorthand, phase bounds, ID151casing, cue emptiness, and sound names. A single pure normalizer should feed152direct games, trusted modules, and data-only shares, with caller-specific153error adapters and parity fixtures. That work precedes new gates and FRAIL154runtime behavior so their authored fields do not multiply validator drift.155156### Shared schema boundary and next mechanic seam1571582026-09-24. `2950532` puts standalone engine levels, CommonJS packs, and159data-only shared packs through `src/level-schema.js`. It canonicalizes object160kinds, tokens, and automaton steps while retaining pack-only identity/size161limits as a profile. Direct, module, and shared errors carry the same structural162issue code/path in caller-specific envelopes. `4a8d371` rejects conflicting163object fields and falsey explicit kinds so normalization cannot silently drop164authored data. Catalog and Gift preview metadata cross a separate165`playableLevelFromView()` adapter before game creation. The full 388-test Node166suite, production build, D20 world-index browser flow, and Gift/Workshop/share167browser flow pass without page errors. The sequential world solver also solved168all 30 Core, Crossword, Crate Works, Tidewater, and Clockwork proofs with no169issues or warnings.170171The next bounded implementation is the accepted declarative mechanic172foundation. Its registry must distinguish derived projections from truly173stateful scalar data. Gate `pressed`, `open`, and `jammed` derive from current174occupancy and nouns; FRAIL crumble derives from authoritative object movement,175existence, noun, and a static level-scoped current-noun map. Neither may add a176hidden solver bit. A stateful future mechanic must register every dynamic177scalar in the codec and survive accepted/rejected turns, Undo, reset, restore,178and public/fast parity. The level schema rejects undeclared `mechanics` and179object-level `leaves`; it will admit host-supported declarations and gate180`channel` metadata through the mechanic registry. The worlds team's six181data-only gate and FRAIL fixtures are committed for differential replay.