- commit
- 9e91d4229e810be7370a9f4b6592c6cbc83ae173
- parent
- 8080d4c32c65fb91f2017ce26b14a585e64d06d4
- author
- Dami <dami.etoile@gmail.com>
| A | openspec/ | 149 | ++++++++++++++++++++++++++++++ |
| A | openspec/ | 47 | +++++++++ |
| A | openspec/ | 136 | +++++++++++++++++++++++++++ |
| A | openspec/ | 46 | +++++++++ |
@@ -0,0 +1,149 @@
1+## Context2+3+`createGame()` owns authoritative settled state, history, restore, and turn4+resolution. A turn currently moves player-controlled objects, reparses and5+transforms rules, advances automata, reparses and transforms again, then6+resolves contacts and victory. `state.objects` and automata phases are carried7+through snapshots, while the solver's codec can accept dynamic fields only8+through an explicit scalar registry. The missing seam is a mechanic lifecycle9+that all of those boundaries can consume.10+11+## Goals and non-goals12+13+### Goals14+15+- Keep mechanic definitions data-only, deterministic, and validated before play.16+- Give every mechanic one authoritative state envelope and one turn lifecycle.17+- Preserve that state through reset, accepted and rejected turns, Undo, restore,18+ solver replay, pack export, and data-only share links.19+- Make event and solver registration requirements visible before a mechanic is20+ eligible for bundled-world acceptance.21+- Keep mechanics host-defined. A CommonJS pack declares supported metadata but22+ does not provide callbacks or executable behavior.23+24+### Non-goals25+26+- Decide the semantics of gates, switches, plates, FRAIL, crumbling, counters,27+ or floor layers.28+- Add a renderer, audio cue, or visual language for a mechanic.29+- Permit remote or untrusted code to implement mechanics.30+- Replace the existing object, automaton, rule, or contact model.31+32+## Decisions33+34+### Use a normalized declarative definition35+36+After mod normalization, a level may contain a `mechanics` array. Each entry37+has a stable lowercase `id`, a host-supported `type`, a version, static38+configuration, and a deterministic state schema. Static configuration may39+identify authored object IDs or channels, but it cannot contain functions,40+URLs, or values outside the host schema. The host assigns a unique order by41+mechanic ID and validates all object references before creating a game.42+43+The exact fields for a concrete mechanic are defined by its later capability44+spec. This foundation only requires that the host can validate the definition,45+initialize its state, and supply a state codec registration.46+47+### Keep dynamic state in one envelope48+49+The settled snapshot gains `mechanics`, keyed by mechanic ID. Each value is50+owned by the host implementation for that mechanic and contains only declared51+fields. The engine initializes it from the normalized level, clones it with the52+same transaction snapshot as objects and automata, and rejects unknown IDs or53+fields during restore. A mechanic must not hide mutable state in renderer,54+juice, module globals, or authored object fields.55+56+For levels without mechanics, the existing public behavior remains unchanged;57+the envelope may be omitted from serialized data when empty. For levels with58+mechanics, every declared mechanic must have a complete initialized value.59+60+### Define explicit phase hooks, not arbitrary callbacks61+62+The engine exposes a host-internal lifecycle with deterministic phases:63+64+1. `beforeMovement`: inspect the settled state and prepare blockers or intent.65+2. `afterMovement`: observe player and automaton movement before contacts.66+3. `afterTransforms`: observe the final noun/rule projection for the turn.67+4. `beforeContacts`: apply mechanic effects that intentionally precede SINK or68+ DEFEAT contacts.69+5. `afterContacts`: observe destruction and final occupancy.70+6. `settled`: finalize state and event data.71+72+The phase order is part of the engine contract. A concrete mechanic must state73+which hooks it uses and whether its state mutation is transactional. Hook74+implementations are registered host code selected by a validated type; mod75+data cannot add a callback.76+77+Rejected or complexity-blocked turns do not commit mechanic state. A mechanic78+must be deterministic from the authored definition, the prior settled state,79+and the current phase inputs.80+81+### Treat events as the presentation boundary82+83+Settled results retain the existing movement, rule, transformation,84+destruction, and automata fields and gain a normalized `mechanics` collection85+only when a mechanic reports a transition. Each record contains the mechanic86+ID, stable subject ID when applicable, `from` and `to` state values or a87+declared event payload, and a bounded cause/type token. The engine returns88+cloned event data so presentation cannot mutate authoritative state.89+90+The foundation does not prescribe names such as `pressed` or `crumbled`.91+Concrete mechanic specs define their event vocabulary and ordering.92+93+### Make solver registration mandatory94+95+The solver codec receives a normalized registry derived from the level. Each96+registration has a stable mechanic ID, an ordered schema, and a codec version.97+The registry must encode every dynamic field that can affect a future turn.98+Unknown mechanic IDs, fields, types, missing values, or unregistered runtime99+state fail with actionable errors. Static authored metadata stays outside the100+frontier blob.101+102+The registry may use fixed authored slots and scalar fields already supported by103+the codec. If a future mechanic needs arrays, maps, or more than the scalar104+range, it must add an explicit deterministic codec representation rather than105+falling back to JSON key order. Encoding and decoding must round-trip through106+`game.restore()` without applying a transform or advancing an automaton.107+108+### Preserve data through pack and share boundaries109+110+Mod validation, normalization, catalog registration, export, and data-only111+sharing preserve the normalized mechanic definitions. Shared links never carry112+executable mechanic code. A pack declaring a mechanic type unsupported by the113+host fails with a feature diagnostic before installation; it is not treated as114+an ordinary noun transformation.115+116+## Risks and mitigations117+118+- **State omission merges solver futures.** Fail closed on unknown dynamic119+ fields and require a same-board/different-state identity fixture per later120+ mechanic.121+- **Hook order diverges from gameplay.** Add public/fast parity tests at every122+ lifecycle boundary before enabling solver acceptance.123+- **Metadata drifts during sharing.** Round-trip normalized pack, exported124+ CommonJS, and shared JSON fixtures through the same validator.125+- **A generic schema becomes an accidental plugin API.** Keep type handlers126+ host-owned and reject functions, callbacks, and unknown fields.127+- **A future mechanic needs richer state than scalar fields.** Version the128+ registry and require a mechanic-specific codec extension before content use.129+130+## Migration and rollback131+132+1. Add metadata validation and normalization with no existing mechanic entries.133+2. Add the engine envelope and lifecycle adapter with empty-mechanic parity.134+3. Connect restore, Undo, event cloning, pack/share, and solver registry tests.135+4. Add later gate and FRAIL behavior changes one at a time.136+137+Rollback removes the foundation consumers and leaves existing packs valid. A138+later mechanic must not be enabled until this contract and its codec fixtures139+are present.140+141+## Open questions for later mechanic specs142+143+- Which static metadata fields identify entities, channels, or authored slots?144+- Which mechanics need `beforeContacts` versus `afterContacts` semantics?145+- Does a mechanic's state transition remain valid when a referenced object is146+ transformed or destroyed?147+- What event payload is sufficient for presentation without exposing internal148+ mutable state?149+- Which concrete mechanic types should be included in the host feature list?@@ -0,0 +1,47 @@
1+## Why2+3+The engine is ready for dynamic rules and automata, but it has no shared4+contract for mechanics that carry state across turns. Gates, switches, and5+one-step tiles will otherwise add fields that work in play but disappear on6+Undo, restore, sharing, or solver replay. The solver already has an explicit7+registry for compact mechanic fields, but the gameplay engine and mod boundary8+do not provide the corresponding lifecycle.9+10+## What Changes11+12+- Define a versioned, declarative mechanic metadata contract for levels and13+ CommonJS packs.14+- Add an engine-owned `state.mechanics` envelope initialized from authored15+ metadata and carried through reset, turns, Undo, restore, and `getState()`.16+- Define named mechanic turn hooks around the existing movement, transform,17+ automata, and contact phases without allowing arbitrary mod code to run.18+- Extend settled events with normalized mechanic transitions and diagnostics.19+- Preserve mechanic metadata through normalized packs, data-only sharing, and20+ CommonJS export/import.21+- Require every dynamic mechanic field to register a deterministic compact22+ solver representation and fail closed when state is unregistered or cannot23+ round-trip through authoritative restore.24+25+This change defines the foundation only. It does not decide gate occupancy,26+switch edge behavior, FRAIL timing, crumble leftovers, or any other mechanic's27+gameplay semantics. Those behaviors belong in separate changes with world28+fixtures and design approval.29+30+## Capabilities31+32+### New Capabilities33+34+- `declarative-mechanic-state`: validated mechanic metadata, lifecycle state,35+ events, snapshots, sharing, and solver registration.36+37+### Modified Capabilities38+39+None.40+41+## Impact42+43+The engine state and restore boundary, CommonJS/mod validation, normalized pack44+and sharing paths, solver codec/transition setup, and focused tests will change.45+Existing levels without mechanic metadata retain the current state shape and46+behavior. Designer-owned rendering and world-owned level content remain out of47+scope.@@ -0,0 +1,136 @@
1+## ADDED Requirements2+3+### Requirement: Mechanic metadata SHALL be declarative and host validated4+5+The host SHALL accept only normalized mechanic definitions with a stable ID,6+supported type, version, static configuration, and declared state schema. A7+CommonJS pack MAY declare supported mechanic metadata but SHALL NOT supply8+callbacks, functions, URLs, or executable lifecycle behavior.9+10+#### Scenario: A supported declaration loads11+12+- **WHEN** a pack declares a supported mechanic with valid static metadata and13+ a complete state schema14+- **THEN** normalization preserves it and the level can create a game with an15+ initialized mechanic state16+17+#### Scenario: Unsupported mechanic behavior fails closed18+19+- **WHEN** a pack declares an unknown mechanic type or an unsupported field20+- **THEN** validation rejects the pack with the declaration path and supported21+ feature information, before catalog installation or play22+23+#### Scenario: Executable mechanic data is rejected24+25+- **WHEN** a mechanic declaration contains a function, callback, URL, or other26+ executable value27+- **THEN** validation rejects it and the CommonJS loader never exposes it as a28+ host lifecycle hook29+30+### Requirement: Mechanics SHALL have one transactional settled state31+32+The engine SHALL initialize every declared mechanic in `state.mechanics` and33+carry that envelope through accepted turns, Undo, reset, and restore. A34+rejected or complexity-blocked turn SHALL leave mechanic state, objects,35+automata phases, and history unchanged.36+37+#### Scenario: Reset restores authored mechanic state38+39+- **WHEN** a game with a mechanic has accepted turns and then receives reset40+- **THEN** objects, rules, automata phases, moves, and mechanic state match the41+ authored initial state42+43+#### Scenario: Undo restores mechanic state44+45+- **WHEN** an accepted turn changes a mechanic field and the player invokes Undo46+- **THEN** the field returns to its prior value and the returned event reports a47+ restored transition without mutating the snapshot returned earlier48+49+#### Scenario: Restore keeps a settled mechanic state inert50+51+- **WHEN** a valid snapshot containing mechanic state is restored52+- **THEN** the engine accepts that state without applying a noun transform,53+ advancing an automaton, or invoking a later turn phase54+55+#### Scenario: Rejected turns are atomic56+57+- **WHEN** a mechanic or rule parser rejects a turn after speculative movement58+- **THEN** the complete prior settled state, including mechanic fields, is59+ restored and no undo entry is added60+61+### Requirement: Mechanic lifecycle phases SHALL be deterministic62+63+Host-owned mechanics SHALL receive named lifecycle phases around the existing64+turn pipeline: before movement, after movement, after transforms, before65+contacts, after contacts, and settled finalization. The phase order SHALL be66+stable and a mechanic SHALL declare which phases it uses.67+68+#### Scenario: A mechanic observes the documented order69+70+- **WHEN** an accepted turn moves an object, changes a rule, and resolves a71+ contact72+- **THEN** the mechanic observes the phases in the documented order and its73+ state transition is committed once at settled finalization74+75+#### Scenario: Mechanics do not run on an invalid input76+77+- **WHEN** input has an invalid direction or no controllable entity can move78+- **THEN** no mutating mechanic phase commits state or emits a transition79+80+### Requirement: Settled events SHALL expose mechanic transitions safely81+82+The engine SHALL include bounded, cloned mechanic transition records in settled83+events when mechanic state or mechanic-reported effects change. Levels without84+mechanic changes SHALL preserve the existing event shape and behavior.85+86+#### Scenario: A mechanic change is observable87+88+- **WHEN** a settled turn changes a registered mechanic value89+- **THEN** the event identifies the mechanic, stable subject when applicable,90+ prior and new values or an approved event payload, and a bounded cause token91+92+#### Scenario: Event data cannot mutate state93+94+- **WHEN** presentation code mutates a returned mechanic event or snapshot95+- **THEN** a later `getState()` call and the next turn remain unchanged96+97+### Requirement: Mechanic metadata preservation98+Pack normalization, catalog registration, CommonJS export/import, and data-only sharing SHALL preserve validated mechanic metadata. Shared data SHALL contain declarative JSON only and SHALL be revalidated on install.99+100+#### Scenario: Export and share preserve a declaration101+102+- **WHEN** a valid pack with mechanic metadata is exported and shared, then103+ loaded through each supported path104+- **THEN** the normalized level metadata and initial mechanic state are105+ equivalent to the source pack106+107+#### Scenario: Shared unsupported metadata is rejected108+109+- **WHEN** a data-only share payload declares an unsupported mechanic type or110+ field111+- **THEN** decoding rejects it before installation and does not execute code112+113+### Requirement: Solver state SHALL register every dynamic mechanic field114+115+The solver SHALL derive or receive a deterministic codec registry for every116+declared mechanic. Encoding SHALL fail on unknown IDs, unknown fields, missing117+registrations, invalid values, or unsupported schema versions. A compact key118+SHALL include every registered value that can affect a future transition.119+120+#### Scenario: Same board with different mechanic state stays distinct121+122+- **WHEN** two nonterminal snapshots have identical objects and automata but123+ different registered mechanic state124+- **THEN** their compact solver keys differ125+126+#### Scenario: Unregistered dynamic state fails closed127+128+- **WHEN** a snapshot contains mechanic state without a matching registry entry129+- **THEN** codec encoding fails with an actionable unregistered-state error130+131+#### Scenario: Mechanic codec round-trips through restore132+133+- **WHEN** a compact solver state containing mechanic values is decoded and134+ restored135+- **THEN** the next public and fast transitions match direct replay from the136+ original settled state@@ -0,0 +1,46 @@
1+## 1. Contract and validation2+3+- [ ] 1.1 Define the normalized level `mechanics` schema with stable IDs,4+ host-supported types, versions, static configuration, and state schema.5+- [ ] 1.2 Validate mechanic IDs, duplicate definitions, authored references,6+ unsupported types, unsupported fields, and executable values in `src/mods.js`7+ and the engine boundary.8+- [ ] 1.3 Normalize and clone mechanic metadata consistently in CommonJS,9+ catalog, pack service, export, and data-only share paths.10+- [ ] 1.4 Add diagnostics that identify an unsupported mechanic feature and the11+ path that declared it.12+13+## 2. Engine lifecycle and snapshots14+15+- [ ] 2.1 Initialize `state.mechanics` from normalized definitions during reset16+ and expose a cloned settled envelope from `getState()`.17+- [ ] 2.2 Add host-owned deterministic phase adapters for before movement,18+ after movement, after transforms, before contacts, after contacts, and19+ settled finalization.20+- [ ] 2.3 Include mechanic state in accepted-turn transactions and restore the21+ previous state on blocked, invalid, or rule-complexity-rejected turns.22+- [ ] 2.4 Preserve and validate mechanic state through Undo, reset, and23+ `restore()`; prove restored state does not advance transformations or phases.24+- [ ] 2.5 Add bounded, cloned mechanic transition records to settled events25+ without changing existing events for levels with no mechanics.26+27+## 3. Solver and replay contract28+29+- [ ] 3.1 Derive a deterministic mechanic codec registry from normalized level30+ definitions and pass it to `createStateCodec()` and `solveLevel()`.31+- [ ] 3.2 Reject unknown mechanic IDs, fields, missing registrations, invalid32+ values, and unsupported schema versions before search expands a state.33+- [ ] 3.3 Round-trip mechanic state through encode/decode/restore and compare34+ the next public and fast transitions.35+- [ ] 3.4 Add a same-board/different-mechanic-state key fixture and confirm36+ unregistered state cannot be silently merged.37+- [ ] 3.5 Preserve solver shortest-path, BYPASS, HANDS-OFF, cap, and replay38+ behavior for levels without mechanics.39+40+## 4. Verification and handoff41+42+- [ ] 4.1 Add focused engine, mod, pack/share, codec, and solver parity tests.43+- [ ] 4.2 Run the full Node suite and production build.44+- [ ] 4.3 Run strict OpenSpec validation for this change.45+- [ ] 4.4 Send the foundation contract and open semantic questions to the46+ worlds team before beginning gate or FRAIL behavior specs.