- commit
- 77acd7d5cf5caf349c715f61fda20f42b832d2cf
- parent
- 741f4c71650aa2265ea4f6e7f3b448fd749f1bc9
- author
- Dami <dami.etoile@gmail.com>
| A | openspec/ | 2 | + |
| A | openspec/ | 97 | ++++++++++++++++++++++++++++++ |
| A | openspec/ | 29 | +++++++++ |
| A | openspec/ | 12 | ++++ |
| A | openspec/ | 64 | ++++++++++++++++++++ |
| A | openspec/ | 93 | +++++++++++++++++++++++++++++ |
| A | openspec/ | 12 | ++++ |
| A | openspec/ | 44 | ++++++++++++++ |
| M | openspec/ | 15 | ++++- |
| A | openspec/ | 68 | +++++++++++++++++++++ |
| A | openspec/ | 97 | ++++++++++++++++++++++++++++++ |
| M | openspec/ | 14 | ++++- |
@@ -0,0 +1,97 @@
1+## Context2+3+The archived engine change provides three MIAU teaching levels, a validated local CommonJS loader, and a browser shell that currently treats loaded mod levels as one temporary list. This change turns authored content into a stable pack registry and adds two sharing paths: executable `.cjs` export/import for explicitly trusted local files, and a portable data-only URL that never evaluates code. The designer owns scenes and visual layout, so this design specifies content identity, data flow, and feedback semantics without prescribing panel placement or animation.4+5+## Goals / Non-Goals6+7+**Goals:**8+9+- Give every shipped pack and level a stable identity independent of display titles or array positions.10+- Retain multiple bundled, URL, and explicitly trusted local packs in one session.11+- Ship a curated basic content set while preserving the original three MIAU teaching levels.12+- Export validated authored pack data as a deterministic `.cjs` module.13+- Share validated pack data through a versioned URL representation that performs no code execution.14+- Define atomic validation, collision, replacement, and user feedback behavior.15+- Verify registry behavior, browser controls, dynamic rule changes, mod loading, sharing, and undo together.16+17+**Non-Goals:**18+19+- A visual redesign, scene graph, menu layout, animation language, or input ownership. Those decisions remain with the designer.20+- Server hosted packs, accounts, cloud persistence, analytics, or multiplayer sharing.21+- Executing JavaScript from a URL payload or treating a URL as a CommonJS module.22+- Sandboxing arbitrary untrusted CommonJS. Local CJS import remains an explicit trusted action.23+- Changing the engine's six properties, turn ordering, or rule vocabulary.24+25+## Decisions26+27+### 1. Use pack id plus level id as the content address28+29+`manifest.id` is the stable pack id. Each level gains a stable lowercase id unique within that pack. The registry key is `{ packId, levelId }`; titles and array positions are display and ordering metadata only. A generated id is not acceptable for authored content because it would make URLs and exported files unstable.30+31+### 2. Keep a session registry with origin metadata32+33+The registry stores normalized packs in insertion order with origin `bundled`, `url`, or `local`. Registration validates the entire pack before mutation. A failed registration leaves the registry byte-for-byte unchanged. The active selection stores both pack and level ids and resolves the selected level through the engine's existing validation path.34+35+### 3. Preserve the original teaching pack and add curated content as separate packs36+37+The existing three MIAU levels become a named teaching pack with stable level ids. Additional basic packs are separate bundled records, so the teaching sequence remains stable and imported content cannot alter its order. The registry can later support authored progression metadata without coupling it to the engine.38+39+### 4. Resolve pack collisions with Replace or an explicit fork40+41+A duplicate pack id pauses for an explicit choice. `Replace` validates the complete candidate first, then swaps it atomically while retaining the authored id and progress key. `Keep both as a copy` chooses the next available suffixed id (`-2`, `-3`, …), records `forkOf` provenance, and starts progress under the new id. Level ids only need to be unique inside a pack; `intro` in two different packs is valid. This makes stale share links diagnosable and avoids silently replacing player content.42+43+### 5. Serialize one canonical data model through both sharing paths44+45+The canonical pack data contains manifest identity/version, ordered levels with ids and authored objects, vocabulary, and palette. It excludes current engine objects, move count, undo history, win state, and UI state. `.cjs` export prints deterministic CommonJS source that returns this data. URL sharing serializes the same data as canonical JSON inside a versioned `miau1.<base64url>.<checksum>` payload in the URL hash. The checksum catches accidental corruption; it does not authenticate authorship.46+47+### 6. Keep URL decoding data-only and compatible48+49+The share link is exactly `#pack=<versioned-payload>`, with a maximum encoded payload of 16 KiB. The URL path decodes bytes, parses JSON, checks the envelope version and size, and runs pack validation. Schema `v1` is the only currently documented share format, so the decoder keeps v1 compatible across site revisions and rejects unknown future versions before registration; a later documented schema gets an explicit adapter and fixture at that time. It never calls the CommonJS loader, `new Function`, `require`, or any hook. A valid decoded pack is registered only after complete validation. This gives the user a compact portable link without turning links into executable content.50+51+### 7. Make local CJS trust explicit52+53+The local file control is separate from URL import and labels the operation as trusted code execution. It calls the existing CommonJS loader, validates its exported pack, and passes the normalized data to the same registry. Loader errors, unsupported imports, validation errors, and collisions are reported without discarding prior registry entries.54+55+The URL decoder never falls through to this path. Conversely, a `.cjs` file is never treated as data-only URL content.56+57+### 8. Normalize preview results and service failures58+59+`importPackText` and `decodePackLink` return the same `PackView` shape used by `listPacks`, with pack identity, author, origin, level summaries, and the complete first level for the Gift or Workshop mini preview. They also return `conflict: null` or `{ id, name }` when the candidate id is already registered; preview never mutates the registry. All player-facing hooks return `{ ok, ...value, errors }` envelopes. Errors carry a readable `message`, raw `detail` for Copy details, and an optional `levelId`, so scenes can show the first five without parsing exception text. Missing export/link ids and attempts to remove bundled packs are normal error results, not throws.60+61+### 9. Route player sharing through Gift and Workshop semantics62+63+A valid `#pack=` hash becomes a pending Gift state. It does not install automatically; the player must choose `Keep and play`, after which the registry applies collision handling. Pack management is exposed through a presentation-independent service with the hook IDs `listPacks`, `importPackText(text, filename)`, `exportPack(id)`, `packLink(id)`, `decodePackLink(hash)`, `installPack(pack, {replace})`, and `removePack(id)`. A Workshop scene can map these to Import, Play, Share link, Export, and Remove. Interim sidebar controls are developer-only behind `?dev`; stage, scene, and layout work belongs in a separate change.64+65+### 10. Use a semantic status channel without owning layout66+67+Sharing and registry operations report readable success or failure text with pack identity where available. The designer can render these in the appropriate scene or menu. The contract requires status to remain understandable without animation, but leaves placement, typography, and transitions open.68+69+### 11. Verify at data, engine, and browser boundaries70+71+Unit tests cover canonical serialization, URL tampering, no-code-execution decoding, identity/collision rules, multi-pack retention, and export round trips. Engine fixtures continue to prove rule changes and undo. Browser checks cover built-in selection, keyboard/touch movement, sharing controls, trusted local import, URL import, error feedback, and preservation of existing packs.72+73+## Risks / Trade-offs74+75+- [Pack URLs become too long] → Use compact canonical JSON and base64url, enforce the 16 KiB encoded maximum, and fall back to `.cjs` export for larger packs.76+- [A pack id collision can hide intended content] → Reject by default, show both origins, and require explicit replacement with atomic validation.77+- [Exported source is executable when imported] → Keep `.cjs` import visibly separate from URL import and require an explicit trusted local action.78+- [A URL payload is mistaken for code] → Use a versioned data envelope and a decoder that has no CommonJS or dynamic evaluation path.79+- [A site revision breaks old links] → Keep explicit compatibility adapters for every still-documented payload version and test links across decoder revisions.80+- [Authored ids are missing from existing levels] → Add stable ids to bundled content before registry activation and reject imported levels without them.81+- [Generated ids collide across imports] → Require authored stable pack and level ids at validation time; never derive registry identity from array position or a generated fallback.82+- [Additional packs dilute the teaching sequence] → Keep the MIAU teaching pack ordered and marked as the default; expose other packs as separate selectable content.83+- [Feedback conflicts with designer scenes] → Specify semantic status events and accessible text only; leave visual placement and presentation to the designer.84+85+## Migration Plan86+87+1. Add stable pack and level ids to the existing three MIAU levels and normalize the current example mod shape.88+2. Introduce the registry and load bundled content through it before enabling imported packs.89+3. Retain the existing `loadMod` validator as the trusted local CJS execution boundary, adapting its normalized output to pack records.90+4. Add canonical export and data-only URL encode/decode, then register both through the same validation and collision pipeline.91+5. Update selection and feedback adapters without changing the engine turn API or designer-owned layout.92+6. If rollback is needed, disable share controls and registry imports; the bundled teaching pack and existing engine remain playable.93+94+## Open Questions95+96+- Should a future scene display replacement history and origin alongside pack credits? The current registry preserves the active level if its id survives replacement; otherwise it selects the replacement pack's first level.97+- Should the designer's map scene group Garden Basics and Word Lab by concept, or preserve the current flat level list within each pack?@@ -0,0 +1,29 @@
1+## Why2+3+The current release has only a small built-in teaching campaign and a single selected mod path. Players need a durable way to browse curated content, combine local level packs, and share a level without handing executable code to a recipient.4+5+## What Changes6+7+- Add stable pack and level identifiers and a registry for multiple bundled and loaded packs.8+- Ship a curated set of basic level packs and retain all successfully loaded CommonJS modules for the session.9+- Export a selected pack as a CommonJS `.cjs` file for explicitly trusted local reuse.10+- Encode selected pack data into a portable `#pack=<versioned-payload>` data-only URL, capped at 16 KiB encoded, and validate/decode it without executing JavaScript across site revisions.11+- Keep local CommonJS import explicit and trusted, with duplicate and collision diagnostics.12+- Add in-app feedback for pack loading, selection, sharing, export, import, and validation failures.13+- Expand the built-in playable-puzzles contract from exactly three levels to a curated pack set while preserving the original three MIAU teaching puzzles.14+15+## Capabilities16+17+### New Capabilities18+19+- `level-packs`: Stable pack/level identity, curated bundled packs, multi-pack registry, selection, and collision policy.20+- `level-sharing`: Data-only URL sharing, `.cjs` export, explicit trusted local CommonJS import, validation, and user feedback.21+22+### Modified Capabilities23+24+- `playable-puzzles`: Replace the exact-three-level limit with a curated set of packs that includes the original three MIAU teaching puzzles.25+- `commonjs-mods`: Retain multiple valid modules and define collision behavior when packs or levels overlap.26+27+## Impact28+29+Adds pack registry and serialization modules, bundled content metadata, share/export/import controls, validation diagnostics, and browser and engine verification. The design specifies data contracts and feedback states while leaving visual layout and scene ownership to the designer.@@ -0,0 +1,12 @@
1+## ADDED Requirements2+3+### Requirement: The loader SHALL report pack identity collisions4+The loader SHALL report a valid module whose pack id is already registered to the pack service, which SHALL offer explicit `Replace` or `Keep both as a copy` handling. Replacement keeps the authored id; a copy receives the next available suffixed id and `forkOf` provenance. Either operation SHALL validate completely before changing the registry.5+6+#### Scenario: Duplicate pack ids are rejected7+- **WHEN** a second local module exports an already registered pack id8+- **THEN** the loader reports the collision without silently replacing the original pack and exposes the two explicit resolution choices9+10+#### Scenario: A forked local pack gets fresh progress identity11+- **WHEN** the player chooses `Keep both as a copy` for a duplicate local module12+- **THEN** the registered pack receives a suffixed id with `forkOf` provenance and progress is keyed by the new id@@ -0,0 +1,64 @@
1+## ADDED Requirements2+3+### Requirement: Packs and levels SHALL have stable identities4+Every pack SHALL have a stable lowercase pack id, display name, version, and ordered levels. Every level SHALL have a stable level id unique within its pack. Registry keys SHALL use the pair `{ packId, levelId }`, and display titles SHALL not be used as identity.5+6+#### Scenario: A renamed level keeps its identity7+- **WHEN** a pack changes a level title but retains its pack id and level id8+- **THEN** the registry treats it as the same level identity and updates only display metadata9+10+#### Scenario: Duplicate level ids are rejected within a pack11+- **WHEN** a pack contains two levels with the same level id12+- **THEN** pack validation fails before either level is registered13+14+### Requirement: The app SHALL ship curated bundled packs15+The app SHALL include a curated set of valid bundled packs, including the original three MIAU teaching puzzles as one ordered teaching pack. Bundled packs SHALL be available without a local file or URL import and SHALL pass the same identity and level validation as imported packs.16+17+#### Scenario: The teaching pack is available on a clean start18+- **WHEN** the app starts without imported content19+- **THEN** the registry contains the MIAU teaching pack and its three original levels in stable order20+21+#### Scenario: A bundled pack has invalid data22+- **WHEN** a bundled pack fails validation during startup23+- **THEN** the app reports the pack id and validation error and keeps other valid bundled packs available24+25+### Requirement: The registry SHALL retain multiple loaded packs26+The registry SHALL retain every valid bundled, URL, and explicitly trusted local pack loaded during the session. Each pack SHALL retain its origin metadata, and selecting one pack or level SHALL not discard other valid registered content.27+28+#### Scenario: Two local packs remain selectable29+- **WHEN** the player loads two valid packs with different pack ids30+- **THEN** both packs appear in the pack registry and levels from either pack can be selected31+32+#### Scenario: A failed pack does not remove earlier content33+- **WHEN** a later import fails validation or execution34+- **THEN** previously registered packs remain selectable and the failed pack is absent35+36+### Requirement: The registry SHALL apply an explicit collision policy37+The registry SHALL present an explicit choice when a pack id collides: `Replace`, which keeps the authored id and its progress, or `Keep both as a copy`, which creates a new fork id using the next available `-2`, `-3`, and so on suffix, records `forkOf` as the original id, and starts progress under the new id. A level id collision SHALL be scoped to its pack id; identical level ids in different packs SHALL coexist. Both replacement and forking SHALL validate atomically and preserve the existing pack if validation fails.38+39+#### Scenario: A pack id collision offers explicit choices40+- **WHEN** a newly loaded pack uses an existing pack id41+- **THEN** the app offers `Replace` and `Keep both as a copy`, and does not change the registry until one choice is made42+43+#### Scenario: Keep both creates a fresh fork44+- **WHEN** the player chooses `Keep both as a copy` for pack `garden-basics`45+- **THEN** the registry installs the imported pack under the next available id such as `garden-basics-2`, records `forkOf: garden-basics`, and starts progress under the fork id46+47+#### Scenario: Replace preserves authored identity48+- **WHEN** the player chooses `Replace` for an existing pack id49+- **THEN** the new pack keeps that authored id and its existing progress key, subject to complete validation50+51+#### Scenario: Same level id in different packs is allowed52+- **WHEN** two registered packs each contain a level id `intro`53+- **THEN** both levels remain addressable by their `{ packId, levelId }` pair54+55+### Requirement: Pack selection SHALL identify the active content56+The app SHALL expose the active pack id and level id to the engine and UI, and SHALL load a selected valid level through the existing engine validation path. A pack selection failure SHALL leave the previously active level playable.57+58+#### Scenario: Selecting a level changes active identity59+- **WHEN** the player selects level `intro` from pack `garden-basics`60+- **THEN** the active selection reports both ids and the engine loads that level's validated data61+62+#### Scenario: Selecting an unknown level is harmless63+- **WHEN** a stale link or UI action requests an unknown pack/level pair64+- **THEN** the app reports the missing identity and retains the current playable level@@ -0,0 +1,93 @@
1+## ADDED Requirements2+3+### Requirement: The app SHALL export a selected pack as a CommonJS file4+The app SHALL export the selected pack's validated data as a `.cjs` file containing a deterministic `module.exports` pack manifest, levels, vocabulary, and palette data. Export SHALL include stable pack and level ids and SHALL not include transient engine state, history, or browser UI state.5+6+#### Scenario: Export round-trips a selected pack7+- **WHEN** the player exports a registered pack8+- **THEN** the downloaded `.cjs` source contains the same pack id, level ids, and validated level data when imported through the trusted local path9+10+#### Scenario: Export excludes a running turn11+- **WHEN** the player exports a pack after moving an entity in a level12+- **THEN** the exported pack contains the authored initial level data and no current positions, undo history, or win state13+14+### Requirement: The app SHALL create and consume a portable data-only URL15+The app SHALL encode a selected pack as canonical data in the URL hash as `#pack=<versioned-payload>`. The encoded payload SHALL be URL-safe and no larger than 16 KiB. The current documented compatibility set contains schema `v1`; decoders SHALL keep that version usable across site revisions, reject unknown future versions before registration, and add a compatibility adapter only when a later version is explicitly documented. Decoding SHALL parse and validate data only; it SHALL never evaluate JavaScript, invoke CommonJS loading, resolve imports, or execute hooks. A malformed, unsupported, or oversized payload SHALL be rejected before registration.16+17+#### Scenario: A shared URL loads data without code execution18+- **WHEN** the player opens a valid URL containing `#pack=<versioned-payload>`19+- **THEN** the app decodes and validates its data, registers the pack with URL origin metadata, and executes no module code20+21+#### Scenario: A tampered URL is rejected22+- **WHEN** a shared URL contains invalid encoding, unknown schema version, malformed JSON, or invalid pack data23+- **THEN** the app reports a share-link validation error and does not register partial content24+25+#### Scenario: An oversized share is rejected26+- **WHEN** the encoded `#pack=` payload exceeds 16 KiB27+- **THEN** the app does not create or register the link and reports that the pack must be exported as `.cjs` or reduced in size28+29+#### Scenario: An older supported link remains usable30+- **WHEN** a later site revision opens a payload version that remains in the documented compatibility set31+- **THEN** the decoder converts it to the current pack schema and registers it without executing code32+33+### Requirement: Local CommonJS import SHALL be explicitly trusted34+The app SHALL provide a separate local-file import action that clearly identifies CommonJS execution as trusted code. The action SHALL pass the file through the existing CommonJS validator and loader, and SHALL retain previously registered packs when loading or execution fails.35+36+#### Scenario: The player confirms a local CJS import37+- **WHEN** the player chooses a `.cjs` file through the trusted local import action38+- **THEN** the app executes it through the documented loader, validates its exported pack, and registers it only after complete validation39+40+#### Scenario: A local module throws41+- **WHEN** the chosen `.cjs` file throws during module evaluation or requests an unsupported import42+- **THEN** the app shows the module error, registers no partial pack, and keeps existing packs available43+44+#### Scenario: URL import never falls through to CommonJS45+- **WHEN** a `#pack=` payload is present in the current URL46+- **THEN** the app uses the data decoder only and does not pass the payload to the trusted local CommonJS loader47+48+### Requirement: A shared URL SHALL enter a pending Gift flow49+When the app opens with a valid `#pack=` hash, it SHALL decode and validate the pack into a pending Gift state but SHALL not install it or replace the active pack automatically. The player SHALL explicitly choose `Keep and play` before registration; declining or leaving the Gift state SHALL preserve the existing registry.50+51+#### Scenario: Opening a valid link does not auto-install52+- **WHEN** the app starts with a valid `#pack=` hash53+- **THEN** it presents the pending pack identity for confirmation and the pack is absent from the registry until `Keep and play` is chosen54+55+#### Scenario: Gift confirmation installs the pack56+- **WHEN** the player chooses `Keep and play` in the pending Gift state57+- **THEN** the app routes the pack through collision handling and installs or forks it only after the explicit choice is complete58+59+### Requirement: Pack management SHALL expose Workshop operations60+The presentation-independent pack service SHALL expose operations equivalent to `listPacks`, `importPackText(text, filename)`, `exportPack(id)`, `packLink(id)`, `decodePackLink(hash)`, `installPack(pack, {replace})`, and `removePack(id)`. Preview and installed packs SHALL use one normalized view shape containing `id`, `name`, `author`, `origin`, and level summaries; a preview SHALL include the full first level in `levels[0]` for a mini preview. A Workshop scene MAY present these operations as Import, Play, Share link, Export, and Remove; visual layout remains outside this capability.61+62+Every player-facing service operation SHALL return a result envelope rather than throw. Successful results SHALL set `ok: true` and include the operation value with `errors: []`; failures SHALL set `ok: false`, preserve the registry, and return `errors` containing objects with a player-language `message`, raw diagnostic `detail`, and an optional `levelId`. `importPackText` and `decodePackLink` SHALL also return `conflict: null` or `{id, name}` before installation when the candidate id already exists. `exportPack`, `packLink`, and `removePack` SHALL report missing ids or protected bundled packs through the same envelope.63+64+#### Scenario: Workshop operations use stable identity65+- **WHEN** the Workshop requests a pack list or selects a pack for Play, Share link, Export, or Remove66+- **THEN** the service resolves the requested pack by stable id and returns a plain-language result or error67+68+#### Scenario: Preview exposes the same shape as an installed pack69+- **WHEN** Workshop previews a valid local module or Gift decodes a valid share link70+- **THEN** the result exposes the normalized pack view, including `name`, `author`, level count, and the full first level, without installing it71+72+#### Scenario: A duplicate is visible before confirmation73+- **WHEN** a previewed pack id already exists74+- **THEN** the result returns `conflict: {id, name}` and leaves installation to the explicit Replace or Keep both choice75+76+#### Scenario: Player errors do not escape the service77+- **WHEN** a player requests export, link, remove, import, or decode with invalid or unavailable content78+- **THEN** the operation returns `ok: false` with readable `message` values and raw `detail` values, and the caller receives no uncaught exception79+80+#### Scenario: Temporary developer controls are hidden by default81+- **WHEN** the app runs without the `?dev` query flag82+- **THEN** interim sidebar controls are absent from the default player-facing flow, while developer-only controls MAY be exposed when `?dev` is present83+84+### Requirement: Sharing actions SHALL provide observable feedback85+The app SHALL report success or failure for pack selection, URL copy, URL decode, `.cjs` export, trusted local import, validation, and collision handling through a readable status channel that does not depend on animation alone.86+87+#### Scenario: Copy succeeds88+- **WHEN** the player requests a share URL and clipboard writing succeeds89+- **THEN** the app reports that the URL is ready and identifies the shared pack90+91+#### Scenario: Clipboard access fails92+- **WHEN** the browser denies clipboard access93+- **THEN** the app reports the failure and exposes the generated URL for manual copying@@ -0,0 +1,12 @@
1+## ADDED Requirements2+3+### Requirement: The app SHALL ship curated additional puzzle packs4+The app SHALL ship additional curated packs alongside the existing three-level MIAU teaching campaign. Each added pack SHALL contain valid levels with stable pack and level ids, dimensions, entities, and title or hint metadata; adding these packs SHALL not change the teaching campaign's identities or order.5+6+#### Scenario: Additional curated content is available7+- **WHEN** the player opens the pack and level selector8+- **THEN** the selector lists the built-in teaching pack and every other valid bundled pack with stable ids9+10+#### Scenario: Imported content does not replace built-ins11+- **WHEN** a valid local or shared pack is loaded12+- **THEN** the three MIAU teaching levels and other valid bundled packs remain selectable@@ -0,0 +1,44 @@
1+## 1. Stable content model2+3+- [x] 1.1 Define normalized pack and level records with authored stable lowercase ids, version, origin metadata, ordered levels, vocabulary, and palette; reject generated identity fallbacks.4+- [x] 1.2 Assign stable pack and level ids to the three MIAU teaching levels without changing their authored puzzle behavior.5+- [x] 1.3 Define and validate the additional curated basic packs and their level ids.6+7+## 2. Pack registry8+9+- [x] 2.1 Implement atomic registration, active selection, origin tracking, and lookup by `{ packId, levelId }`.10+- [x] 2.2 Retain multiple valid bundled and imported packs and preserve earlier packs after failed registration.11+- [x] 2.3 Implement duplicate pack collision rejection and explicit atomic replacement.12+- [x] 2.4 Add registry fixtures for same level ids across different packs, duplicate pack ids, unknown selection, and replacement failure.13+- [x] 2.5 Add explicit `Keep both as a copy` fork handling with suffixed id, `forkOf`, and fresh progress identity.14+15+## 3. CommonJS pack compatibility16+17+- [x] 3.1 Adapt the existing validated local CommonJS manifest to include stable level ids and normalized pack identity.18+- [x] 3.2 Keep local CJS import explicitly trusted and preserve existing pack content after module errors or unsupported imports.19+- [x] 3.3 Add fixtures for loading two valid modules, malformed modules, duplicate pack ids, and palette/vocabulary retention.20+21+## 4. Data-only sharing and export22+23+- [x] 4.1 Implement canonical authored-pack serialization that excludes current positions, move history, win state, and UI state.24+- [x] 4.2 Implement deterministic `.cjs` export and trusted local import round-trip verification.25+- [x] 4.3 Implement `#pack=<versioned-payload>` URL-safe data encoding with a 16 KiB encoded payload cap.26+- [x] 4.4 Decode and validate hash payloads without invoking CommonJS evaluation, imports, or hooks.27+- [x] 4.5 Add fixtures for valid URL round trips, malformed encoding, unknown versions, tampering, oversized payloads, and code-execution isolation.28+- [x] 4.6 Define the forward-version policy for the documented v1 share schema: keep v1 decoding compatible, reject unknown future versions before registration, and add adapters/fixtures only when a later schema is documented.29+30+## 5. Browser integration and feedback31+32+- [x] 5.1 Route bundled pack and level selection through the registry while retaining keyboard/touch movement, dynamic rule changes, and undo behavior.33+- [x] 5.2 Add the E4 service hooks: `listPacks`, `importPackText`, `exportPack`, `packLink`, `decodePackLink`, `installPack`, and `removePack`; keep URL intake separate from CJS execution.34+- [x] 5.3 Normalize preview and installed views, return duplicate `conflict` metadata before installation, and make every player-facing hook return structured no-throw `{ok, errors}` results with readable messages and raw details.35+- [x] 5.4 Move URL intake to a pending Gift confirmation and provide Workshop operations for Import, Play, Share link, Export, and Remove; keep interim sidebar controls behind `?dev`.36+- [x] 5.5 Add browser checks for built-in pack startup, level selection, rule editing, undo/reset, multi-pack retention, trusted CJS import, URL import, export, and failure feedback.37+38+## 6. Verification39+40+- [x] 6.1 Run the full Node test suite and production build with all bundled packs registered.41+- [x] 6.2 Verify engine behavior remains deterministic after pack selection and imported level normalization.42+- [x] 6.3 Review serialized output for stable ordering and absence of transient engine or UI state.43+44+Stage, scene, and visual layout work for Workshop/Gift remains a separate follow-up change owned by the designer.@@ -1,7 +1,9 @@
11 # commonjs-mods Specification22 33 ## Purpose4-TBD - created by archiving change build-dynamic-rule-engine. Update Purpose after archive.4+Define the trusted local CommonJS content format, validation boundary, and5+failure behavior for authored puzzle packs. Remote, data-only sharing is6+specified separately from executing local module source.57 ## Requirements68 ### Requirement: The app SHALL load local CommonJS mods through a validated manifest79 The mod loader SHALL load local CommonJS source and read its `module.exports` object containing a required `manifest` (`id`, `name`, and semantic `version`), one or more `levels`, and optional `vocabulary` and `palette` records. The host SHALL validate the complete export before registering any content and SHALL keep built-in content available when a mod fails validation.@@ -32,3 +34,14 @@ The loader SHALL run local mods with the host's documented CommonJS compatibilit
3234 - **WHEN** a local module throws while loading or requests an unsupported import3335 - **THEN** the app reports the mod error and keeps built-in levels playable3436 37+### Requirement: The loader SHALL report pack identity collisions38+The loader SHALL report a valid module whose pack id is already registered to the pack service, which SHALL offer explicit `Replace` or `Keep both as a copy` handling. Replacement keeps the authored id; a copy receives the next available suffixed id and `forkOf` provenance. Either operation SHALL validate completely before changing the registry.39+40+#### Scenario: Duplicate pack ids are rejected41+- **WHEN** a second local module exports an already registered pack id42+- **THEN** the loader reports the collision without silently replacing the original pack and exposes the two explicit resolution choices43+44+#### Scenario: A forked local pack gets fresh progress identity45+- **WHEN** the player chooses `Keep both as a copy` for a duplicate local module46+- **THEN** the registered pack receives a suffixed id with `forkOf` provenance and progress is keyed by the new id47+@@ -0,0 +1,68 @@
1+# level-packs Specification2+3+## Purpose4+TBD - created by archiving change share-level-packs. Update Purpose after archive.5+## Requirements6+### Requirement: Packs and levels SHALL have stable identities7+Every pack SHALL have a stable lowercase pack id, display name, version, and ordered levels. Every level SHALL have a stable level id unique within its pack. Registry keys SHALL use the pair `{ packId, levelId }`, and display titles SHALL not be used as identity.8+9+#### Scenario: A renamed level keeps its identity10+- **WHEN** a pack changes a level title but retains its pack id and level id11+- **THEN** the registry treats it as the same level identity and updates only display metadata12+13+#### Scenario: Duplicate level ids are rejected within a pack14+- **WHEN** a pack contains two levels with the same level id15+- **THEN** pack validation fails before either level is registered16+17+### Requirement: The app SHALL ship curated bundled packs18+The app SHALL include a curated set of valid bundled packs, including the original three MIAU teaching puzzles as one ordered teaching pack. Bundled packs SHALL be available without a local file or URL import and SHALL pass the same identity and level validation as imported packs.19+20+#### Scenario: The teaching pack is available on a clean start21+- **WHEN** the app starts without imported content22+- **THEN** the registry contains the MIAU teaching pack and its three original levels in stable order23+24+#### Scenario: A bundled pack has invalid data25+- **WHEN** a bundled pack fails validation during startup26+- **THEN** the app reports the pack id and validation error and keeps other valid bundled packs available27+28+### Requirement: The registry SHALL retain multiple loaded packs29+The registry SHALL retain every valid bundled, URL, and explicitly trusted local pack loaded during the session. Each pack SHALL retain its origin metadata, and selecting one pack or level SHALL not discard other valid registered content.30+31+#### Scenario: Two local packs remain selectable32+- **WHEN** the player loads two valid packs with different pack ids33+- **THEN** both packs appear in the pack registry and levels from either pack can be selected34+35+#### Scenario: A failed pack does not remove earlier content36+- **WHEN** a later import fails validation or execution37+- **THEN** previously registered packs remain selectable and the failed pack is absent38+39+### Requirement: The registry SHALL apply an explicit collision policy40+The registry SHALL present an explicit choice when a pack id collides: `Replace`, which keeps the authored id and its progress, or `Keep both as a copy`, which creates a new fork id using the next available `-2`, `-3`, and so on suffix, records `forkOf` as the original id, and starts progress under the new id. A level id collision SHALL be scoped to its pack id; identical level ids in different packs SHALL coexist. Both replacement and forking SHALL validate atomically and preserve the existing pack if validation fails.41+42+#### Scenario: A pack id collision offers explicit choices43+- **WHEN** a newly loaded pack uses an existing pack id44+- **THEN** the app offers `Replace` and `Keep both as a copy`, and does not change the registry until one choice is made45+46+#### Scenario: Keep both creates a fresh fork47+- **WHEN** the player chooses `Keep both as a copy` for pack `garden-basics`48+- **THEN** the registry installs the imported pack under the next available id such as `garden-basics-2`, records `forkOf: garden-basics`, and starts progress under the fork id49+50+#### Scenario: Replace preserves authored identity51+- **WHEN** the player chooses `Replace` for an existing pack id52+- **THEN** the new pack keeps that authored id and its existing progress key, subject to complete validation53+54+#### Scenario: Same level id in different packs is allowed55+- **WHEN** two registered packs each contain a level id `intro`56+- **THEN** both levels remain addressable by their `{ packId, levelId }` pair57+58+### Requirement: Pack selection SHALL identify the active content59+The app SHALL expose the active pack id and level id to the engine and UI, and SHALL load a selected valid level through the existing engine validation path. A pack selection failure SHALL leave the previously active level playable.60+61+#### Scenario: Selecting a level changes active identity62+- **WHEN** the player selects level `intro` from pack `garden-basics`63+- **THEN** the active selection reports both ids and the engine loads that level's validated data64+65+#### Scenario: Selecting an unknown level is harmless66+- **WHEN** a stale link or UI action requests an unknown pack/level pair67+- **THEN** the app reports the missing identity and retains the current playable level68+@@ -0,0 +1,97 @@
1+# level-sharing Specification2+3+## Purpose4+TBD - created by archiving change share-level-packs. Update Purpose after archive.5+## Requirements6+### Requirement: The app SHALL export a selected pack as a CommonJS file7+The app SHALL export the selected pack's validated data as a `.cjs` file containing a deterministic `module.exports` pack manifest, levels, vocabulary, and palette data. Export SHALL include stable pack and level ids and SHALL not include transient engine state, history, or browser UI state.8+9+#### Scenario: Export round-trips a selected pack10+- **WHEN** the player exports a registered pack11+- **THEN** the downloaded `.cjs` source contains the same pack id, level ids, and validated level data when imported through the trusted local path12+13+#### Scenario: Export excludes a running turn14+- **WHEN** the player exports a pack after moving an entity in a level15+- **THEN** the exported pack contains the authored initial level data and no current positions, undo history, or win state16+17+### Requirement: The app SHALL create and consume a portable data-only URL18+The app SHALL encode a selected pack as canonical data in the URL hash as `#pack=<versioned-payload>`. The encoded payload SHALL be URL-safe and no larger than 16 KiB. The current documented compatibility set contains schema `v1`; decoders SHALL keep that version usable across site revisions, reject unknown future versions before registration, and add a compatibility adapter only when a later version is explicitly documented. Decoding SHALL parse and validate data only; it SHALL never evaluate JavaScript, invoke CommonJS loading, resolve imports, or execute hooks. A malformed, unsupported, or oversized payload SHALL be rejected before registration.19+20+#### Scenario: A shared URL loads data without code execution21+- **WHEN** the player opens a valid URL containing `#pack=<versioned-payload>`22+- **THEN** the app decodes and validates its data, registers the pack with URL origin metadata, and executes no module code23+24+#### Scenario: A tampered URL is rejected25+- **WHEN** a shared URL contains invalid encoding, unknown schema version, malformed JSON, or invalid pack data26+- **THEN** the app reports a share-link validation error and does not register partial content27+28+#### Scenario: An oversized share is rejected29+- **WHEN** the encoded `#pack=` payload exceeds 16 KiB30+- **THEN** the app does not create or register the link and reports that the pack must be exported as `.cjs` or reduced in size31+32+#### Scenario: An older supported link remains usable33+- **WHEN** a later site revision opens a payload version that remains in the documented compatibility set34+- **THEN** the decoder converts it to the current pack schema and registers it without executing code35+36+### Requirement: Local CommonJS import SHALL be explicitly trusted37+The app SHALL provide a separate local-file import action that clearly identifies CommonJS execution as trusted code. The action SHALL pass the file through the existing CommonJS validator and loader, and SHALL retain previously registered packs when loading or execution fails.38+39+#### Scenario: The player confirms a local CJS import40+- **WHEN** the player chooses a `.cjs` file through the trusted local import action41+- **THEN** the app executes it through the documented loader, validates its exported pack, and registers it only after complete validation42+43+#### Scenario: A local module throws44+- **WHEN** the chosen `.cjs` file throws during module evaluation or requests an unsupported import45+- **THEN** the app shows the module error, registers no partial pack, and keeps existing packs available46+47+#### Scenario: URL import never falls through to CommonJS48+- **WHEN** a `#pack=` payload is present in the current URL49+- **THEN** the app uses the data decoder only and does not pass the payload to the trusted local CommonJS loader50+51+### Requirement: A shared URL SHALL enter a pending Gift flow52+When the app opens with a valid `#pack=` hash, it SHALL decode and validate the pack into a pending Gift state but SHALL not install it or replace the active pack automatically. The player SHALL explicitly choose `Keep and play` before registration; declining or leaving the Gift state SHALL preserve the existing registry.53+54+#### Scenario: Opening a valid link does not auto-install55+- **WHEN** the app starts with a valid `#pack=` hash56+- **THEN** it presents the pending pack identity for confirmation and the pack is absent from the registry until `Keep and play` is chosen57+58+#### Scenario: Gift confirmation installs the pack59+- **WHEN** the player chooses `Keep and play` in the pending Gift state60+- **THEN** the app routes the pack through collision handling and installs or forks it only after the explicit choice is complete61+62+### Requirement: Pack management SHALL expose Workshop operations63+The presentation-independent pack service SHALL expose operations equivalent to `listPacks`, `importPackText(text, filename)`, `exportPack(id)`, `packLink(id)`, `decodePackLink(hash)`, `installPack(pack, {replace})`, and `removePack(id)`. Preview and installed packs SHALL use one normalized view shape containing `id`, `name`, `author`, `origin`, and level summaries; a preview SHALL include the full first level in `levels[0]` for a mini preview. A Workshop scene MAY present these operations as Import, Play, Share link, Export, and Remove; visual layout remains outside this capability.64+65+Every player-facing service operation SHALL return a result envelope rather than throw. Successful results SHALL set `ok: true` and include the operation value with `errors: []`; failures SHALL set `ok: false`, preserve the registry, and return `errors` containing objects with a player-language `message`, raw diagnostic `detail`, and an optional `levelId`. `importPackText` and `decodePackLink` SHALL also return `conflict: null` or `{id, name}` before installation when the candidate id already exists. `exportPack`, `packLink`, and `removePack` SHALL report missing ids or protected bundled packs through the same envelope.66+67+#### Scenario: Workshop operations use stable identity68+- **WHEN** the Workshop requests a pack list or selects a pack for Play, Share link, Export, or Remove69+- **THEN** the service resolves the requested pack by stable id and returns a plain-language result or error70+71+#### Scenario: Preview exposes the same shape as an installed pack72+- **WHEN** Workshop previews a valid local module or Gift decodes a valid share link73+- **THEN** the result exposes the normalized pack view, including `name`, `author`, level count, and the full first level, without installing it74+75+#### Scenario: A duplicate is visible before confirmation76+- **WHEN** a previewed pack id already exists77+- **THEN** the result returns `conflict: {id, name}` and leaves installation to the explicit Replace or Keep both choice78+79+#### Scenario: Player errors do not escape the service80+- **WHEN** a player requests export, link, remove, import, or decode with invalid or unavailable content81+- **THEN** the operation returns `ok: false` with readable `message` values and raw `detail` values, and the caller receives no uncaught exception82+83+#### Scenario: Temporary developer controls are hidden by default84+- **WHEN** the app runs without the `?dev` query flag85+- **THEN** interim sidebar controls are absent from the default player-facing flow, while developer-only controls MAY be exposed when `?dev` is present86+87+### Requirement: Sharing actions SHALL provide observable feedback88+The app SHALL report success or failure for pack selection, URL copy, URL decode, `.cjs` export, trusted local import, validation, and collision handling through a readable status channel that does not depend on animation alone.89+90+#### Scenario: Copy succeeds91+- **WHEN** the player requests a share URL and clipboard writing succeeds92+- **THEN** the app reports that the URL is ready and identifies the shared pack93+94+#### Scenario: Clipboard access fails95+- **WHEN** the browser denies clipboard access96+- **THEN** the app reports the failure and exposes the generated URL for manual copying97+@@ -1,7 +1,8 @@
11 # playable-puzzles Specification22 33 ## Purpose4-TBD - created by archiving change build-dynamic-rule-engine. Update Purpose after archive.4+Define the initial ordered MIAU teaching campaign and the controls needed to5+play, undo, reset, and advance through its levels.56 ## Requirements67 ### Requirement: The app SHALL provide three built-in MIAU teaching puzzles78 The app SHALL ship exactly three deterministic ordered built-in levels using `MIAU` as the initial controlled noun. The puzzles SHALL cover movement and pushing, `YOU`, `PUSH` and `STOP`, word manipulation, `WIN`, and `AND` subject expansion; each level SHALL include dimensions, entities, and title or hint metadata.@@ -36,3 +37,14 @@ The app SHALL reject a built-in level whose dimensions, coordinates, entity ids,
3637 - **WHEN** a level definition places an entity outside its declared width or height3738 - **THEN** the level does not start and validation reports the offending entity3839 40+### Requirement: The app SHALL ship curated additional puzzle packs41+The app SHALL ship additional curated packs alongside the existing three-level MIAU teaching campaign. Each added pack SHALL contain valid levels with stable pack and level ids, dimensions, entities, and title or hint metadata; adding these packs SHALL not change the teaching campaign's identities or order.42+43+#### Scenario: Additional curated content is available44+- **WHEN** the player opens the pack and level selector45+- **THEN** the selector lists the built-in teaching pack and every other valid bundled pack with stable ids46+47+#### Scenario: Imported content does not replace built-ins48+- **WHEN** a valid local or shared pack is loaded49+- **THEN** the three MIAU teaching levels and other valid bundled packs remain selectable50+