Skip to content

Current specification

The versioned proposal stack under docs/proposals/scenario-dsl/ remains the normative source of truth. This page does not replace it — it is the consolidated index of what is current at language version 0.53.0: for each language area, which proposal revision introduced it, which last changed it, and where to read the normative text.

0.42.0 — player contract and volume bindings

Section titled “0.42.0 — player contract and volume bindings”

The 0.42.0 contract is a clean cutover across the language, IR, Event and snapshot contracts, player bindings, generated contracts, and player packages. The normative release proposal is 0.42.0.md.

  • volume is an absolute project domain. The documented scale is silent, quiet, normal, loud, full; numeric volume is not accepted. lute fix renames down to quiet and up to loud only for the exact old ordered domain [silent, down, normal, up, full].
  • player.bindings.yaml uses bindingsVersion: "0.42" and an optional volumes: { member: gain } map. Gains MUST be finite numbers in [0, 1]; unknown keys are E-BINDING-KEY, missing declared members are W-BINDING-MISSING, and malformed gains are E-BINDING-SHAPE.
  • Runtime records for bg, music, sfx, vfx, actor, camera, cg, video, and sequence carry resolved attrs; hosts read media fields from attrs rather than reopening artifacts. Event and snapshot schemas move to the -0.42 line.
  • LutePlayer.make accepts a one-time cueFactory(base) wrapper and the handle exposes a coherent snapshot; replay uses voiceKey, and Director gives host.plugin precedence over a duplicate pre-composed handler.
  • Package typecheck uses Bun’s checker (bun check), CI pins Bun 1.4.3, and the seven published packages and all public version axes stamp 0.42.0/0.42.

Historical behavior remains attributed to its introducing proposals below; this index does not rewrite those citations.

0.53.0 (“dogfood round 3”) changes answers rather than the artifact: the IR, the runtime, the event contract and the plugin manifests do not move, and the schemas only take the -0.53 name; the area index below names each change in its row. The normative text is 0.53.0.md.

  • Loops across sections (§9, #231). The top-level ## sections of a document are one loop scope, as they run as one range: a bounded ::jump{to max} may loop back to a label in an earlier section. A section is presentation and identity, not a runtime range, a resume point, a host entry or a jump target (§9.1). E-JUMP-LOOP-SCOPE no longer names a section; E-JUMP-INTO-LOOP adds a one-option hint.
  • Structured frontmatter references (§6, #226). A reference typed entity, domain, enumFromOption, providerRef or assetKind is checked wherever it sits in a plugin frontmatter record, list or map value, with the field path in the message. A plugin frontmatter type counts as a reader for W-DOMAIN-UNREAD.
  • Accepts (§7, #227). An in-script accept activates an accept-driven quest whatever its follows; an accept="external" quest has a host route; the condition the host waits for is written follows="…" and is host policy, which Lute carries and analysis reads (heuristic); reach repeats over undrawn accept edges; an in-script accept deadlock is E-QUEST-START-CYCLE.
  • Markers (§8, #229, #241). A marker whose expression opens on { is E-INTERP-BRACE naming the run length n, where 0.52 gave E-CEL-PROFILE. A translation shows each {{…}} marker of its source as often, in any order; otherwise it is E-L10N-MARKERS, not merged.
  • Lint (§5, #225). A pair lint takes scope: document.
  • lute fix (§4, #224). It applies every migration of every older release, keeps a directive attribute the profile declares as a stamp, names the rules it applied (report 0.53.0.fix) and prints each kept site. See What lute fix runs.
  • Cost and editors (§3, #228, #240). Every multi-document command builds the project input layer once; lute lint reads only its operands; catalog refresh and doctor read catalogDir from the manifest; check --project reports the beat shadows lute beats reports; the LSP keeps one input cache per project root across requests.

0.52.0 (“clean slate”) closes gaps between what the checker accepts and what runs, with no new keyword and no new element; the area index below names each change in its row. The normative text is 0.52.0.md.

  • Literal braces (§3, #139, #211, #221). In line text a literal { (an odd run) directly before a {{…}} marker is E-INTERP-BRACE, since compiled text cannot tell it from the marker; so is a ::use argument ending in a plain {{ spliced before a marker. \{{ writes a literal {{ in every value that takes the label grammar — a <choice text>, a prompt, a plugin text attribute — and stays E-STRING-ESCAPE in any other quoted value.
  • Quest top level (§4, #208). A quest runs only its <on> and <objective> bodies, so nothing else but <reward> sits directly inside <quest> (E-GRAMMAR-NOT-ADMITTED). E-JUMP-LOOP-SCOPE loses its quest-top-level cause.
  • Hubs (§5, #216, #217, #222). A forward ::jump out of hub option or <return> bodies leaves those hubs (IR jump.leaves, semantic id lute.core.hubLeave/1); a jump into a body that runs as its own range is E-JUMP-INTO-BODY. Each hub option is judged from the hub head, a fixed point over what earlier picks can leave, so W-STAGE-ABSENT and the injected staging see a sibling option’s exit; a join keeps a character on stage on every route on stage.
  • Menu text (§6, #218, #212). A prompt takes the label grammar and is checked like a label (IR promptPlaceholders). The runtime renders labels and prompts onto the awaitChoice menu (text, texts, prompt, promptTexts), and the reference player shows them. Whole-project E-QUEST-REF-UNKNOWN reads prompts and plugin text attributes. An <objective title> is plain text: E-OBJECTIVE-TITLE-INTERP.
  • Reward-kind attributes (§7, #201). A <reward> authors its kind’s declared attrs:, typed like directive attributes; the kind’s effects read @<attr>; IR RewardEntry.attrs, copied onto the grant record. Declaration faults are E-PLUGIN-PARSE at load.
  • Tooling (§8, #209, #210, #213, #214). lute scenario envelope provenance stops at [accept] edges as at [raises] edges; lute impact follows [accept] and [raises] edges; loc export takes the prefix in force inside choice bodies; lute test --project prints no discovery note.
  • 32-bit casts (§9, #198). List indexing, countDistinct columns, weekday labels and lute lint indexing give the native result on wasm32.

0.51.0 (“flow and text”) adds four features, with no new keyword and no new element; the area index below names each in its row. The normative text is 0.51.0.md.

  • Bounded backward jump (§3). ::jump{to max} may target an earlier ::label at most max times (a literal integer from 1 to 99); after that control falls through. A loop stays inside one body, is entered only at its head label, and nests without crossing another loop. Every flow analysis joins the states of all jumps at a label and iterates a loop head to a fixed point, so a read after a label that a jump skipping a write reaches is E-MAYBE-UNSET, and a line there can be W-STAGE-ABSENT. New codes E-JUMP-MAX, E-JUMP-LOOP-SCOPE, E-JUMP-INTO-LOOP, E-JUMP-LOOP-CROSS. IR: jump.max and the semantic id lute.core.boundedJump/1; the reference runtime re-checks the loop topology at load. A label in a component is compiled per ::use instance (#207).
  • Declared occasion raises (§4). A directive or reward kind declares effects.raises, and an occasion may be raisedBy: declared. The scenario graph draws a [raises] edge from the raiser to the scenes and beats that answer it (since 0.52 lute impact follows it too, #210). A declared raise is a host promise and is possible, never guaranteed. New codes E-PLUGIN-RAISES, E-RAISES-TARGET, W-OCCASION-NEVER-RAISED. IR: raises. The reference player keeps the raises it has not sent yet in its save (pendingRaises).
  • Localizable directive text (§5). An attribute typed text is localizable; its directive takes code=, with the identity {prefix}~{tag}~{code}~{attr}. lute tag and lute loc export / import / report handle it. New codes E-LINE-CODE-RESERVED, E-LINE-ID-DUP, E-COMPONENT-TEXT-DIRECTIVE. IR: localized.
  • target: pair lints (§6). open, close and optional inside predicates find unclosed, unopened, nested and misplaced pairs, in body or section scope.

Structured attribute values (#176: list, map, record) are deferred, not covered; the refusal stays and its message names the scalar decompositions (§7).

0.51.0 supersedes a 0.1.0 non-goal. The 0.1.0 proposal (§14) lists backward diverts as a non-goal. A bounded, well-nested loop keeps every analysis finite and termination provable, so 0.51.0 allows it. The older proposals and the history sections of this page are not rewritten.

Language areaIntroducedLast changedNormative source
AI edit loop & lossless source tooling0.36.6Lossless syntax and canonical lute fmt, source/project revisions, task-scoped lute context, semantic lute diff, and atomic revision-checked lute patch. Since 0.53.0 lute fix applies every migration of every older release, not only the version raise, keeps a directive attribute the active profile declares as a stamp, names the rules it applied, and prints each kept site; its report is 0.53.0.fix.0.36.0.md §1–§8, 0.53.0.md §4
Project semantic model & impact analysis0.34.0Project commands share one model; lute impact computes deterministic reverse dependency closures, and lute constraints checks policy declarations with explicit evidence levels and bounded scopes. Since 0.52.0 lute impact follows the scenario graph’s [accept] and [raises] edges (a raises link is heuristic when every raise site behind it is dynamic). Since 0.53.0 every multi-document command builds the project input layer once per cache key (the LSP keeps one per project root across requests, validated on every request), lute lint reads only its operands, and check --project reports the beat shadows, priority ties and fact-guard verdicts that lute beats reports (one reconciliation per root, #240).0.34.0.md §1–§6, 0.52.0.md §8.2, 0.53.0.md §3, 0.53.0.md §3.10
Engine semantic negotiation0.33.0The compiler derives sorted requiredSemantics ids from lowered features; engines load lute.engine.yaml after the exact-minor IR gate and refuse unsupported ids before playback. check --engine and check-project --engine report missing capabilities at author time.0.33.0.md §2–§5
Frontmatter & profiles0.1.00.53.0 (a reference inside a structured plugin frontmatter value — a record field, a list item, a map key or value, nested — is checked like a top-level one, with the field path in the message; an unrepresentable value or an unsupported map key type stays E-FRONTMATTER-SCHEMA; a plugin frontmatter type counts as a reader for W-DOMAIN-UNREAD; 0.49.0: a plugin frontmatter key typed by reference — entity, domain, enumFromOption, providerRef, assetKind, or a list of one — checks its string value as an attribute of that type, with the attribute codes and did-you-mean; another value shape stays E-FRONTMATTER-SCHEMA; 0.37.0: frontmatter title: is the only document title — a body # Title line is E-INERT-TITLE; monoSpeakers: sits beside pov: on scene, quest and lore documents and in the manifest’s defaults:; an authored key containing _ is E-AUTHOR-CASE, naming the lowerCamelCase spelling; the manifest’s sequence: key is gone (E-REMOVED-PROJECT-KEY); 0.28.0: a frontmatter key that belongs to another layer names that layer (terminal: → a schema, chapters: / questTier → lute.project.yaml, rearm / tier → the <quest> attribute); once beside spentBy: is legal and sets the latch’s period (once: false or a share key beside it is E-BEAT-ATTR); a quoted scalar where a bool or number is meant (once: "false") says to write it unquoted; a kind: lore document accepts pov:; the manifest’s defaults.questTier accepts season:<name>, and the manifest refuses unknown keys (E-MANIFEST-KEY); 0.27.0: a scene beat may write for: "kind:<kind>" on an untargeted select: sequence occasion and is presented once per member whose when holds; spentBy: spends a beat when its condition holds (0.28.0: a latch — once the condition has held, in the settled world after any settle or when the beat is judged, per member for a kind or for beat, the beat stays spent for its once period, run unless written, even if the condition turns false; once: false or a share key beside it is E-BEAT-ATTR, one that holds at the start of play is W-BEAT-SPENT-AT-START, and one with no once written that can turn false again is W-SPENT-BY-REVERSIBLE); once: week (with the clock’s week:) and once: season:<name>; the manifest’s chapters: (0.28.0, a list of { on, scenes } chains, one per occasion) gives each listed scene on:, a descending priority: and — except on a select: sequence occasion — after:, unless it writes the key; on a targeted occasion every listed scene declares target: (E-CHAPTERS, W-CHAPTER-ORDER, W-CHAPTER-STALL); 0.26.0: a scene beat may target: "kind:<kind>" — it answers every member of the kind and reads the raised member as occasion.target; the manifest’s defaults.uses entries may be globs and defaults.questTier sets the default quest tier=; 0.25.0: a scene beat may name a share: key beside a written once, spent together with every beat of the key; 0.24.0: scene and bundle beats take once: day / once: slot when the project declares a clock — without one, E-BEAT-ATTR; 0.21.0: a scene becomes a beat with the scene-only keys on: / target: / when: / priority: / once:; when, target, priority, or once without on is E-BEAT-ATTR)0.49.0.md §4.3; 0.37.0.md §2.1, §3.1, §3.4; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.25.0.md; 0.24.0.md; 0.21.0.md, 0.53.0.md §6
Sections (## Heading {#id})0.1.00.53.0 (a section is presentation and identity — the heading, the optional {#id} and the first segment of position — not a runtime range, a resume point, a host entry or a jump target; a scene’s top-level sections run as one range and are one loop scope; 0.37.0: a document’s body is ## sections — the IR’s sections table, { section, heading, id? }; an optional final {#id} is a stable section id, unique per document (E-SECTION-DUP), identity metadata only and never a ::jump target; content before the first heading is E-CONTENT-OUTSIDE-SECTION; the reserved <scene> tag is removed (E-REMOVED-TAG); 0.6.0: free heading text)0.37.0.md §3.1; 0.6.0.md, 0.53.0.md §9.1
Content lines (@speaker dialogue)0.1.00.53.0 (a marker whose expression begins with { — {{{run.a}} — reads as the marker after n literal braces and is E-INTERP-BRACE in line text and in every label-grammar value, where 0.52 gave E-CEL-PROFILE; the message names the run length n and spells both ways out for exactly n braces; a translation whose {{…}} markers do not parse or are not its source’s (each as often, any order) is E-L10N-MARKERS and is not merged; 0.52.0: a literal { (an odd run) directly before a {{…}} marker in line text is E-INTERP-BRACE, as is a ::use argument ending in a plain {{ spliced before one; \{{ is a literal {{ in every value that takes the label grammar — a <choice text>, a prompt, a plugin text attribute — and E-STRING-ESCAPE in any other quoted value; 0.50.0: a bare-identifier {{name}} is a host token when a plugin’s hostTokens export (or lute.core’s userName) declares it, its format hint typed by the token; an undeclared one is E-CEL-PROFILE naming the declared tokens, and the IR keeps the reserved placeholder the host substitutes; a plugin stamp scoped on: [line] applies to lines only, and a required: true one is E-MISSING-ATTR on a line without it; 0.49.0: a content line takes the core timing key wait (a bool, or a bool @ref), and the IR carries timing.wait only when it is authored; delay / duration stay E-UNKNOWN-ATTR; the reference player does not hold the following staging cues behind a wait: false line; 0.47.1: \{{ in a line is literal text under one escape rule — an odd run of backslashes directly before {{ escapes it, in every scanner; a ::use string argument is judged on its decoded value; the IR writes a literal {{ as \{{ in text, segments and translations, backslashes before {{ doubled, and engines that substitute placeholders themselves read that escape; 0.37.0: a line’s role is dialogue, narration (@narrator), or the {mono} / {os} / {vo} flag’s mono / os / vo; a {mono} speaker must be the effective point of view (pov:, else defaults.pov) or listed in monoSpeakers: — E-MONO-POV, or E-MONO-NO-POV when no point of view resolves — judged at every ::use of a component with that caller’s context; a line takes no id= (a ::label before it marks the place); inline text modifiers in line text — the core :pause{s=…} and :speed[…]{rate=…}, and span styles from the project’s textStyle domain — with the IR text the plain derivation and a modified line adding segments (E-TEXT-MODIFIER, E-TEXT-ESCAPE); 0.28.0: a kind’s labels: entry wins over a cast name: in text (a speaker head keeps the cast name; W-LABEL-CAST-SHADOWED retired) and may be { text, start, indefinite }, rendered by {{…:start}} / {{…:indefinite}}; :capitalize and :cardinalWord join the hints and #word / #Word spell the number in a plural(…) form; another language’s markup in line text or a choice label warns — W-TEXT-SINGLE-BRACE ({run.oil}, Ink / Yarn braces), W-TEXT-COMMENT-LIKE, W-TEXT-BRACKET-LABEL, W-TEXT-GLUE; a quoted or ,-separated plural form is E-PLURAL-FORM; diagnostic columns count characters; 0.27.0: {{occasion.target}} and a { domain: K } / { entity: K } path render the kind’s labels: (0.28.0: the label wins over a cast name:; a speaker head keeps the cast name); {{n:plural(lamp|lamps)}} picks a form by the number, # standing for it; curly quotes around an attribute value are one E-ATTR-QUOTE; 0.26.0: a component line @@who: speaks as the cast member a speaker param names; two cast entries or speaker display names that are exactly equal are W-DISPLAY-NAME-DUP unless the entry is sharedName: true; 0.25.0: a reserved relation’s changedOn: names the occasions on which the engine changes it, and cast assume: true then no longer covers it in a unit presented on one of them, in that unit’s after-descendants, or under a guard that needs a fact of it; the {{x:ordinalWord}} format hint — lute play renders first … twentieth, the engine localizes; 0.24.0: a cast entry may declare present: — a line whose guards do not imply it is W-CAST-ABSENT, assume: true reads a negated engine-reserved fact as true — and emotions:, outside which emotion= is E-BAD-ENUM; the {{x:ordinal}} format hint, and {{path}} of an enum-typed path renders the member’s display labels:; 0.23.0: a declared cast — a plugin cast export or a schema document’s cast: — makes a speaker outside it E-CAST-UNKNOWN with a did-you-mean; without a cast speakers stay shape-only; 0.5.1: delivery-flag authoring-surface honesty)0.50.0.md §4; 0.49.0.md §4.2; 0.47.1.md §3; 0.37.0.md §3.4, §3.6; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.25.0.md; 0.24.0.md; 0.23.0.md; 0.5.1.md, 0.53.0.md §8, 0.53.0.md §8.1
Core directives (the closed lute.core vocabulary)0.1.00.53.0 (a bounded ::jump{to max} may loop back to a label in an earlier top-level ## section, since the sections are one range; E-JUMP-LOOP-SCOPE no longer names a section; E-JUMP-INTO-LOOP adds a one-option hint when the entering jump ends a <choice> body that no other jump targets; 0.52.0: a forward ::jump out of hub option or <return> bodies to a label after the hub leaves those hubs (jump.leaves, semantic id lute.core.hubLeave/1); a jump into a body that runs as its own range — a hub option or <return>, an <on> or <objective> body, an entry, a beat — is E-JUMP-INTO-BODY; 0.51.0: ::jump takes max (a literal integer from 1 to 99) and may then target an earlier ::label in its own body: a bounded loop, entered only at its head label, nested without crossing another loop (E-JUMP-MAX, E-JUMP-LOOP-SCOPE, E-JUMP-INTO-LOOP, E-JUMP-LOOP-CROSS); every flow analysis joins the states of all jumps at a label and iterates a loop head to a fixed point; a bounded jump does not end its body for W-CODE-AFTER-JUMP; the core jump declaring max moves every capabilitySnapshot once; 0.50.0: a plugin stampAttrs entry’s on: scopes it to named surfaces — line, branch, hub, choice or a directive tag with a stamp carrier — while an entry without on: stays admissible on every stamped directive and content line; a stamp scoped elsewhere is E-UNKNOWN-ATTR naming its surfaces; required: true needs on: and is E-MISSING-ATTR where the stamp is absent; an on: naming no stampable surface is E-PLUGIN-STAMP-SURFACE; 0.49.0: ::jump, ::label and ::clear emit no stamped record, so a core timing key or a plugin stampAttrs key on them is E-UNKNOWN-ATTR at the attribute; a directive timing @ref is typed by its timing key (E-REF-TYPE); 0.42.0: volume is an absolute project domain; the documented scale is silent / quiet / normal / loud / full, and numeric volume is rejected. The staging directives remain the 0.37 surface — ::bg, ::music, ::sfx, ::vfx, ::actor, ::camera, ::cg, ::video, and ::sequence — with project-declared domains and resolved runtime fields. Historical cutovers remain: 0.37.0 renamed the directives and removed numeric camera vocabulary; earlier changes are listed in the linked proposals.)0.51.0.md §3.1–§3.5; 0.50.0.md §3.1–§3.4; 0.49.0.md §4.1; 0.47.0.md §4; 0.42.0.md §3; 0.37.0.md §3.3, §3.5; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.24.0.md; 0.9.0.md, 0.53.0.md §9, 0.53.0.md §9.4
Implicit staging (stage hygiene injections)0.1.00.52.0 (each hub option is judged from the hub head — the entry stage joined with what every non-exit option and <return> can leave, iterated to a fixed point — so W-STAGE-ABSENT and the injected anchors and preloads see an earlier pick’s exit; where routes join, a character on stage on every route stays on stage with an unknown sprite, so the next line keeps its auto-pose-reset; 0.45.0: injected staging — scene-change hides, actor anchor/preload, pose resets, stage-clear — and W-STAGE-ABSENT / W-EXIT-INERT key on the staging record a directive lowers to, so a plugin directive lowered to bg changes the scene like ::bg and one lowered to actor stages like ::actor (leaving on exit: true); amends base §11.5: still a toolchain rule, but a function of the lowered records, never of the source directive; an actor lowering must bind character to a required attribute or a literal, else E-LOWER-RECORD-FIELD)0.45.0.md §2
Content vocabulary (emotion, action, anchor, mood, volume, musicPlayback, vfxType, costume, framing, cameraMove, transition, cgLayout, sequence, textStyle)0.1.0 (closed member lists shipped inside lute.core)0.42.0 (volume members are absolute levels; the documented scale is silent, quiet, normal, loud, full, and lute fix migrates only the exact old ordered [silent, down, normal, up, full] domain. Other slots remain project-owned. 0.41.0’s musicAction → musicPlayback migration remains historical.)0.42.0.md §3; 0.41.0.md §3.1; 0.37.0.md §3.2; 0.9.0.md
Branch / match / when / hub0.1.00.52.0 (a prompt takes the label grammar: its markers are checked as a label’s and compile promptPlaceholders; the runtime renders option labels and prompts onto the awaitChoice menu (text, texts, prompt, promptTexts); 0.50.0: <branch>, <hub> and <choice> admit a plugin stamp whose on: names them, typed by its declaration, @ref values included; the IR’s choice / hub record stamps holds the element’s stamps and each option entry carries its <choice>’s stamps; the reference player’s ChoiceView carries both; 0.47.0: a branch or hub with prompt= owns the prompt lineId {prefix}.{id}.prompt — the IR’s choice / hub record carries it as promptLineId, and a translated prompt as promptTexts — so a <choice id="prompt"> inside it is E-CHOICE-DUP; 0.37.0: a <choice>’s displayed text is text=; a <match> names its subject with subject=; the IR’s choice and hub records carry selectionKey; 0.28.0: a <hub> may hold one <return> block that runs after every non-exit option’s arm, before the options are judged again; flag attributes (once, exit, optional, also) are written bare, ="true" / ="false" mean what they say and any other value is E-FLAG-VALUE, a <choice>’s once meaning this hub visit only; comparisons are typed in every condition slot (E-CEL-TYPE); a <when is> arm in a <match> with no subject is E-MATCH-NO-SUBJECT; <else> is one error naming <otherwise>; W-BRANCH-ID-SHARED for two documents’ menus with one id; 0.27.0: a { domain: K } / { entity: K } path is member-checked wherever a literal meets it — ::set, == / != / in, <when is>, into= — and a <match> over it must cover K; a <match> whose subject @def is a fact query is E-MATCH-RELATION-SUBJECT, as the inline query is — test it in an arm’s <when test> of a <match> with no subject; a beat’s when: narrows a subject through `
Reusable content components (::use expansion)0.1.00.46.0 (a ::use site reserves component, when, at and instance: a component param with one of those names is E-TEMPLATE once, at its params: entry, naming the list; 0.28.0: a template header accepts F[@param] (the dot form draws W-TEMPLATE-DOT-PARAM), may declare also: true, and a use’s own when= replacing the header’s says so (W-TEMPLATE-OVERRIDE); a component param named component / when, or a template param named like a <beat> header key, is E-TEMPLATE; a param typed by a closed { domain: K } (including { domain: clock.slot } / { domain: clock.weekdayLabel }) is judged over K’s members; occasion.target may be a component argument in a kind or for= beat; 0.27.0: a component may declare a beat: header and be used as a beat template — <beat use="…" id="…" …> (or self-closing) takes every header key it does not write, runs the template’s body then its own (::body places it), and desugars into an ordinary beat before any check; misuse is E-TEMPLATE; 0.26.0: parity with direct text — a component’s plugin result slots are declared in the host, an untyped bridge answer is refused, {{…}} in a string argument and a speaker param in an attribute value render as in text; @@who: speaks as a speaker param (@@x for a non-speaker param is E-COMPONENT-ARG); a param may declare default:; a component reads its own plugin directives’ result slots; 0.24.0: effects: true — a component may ::set / ::assert / ::retract, each write checked at every ::use against the host’s schema and compiled where the ::use sits; speaker params take a cast id; 0.23.0: a component string param may be interpolated as {{@p}} — a literal ::use argument is substituted per call site under that call’s own lineId, and binding the param to a @def is E-REF-TYPE; 0.22.0: a component line gets its own identity scope — {prefix}.{component}#{n}.{speaker}_{code}, n the host’s 1-based ::use ordinal of that component — so two uses never share a lineId; 0.9.0 made five root-only check stages now run over an imported component body — content-line attrs, E-DUP-LINE-CODE, reachability, unwalked-content admission, injection folding)0.46.0.md §2.1; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.24.0.md; 0.23.0.md; 0.22.0.md; 0.9.0.md
into= records (choice run-record sugar)0.1.0 (as persist=/into=, renamed from 0.0.1 as)0.49.0 (the arm’s recorded set comes first, before the arm body, so an arm that ends in ::jump or ::end still records its pick; 0.6.0: breaking — persist= removed, into= alone records)0.49.0.md §5.1; 0.6.0.md
State tiers (scalar scene/run/user/app)0.1.00.49.0 (a blocking bridge call’s fromBridgeResult writes are assigned after the call unless it is written wait="false" or guarded; '<member>' in <family> is the presence guard of <family>.<member> for a per: family, and every hint names an accepted spelling; an inline or schema per: family over an imported kind merges member by member like explicit paths (E-STATE-REDECLARE, E-USES-DUP-STATE, E-EXTENDS-STATE-TYPE, or the extends refinement); 0.41.0: a type: number — on a state: row and on every other declared type: def results and params:, component params:, directive attrs:, bridge result: and occasion payload: fields, plugin state-shape fields: — is E-TYPE-NUMBER, suggesting int when every value seen for it is whole, with no lute fix rule; a declaration that fails to parse but keeps its name stays declared as invalid and reports only its own errors — its dependants are not reported as unknown or mistyped (no E-CLOCK-DECL for a clock: naming it, no E-UNKNOWN-DIRECTIVE for a directive declared by it), and one info W-DEPENDS-ON-INVALID at its first dependant names it; a plugin whose export entries are partly invalid still loads its valid ones; 0.32.0: int/double replace number; state remains scalar; number is removed; engine-owned writes report E-RUN-OWNED-WRITE; 0.30.0 (a state path’s segment is a name — letters, digits, _ or -, not starting with - — and a condition, a ::set target, a {{…}} or a state: key reaches one that is not an identifier with a quoted index (run.visits["lab-b2"], single or double quotes), the same path as the bare spelling; a non-identifier name after a . is one E-PATH-IDENT naming the bracket spelling; 0.28.0: a state: row with an unknown key, or a path declared both as a value and as a prefix of other rows, is E-STATE-DECL; engine-owned namespaces (scene.choices.*, scene.visited.*, occasion.*, a bare quest.<x> / season.<x>) cannot be declared; a reserved word as a path segment is E-RESERVED-NAME; F[occasion.target] may be written in a kind or for= beat, checked per member; per: may name a kind another schema declares; 0.27.0: a schema may declare seasons: — season.<name>.* paths and the read-only prev.season.<name>.*, reset each time the season’s live goes false→true, seasons overlapping freely (E-SEASON-DECL); per: K declares the members K’s subsetOf: sub-kinds add; 0.26.0: check-project collects every frontmatter state: declaration of a path — inline or imported — and declarations that disagree on type, default, per or owner are E-STATE-DECL-CONFLICT, naming both files and lines; 0.24.0: entity-indexed state — { type: int, default: 0, per: <kind> } declares one path per member, with per-member defaults { _: 0, <member>: 1 }; with a declared clock the reserved read-only clock.index / clock.weekday / clock.weekdayLabel; integer % joins the CEL profile; 0.23.0: the reserved read-only prev.run.<path> mirrors every declared run.<path> with the value it had when the previous run ended — maybe-unset, so a read needs isSet or an unset arm, a write is E-QUEST-RESERVED-WRITE, and declaring prev.* is E-STATE-NAMESPACE; 0.22.0: a state: declaration may carry owner: engine — content that ::sets it is E-ENGINE-OWNED-WRITE, reads stay unrestricted; the reserved user-tier entry.<id>.everRead joins the run-tier entry.<id>.read; 0.8.0 enforced scalar-only author state: — E-STATE-COLLECTION)0.49.0.md §5.2; 0.49.0.md §5.3; 0.41.0.md §3.2; 0.30.0.md; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.24.0.md; 0.23.0.md; 0.22.0.md; 0.8.0.md
Defs (defs: in frontmatter, schemas and plugins; @name / @name(args))0.1.00.41.1 (W-DEF-UNUSED on a project schema’s def names the host: true escape (for example, Program.def); on a frontmatter def, which refuses host:, it does not; 0.41.0: a def declared in a project schema file or a plugin’s defs: export MAY carry host: true — exempt from W-DEF-UNUSED and marked "host": true in the index — while host: on a frontmatter def stays an unknown key, with a hint that frontmatter defs are local to their document; lute compile --all exports the defs documents see through plugins and uses: as index defs with their params (scalar parameter types only; a name two documents see with different bodies is left out), a parameter lowering to the param node; lute expr <CEL> [--project <dir>] [--json] checks a host condition as a project-root when: against the index defs and prints its exprNode, for the runtime’s evaluate; 0.21.1: a def body passes the CEL profile gate, a def in a directive attribute folds to its literal or is E-ATTR-DEF-DYNAMIC, and a {{@def}} that cannot be inlined is E-INTERP-DEF)0.41.1.md; 0.41.0.md §3.5–§3.6, §4.3; 0.1.0.md; CHANGELOG 0.21.1
Facts & Datalog (relational layer)0.3.00.32.0 (condition fact queries use standard CEL list form — holds(string, list(dyn)), count(string, list(dyn)), countDistinct(string, list(dyn), int), validAt(string, list(dyn), int); rule syntax is unchanged; E-FACT-QUERY covers malformed query calls; 0.30.0 (a relation and a fact argument are names; a fact-query or rule argument that is not an identifier is quoted — holds('at', ["lab-b2"]), at(X, "lab-b2"), "lab-b2" = X — and names the same member as the bare spelling; 0.28.0: a relation’s tier: accepts season:<name> (its facts go back to the seeds each time the season opens) and a relation without tier: warns W-RELATION-TIER-IMPLICIT; ::assert / ::retract may take occasion.target in a kind or for= beat; an enum and an entity kind with one name are E-DOMAIN-NAME-CLASH; reserved names — including completed / active / cel / not for a relation — are E-RESERVED-NAME, which absorbs E-RELATION-RESERVED-NAME; a rule cel() guard gets the typed and literal-domain checks; 0.27.0: occasion.target is a ground term in a kind beat’s fact queries and per: indexes, checked per member; a rule cel() guard may compare a bound variable with a { domain: K } path, grounded per member; a plugin directive’s effects: { asserts, retracts } are checked at each call and count as ::assert / ::retract of the call; an entity kind may declare display labels:, and any other unknown key is E-ENTITY-KIND-SHAPE; 0.26.0: count('R', ['…']) <op> n and countDistinct may appear in a rule body over a relation the head does not depend on, else E-RULE-AGGREGATE-CYCLE; a schema may add: members to an entity kind exactly one other import declares, a subsetOf: child’s members are its parent’s, and a member listed twice — in one kind or added by two files — is E-ENTITY-KIND-SHAPE; 0.25.0: a relation may declare excludes: — symmetric, partners with the same argument kinds or E-RELATION-DECL; check-project reads a guard needing both as dead and a negated partner as following, an ::assert where the excluded fact holds on every route is E-FACT-EXCLUSIVE, a rule deriving one side from the other on the head’s own arguments is E-RULE-EXCLUSIVE, and lute play / lute trace / lute test halt at a write that makes both hold; a reserved relation may declare changedOn: — on a non-reserved relation or naming an undeclared occasion it is E-RELATION-DECL; 0.24.0: _ in a rule body is a fresh anonymous variable, existential under not; countDistinct(<pattern>, <Var>); entity sub-kinds (subsetOf:) and an entity kind atom in a rule body as a membership test; a rule cel() guard may read F[P] of a per: family, compiled grounded, one IR rule per member; E-RESERVED-NAME)0.30.0.md; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.25.0.md; 0.24.0.md; 0.3.0.md
Quests (<quest>, <on> ECA triggers)0.2.00.53.0 (an in-script accept — ::accept or a reward’s accept — activates an accept-driven quest whatever its follows; a quest the host grants once a condition holds is written accept="external" follows="…": the host applies the condition, Lute carries it and analysis reads it as host policy; an in-script accept deadlock is E-QUEST-START-CYCLE, anchored at the quest’s id when it has no start; E-QUEST-START-SELF, E-ACCEPT-TARGET and E-ATTR-TYPE name the host-policy spelling; 0.52.0: only <on>, <objective> and <reward> sit directly inside <quest>, other top-level content is E-GRAMMAR-NOT-ADMITTED; an <objective title> is plain text and a {{ in it is E-OBJECTIVE-TITLE-INTERP; whole-project E-QUEST-REF-UNKNOWN reads prompts and plugin text attributes too; 0.41.0: W-OBJECTIVE-STRANDED is not reported when the objective’s quest re-arms (tier="run", tier="season:…", a non-empty rearm=) or when a completion beat lives in another document; it is always a warning, never info, and its message states its evidence (bounded — the declared clock windows of the document’s beats, no path search); 0.32.0: quest instances carry an instance number and grants carry instance / index; saves expose questInstances; 0.28.0 (renamed attributes, each old spelling an error naming the new: <quest follows=> (was after=; quest-graph metadata that never gates the quest), <objective visibleWhen=> (was when=; it only hides the objective), <reward outcome=> (was on=); a quest failed by a required subquest reads failedBy == 'subquest'; a subquest’s rearm= is E-SUBQUEST-REARM and a constant one W-QUEST-REARM-CONSTANT; a season-tier quest not gated on the season’s live warns W-SEASON-UNGATED; occasion.payload.<field> is readable in the raising occasion’s objectives and handlers; a quest or objective id with a . is E-PATH-IDENT, and over a whole project an unknown quest.<id> read is E-QUEST-REF-UNKNOWN; 0.27.0: rearm="<condition>" returns a quest to unset each time the condition goes false→true; tier="season:<name>" resets with a season; a quest reading only run state with no tier= (and no defaults.questTier) is W-QUEST-TIER-IMPLICIT; a by= deadline a finite clock never reaches is W-DEADLINE-NEVER; 0.25.0: <quest accept="external"> — accepted outside the script — is the only acceptance outside content that silences W-QUEST-NEVER-ACCEPTED, a test or trace accepts: mock no longer does, and beside start it is E-ATTR-TYPE; lute scenario draws [subquest] edges and anchors a quest by the top-level start conjuncts that read visited(), entry.X.everRead or quest.Y.state ([start] edges); 0.24.0: <quest activate="accept"> subquests wait for an ::accept while the parent is active; <quest complete="any"> alternatives fail the rest as superseded; by= is judged at every settle and the place-bound rule is the new until=, with W-DEADLINE-BEFORE-DONE for a done that implies its by; the read-only quest.<id>.failedBy / objectives.<o>.failed; <on event target>; ::accept{at="nextRun"}; W-QUEST-NEVER-ACCEPTED; 0.23.1: E-QUEST-TIER-MIX — a subquest whose tier differs from its parent’s, since a mixed tree locks for good; an on= objective’s by is judged only when its occasion is raised, right after its done; a raised occasion first fires a same-named declared world event, so every active quest’s <on event> handlers run before its on= objectives are judged; 0.23.0: <objective by="…"> deadlines — the first time by holds while the objective is not done, it fails, and a failed required objective fails the quest; <objective on target> is judged only when the occasion is raised for that target, IR ObjectiveEntry.by / target; 0.22.0: <quest tier="run"> — status and objectives reset to unset when a run starts, default tier="user" persists across runs, IR QuestCmd.tier; check-project warns W-QUEST-HANDLER-DEAD on a questFailed handler of a quest that can never fail; 0.14.0 added subquests — <objective quest="childId"/> makes a child quest’s completion a parent objective; synthesized done/upward fail, engine-rule downward cascade and referenced-child activation; tree-not-DAG project shape with four new diagnostics)0.41.0.md §3.4; 0.28.0.md; 0.27.0.md; 0.25.0.md; 0.24.0.md; 0.23.0.md; 0.22.0.md; 0.14.0.md; CHANGELOG 0.23.1, 0.53.0.md §7, 0.53.0.md §7.3, 0.53.0.md §7.4
Timeline & property tracks0.1.00.37.0 (a record’s timing is one timing object — wait, and duration / delay / at in finite non-negative seconds, plus the timeline ordinal on a record emitted from a timeline; a barrier keeps its own direct timeline and at)0.37.0.md §5.2; 0.1.0.md
Connectivity & after: sequencing0.2.0 (after: scene sequencing)0.53.0 (an accept="external" quest has a host route — open, or, once its follows holds as host policy, heuristic — and a reward that also accepts it makes it reachable through the host; reach repeats over undrawn accept edges; scenario reach evidence may read heuristic; 0.49.0: every quest start anchor edge is drawn, including one that closes a cycle; quests whose start / subquest anchors can never all open are E-QUEST-START-CYCLE, and a start that reads its own quest is E-QUEST-START-SELF; scenario reach and constraints share one reachability claim; 0.29.0: W-CHAPTER-STALL stays specific to chapters: — a hand-written after: outside a chain is itself the statement “wait for that scene” and never warns; lute new scene --occasion O always writes on: O and, when O carries a chain, says how to make the scene a chapter instead; 0.28.0: the manifest’s sequence: { occasion, scenes } is chapters: [{ on, scenes }], one chain per occasion (E-CHAPTERS, W-CHAPTER-ORDER, W-CHAPTER-STALL — which fires only on a when: that can stay false for good); a chain on a targeted occasion requires every listed scene to declare target:; a quest’s follows= draws a scenario edge without gating; 0.27.0: the manifest’s chapters: chains the scenes each chain lists with after: visited("<previous>"); 0.25.0: a bundle <beat after=…> is a scenario edge like a scene’s after:; nested quests draw [subquest] edges and a quest’s top-level start reads draw [start] anchors, so a start-driven quest needs no copied after=; lute scenario lists a unit whose when reads visited() without an after as unanchored, with the after to write; 0.8.0: active("questId") — the third prerequisite primitive)0.49.0.md §6.1; 0.49.0.md §6.2; 0.49.0.md §6.3; 0.29.0.md; 0.28.0.md; 0.27.0.md; 0.25.0.md; 0.8.0.md, 0.53.0.md §7.1, 0.53.0.md §7.2
Identity & localization (lineId / voiceKey, locale texts)0.1.00.53.0 (a translated text must show its source’s {{…}} markers: E-L10N-MARKERS fails compile --locale-bundle, and loc import refuses a marker that does not parse; 0.51.0: a plugin directive attribute typed text is localizable: its directive takes code=, its id is {prefix}~{tag}~{code}~{attr} outside the lineId template, and lute tag, lute loc export / import / report and --locale-bundle handle it (kind: "directive"); ~ is refused in a lineId template (E-IDENTITY-TEMPLATE) and in a code (E-LINE-CODE-RESERVED), and final ids are unique project-wide (E-LINE-ID-DUP); 0.47.1: sourceHash hashes the encoded line text, so a line with \{{ gets a new hash, and a translation may contain a literal \{{; 0.47.0: lute loc export emits prompt rows (kind: "prompt") and every row carries sourceHash; locale bundles are schemaVersion: 2 with sourceHashes, and compile --locales warns W-L10N-STALE for a translation whose source text changed since export; 0.37.0: every line carries a voiceKey, narration and {mono} included, and E-DUP-VOICEKEY compares the modifier-stripped text of every line; lute loc export keeps modifier markup, and a translation whose modifiers differ from the source’s is E-L10N-MODIFIERS and is not merged; every translated locale has a plain texts[locale], and a modified line adds localeSegments[locale]; 0.23.1: an untagged component line takes the code lute tag would write into the component file before a param-scoped <match> folds, so a line in a non-first arm gets a new lineId / voiceKey once — the one lute tag persists; 0.22.0: breaking for compiled ids — the default voiceKey becomes {prefix}.{speaker}-{code}, pin identity.voiceKey: "{speaker}-{code}" to keep audio recorded against the old keys; a component line is addressed {prefix}.{component}#{n}.{speaker}_{code}; 0.21.1 added E-DUP-VOICEKEY — check-project and compile --all refuse a voiceKey carried by lines with different text; 0.8.0 introduced identity: templates and the loc import → compile --locales round trip)0.51.0.md §5.1–§5.5; 0.47.1.md §3; 0.47.0.md §3; 0.37.0.md §3.4, §6; 0.22.0.md; 0.8.0.md; CHANGELOG 0.21.1 / 0.23.1, 0.53.0.md §8.1
Compiled artifact shape (position addressing, IR carriers)0.1.00.53.0 (no shape change; the IR schema is lute-ir-0.53; 0.52.0: a jump that leaves hubs carries leaves, collected as the semantic id lute.core.hubLeave/1; a choice / hub whose prompt has a marker carries promptPlaceholders, and a RewardEntry its reward-kind attrs (descriptive, no semantic id); the IR schema is lute-ir-0.52; 0.51.0: a jump carries max when bounded, collected as the semantic id lute.core.boundedJump/1; a plugin command carries its declared raises and its text attributes’ localized entries, and a RewardEntry its kind’s raises (descriptive, no semantic id); the IR schema is lute-ir-0.51; 0.50.0: choice / hub option entries carry stamps; a RewardEntry carries its kind’s resolved retracts / asserts / accepts, collected as the semantic id lute.quest.rewardEffects/1; the IR schema is lute-ir-0.50; 0.49.0: plugin stampAttrs values are one nested stamps object after timing, provenance and source, omitted when none is authored, never top-level record keys; a list / map / record plugin attribute type is refused; the IR schema is lute-ir-0.49; 0.42.0: the nine staging runtime records bg, music, sfx, vfx, actor, camera, cg, video, sequence carry resolved attrs; optional fields are omitted, while timing, provenance, plugin fields and envelope fields retain their existing locations. Event and snapshot schemas move to lute-events-0.42 / lute-snapshot-0.42; the 0.37 command families, position, sections, capabilitySnapshot and requiredSemantics remain.)0.51.0.md §3.6, §4.6, §5.4; 0.50.0.md §3.5, §5.5; 0.49.0.md §3.1; 0.49.0.md §3.2; 0.42.0.md §4; 0.41.0.md §4; 0.37.0.md §5; 0.32.0.md, 0.53.0.md §1
Warning-severity diagnostics (W-LUTE-VERSION-STALE, W-TRACE-MOCK-UNPRODUCIBLE, W-CODE-AFTER-END, W-L10N-MISSING, W-FACT-GUARANTEED, W-BEAT-SHADOWED, W-QUEST-STATE-ISSET, W-TEXT-LOOKS-LIKE-REF, W-QUEST-HANDLER-DEAD, W-BEAT-PRIORITY-TIE, W-BEAT-ONCE-RUN-USER, W-REWARD-DOUBLE-CREDIT, W-CAST-ABSENT, W-DEADLINE-BEFORE-DONE, W-QUEST-NEVER-ACCEPTED, W-RELATION-UNREAD, W-DEF-UNUSED, W-DISPLAY-NAME-DUP, W-ENTRY-WRITE-REREAD, W-QUEST-TIER-IMPLICIT, W-CHAPTER-ORDER, W-CHAPTER-STALL, W-DEADLINE-NEVER, W-TEXT-SINGLE-BRACE, W-TEXT-COMMENT-LIKE, W-TEXT-BRACKET-LABEL, W-TEXT-GLUE, W-RELATION-TIER-IMPLICIT, W-SEASON-UNGATED, W-BRANCH-ID-SHARED, W-QUEST-REARM-CONSTANT, W-SPENT-BY-REVERSIBLE, W-TERMINAL-PERSISTENT, W-BEAT-UNRAISED, W-DEADLINE-BEFORE-WINDOW, W-TEMPLATE-OVERRIDE, W-TEMPLATE-DOT-PARAM, W-WIP, W-OBJECTIVE-STRANDED)0.6.10.41.0 (lute check / check-project summaries count info diagnostics separately and never as warnings, appending , I info only when some are present; the new W-DEPENDS-ON-INVALID (one per invalid declaration, at its first suppressed dependant) carries the W- prefix but is info severity, so it is counted there and not listed here; W-OBJECTIVE-STRANDED is always a warning; 0.32.0: W-QUEST-STATE-HAS replaces W-QUEST-STATE-ISSET; new E-FACT-QUERY and E-RUN-OWNED-WRITE; no new code; a warning that can describe an intended design has a spelling that states the intent — W-TERMINAL-PERSISTENT is silent on a terminal: long form with persists: true, and W-CHAPTER-STALL warns only on a chapters: chain; 0.28.0: W-TEXT-SINGLE-BRACE, W-TEXT-COMMENT-LIKE, W-TEXT-BRACKET-LABEL and W-TEXT-GLUE (another language’s markup in text), W-RELATION-TIER-IMPLICIT, W-SEASON-UNGATED, W-BRANCH-ID-SHARED, W-QUEST-REARM-CONSTANT, W-SPENT-BY-REVERSIBLE, W-TERMINAL-PERSISTENT, W-BEAT-UNRAISED, W-DEADLINE-BEFORE-WINDOW, W-TEMPLATE-OVERRIDE, W-TEMPLATE-DOT-PARAM, and W-WIP for the dead guards check-project --wip spares (was the error code at warning severity); W-LABEL-CAST-SHADOWED is retired; 0.27.0: W-QUEST-TIER-IMPLICIT, W-CHAPTER-ORDER, W-CHAPTER-STALL, W-LABEL-CAST-SHADOWED and W-DEADLINE-NEVER; every message drops its spec citations — the sections move to the --json spec array and lute --explain <CODE>; 0.26.0: W-DISPLAY-NAME-DUP — two cast entries or speaker display names exactly equal; W-ENTRY-WRITE-REREAD — a repeatable entry beat whose body sets state or retracts a fact; W-BEAT-PRIORITY-TIE normalizes negations and says why two whens are not exclusive; 0.25.0: W-LUTE-VERSION-STALE for a stamp inherited from the manifest’s defaults: is reported once, at the manifest; W-QUEST-NEVER-ACCEPTED no longer counts a mock’s accepts:; a negation that a declared exclusion implies is W-FACT-GUARANTEED; 0.24.0: W-CAST-ABSENT — a line whose guards do not imply its speaker’s cast present:; W-DEADLINE-BEFORE-DONE — an on= objective whose done implies its by; W-QUEST-NEVER-ACCEPTED — an accept-driven quest nothing accepts; W-RELATION-UNREAD / W-DEF-UNUSED — a relation nothing queries, a def nothing references; 0.23.0: W-REWARD-DOUBLE-CREDIT — a reward whose kind credits a state path beside a handler ::set of that same path pays twice; 0.22.0: W-QUEST-HANDLER-DEAD — a questFailed handler on a quest with no way to fail; W-BEAT-PRIORITY-TIE — equal-priority beats on one select: first occasion whose whens are not provably exclusive, so file order picks; W-BEAT-ONCE-RUN-USER — a once: run beat gated only on user-tier state replays every run; W-STAGE-ABSENT follows paths, forking at each arm and joining at convergence; 0.21.1 added W-QUEST-STATE-ISSET — has(quest.<id>.state) is always true, compare with == 'unset'; W-TEXT-LOOKS-LIKE-REF — a line whose whole text is @name for a def or param ships that literal text; 0.21.0 added W-BEAT-SHADOWED — check-project warns when a select: first beat can never win because an earlier-ordered beat on the same occasion and target is always eligible and never spent; 0.20.0 added W-FACT-GUARANTEED and removed W-UNPROVEN-RELATIONAL, one of the three original 0.6.1 coverage warnings — naming it in --deny is a usage error)0.41.0.md §3.2–§3.4; 0.29.0.md; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.25.0.md; 0.24.0.md; 0.23.0.md; 0.22.0.md; 0.21.0.md
Deny promotion (--deny / --deny-warnings) and project diagnostic levels (diagnostics:)0.6.10.50.0 (lute.project.yaml diagnostics: sets a W-* code’s level — off, info, warn or error, optionally only under paths: globs — at the reporting boundary of check, compile, trace, expr, lint and the LSP; --deny CODE beats it and --deny-warnings promotes what is still a warning; a relevelled diagnostic carries [project] / configured, suppressed ones are counted, and a malformed entry is E-DIAGNOSTICS-CONFIG; compile fails on every promoted check warning; 0.6.1: --deny CODE and --deny-warnings promote warnings to errors)0.50.0.md §6; 0.6.1.md
Project lints (lute lint, lute.lint.yaml, plugin lints: exports)0.13.0 (toolchain companion, plugin-system 0.0.5)0.53.0 (scope: document — one pair slot for the whole document, every section included; lute lint builds the project input layer once per root and reads only its operands; 0.51.0: target: pair finds unclosed, unopened, nested and misplaced pairs with open, close and optional inside predicates over node rows, in body or section scope; when is refused on it; 0.50.0: target: node binds one row per node — content lines, directives and logic elements in document order — as node (kind, tag, speaker, attrs as authored, position, prev / next, parent, the scoped preceding map), with scene and options, findings anchored at the node; the string members startsWith, endsWith, contains; E-LINT-CONFIG sends a checker code to lute.project.yaml diagnostics:; 0.49.0: lint <DIR> applies the project’s lute.lint.yaml, and asset-exists takes attribute selectors)0.51.0.md §6; 0.50.0.md §8; 0.49.0.md; plugin-system 0.0.5, 0.53.0.md §5, 0.53.0.md §3.2
Version stamp & axis alignment0.1.00.53.0 (every visible axis reads 0.53.0; the schemas move to the 0.53 line, bindingsVersion is "0.53" and player saves read "0.53" ("0.52" saves are refused); 0.52.0: every visible axis reads 0.52.0; the schemas move to the 0.52 line, bindingsVersion is "0.52" and player saves read "0.52" (the pending menu they hold carries rendered text; "0.51" saves are refused); 0.51.0: every visible axis reads 0.51.0; the schemas move to the 0.51 line, bindingsVersion is "0.51" and player saves read "0.51" (they carry pendingRaises; "0.50" saves are refused); 0.50.0: every visible axis reads 0.50.0; the schemas move to the 0.50 line, bindingsVersion is "0.50" and player saves read "0.50"; 0.49.1 was a behaviour-preserving patch on the 0.49 line; 0.49.0 moved the schemas to the 0.49 line and every axis to 0.49; 0.48.1 was a fix patch on the 0.48 line; 0.48.0 moved the schemas to the 0.48 line and every axis to 0.48; 0.47.2 and 0.47.1 were fix patches on the 0.47 line; 0.47.0 moved every axis to 0.47; 0.46.0 moved every axis to 0.46; 0.45.0 moved every axis to 0.45; 0.44.0 moved every axis to 0.44; 0.43.0 moved every axis to 0.43; 0.42.1 was a stamp-only patch; 0.42.0: language/toolchain, luteVersion, irVersion, Event, snapshot, bindings, generated contracts and player packages move together; schemas use the 0.42 line, and bindings use bindingsVersion: "0.42"; pre-1.0 engines still pin the exact IR minor.)0.51.0.md §1; 0.50.0.md §1; 0.49.1.md §1; 0.49.0.md §1; 0.48.0.md §1; 0.47.1.md §1; 0.47.0.md §1; 0.46.0.md §1; 0.45.0.md §1; 0.44.0.md §1; 0.43.0.md §1; 0.42.1.md §1; 0.42.0.md §1; 0.41.1.md; 0.37.0.md §1; 0.32.0.md, 0.53.0.md §1
Scene & document identity (id: frontmatter)0.15.0 (authored canonical scene key; new id: frontmatter is the lineId prefix, visited()/connectivity node, and prereqEdges[].node, superseding the derived {character}.{episodeId} join wherever it was consumed; character/season/episode/episodeId demote to optional when id: is present)0.30.0 (one name rule: each segment of a document id — like every scene, beat, entry, quest, objective, branch, hub, choice and mark id, share key, season, relation, kind and member, plugin occasion and event, target and category — is letters, digits, _ or -, not starting with -; only a name read bare as @name (a def, a def’s param, a component or template param) stays an identifier; 0.29.0: one identifier rule: each segment of a document id — like every scene, beat, entry, quest, objective, branch, hub, choice and mark id, share key, season, relation, def, kind, member and component param — is a letter, then letters, digits or _; a - or any other character, which 0.28 accepted outside CEL, is an error at the name under the slot’s code (E-META-ID here) naming the camelCase spelling; 0.19.0: quest and lore documents may declare an optional document id: — the bundle name, the artifact’s meta.id, and its ProjectIndex key; document ids share one project-wide namespace with scene ids, so E-META-ID and E-CONN-EPISODE-ID-DUP extend to them)0.30.0.md; 0.29.0.md; 0.19.0.md
Descriptive extra: block0.15.0 (open mapping of scalars or flat scalar-lists on scene and quest roots; carried verbatim into meta.extra and read by no language rule — E-META-VALUE on a nested mapping or non-scalar list entry)0.15.00.15.0.md
Legacy identity keys (character / season / episode / episodeId)0.1.00.15.0 (deprecated in prose only — W-META-LEGACY warns per legacy key when a document also authors id:; the four keys are no longer required when id: is present, removal deferred to a future major)0.15.0.md
Declarative rewards (quest / objective <reward/>, range amounts)0.16.0 (self-closing <reward kind= target= amount= when= outcome=/> (on= until 0.28.0) as a direct child of <quest> or <objective> — pure data on the owning records, QuestCmd.rewards / ObjectiveEntry.rewards; amount admits an integer scalar or the range literal N..M, negatives real; two new diagnostics E-REWARD-ATTR shape / E-REWARD-KIND vocabulary; reward.when joins the CEL-slot registry; lute run / play / trace emit deterministic grant transcript events at each fresh transition — the engine grants, the reference runtime never rolls a range)0.52.0 (a <reward> authors its kind’s declared attributes, typed like directive attributes (an @ref value is E-REWARD-ATTR), compiled into attrs and copied onto the grant record; 0.50.0: a granted reward performs its kind’s effects — retracts, asserts and accepts, @target bound to its target= — after its grant record; every analysis reads them, a reward’s asserts as possible only and an accepts as a way into its quest (E-ACCEPT-TARGET for a bad target); 0.37.0: an optional <reward id="…">, unique among one quest’s rewards (E-REWARD-DUP), is the reward’s stable key in the IR’s RewardEntry.id and in source maps; 0.28.0: on= is renamed outcome= (<reward outcome="failed"/>; on= is E-UNKNOWN-ATTR naming it), and the IR’s RewardEntry.on is outcome)0.50.0.md §5; 0.37.0.md §3.5; 0.28.0.md; 0.16.0.md
Reward-kind plugin vocabulary (rewardKinds: manifest export)0.16.0 (optional plugin-manifest map of kind id → contract — optional target provider domain and optional extra attr schema — that makes <reward kind=> / target= statically checkable when declared; folded into the capability snapshot as a guarded, sorted section so capabilityVersion moves only for projects that install a rewardKinds:-declaring plugin; the empty core section hashes byte-identically)0.52.0 (a kind’s attrs: are validated at load (a <reward> key, a duplicate, a default of the wrong type and a structured type are E-PLUGIN-PARSE), and its effects read @<attr> in facts and raise targets; 0.50.0: a reward kind may declare effects: { asserts, retracts, accepts }, checked like directive fact effects with @target the only reference; effects.writes is E-PLUGIN-KEY (a reward’s state write is credits:); a kind without effects keeps its capabilitySnapshot; 0.49.0: a reward kind’s target: { forbidden: true } refuses any target= (E-REWARD-TARGET); only a quoted target="…" reaches the IR, and an @ref or bare target is E-REWARD-ATTR under every contract; 0.28.0: plugin export keys are camelCase only — rewardkinds / assetkinds / stampattrs are E-PLUGIN-UNKNOWN-EXPORT naming rewardKinds / assetKinds / stampAttrs; plugin.yaml and its export files refuse unknown keys (E-PLUGIN-KEY); 0.26.0: a reward kind’s target: contract is checked — an { entity: kind } or provider contract validates the value and required: true rejects a missing target, E-REWARD-TARGET; a directive attribute may be typed { entity: K }, checked at a component’s ::use argument or param default:; 0.23.0: a reward kind may declare credits: <state path>, stamped on each reward as RewardEntry.credits; lute run / play add a scalar amount there on grant)0.50.0.md §5.1; 0.49.0.md §3.3; 0.28.0.md; 0.26.0.md; 0.23.0.md; 0.16.0.md
Plugin passthrough ownership0.17.2 (optional plugin owner id on kind: "plugin" records; hosts dispatch by (plugin, tag); older artifacts may omit the field)0.41.1 (a directive declaring layer: bridge and bridgeCall needs an effects.writes entry reading a bridge result, else E-BRIDGE-NO-RESULT-WRITE at its declaration and the project does not compile; an enumFromOption attribute is checked against the option’s profile value, else its manifest default; 0.41.0: a plugin execution record — runtime record events and lute run --json — carries the call’s attrs after tag: the command’s fields with occasion.target bound, never the stamp’s timing or stampAttrs keys; the compiler makes fields complete, adding each omitted attribute whose directive declaration has a default, which assembly refuses when it is not a value of its type (E-PLUGIN-INVALID-DIRECTIVE) and the checker judges at each call that omits it as an authored value; 0.17.2: hosts dispatch by (plugin, tag))0.41.1.md; 0.41.0.md §5.5; 0.17.2.md
Lore entries (queried content)0.19.0 (kind: lore documents of top-level <entry> declarations — content the engine looks up rather than plays; id / target / category / title / series / order / when attributes, target and category shape-only; bodies admit content lines, <match>, ::set, ::assert, ::retract; knowledge revealed by plain ::assert; the reserved engine-written entry.<id>.read path; first-read-only effects; a document-level series: ordering entries by file position; new E-ENTRY-ATTR, E-ENTRY-ID-DUP, E-ENTRY-SERIES-ORDER)0.28.0 (an <entry> and a <beat> of one document with the same id, or a repeated <beat id>, is E-BEAT-ID-DUP; <entry once="false"> is accepted as a repeatable entry beat; a for entry’s writes apply on each member’s first read in the run; W-ENTRY-WRITE-REREAD covers every entry that can be read again in a run and writes; <entry use=…> is one E-TEMPLATE; 0.27.0: an entry may take for="kind:<kind>", once="week" and spentBy= (0.28.0: spentBy= latches as a beat’s does; once="false" means the omission; a for entry’s writes apply on each member’s first read; every entry that can be read again in a run and writes — a lookup entry, one without once, a once shorter than the run, spentBy, for — is W-ENTRY-WRITE-REREAD), and its body may call a plugin directive whose only behaviour is its declared effects, applied on the first read; 0.26.0: an entry may target="kind:<kind>" and read occasion.target; a repeatable entry beat whose body sets state or retracts a fact is W-ENTRY-WRITE-REREAD; play and test accept <doc>.<entry> as an alias for an entry id; 0.25.0: <entry share=…> beside a written once joins a shared spend — reading the entry spends every beat of the key; 0.24.0: an entry’s target= on an untargeted occasion is metadata; entries take once="day" / once="slot" with a declared clock; under entry.X.read the facts X’s body asserts on every route join the must set; 0.23.0: a lore document may bundle scene-like <beat id on target title when priority once also> blocks beside its entries, canonical id <document id>.<beat id>; an entry’s or beat’s body segment runs to the next entry or beat record; 0.22.0: an entry beat takes once="run" | "user" — not eligible once entry.<id>.read / the new user-tier entry.<id>.everRead is set, absent = repeatable; 0.21.0 let an entry answer an engine occasion with on= / priority= beside its when / target, making it an entry beat)0.28.0.md; 0.27.0.md; 0.26.0.md; 0.25.0.md; 0.24.0.md; 0.23.0.md; 0.22.0.md; 0.21.0.md
Relational guard analysis (fact envelopes)0.20.0 (check-project decides every holds(…) / count(…) in a guard slot as impossible, guaranteed, or possible from a project-wide, argument-level may set and a path-sensitive must set of monotone facts propagated across the after: graph; guards are assumptions inside their regions and count is decided over an interval; a dead relational guard reuses its slot’s code — E-ARM-DEAD, E-OBJECTIVE-UNSATISFIABLE, E-QUEST-UNREACHABLE, W-OBJECTIVE-HIDDEN — plus the new E-ENTRY-UNREACHABLE for a lore entry when, and a guaranteed one is W-FACT-GUARANTEED; project-level only — single-file check leaves relational queries undecided; lute scenario … envelope lists the guaranteed facts at a node)0.25.0 (a declared excludes: pair decides a guard needing both false and a negated partner true — in the same guard, an enclosing one, or on every route; 0.23.0: the decider reasons per path across && / `
Beats and occasions (story selection)0.21.0 (a scene or lore entry becomes a beat by naming the engine occasion it answers — scene frontmatter on: / target: / when: / priority: / once: run | user | false, entry attributes on= / priority= beside when / target; eligible when after: and when hold and once is unspent, ordered by priority then ProjectIndex.beats order; an optional plugin occasions: export — select: first | all, target: true — folded into the capability snapshot as a guarded section, occasion names shape-only until some plugin declares them; new E-BEAT-ATTR, E-OCCASION-UNKNOWN, E-BEAT-UNREACHABLE, W-BEAT-SHADOWED; when joins the CEL-slot registry; schedule.yaml and every E-SCHED-* / W-SCHED-* code removed, lute play rebuilt on raised occasions and driving quest lifecycles — see Playing a story)0.51.0 (a directive’s or reward kind’s effects.raises declares the occasions the host raises in answer to the call or grant, and an occasion may be raisedBy: declared; lute scenario / scenario reach draw a [raises] edge from the raiser to the scenes and beats that answer it (a possible edge, never a guarantee; since 0.52.0 lute impact follows it, #210); E-PLUGIN-RAISES, E-RAISES-TARGET, W-OCCASION-NEVER-RAISED; 0.30.0: a share key, an occasion, a target and a category are names, so - and a leading digit are allowed; 0.29.1’s split between Lute-declared names and engine-owned ids is gone; 0.29.0: a schema’s terminal: may be the long form { when: "<condition>", persists: true } — the ending outlives runs on purpose: W-TERMINAL-PERSISTENT is silent, lute play says the game is over for good instead of offering newRun, the long form’s keys are closed and persists: true over run state only is E-META-VALUE; a share key is an identifier; 0.28.0: spentBy latches: once its condition has held the beat stays spent for its once period (written beside it, run unless written) even if the condition turns false again (W-SPENT-BY-REVERSIBLE); a select: sequence raise re-judges each beat just before its turn; a for beat spends once per member; an occasion may declare outsideRun: true (raised even after terminal: holds); the seam is decided when the occasion is raised; a once: season:<name> beat not gated on the season warns W-SEASON-UNGATED, a beat only an unraised clock position would take W-BEAT-UNRAISED, and a terminal: reading state a new run keeps W-TERMINAL-PERSISTENT; also needs a select: first occasion everywhere; 0.27.0: an occasion may declare a raisedWhen: gate and typed payload: fields (occasion.payload.<field>), and a schema a terminal: condition — the checker judges every beat under its gate and !terminal; for="kind:<kind>" presents a beat once per member (0.28.0: its once is spent per member); 0.28.0: a select: sequence raise judges each later beat again at its turn, once an earlier beat of the raise has played, and an occasion may declare outsideRun: true to be raised after terminal: holds; at equal priority a sub-kind’s beat outranks its parent kind’s; 0.26.0: a beat or entry may target="kind:<kind>" — it answers every member, occasion.target is the raised member typed by the kind, and a member-specific beat for the same cell outranks it at equal priority; lute beats marks a fallback another beat always covers; 0.25.0: a scene share:, bundle <beat share=…> or <entry share=…> beside a written once names a project-wide key whose beats are spent together — a malformed key, a share without a spending once, or beats of one key with different onces are E-BEAT-ATTR; a bundle <beat after=…> is an eligibility conjunct; 0.24.0: occasion judge: before judges its on= objectives and settles the quests before its beats are decided; once: day / once: slot; a bundle beat may be an after: predecessor and an ::accept anchors an accept-driven quest; 0.23.1: an occasion target: domain may narrow to a member subset, { prefix, entity, members: [...] } — a member outside a closed kind is E-BEAT-ATTR, and the list restamps capabilityVersion only where declared; raising an occasion also fires a same-named declared world event; W-BEAT-ONCE-RUN-USER fires only on a defaulted once; 0.23.0: occasion select: sequence presents every eligible beat in order; a scene or bundle beat’s also: true rides along after a select: first winner and is ignored by W-BEAT-SHADOWED / W-BEAT-PRIORITY-TIE; bundle beats in lore documents; index beat rows carry when and title for menus; 0.22.0: an occasion’s target: may name a domain, { prefix, entity }, so a beat target must be <prefix>.<member> of that entities: kind — E-BEAT-ATTR with a did-you-mean; entry beats take once; pick: none passes on a select: all list; new W-BEAT-PRIORITY-TIE and W-BEAT-ONCE-RUN-USER)0.51.0.md §4.1–§4.5; 0.30.0.md; 0.29.0.md; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.25.0.md; 0.24.0.md; 0.23.0.md; 0.22.0.md; 0.21.0.md; CHANGELOG 0.23.1
Play / test harness (lute play scripts, derivation in trace and test)0.22.0 (engine: play steps write engine-owned state and facts, reserved relations included; a script starts from a save — visited:, presented:, quests:, entriesRead: — varies choose: per step, fires world events with event:, and asserts with step and top-level expect:; lute test runs every play carrying an expect:; lute trace / test / play apply seed facts and Datalog rules by default, derive: false / --no-derive restores the 0.21 answer)0.41.1 (a bridges: answer may take the long form { fields, writes }, writes being the runtime’s bridgeResult.writes, applied at the call and sent by --events; the human transcript shows a beat’s advances: movement and each clock raise it made or found closed; 0.41.0: an advance: step’s engine: writes land before the clock moves, so the new day’s raises read them — a script that relied on the 0.40 order moves those writes into a following engine: step; lute play --events prints a gate refusal as a rejected line (exit 1), while human and --json play and lute test keep their refusal text; 0.30.0: a state: key, expect.state, lute trace --state and lute calendar --axis take a state path in either spelling (run.visits["lab-b2"]), and a fact seed or expectation may quote an argument (at("lab-b2")); 0.28.0: play and test expect.exit is expect.end: complete | terminal | incomplete | error; a test’s menu expectation is expect.options (was offered) and its quest list only accepts:; a play script reports every usage error at once, each located; a hub script that runs out halts incomplete in lute trace / lute test as in lute play; vacuous assertions are refused (E-TEST-NEEDLE, a mocked occasion the engine would not raise); an include: may carry label:; 0.27.0: lute play, lute run, lute trace and lute test execute the compiled IR on one walker, held to one transcript by a differential test; lute play refuses a raise whose gate is false or once terminal: holds (E-OCCASION-GATE), an occasion: step may carry engine: writes and a payload:, and include: takes repeat: / choose: / bridges:; transcript needles judge their attribute blocks; lute test gates a document on its project’s verdict; play and test scripts are validated before they run; 0.26.0: lute trace / lute test follow a taken ::next, apply a handler ::accept as play does, and fail a test that walks a beat whose when is false under its mocks unless it asserts eligible:, naming the false premise; lute test loads the project once and runs in parallel; advance: { to: … }, step expect: { clock }, engine: { accept: [...] }; accepts: resolves quests project-wide; 0.25.0: a write that makes two exclusive facts hold halts lute play at the step (exit 1) and refuses the lute trace / lute test walk with E-FACT-EXCLUSIVE, and seeded facts that already break an exclusion refuse the run; a bridges: answer may omit result fields no content reads, and lute play no longer halts at a call none of whose results are read; 0.24.0: advance: and include: play steps move a declared clock and raise its occasions; a plugin call is answered by bridges: in play, test and trace — a scene.* seed no longer answers it; lute calendar gains clock, visited() and per-occasion axes; 0.23.1: a ::end in lute play ends only the presentation it runs in and a step end: true ends the playthrough; step expect: gains quests / state / facts / notFacts; a *.test.yaml gains expect.facts / notFacts / eligible and beat:; trace and test apply reward credits: and walk objective bodies as play does; lute calendar --script replays the script’s steps; 0.23.0: lute beats, lute calendar and lute scenario knowledge overviews; play presents sequence lists and also beats, gains the presented: expectation, snapshots prev.run at newRun, and credits rewards; trace and run raise <occasion>@<target> and present one bundle beat with --beat)0.41.1.md; 0.41.0.md §5.2, §5.4; 0.30.0.md; 0.28.0.md; 0.27.0.md; 0.26.0.md; 0.25.0.md; 0.24.0.md; 0.23.0.md; 0.22.0.md; CHANGELOG 0.23.1
Clock (clock: schema declaration)0.24.0 (a schema may declare one clock: { day, slot, slots, raise, week } over two owner: engine paths; content reads the reserved read-only clock.index / clock.weekday / clock.weekdayLabel; beats spend once: day / once: slot; raise: names the occasion an advance raises, or a map { slot, dayStart, dayEnd }; a clock may omit slot / slots and count whole days; a malformed clock is E-CLOCK-DECL)0.41.0 (an advance’s host writes land and settle before the clock moves, and a terminal they make true ends the input without moving it; each crossed midnight then raises dayEnd and dayStart, the destination raises the slot occasion, and the clock { from, to } event stays last; a beat’s advances: moves after its presentationEnd; raiseAtStart: true states that the host raises dayStart and then the slot occasion at the run’s first position as two ordinary raiseOccasion inputs — the runtime raises nothing at begin / newRun; 0.28.0: the read-only clock.day / clock.slot aliases and, on a finite clock, clock.ended (checked in a play with expect: { clock: { ended: true } }); raiseAtStart: true says the engine raises the slot occasion (and dayStart) where a run starts; a type may name the clock’s enums ({ domain: clock.slot }, { domain: clock.weekdayLabel }); a day path defaulting below 1 is E-CLOCK-DECL; on a clock that ends on its first day, a slot past the last one is a dead guard or arm; a clock whose day path outlives the run keeps its spends and its end across a newRun; 0.27.0: a clock may declare its end — last: { day, slot } or days: N; an advance past it stops there and ends the clock, a later advance: is E-CLOCK-END, and the checker ranges clock.index and the day path over the finite span)0.41.0.md §5.4; 0.28.0.md; 0.27.0.md; 0.24.0.md
Runtime event contract (step, inputs, awaits, queries, snapshot)0.38.0 (the first Event version: a parser-free runtime steps a compiled bundle and streams deterministic events)0.53.0 (the runtime and the event contract do not change: a section is not a runtime boundary; the schemas move to lute-events-0.53 / lute-snapshot-0.53; 0.52.0: the awaitChoice menu carries the rendered prompt and option text with their translations (prompt, promptTexts, each option’s text and texts), which the snapshot’s pending await holds too; a jump with leaves ends the hub visits it leaves; a grant record’s reward carries attrs; 32-bit index, column and weekday casts agree with native; schemas move to lute-events-0.52 / lute-snapshot-0.52; 0.51.0: a plugin record carries its resolved raises and its rendered text attributes (attrs and localized), a grant record its kind’s raises; the runtime keeps bounded-jump counters for one walk and rebuilds them by replay, so the snapshot shape is unchanged; schemas move to lute-events-0.51 / lute-snapshot-0.51; 0.50.0: a granted reward’s effects follow its grant record as retract / assert / accept records naming their grant; host-token markers stay verbatim for the host to substitute; schemas move to lute-events-0.50 / lute-snapshot-0.50; 0.49.0: plugin event records (normal and halted) and the bridge await, with the snapshot and request that carry it, hold the record’s plugin stamps in an optional stamps object beside attrs, never merged; schemas move to lute-events-0.49 / lute-snapshot-0.49; 0.47.1: a line record’s text, segments and rendered translations read the IR escape rule — a literal \{{ renders {{ and only real markers are substituted; 0.47.0: a line record also carries the line’s texts / localeSegments with each {{…}} marker rendered, matched to the source marker by its trimmed text; 0.42.0: the nine staging records bg, music, sfx, vfx, actor, camera, cg, video, and sequence carry resolved attrs; timing and envelope ownership stays unchanged, and Event/snapshot schemas are lute-events-0.42 / lute-snapshot-0.42)0.51.0.md §3.7, §4.6, §5.6; 0.50.0.md §5.6; 0.49.0.md §3.1; 0.47.1.md §3; 0.47.0.md §2.2; 0.42.0.md §4; 0.41.0.md §5; 0.40.0.md §4; 0.39.0.md; 0.38.0.md, 0.53.0.md §1
Player bindings (player.bindings.yaml)0.40.0 (an optional project file beside lute.project.yaml mapping staging vocabulary and assets to player effects)0.47.1 (flow.*.target values are full occasion targets in the prefixed form (place.square), checked by the rule beat and play targets use — a bare member, or a target on an occasion without a target domain, is E-BINDING-KEY; 0.47.0: ui.title gains optional text, the title heading, and background / music become optional, an empty ui.title being E-BINDING-SHAPE; bindingsVersion follows the luteVersion rule — W-BINDINGS-VERSION-STALE when it differs from the toolchain line (lute fix raises an older value), and a section newer than the declared line is E-BINDINGS-VERSION (vfx custom 0.41, volumes 0.42, ui.ended 0.46, ui.title.text 0.47); 0.42.0: bindingsVersion: "0.42" and the -0.42 schema line; optional volumes: { member: gain } gains are finite and in [0, 1], with E-BINDING-KEY, W-BINDING-MISSING, and E-BINDING-SHAPE diagnostics. The player resolves volume only through this map.)0.47.1.md §2; 0.42.0.md §3; 0.41.0.md §7.7; 0.40.0.md §5
  • 0.53.0 (“dogfood round 3”) changes what the toolchain answers, with no new keyword or element and no IR change: bounded loops across top-level sections (E-JUMP-LOOP-SCOPE no longer names a section), references inside structured plugin frontmatter (record, list, map) checked with their field path, accept routes (in-script accepts activate whatever follows; accept="external" follows="…" is host policy; accept deadlocks are E-QUEST-START-CYCLE), a marker that opens on a literal brace run (E-INTERP-BRACE replaces E-CEL-PROFILE), translations that keep their source’s markers (E-L10N-MARKERS), pair lints with scope: document, lute fix that keeps stamps and names its rules, and one project input layer per command and per LSP root. New code: E-L10N-MARKERS. The IR, the runtime, capabilitySnapshot and the plugin manifests do not change. See the 0.53.0 proposal.

  • 0.52.0 closes checker/runtime gaps with no new keyword or element: literal braces (E-INTERP-BRACE; \{{ in label-grammar values), quest top-level content refused (E-GRAMMAR-NOT-ADMITTED), hub leaves (jump.leaves, lute.core.hubLeave/1, E-JUMP-INTO-BODY) and the hub stage head, checked and rendered prompts and labels on the awaitChoice menu, plain-text objective titles (E-OBJECTIVE-TITLE-INTERP), reward-kind attributes on <reward>, and lute impact / envelope provenance over [accept] and [raises] edges. New codes: E-INTERP-BRACE, E-JUMP-INTO-BODY, E-OBJECTIVE-TITLE-INTERP. capabilitySnapshot does not move. See the 0.52.0 proposal.

  • 0.51.0 adds flow and text with no new keyword or element: bounded backward jumps (::jump{to max}) that every flow analysis joins at labels and iterates to a fixed point (which also closes the forward-jump gaps in definite assignment and stage state, #215), declared occasion raises (effects.raises, raisedBy: declared) drawn as [raises] edges, localizable text directive attributes with code= and lute loc support, and target: pair lints. New codes: E-JUMP-MAX, E-JUMP-LOOP-SCOPE, E-JUMP-INTO-LOOP, E-JUMP-LOOP-CROSS, E-PLUGIN-RAISES, E-RAISES-TARGET, W-OCCASION-NEVER-RAISED, E-LINE-CODE-RESERVED, E-LINE-ID-DUP, E-COMPONENT-TEXT-DIRECTIVE. Every capabilitySnapshot moves once (the core jump declares max); the schemas move to the -0.51 line and player saves to "0.51". Structured attribute values (#176) stay deferred. The release record is the changelog and the versioning guide.

  • 0.50.0 adds authoring surface with no new keyword or element: plugin stampAttrs entries scoped to line, branch, hub, choice or a directive with on: (a scoped stamp may be required), host interpolation tokens from a plugin’s hostTokens export, reward-kind effects (asserts, retracts, accepts) applied at the grant and read by every analysis, project diagnostics: levels for checker W-* codes, and target: node lints with string members. New codes: E-PLUGIN-STAMP-SURFACE, E-DIAGNOSTICS-CONFIG. The IR change is additive, with the new semantic id lute.quest.rewardEffects/1. See the 0.50.0 proposal.

  • 0.49.0 fixes correctness and diagnostics and makes the project model cheap; it adds no syntax. Plugin stamps move under one nested stamps object in the IR and on plugin event records and the bridge await; a <choice into=…> arm records first; a content line may carry wait. The checker refuses what it used to drop silently (stamps on ::jump / ::label / ::clear, list / map / record plugin attribute types, unchecked reference-typed frontmatter values, unquoted reward targets, start cycles and self-reads) and accepts bridge results and 'm' in family guards it used to reject. constraints prints the 0.49.0.constraints report (a reason per verdict, counterexamples only on violated, exit 1 on an invalid manifest); a single-file check names the quest references only the whole project can judge (W-PROJECT-CHECK-SKIPPED); W-BRANCH-ID-SHARED fires only where a play or test names the id; asset-exists reads line and named attributes; lint <DIR> uses the project. The project vocabulary is built once per project. See the 0.49.0 proposal and the changelog.

  • 0.37.0 is a breaking surface cleanup with no compatibility mode. Every renamed spelling is an error naming its replacement, never an alias: ::auto → ::actor, ::cut → ::cg, ::next → ::jump, ::mark{id} and a line’s id= → ::label{name}, choice label= → text=, <match on=> → <match subject=>, music action= → playback= (domain musicPlayback), cg / video action= → display=. lute fix <path>… rewrites those losslessly; camera numbers (zoom, moveX, moveY, shake, reset, easing), cg full, music track, sfx name, the manifest’s sequence:, the <scene> tag and a body # Title have no lossless rewrite and are migrated by hand, as is a pov: for every document with {mono} lines. The IR renames addr → position, capabilityVersion → capabilitySnapshot, shots → sections, recordKey → selectionKey, the kinds background / sprite / cut → bg / actor / cg and the roles monologue / offscreen / voiceover → mono / os / vo; it adds family and a nested timing object to every command and a voiceKey to every line, and drops provenance.injected. An engine gated on the 0.36 IR minor must move to 0.37 and migrate each field it reads. See the 0.37.0 proposal (§2.2 is the complete rename table, §7 the lute fix contract).

  • 0.33.0 makes engine capability explicit. The execution IR and project index carry compiler-derived, sorted requiredSemantics; the semantic-id registry names the engine-visible obligations. A lute.engine.yaml matrix is checked only after the pre-1.0 exact-major.minor IR gate: run/play refuse unsupported ids before playback with E-ENGINE-SEMANTICS, while author-time check --engine/check-project --engine report E-CHECK-ENGINE-SEMANTICS. The reference matrix supports every current id; plugin meaning remains gated by the exact capability snapshot. See the 0.33.0 proposal and the changelog.

  • 0.32.0 standardizes the CEL and execution contracts. Conditions use the closed standard-CEL profile with int / double and list-form fact queries; Datalog rule syntax is unchanged. Scalar state removes number; quest saves expose numbered questInstances, and quest grants carry instance / index. Execution IR uses {cel, expr, authored?} slots, celEnv, and owner; W-QUEST-STATE-HAS replaces W-QUEST-STATE-ISSET, with new E-FACT-QUERY and E-RUN-OWNED-WRITE. See the 0.32.0 proposal and the changelog.

  • 0.30.0 writes names as written and reaches them like JavaScript. Every 0.29.1 document checks and compiles as before, and names 0.29 refused for a - or a leading digit are accepted: every name a condition reaches by path, index or fact argument is letters, digits, _ or -, not starting with -, while a name read bare as @name (defs, def params, component and template params) stays an identifier. A name that is not an identifier is written in a condition as a quoted index (quest["zero-coke-001"].state) or a quoted fact argument (holds('at', ["lab-b2"])); after a . it is E-PATH-IDENT, naming the bracket spelling. The IR adds no field — path fields carry the canonical dotted path — and the schema file is renamed schemas/lute-ir-0.30.schema.json; lute.core does not move, so neither does capabilityVersion. The release record is the changelog and the versioning guide.

  • 0.29.1 is a bug-fix patch on the 0.29 line: no new syntax, no IR shape change. Every 0.29.0 document checks and compiles as before, and ids the engine owns keep their spelling again: a target whose members no entities: kind lists and an entry’s category accept -, as in 0.28.1, while every name Lute declares stays an identifier. Artifacts differ only in the version strings, and schemas/lute-ir-0.29.schema.json kept its name and $id. The release record is the changelog and the versioning guide.

  • 0.29.0 has one identifier rule and a spelling for intent. A 0.28.1 document checks as before unless it writes a name with a - (or another character) where 0.28 accepted one — a share= key, a document id, a branch, hub, choice or mark id, an enum or entity member, a plugin’s occasion, event or enum — which is now an error at the name naming the camelCase spelling. terminal: keeps its string form beside the new long form { when, persists }; lute check-project prints causes first (lute.project.yaml, plugins, schemas, documents in path order, then the project-wide rows); W-CHAPTER-STALL warns only on a chapters: chain; and lute new scene --occasion writes the on: it was asked for. The IR adds the optional terminalPersists and the schema file is renamed schemas/lute-ir-0.29.schema.json; lute.core does not move, so neither does capabilityVersion. The release record is the changelog and the versioning guide.

  • 0.28.1 is a bug-fix patch on the 0.28 line: no language change, no IR shape change. Every 0.28.0 document checks and compiles as before; artifacts differ only in the version strings, and schemas/lute-ir-0.28.schema.json kept its name and $id. The release record is the changelog and the versioning guide.

  • 0.28.0 renames as clean cutovers and checks every slot the same way. A 0.27.0 document that uses none of the renamed spellings parses as before; each old spelling is an error naming the new one — the manifest’s sequence: is chapters:, <quest after=> / <reward on=> / <objective when=> are follows= / outcome= / visibleWhen=, a test’s expect.offered is expect.options and accept: is accepts:, expect.exit is expect.end, ::camera{move-x move-y} is moveX / moveY, plugin exports are camelCase and lute new --on is --occasion. What else moves: every condition slot runs the typed and literal-domain checks a when gets (E-CEL-TYPE, E-WHEN-LITERAL-DOMAIN); every YAML surface refuses unknown keys (E-MANIFEST-KEY, E-PLUGIN-KEY, E-STATE-DECL); reserved words are E-RESERVED-NAME where declared; a flag given any value but true / false is E-FLAG-VALUE; spentBy latches; select: sequence re-judges each beat at its turn and a for beat spends once per member; kind labels win over cast names in text; and new advisories — among them W-TEXT-SINGLE-BRACE, W-RELATION-TIER-IMPLICIT and W-SEASON-UNGATED — can appear on a 0.27-clean project. The IR renames three quest-layer fields and is otherwise additive; lute.core moves with ::camera’s moveX / moveY, so capabilityVersion moves. The release record is the changelog.

  • 0.27.0 is additive in syntax, but the checker says no more often and one runtime settles every tool. Every 0.26.0 document parses as before and the new syntax — for="kind:…", occasion payload: / raisedWhen:, terminal:, clock last: / days:, directive asserts / retracts, once: week, seasons:, rearm=, spentBy:, beat templates, sequence:, kind labels:, :plural(…) — is opt-in. What moves: ::set without an assignment operator is E-SET-SHAPE; a { domain: K } / { entity: K } path is member-checked and a <match> over it must cover K; a <match on="@def"> over a fact query is E-MATCH-RELATION-SUBJECT; curly-quoted attribute values are E-ATTR-QUOTE; a run-reading quest with no tier= warns W-QUEST-TIER-IMPLICIT; lute trace, test, play and run execute the compiled IR on one walker, so behaviour the runtimes disagreed on (directive effects in trace, exclusivity after every write, bridge unknowns, quest settle order) has one answer; lute test gates on the project’s verdict and judges raisedWhen / terminal:; transcript needles judge their attribute blocks; and messages drop their spec citations. capabilityVersion does not move. The release record is the changelog.

  • 0.26.0 is additive in syntax, but several answers move. Every 0.25.1 document parses as before and the new syntax — add:, defaults.uses globs, defaults.questTier, { entity: K } attribute types, @@who:, param default:, directive when=, target="kind:…", rule-body count / countDistinct, sharedName: — is opt-in. What moves: a test that walks a beat whose when is false under its mocks fails unless it asserts eligible:; trace and test follow a taken ::next, so expectations after a jump are judged; a path declared twice with different shapes is E-STATE-DECL-CONFLICT and a duplicate member is E-ENTITY-KIND-SHAPE; E-WHEN-LITERAL-DOMAIN also covers != / in and replaces E-ARM-DEAD for a non-member literal; an untyped bridge answer is refused; when is reserved on directives; and W-ENTRY-WRITE-REREAD / W-DISPLAY-NAME-DUP are new. capabilityVersion does not move. The release record is the changelog.

  • 0.25.1 is a performance patch on the 0.25 line: no language change, no IR shape change. Every 0.25.0 document checks and compiles as before; artifacts differ only in the version strings, and schemas/lute-ir-0.25.schema.json kept its name and $id. The release record is the changelog and the versioning guide.

  • 0.25.0 is additive in syntax, but a few answers move. Every 0.24.0 document parses as before and the new syntax — excludes:, changedOn:, share, a bundle <beat after=…>, accept="external", {{x:ordinalWord}} — is opt-in. What moves: a quest only a test or trace mock accepts: warns again (W-QUEST-NEVER-ACCEPTED; declare accept="external"); a write that breaks an exclusion halts lute play and lute trace; a project whose rules contradict a declared exclusion is E-RULE-EXCLUSIVE; W-LUTE-VERSION-STALE for a stamp inherited from the manifest is reported once, at the manifest; and a bridge answer may omit fields no content reads, so lute play no longer halts at a call whose results nobody reads. capabilityVersion does not move. The release record is the changelog.

  • 0.24.0 is additive in syntax, but a few answers move. Every 0.23.1 document parses as before and the new syntax — clock:, once: day|slot, activate=, complete=, until=, <on target>, per:, subsetOf:, cast present: / emotions:, effects: true, ::clear, %, a guarded ::set, {{x:ordinal}} — is opt-in. What moves: by= is judged at every settle (the old place-bound rule is until=); a beat’s when: narrows its body, so a match arm kept only to satisfy the checker is now E-ARM-DEAD; a plugin call’s result is answered by bridges:, not a scene.* seed; a cast that declares present: checks every line (W-CAST-ABSENT); and capabilityVersion moves for every document because lute.core gains ::clear. The release record is the changelog.

  • 0.23.1 is a patch on the 0.23 line: no new syntax, no IR shape change. A subquest whose tier differs from its parent’s is now E-QUEST-TIER-MIX; an occasion target domain may narrow to members:; a raised occasion first fires a same-named declared world event (every active quest’s <on event> handlers) and then judges its on= objectives, whose by deadline is judged only there, after done. An untagged component line takes the code lute tag would write, so its lineId may change once. The spec text is still the proposal that introduced each construct; the release record is the changelog and the versioning guide.

  • 0.23.0 is additive, but a sharper checker can redden a clean project. Every 0.22.0 document parses and compiles as before (only the version strings move); the new syntax — by=, objective target=, also:, select: sequence, <beat> in lore, <hub prompt>, {{@p}} string params, prev.run.*, cast:, credits: — is opt-in. What can change is check-project’s answer: the condition decider now proves same-path contradictions (run.n > 5 && run.n < 3, x && !x) dead, so a guard that was always false is now E-BEAT-UNREACHABLE, E-ENTRY-UNREACHABLE, E-ARM-DEAD or E-OBJECTIVE-UNSATISFIABLE, and a project that declares a cast checks its speakers.

  • 0.22.0 is breaking for compiled identity, not for grammar. Every 0.21.1 document still parses and checks as before apart from the new diagnostics; what moves is the ids lute compile writes. The default voiceKey template gains {prefix} (the 0.21 default collided across documents), and a line expanded from a component gets its own {prefix}.{component}#{n}.{speaker}_{code} address. A project that recorded audio against the old keys pins identity.voiceKey: "{speaker}-{code}"; documents that ::use components re-export their localization and voice manifests. The toolchain side changes answers too: trace and test now apply the project’s Datalog rules by default. <quest tier> defaults to user, so no existing quest changes its persistence.

  • 0.21.1 is a patch on the 0.21 line: no new syntax, tighter static semantics. Checks that accepted a defect and shipped the wrong thing now report it: quest.<id>.state reads as an always-assigned lifecycle enum; a def body passes the CEL profile gate; a def in a directive attribute folds to its literal or is E-ATTR-DEF-DYNAMIC; a {{@def}} that cannot be inlined is E-INTERP-DEF; <quest> / <objective> / <on> close their attributes (E-UNKNOWN-ATTR); a single-quoted attribute value is E-ATTR-QUOTE; an is= + test= arm covers only what both prove; E-DUP-LINE-CODE runs over expanded components; and check-project runs the compile, the single-snapshot gate (E-CAPABILITY-MISMATCH), and E-DUP-VOICEKEY. The spec text for these rules is still the proposal that introduced each construct; the release record is the changelog and the versioning guide.

  • 0.11.0 moves the IR’s major.minor with nothing behind it — the first time this stack records that shape. 0.10.1 stayed inside 0.10 and cost nothing; 0.10.2 also stayed inside 0.10 but moved real content (meta.plugin); 0.11.0 moves the number itself out of 0.10 into 0.11 while carrying zero content or shape change — the artifact 0.11.0 produces is byte-identical to 0.10.2’s. An engine gated on IR 0.10 still has to widen its gate to 0.11 to keep accepting artifacts (the runtime contract gates on major.minor, not on whether anything inside actually changed), and schemas/lute-ir-0.10.schema.json is renamed to schemas/lute-ir-0.30.schema.json under the 0.7.0 precedent — a major.minor move renames the schema file regardless of why it moved. The release itself is entirely toolchain: a new schedule.yaml project-file layer and lute play command (0.21.0 removed the layer again and rebuilt lute play on occasions — Playing a story), plus two fixes in the shared reference runner (a compiled <when is=…> match arm now reads its structured expr instead of always falling through to <otherwise>, and a hub whose scripted decisions run out with an eligible option remaining now halts incomplete instead of silently converging).

  • 0.6.0 is the one breaking grammar revision in the current stack. It removed the persist= attribute so into= alone drives the choice run-record sugar, and made shot headings free text. Pre-0.6.0 documents carrying a bare into= (previously a silent no-op) now record.

  • 0.8.0 — the adoption release — is backward compatible in the grammar, but it is not a pure addition. Every item in it traces to a gap found assessing Lute against a real 777-scene / 583-quest game catalog, and two of those items have edges worth naming. The IR gains a command kind, end; an unknown kind is a hard error under the execution-model version policy, so a 0.7 engine MUST refuse an artifact carrying one — which is the intended signal, since termination is capability an older engine cannot fake. And E-STATE-COLLECTION enforces a rule that was always normative but never checked: a state: declaration typed list/record/map used to pass the shape validator and now fails. Everything else is optional and append-only — apart from that one declaration, a 0.7.0-clean document needs nothing but a restamped luteVersion: to check clean under 0.8.0.

  • addr widths are uniform per artifact, and that is now a guarantee. The index segment used to be a fixed four digits, so a shot with 100 or more records emitted 001-11500 beside 001-1400 and string comparison reported "001-11500" < "001-1400" — lexicographic order silently diverged from execution order. Both segments are now padded to a width computed from the document and held uniform across the artifact, so lexicographic order over addr equals execution order is something an engine may rely on. A document whose every shot emits fewer than 100 addresses compiles byte-identically to 0.7.0. addr is still a position regenerated on every compile, never an identity — the stable joins remain lineId / voiceKey.

  • 0.9.0 — vocabulary ownership — is breaking in validation, not in grammar. The compiler declares the seven content-vocabulary slots and ships no members, so a document writing emotion="delighted" needs its project to declare emotion; using a slot nobody declared is E-DOMAIN-UNKNOWN, an error, where action in particular used to be skipped outright. Every pre-0.9.0 enums: block still parses byte-for-byte — a bare sequence is shorthand for { members: [...] } — but a declaration of action MUST now supply exits: and one of anchor MUST supply default:, because those are the two places the compiler branches on which member, and it no longer infers them from a name prefix. The IR shape did not move in that release: no field was added, renamed, or moved, and irVersion read "0.9.0" only because a release re-aligns every axis — an engine gated on IR 0.8 had only to widen its gate to 0.9. The artifact’s content does move — enums becomes populated for a project that declares inline or through uses:, and capabilityVersion shifts. The authoring side is written up at Content vocabulary.

  • 0.9.0 also made an imported component body check like the content it is. Five of check()’s eighteen diagnostic stages were root-only and never ran over a component body, so the same lines checked clean through a ::use and dirty at scene level — content-line attribute rules, E-DUP-LINE-CODE, reachability, admission of content the walker does not process, and injection folding. All five run now, anchored at the ::use site with a prefix naming the component and its file. A component body that used to pass may report, and every such report was already reaching the artifact or already being silently dropped. What a component body still does not get is its own vocabulary scope: its uses: and its own inline enums: are both discarded at parse, so the body resolves vocabulary against the importing document.

  • 0.10.0 — the toolchain says what it knows — reddens documents, mocks, and the IR shape. Thirteen language changes, and the through-line is that every one of them is a place the checker already held the answer. Three of them can redden a document that checks clean today: ::set now types its right-hand side against the path it writes (E-SET-TYPE), the six logic tags close their attribute sets (E-UNKNOWN-ATTR, and E-AS-REMOVED for as= on a <choice>), and a <quest start> gate that can never open is E-QUEST-UNREACHABLE where it used to be silent. mocks/*.yaml is validated for the first time and its file: key is required — a mock without one is E-MOCK-SUBJECT. And the IR shape moves for the first time since 0.8.0: provenance.reason becomes provenance.explanation, so an engine gated on IR 0.9 must widen to 0.10 and rename the one field it reads. W-INJECT-CONFLICT is removed rather than narrowed, and the information it carried is dropped, not migrated — agreement with the declared default was its only trigger, so there was nothing left to warn about.

  • The 0.6.1 coverage warnings are honesty, not errors. They name the exact edge of what static analysis can prove — a stale luteVersion stamp, an unproducible trace mock — and never flip the exit code on their own. The third, W-UNPROVEN-RELATIONAL (a relational fact query the checker could neither prove nor refute), is gone since 0.20.0: check-project now decides relational guards, reporting the dead ones through their slots’ errors and the redundant ones as W-FACT-GUARANTEED, and a merely possible guard is the normal case and silent. The two 0.8.0 additions are ordinary findings rather than coverage claims — unreachable content after an ::end, and a translatable record missing a locale the bundle declares — but they carry the same severity. Promote any of them to an error with --deny <CODE> or --deny-warnings.

  • Design rationale lives alongside the specs. The four-tier state model’s why is recorded in state-model-design.md, a non-normative companion to 0.0.1 §9.

  • Capability surfaces are specified separately. Character/cast identity and the plugin system are capability proposals, not core scenario-DSL revisions. The plugin system’s current revision is 0.0.4, landing alongside 0.10.2: a plugin-owned, checker-validated frontmatter key now reaches the compiled artifact (meta.plugin) instead of being discarded at compile time. 0.0.3 — lute.core exports an empty enums, an enums entry may carry the long form’s member semantics, and the closed semantics flag vocabulary drops the two flags no consumer read — and 0.0.2 — option and frontmatter value validation, reserved stamp-attribute rejection, cross-cutting stampAttrs, and the declarative lower: { record, fields } form made real — remain its base as amended. See the specification index for all four.

The language and artifact shapes remain governed by the version claims above. The 0.43 agent-facing addition is the CLI contract: JSON reports have a flat command, schemaVersion, and exit-consistent ok; JSON-mode failures use E-CLI-USAGE, E-CLI-IO, or E-CLI-INPUT; lute schema describes the parser’s command tree; and local telemetry is opt-out with LUTE_TELEMETRY=0 or DO_NOT_TRACK=1. Artifacts, run --json, and event streams keep their existing non-report protocols.

0.48.0 changes the command line (and one behaviour, below); the language and artifact shapes are unchanged. A project is always named with --project <DIR>, and a command given no --project finds the nearest lute.project.yaml at or above the current directory (or at or above the file operand’s directory for check, compile, context and trace). A discovered project is used exactly like an explicit --project, so compile <FILE> and trace <FILE> inside a project now gate on the project verdict. check-project is folded into lute check: lute check --project <DIR> is the whole-project check, while lute check <FILE>… checks documents only. lute compile --project <DIR> -o <OUT> replaces compile --all, and lute compile --stream <FILE> replaces compile-stream. The check --json report is 0.48.0.check with files[] and, in whole-project mode, project_diagnostics; the 0.43.0.check-project report is retired. Flags are renamed uniformly (for example --out is -o, --output, --locales is --locale-bundle, context --target is --node). Earlier rows and notes on this page keep the spelling of the version they describe.