systems/CONDITION_EXPRESSION_GRAMMAR.md
Status: ACTIVE v1.0 · Owner: Build Pipeline (T0.11) · Reference evaluator: harness/condition_expr.py
This document specifies the single conditional expression language used everywhere a
build artifact tests state: spine Block-11 side-questline gates, Block-10b fork and beat
gate expressions, T0_Quest_Definition objective completion_condition and quest-level
completion_condition, and the Dialogue-Dataset line_variant condition_expression.
There is exactly one grammar; every surface was reconciled to it.
T1_Build_Pipeline_Contracts [ACTIVE v1.0] §9.5 — "Gate and condition expressions,produce-before-consume. One mini-grammar: has(local.key), at(site_id), flag(local.key),
ws.key with ==, >=, <=, or in, read(surface), and boolean & and |. Every consumed flag or
local key must be produced by a prior beat's out:."]
T1_Build_Pipeline_Contracts [ACTIVE v1.0] §6.5 / CG-11 closed 2026-07-20 — the line_variant condition_expression is RECONCILED to the single §9.5 grammar so one
expression language covers quest gates, objective completion_conditions, and line-variant
selection, with §9.5 as owner.]
harness/check_spine_graph.py — the deterministic half of the §9.5 contract; it isthe cross-beat produce-before-consume enforcer. This grammar's evaluator is the per-expression
half.]
expr = or_expr ;
or_expr = and_expr , { "|" , and_expr } ;
and_expr = unary , { "&" , unary } ;
unary = "(" , expr , ")" | predicate ;
predicate = has | flag | at | read | ws_cmp ;
has = "has" , "(" , local_key , ")" ;
flag = "flag" , "(" , local_key , ")" ;
at = "at" , "(" , site_id , ")" ;
read = "read" , "(" , surface , ")" ;
ws_cmp = ws_key , ( "==" | ">=" | "<=" ) , value
| ws_key , "in" , "[" , value , { "," , value } , "]" ;
local_key = "local." , ident ;
ws_key = "ws." , ident ;
site_id = ident ;
surface = ident ;
value = number | string | enum_member ;
enum_member = ident ;
ident = letter_or_us , { letter_or_us | digit } ;
number = [ "-" ] , digit , { digit } , [ "." , digit , { digit } ] ;
string = '"' , { any_char_except_quote } , '"'
| "'" , { any_char_except_quote } , "'" ;
has(local.key) — true when the local key was produced by a prior beat's out:. In theexecuted corpus this reads produced narrative or item state (for example
has(local.naga_contract_offered), docs/spine/CH_03.md:186).
flag(local.key) — true when a boolean local flag is set. Distinct surface from has(), same local. namespace and same produce-before-consume doctrine. The contract names both;
production spine uses has() predominantly and reserves flag() for pure booleans (for
example flag(local.bargain_refused), the §9.5 completion_condition example).
at(site_id) — true when the player's current location matches site_id (for example at(wayang_performance_ground), docs/spine/CH_04.md:195). The site_id is NOT registry-
validated here; site resolution is check_spine_graph.py's §3b concern.
read(surface) — true when the named surface (an inscription or readable object) has been read (for example read(surface), docs/proposals/quest_spike/CH_04_quests.md:224).
ws.key OP value — a durable-worldstate comparison. ws.key must resolve to a T0_Worldstate_Variables variable_name; the value is domain-checked (Section 4). Example:
ws.path_bucket == EVIL (docs/spine/CH_75.md:200).
& is conjunction, | is disjunction. Both are attested in the corpus: has(local.oracle_bones_decoded) & has(local.triage_underway) (docs/spine/CH_21.md:199)
and at(meroe_pyramid_field) | at(gebel_barkal) (docs/spine/CH_14.md:200).
no precedence rule. This grammar binds & tighter than | (standard boolean algebra), so
a & b | c parses as (a & b) | c. Recommendation to Josh / schema owner: ratify this, or
ratify a parentheses-mandatory rule; do not leave it implicit, because a C++ port with a
different default would silently invert live gates.
the corpus. IMPLEMENTED here as an explicit grouping form so precedence is never ambiguous
and the port is unambiguous. Corpus authors need not use them; they exist so an author CAN
disambiguate.
recurses per ( level, so an unbounded grammar would leak an uncaught RecursionError (and
SEGFAULT the C++ port, which has no default recursion limit) on adversarial/malformed input.
Beyond the cap the evaluator raises ConditionSyntaxError("expression nesting too deep").
Real corpus expressions nest one or two levels at most; the port MUST bake the same 64-level
bound and raise the same typed error.
ws.key operators and domain rules== is legal on every scalar variable (enum, integer, float).>= and <= are legal only on integer and float variables. On an enum they are a DomainError: an enum domain is unordered, so ordering has no defined meaning. (Contract-
named >=/<= are corpus-unattested to date; only == appears in production.)
in [m, m, ...] tests membership against a bracketed list of one or more domain members. Contract-named, corpus-unattested; the single-member list in [EVIL] is legal.
T0_Worldstate_Variables domain: DomainError.
LO to HI range.relationship_history, companion_roster_state) — not scalar-comparable; any ws. comparison against it is a DomainError. Object state is
read through its named sub-fields at a layer above this grammar, not by a bare comparison.
ws.key (no matching variable_name) is an UnknownKeyError.Every consumed local. key — every has(local.k) and flag(local.k) — must be produced by a
prior beat's out: line within the chapter, or be an explicitly promoted ws. key. This is a
CROSS-BEAT property: a single expression cannot verify it, so the reference evaluator does not.
harness/check_spine_graph.py is the enforcer (it walks beats in seq order and flags any
has()/flag() local key not yet produced). The evaluator exposes
Condition.consumed_local_keys() so a caller can obtain the consume set and perform the check.
harness/condition_expr.pyfile, so it is deliberately boring and explicit: a hand-written tokenizer, a recursive-descent
parser, an AST of small tagged classes, and a pure (AST, state) -> bool evaluator.
parse(src) -> AST — raises ConditionSyntaxError.typecheck(ast, schema) -> None — raises UnknownKeyError or DomainError.evaluate(ast, state, schema) -> bool — raises EvalStateError when a required namespacevalue is absent.
Condition(src, schema) compiles (parse + typecheck) once; .evaluate(state) runs many.state dict namespaces (each optional; a missing bucket is empty): inventory (set, for has), flags (set, for flag), location (str or set, for at),
reads (set, for read), ws (dict name -> value, for ws.key).
standalone; load_schema_from_csv(path) loads the full live 42-row
T0_Worldstate_Variables [ACTIVE v0.1] for build-time validation. --self-test --check-registry
confirms every embedded key still resolves in the live registry.
ConditionSyntaxError (malformed syntax), UnknownKeyError (unknown ws key), DomainError (domain-illegal value or operator), EvalStateError (runtime state gap).
python harness/condition_expr.py --self-test runs the accept/reject corpus embedded in the
evaluator: real expressions harvested from the repo (each cited to its source line) plus
adversarial cases (operator precedence, parenthesized grouping, quoted strings with spaces, a ws
key that is a prefix of another, single-member in-lists, empty and malformed expressions,
out-of-range and wrong-type values, object-variable comparison, and namespace violations). Every
accepted expression evaluates to its expected boolean against a fixture state; every rejected one
raises its expected typed error.
& versus | — implemented as &-tighter; ratify or override (Section 3).evaluator-only convenience (Section 3).
>=/<=/in are contract-named but corpus-unattested; the first authored use should bespot-verified against this evaluator's domain rules (Section 4).