Parlance Editor — User Guide

A complete reference for the Parlance visual editor. The editor runs locally against your project's data/ directory; every change is written to disk as human-readable JSON so your narrative data stays in git.


Table of Contents

  1. Starting the editor
  2. Layout overview
  3. Entity types
  4. Entity list & search
  5. Entity detail — view & edit
  6. Dialogue canvas
  7. Quest canvas
  8. Quest dependency graph
  9. Location map
  10. Validation panel
  11. Reports — coverage & reference index
  12. Playtest mode
  13. Undo / redo and navigation history
  14. Data format & git workflow
  15. Localization & VO
  16. Review — reading someone else's branch
  17. Getting help & sending feedback
  18. Bringing in a story from another tool

1. Starting the editor

Launch Parlance and point it at a project folder. A project is just a directory of narrative files — most often the data/ directory inside your game's own repository, so the story is versioned alongside the game that reads it. Nothing is imported and nothing is copied into a library: the editor reads and writes those files in place.

A folder counts as a project if it contains a parlance.config.json, a data/ directory, or a schema/ directory. New Project… offers a starting point: Blank lays down the standard empty layout, and First conversation seeds a tiny working sketch — one character and a branching dialogue that remembers whether you accepted their help — so your first project isn't an empty directory. The command line mirrors this with parlance init <dir> [--template first-conversation] (the default is blank). Opening an empty folder still scaffolds the layout on first save, exactly as before.

Start with the demo

The Mistfall Inn ships with the editor: a complete, tiny murder mystery — one night, one body, three suspects, three endings — with no art and no engine required. It exists so your first session is spent on something real instead of an empty directory.

In the desktop app, choose File ▸ Open the Demo, or Open the demo on the welcome screen. The first time, Parlance copies the demo to Documents/Parlance/The Mistfall Inn and opens that copy — the copy is yours to edit, break and play with, and the app's bundled original is never touched. Every later Open the Demo reopens the same copy, edits and all; it is never overwritten. To start over, delete or rename that folder and choose it again. (On Linux, "Documents" is your XDG Documents directory, or your home folder if none is set.) The copy is an ordinary folder, not a git repository — the same as a New Project….

Start with The Common Room under Locations, or go straight to dlg_examine_body and press ▶ Play. Each of its parts is demonstrating something specific; the README.md in the demo's folder (examples/mistfall-inn/README.md in a source checkout) maps features to the scenes that show them off.

Where your files live

<your game repo>/
  data/    the narrative — what your game reads at runtime
  tests/   route fixtures; a shipping game never loads these
  lore/    Markdown canon docs, edited on the Lore surface; never shipped
  review/  review threads; invisible to the runtime and the validator

Full detail, including how to relocate any of those directories, is in §14 and in the setup guide's per-project configuration section.

Running from source

Contributors and self-hosters can run the editor as a local host plus web client instead of the packaged app. That path — prerequisites, the two dev-server processes and their ports, pointing the host at a project, production builds, and packaging — is documented in SETUP_AND_MANAGEMENT.md §§2–3, which is where the build-level detail lives so this guide can stay about using the editor.

Everything in this guide applies identically either way: the same editor, the same validator, the same files on disk.


2. Layout overview

┌────────────┬──────────────┬──────────────────────────────────────┐
│ Header: title, breadcrumb (Type › entity), ⌘K hint, Undo/Redo, AI │
├────────────┼──────────────┼──────────────────────────────────────┤
│ Type       │ Entity list  │ Main panel                           │
│ sidebar    │ pane         │ (fills remaining width)               │
│ (icon +    │ Search,      │                                      │
│ label +    │ group, +New, │ Entity detail form                   │
│ count per  │ entities     │   — or —                             │
│ type;      │              │ Dialogue canvas                      │
│ Custom     │              │   — or —                             │
│ types;     │              │ Quest canvas / dependency graph      │
│ Reports,   │              │   — or —                             │
│ Lore … in  │              │ Location map / custom-type grid /    │
│ the footer)│              │ Reports / Lore / other surfaces      │
├────────────┴──────────────┴──────────────────────────────────────┤
│ Validation bar (appears only when there are issues; click to expand)   │
└────────────────────────────────────────────────────────────────────────┘

The left side is a fixed type sidebar — one row per entity type, each with an icon, label, and a live entity count, always in the same place (it never reflows). Below the built-in types, a Custom heading lists the project's own types from data/types.json, each opening as a grid; its + declares a new one (§3, Custom types — the grid). Click a row to load that type's list in the pane beside it (search, group, create, and the entities themselves).

The sidebar's footer holds the surfaces that are not entity types: Reports (§11), showing the total error/warning count once the project has any; Localization (§15); Lore, the project's Markdown canon under lore/, which you read and edit there (§5, Lore — linked Markdown documents); Drafts, your own draft branches (§16); and Review (§16), with a badge counting reviews waiting for you. The « button collapses the whole panel — sidebar and list — to a thin 32px rail to maximise canvas width; » expands it again. The collapsed/expanded choice is remembered across sessions.

Selecting an entity from the list loads the main panel. For dialogues and quests the main panel is a visual canvas rather than a form; for locations, selecting no entity shows the location map (§9).

Command palette (Cmd/Ctrl+K). Opens a searchable palette from anywhere in the app — even while a text field is focused. Type to fuzzy-match against every entity across every type (by name, id, or title) and jump straight to it, or run a curated action (Create new <Type>, Open Reports). This is the fastest way to get anywhere once a project has more than a handful of entities; ↑/↓ to move, Enter to open, Esc to close.

Breadcrumb. The header shows <Type> › <entityId> for whatever you're currently viewing. Click the type segment to jump back to that type's list.

Text Size. The dropdown next to Undo/Redo (90%–150%) scales font size across the whole editor — chrome, canvas, the Play panel transcript, everything. It does not scale layout: buttons, icons, and panel widths stay put, so at higher sizes a few fixed-width labels (the type sidebar's, in particular) may elide with … rather than grow to fit. That's a deliberate trade-off — the alternative is scaling the whole UI, which cannot be made to render within the actual window at every size (ask if you want the history).

Resizable side panels. The node inspector and the Play panel (§6, §12) both have a drag handle on their left edge — drag to resize, double-click to reset to the default width, or focus it and use ←/→ (Shift for a bigger step) and Home to reset. Width is remembered per panel across reloads. The canvas always keeps a minimum width, so a dragged-wide panel yields if you narrow the window.

Persisted UI preferences. Several view choices are saved to the browser's localStorage so they stick across reloads: entity-panel collapsed state, detail default (edit form vs. raw JSON), dialogue node density, per-canvas minimap visibility (dialogue / quest / quest-dependency / location map), the validation-bar collapsed state, Text Size, and both resizable panel widths. (See the setup guide for the exact keys.)


3. Entity types

Type What it represents Key fields
Skills Stat names used in checks (wit, empathy, …) id, name, description; optional cluster
Variables Global state values — flags (bool), counters (int), text (string) id, kind, default
Factions Groups whose reputation the player can influence id, reputationRange.min/max
Characters NPCs and the player id, optional archetype, optional stats (skill → int)
Dialogues Conversation graphs nodes, choices, checks, onEnter effects
Quests Staged tasks with trigger/completion conditions stages, outcomes, dependencies
Locations Named places referenced from quests / lore id, name, optional zone
Endings Named story endings id, name, summary, unlockedBy, optional kind (success / failure / neutral)
Codex Player-facing knowledge entries (codex / bestiary / glossary) id, name, body, optional category, unlockedBy? (absent = always unlocked)
Items Things the player can carry. Possession is runtime state; this registry gives an item a player-facing identity id, name; optional description, tags
Portraits Character portrait registry (referenced from dialogue nodes) id, character, tags
Cutscenes Manifest: opaque engine asset key + effects applied on completion (effect-triggered via play_cutscene) id, asset, skippable, effectsOnComplete, entersDialogue?

All types share a stable string id used as the cross-reference key everywhere. The sidebar row for each type shows a live count of how many entities it holds.

Custom types — the grid

A project can declare its own types in data/types.json — weapons, loot tables, recipes, anything a designer would otherwise keep in a spreadsheet beside the repo. Each field has a type (string, number, boolean, enum, reference, array), and both validators check every row against it, including that a reference names an entity that exists. The demo declares two: open examples/mistfall-inn and look under Custom in the sidebar. Drink keeps its rows in one file, data/drinks.json. Supplier keeps one file per row in data/suppliers/, and its stocks field is a list of references to drinks.

{
  "drink": {
    "name": "Drink",
    "plural": "drinks",
    "fields": {
      "price": { "type": "number", "required": true },
      "strength": { "type": "enum", "options": ["small", "house", "strong"], "default": "house" },
      "servedBy": { "type": "reference", "target": "character" }
    }
  }
}

Rows live in data/<plural>.json ({ "drinks": [ … ] }) or one file per row in data/<plural>/. The single file is the safer choice for a table edited in bulk: a grid save rewrites it in one step, all rows or none. With one file per row, each row is written whole, but if the editor is killed mid-save some rows can land and others not. Reopen the grid to see exactly what was saved, and save again.

Declaring a type. Click + beside Custom in the sidebar. Give it a name. The id (what references use) and the file it's stored in follow from the name until you edit them. The new type opens as an empty grid with its Fields panel open.

Fields. The Fields button on any grid opens the type's declaration:

Renaming a field renames it in every row, so its values go with it. Removing a field deletes its values from every row, and the panel says so before you save. Save the declaration and the rows together. The editor refuses a declaration that would misbehave: an id that shadows a built-in type, a file name another type or a built-in folder already uses, or a reference to a type that doesn't exist. Delete type is available once a type has no rows and no other type references it. The panel is locked while the grid has unsaved cell edits. Editing types.json by hand still works, and the editor picks it up as soon as the file is saved.

A custom type opens as a grid: one row per entity, one column per field, id first.

Everywhere else in the editor. Custom rows are first-class outside the grid too:

If a row changed on disk while you were editing it (a teammate's pull, an agent, a text editor), the grid re-reads it and keeps your edits on top of the new version. If the save gets there first, nothing is written and a notice lists the rows. Your edits are still there, applied over the new versions. Check them and save again. If someone else created a row with the same id as one you added, your row is not saved over theirs: remove yours or add it under another id. A save never quietly overwrites someone else's change.



5. Entity detail — view & edit

Selecting any non-dialogue, non-quest entity opens the detail panel. The toolbar's JSON / Edit toggle switches views; your choice persists across reloads.

If the entity has a loreRef field pointing to a Markdown file, a Lore button appears in the toolbar. Click it to open an inline Markdown viewer of the referenced canon doc — useful for keeping the entity's narrative context visible while editing.

Edit mode (default)

Fields are grouped into labelled sections in a fixed order — Identity, Description, Relationships, Logic, Structure, and (if the type has them) a Metadata section that starts collapsed, since loreRef and tags are low-priority housekeeping fields you don't need open by default. Section headers only appear when a form genuinely has more than one section, so small entities (e.g. a skill) still just show a flat list.

Most fields get a real editor, not a generic fallback:

Click Save to write the change to disk. If the file changed on disk since you loaded it (e.g., another tool edited it), a 409 conflict error is shown and the save is blocked — click Reload to see the latest version.

JSON mode

Shows the entity's raw JSON with syntax colouring — useful for a quick read-only glance, or for copying the exact on-disk representation. Below the JSON, any validation issues scoped to that entity are listed.

Click Delete (then Confirm delete) to remove the entity permanently.

Flow (flags, counters, items)

When the selected entity is a variable or an item, a Flow panel at the bottom of the detail pane shows every place that variable is used, split by direction:

Each row names the owning entity and the exact path (e.g. nodes/node_cleared/onEnter[0]) and is clickable — it jumps you straight to that place: the dialogue node (and choice) selected on the canvas, the quest stage selected on the quest canvas, or the entity's form. If a variable is only ever written or only ever read, the panel says so: a flag nothing checks, or a condition that gates on a flag nothing sets, is usually a wiring mistake worth catching early.

A faction and a character carry the same panel for their reputation and relationship state — Reputation / Relationship checked by (reputation and relationship conditions) and adjusted by (adjust_reputation, adjust_relationship effects) — whenever anything checks or adjusts it. That is where a REP or REL warning in the Validation panel (§10) takes you.

Creating a flag inline

You don't have to define a flag before you can reference it. In any condition or effect picker (a flag / counter / item field), type a name that doesn't exist yet and choose the + New flag “…” row that appears (New counter or New item in those fields). The row already shows the name slugified to a valid id — type Talked past gate and it reads New flag “talked_past_gate”. The variable is created immediately, and the field is set to it — no trip to the Variables list. Fill in its description later.

Exclusive flag groups

Some flags describe one choice among several: the player sided with the guard, or the thief, or neither. Nothing in the format stops a single effect list from setting two of them, and a game that later tests one of them reads a state the story never meant to allow. Declare such sets in data/rules.json:

{ "flag": { "exclusiveGroups": [["sided_guard", "sided_thief", "sided_neither"]] } }

Each group lists at least two flag ids. When one effect list — a node's onEnter, a choice's effects, a quest stage's onComplete, a quest outcome's effects, or a cutscene's effectsOnComplete — sets two flags of the same group to true, validation reports a FLAG error naming them. The check is per effect list: it does not follow the player across nodes, so clearing the old flag before setting the new one elsewhere in the story is still your job. There is no form for rules.json in the editor; edit the file directly, and the next validation pass picks it up. Renaming a flag (§5) rewrites its entries in these groups too.

Lore — linked Markdown documents

lore/*.md is your canon: character bios, faction histories, the world bible. It never ships to the game and the validator never reads its content. The Lore row at the bottom of the sidebar lists every lore file (except lore/dictionary.md, the prose dictionary — §11) and edits them in place.

Links. Link an entity with an ordinary Markdown link using the parlance: scheme and the singular entity type:

[Mara](parlance:character/mara) keeps the ledger at [the inn](parlance:location/inn).

Types: character, faction, quest, location, item, skill, dialogue, codex, ending, cutscene, variable, and any custom entity type by its id. The file stays plain Markdown — it reads normally on GitHub or in any viewer; the editor makes the links live. Links inside backticks or a fenced code block are examples, not links.

The editor is the Text view's editor: syntax colour, a line gutter, ⌘F find & replace, and autocomplete — type ]( and pick parlance:, then a type, then an id (Ctrl+Space re-opens the list). Save with ⌘S or Save; unsaved lore is guarded like an unsaved script, so navigating away asks first. If the file changed on disk since you opened it (a git pull, another editor), the save is refused rather than overwriting it: the banner reports it and you choose Discard mine, load the file or Keep mine, overwrite. Preview renders the file with live links; + New file creates one (lowercase name, - and _ allowed).

Unlinked mentions. An entity's name written as plain text (Mara, the Mistfall Inn) is underlined, and listed under the editor with a Link button that turns it into [Mara](parlance:character/mara) in one click. With the caret inside a mention the toolbar offers the same. Names that are ordinary English words on their own (a character called "Hawk") are not flagged.

Mentioned in lore. An entity's detail panel lists every lore line that links to it; click one to open the file at that line. The entity's loreRef stays its one canonical document — backlinks are everything else. Links are usages like any other: they show in Find usages (§11), and ⌘⇧F content search covers lore text too.

Deleting an entity lists everything that refers to it — data and lore links — in the confirm step. It is a warning, never a block: the lore links go dangling (lore about a cut character is normal) and show up in the prose report as Dangling lore links.

Renaming an id

Rename id… in the entity's detail header (or Rename id of … in the ⌘K palette, for whatever is selected — dialogues and quests included) changes an entity's id and rewrites every reference to it in the same operation:

A character rename also renames its routing flag, active_dialogue__<id>, because set_active_dialogue writes that flag by name.

Preview first. Type the new id (checked as you type, with the same rules as creating an entity), then Preview. The preview counts what changes per category, lists every lore line it will rewrite as before → after, and expands to every file it touches. Rename stays disabled until you have previewed the exact id in the box. If anything changed between the preview and the confirm (a save, a pull, a lore edit), the rename is refused with The project changed since the preview — preview again. and the fresh plan is shown instead, so you never confirm one list of changes and get another.

Refused: an id that already exists in the same type, an invalid id, and a character's active_dialogue__ flag on its own (rename the character). An id that also names an entity of a different type is allowed with a warning. Also refused, before anything is written: a rename whose new file name is already taken by a file holding something else (file … already exists (it holds id '…'); rename would overwrite it), and a rename that would have to save an entity whose own id is invalid — the entity being renamed or any referrer ('Mara' is not a valid id; rename can't fix invalid ids yet — edit the file by hand).

A file named for a different id than the one inside it is fine, and renaming the entity to match its file name is how to tidy it up: quests/old_name.json holding task_new, renamed to old_name, is rewritten in place — the preview lists it as one update, and the file keeps its name.

Quest stages and outcomes are the one id inside an entity that can be renamed, because other entities name them: quest conditions ({type:"quest", stage}, {type:"questOutcome", outcome}), advance_quest's toStage, and the questStages / questFired entries of routes and snapshots. Select the stage or outcome on the quest canvas and use Rename id… in its inspector (see Renaming a stage or outcome id in §7). The rename is scoped to its quest — a stage called stage_1 in another quest, and every reference to that one, is left alone — and it rewrites, in the same operation, every reference above, the localization and VO keys under the stage (quest/<quest>/stages/<id>/…, objectives included), review threads anchored to it, and its position on the canvas. The new id must be valid and unique among that quest's stages (or outcomes); a stage and an outcome may share an id, because every reference says which one it means.

Not renamed: other ids inside an entity — dialogue node and choice ids, objective ids, exit and spawn ids — and custom entities themselves (a custom entity that refers to a renamed one is rewritten). VO asset file names stay as they are: they are paths on disk, not keys.

The same rename runs from a terminal — parlance rename characters mara mara_vell --dry-run prints the plan, lore lines included; drop --dry-run to apply — and from an agent through the MCP server's rename_entity tool. A stage or outcome is addressed as <quest>/<id> with the type questStages or questOutcomes: parlance rename questStages task_pick_side/stg_commit stg_chose_side.


6. Dialogue canvas

Flow map (all dialogues)

Selecting the Dialogues type without opening a specific dialogue shows the flow map — every dialogue as a single node, laid out left-to-right, with edges for the cross-scene jumps between them:

Each node shows the dialogue's title, id, speaker, and word count; a START badge marks dialogues with no incoming jump (reached by an offer, a quest, or a cutscene from elsewhere). Click a node to open that dialogue's node graph. "Auto layout" re-runs the dagre arrangement; positions you drag are saved. It's the project-level companion to the quest dependency graph.

Node graph (one dialogue)

Opening a dialogue from the entity list loads a node graph. Each node represents a moment in the conversation; directed edges represent the paths a player can take. The graph flows left-to-right: the entry node sits on the left and the conversation reads forward across the canvas.

Graph vs. Text

The Graph · Text toggle (toolbar, top-left) switches between the visual canvas and an editable script view — the whole scene as text, for authors who'd rather type than click. Node headers are == node_id [entry] [end] == (the brackets mark optional words: write == node_open entry ==, not [entry]), an optional author note is a > … line under the header, prose follows, choices are - choice_id: "text" -> target, node/choice effects are + set_flag x = true, and checks are check wit >= 12 -> pass / fail — with a conditional modifier written on an indented bonus <±n> ["label"] ? <condition> line under the check:

- ch_bribe: "Slip him a coin."
    check wit >= 12 -> win / lose
    bonus +2 "Coin purse" ? has coin_purse
    bonus -3 ? flag drunk

The script is a lossless representation: saving reproduces the dialogue exactly, changing only what you edited (a byte-level round-trip is enforced by tests over every dialogue).

A running playtest survives the switch: the Play panel stays beside the script, and a save re-reads the scene into the session.

The text is syntax-highlighted as you type: ~ directives, == node fences and the - / + markers in one colour, node ids and -> targets in another, ids that refer to other entities (flags, items, skills, characters) in a third, quoted text, numbers and grammar keywords each their own, and > notes in a muted italic. A keyword the grammar does not know is marked red at the word, and any line the parser rejects is underlined — the same lines the error list below the text points at. The highlighting is a layer painted over the text, never part of it: what you type is exactly what is saved.

Three more affordances ride on that layer:

A node's display gate (conditional narration) rides in the script as a ~ showIf: directive on the line after the node header — the same compact condition syntax choices already use after ?:

== n_aside next=n_close ==
~ showIf: flag met_keeper
You have been here before, and they know it.

An older editor build that predates the directive rejects it with a parse error and blocks the save — loud by design, never a silent drop of the gate. A syntax error blocks the save and points at the line — it never writes partial data. Rich logic (nested conditions, all effect types) is expressible in the compact grammar, but the graph's builders remain the friendliest way to author it; use whichever fits the moment.

Line tags and engine commands

Two hand-offs to the game engine, both opaque to Parlance:

In the Text view:

== n_door ==
~ tags: mood:angry, sfx:door
The door slams.
+ engine shake 0.5
- c_calm: "Easy." -> n_next ? flag met #tone:calm #"vo skip"
    + engine play_sfx door_creak 0.8

A node's tags are a ~ tags: line under its header, in the same list form as the dialogue's. A tag holding a comma or a quote is written "quoted". A choice's tags trail its line as #tag tokens after any ? gate; a # inside the quoted choice text is never a tag, and a tag with a space is #"quoted". An engine command's args are typed by token: a number, true/false, a bare word (a string), or a "quoted" string. Anything that would read back as a different type is quoted. Autocomplete offers the project's existing tags after ~ tags: or #, and declared engine commands after + engine.

To catch typos, declare the commands your engine handles in data/rules.json:

{ "engine": { "commands": { "shake": { "args": 1, "description": "camera shake" }, "play_sfx": { "args": "any" } } } }

Patterns. For the recurring shapes these pieces compose into — say-it-once re-entry, one-shot choices, hub-and-spoke topic menus, reputation tone shifts, quest state machines, storylet selection — see the Pattern cookbook. Each is a copyable recipe with the pitfalls that bite, and an also known as map to Ink, Yarn Spinner, Twine, and Ren'Py for authors arriving from another tool.

Canvas controls

Action How
Pan Click and drag on empty canvas
Zoom Scroll wheel, or use the Controls cluster (bottom-left)
Fit all Fit-view button in Controls, or "Auto layout" in the toolbar
Select node Click a node (with Play open, this also opens the node in the Play column's ✎ Edit tab — §12)
Deselect Click empty canvas
Drag node Drag the node header to reposition (saved automatically)
Delete node Select it → "Delete node" button in the toolbar
Delete edge Select the edge → Delete key
Connect a next (choiceless advance) Drag from the node's dedicated continue handle (only shown on nodes with no choices) to the target node

Node density toggle (Compact · Card · Script) in the toolbar controls how much of each node's text is shown:

The chosen density is remembered across sessions. Switching density reflows new / unpositioned nodes; nodes you've manually arranged keep their saved positions — click Auto layout to reflow everything to the current density and direction.

Map toggles the minimap (bottom-right) on/off — handy when it overlaps a node you're working on. Auto layout re-runs the automatic left-to-right layout for the whole graph.

Node positions are persisted per-dialogue in a *.layout.json sidecar file. These sidecars are gitignored — your graph arrangement is a local, personal concern that is never committed. Deleting one just makes the editor re-run auto-layout next time.

Share build

⇪ Share build (canvas toolbar) downloads a self-contained HTML file of the current scene — play-<id>.html. It's a complete, serverless playable: double-clicking it runs the scene in any browser, through the same core engine the editor uses (checks roll, showIf gates, effects apply, scenes route). No install, no host, nothing to set up. Hand it to a writer or playtester to get feedback on a scene without walking them through running the editor.

Adding nodes

Click + Node in the toolbar. A new blank node appears to the right of the graph. Click it to open the inspector, then fill in the text.

New nodes get generated ids (node_1, node_2, …), and the inspector shows the id read-only in its title (Node: node_3) — there is no id field in the graph view. To give a node a meaningful id, edit its header line in the Text view, along with any -> lines that point at it (see Graph vs. Text). The same goes for choices: + Add choice names them ch_1, ch_2, … per node.

Connecting nodes

Each choice has a source handle on its right side — a small dot. Drag from that handle to the target handle (left edge) of the destination node.

A node with no choices instead shows a single continue handle — drag it to another node to set that node's next field: a choiceless advance, for a listen-only beat (narration, an overheard line) that shouldn't cost a synthetic "Continue" choice. A node can have next or choices or be isEnd, never more than one — the inspector's Next field disables itself with a hint when the node already has choices, and vice versa. next edges draw as a thin unlabelled line, distinct from goto/check edges, and — like choice gotos — can point back at the node's own dialogue only; the runtime never chases a next chain automatically, each advance is one discrete, player-facing step (a Continue → button in Play mode, §12).

To remove a connection, select the edge and press Delete.

Node inspector (right panel)

Click a node to open its inspector.

Node section:

Choice section: Below the node fields, + Add choice appends a choice (and selects it), and ✨ Draft opens AI drafting for this node (see AI drafting — suggested player choices below). The node's choices are listed under Choices, in the order the player sees them; click one to open its fields. The ↑ / ↓ buttons beside each choice move it one place up or down (disabled at the ends), and with a choice focused, Alt+↑ / Alt+↓ does the same from the keyboard. Each move is one save and one undo step. Edges, routes and localization keys all follow a choice by its id, so a move rewires nothing. It changes only the order the choices are offered in, in Play and in the game.

The inspector has no destination field. A plain choice's goto is set only by dragging its handle to a node on the canvas (see Connecting nodes), or by editing the -> target in the Text view. In the Text view you can also reorder choices by moving their - choice lines.

Skill checks and conditions are shown automatically. On the canvas, a choice with an active check shows a skill / difficulty badge (e.g. wit / 10), and a choice gated by a showIf shows a condition summary (e.g. wit >= 6), derived from the structured fields. You do not need to type a [Wit]-style prefix into the choice text — the badge is generated for you, and the text stays clean prose that serializes verbatim to JSON.

AI drafting — suggested player choices

Optional, and off until you configure a provider. AI drafting proposes player choices for one node — the options the player picks from at that line. It never writes an NPC's reply, a node's text, or new nodes; the NPC's side of the scene stays yours to write.

Set up. Click ⚙ AI in the header to open AI Provider Settings: Provider (Anthropic (Claude) or OpenAI-compatible (Ollama, OpenRouter, etc.), which also takes a Base URL), Model and your API key. Settings are stored on your machine in parlance-settings.json (the desktop app's user-data folder, or ~/.config/parlance/), readable only by you; the key is never sent back to the page in full.

Draft. Select a node and click ✨ Draft under its fields. The ✨ AI Draft panel opens with a Count (1–5, default 3); press Draft, and Regenerate for a fresh set. Each candidate card shows the choice text, its id (ch_…, generated by the model), and any check, Show If or effects the model proposed. Add ↓ appends it to the node's choices and selects it.

What it checks, and what it leaves to you. The model may only use skill, flag, faction and character ids that already exist. Every candidate is validated against your project before you see it: one that would add a validation error (an unknown id, a malformed condition, a duplicate choice id) is shown as Invalid candidate, with the reason one click away, after one automatic retry. Wiring is not the model's job: a candidate never carries a destination, and a check comes without its success and failure targets. Those FLOW and GATE problems are ignored when judging a candidate, so an added choice is a dead end — a FLOW error in the validation bar — until you drag its handle to a node on the canvas.

Once added, a drafted choice is an ordinary choice. The purple marking lives on the candidate cards only; nothing in the saved data records that a choice was drafted by AI.

What leaves your machine, and only when you press Draft or Regenerate, sent to the provider you configured with your key:

Nothing else from the project is sent — not other nodes or dialogues, not lore/, and not the character's description or dialogueStyle.

Fallback and locked choices, text-less and gated choice nodes

Conditional check modifiers

New in 0.14.0. A check's odds can shift with the situation. Under a choice's Check, Modifiers are conditional bonuses — each a when condition and a signed bonus, with an optional label: "+2 with the crowbar", "−1 while drunk". Every modifier whose condition holds contributes its bonus, and they sum.

The cookbook's skill-check recipe (recipe 16 in tooling/COOKBOOK.md) works a full example and lists the mistakes to avoid.

Effects reference

The effect picker offers twelve types. The dropdown shows the label in the second column; the data stores the type in the first.

Type Label What it does
set_flag set flag Sets a boolean flag to true/false
adjust_reputation adjust reputation Adds a delta to a faction's reputation, clamped to its declared range
adjust_relationship adjust relationship Adds a delta to one character's relationship with the player. Unclamped — a character declares no range
adjust_counter adjust counter Adds a delta to a named counter (unbounded)
give_item give item Adds an item id to the player's inventory
take_item take item Removes an item id from inventory (no-op if not held)
advance_quest advance quest Records the stage id you type as the quest's current stage (what a quest condition reads). It does not itself fire the stage's On Complete Effects — those fire when the stage's Complete When holds
grant_xp grant XP Adds to the player's total earned XP (levels and skill points derive from it; see progression.json). By convention it goes on quest outcomes — the XP check notes one anywhere else
set_active_dialogue set active dialogue Queues a specific dialogue for a character (push-based scene switch): it sets the flag active_dialogue__<character>, which the target dialogue's forced offer reads (see Offers)
play_cutscene play cutscene Queues a cutscene manifest (pendingCutscene); the host plays its asset, applies effectsOnComplete, then enters entersDialogue if set
set_text set text Sets a text variable to a literal string, substituted wherever {variable} appears in player-facing text. Pick the variable from the dropdown (or create one inline) and type the value.
engine engine command Engine command. A command for the game engine (camera shake, a sound cue), handed over in order among the other effects. It changes no state in Parlance. Type the command name (suggested from rules.engine.commands when the project declares them) and the args on one line: 0.5 camera_main true "two words". See Line tags and engine commands below.

Dialogue metadata (inspector, top section)

Offers — which dialogue a character opens with

When the player talks to a character, the game asks: of everything this character could say right now, what is the most relevant? Offers are how a dialogue answers "me, when…". There is no list to keep in order — each dialogue carries its own claim, and the engine ranks the claims.

New in 0.14.0 — offers replace the character dialogues ladder; see "Coming from a 0.13 project" at the end of this section to migrate.

The model, in four rules.

  1. A dialogue opts in by carrying an offer. No offer means it is never presented on its own; it is reached only by a goto, a route, a cutscene, or a place in the world (an interactable). Click + Offer this dialogue in the inspector to opt in.
  2. An offer with no gate is the fallback. It is what the character says when nothing more specific applies. Every character with offers should have exactly one; the validator warns (OFFER) when there is none (a character whose every offer is a forced routing target is exempt: resolving to nothing outside a routed beat is the intent).
  3. Among offers whose gate passes, the most specific wins. Specificity is how many conditions the gate carries: a single flag is 1, an all of three is 3, an any counts its weakest side. So "betrayed me AND mid-quest" beats "mid-quest" beats the fallback, with no ordering on your part.
  4. A priority tier beats specificity. Set priority above 0 only when one scene must win regardless — a forced next beat queued by set_active_dialogue is the standard case (tier 1, gated on the active_dialogue__<character> flag the effect sets). A tier above 0 with no gate wins forever, which is almost never what you mean; the validator says so.

Two offers with the same tier and specificity are a tie, and the lower id wins. The validator names the winner in an OFFER warning, unless it can prove the two can never both apply: opposite values of one flag or item, or ranges of one counter (or reputation, relationship, skill) that do not overlap, which is why a sequence gated == 0, == 1, == 2 on one counter is silent. Break a real tie on purpose: raise a tier, or add the dominant fact to the gate so it is more specific. A dialogue that has been played and is not Replayable drops out of the running (the preview in Play ignores that, so a played one-shot can still show as the winner there).

Where you set it. Open the dialogue on the canvas; with no node selected, the inspector's top section shows Offer:

In the Text view the same fields are header directives under the title line: a bare ~ offer opts in as a fallback; ~ offer by: npc_wren, ~ offer priority: 1, and ~ offer: flag betrayed_wren set the three fields (the condition uses the same syntax as ~ showIf:). Deleting the lines removes the offer.

How to see what will happen. There are two previews. The quick one is in the inspector itself: under Offer, a line ranks this dialogue among its character's offers ("Ranks #2 of 3 for Wren — …"), and an open Resolution preview lists that character's offers in rank order with the winner marked, resolved against the project's default starting state, with toggles for the flags the gates read. The fuller one is in Play: open ▶ Play on any dialogue and expand Dialogue Offers above the transcript. Pick a character (it opens on the first with offers); it lists every offer of that character in rank order, marks the one that wins against the current state, and shows each offer's tier and condition count — the two numbers that decide. Flip the flags the gates read, right there, and watch the winner change. On the flow map, a dialogue with an offer carries an offered badge; one with no offer and no incoming jump is marked orphan, because nothing presents it.

Patterns. The cookbook (tooling/COOKBOOK.md) builds the common shapes out of offers: say-it-once (recipe 1), the salience greeting (12), and push versus pull for a queued next scene (14).

Coming from a 0.13 project. The old character dialogues ladder is converted for you: the Validation panel's Convert ladders to offers button turns each rung into an offer that resolves exactly as the ladder did, and prints a report of the few places worth a look (a rung that needed a tier, a rung that was already shadowed).

Pacing (inspector, with no node selected)

Below the metadata, a Pacing grid gives the shape of the scene at a glance, so you can tell a two-beat exchange from a sprawling branch without counting:


7. Quest canvas

Selecting a quest from the entity list opens a stage graph. Stages are chained in play order (order); outcomes stand beside them. The quest canvas shares the same dark controls and Map minimap toggle as the dialogue canvas.

Quest structure

A quest has:

Quest inspector

With nothing selected the inspector shows the quest itself:

Field What it is
Name the authoring-facing label
Journal Name the player-facing title in the journal; leave it empty and the journal shows Name. {variable} placeholders are filled at runtime. Emptying it removes the field.
Summary one or two lines for the journal
Tags free labels the journal groups and prioritises by — main vs. side is a tag, never a checkbox. Type and press Enter; × removes one. Linted against rules.quest.tagVocabulary when the project declares one (an unrecognised tag is a warning).
Starts available, Available When, Closed When when the quest enters and leaves the journal

Click a stage or outcome node to add its own section below: description, conditions and effects, and — for a stage — its journal objectives and its place in play order. + Stage / + Outcome append new nodes with placeholder ids (stage_N, outcome_N); rename them with Rename id…. With a node selected, Delete stage / Delete outcome removes it after a confirm. Every change is written to disk as you make it ("Saved automatically · ⌘Z to undo").

Renaming a stage or outcome id

Rename id… in a stage's (or outcome's) inspector heading opens the same preview-then-confirm dialog as an entity rename (§5, Renaming an id). Type the new id — it is checked as you type: valid, and unique among this quest's stages (or outcomes) — then Preview lists every file the rename touches: the quest itself, every entity whose quest condition or advance_quest names the stage, tests/ routes and snapshots, localization keys, review threads. Rename applies exactly that list; the node keeps its place on the canvas and stays selected under its new id. Only this quest's stage is renamed — a same-named stage in another quest keeps its id and its references.

Stage order

A stage's inspector shows its position (2 of 4) with ▲ Earlier and ▼ Later. A move swaps the stage with its neighbour in play order and renumbers every stage's order 1, 2, 3… (the array is kept in the same order, so the validator's stages not in ascending order warning never appears).

Quest conditions compare stages by order, not by id — >= stg_commit means "at or past stg_commit" — so moving a stage can change game logic without touching a single reference. Before such a move is written, a one-line warning names how many existing quest conditions would change meaning and where they are (Moving stg_commit earlier changes what 2 quest conditions mean — they compare stage order (dialogues/dlg_x, codex/cx_y).). Move anyway writes it; Cancel writes nothing. A move that changes no condition's meaning (and == comparisons never do) is saved immediately. Like every canvas edit, a move is one step on the undo stack.

Journal objectives (stage inspector)

A stage's inspector has two prose fields, and the difference between them is the whole point of the journal:

Under Objectives (journal) each row carries:

Control What it does
▲ / ▼ reorder — array order is authoring order, and the journal renders in it
id field unique within the stage; used by the validator and by diffs, never shown to the player
text area the intention line, in the protagonist's voice
show if optional visibility gate, using the same condition builder as everywhere else

An objective with no show if reads "always visible". Gate on knowledge and acquaintance — has she met this character, does she know this place — so a route only appears if she could actually name it.

Objectives are display-only: there is deliberately no effects builder and no goto here. Complete When below still decides when the stage is done, no matter which route the player took.

Every edit — add, retype, reorder, delete — goes through the normal save path, so Cmd/Ctrl+Z restores the previous JSON exactly.

Two warnings you may see in the validation panel:

Quest-level Journal Name (the player-facing title, falling back to name) and Tags are edited in the quest inspector with nothing selected (see Quest inspector above). Tags drive the journal's grouping — main vs. side is a tag, never a checkbox — and are linted against a controlled vocabulary; an unrecognised tag is a warning.


8. Quest dependency graph

Selecting Quests in the sidebar without opening a specific quest shows the quest dependency graph — all quests laid out in a DAG showing which quests are prerequisites for others.

Each node shows:

Edges are drawn from flags. An opens edge (grey, labelled opens: <flags>) runs from a quest whose stage or outcome effects set a flag to a quest whose Available When reads it. A closes edge (orange, dashed, labelled closes: <flags>) does the same for Closed When. A flag read only under a not draws no edge. Quests caught in a circular dependency (a QUEST error) have their edges drawn red. Auto layout re-arranges the graph.

Click a quest node to jump to that quest's detail canvas.


9. Location map

Selecting Locations in the sidebar without opening a specific location shows the location map — every location laid out as a graph, mirroring the quest dependency graph.

Edges represent traversal:

Each node shows the location's id, name, zone, and exit count. Click a node (or pick it from the entity list) to open its detail form. The map shares the same Auto layout and Map minimap toggle as the other canvases.


10. Validation panel

A bar at the bottom of the editor shows the live validation state. Every save triggers a re-validation — incremental, so only the changed entities and whatever references them are re-checked (a full pass runs when project-wide configuration like rules.json changes) — and the pass runs off the editor's serving thread, so even very large projects stay responsive while you type. Results are pushed to all open editor windows via WebSocket.

On a clean project there is no bar at all. It appears as soon as there is at least one error or warning, and disappears again when the last one is fixed — so a missing bar means "no issues", not "not checking". (The one exception: if the live connection to the host drops, the bar stays up and says "live validation disconnected — counts may be stale", with a Retry button.)

By default the bar is collapsed to a single status row — it still shows the live error / warning counts and per-code filter chips, so project health stays glanceable, but the issue list stays out of the way. Click "Validation ▸" to expand it and see all issues; the expanded / collapsed choice is remembered across sessions. The error and warning chips filter the list by severity, and each code chip (FLAG 3) filters it to that code; clear removes the filters. Issues are listed entity by entity, errors before warnings within each entity; the rare issue that belongs to no entity (a file the editor could not read at all) comes first. Each row shows:

+ Add condition, + Add effect and + Add modifier do not save a blank entry. The new entry stays in the inspector, marked Not saved yet — choose a flag (or item, quest…), and is saved when its reference is picked. Removing it before then writes nothing. An edit that blanks a saved entry is held back the same way. Switching a condition's type, for example, keeps the saved condition in force until the new one is complete. The one effect with no reference, grant XP, saves as soon as you choose it.

Common codes:

Code Meaning
SCHEMA Field fails JSON Schema validation
REF References an id that doesn't exist
DUP Duplicate id detected (entity ids, dialogue nodes/choices, or a location's spawns/exits/interactables)
COND A node's showIf breaks a conditional-narration rule — a gated narration node (no choices, not an end) must have next, a gate needs a line to hide, a next chain must not end at a gated node, and gated nodes must not form a ring. Also warns when a skippable gated node carries onEnter, since those effects do not fire when it is skipped
FLOW A dialogue's flow is broken or suspicious. Errors: a dead-end choice (no goto, no check, not isEnd), a node with no choices, no next and not isEnd (the player is stuck), a node with no text and no choices, next together with choices or isEnd, a next cycle, and a node named end (reserved). Warnings: every choice on a node has showIf and none is a fallback (the player may be stuck), more than one fallback choice on a node, a fallback with no gated sibling (it is always offered, so the flag does nothing), whenLocked/lockedText on a choice with no showIf (it can never be locked), and a node whose every non-fallback choice is a passive check (the runtime counts them as visible even when unrevealed, so a game that hides unrevealed passive choices can show nothing clickable there and the node's fallback, if any, is suppressed; the warning fires whatever the difficulty, because no skill has a floor and every modifier is gated, so no reveal is guaranteed)
GATE Active check missing onSuccess / onFailure destination
QUEST Quest stage issue — including stage/outcome effects with no completeWhen/reachedWhen (they can never fire; quest resolution only fires condition-gated items)
FLAG Flag written but never read, read but never written, or declared but never used (warnings). An error when one effect list sets two flags of the same rules.flag.exclusiveGroups group to true — see Exclusive flag groups
REP A faction's reputation is checked but never adjusted
REL A character's relationship is checked but never adjusted
ENDING Ending is unreachable
CODEX A codex entry's unlockedBy needs a flag nothing sets, so it may never unlock
LOGIC A faction lists itself in opposes
COVERAGE Character has no dialogue
REACH Dialogue node unreachable from entry
LORE loreRef points to a file that doesn't exist
PORT Portrait issue — a character or node names a portrait that is not in portraits.json, a portrait names an unknown character, or a registered portrait is never used (warning)
TEXT A {placeholder} in node text, choice text, a choice's lockedText, or a quest's journal name, stage description or objective text is not a registered text variable (error); or a text variable is declared but never referenced (warning)
OBJ Journal objective issue — a duplicate objective id within a stage (error); a stage with completeWhen but no objectives, or one whose every objective is gated by showIf, so the journal can show an empty stage (warnings)
RULES Malformed dice notation in rules.check.dice or in one check's dice
ROUTE A route in tests/routes/ names an unknown snapshot, dialogue, choice, quest or ending, or its startState names an unregistered flag, counter or item
SNAP A snapshot in tests/snapshots/ names an unknown quest, stage, outcome, cutscene or dialogue in its state
LOC Location graph issue — bad exit spawn, a spawn nothing arrives at, more than one default spawn, gate/gateType mismatch, unreachable location, npc interactable missing its character
CUT Cutscene issue — unknown entersDialogue, never-triggered cutscene, or ambiguous ordering (two play_cutscene effects on one node)
OFFER Dialogue-offer selection issue — no fallback (a character has offers but none is unconditional, so resolution can return nothing — silent for a routing-only character whose every offer is forced through its active_dialogue__ flag), names no character (an offer on a dialogue with neither a speaker nor offer.character, so nothing ever presents it), a prioritized fallback (an offer with priority > 0 and no when shadows every lower tier forever and re-fires on re-entry), an unbreakable tie (two of one character's offers share priority and specificity and aren't provably exclusive, so the id decides — provable means opposite values of one flag or item, disjoint ranges of one counter/reputation/relationship/skill, each also under a not, or an OR whose every side is), an offer-only stranded speaker dialogue (nothing offers it and it has no world placement), a set_active_dialogue target with no forced offer (the named dialogue carries no offer for that character gated on active_dialogue__<character>, so the flag routes nothing), or a forced offer that can be out-ranked (an ordinary offer of the same character beats it while the flag is set, so routing plays the wrong scene — raise its tier). A dangling offer.character/offer.when ref is a REF error. All warning-level; none blocks a save.
MIGRATE A stale project still carrying the retired character.dialogues ladder (Parlance 0.13). An error; the Validation panel shows a Convert ladders to offers button that rewrites every ladder into dialogue offers and prints the conversion report (parlance migrate <project> does the same from a terminal, and tooling/scripts/migrate_ladders.py without the editor).
PROG Progression config (progression.json) issue — malformed thresholds (not strictly increasing), pointsPerLevel/maxSkill < 1 (error), a starting skill already at the ceiling, or the soft-cap sanity warning (authored XP grants enough points to max every skill).
XP grant_xp issue — non-positive amount (warning), or grant_xp authored outside a quest outcome (advisory; the convention is XP from quests only — silent in a project with no quests).
ENGINE engine effect issue. A command name that is not lowercase snake_case is an error. Once rules.engine.commands declares the project's commands, a command outside that set (a typo is otherwise a silent no-op in the game) or a call with the wrong number of args is a warning.
CHECK Priced/oneshot check discipline — a priced (default) active check whose failure doesn't proceed (no onFailure branch), or a priced-gate failure that sets a flag some offer.when reads (advisory). oneshot checks are exempt from the proceed requirement.

Spelling is deliberately not in this table. Prose findings carry the code SPELL, they are never produced by validation, and they never appear in this panel or block a save — they live in Reports → Prose (§11). The reason is that spelling is editorial rather than structural: it says nothing about whether a project conforms to the format, so it is not part of the published contract and the reference validator (tooling/validate.py) does not implement it. The lore findings are the same kind of thing: LORE_LINK (a parlance: link in lore that names nothing) and LORE_MENTION (a name that could be a link) live in Reports → Prose too, and never here — lore never ships. So does CODEX_MENTION (a codex entry's name used in play text), an information note of the same pass.

The Reports row (sidebar footer) also shows the total issue count, coloured red for errors or yellow for warnings.


11. Reports — coverage & reference index

Click Reports (pinned to the sidebar footer) to open the Reports panel.

The panel has six tabs — Issues, Prose, Explore, Find usages, Flag flow and Speakers — plus Search text, which opens the shared full-text overlay (⌘⇧F).

The Issues tab has two columns:

Left — Coverage & structural issues

Issues are grouped by code category. Each row is clickable and navigates to the affected entity (same as the validation panel). Use this view to:

Right — Reference index

A searchable index of every id in the project, shown beside every tab of the panel (the Find usages tab itself only points you at it). Type part of a flag id, character id, skill id, etc. — it matches ids, and lists the first 30 — to see:

Each row names the entity (dialogues/dlg_arrival) and the JSON path. The rows are a read-out, not links: only a lore row (lore/x.md:L12) is a button, which opens the file on the Lore surface at that line. To jump from a variable's usages to the entities themselves, use the Flow panel on the variable's own detail page (§5), whose rows are clickable. This is the "find usages" feature — useful for safely renaming or removing a variable.

Flag flow

The Flag flow tab answers one question about one flag: where is it set, and where is it tested? Type in the search box (it suggests the project's flags) and pick one. The tab lists it in two halves:

Each row names the owning entity, its type and the JSON path. An empty half is the finding: "never explicitly set" is a gate that cannot open unless the game sets the flag itself, and "never used as a gate" is state nothing reads. The FLAG warnings report the same two cases project-wide; this tab is for following one flag through the story. The Flow panel on a variable's detail page (§5) shows the same split with clickable rows.

Prose — spelling & canon names

The Prose tab checks the words themselves. It runs on demand (never on save) and reports findings grouped by kind:

Finding What it means
Canon near-miss A word within two edits of one of your own names — Mistfal where the location is Mistfall. Usually a misspelling of it.
Canon capitalization One of your names written in lowercase — calloway where the character is Calloway.
Unknown words Not in the dictionary. On a fresh project most of these are proper nouns, not typos.
Repeated words The same word twice in a row (and and). Punctuation-separated repeats ("No, no") are not flagged.
Unbalanced quotes/brackets An odd number of ", or mismatched ( ) / [ ] / “ ”.
Double spaces Two or more spaces mid-line.
Quote style A straight quote in a project that otherwise uses curly ones, or vice versa — only when one style is clearly dominant.
Unparsed dictionary lines A line in lore/dictionary.md that looked like an entry but parsed as nothing.
Dangling lore links A parlance: link in lore that names nothing — an unknown type, or an id no entity has (renamed or deleted). Code LORE_LINK.

Rows navigate to the entity like every other report row, and unknown words carry a + dictionary button.

Lore is part of the corpus

Every lore/*.md file except the dictionary is checked with the same rules (quote style excepted — lore is authoring text, not player-facing), with link targets and code masked out. Lore rows read lore/factions.md:L12 and open the Lore surface at that line.

Unlinked lore mentions are listed below the findings as notes (code LORE_MENTION): an entity's name written as plain text in lore. They are suggestions, not problems — they never count as findings and never fail --check. Open one and the editor offers Link.

LORE_LINK and LORE_MENTION are editorial, like SPELL: produced by this pass only, never by validation, and not part of the published format's rule set.

Where the words come from

Three layers, checked in order:

  1. Your project's own names, derived automatically from character, faction, item, location, skill, codex, ending and quest-journal names. Nothing to maintain — rename a character and the check follows. This layer is case-sensitive, which is what makes the near-miss and capitalization findings possible.
  2. lore/dictionary.md — words you add by hand or with the + dictionary button. It lives in lore/ because it is authoring canon that never ships to the game.
  3. The English dictionary bundled with the editor.

One deliberate limitation: a name that is also an ordinary English word — a character called "Hawk", a faction called "The Order" — is not treated as a canon name, because nothing can tell "the hawk circled" from "the Hawk circled". Those words are simply spell-checked normally.

lore/dictionary.md

Plain Markdown, so a writer can edit it without touching JSON:

## Locale

- en-US

## Words

- Vashti — the merchant. Not "Vashi".
- gaolhouse

## Style

- grey → gray
- OK -> okay — house style

The ## Locale section sets the language for the check (English only today). Notes after an em dash are for humans and are ignored.

While you type

Prose fields underline findings inline as you write, with a heavier underline for canon near-misses and capitalization than for ordinary unknown words. The word and character count below each field also reports the number of prose notes; hover it to read them.

From the command line

npm run prose                        # report on the project
npm run prose -- --check             # exit non-zero if anything is found (CI)
npm run prose -- --write-dictionary  # seed lore/dictionary.md from the unknown words

--check runs in CI, so a new typo fails the build.

Sorting the first run with AI (optional)

The first run on a large project surfaces a lot of unknown words, most of them names. Sort these with AI groups them into names, jargon, dialect and real typos so the names can be accepted in one click. It uses the API key configured in ⚙ AI (header) and is entirely optional — everything above works offline and free.

The model only classifies words that were already found; it never edits your prose, and typos are never added to the dictionary. Nothing is written until you accept the result.

Codex mentions — terms the player could look up

Below the prose findings, Codex mentions lists every place a codex entry's name appears as plain text in dialogue (node and choice lines), quest journal text (journal title, stage, objective and outcome descriptions) or a location description, grouped by the entry. It is the glossary report: the terms your codex explains, and where the player meets them — worth a look when deciding which entries to unlock early, or which lines could carry a hover definition in your engine.

Rows open the entity that owns the line. The matching follows the lore mention rules exactly: whole names on word boundaries, case-insensitive, the longest name winning, and a single-word name that is also an ordinary English word (an entry called "Order") is never matched. Codex bodies, entry names and ending text are not searched — an entry naming itself is not a term to explain.

Code CODEX_MENTION. Like LORE_MENTION it is an information note, never a finding: it is not produced by validation, never fails npm run prose -- --check, and is not part of the published format's rule set.

Speakers — lines & words per character

The Speakers tab is the casting and budgeting table: one row per resolved speaker with its lines, words, the number of dialogues it appears in, and its voiceable lines. Rows are sorted most words first; click a character or skill to open it. ⇩ CSV downloads the table.

Speakers resolve the way the transcript and the Word/Excel exports resolve them — a node's own speaker, else the dialogue's default, else Narration — so the table agrees with the line sheet row for row. Two attributions are worth knowing:

The words column sums to the project word count in the stats bar above.

Explore — playthrough explorer & route coverage

The Explore tab plays the project instead of reading it. Every other check is static — REACH walks the node graph, the FLOW "all choices have showIf — may be stuck" warning reasons about the data — so none of them knows which states a player can actually reach. Explore does: press Run and the editor plays N seeded random runs, then (with exhaustive on) every reachable state, forking at every visible choice, at both outcomes of every active check, at every continuation and cutscene chain, with a cache of visited states so loops terminate. It starts from every dialogue's entry, from the project defaults — and, with from snapshots too, from each snapshot in tests/snapshots.

When a scene ends, the player is not limited to the continuations the feed offers: they can walk the location map and open a scene placed there, again, with whatever the story has changed since. Explore follows the runtime's rules for that — an object or environment interactable plays its dialogue whenever its showIf passes (placed scenes have no one-shot filter; that belongs to offers), an npc interactable resolves the character's offer with the seen set, so a played non-replayable offer does not come back, and an exit whose gate fails plays its denialDialogue. Where the player may stand starts at the locations tagged start (every location, if none is) and grows through every exit whose gate passes and every cutscene's arrivesAt. So a "look again" line gated on what the first look found, or an arrival scene gated on evidence gathered elsewhere, is reached — before this was modelled, the demo reported six such nodes unreached even after a complete search.

The banner above the results is the part to read first. Complete means every reachable state was visited, so "unreached" below means no path exists from any start. Bounded names what stopped the search (states, depth, random-run steps) and means an unreached node may still be reachable — raise max states and run again. The report never calls a node unreachable when it only ran out of budget. A random run that hits its step limit does not make a complete search bounded: with return visits a run never ends by itself, and the exhaustive phase has seen everything the run could have gone on to see.

A state is counted once however it was reached, and "the same state" means the same for everything that can decide a move — a flag only an XP-granting quest outcome reads, the XP itself, or a text variable never changes what a player may do next, so states differing only there are one. A scene is played once per distinct situation it can see and re-used for every other state that looks the same to it. That is why max states stays at 5,000 by default: the demo completes in about 3,500, and this repository's own data/ in about 1,800.

List What it means
Dead ends States the engine reported a problem in: a node with choices none of which were visible and no next (stuck — the FLOW warning confirmed, with the exact state), a goto or next to a missing node, a check whose taken branch has no target. Load into Play saves that state as a snapshot and opens the Play panel on it; Copy state JSON copies it.
Unreached nodes / choices Never shown on any explored path. Click a row to land on the node with the inspector open.
Never ends Dialogues entered on some path but never seen ending — meaningful only when the search was complete.

Two things the explorer does not model, on purpose. Rolls: every check is taken both ways, so a node behind a check is reachable regardless of luck. Where exactly the player stands: the map is walked as a set of places the player may be in, and a place once reachable stays so — a door that locked behind the player may leave them on the far side — so where the map is uncertain Explore assumes the player could be there, and never calls a node unreachable because it assumed a door was shut. An on_enter scene is treated like any other placed scene: something the player can open, not something that must play first.

Route coverage, below the explorer, replays every route in tests/routes and lists, per dialogue, how many of its nodes some passing route stands on and which routes they are — and the dialogues no route touches at all. A route that fails to replay covers nothing and is listed as failing. Show on map opens the dialogue flow map with every covered scene marked, using the same trail the Play panel draws for a loaded route.

The same two reports run headlessly:

parlance explore [--runs N] [--seed S] [--exhaustive | --no-exhaustive] [--max-states N] [--max-depth N] [--from-snapshots] [--json]
parlance route --all --coverage

parlance explore exits 1 when it finds a dead end, so it can gate CI beside parlance ci-check; --json prints the whole report. The exhaustive phase is on by default (--exhaustive says so explicitly); --no-exhaustive keeps only the random runs, and --max-depth caps how deep one path may go.

Export — Word screenplay & Excel line sheet

For the people who read the script outside the editor — a VO director, a proofreader, a producer — dialogues export to two Office formats:

Format What you get
.docx screenplay A heading per dialogue, then each line as a speaker cue and the text beneath it, with gates, notes and on-enter effects as italic parentheticals and each choice listed as → text {if gate} [check] → target. Paragraph styles (Character, Dialogue, Parenthetical, Choice) are named, so the whole script restyles from Word's styles pane.
.xlsx line sheet One row per line and per choice. Columns, in order: Dialogue, Title, Node, Choice, Kind (line or choice), Speaker, Text, Condition, Check, Effects, Tags, Next, Notes, Entry, End, Loc key and VO key. Header frozen, filters on.

Speakers are resolved to names the way the Play transcript shows them — the character or skill name, Narration for a line with no speaker, Player for a plain choice, and the skill for a check choice. The Loc key column is the same key the Localization catalog uses, and VO key is filled on exactly the lines the Localization panel counts as voiceable, so the sheet lines up with a VO manifest row for row.

Two places to export from:

And from the command line, run in the project folder:

parlance export --format docx --out script.docx                   # every dialogue
parlance export --format xlsx --out lines.xlsx --dialogue dlg_arrival

Exit codes: 0 written, 1 unknown dialogue, 2 bad usage or not a project.

Custom-type tables

A custom type exports as a table: ⇩ .csv / .json on its grid toolbar, or

parlance export --format csv --type drink --out drinks.csv
parlance export --format json --type drink --out drinks.json

(exit 1 for a type types.json does not declare). The CSV has a header row (id, name, the declared fields in the order types.json lists them, then any fields rows carry that the type does not declare), one row per entity in id order. List cells are written the way the grid shows them, so a column copied from the export pastes back into the grid. Text that a spreadsheet would run as a formula (starting with =, +, - or @) gets a leading ', so opening a file from someone else's project can't run anything. JSON is the lossless form: the type's declaration and every row as stored. Unsaved grid edits are not included.

Export is one-way. Nothing reads a .docx or .xlsx back into the project; edits made in Word or Excel have to be carried back by hand (or through a translation catalog, for text — see Localization & VO).


12. Playtest mode

Playtest mode lets you walk through a dialogue interactively with a live simulated GameState, right inside the canvas view.

Opening playtest

  1. Open any dialogue in the canvas.
  2. Click the ▶ Play button in the canvas toolbar (top-left).

A Play panel takes the node inspector's place on the right side, under two tabs: ▶ Play (the panel) and ✎ Edit (the node inspector — see Editing while playing). The active node is highlighted with a green glow on the canvas; visited nodes are dimmed.

Starting state editor

Before starting, the panel shows the Starting State editor:

Click ▶ Start Session to begin. You can also click ▶ Play again (it acts as a toggle) to close the panel and return to the editor.

Editing while playing (auto-reload)

A running session stays live when you edit the dialogue it's playing. Change node text, add or delete a node, rewire an edge — the session keeps your accumulated game state (flags, XP, quests, reputation) and re-reads the scene, recomputing which choices are visible at the current node. If the node you were standing on is deleted, the session snaps back to the entry node with state intact rather than dead-ending. This makes the tight loop — tweak a line, see it in context, tweak again — instant, without restarting from the top each time.

You make those edits without leaving Play:

The session also survives a switch to the Text view: the Play panel stays open beside the script, and saving the script re-reads the scene into the session the same way. (There is no Edit tab in the Text view — the script is the editor.) Only closing Play (▶ Play in the toolbar) or opening another dialogue ends the session.

While a session has continued into another dialogue, the canvas shows that dialogue's graph and the Edit tab is not offered — the inspector edits the dialogue you opened. Open the other dialogue to edit its lines.

Transcript

Once a session is running, the transcript shows each step top to bottom:

Past steps are shown above the current step. Each past step has a ↩ rewind here link that truncates the timeline back to that point.

Continuing into the next scene

Once a conversation ends, if there's somewhere for the player to go next, a section appears below the transcript:

This is how you playtest across a scene boundary without manually reopening the next dialogue and re-entering its starting state by hand.

Check affordances

For active-check choices, two extra buttons appear alongside the normal choice button:

These let you test both branches of a check without needing to set skills to extreme values.

Passive checks

A passive check never rolls. The game shows its choice only when the player's skill plus every modifier that applies reaches the difficulty (passiveCheckPasses in the runtime contract), and Play does the same:

Spending a skill point mid-scene (the progression row) re-checks the reveal at once. To see what a higher skill would reveal without changing it, tick Show what a higher skill would reveal under the choices. It lists the hidden passive choices greyed, with how much more skill each needs. It is Play-only and off by default, and those choices can never be clicked.

If the reveal leaves a node with nothing to click, Play says so. When the node has a fallback choice it is still not offered: the runtime counts the hidden passive choice as visible, and a fallback is only offered when nothing else is. The notice names the fallback so you can see why.

In the Log, a passive choice reads (passive reveal). A replayed route can still walk a passive choice its state would not reveal, and the log marks that one (passive — not revealed at this skill).

Toolbar controls (while session is running)

Button Action
seed:XXXXXXXX Displays the current session seed (read-only)
↺ Restart Resets to step 0, same seed and starting state
⟳ Reroll Appears after a check step. Rewinds one step and re-runs the same choice with seed+1. Use this to flip a pass to a fail (or vice versa) without manually changing skills.
✕ Stop Closes the play panel and returns to edit mode

State inspector

At the bottom of the play panel, a live State table shows the current values of all flags, reputation, relationships and skills referenced in the dialogue, then every counter, the inventory (held items by name, or (empty)) and quest stages. Values that changed on the most recent transition are highlighted in purple.

Determinism guarantee

The session uses a seeded RNG (mulberry32(seed + stepIndex)). A session is fully reproducible: given the same seed and starting state, every choice leads to the same rolls and the same outcomes. Rewind + replay gives identical results. The seed only changes when you click ⟳ Reroll (seed+1) or 🎲 Randomize before a new session.

Snapshots, recorded routes, and imported saves

A play session is throwaway by default. Three buttons make one permanent:

Button What it writes
💾 Snapshot tests/snapshots/<id>.json — the current game state as a named, reusable starting point
⦿ Save route tests/routes/<id>.json — the steps you just walked, plus the assertions you tick, as a regression test
⤓ Import save file… tests/snapshots/snap_<file name>.json — a save file written by the game, turned into a snapshot

The id for a snapshot or route is the name you type, lowercased with spaces and punctuation turned into _: "Bragg two scenes" is saved as tests/routes/bragg_two_scenes.json. No rt_ or snap_ prefix is added — only a name that doesn't start with a letter gets one (2nd visit → rt_2nd_visit), and a name already taken gets _2, _3. If you want the prefixes as a convention, type them into the name. (Find a path here and Explore's Load into Play do prefix their files: snap_witness_…, rt_witness_…, snap_deadend_….)

Load saved state (snapshot) at the top of the Starting State editor picks a snapshot to start from; the state inputs below hydrate from it, so you can load a baseline and then tweak one flag.

A snapshot also remembers which dialogues had already been seen when it was taken. That matters more than it sounds: a dialogue that is not replayable stops being offered once it has been played, so a baseline that forgot its own history would offer you — and any route starting from it — content the player at that point could never see again. Saving a snapshot mid-session records the session's seen set; loading one restores it.

Importing a save is the way a bug found in the actual game becomes something the editor can open. Point it at a save file the game wrote and you get a snapshot you can start playing from immediately, carrying the state and the seen set.

The import is refused if the save names content this project does not have — an unregistered flag, an unknown item, a dialogue from another build. That is not fussiness: a snapshot referring to something undeclared is a validation error, so importing it would hand you a red project and a fixture that fails CI. If the save came from a newer build, import it from the branch that has the content it refers to.

The same import is available from the command line, which is what a build box or a bug-report triage script wants:

parlance save import path/to/slot1.json --id snap_bug_41 --name "Bug 41 repro"
parlance route rt_bug_41

On a build box without the desktop app, parlance is the published CLI — npx @orbitope/parlance-cli save import … (or … route …) runs the same verbs.

A route that was fast-forwarded with Start at cannot be saved: a route always replays from the dialogue's entry node, so it would fail on its first step. The ⦿ Save route button is disabled for such a session and its tooltip says why — restart from Entry to record one.

Replaying a saved route

Replay a saved route, in the Starting State editor under the snapshot picker, lists the routes that start in this dialogue. Loading one starts the session from exactly the state the headless runner uses — the route's snapshot, its start-state overrides and its seed — and shows the route's steps with a cursor:

Control What it does
Step Applies the next step: a choice (forced if the route forced it), a continuation into the next scene, a cutscene, or a run of Continue hops
Play all Steps until the route ends, then checks its end assertions
Show on map Opens the dialogue flow map with the route's scenes highlighted in order
Stop replay Leaves replay mode; the session stays, as an ordinary one

The canvas follows the replay exactly as it follows a live session, including into other dialogues.

When an edit has broken the route, the replay stops at the first step that no longer holds and says why — Step 2: choice 'ch_press_check' not found in dialogue …, or End: … when every step ran but an assertion no longer holds. Nothing else happens: the session is left live where it broke, so you can play on by hand from there. Then:

Editing the dialogue during a replay pauses it, since the remaining steps were recorded against the old version. Resume re-checks them against your edit, from where the replay stopped.

The replay and parlance route are the same engine, so a route that passes here passes in CI, and parlance route reports a failure as FAIL at step N: … with the same step number.

On the dialogue flow map, a route's scenes carry a route N badge (a scene the route visits twice shows both positions). A jump between two scenes that the map has no edge for — a scene reached because its offer became available, which no effect names — is drawn as a dotted discovered edge. Clear route in the map's toolbar removes the trail.

Find a path here — the witness solver

Select a node and the inspector offers Find a path here. The editor searches the same state graph the Explore report walks — return visits included — breadth-first, so the first path found is a shortest one, for a way from the project's start to that node, and shows the moves: choices (with the check outcome they force), continuations into other scenes, cutscenes, Continue runs. Tick from this dialogue's entry only to search from the current scene rather than from every dialogue. max states is the search budget, the same one Explore uses and with the same default; when the budget is what stopped a search, the result offers Search again with a larger one (up to the editor's ceiling of 20,000; the command line takes more).

A found path can be kept three ways:

Button What it writes
Save as snapshot tests/snapshots/snap_witness_<node>.json — the state on arrival at the node, after its onEnter effects, with the seen set
Save as route tests/routes/rt_witness_<node>.json — a route that replays from the project defaults (recorded as its startState) to the node; only offered when the walker has verified it ends there. If the path walks back to a scene a place opens — something no route step can say — the route starts in that scene instead, from tests/snapshots/snap_witness_<node>_start.json, the exact state the player opened it with; the panel says so, and saving the route saves that snapshot too
Load into Play saves the snapshot and opens the Play panel loaded on it, so playing a late branch no longer means playing to it by hand

"No path found" with the search was complete means no start reaches the node under the engine's rules; with bounded it means the budget ran out first. From the command line, parlance witness <dialogueId> <nodeId> [--from <dialogueId>] [--max-states N] [--max-depth N] [--save-snapshot <id>] [--save-route <id>] [--json] does the same and exits 1 when nothing is found; --from searches from one dialogue's entry, like the checkbox, and --max-states / --max-depth are Explore's budget flags with Explore's meaning. A bounded result names the flag to raise.

What playtest does NOT change

Playtest is read-only. It never writes to any dialogue file or layout file. You can verify this — the dialogue JSON and .layout.json are byte-identical before and after any play session.

The one thing a session can write is placeholder voice audio, and only when you ask for it: Generate in the voice row saves to the gitignored tts/ folder, never to data/ (see Placeholder voice (TTS)).

advance_quest effects fire in the transcript (they appear in the applied list) but are a no-op in playtest — quest stage tracking is the responsibility of the host game engine.


13. Undo / redo and navigation history

There are two independent histories, and they do different things:

Undo / redo (edit history). The ↩ Undo and ↪ Redo buttons in the top toolbar undo/redo saves (Cmd/Ctrl+Z, Shift+Cmd/Ctrl+Z, or Ctrl+Y). Up to 100 operations are kept per session. Each save records a { before, after } snapshot of the entity; Undo replays before, Redo replays after. This history is in-memory only — it clears on page reload. Canvas layout changes (node drag) are not in the undo stack — they write directly to the layout sidecar and are not undoable.

Back / forward (navigation history). The ‹ and › buttons (next to Undo/Redo) move through your selection history — which type and entity you were viewing — like a browser's back/forward. Shortcuts: Alt+← / Alt+→, or Cmd/Ctrl+[ / Cmd/Ctrl+] (the Xcode/VS Code "Go Back" convention). This is purely navigational: it changes what's shown, never your data.

For a third way to get around — jumping directly to a distant entity rather than stepping through history — use the command palette (Cmd/Ctrl+K, §2).


14. Data format & git workflow

File layout

data/
  skills/          one JSON file per entity
  variables/
  factions/
  characters/
  dialogues/       dlg_arrival.json
                   dlg_arrival.layout.json            ← canvas positions (gitignored)
  quests/
  locations/
  endings/
  codex/
  items.json       flat registries (items, portraits)
  portraits.json
  cutscenes/       one JSON file per cutscene
tests/
  routes/          *.json — scripted playthroughs with assertions
  snapshots/       *.json — saved states to resume from
schema/            JSON Schemas; editor loads these for validation + forms
lore/              Markdown canon docs (the Lore surface, §5; never shipped)
review/            review requests + comment threads (§16)

data/ holds narrative content only. Routes and snapshots are regression fixtures — a shipping game never loads them — so they live beside it rather than inside it.

Why this layout matters

Stale-load detection

If two editors (or a script) write to the same file concurrently, the host detects the conflict via a hash check and returns a 409. The editor surfaces this as "File changed on disk — reload to see latest version". Reload to pull the latest before saving again.

Validation on every save

Every save re-validates and broadcasts updated issues to all open editor windows via WebSocket, so you always see live validation without manually refreshing.

The save itself never waits on validation: the moment your change is on disk the editor is ready for the next edit, and the problems panel catches up a moment later. Validation is incremental — a save re-checks only the entity you changed and the entities that reference it, not the whole project — and runs on a background thread, so typing stays responsive no matter how large the project grows. In practice the panel refreshes within a few tens of milliseconds of a save; only an unusually large single dialogue (many hundreds of nodes) adds a noticeable lag to that refresh, and even then it is the squiggles catching up, never the typing.

You don't need to configure any of this. For debugging, a few environment variables on the host change the behavior: PARLANCE_VALIDATE_WORKER=0 runs validation in-process instead of on the worker thread, PARLANCE_VALIDATE_INCREMENTAL=0 forces a full re-validation on every save, and PARLANCE_VALIDATE_CHECK=1 cross-checks every incremental pass against a full one (on by default in dev builds).


15. Localization & VO

The Localization entry in the sidebar footer (globe icon) opens the translation and voice-over pipeline. It's a read-and-export surface: content stays authored in the base language on the entities themselves, and localized strings live in catalog files alongside your data.

What it shows

The pipeline

  1. Download source catalog — a flat key → source text JSON of everything translatable, for translator reference.
  2. Enter a language code and Locale template — a key → "" file (with any existing translations for that locale already filled in) to hand off. It also records which English each line is to be translated from, and lists the lines whose English changed since they were translated under @outdated.
  3. Translators fill in the blanks, revise the @outdated lines (deleting each from the list once it's done), and return the file; drop it at data/locales/<lang>.json. Reload — the coverage bar fills in.
  4. VO template works the same way for data/vo/<lang>.json, mapping voiceable keys to opaque audio asset keys (the engine resolves them, exactly like cutscene manifest assets). Parlance never touches recorded masters; it can generate throwaway placeholder audio on your own provider, kept out of data/ and out of git — see Placeholder voice (TTS) below.

Keys

A string's key mirrors the reference-index path, e.g. dialogue/dlg_arrival/nodes/node_open/text or quest/qst_inquest/summary. Keys are stable as long as the underlying ids are. Renaming an entity with Rename id… (§5) rewrites its keys in every catalog, so translations follow; changing a node or choice id by hand orphans its translation, which shows up as a stale key on the coverage bar so you know to remap it.

Translations only arrive as files: the panel reads and exports, and has no field for typing a translation in. Fill data/locales/<lang>.json outside the editor (or have your translators return it) and drop it in place.

Outdated translations

Editing an English line after it was translated makes that translation outdated: the key still exists, but the text it was translated from does not. Each locale file records a short fingerprint of the English each translation was made from, under an @sourceHashes entry at the top of the file:

{
  "@outdated": ["dialogue/dlg_arrival/nodes/node_open/text"],
  "@sourceHashes": {
    "dialogue/dlg_arrival/nodes/node_open/text": "3f0a91c2",
    "dialogue/dlg_arrival/nodes/node_close/text": "c81e7d04"
  },
  "dialogue/dlg_arrival/nodes/node_close/text": "Adieu.",
  "dialogue/dlg_arrival/nodes/node_open/text": "Bonjour, voyageur."
}

A line is outdated when its recorded fingerprint no longer matches the current English, or while it is listed in @outdated. The coverage bar counts it apart from done and lists it under N outdated; click a row to jump to the line.

Keys starting with @ are metadata. No string key starts with @, so a game loading the catalog should skip them. VO manifests (data/vo/) have no fingerprints; a recorded take is not flagged when its line changes.

Placeholder voice (TTS)

Before a line is recorded you can still hear it in playtest. The Play panel shows a voice row under the current line:

Control What it does
Status pill Recorded (the VO manifest points at an audio file in the project), Placeholder (generated audio exists for this exact text), or None
▶ Play Plays the recorded take if there is one, else the placeholder
🔈 Generate Synthesizes a placeholder for the line on your TTS provider. Disabled, with the reason in its tooltip, when no provider is configured
Language base (the text on the entity) or any language that has a data/locales/ or data/vo/ file. A locale language speaks the translation, falling back to the base text
Auto-play Plays each new line as you step: the recorded take, else a cached placeholder, else nothing
auto-generate Shown once Auto-play is on, and off by default because it spends your provider credits: also generates a missing placeholder as you step

The line spoken is exactly the one shown, including interpolated values, so a line that reads differently depending on game state gets one placeholder per variant.

Recorded takes. A data/vo/<lang>.json value that is a relative path to a .wav, .mp3, .ogg, .m4a or .flac file inside the project plays directly. Anything else — a res:// path, an engine asset key, an absolute path, a URL — is the engine's to resolve, and the row shows no recorded take for it.

Choosing a provider. Click ⚙ AI in the header to open AI Provider Settings (the same dialog as AI drafting) and fill in the Voice (placeholder TTS) section:

There is one voice for the whole project: the Voice field applies to every line, whoever speaks it. There is no per-character or per-speaker voice mapping, so to hear two characters differently, change the voice between takes (each voice caches its own placeholders).

The key is stored with the AI key — locally, owner-only, never sent to the client in full. Clear voice provider removes it.

Where the audio goes. Placeholders are written to tts/<lang>/ at the project root, named by a hash of the language, voice, provider and text, with an index.json beside them recording which line each file belongs to. parlance init adds tts/ to the project's .gitignore. Nothing is written to data/, no VO manifest or binding is touched, and VO coverage never counts a placeholder — it only ever reads data/vo/. Editing a line changes its hash, so the old placeholder simply stops matching; stale audio is never played. Deleting tts/ at any time is safe.

Privacy. Parlance runs no voice service. A line's text leaves your machine only when you press Generate (or turn on auto-generate), and only to the provider you configured — a local command never leaves the machine at all.


16. Review — reading someone else's branch

The Review entry in the sidebar footer is where narrative work gets read, questioned, signed off and published. It needs nothing but git: comments live in the repository, on the branch they are about, so a two-person team on plain clones gets working review with no server anywhere.

The panel has three columns: Reviews waiting for you on the left, the branch's Changes (or Play the branch) in the middle, and Comments — the verdict, the comment composer and the threads — on the right.

Drafts — how a writer's work reaches Review

Writers don't need to handle branches themselves. Drafts (sidebar footer) does it for them: New draft asks "What are you writing?" and Start creates a branch for the draft (writer/<name>/<id>, cut from the remote's default branch) and switches the project to it. If you have unsaved changes, Start the draft with these changes carries them onto it. Each draft row shows its state — Drafting, In review, Changes requested, Approved — waiting to be published, In the game — with the actions that apply:

Drafts needs a git remote; without one the panel says why it isn't available.

Author or reviewer is decided for you

There is no mode to switch. For the branch you are looking at, you are its author if it is the branch you currently have checked out, and a reviewer if it isn't. The badge at the top says which — AUTHORING or REVIEWING — and what follows from it. A review opened from Reviews waiting for you is always REVIEWING, even when it is your own draft: the inbox never checks anything out.

AUTHORING (checked out) REVIEWING (not checked out)
You are looking at your working files a read-only snapshot of the branch
Comments written straight into review/ queued locally, sent by Sync comments or a verdict
Story files yours to edit read-only
Suggestions Apply suggestion propose only
Verdict and publish — (you don't sign off your own work) Approve & publish…, Approve only, Request changes

Reviewers never check the branch out, so their own working copy stays clean and on whatever they were doing. Check out to edit (shown in REVIEWING mode on a branch picked under Advanced) switches you to the branch when you want to become its author; it refuses if you have uncommitted changes rather than stashing them behind your back.

Reading the changes

Reviews waiting for you is the front door. It lists open review requests by title, author and age, grouped by what needs doing: Waiting for you (not yet reviewed), Ready to publish (approved), Waiting on the writer (changes requested) and Published recently. Refresh fetches the latest. Click a row and the Changes tab loads that draft's narrative diff by itself — not a file diff. It reports what happened to the story: "2 nodes added, 1 line edited, offer changed", each entity's before/after lines, flags introduced or retired, and the validation delta.

To compare any two branches by name instead, open Advanced: compare any two branches: pick a Base and Head, press Show changes, and Fetch to pull the latest branch list from the remote. Both pickers are searchable: type any part of a name — several words, in any order. Local branches are listed before remote ones, and the branch you have checked out is marked, since main and origin/main are otherwise the same word and picking the wrong one silently swaps AUTHORING for REVIEWING. Under the pickers, Reviews on this branch lists the review requests on the Head branch (Archive shows merged and abandoned ones), and New opens a review of it.

Each changed entity carries a button to open it. As the author that is Open, which draws the dialogue on the canvas with this branch's changes marked. As a reviewer the canvas can't help — it draws your checked-out working tree, which is a different branch — so the button is Play, and it takes you to the scene read-only in the tab beside it. Entity types with no read-only viewer yet (characters, quests, …) show the button disabled.

Playing the branch

Play the branch runs the branch's own content — the snapshot read from git, not your files. This is the point of reviewer mode: you hear the scene as it actually plays before saying anything about it. Everything from Playtest mode (§12) works here, including the seed, rewind, and forced check outcomes.

The Scene picker offers every dialogue on the branch, searchable by title or id. Once the branch's changes are loaded, the ones this branch touched are grouped first and annotated with what happened to them ("1 line edited") — the annotation is searchable too, so typing line edited narrows the list to just those scenes.

The rest of the project stays in the list on purpose. A branch that edits a quest, a flag, or a dialogue's offer changes how a scene behaves without touching that scene's own file, so the dialogues worth playing are often ones that show up as unchanged — and reading an edit in context usually means playing the scenes on either side of it.

When the scene you are playing is one the branch changed, its before/after lines sit above the transcript, so you can watch it play and see what moved without switching back to Changes.

Expand Log under the transcript and each line grows a 💬. Clicking it opens a comment already anchored to the node or choice you just heard, so the thread lands on a spot in the story rather than on a line number.

Comments, suggestions, verdicts

Comments hang on an anchor — a node, a choice, a field — or on the review itself for notes that belong to no single line. Anchors use the same keys as localization, so renaming a node doesn't silently orphan the discussion: the thread is flagged stale anchor and listed under Unanchored, and Reports carries the same warning.

On a text anchor you can attach a Suggested replacement. That is the reviewer's edit: they propose the words, and the author applies them. Reviewers cannot change story files directly — the branch isn't on their disk, and keeping content edits to one writer is what keeps review data conflict-free.

Applying a suggestion needs AUTHORING mode, which the inbox never gives you. As the writer: have the draft open (Drafts → Open), then in Review open Advanced: compare any two branches, set Head to that branch (the one marked as checked out), and pick the review under Reviews on this branch. Each thread with a suggestion now shows Apply suggestion, which writes the suggested text into the story file. This reads the review from your own copy of the branch, so it shows only comments that have reached your disk: opening the draft from Drafts brings them down, but there is no button that does so for the draft you already have open — pull the branch with git (or open another draft and come back) to see comments sent since.

A verdict records a sign-off against the commit you read. The buttons depend on where the review stands:

A reviewer's verdict is sent right away (it syncs like Sync comments); if that fails, it stays queued and the panel says so. If the author pushes more work afterwards, the review says so — "The branch has moved since this verdict" — rather than showing a stale tick over unread lines. Nothing enforces a verdict beyond the publish rule below; with no server there is nothing that could. It is a note between colleagues, and a record of who read what.

Comments on lore paragraphs

Lore can be reviewed too. With a review open (pick it in Review), go to Lore, open the file, and switch to Preview: every paragraph carries a 💬 Comment button (a heading and a fenced code block each count as one paragraph). Clicking it takes you back to Review with the composer already anchored — "on lore/the-order.md ¶3" — and, like a text field, the comment can carry a suggested replacement for the whole paragraph. Save the file first; a comment anchors to the saved text, so the button waits while the buffer is unsaved. Paragraphs that have threads show a count badge, and each lore thread in Review has Open ¶n, which lands on that paragraph in the preview.

Markdown has no ids, so the anchor is the paragraph's position plus the words you commented on. When the file changes, the editor compares the two and says what happened rather than guessing:

All three are warnings in Reports, never errors. A suggestion is applied only to a paragraph that resolves — on a moved one, re-anchor first, so the new words never land on whichever paragraph slid into the old slot. Applying goes through the same guarded write as saving the file, so it is refused, not merged, if the file changed on disk in the meantime.

One caveat for reviewers: the Lore surface edits your checked-out files, while a thread is judged against the lore on the branch under review. When the two differ, a fresh comment can show as moved or stale straight away — the same reason the canvas can't show a branch you haven't checked out.

Lore files named outside the new-file rule (lowercase letters, digits, - and _) cannot take paragraph comments; the preview says so. The Changes tab lists what the branch did to lore, paragraph by paragraph, below the entity changes — old words beside new — with Open to jump to the file.

Syncing, and merging

As a reviewer your comments sit in a local queue (inside .git/, so they can never be committed by accident) and the review shows how many are unsynced. Sync comments fetches, merges your notes into whatever is already on the branch, commits, and pushes — your working tree is never touched. A verdict syncs the same way on its own. Concurrent reviewers don't conflict: comment threads are separate files, and same-thread replies merge by union.

Publishing is how an approved draft goes into the game: Approve & publish… (approve and publish in one step), or Publish… once it is approved. Both ask first — "Publish “…” into the game? The app can’t undo this." — and publishing is refused unless the review is approved.

Publishing works from whatever branch you are on, with uncommitted changes in your working copy: it never checks anything out and never touches your files. It merges the draft into the base branch on the remote and pushes. Your own local copy of the base branch is not moved; pull it when you want the published work on your disk. It merges cleanly or not at all — on any conflict nothing is written anywhere and the panel names the overlapping files, so the two can be combined in git by someone who chose to. It needs a git name and email set, and git 2.38 or newer. A published branch carries its review files into the base branch; that is the archive, which is why reviews are never deleted.

What this deliberately isn't

Parlance is not a git client. The only branches it creates are writers' drafts (above), and it deletes one only after merging, once git confirms the work is in. Beyond that it has no conflict resolution, no history editing, and no GitHub pull-request sync. Conflicts and history stay in the tools built for them.


17. Getting help & sending feedback

Help ▸ Send Feedback opens the feedback page in your browser. That page is the whole process: bug reports and feature requests go to public GitHub issues, and anything you can't say in public — security, licensing, or a bug you could only demonstrate with unreleased story content — goes to orbitopegames@gmail.com instead.

Parlance has no telemetry and no crash reporter. Nothing about your session, your project, or your machine is ever sent anywhere. The trade is that a bug you don't report is a bug nobody knows about, so please report them.

The two things that make a report actionable are the version (Parlance ▸ About Parlance) and whether the problem reproduces on the bundled demo project — a demo repro is one anyone can run, and one you can paste in full without revealing anything about your own game.


18. Bringing in a story from another tool

If your story is already written in Yarn Spinner, Ink, Twine (Harlowe or SugarCube), ChoiceScript, Arcweave or Ren'Py, you do not have to retype it. Seven importers convert a story into a Parlance project, and they are published — with their source, their gate, and five worked migrations of real stories (ChoiceScript and Arcweave have test fixtures only) — at github.com/orbitope/parlance-spec under importers/.

They are not part of the editor. Nothing is installed with Parlance and nothing runs unless you run it; they are optional agent skills (SKILL.md) you copy into a project, MIT-licensed and meant to be forked when your story uses a dialect they do not.

What they promise, and what they do not

They convert. They never rewrite. Every player-facing string in the output came from your file byte for byte, and that is enforced rather than intended: a content check compares the result against the source and refuses to finish if a line went missing, and stops outright — permanently — if a line appears that you did not write. Nothing fills in a summary, invents a variable, or rephrases a line that did not quite fit.

What the format cannot carry is declared, never quietly dropped. Each import ends with a report that leads with what was lost, names the construct and the source line, and says which losses you could fix by moving a line and which are real gaps.

Read the last number in the report

An import can preserve every word of your story and still hand you something a player cannot walk through. That is not a contradiction: the content check proves no prose was lost, and it is blind to whether the story still hangs together. A single condition the format cannot express, sitting on a link everyone passes through, cuts off everything behind it.

So every report states how many nodes a player can actually reach, and that is the number to look at first. Of the five worked migrations, the Yarn one reaches 87% of its story, the Ink one 74% and the Harlowe one 48%, while the smaller Ren'Py and SugarCube stories reach all of theirs — every one of them having preserved every line.

Before you start

The importers' own fit guide (importers/IMPORTERS.md) answers whether your story will survive the trip, and it turns on one question: how does your story move forward?

Then read a worked migration under importers/examples/. Each one holds the author's original file beside the imported project, so you can run the check yourself and see what the honest result of a real conversion looks like before committing your own story to one.