Skip to content

Language guide · Story DSL v1

Write the story as source

Lattice is TypeApe’s declarative story language. It records readable prose and the exact structure around that prose, so the same project can be edited, validated, and experienced.

1

StoryIdentity and project metadata

2

BlockA readable unit of narrative

3

TextVisible prose in braces

4

FlowAn explicit edge, never file order

Use the complete reference

This overview explains the language model. Use the four reference chapters when you need exact declaration shapes, allowed values, ownership rules, or recovery guidance:

  1. Source files and declarations — imports, comments, delimiters, identifiers, strings, prose, metadata, Story fields, entities, and Scenes.
  2. Blocks and Linear flow — every Block field and purpose, narration, Dialogue presentation, continuity annotations, terminators, and complete Linear constraints.
  3. CYOA reference — choices, Predicate schemas, grounded State, Groups, initial State, Guards, Pools, Modules, Effects, and exhaustive runtime rules.
  4. Validation and diagnostics — parse versus validate, error recovery, broken references, soft locks, runtime cycles, warnings, and what validation can and cannot check.

TypeApe currently reads and writes Story DSL v1. When a rule is broken, the diagnostic points to the affected source and explains what to fix.

Files, imports, and values

The configured entry file is the root. Split a project with quoted, relative imports:

text
# main.la
use "./story/characters.la";
use "./story/opening.la";

After imports are loaded, a project contains exactly one story declaration. Imports must stay inside the project, cannot form cycles, and cannot load the same file twice.

Lattice uses a small set of value forms:

FormExampleMeaning
Identifieropening, ch_maraStable IDs and references
String"en"Exact quoted text
Prose{ A light wakes. }Narrative text, including line breaks
List[opening, resolution]Ordered metadata values
Boolean / numbertrue, 3Accepted extension or event literals

Comments begin with #. Use semicolons after imports, declarations without bodies, content items, and flow statements. Metadata properties are comma-separated. A custom metadata key must start with x_; it is preserved but does not change built-in behavior.

Story, entities, and scenes

The Story declaration identifies the reading entry and project format:

text
story main (
  title: "Signal Garden",
  language: "en",
  format: novel,
  entry: opening,
  prose_unit: word_like,
  prose_scope: authored_body,
  prose_minimum: 4500,
  prose_target: 5000,
  prose_maximum: 5500,
  beat_order: [opening, midpoint, resolution]
);

title, quoted language, format, and entry are required. initial_state is available only in CYOA.

The optional prose goal records a durable authoring target. Declare prose_unit, prose_scope, and prose_target together. word_like counts word-like tokens; non_whitespace_graphemes is useful for requests expressed as Chinese 字数. Optional minimum and maximum bounds must contain the target.

Entities give characters, locations, and items stable identities:

text
entity character ch_mara (
  name: "Mara",
  role: protagonist,
  traits: [observant, stubborn]
);

entity location loc_garden (name: "Signal Garden");
entity item it_key (name: "Signal key");

Scenes group narrative context. Their pov must reference a character and location must reference a location:

text
scene garden (
  title: "The garden",
  beat: opening,
  pov: ch_mara,
  location: loc_garden,
  prose_target: 1200
) {
  # Blocks live here.
}

Blocks, prose, and dialogue

A Block is the smallest flow-addressable passage. A publish-ready Block needs a globally unique ID, a title, a narrative purpose, visible content, and exactly one terminator. During outline work, content may remain empty while its stable identity, summary, flow, and optional prose budget are inspected.

text
block opening (
  title: "The warning",
  purpose: setup,
  summary: { Mara finds the signal source. },
  prose_target: 500,
  setup: [signal_arc]
) {
  text: {
    A light wakes beneath the garden.
  };

  dialogue ch_mara (
    mode: whisper,
    emotion: cautious,
    stage_direction: { She leans toward the soil. }
  ): { Not again. };

  next -> answer;
}

Dialogue speakers must be declared characters. mode can be speech, thought, whisper, or shout; position, emotion, expression, pose, and stage direction are optional presentation metadata.

Narrative continuity annotations

setup, requires, and resolves label lightweight narrative promises. They help validation reason about setup and payoff, but never create flow or mutate CYOA State.

Structured events can attach richer authoring evidence:

text
foreshadowing fs_signal: event {
  subject: ch_mara,
  object: it_key,
  predicate: notices
};

reveals rv_signal resolves fs_signal: event {
  subject: ch_mara,
  object: it_key,
  predicate: understands
};

Linear story flow

Every Linear Block ends in one automatic continuation or one ending:

text
next -> target_block;
text
end;

The configured entry must lead through one unbranched, acyclic chain to exactly one ending. Linear story rejects choices, merges, disconnected Blocks, cycles, State, Pools, and Modules.

CYOA choices and endings

CYOA may branch and converge. An ordinary choice has at least two options:

text
choice {
  option inspect_key: { Inspect the key } -> inspect;
  option leave_garden: { Leave the garden } -> departure;
}

Option labels describe reader-facing intentions. A choice changes route topology, but it does not remember what the reader selected. If a later rule depends on that decision, model it as declared finite State.

Finite State

CYOA State is intentionally finite and closed-world: no scripts, variables, counters, randomness, or arbitrary expressions.

First declare a Predicate schema and a grounded State:

text
predicate knows (subject: character, object: item);

state st_knows_signal (title: "Signal understood"): event {
  subject: ch_mara,
  object: it_key,
  predicate: knows
};

An ungrouped State is a permanent flag activated with add. A State Group is one mutually exclusive slot changed with set:

text
state group location (
  members: [st_location_gate, st_location_garden]
);

Guards use lists of State IDs:

  • requires: every listed State is active.
  • requires_any: at least one listed State is active.
  • forbids: every listed State is inactive.

Pools and Floating Modules

A Pool pauses the main route while eligible, one-shot Modules become available. It resumes only when its exit Guard matches.

text
pool investigation (
  title: "Investigate the garden",
  exit_requires: [st_knows_signal]
);

scene garden (title: "The garden") {
  block arrival (title: "Arrival", purpose: setup) {
    text: { The buried key pulses beneath Mara's hand. };
    enter investigation -> departure;
  }
}

A Module belongs to one Pool and cannot jump into the main story or another Module:

text
module md_decode (
  title: "Decode the key",
  pool: investigation,
  entry: decode,
  forbids: [st_knows_signal]
) {
  scene decoding (title: "The buried code") {
    block decode (title: "Decode", purpose: development) {
      text: { The light resolves into a patient sequence. };
      complete (add: [st_knows_signal]);
    }
  }
}

On Pool entry and after each completion, TypeApe checks the exit first, then presents eligible unfinished Modules. If the exit is false and no Module is eligible, the story is soft-locked and validation fails.

Parse, inspect, validate, experience

Use the same order after each coherent source change:

  1. Parse the configured entry.
    Fix syntax and import diagnostics before interpreting later phases.
  2. Validate and inspect the outline.
    Check the structure, authoring contract, exact prose progress, and largest local deficits while prose is incomplete.
  3. Validate for publish.
    Restore the visible-content requirement, fix every error, and judge each warning against the intended experience.
  4. Read the affected route.
    A valid graph can still miss the creative intention.

Preserve stable IDs unless the identity itself must change. When TypeApe reports an error, fix the underlying source instead of hiding it behind x_ metadata.

Next: read the complete source and declaration reference.