clone

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

main
miau-is-true / openspec / specs / level-sharing / spec.md
spec.md 13.2K rendered
1# level-sharing Specification23## Purpose4TBD - created by archiving change share-level-packs. Update Purpose after archive.5## Requirements6### Requirement: The app SHALL export a selected pack as a CommonJS file7The 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.89#### 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 path1213#### 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 state1617### Requirement: The app SHALL create and consume a portable data-only URL18The 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.1920#### 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 code2324#### 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 content2728#### 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 size3132#### 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 code3536### Requirement: Local CommonJS import SHALL be explicitly trusted37The 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.3839#### 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 validation4243#### 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 available4647#### 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 loader5051### Requirement: A shared URL SHALL enter a pending Gift flow52When 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.5354#### 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 chosen5758#### 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 complete6162### Requirement: Pack management SHALL expose Workshop operations63The 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.6465Every 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.6667#### 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 error7071#### 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 it7475#### 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 choice7879#### 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 exception8283#### 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 present8687### Requirement: Sharing actions SHALL provide observable feedback88The 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.8990#### 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 pack9394#### Scenario: Clipboard access fails95- **WHEN** the browser denies clipboard access96- **THEN** the app reports the failure and exposes the generated URL for manual copying9798### Requirement: Pack previews SHALL expose nonfatal rule warnings without serializing them99Local CommonJS previews and data-only shared-link previews SHALL apply the same supported-feature and property validation. A successful preview SHALL return a bounded `warnings` collection separate from `errors: []`, with `word`, `levelId`, `levelTitle`, optional `sentence`, a player-language `message`, and technical `detail`. Warnings SHALL not prevent installation and SHALL not be serialized into exported CommonJS source, shared payloads, installed pack data, or engine snapshots. Unknown required features SHALL fail atomically before installation.100101#### Scenario: Local and shared preview agree on warnings102- **WHEN** the same valid pack with an initially formed unknown predicate is previewed from a local module and a shared link103- **THEN** both previews succeed and expose the same warning context without installing the pack104105#### Scenario: Diagnostics stay outside portable data106- **WHEN** a warned pack is exported as CommonJS or encoded into a share URL and decoded again107- **THEN** the authored data remains valid, the warning is recomputed on preview, and no transient diagnostic field appears in the exported data108109#### Scenario: Unsupported shared feature fails before Gift installation110- **WHEN** a shared payload declares a feature unsupported by the current host111- **THEN** the decoder returns a player-readable error and Gift cannot install partial content112113### Requirement: Pack manifests SHALL declare or derive a semantic rules revision114115The loader SHALL validate `manifest.rules` as a positive integer no greater116than the host's current rules revision. The host's current revision SHALL be117`2`, representing the post-W6 precedence, transform-reading, and contact-order118contract. An omitted field SHALL mean legacy revision `1` and SHALL be119materialized in normalized pack data. A value greater than `2` SHALL reject the120pack before any catalog mutation.121122All pack-service manifest projections, including `manifestFromView()` and123`createPackView()`, SHALL preserve the effective revision. A view round trip or124persisted catalog record SHALL not silently remove `manifest.rules`.125126#### Scenario: A current pack loads without a compatibility note127128- **WHEN** a valid pack declares `manifest.rules: 2`129- **THEN** local preview succeeds with `errors: []`, no rules-version warning,130 and normalized data retains `manifest.rules: 2`131132#### Scenario: An omitted revision uses legacy semantics metadata133134- **WHEN** a valid imported pack omits `manifest.rules`135- **THEN** preview succeeds, normalized data contains `manifest.rules: 1`, and136 the catalog remains unchanged until the caller explicitly installs it137138#### Scenario: A pack view preserves its revision139140- **WHEN** a legacy pack is previewed, installed, and projected back through a141 pack-service view142- **THEN** the view and any later export/share operation retain143 `manifest.rules: 1` rather than rebuilding a manifest without the field144145#### Scenario: Malformed revisions fail validation with player copy146147- **WHEN** a pack declares `manifest.rules` as `0`, `-1`, `1.5`, or a string148- **THEN** preview fails with the player message `This pack's rules version149 isn't valid, so it can't be installed.`, preserves the technical150 `manifest.rules` validation detail, and does not install or partially register151 the pack152153#### Scenario: Future revisions fail atomically with player copy154155- **WHEN** a pack declares `manifest.rules: 3`156- **THEN** preview fails with the player message `This pack needs a newer157 version of Miau Is True. Update the game to play it.`, preserves the158 technical unsupported-rules-revision detail, and the catalog contains none159 of that pack's levels160161### Requirement: Older rules SHALL produce one nonblocking W11 warning162163The preview service SHALL return one bounded, nonblocking warning when a164pack's effective rules revision is below the host revision.165166When a pack's effective rules revision is below the host revision, local167CommonJS and data-only shared previews SHALL return one warning, while keeping168`errors: []` and allowing explicit installation. The warning SHALL have169`word: 'RULES'`, `levelId: null`, `levelTitle: null`, `sentence: null`, and the170player message `This pack was made for older rules; some proofs may play differently.`171Its technical detail SHALL identify the declared or derived revision and the172current host revision. The host SHALL continue to run current rules semantics;173the warning is not a legacy simulation mode.174175#### Scenario: An explicit old revision is surfaced once176177- **WHEN** a valid pack declares `manifest.rules: 1` and contains seven proofs178- **THEN** preview succeeds with one pack-level warning, the exact player179 message, and detail showing `1` versus current `2`; it does not emit seven180 duplicate warnings181182#### Scenario: Local and shared previews agree183184- **WHEN** the same legacy pack is previewed from CommonJS and from a data-only185 shared link186- **THEN** both previews expose the same warning envelope and normalized187 `manifest.rules: 1` without installing the pack as a side effect188189#### Scenario: Warnings remain transient190191- **WHEN** a warned legacy pack is installed, exported, or encoded as a shared192 link193- **THEN** its normalized manifest retains only `rules: 1`, no warning field is194 serialized, and the warning is recomputed when the pack is previewed again195