clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / openspec / changes / archive / 2026-09-24-consolidate-level-schema-boundary / design.md
design.md 6.9K rendered
1## Context23The engine is the authoritative simulator, but its direct level validator and4the CommonJS validator currently perform overlapping work with different5limits and error shapes. The engine accepts a level without a pack identity and6accepts `U`/`D`/`L`/`R`/`W` automaton steps. `mods.js` requires stable pack level7IDs, has bounded pack limits, and currently expects object-shaped automaton8steps. `share.js` repeats normalization before invoking the mod validator.910The project already has stable rules and sharing contracts. This change makes11the data boundary explicit so gates, switches, FRAIL tiles, and future12automata metadata have one place to enter the host without forcing the13simulator to understand pack-service diagnostics.1415## Goals / Non-Goals1617**Goals:**1819- Establish a pure normalization result that does not mutate caller data,20 execute code, access the catalog, or depend on DOM state.21- Validate shared structural fields once, including coordinates, object kind,22 token casing, object IDs, automata IDs, pattern actions, and supported23 scalar metadata.24- Keep context policy explicit: standalone engine levels may omit pack-only25 identity fields, while pack validation still requires stable level IDs and26 applies pack count and size limits.27- Preserve actionable source paths and map one canonical issue code into the28 engine's thrown errors and loader/share diagnostics.29- Make canonical data deterministic for export, share encoding, catalog30 registration, solver input, and snapshot initialization.3132**Non-Goals:**3334- Adding gates, switches, FRAIL behavior, new rule vocabulary, or mechanic35 lifecycle state.36- Changing current movement, rule precedence, automaton ordering, or solver37 semantics.38- Sandboxing trusted local CommonJS or allowing remote package resolution.39- Removing the legacy shorthand in this change.4041## Decisions4243### One canonical normalization pass4445Add a data-only level boundary with an explicit profile for standalone engine46levels and pack levels. It clones input first, validates the shared structural47contract, then returns canonical data. Canonical objects always have an48explicit `kind`; word `text`, object `noun`, automaton `subject` and `property`49tokens are uppercase; and automaton patterns contain `{ action: 'move', dx,50dy }` or `{ action: 'wait' }` records. Optional metadata is preserved only51when it is part of the documented level schema.5253Pack-only rules such as a required lowercase level ID, pack level count, and54pack source limits remain profile checks around the shared boundary. They may55reject a canonical level, but they may not normalize it differently.5657### Legacy shorthand is input-only5859For the current compatibility window, the boundary accepts an omitted object60`kind` (inferred from `text`), case-insensitive word and noun tokens, and the61automaton strings `U`, `D`, `L`, `R`, and `W`. It materializes these into the62canonical explicit shape before any caller sees the result. Unknown strings,63mixed direction vectors, and unsupported action fields remain validation64errors. New authored content and CommonJS exports should use canonical object65steps; a later compatibility change may remove shorthand after a measured66warning period.6768### Adapters own presentation of errors6970The boundary reports stable issue codes, paths, and values. `createGame()` maps71one issue into its existing `TypeError` or `RangeError` wording with the level72label. The loader maps issues to `levels[i]...` strings and `ModValidationError`73without executing or registering invalid data. Shared decoding maps issues to74its existing `Invalid shared pack` error. These adapters may add filename or75pack context but may not reimplement validation.7677### Round trips prove the boundary7879Fixtures run the same authored level through direct engine creation, local80CommonJS loading, CommonJS export, shared encoding/decoding, and engine81creation from the decoded result. Tests compare canonical gameplay fields and82issue codes, not incidental message punctuation. They also prove source data83is not mutated and that malformed data fails before catalog installation.8485## Risks / Trade-offs8687- A stricter shared boundary could reject a fixture that only passed through a88 permissive path. → Inventory current fixtures, preserve documented legacy89 shorthands, and add adapters before deleting old checks.90- Different pack and standalone limits could look like a validation split. →91 Keep shared structural checks in one module and make profile limits explicit92 in the public contract and tests.93- Future mechanic metadata could reintroduce ad hoc fields. → Require each94 new metadata family to register its schema with the same boundary before95 entering engine state or sharing.9697## Migration Plan98991. Add the boundary module and fixtures without changing gameplay semantics.1002. Route direct engine, CommonJS, and share paths through it, keeping their101 public error adapters and pack-only checks.1023. Update canonical exports and shared payloads to emit explicit shapes.1034. Run focused parity tests, full Node tests, the production build, world104 solver, and browser import/share flows.1055. Record any remaining unowned metadata fields before adding a mechanic106 proposal that depends on them.107108## Open Questions109110- Should future legacy shorthand produce a nonblocking authoring warning111 before it is removed?112- Which profile should own limits for mechanic metadata once gates and113 one-step tiles become authored content?114115## Boundary inventory and implementation evidence116117| Input | Authored fields and policy | Adapter |118| --- | --- | --- |119| Direct engine | Board size, objects, optional ID/title/hint/lesson metadata and automata; no pack ID or pack size cap | `createGame()` uses the standalone profile and preserves issue code/path on level-labeled errors. |120| CommonJS pack | The same level structure, plus required stable local level ID, 128×128 board, 4096 objects, 32 automata, 64-character title, and 1–64 levels per pack | `loadMod()` retains `ModValidationError` and source context; normalized levels contain explicit kinds/actions. |121| Data-only share/export | The pack profile plus the existing payload and schema revision limits | `encodeSharedPack()`, `decodeSharedPack()`, and `exportCommonJS()` revalidate canonical data without executing shared code. |122| Catalog and Gift views | Catalog identity, origin, palette, and preview fields are host metadata | `playableLevelFromView()` removes known view fields before `createGame()`; unknown authored fields still fail schema validation. |123| Solver | Level gameplay data and authoritative object/automaton state | `checkLevel()` uses the normalized engine path; no presentation or catalog metadata enters the state key. |124125`mechanics`, object `channel`, and object `leaves` remain intentionally126unsupported until the declarative mechanic-state, gate, and FRAIL changes add127their schema and state contracts. The boundary reports them as128`unsupported-field` instead of losing them during normalization.