clone

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

Specify one level schema boundary across engine and mods

Dami committed 73084e6
commit
73084e69c56278b98438dc2121b53694da45d305
parent
7b6e2c92a8bffb82f73f455cf4cb036db746e375
author
Dami <dami.etoile@gmail.com>
4 files changed, +311 -0
Aopenspec/changes/consolidate-level-schema-boundary/design.md113+++++++++++++++++++++++++++
Aopenspec/changes/consolidate-level-schema-boundary/proposal.md42++++++++++
Aopenspec/changes/consolidate-level-schema-boundary/specs/level-schema-boundary/spec.md124++++++++++++++++++++++++++++++
Aopenspec/changes/consolidate-level-schema-boundary/tasks.md32++++++++
A openspec/changes/consolidate-level-schema-boundary/design.md +113 -0
@@ -0,0 +1,113 @@
1+## Context2+3+The engine is the authoritative simulator, but its direct level validator and4+the CommonJS validator currently perform overlapping work with different5+limits and error shapes. The engine accepts a level without a pack identity and6+accepts `U`/`D`/`L`/`R`/`W` automaton steps. `mods.js` requires stable pack level7+IDs, has bounded pack limits, and currently expects object-shaped automaton8+steps. `share.js` repeats normalization before invoking the mod validator.9+10+The project already has stable rules and sharing contracts. This change makes11+the data boundary explicit so gates, switches, FRAIL tiles, and future12+automata metadata have one place to enter the host without forcing the13+simulator to understand pack-service diagnostics.14+15+## Goals / Non-Goals16+17+**Goals:**18+19+- 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.31+32+**Non-Goals:**33+34+- 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.40+41+## Decisions42+43+### One canonical normalization pass44+45+Add a data-only level boundary with an explicit profile for standalone engine46+levels and pack levels. It clones input first, validates the shared structural47+contract, then returns canonical data. Canonical objects always have an48+explicit `kind`; word `text`, object `noun`, automaton `subject` and `property`49+tokens are uppercase; and automaton patterns contain `{ action: 'move', dx,50+dy }` or `{ action: 'wait' }` records. Optional metadata is preserved only51+when it is part of the documented level schema.52+53+Pack-only rules such as a required lowercase level ID, pack level count, and54+pack source limits remain profile checks around the shared boundary. They may55+reject a canonical level, but they may not normalize it differently.56+57+### Legacy shorthand is input-only58+59+For the current compatibility window, the boundary accepts an omitted object60+`kind` (inferred from `text`), case-insensitive word and noun tokens, and the61+automaton strings `U`, `D`, `L`, `R`, and `W`. It materializes these into the62+canonical explicit shape before any caller sees the result. Unknown strings,63+mixed direction vectors, and unsupported action fields remain validation64+errors. New authored content and CommonJS exports should use canonical object65+steps; a later compatibility change may remove shorthand after a measured66+warning period.67+68+### Adapters own presentation of errors69+70+The boundary reports stable issue codes, paths, and values. `createGame()` maps71+one issue into its existing `TypeError` or `RangeError` wording with the level72+label. The loader maps issues to `levels[i]...` strings and `ModValidationError`73+without executing or registering invalid data. Shared decoding maps issues to74+its existing `Invalid shared pack` error. These adapters may add filename or75+pack context but may not reimplement validation.76+77+### Round trips prove the boundary78+79+Fixtures run the same authored level through direct engine creation, local80+CommonJS loading, CommonJS export, shared encoding/decoding, and engine81+creation from the decoded result. Tests compare canonical gameplay fields and82+issue codes, not incidental message punctuation. They also prove source data83+is not mutated and that malformed data fails before catalog installation.84+85+## Risks / Trade-offs86+87+- 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.96+97+## Migration Plan98+99+1. Add the boundary module and fixtures without changing gameplay semantics.100+2. Route direct engine, CommonJS, and share paths through it, keeping their101+ public error adapters and pack-only checks.102+3. Update canonical exports and shared payloads to emit explicit shapes.103+4. Run focused parity tests, full Node tests, the production build, world104+ solver, and browser import/share flows.105+5. Record any remaining unowned metadata fields before adding a mechanic106+ proposal that depends on them.107+108+## Open Questions109+110+- 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?
A openspec/changes/consolidate-level-schema-boundary/proposal.md +42 -0
@@ -0,0 +1,42 @@
1+## Why2+3+Level data currently crosses several validation boundaries with slightly4+different rules. `createGame()` infers object kinds and accepts legacy5+automaton direction strings, while the CommonJS loader validates a related6+shape and normalizes it later. Data-only sharing has another copy of the same7+normalization. This makes a level valid through one path but invalid through8+another, and makes future mechanics expensive to add safely.9+10+## What Changes11+12+- Define one pure, host-owned level normalization and validation boundary for13+ authored level data.14+- Make direct engine creation, trusted CommonJS loading, and data-only sharing15+ use that boundary before their own context-specific error adapters.16+- Preserve the current legacy level shorthand during a documented migration:17+ inferred `kind`, case-insensitive word tokens, and `U`/`D`/`L`/`R`/`W`18+ automaton steps are accepted at input and materialized as canonical data.19+- Add parity fixtures proving valid data, invalid data, legacy normalization,20+ and CommonJS/share round trips agree before play or installation.21+22+## Capabilities23+24+### New Capabilities25+26+- `level-schema-boundary`: canonical level normalization, validation, and27+ adapters for engine, CommonJS, and data-only sharing.28+29+### Modified Capabilities30+31+- `commonjs-mods`: use the canonical level boundary instead of maintaining a32+ second level validator.33+- `level-sharing`: validate and normalize shared levels through the same34+ boundary used by local CommonJS previews and direct engine creation.35+36+## Impact37+38+The implementation will add a small data/schema module and reduce duplicated39+level checks in `src/engine.js`, `src/mods.js`, and `src/share.js`. It will add40+focused tests for direct creation, local module loading, shared payloads, and41+export/import. Existing normalized pack fields, level IDs, rules semantics,42+and trusted-local CommonJS execution remain unchanged.
A openspec/changes/consolidate-level-schema-boundary/specs/level-schema-boundary/spec.md +124 -0
@@ -0,0 +1,124 @@
1+## ADDED Requirements2+3+### Requirement: Level inputs SHALL cross one canonical normalization boundary4+5+The host SHALL expose one pure level normalization and validation boundary for6+standalone engine levels, trusted CommonJS levels, and data-only shared levels.7+The boundary SHALL clone caller data before normalization, SHALL not execute8+code or access presentation state, and SHALL return canonical level data with9+explicit object kinds, uppercase word and noun tokens, and explicit automaton10+action objects. A context profile MAY add pack identity and size limits, but it11+SHALL reuse the same structural checks and canonical output.12+13+#### Scenario: Direct and pack paths produce equivalent canonical data14+15+- **WHEN** the same valid level is passed to direct engine creation and to a16+ CommonJS pack preview17+- **THEN** both paths use equivalent canonical gameplay fields, including18+ inferred kinds, token casing, IDs, and automaton actions19+20+#### Scenario: Input is not mutated21+22+- **WHEN** a caller supplies lowercase tokens, omitted kinds, or shorthand23+ automaton steps and then mutates the original input after normalization24+- **THEN** the canonical level and the active game remain unchanged and the25+ original input retains its original values26+27+### Requirement: Legacy level shorthand SHALL be normalized at input28+29+During the compatibility window, the boundary SHALL accept omitted object30+`kind` when `text` or `noun` makes the kind unambiguous, case-insensitive word31+and noun tokens, and automaton pattern strings `U`, `D`, `L`, `R`, and `W`.32+Canonical output SHALL materialize these as `kind: 'word'` or `kind: 'object'`,33+uppercase tokens, and `{ action: 'move', dx, dy }` or `{ action: 'wait' }`.34+Unknown shorthand, non-cardinal vectors, and unsupported action fields SHALL35+remain errors. The boundary SHALL not emit new shorthand in CommonJS exports36+or shared payloads.37+38+#### Scenario: Legacy automaton shorthand survives a share round trip39+40+- **WHEN** a valid level uses `['L', 'W', 'R']` in an automaton pattern and is41+ exported to CommonJS or encoded as a shared pack42+- **THEN** loading or decoding succeeds, the normalized pattern contains three43+ explicit action objects, and a game created from the result has the same44+ next transitions as the source game45+46+#### Scenario: Ambiguous shorthand fails consistently47+48+- **WHEN** a level contains an unsupported pattern string or a diagonal49+ `{ action: 'move', dx: 1, dy: 1 }`50+- **THEN** direct engine creation, CommonJS preview, and shared decoding all51+ reject the same structural issue before play or installation52+53+### Requirement: Pack-only policy SHALL be an explicit adapter54+55+The pack profile SHALL require a stable lowercase level ID, enforce the56+documented pack-level and pack-count bounds, and retain duplicate-ID checks.57+The standalone engine profile MAY omit pack-only identity fields and SHALL not58+silently inherit pack limits. Pack policy errors SHALL preserve the canonical59+field path and SHALL occur before catalog registration.60+61+#### Scenario: A standalone fixture remains playable without a pack ID62+63+- **WHEN** a valid direct engine fixture has width, height, and objects but no64+ `id`65+- **THEN** direct game creation succeeds with canonical gameplay data66+67+#### Scenario: Pack identity is required before installation68+69+- **WHEN** a CommonJS pack contains a level without a stable lowercase `id`70+- **THEN** preview reports the level path, rejects the pack atomically, and71+ leaves existing catalog content unchanged72+73+### Requirement: Error adapters SHALL preserve canonical issue identity74+75+The shared boundary SHALL report stable issue codes and source paths. The76+engine adapter SHALL retain its level-labeled `TypeError`/`RangeError` style;77+the CommonJS adapter SHALL retain `ModValidationError` paths and source78+context; and the shared decoder SHALL retain its player-readable invalid-pack79+error. Adapters MAY add filename, pack, or level labels but SHALL not duplicate80+structural validation or alter issue severity.81+82+#### Scenario: One malformed object has equivalent diagnostics83+84+- **WHEN** `objects[0]` has an invalid coordinate type through all three input85+ paths86+- **THEN** each path reports the same structural issue code and object index,87+ while retaining its existing caller-specific error envelope88+89+### Requirement: Canonical data SHALL round-trip through sharing and export90+91+CommonJS export and data-only shared encoding SHALL serialize canonical level92+data only. Decoding or loading SHALL revalidate through the boundary before93+returning normalized data. A valid canonical level SHALL preserve gameplay94+fields through CommonJS export/import and share encode/decode, including95+automata metadata and optional authored level metadata.96+97+#### Scenario: Export/share parity preserves gameplay98+99+- **WHEN** a pack with words, objects, automata, and level metadata is exported100+ and shared, then imported through both supported paths101+- **THEN** both results compare equal on canonical gameplay data and create102+ games with matching initial snapshots and first accepted transitions103+104+#### Scenario: Invalid shared data cannot bypass the boundary105+106+- **WHEN** a shared payload contains malformed level data or unsupported107+ metadata108+- **THEN** decoding fails before catalog installation and does not execute109+ CommonJS or leave partial content registered110+111+### Requirement: Boundary parity SHALL be a release gate112+113+The project SHALL maintain focused fixtures covering canonical valid levels,114+legacy shorthand, invalid coordinates, duplicate IDs, invalid tokens,115+automaton action errors, and share/export round trips. The gate SHALL run the116+direct engine, CommonJS loader, and data-only sharing paths against those117+fixtures, followed by the full Node suite and production build.118+119+#### Scenario: Parity fixtures catch validator drift120+121+- **WHEN** one path accepts or normalizes a fixture differently from the122+ canonical boundary123+- **THEN** the focused parity test fails with the path and issue difference124+ before the change can be marked complete
A openspec/changes/consolidate-level-schema-boundary/tasks.md +32 -0
@@ -0,0 +1,32 @@
1+## 1. Canonical boundary2+3+- [ ] 1.1 Inventory current direct engine, CommonJS, share, catalog, and solver4+ level fields and write the canonical normalization API and context profiles.5+- [ ] 1.2 Add a pure level schema module with cloned canonical output, stable6+ issue codes/paths, explicit object kinds, uppercase tokens, and explicit7+ automaton actions.8+- [ ] 1.3 Preserve and test the documented legacy shorthand as input-only;9+ ensure export and shared encoding emit canonical action objects.10+- [ ] 1.4 Keep pack-only identity, count, and size limits as explicit profile11+ adapters around the shared structural validator.12+13+## 2. Integrations14+15+- [ ] 2.1 Route `createGame()` through the canonical boundary while preserving16+ its current level-labeled error types and standalone fixtures.17+- [ ] 2.2 Route CommonJS validation/normalization through the boundary while18+ preserving `ModValidationError`, source context, and atomic registration.19+- [ ] 2.3 Route CommonJS export and data-only share encode/decode through the20+ canonical boundary and preserve current schema/revision behavior.21+22+## 3. Verification23+24+- [ ] 3.1 Add parity fixtures for valid data, legacy shorthand, malformed25+ coordinates/tokens/actions, duplicate IDs, and source immutability.26+- [ ] 3.2 Add CommonJS and shared round-trip assertions for canonical gameplay27+ data, automata metadata, and first-transition parity.28+- [ ] 3.3 Run focused engine/mod/share tests, full Node tests, production build,29+ world solver, and browser import/share flows.30+- [ ] 3.4 Run strict OpenSpec validation, document any deferred metadata fields,31+ and archive this change only after all adapter and round-trip evidence is32+ complete.