Validation checks

The complete catalog of check families. Each issue in the validation bar carries its family code; a row navigates to the exact place it is about — the node or choice, the quest stage or objective, the form field — or, for FLAG/REP/REL, to the entity's Flow panel. Errors are broken wiring; warnings are story smells — and the editor never blocks a save on either. CI draws the line with ci-check (--strict to fail warnings too).

Shape & wiring

Code Severity Fires when Fix by
SCHEMA error A field fails JSON Schema validation — wrong type, missing required key, unknown enum. Also covers custom game data: a declaration in data/types.json that can't work (a type named after a built-in, a plural that isn't a plain name or is already taken, an unknown field type, an enum with no options, a reference with no or an unknown target), and a custom row missing a required field or holding a value of the wrong type Correcting the field; the schemas are the format's ground truth. The editor's type editor refuses the broken declarations, so these usually come from hand-edited files
REF error Any reference names an id that doesn't exist — a choice's goto, an offer's dialogue, a factionId, a condition's flag, a custom row's reference field (including one item of a list) Re-pointing or creating the target. The reference index finds every usage of an id
MIGRATE error A project still carries the retired character.dialogues ladder (pre-0.14). Blocks loading until converted The Validation panel's Convert ladders to offers button, parlance migrate <project>, or migrate_ladders.py — see dialogue offers
DUP error Duplicate ids — entities, dialogue nodes/choices, or a location's spawns/exits/interactables. The reference validator also reports a row id repeated within one custom type Renaming one of the twins

Dialogue flow

Code Severity Fires when Fix by
FLOW error/warning Errors: a dead-end choice (no goto, no check, not an end), a node with no choices, no next and not an end (the player is stuck), a node with no text and no choices, next together with choices or an end, a next cycle, a node named end. Warnings: every choice on a node is gated and none is a fallback (the player may be stuck), more than one fallback 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 (not an end, no next) whose every non-fallback choice is a passive check — the runtime counts passive choices as visible even when unrevealed, so a game that hides them can show nothing clickable there, and the node's fallback is suppressed. That one fires whatever the difficulty, since no reveal is guaranteed Wiring the missing destination, marking an intended terminal Is End, or adding a fallback choice. For the passive-check warning, give the node one choice without a passive check — a greyed whenLocked: "show" choice doesn't count, because it can't be clicked
REACH warning A node can't be reached from the dialogue's entry Connecting it or deleting it — the Pacing panel spots these too
GATE error An active check is missing its onSuccess / onFailure destination Dragging both the green and red handles somewhere
COND error/warning A node's display gate breaks a conditional-narration rule — a gated narration node (no choices, not an end) needs next, a next chain must not end at a gated narration node, gated narration nodes must not form a ring, and a gate on any node needs a line to hide. Warns when a gated narration node carries onEnter, since those effects don't fire when it's skipped. Since v0.15.0 a gate on a node with choices, or on an end node, is legal: it hides only the line Giving the node a next, or ungating it. The inspector hides the control where a gate is illegal, so this usually only appears in hand-edited data
ENGINE error/warning An engine effect's command isn't lowercase snake_case (error). Once rules.engine.commands declares your commands, a command outside that list or a call with the wrong number of args is a warning — otherwise a typo is a silent no-op in the game Fixing the command name or its arguments, or declaring it in rules.engine.commands (line tags and engine commands)

State

Code Severity Fires when Fix by
FLAG error/warning A flag is written but never read, or read but never written (warnings). An error when one effect list sets two flags of the same rules.flag.exclusiveGroups group to true Deleting the orphan or wiring the missing half — the variable's Flow panel shows both directions. For the error, setting the flags in separate places, or fixing the group
REP error A reputation reference names an unknown faction Fixing the faction id
REL warning A character's relationship is checked but never adjusted, or adjusted but never checked Wiring the missing half, same as FLAG
TEXT error/warning A {placeholder} — in node or choice text, a choice's lockedText, or quest journal text — names something that isn't a registered kind: "text" variable, or names one of the wrong kind. Warns for a text variable nothing ever interpolates Registering the variable, or fixing the name
RULES error data/rules.json is malformed — an unparseable dice expression, say Correcting the field; see configuration

Structure

Code Severity Fires when Fix by
QUEST warning/error Quest stage issues — notably stage/outcome effects with no condition (completeWhen/reachedWhen), which can never fire; outcome reference cycles are errors Adding the condition; quest resolution only fires condition-gated items (why)
ENDING warning An ending is unreachable — no path leads to it Wiring the path, or retiring the ending
COVERAGE warning A character has no dialogue Giving them an offer — or accepting silence knowingly
LOC warning Location graph issues — a bad exit spawn, a spawn nothing arrives at, more than one default spawn, gate/gateType mismatch, an unreachable location, an npc interactable whose character can't speak Following the message; the location map shows the topology
CUT warning Cutscene issues — unknown entersDialogue, a cutscene nothing triggers, or two play_cutscene effects racing on one node Fixing the reference or the ordering
CODEX warning A codex entry is gated on a flag nothing ever sets, so it may be unreachable Setting the flag somewhere, or ungating the entry
LOGIC warning A relationship contradicts itself — a faction that opposes itself, say Fixing whichever side is wrong
PORT error A portrait id isn't in the registry Adding it to portraits.json, or fixing the reference
OBJ warning/error Journal objective problems — a duplicate objective id inside a stage, or a quest tag outside a declared rules.quest.tagVocabulary Renaming the twin, or adding the tag to the vocabulary
OFFER warning Offer resolution mistakes — no fallback (a character's offers are all gated, so some states resolve to nothing), prioritized fallback (a no-when offer carrying a priority — wins forever, re-fires), unbreakable tie (two offers at equal priority and specificity that aren't provably exclusive — the id decides), forced offer out-ranked / routes nothing (a set_active_dialogue target an ordinary offer beats, or one carrying no offer for its flag), stranded speaker (a dialogue offered by nothing with no world placement). A dangling offer.character is an error The deep dive shows each shape with examples

Content & progression

Code Severity Fires when Fix by
LORE error A loreRef points at a file that doesn't exist Fixing the path — the form's file dropdown exists so this can't be typed wrong
PROG warning/error Progression config problems — thresholds not strictly increasing, pointsPerLevel/maxSkill < 1 (errors), a starting skill already at the ceiling, or authored XP generous enough to max every skill (the soft-cap sanity warning) Adjusting progression.json
XP warning A grant_xp with a non-positive amount, or authored outside a quest outcome (advisory — the convention is XP from quests only) Moving the grant, or granting something
CHECK warning Priced-check discipline — a priced active check whose failure branch doesn't proceed, or a priced-gate failure that sets a flag some offer reads (the punishment-spiral advisory). oneshot checks are exempt Giving failure somewhere to go — failure is content
BIND warning Reference validator only. An asset binding file in data/bindings/ leaves a used portrait, a voiceable line or a triggered cutscene unbound, or binds a portrait, cutscene or VO key that doesn't exist. A binding file that fails its schema is a SCHEMA error Adding the missing binding, or removing the stale one. The editor doesn't load bindings, so these appear only from validate.py

Test fixtures

These scan tests/, not data/ — the route and snapshot fixtures a shipping game never reads.

Code Severity Fires when Fix by
ROUTE error A route names an id that doesn't exist — a dialogue, a choice, a starting flag Re-pointing the step, or deleting a route whose content is gone
SNAP error A saved snapshot references something unknown, or its questFired ledger is malformed Re-capturing the snapshot after the rename that orphaned it

Prose and review

Two families that validation never produces. They have their own passes and their own panels, and neither can fail a build the way the families above do.

Code Severity Fires when Fix by
SPELL warning The prose check found a misspelling, one of your own names spelled inconsistently, a doubled word, or an unbalanced quote Fixing the line, or adding the word to lore/dictionary.md. Runs on demand in Reports → Prose, and in CI via npm run prose -- --check
REV warning A review comment's anchor no longer resolves — usually a rename orphaned it Re-anchoring or resolving the thread. Never blocks anything: a stale comment must not fail a narrative build

Spelling is deliberately outside the contract. It is editorial rather than structural — it says nothing about whether a project conforms to the format — so the reference validator doesn't implement it, and no conformance vector covers it.

Reading the output

In the editor: errors first, then warnings, filter chips per code, click to navigate. In CI: the same list grouped by family, with counts, and exit codes your pipeline can gate on (tutorial).

Two implementations produce this list — the TypeScript validator (editor + CI) and the independent Python one — kept in enforced parity, so the list you read is the list, everywhere. Two checks are Python-only: the BIND family, because asset bindings are not part of the editor's project model, and a row id repeated within one custom type, because the editor loads custom rows keyed by id.