clone

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

Specify derived versus stateful mechanic contracts

Dami committed 7d21b78
commit
7d21b78fc3aa506e5d899233cb47292a2439c75a
parent
a3f32aeaa2a53755556ca8d7cf98e5b43f6a4f94
author
Dami <dami.etoile@gmail.com>
7 files changed, +129 -31
Mopenspec/changes/add-channel-linked-gates/specs/channel-linked-gates/spec.md20++++++++++
Mopenspec/changes/add-declarative-mechanic-state-contract/design.md34+++++++++++------
Mopenspec/changes/add-declarative-mechanic-state-contract/specs/declarative-mechanic-state/spec.md61++++++++++++++++++++++++++----
Mopenspec/changes/add-declarative-mechanic-state-contract/tasks.md14++++--
Mopenspec/changes/add-frail-leftover-tiles/design.md3+-
Mopenspec/changes/add-frail-leftover-tiles/specs/frail-leftover-tiles/spec.md23++++++++++-
Mopenspec/changes/add-frail-leftover-tiles/tasks.md5+-
M openspec/changes/add-channel-linked-gates/specs/channel-linked-gates/spec.md +20 -0
@@ -95,6 +95,12 @@ SHALL recompute its state from the final board without remembered openness.
9595 final channel plate and gate cell are empty9696 - **THEN** it rejoins closed; no prior open state affects the result9797 98+The gate and plate projections SHALL be a derived mechanic contract. `open`,99+`jammed`, and `pressed` SHALL NOT be persisted as hidden mutable mechanic100+fields or redundantly encoded in solver frontier bytes. They SHALL be101+recomputed from the restored objects, rules, occupancy, and authored channel102+metadata.103+98104 ### Requirement: Gate and plate changes SHALL be observable in settled events99105 100106 Settled results SHALL expose cloned `gateChanged` and `plateChanged` arrays when
@@ -132,6 +138,20 @@ prior settled blocker projection and SHALL reject hidden mutable gate fields.
132138 - **THEN** public and fast transitions agree on accepted status, object state,133139 gate projection, and emitted gate/plate changes134140 141+#### Scenario: Restore recomputes a memoryless projection142+143+- **WHEN** a snapshot containing the same authoritative objects and channel144+ metadata is restored after a prior gate projection145+- **THEN** the gate and plate projections are recomputed identically without146+ carrying an `open`, `jammed`, or `pressed` bit from the prior state147+148+#### Scenario: Hidden gate state fails closed149+150+- **WHEN** a gate implementation attempts to encode a mutable openness field151+ that is not part of the derived projection inputs152+- **THEN** the mechanic registry or solver codec rejects the state before play153+ or search154+135155 #### Scenario: Two channels remain independent136156 137157 - **WHEN** an amber plate is pressed while a teal plate is not
M openspec/changes/add-declarative-mechanic-state-contract/design.md +22 -12
@@ -42,20 +42,28 @@ mechanic ID and validates all object references before creating a game.
4242 4343 The exact fields for a concrete mechanic are defined by its later capability4444 spec. This foundation only requires that the host can validate the definition,45-initialize its state, and supply a state codec registration.45+initialize state when needed, and supply either a state codec registration or a46+derived no-hidden-state assertion.4647 47-### Keep dynamic state in one envelope48+### Separate derived projections from stateful mechanics4849 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.50+The settled snapshot may contain `mechanics`, keyed by mechanic ID, for51+stateful adapters. Each stateful value is owned by the host implementation and52+contains only declared fields. The engine initializes it from the normalized53+level, clones it with the same transaction snapshot as objects and automata,54+and rejects unknown IDs or fields during restore.55+56+A derived adapter declares an empty state contract and recomputes its projection57+from settled objects, rules, occupancy, and authored metadata. Derived outputs58+such as gate openness, plate pressure, jam status, and FRAIL existence are not59+stored as a second mutable truth and are not redundantly encoded in solver60+frontier bytes. A mechanic must not hide mutable state in renderer, juice,61+module globals, or authored object fields.5562 5663 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.64+the envelope may be omitted from serialized data when empty. Stateful65+mechanics must have a complete initialized value; derived mechanics must have66+an explicit empty state contract.5967 6068 ### Define explicit phase hooks, not arbitrary callbacks6169
@@ -93,8 +101,10 @@ Concrete mechanic specs define their event vocabulary and ordering.
93101 ### Make solver registration mandatory94102 95103 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.104+stateful registration has a stable mechanic ID, an ordered schema, and a codec105+version. The registry must encode every dynamic field that can affect a future106+turn. A derived registration instead identifies its authoritative projection107+inputs and asserts that no additional mutable bit is required.98108 Unknown mechanic IDs, fields, types, missing values, or unregistered runtime99109 state fail with actionable errors. Static authored metadata stays outside the100110 frontier blob.
M openspec/changes/add-declarative-mechanic-state-contract/specs/declarative-mechanic-state/spec.md +52 -9
@@ -3,16 +3,26 @@
33 ### Requirement: Mechanic metadata SHALL be declarative and host validated44 55 The host SHALL accept only normalized mechanic definitions with a stable ID,6-supported type, version, static configuration, and declared state schema. A6+supported type, version, static configuration, and a declared state contract.7+The state contract SHALL identify either a `derived` mechanic with no mutable8+runtime fields or a `stateful` mechanic with a complete registered scalar9+schema. A710 CommonJS pack MAY declare supported mechanic metadata but SHALL NOT supply811 callbacks, functions, URLs, or executable lifecycle behavior.912 10-#### Scenario: A supported declaration loads13+#### Scenario: A supported stateful declaration loads1114 1215 - **WHEN** a pack declares a supported mechanic with valid static metadata and1316 a complete state schema14-- **THEN** normalization preserves it and the level can create a game with an15- initialized mechanic state17+- **THEN** normalization preserves it and the level can create a game with a18+ complete initialized mechanic state19+20+#### Scenario: A supported derived declaration loads21+22+- **WHEN** a pack declares a supported derived mechanic with valid static23+ metadata and no mutable state fields24+- **THEN** normalization preserves it and the level creates no redundant25+ runtime mechanic value for the derived projection1626 1727 #### Scenario: Unsupported mechanic behavior fails closed1828
@@ -29,9 +39,12 @@ callbacks, functions, URLs, or executable lifecycle behavior.
2939 3040 ### Requirement: Mechanics SHALL have one transactional settled state3141 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,42+The engine SHALL initialize every stateful mechanic in `state.mechanics` and43+carry that envelope through accepted turns, Undo, reset, and restore. Derived44+mechanics SHALL recompute their projections from authoritative settled objects,45+rules, and authored metadata; their projections SHALL NOT be persisted as46+redundant mutable state. A rejected or complexity-blocked turn SHALL leave47+mechanic state, objects,3548 automata phases, and history unchanged.3649 3750 #### Scenario: Reset restores authored mechanic state
@@ -58,6 +71,13 @@ automata phases, and history unchanged.
5871 - **THEN** the complete prior settled state, including mechanic fields, is5972 restored and no undo entry is added6073 74+#### Scenario: Derived state restores from the board75+76+- **WHEN** a valid snapshot containing a derived mechanic's authoritative77+ objects and authored metadata is restored78+- **THEN** the same derived projection is recomputed without applying a hidden79+ bit, advancing an automaton, or invoking a later turn phase80+6181 ### Requirement: Mechanic lifecycle phases SHALL be deterministic6282 6383 Host-owned mechanics SHALL receive named lifecycle phases around the existing
@@ -80,7 +100,8 @@ stable and a mechanic SHALL declare which phases it uses.
80100 ### Requirement: Settled events SHALL expose mechanic transitions safely81101 82102 The engine SHALL include bounded, cloned mechanic transition records in settled83-events when mechanic state or mechanic-reported effects change. Levels without103+events when stateful mechanic state or derived mechanic effects change. Levels104+without84105 mechanic changes SHALL preserve the existing event shape and behavior.85106 86107 #### Scenario: A mechanic change is observable
@@ -89,6 +110,12 @@ mechanic changes SHALL preserve the existing event shape and behavior.
89110 - **THEN** the event identifies the mechanic, stable subject when applicable,90111 prior and new values or an approved event payload, and a bounded cause token91112 113+#### Scenario: A derived mechanic effect is observable without hidden state114+115+- **WHEN** a derived projection changes after a settled turn116+- **THEN** the event exposes the approved concrete transition records while117+ the snapshot contains no redundant projection bit118+92119 #### Scenario: Event data cannot mutate state93120 94121 - **WHEN** presentation code mutates a returned mechanic event or snapshot
@@ -113,9 +140,12 @@ Pack normalization, catalog registration, CommonJS export/import, and data-only
113140 ### Requirement: Solver state SHALL register every dynamic mechanic field114141 115142 The solver SHALL derive or receive a deterministic codec registry for every116-declared mechanic. Encoding SHALL fail on unknown IDs, unknown fields, missing143+stateful mechanic. Encoding SHALL fail on unknown IDs, unknown fields, missing117144 registrations, invalid values, or unsupported schema versions. A compact key118145 SHALL include every registered value that can affect a future transition.146+Derived mechanics SHALL register their projection inputs or an explicit147+no-hidden-state assertion; their derived output SHALL NOT be redundantly148+encoded as a second mutable value.119149 120150 #### Scenario: Same board with different mechanic state stays distinct121151
@@ -134,3 +164,16 @@ SHALL include every registered value that can affect a future transition.
134164 restored135165 - **THEN** the next public and fast transitions match direct replay from the136166 original settled state167+168+#### Scenario: Derived projections preserve public and fast parity169+170+- **WHEN** a derived mechanic snapshot is decoded and restored before the next171+ transition172+- **THEN** public and fast transitions recompute the same projection and event173+ records without a serialized derived-state bit174+175+#### Scenario: Hidden derived bits fail closed176+177+- **WHEN** an implementation attempts to add an unregistered mutable field for178+ a derived mechanic179+- **THEN** codec or engine validation rejects it before search or play
M openspec/changes/add-declarative-mechanic-state-contract/tasks.md +9 -5
@@ -1,7 +1,8 @@
11 ## 1. Contract and validation22 33 - [ ] 1.1 Define the normalized level `mechanics` schema with stable IDs,4- host-supported types, versions, static configuration, and state schema.4+ host-supported types, versions, static configuration, and either a derived5+ no-state contract or a stateful scalar schema.56 - [ ] 1.2 Validate mechanic IDs, duplicate definitions, authored references,67 unsupported types, unsupported fields, and executable values in `src/mods.js`78 and the engine boundary.
@@ -12,8 +13,9 @@
1213 1314 ## 2. Engine lifecycle and snapshots1415 15-- [ ] 2.1 Initialize `state.mechanics` from normalized definitions during reset16- and expose a cloned settled envelope from `getState()`.16+- [ ] 2.1 Initialize `state.mechanics` from stateful normalized definitions17+ during reset and expose a cloned settled envelope from `getState()`; derived18+ projections remain recomputable and are not duplicated in the envelope.1719 - [ ] 2.2 Add host-owned deterministic phase adapters for before movement,1820 after movement, after transforms, before contacts, after contacts, and1921 settled finalization.
@@ -26,8 +28,10 @@
2628 2729 ## 3. Solver and replay contract2830 29-- [ ] 3.1 Derive a deterministic mechanic codec registry from normalized level30- definitions and pass it to `createStateCodec()` and `solveLevel()`.31+- [ ] 3.1 Derive a deterministic stateful mechanic codec registry from32+ normalized level definitions and pass it to `createStateCodec()` and33+ `solveLevel()`; require derived mechanics to declare their projection inputs34+ and no-hidden-state assertion.3135 - [ ] 3.2 Reject unknown mechanic IDs, fields, missing registrations, invalid3236 values, and unsupported schema versions before search expands a state.3337 - [ ] 3.3 Round-trip mechanic state through encode/decode/restore and compare
M openspec/changes/add-frail-leftover-tiles/design.md +2 -1
M openspec/changes/add-frail-leftover-tiles/specs/frail-leftover-tiles/spec.md +21 -2
@@ -111,8 +111,11 @@ authoritative object noun/existence and leaves metadata exactly.
111111 The compact solver SHALL use the same movement, transform, FRAIL checkpoint,112112 and contact ordering as the public engine. Its key SHALL distinguish object113113 identity, position, current noun, existence, and any registered mechanic state114-that affects a future crumble. Solver replay SHALL reproduce `crumbled` and115-chain transitions without JSON-order or presentation-only state.114+that affects a future crumble. Authored `leaves` metadata is level-bound115+static data, while existence and current noun are already authoritative object116+state; neither may be duplicated as a hidden FRAIL runtime bit. Solver replay117+SHALL reproduce `crumbled` and chain transitions without JSON-order or118+presentation-only state.116119 117120 #### Scenario: Same board different chain stages remain distinct118121 - **WHEN** two search states have identical positions but one object is SAND
@@ -125,3 +128,19 @@ chain transitions without JSON-order or presentation-only state.
125128 automaton departure, and two-link chain through both transition paths126129 - **THEN** each path has the same settled objects, rules, contacts, and127130 `crumbled` records131+132+#### Scenario: Restore preserves derived FRAIL parity133+134+- **WHEN** a snapshot is decoded and restored at either link of a leftover135+ chain136+- **THEN** public and fast transitions derive the same crumble result from137+ object existence, noun, position, and authored `leaves` metadata without a138+ hidden FRAIL state field139+140+#### Scenario: Hidden FRAIL state fails closed141+142+- **WHEN** an implementation attempts to encode a separate crumble counter or143+ existence bit for an object already represented by the authoritative object144+ snapshot145+- **THEN** codec or mechanic validation rejects the redundant state before146+ search or play
M openspec/changes/add-frail-leftover-tiles/tasks.md +3 -2