1# commonjs-mods Specification23## Purpose4Define the trusted local CommonJS content format, validation boundary, and5failure behavior for authored puzzle packs. Remote, data-only sharing is6specified separately from executing local module source.7## Requirements8### Requirement: The app SHALL load local CommonJS mods through a validated manifest9The 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.1011#### Scenario: A valid mod registers a level12- **WHEN** a local module exports a valid manifest, level definition, vocabulary, and palette13- **THEN** the loader registers its level and uses its palette for noun presentation under that mod id1415#### Scenario: A malformed mod is rejected16- **WHEN** a module has no id, duplicate identifiers, invalid coordinates, or unsupported vocabulary values17- **THEN** the loader rejects it with a diagnostic and does not partially register its content1819### Requirement: Mods SHALL use the host engine contract20Mods SHALL define vocabulary, palette, and level data through the exported object. Mods SHALL NOT directly mutate the active rule table, DOM, renderer scheduler, or persisted engine state; the host SHALL pass validated level data to the engine.2122#### Scenario: A mod adds a noun and emoji23- **WHEN** a manifest declares noun `FENN` and its emoji representation24- **THEN** levels from that mod can place `FENN` objects and the presentation uses the declared mapping2526#### Scenario: A mod cannot bypass the engine state contract27- **WHEN** a mod supplies malformed level state or attempts to change state through data outside the documented manifest fields28- **THEN** the host registers only validated manifest, vocabulary, palette, and level data and keeps engine state under host control2930### Requirement: Mod loading SHALL be deterministic for the local MVP31The loader SHALL run local mods with the host's documented CommonJS compatibility layer, allow only the host's supported helper import, and avoid URL or package resolution. Module execution and validation errors SHALL be isolated to the affected mod and shown in diagnostics.3233#### Scenario: A mod error does not break built-in play34- **WHEN** a local module throws while loading or requests an unsupported import35- **THEN** the app reports the mod error and keeps built-in levels playable3637### Requirement: The loader SHALL report pack identity collisions38The 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.3940#### 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 choices4344#### 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 id4748### Requirement: Pack validation SHALL distinguish unknown rule words from unsupported mechanics49The loader SHALL accept optional `manifest.features` as a list of unique supported feature identifiers and SHALL reject unknown required features before registration with an upgrade message. A level SHALL reject an object noun equal to a custom property implemented by that level. A word SHALL count as a known noun when it is placed or named as an automaton subject in that proof, or listed in the pack's palette or vocabulary. Other words that are neither core words nor implemented properties in that proof SHALL receive a nonfatal note with word and proof context, whether or not they currently complete a sentence. A word declared as a property in one proof and used as an object noun in another SHALL receive an ambiguity note even if it is in the pack palette or vocabulary. Existing packs with automata and no feature list SHALL remain valid.5051#### Scenario: Unknown predicate receives a note52- **WHEN** a proof contains `DOOR IS SHUT` with `SHUT` absent from that proof's implemented properties and nouns53- **THEN** local preview succeeds, reports a note identifying `SHUT`, the proof, and the noun-transform consequence, without blocking installation5455#### Scenario: Loose unknown word receives a note56- **WHEN** a proof places a loose `SHUT` word that can be moved into a sentence but declares no `SHUT` behavior or noun in that proof57- **THEN** preview reports a note for `SHUT` even before the sentence is formed5859#### Scenario: Known noun transformation remains valid60- **WHEN** a proof contains `ROCK IS MIAU` and places a `MIAU` object in that proof61- **THEN** no unknown-word note is returned for `MIAU`6263#### Scenario: Pack-defined transformation target is known64- **WHEN** one proof contains `ROCK IS GEM`, the pack palette or vocabulary defines `GEM`, and no GEM object starts in that proof65- **THEN** no unknown-word note is returned for `GEM`6667#### Scenario: Pack-wide property and thing ambiguity receives a note68- **WHEN** one proof declares `SHUT` as a custom property and another proof uses `SHUT` as an object noun and places its word69- **THEN** preview reports a nonfatal ambiguity note for `SHUT` even if the pack palette or vocabulary names it7071#### Scenario: Declared automaton property remains semantic72- **WHEN** a level declares `TICK` through `automata[].property` and contains `MOUSE IS TICK`73- **THEN** no unknown-predicate warning is returned for `TICK`7475#### Scenario: Property declared only in another proof receives a note76- **WHEN** one proof declares `TICK` through automata and another proof contains `CLOCK IS TICK` without a TICK automaton77- **THEN** the second proof reports that TICK has no property behavior there unless the pack defines TICK as a noun7879#### Scenario: Property noun collision is rejected80- **WHEN** a level declares `TICK` as an automaton property and places an object whose noun is `TICK`81- **THEN** the loader rejects the level with the object's path and does not register part of the pack8283#### Scenario: Unsupported feature is rejected84- **WHEN** a pack declares `manifest.features: ['pairs']` in a host that supports only `automata`85- **THEN** the loader rejects the pack with a readable newer-version message before any level is installed8687#### Scenario: Legacy automata pack remains usable88- **WHEN** a valid existing pack contains automata but omits `manifest.features`89- **THEN** the current loader accepts it and preserves its automata behavior90