commonjs-mods Specification
Purpose
Define the trusted local CommonJS content format, validation boundary, and failure behavior for authored puzzle packs. Remote, data-only sharing is specified separately from executing local module source.
Requirements
Requirement: The app SHALL load local CommonJS mods through a validated manifest
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.
Scenario: A valid mod registers a level
- WHEN a local module exports a valid manifest, level definition, vocabulary, and palette
- THEN the loader registers its level and uses its palette for noun presentation under that mod id
Scenario: A malformed mod is rejected
- WHEN a module has no id, duplicate identifiers, invalid coordinates, or unsupported vocabulary values
- THEN the loader rejects it with a diagnostic and does not partially register its content
Requirement: Mods SHALL use the host engine contract
Mods 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.
Scenario: A mod adds a noun and emoji
- WHEN a manifest declares noun
FENNand its emoji representation - THEN levels from that mod can place
FENNobjects and the presentation uses the declared mapping
Scenario: A mod cannot bypass the engine state contract
- WHEN a mod supplies malformed level state or attempts to change state through data outside the documented manifest fields
- THEN the host registers only validated manifest, vocabulary, palette, and level data and keeps engine state under host control
Requirement: Mod loading SHALL be deterministic for the local MVP
The 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.
Scenario: A mod error does not break built-in play
- WHEN a local module throws while loading or requests an unsupported import
- THEN the app reports the mod error and keeps built-in levels playable
Requirement: The loader SHALL report pack identity collisions
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.
Scenario: Duplicate pack ids are rejected
- WHEN a second local module exports an already registered pack id
- THEN the loader reports the collision without silently replacing the original pack and exposes the two explicit resolution choices
Scenario: A forked local pack gets fresh progress identity
- WHEN the player chooses
Keep both as a copyfor a duplicate local module - THEN the registered pack receives a suffixed id with
forkOfprovenance and progress is keyed by the new id
Requirement: Pack validation SHALL distinguish unknown rule words from unsupported mechanics
The 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.
Scenario: Unknown predicate receives a note
- WHEN a proof contains
DOOR IS SHUTwithSHUTabsent from that proof's implemented properties and nouns - THEN local preview succeeds, reports a note identifying
SHUT, the proof, and the noun-transform consequence, without blocking installation
Scenario: Loose unknown word receives a note
- WHEN a proof places a loose
SHUTword that can be moved into a sentence but declares noSHUTbehavior or noun in that proof - THEN preview reports a note for
SHUTeven before the sentence is formed
Scenario: Known noun transformation remains valid
- WHEN a proof contains
ROCK IS MIAUand places aMIAUobject in that proof - THEN no unknown-word note is returned for
MIAU
Scenario: Pack-defined transformation target is known
- WHEN one proof contains
ROCK IS GEM, the pack palette or vocabulary definesGEM, and no GEM object starts in that proof - THEN no unknown-word note is returned for
GEM
Scenario: Pack-wide property and thing ambiguity receives a note
- WHEN one proof declares
SHUTas a custom property and another proof usesSHUTas an object noun and places its word - THEN preview reports a nonfatal ambiguity note for
SHUTeven if the pack palette or vocabulary names it
Scenario: Declared automaton property remains semantic
- WHEN a level declares
TICKthroughautomata[].propertyand containsMOUSE IS TICK - THEN no unknown-predicate warning is returned for
TICK
Scenario: Property declared only in another proof receives a note
- WHEN one proof declares
TICKthrough automata and another proof containsCLOCK IS TICKwithout a TICK automaton - THEN the second proof reports that TICK has no property behavior there unless the pack defines TICK as a noun
Scenario: Property noun collision is rejected
- WHEN a level declares
TICKas an automaton property and places an object whose noun isTICK - THEN the loader rejects the level with the object's path and does not register part of the pack
Scenario: Unsupported feature is rejected
- WHEN a pack declares
manifest.features: ['pairs']in a host that supports onlyautomata - THEN the loader rejects the pack with a readable newer-version message before any level is installed
Scenario: Legacy automata pack remains usable
- WHEN a valid existing pack contains automata but omits
manifest.features - THEN the current loader accepts it and preserves its automata behavior