1## ADDED Requirements23### Requirement: The app SHALL export a selected pack as a CommonJS file4The 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.56#### 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 path910#### 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 state1314### Requirement: The app SHALL create and consume a portable data-only URL15The 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.1617#### 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 code2021#### 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 content2425#### 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 size2829#### 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 code3233### Requirement: Local CommonJS import SHALL be explicitly trusted34The 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.3536#### 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 validation3940#### 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 available4344#### 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 loader4748### Requirement: A shared URL SHALL enter a pending Gift flow49When 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.5051#### 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 chosen5455#### 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 complete5859### Requirement: Pack management SHALL expose Workshop operations60The 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.6162Every 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.6364#### 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 error6768#### 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 it7172#### 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 choice7576#### 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 exception7980#### 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 present8384### Requirement: Sharing actions SHALL provide observable feedback85The 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.8687#### 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 pack9091#### Scenario: Clipboard access fails92- **WHEN** the browser denies clipboard access93- **THEN** the app reports the failure and exposes the generated URL for manual copying