Blaze v0.1: language and compilation contract
Status: proposed design for #157. No parser or working compiler ships with this document. Human review establishes this contract before #158. The coverage inventory and lowering witnesses are part of it. The Ink prior-art decisions explain this second pass and the paired branching example.
Follow-on authoring direction (2026-09-21)
The phase-aware authoring addendum defines the target for the next three milestone issues; the Eliza design trial explains why. The baseline syntax below describes the initial compiler, not a requirement to mirror every runtime field forever. For the follow-on work, the addendum takes precedence on local/inferred IDs, caption defaults, single-authored utterances, media companions, and bounded inspect/source-edit commands. Existing runtime semantics, stable identities, explicit assembly and deterministic output remain mandatory. Issue #167 implements the addendum's source syntax and media companion checks; inspect/edit commands remain unimplemented #168/#169 scope.
Purpose and boundary
Blaze authors bounded, statically composed adventure modules. The intended agent workflow is to read one module, its typed imports and exported IDs, make a local edit, then check the whole entry. Actual context locality must be measured in #159/#161, not inferred from the presence of modules. The result is an ordinary WorldBundle consumed by the unchanged validator, reducer and player. Modules organize authorship, not runtime instances.
Choose one deliberately small syntax: typed, named declarations; explicit imports; readable tagged conditions/effects; named fields and ordered lists. Records retain runtime field names to keep lowering inspectable. Unlike a renamed JSON dump, modules expose typed symbols, reusable substructures and explicit identity references; authors need not manipulate one global object. There are no macros, functions, interpolation, arithmetic, loops or expression evaluation. Narrative prose remains JSON-escaped text, not a second embedded language.
Pin the runtime contract and parity oracle to a1b1c883815927990a61ced953382a3322e4f21e. This includes the fully composed Adirondack Mystery export, not merely the base room literal. Do not silently track main. #156 selected this direction; its proposed action-rule proof was not done.
Syntax
UTF-8, optional initial BOM, LF or CRLF input; output is UTF-8 without BOM. Whitespace and // comments outside strings are ignored. Names are ASCII [A-Za-z_][A-Za-z0-9_]*, case-sensitive; keywords cannot be aliases. Strings and numbers use JSON lexical rules; reject non-finite numbers and invalid Unicode surrogates. No single-quoted or multiline literal strings: use \n, \", \\. Duplicate fields are errors. Trailing list commas are permitted; fields always end with semicolons. Record keys can be strings (for maps keyed by runtime IDs).
module = "blaze", string, ";", "module", string, ";", { statement } ;
statement = import | declaration | data | entry ;
import = "import", "{", binding, { ",", binding }, "}", "from", string, ";" ;
binding = type, name, [ "as", name ] ;
declaration = [ "export" ], type, name, record ;
data = [ "export" ], "data", type, name, "from", string,
"sha256", string, ";" ;
entry = "entry", name, ";" ;
record = "{", { (name | string), ":", value, ";" }, "}" ;
value = string | number | "true" | "false" | "null"
| record | list | "@", name | "id", "(", name, ")"
| tag, record | gather ;
gather = "gather", "id", "(", name, ")", "[", branch,
{ ",", branch }, [ "," ], "]" ;
branch = record | "@", name ;
list = "[", [ value, { ",", value }, [ "," ] ], "]" ;type is a registered name in the table below, not a user-defined class. tag is a Condition or Effect discriminant, allowed only in its corresponding typed position. not { condition: has_item { itemId: id(key); }; } lowers recursively into objects with type fields. A tag record must not also specify type. Tagged syntax has no contextual effects or hidden evaluation.
@name embeds a declaration's checked value; id(name) emits its explicit id string and is only valid for ID-bearing declarations of the expected kind. For types without id (world, manifest, map, configuration) use @, not id. Literal IDs remain legal and are checked against the same reference rules. A scalar field cannot receive @item; a room ID cannot receive id(item). Record fields are exactly those of their registered runtime type, including optional fields; unknown fields, absent required fields and wrong union variants are errors. No automatic defaults, captions, IDs or manifest lists. Adjacency is authored explicitly, including the one shared-edge gather form below. null is accepted only where the pinned target type allows it; absence is not null. No authored undefined, NaN or executable values.
Local branching and gathers
gather is allowed only as a DialogueNode's choices value, with one or more inline choice records or @choice embeddings. It is not a general list operator, not allowed on legacy Npc.dialogue nodes, and not allowed in a conversation owned by a StoryBeat. Plain choice lists remain valid and unchanged.
choices: gather id(afterQuestions) [
{ id: "surveyor.ask_map"; text: "Ask about the map";
captionText: "Ask about the map"; responseText: "The route follows the brook."; },
{ id: "surveyor.ask_mine"; text: "Ask about the mine";
captionText: "Ask about the mine"; responseText: "The mine entrance has collapsed."; }
];afterQuestions must resolve to a typed node with an authored stable ID and be selected into the same emitted conversation as the source node. No target node is implicitly inserted. The conversation's initialNodeId and ordered nodes remain explicit. This permits local alternatives without declaring a scene for each response; one ordinary named continuation still exists.
Lowering copies each checked choice in source order and appends nextNodeId with the target node's ID. Never mutate an embedded shared choice declaration. An entry already containing nextNodeId is an error, even if equal to the gather target; use a plain list for mixed destinations. All normal choice shape/reference checks still apply. Gather creates no synthetic choice/node/line IDs, flags, effects, decisions, default values or extra interactions. Choice and line identities, when used, remain authored because saves and decision records expose them. Renaming aliases/files while repairing imports cannot change the result.
This is shared-edge sugar, not text fall-through. The existing reducer applies an accepted choice's effects in order, emits its response and decision events, and updates dialogueProgress. It does not emit the target's npcText as part of that choice. A later talk/read requests the next prompt. No hidden Continue choice, auto-play, nested weave, tunnel or call stack is implied. A self-target is ordinary repeatable AK dialogue, not Ink's once-only choice behavior. More complex branches use explicit ordinary nodes and nextNodeId edges.
The appended edge's source-map origin is the gather target span with the relevant choice span as a related location; existing copied fields retain their origins. The appended field has deterministic position, which can differ from a manually wired fixed-shape choice record subject to the unchanged parity rules below.
Declarations and lowering
Each registered declaration has the complete public shape of its pinned type in src/domain/types.ts, not a subset inferred from the examples. The compiler implements closed runtime shape checking; TypeScript interfaces alone do not validate input. The World validator is an additional gate.
| Blaze type | Runtime type / destination |
|---|---|
world | WorldBundle (entry only) |
manifest | WorldManifest / world.manifest |
room, hotspot, action, exit, item | Room, Hotspot, HotspotAction, Exit, Item |
npc, character, presence | Npc, CharacterDefinition, CharacterPresence |
dialogue, node, choice, line | DialogueConversation, DialogueNode, DialogueChoice, DialogueLine |
story_beat | StoryBeat / world.storyBeats[] |
room_asset, visual_binding, visual_mask | RoomAsset, SceneVisualBinding, VisualMask |
voice_reference, generated_tts | VoiceReference, GeneratedTts |
party, party_member, party_task | AuthoredPartyConfiguration, AuthoredPartyMemberState, AuthoredPartyTaskDefinition |
regional_map, place, connection | RegionalMapDefinition, RegionalMapPlace, RegionalMapConnection |
initial_state | InitialWorldState |
expansion_packet, frontier_manifest, story_obligation | ExpansionPacket, FrontierManifest, StoryPacketObligation |
resort, resort_packet | ResortConfiguration, ResortExpansionPacket |
geometry, media_identity, moltazine_metadata | Record<HotspotId, NormalizedRect>, SceneVisualMediaIdentity, MoltazineRoomMetadata |
Other nested interfaces (rights, decision metadata, provenance, envelopes, requirements, etc.) use inline checked records in the containing type. References may also embed any compatible registered declaration. Embedding is value reuse, not inheritance, spread or patching. Two embeddings at distinct owned entity positions still have to satisfy runtime identity uniqueness.
All pinned Condition and Effect variants are supported as tags, even those absent from the fixture; the inventory names the observed subset. All five HotspotVerb values are supported. This does not define new Intent commands, scheduler rules, new effect primitives or an alternate condition evaluator. Runtime behavior is in reducer.ts, dialogueChoices.ts, party.ts and storyBeats.ts.
Module linkage, selection and identity
Every file starts blaze "0.1"; module "unique.logical.name";. A module name is only a compiler namespace, never a prefix silently added to runtime IDs. Exactly one entry worldAlias; appears, in the file passed to the CLI; imported files cannot declare entries. That world explicitly selects all content using ordered lists and references. Loading an import does not append its declarations anywhere. No directory discovery, auto-manifest generation or implicit transitive emission.
Imports name exported symbols and their types; optional aliases avoid local collisions. Local paths resolve relative to the importing file, must end .blaze, and must remain inside the entry file's directory tree after realpath resolution. No absolute paths, URL/package imports or symlink escapes. Reject import cycles with the complete cycle and import locations, even if currently unused; also reject recursive value-expansion cycles through @ embeddings. An id(node) reference resolves an authored scalar identity without expanding that node; id-only control-flow loops, including self-gathers, are not expansion cycles. Imported module logical names must be unique; a canonical file imported twice is loaded once. Repeated export names, local bindings and fields fail rather than last-write-wins.
The output is the selected entry value, not the module discovery order. Check all reachable source declarations for shape/link errors; check runtime identity and closure on emitted content. Moving or renaming files while fixing imports must not change output bytes. IDs are authored strings, independent of aliases, paths and prose. Revisions are explicit where the existing type has them; don't invent revisions for items or rooms that lack that field.
Identity uniqueness uses target domains: rooms/items/characters/conversations/ story beats are world-scoped; exit IDs remain room-local (the catalog legitimately reuses to_porch); assets pair identity with assetVersion and external refs with kind/key/revision. Nodes and choices obey their existing conversation/legacy NPC contracts. Reusing the same ID as a reference is not a duplicate declaration. The compiler must not reject legitimate legacy NPC and modern dialogue projections merely for containing equivalent content. The existing validator defines remaining identity constraints, including story beat conversation ownership. Missing IDs are errors in closed local domains, including flag/knowledge rules enforced by the validator; external WorldObjectRef values are pinned references, not demands to fetch or inline an external object. Planned expansion destinations retain the existing validator's allowance; they must not be mistaken for missing published rooms. Preserve manifest list order independently from collection order.
For agent edits: inspect a module's public types/IDs and dependency contracts; edit its private declarations; run check on the entry. A successful local parse alone never certifies global closure or behavioral compatibility.
Public interfaces for agent-owned snippets
The small structural primitives are module, conversation and local node. Story, act, thread, mission and scene are useful directory/document vocabulary, not five new language types or runtime instances. Ink's knots/stitches suggest restraint; they do not prove a two-level hierarchy fits every game.
Source symbol privacy is enforced: another module imports only explicitly exported, typed declarations. Runtime IDs are not private capabilities. Literal external IDs remain legal for pinned-world compatibility and are checked on whole-world assembly. Thus v0.1 cannot promise contract-only runtime linking or forbid every reference to an unexported entity's literal ID. Its import DAG is stricter than Ink INCLUDE's shared namespace, but it is not a security boundary.
Document each owned module's public interface alongside its source:
| Interface | Example / enforcement |
|---|---|
| Inputs | Typed imports plus a list of literal external IDs, their owners and required revisions where supported. Compiler checks types/closure, not ownership prose. |
| Entry | Dialogue initialNodeId, or existing StoryBeat requirements/actors/location. Only gates actually consumed by the runtime enforce entry; a documented requires sentence does not. |
| Outputs | Explicit flag values, knowledge grants and authored decision IDs on accepted branches. Ordinary dialogue does not acquire StoryBeat once-only rules. |
| Parent consumption | Existing flag_is, character_knows or decision_recorded conditions; no return address or callback. |
| Verification | Owning entry's check/build and scenario obligations; local parse alone is insufficient. |
The reunion witness already documents this model: disclosure's persistent flags are owned by its StoryBeat; later conditions consume consequences without inspecting private prose. For a future surveyor/climb-mountain snippet, document route knowledge and actor availability as inputs, an existing conversation/beat as entry, summit_notes as an explicitly granted output on completed branches, and an explicit abandoned outcome if authored. These are interface descriptions, not new entry offered / exit completed grammar or a general mission scheduler. A claim that completion provides knowledge requires every relevant accepted branch to grant it, with recipient and runtime state requirements satisfied. A decision merely records a choice; it does not automatically certify a completed mission. Exit means an observable outcome permitting another activity, not a stack return. Full contract syntax and contract-only linking need a separate future proposal.
Immutable data boundary
Gameplay (rooms, choices, conditions/effects, party, tasks, maps, initial state, expansion puzzles and catalog room interactions) must be Blaze declarations. No data world, data room, raw JSON fragments, spreads, JSON pointers or arbitrary schema names. A resort catalog is not an excuse to hide a room graph in JSON.
The finite data allowlist is room_asset, visual_mask, geometry, media_identity, moltazine_metadata, voice_reference, generated_tts. These retain bulky accepted media geometry, mask validation, rights, transcripts, receipts and audio metadata without pretending to author images or synthesise voices. They cannot contain Conditions, Effects or SceneVisualVariant selection rules. visual_binding is inline-only because surrounding state-selected rules belong in Blaze. Static resort receipt/media fields can use the allowed types; resort_packet.room and its mechanics cannot. No generic catalog import is needed.
Syntax: data geometry lobbyGeometry from "./data/lobby-geometry.json" sha256 "<64 lowercase hex characters>"; (the placeholder here is explanatory, not valid source). The actual file is exactly one JSON value of the named pinned type. Read raw bytes once, check SHA-256, then strict JSON parse with duplicate-key rejection and complete recursive shape checking. Reject unknown/private fields, wrong union tags, schema/revision drift and unresolved references by the same rules as inline data. Paths have the same root containment policy as imports, end .json, and never trigger downloads; asset URIs inside data are inert strings. Hash pins are source-owned; a changed payload requires an explicit new digest. AssetVersion, contentHash, media identity and geometry still must agree under the existing validator. A digest proves byte identity, not rights or publication.
Record errors at the Blaze data declaration and related JSON file/path (and JSON line/column where available). Keep a lowering source map for every emitted field, including embedded values and data nested paths; validator paths using semantic IDs must map back as well as index-based paths. Compiling a FrontierManifest or accepted receipt never confers authority to publish/activate it. Runtime/broker trust boundaries remain unchanged; no compiler-side registration or I/O beyond local source/data/output files.
Story grouping is not a mission runtime
The reunion module owns dialogue and a StoryBeat with explicit persistentFlagIds. resolveStoryBeat uses accepted DecisionRecords as its once-only ledger; it checks active actor, location, participant availability and ordered requirements. The validator requires one node with 1–16 choices, terminal irreversible decisions, attributed player lines and exactly one write of every owned persistent output per choice. No other action/choice/task may overwrite those outputs. Blaze emits this existing contract unchanged; it does not simulate eligibility at compile time.
An outcome can make a later dialogue choice visible through flag_is; there is no call/return stack, hidden mission status, callbacks or suspension of other characters. The followup persists knowledge and flags through convergence.
Conditions, state and metadata
Use the existing tagged Condition/Effect syntax, not an English expression interpreter. For example, in a checked choice record:
visibilityConditions: [ character_knows {
characterId: "kate"; knowledgeId: "surveyor.hidden_mark";
} ];
availabilityConditions: [ all { conditions: [
any { conditions: [ has_item { itemId: "silver_key"; },
character_has_item { characterId: "kate"; itemId: "silver_key"; } ]; },
day_at_most { day: 4; },
not { condition: flag_is { flagId: "surveyor.closed"; value: true; }; }
]; } ];
blockedText: "Bring the key before Day 5, while the route is open.";These are illustrative external IDs, not additional fixture declarations. Visibility hides a choice; availability controls whether a visible choice can be accepted and uses existing blocked text. Preserve legacy conditions behavior; never silently rewrite it as visibility. has_item uses existing inventory semantics, whereas character_has_item names an actor. Action declarations use their existing conditions/effects too; this does not resurrect #156's deferred rulebook runtime.
No automatic seen, visit counters or mission-completed variables. Use existing visited_room { roomId: ...; }, decision_recorded { decisionId: ...; }, character_knows or flag_is where their meanings match, with explicit authored effects when necessary. A visited room is not a read paragraph; a recorded decision is not a completed mission. No independent state/save format, RNG, external callable mutation hooks or Ink flow contexts. Existing AK parties/tasks can interleave; that is not an Ink thread or tunnel execution model.
Keep four channels distinct: canonical conditions/effects govern gameplay; conversation/node edges describe narrative structure; existing typed fields such as captions and audio references describe presentation; comments and companion docs describe author intent. Arbitrary @camera, @music, @tone or extension maps are not valid metadata syntax. Ink tags inspire this separation, not an untyped path to gameplay authority. Structured author annotations and visual graph views are deferred. Future graphs project source/IR; they are not a second saved authoring format.
Compiler and CLI contract (#158)
Use repo-local TypeScript with existing tsx, consistent with scripts/validate-world.ts. Proposed invocation (not available in this PR):
npx tsx scripts/blaze.ts check examples/my-world/main.blaze
npx tsx scripts/blaze.ts build examples/my-world/main.blaze --out build/world.jsonExactly one entry argument. check forbids --out; build requires it. Unknown arguments or missing inputs are invocation errors. No npm publication required. Output cannot overwrite any input or an existing directory. Relative output paths resolve from invocation cwd; create missing parent directories only after successful validation. No watch/format/run/simulate/inspect/explain command now.
Pipeline: located AST → typed resolved authoring IR → lowered ordinary WorldBundle plus provenance → validateWorldBundle → deterministic JSON serialization. The small compiler-only IR retains resolved symbols, typed references, ownership, source spans and gathers before desugaring. Shape/link checking and immutable data hash checks happen before emitting the checked bundle; owner/closure checks must include final selection. IR is explicit compiler data, not new gameplay opcodes, a stable serialized format, a runtime state machine or a second save model. A later typed graph/provenance projection can support inspection, diagnostics and editors without adding an inspect command or editor deliverable now. Never execute source JS/TS, run generators, fetch media, or invent runtime semantics to make validation pass.
Exit 0: valid; check prints a short success summary, build prints output path. Exit 1: source, type, link, hash, unsupported-version or World validation error. Exit 2: CLI usage, filesystem access/write or internal compiler failure. Errors use stderr, with stable code, entry-relative file, 1-based line/column, message, related locations and (when applicable) target World/JSON path. Suggested code families: BLAZE_PARSE, BLAZE_DUPLICATE, BLAZE_IMPORT, BLAZE_CYCLE, BLAZE_TYPE, BLAZE_REFERENCE, BLAZE_HASH, BLAZE_VERSION, BLAZE_WORLD, BLAZE_GATHER, BLAZE_IO, BLAZE_INTERNAL. BLAZE_WORLD includes the original validator code; never turn failed validation into a warning. Sort independent diagnostics by entry-relative path, position, code; avoid cascading messages after parse failure. Machine-readable diagnostic output is future work, not required for v0.1.
| Diagnostic | v0.1 contract / limits |
|---|---|
| Missing/ambiguous symbol; private symbol import | Error at use/import with declaration candidates and an export/alias fix when appropriate. No implicit global search. |
| Duplicate ID; import/expansion cycle | Error with owning sites or full cycle; respect runtime identity domains. |
| Unsupported field/effect/language version | Error with expected field, union tag or supported version; do not execute unknown syntax. |
| Gather target outside conversation or omitted from selection | BLAZE_GATHER error at target with owning conversation; select the correct node explicitly. |
| Gather choice already has nextNodeId | BLAZE_GATHER error with choice field location; remove conflicting wiring or use a plain list. |
| Gather in legacy NPC or StoryBeat conversation | BLAZE_GATHER error at expression and owner; use supported explicit choices. |
| Missing nextNodeId | Legal stay-put behavior, not an Ink-style loose-end error. |
| Unreachable scene | Future conservative warning at most: external entries can make content reachable. |
| Impossible precondition | Future bounded analysis only, not a promise to prove arbitrary state reachability. |
| Unconsumed exits / stale rule versions | Deferred: general exit/rule-version constructs do not exist in v0.1. Existing supported schema/revision validation still applies. |
These are Blaze design requirements, not claims that Ink implements all of them. Ink's evidenced missing-target, duplicate-name and loose-end checks are discussed in the prior-art note; AK's stay-put semantics intentionally differ.
Serialization uses JSON.stringify-compatible primitive encoding, two-space indent and a final LF. Preserve authored record field insertion order (including local JSON data order); JavaScript canonical array-index keys enumerate numerically before other string keys, exactly as at the pinned JSON transport boundary. Tagged records insert type first, then authored operands in source order; embedding preserves the referenced value's order. Never sort arbitrary maps: src/cyberart/geometry.ts enumerates hotspot geometry into ordered landmarks and src/visualMasks.ts enumerates mask maps into arrays. Preserve every array's order, text byte content after escape decoding, numbers and omitted/null distinctions. No timestamps, cwd paths, random IDs, environment-dependent sorting or network state. -0 is rejected to avoid JSON round-trip loss. The compiler may not coerce values or insert defaults. The explicitly authored gather is the sole additional edge-lowering sugar described above. Identical transitive input bytes + same compiler version produce identical bytes, independent of cwd, traversal, import loading order or unrelated files.
Build writes a same-directory temporary file and atomically renames it only after all checks and serialization succeed. On any failure preserve the previous output byte-for-byte and clean up only this build's temporary file. Concurrent builds to the same output are unsupported. Check performs all validation but writes nothing.
Version axes stay distinct: blaze "0.1" pins language semantics (reject other versions); compiler version belongs in tool reporting, not generated worlds; manifest.schemaVersion pins World shape; contentVersion/assetVersion and nested revisions remain exactly authored. A supported compiler cannot silently upgrade source or save schema. Non-Blaze JSON/TypeScript worlds remain supported unchanged.
Parity and finish line
#159 must preserve an independent export oracle from the pinned original before porting. Follow the inspection recipe in the inventory. Never replace that oracle with generated content or modify the original/reducer to make equivalence pass. The oracle is JSON transport data: omit JS undefined properties using the existing JSON serialization boundary, not by arbitrary test normalization. Reject undefined inside Blaze. Structural comparison may ignore field order only for fixed-shape records proven order-insensitive; separately compare key sequences for authored maps, especially hotspot geometry and visual masks, and their runtime-enumerated arrays. Do not normalize arbitrary object order. Arrays, IDs, versions, prose/captions, receipts, flags, initial state, null/absence, numbers and manifest ordering may not. Preserve existing media URLs without fetching/regenerating assets. Byte reproducibility is a separate test.
#160 runs identical intents with deterministic clocks/IDs against original and compiled bundles, checking state, ordered events, narration and available/blocked interactions after each step. Cover key puzzle and repeat actions; conditional Eliza dialogue; Clara private/disclose × Silas quick/certified × timely/late reunion branches and followup; movement cost, co-location, independent party task, knowledge transfer, persistent flags, repeated decision rejection; resort catalog activation with accepted mocked receipts; save/reload before and after decisions and continued play. Use storyBeats.test.ts, adirondack-mystery.party.test.ts, partyContentScenario.test.ts and localSave.test.ts as evidence, not structural comparison substitutes. An intentionally changed consequence must make the parity suite fail; don't bless updated snapshots automatically. Test valid and blocked paths, not only a happy walk-through. No harness is implemented in #157.
Agent-context locality receipt (#159 / #161)
Grace's review correctly separates modularity from demonstrated agent usability. #159 must organize the full source for bounded ownership and expose dependency contracts. #161 must record a real bounded edit with: task-owned files read; dependency contracts/files read; UTF-8 bytes for each and for the entire authored world source; separate owned/dependency totals; optional token counts with tokenizer identity; retries, context escalations and any hidden whole-world reads. Report unique context bytes and repeated reads separately, and include companion contracts in the compared baseline so the accounting is explicit. Compiler reading the whole world is not agent context consumption. Compare the actual packet to the complete authored source; do not omit dependency chasing. If most of the world was needed, report locality failure even when compilation/parity passes. No measured savings are claimed here, and no universal percentage threshold is invented before data.
Handoff and exclusions
- #158: implement this grammar/type surface and check/build with diagnostic, deterministic-build, invalid-source/data and atomic-output tests; gather tests must compare hand-wired output, preserve ordering and shared values, reject conflicts/bad ownership, verify edge provenance and existing interaction timing; a small complete world must validate/play. The illustrative excerpts here are not that complete integration fixture.
- #159: author the complete pinned export including imported reunion and resort content; no hand repairs to emitted JSON; preserve an independent oracle.
- #160: demonstrate structural and behavioral parity with mutation detection.
- #161: concise agent reference, recipes and a bounded independent edit/build/ verification receipt. No undocumented manual JSON repairs.
No editor, graph tooling, general scripting, natural-language parser, new verbs, rulebooks, generic missions, module registry/dynamic installation, automatic save migration, media generation, deployment or publishing. Do not absorb adjacent #22/#56/#142/#156 into this milestone. Genuine target semantic gaps are explicit errors and review items, never hidden compiler transformations.
