Reference
Authoring style
The grammar tells you what's legal; this tells you what's
good — the conventions that keep a .trl file clear to read and safe to extend.
The governing idea: a .trl file is an accretive log. Like the play it
mirrors, it grows by accumulation — entities and facts are laid down and never deleted, only
reinterpreted. The guidance that follows works with that grain.
Bare is here, loud is elsewhere
One abstraction pays off across the whole language. A .trl has context axes —
which story-plane a fact lives on (the level axis) and which
story-time it happens at (the story-time axis). On every such axis
the rule is identical:
A bare reference resolves to the active context. The only thing ever decorated is a reference that leaves it — and that displacement is always loud, written right where it happens.
A directive sets the active context for a region, the 95% case (a reference that stays put) costs nothing,
and the rare crossing is visible in the source. Learn it once and it transfers between axes:
<<heist>> sets the active plane and <<heist|name>> is a
loud crossing; @@ day1 sets the active story-time and name @@ day2 is a loud
anchor. The one difference falls out of what each axis is — a node has exactly one
home plane (so a bare reference to a foreign plane is a mistake — cross loudly), but spans a
whole worldline across story-time (so this character, now is always a valid bare
reference; only its other-time self must be anchored). The levels and
story-time sections below are the two working instances.
Two phases: declarations, then the log
Every file has a clear divide. Declarations and setup come first — the cast, the
objects, the places, the seeds. The temporal event log comes second — the
arc ▸ act ▸ scene ▸ beat timeline, in reading order. Everything the timeline names already
exists above it. In practice the split is often physical, too: stable setup lives in an
immutable corpus of .trl files you import, while a session plays out
in an open, ongoing log-style file.
// @title The Appraisal · @version 1.0
// (setup usually lives in the corpus: import "trl/modules/index.trl")
// ===== CAST =====
char vale [+Protagonist] [+Grifter]
char renn [+Antagonist] [+Collector]
// ===== OBJECTS =====
obj nightingale [+MacGuffin] [+Coveted]
// ===== SEEDS ===== (plant now, surface later)
?vale [~Pursues(goal=justice)]?
// ===== THE EVENT LOG ===== (reading order is time)
scene the_appraisal {
beat 1 { vale -- renn : "earns his trust" }
beat 2 { evt the_switch [&Betrays(agent=vale, target=renn)] }
} The preamble is machine-read
That leading // @title line isn't just a comment — the leading // block
is metadata, surfaced next to a recognition so a tool never re-parses a file the engine already
read. A // @name value line is a tag (the value kept verbatim; repeat a name
for a list), the non-tag prose is captured as a description, and
// @laconic <one line> is the one-line summary. Conventional keys —
@source, @trope, @category — sit beside anything a corpus invents
(@icd11, @occurrence); the reader keeps them all.
Two rules keep it honest. Only the leading block counts: a comment trailing a facet or
sitting inside a rule is annotation, never metadata. And metadata is inert to matching —
it never gates, scores, or changes what is recognized, so adding or removing a header tag leaves
recognition and evaluation byte-for-byte unchanged. The editor surfaces these fields
in its Recognitions panel and hover cards; the CLI reads them per hit with
shape … --emit source,laconic --format json.
Front-load the cast
Declare every entity — char, obj, set, evt,
arc, work, concept — before the log uses it. The firm rule: nothing
in a scene/beat may name an entity that wasn't declared up top. An entity that
springs into being mid-story is the defect this guide is meant to prevent — even a thing the story only
points at from the future (a Chekhov's gun, a planted twist) gets a bare declaration up front,
then occurs later.
And declare generously. Lay out the whole table of pieces before you play, even ones whose payoff is far off or still uncertain — the cost of an unused declaration is one line; the cost of a missing one is a story you have to stop and scaffold mid-sentence.
Scope to meaning — local by default
Front-loading is about order, not breadth. An element's reach should match its meaning:
declare a thing where it's used, no wider. Front-load what recurs or pays off later; keep everything else
local. A one-scene set or obj can be declared just above its scene; a one-off
flavor tag stays bespoke — a red, unresolved tag is the honest mark that it isn't shared
vocabulary, so don't promote color to the corpus; a name earns a shared declaration only once it recurs
across tropes.
The same instinct governs a facet's role in a rule. When a facet is load-bearing for one trope's
discriminator, mark it on the tag (the ! / ? / *w
marks in Make the discriminator bind), not in distant corpus configuration — so an
author reading the trope can see what it does without carrying the whole corpus in their head.
Plant ambiguity early, sharpen late
The accretive style lets you declare a thing before you know what it means. Seed it now,
commit later — a latent fact ?…? (present in the graph, surfaced to no
one), an ambiguity *( A | B ) as name* (one of these, or something not yet
named), and resolve name -> concrete for the in-world moment it becomes fact. Uncertainty
is a first-class material.
Naming is ontological
- snake_case for entity ids (
the_salt_gate); CamelCase for tag names (PendingPayoff); lowercase for concepts (concept honor) — and reference a concept lowercase everywhere, including param values (goal=justice). - A lowercase name is a foundational node — a concept sits at the same level as a character:
declared once, referenced by identity. A CamelCase name is derivative — a prop/state/role
attached to a node.
Corruptionwon't resolve toconcept corruption; the casing is the meaning. - The definite article carries meaning:
hero→Hero→TheHeroare a three-tier signal, andTheXandXare two symbols — link them deliberately withalias TheX -> Xonly when they're truly the same.
Split the verb from the state
A tag's domain facet is canonical — a name always carries the same sigil
(= Body, # Mind, % Essence, @ Rel, &
Verb). Don't reuse one name across facets. The classic split is the act vs. the resulting
state: the act is a present-tense &Verb on an evt; the state is a past
participle on whatever the act lands on, carrying the facet its condition denotes. A rule reads the act
and writes the state.
// the ACT — a &Verb on an evt, present tense
evt turn [&Betrays(agent=vorn, target=aldric)]
// the resulting STATE — a past participle on whom it lands.
// # = Mind (feeling betrayed is mental); = is Body, % is Essence
aldric [#Betrayed] [#Resentful]
(+ is the unmarked default — write it explicitly, no naked tags. It's a dynamic,
not a facet; the dynamics are + straight · - subverted · ~
deconstructed/intent · ? averted · ! invoked.)
Type a slot only where the type is clear
A signature can type each param — Stash(item: obj [+Container]) — and the validator then
checks every call site (and warns if the value lacks a required constraint tag). Typing is
gradual: an untyped slot is fine; add a type where the corpus concretely passes one kind.
But a plain typed param is a required role — every recognizing event must fill it. If a
verb is used two ways, one form supplying the slot and one not, type it optional with
?:: Onsets(subject: char, course?: concept [+Course], finding) domain-checks a
course= value when present yet still matches a course-less call. Rule of thumb: type it,
and add ? if some legitimate call leaves it out. (Full syntax on the
grammar page.)
Epistemic discipline
!S! | absolute | narrator ground truth, immutable — a death, a revealed fact. Use sparingly. |
| (plain) | asserted | the default — what's true on this plane, now. |
*S* | contingent | true-but-perspective-dependent; collapses under a perspective view. |
?S? | latent | present, unsurfaced — the seam a perspective cuts along. Plant early, surface late. |
A non_canon plane can't mint an !absolute! fact. More in
epistemic levels.
Temporal hygiene
- The spine is
scene ▸ beat; the beat is the finest unit — the engine ticks per beat. - Assert a state in the beat it becomes true; retract
[-Tag]in the beat it stops. Don't assert and remove in the same breath. - Reading order is time —
past scene/future sceneread the file top-down. - That reading order is work-time (what's shown). Reach for story-time
— a
chronspine plus@@stamps — only when the two diverge: a flashback, a shuffled timeline, a self-encounter. A linear story needs neither. Keep displacement loud: a bare name is always this character, now; only an other-time self is ever writtenname @@ marker. See the two clocks. - A trope keyed on story order (
before/afterin awhen:) only discriminates if it's asymmetric and positioned: order distinct event classes (&Deedbefore&Result) or an event before the entity it produces — a symmetric same-class pair matches a reversed story too. And both operands need a story-time position (a scene@@stamp or an anchor), or the clause rejects. Verify both ways: the forward vignette confirms, the reversed one must not. - If the work is chronological by genre — a chart, a captain's log, a case file — don't hand-roll
the spine. Declare
@@ autoonce at the top: it derives the clock from presentation order, so everybefore/afterjust works. It's exclusive with explicitchron/@@/~~(declare a clock or derive it, not both), and it changes one reading: under@@ auto,not(before($a,$b))∧not(after($a,$b))means genuine same-tick co-occurrence; under a partial hand-declared clock the same pair only means "not known to be ordered."
Make the discriminator bind
For a trope vignette, assert the trigger facts the rule reads and let the engine derive
the recognition — hand-asserting a rule's own conclusions makes the example look right while the
rule never fires (and a hardcoded output can even block it). But firing isn't enough: a rule can fire at
full confidence on every cousin in its family. Recognition keeps only the when: event patterns for recall and re-checks static tags in coverage — but only on entities an
event role bound. A tag floating beside an unbound var is invisible. So the feature that
distinguishes the trope must ride an event:
- Tie the discriminator to an event role — name it on a verb
(
&Reveals(subject=$self)), so it binds and its tags are re-checked. - Put the distinguishing tag on the bound entity, overtly — coverage re-checks tags
literally, not what's merely implied nor asserted by the rule's own
then:. - Choose a contrastive discriminator —
[+Protagonist]separates nothing where everyone is one; reach for the tag the siblings lack. - Mark the load-bearing anchor
!necessary. Coverage is a flat fraction, so a candidate can still rank on scaffolding while its one defining facet is missing. A necessary mark ([=Sign!]) caps that hit below the report floor, so the trope leaves the differential when its anchor is absent — use it for the single distinctive anchor, sparingly. A supporting signal that should never penalize its own absence gets?instead ([+Smoker?], a present-only bonus). - Weight a discriminating facet for planning with
*w([=Pyuria*9]) when a reasoning consumer (frontier / predict --bayesian) should treat it as stronger evidence — a relative likelihood ratio, default 1. It's recognition-neutral (coverage and confidence ignore it), so weight freely for reasoning without perturbing recognition ranking; it composes with the marks ([=Wounded!*3]).
rule Anti_Hero {
when:
char $c [+Sympathetic_Focus] [+Unscrupulous!] // ! the load-bearing anchor
evt $deed [&Crosses(agent=$c)] // the deed binds $c, so its tags re-check
then:
$c [+AntiHero]
} Discriminate by positive evidence — never by negation or magnitude
Define a character type — or any trope — by the positive constellation of acts and traits that constitute it, not by what it lacks or by a number.
- Never
not([+Tag])as a discriminator. Closed-world negation is true for the whole unwritten majority — anAnti_Herokeyed onnot([+Virtuous])fires on every protagonist nobody happened to mark virtuous. (It's also dropped in recognition — a static-only negation compiles to no shape.) Absence of a flag is not evidence of its opposite. - Don't reach for a
^stat for a qualitative property. Morality, loyalty, heroism have no unit;[^Virtue = 0]is false precision. The^arena is the quantitative one — HP, AC, dice — not narrative character. - Model the tension as a conjunction of positives. "Anti-X" isn't "not-X" or "low X" —
it's a specific co-presence: a protagonist we root for (
[+Sympathetic_Focus]) who is[+Unscrupulous]and is shown crossing a line ([&Crosses], the deed that gives it a bindable shape). Encode the positive thing — present, not merely un-negated, so the unwritten never match.
The work and its skeleton
Most files model the story inside a work; sometimes you need the work itself — the
film, the novel, the game. That's the work entity, and its medium rides on the
Essence facet (work the_film [%Film]), never on a prop. Medium-format conventions —
credits, a prologue, a post-credits stinger — are structure, not properties: model each
as a concept slot on the work's skeleton, not a [+Credits]-style tag.
A scene occupies its slot with the @ presence edge, written right in the
scene head: scene the_stinger @ post_credits { … } reads
“the stinger occupies the post-credits slot.” (It's sugar for a leading
the_stinger @ post_credits edge inside the block — engine ≥ 0.6.0.) A rule then
keys on the slot the same way (when: scene $s @ post_credits).
// the work-as-artifact — its medium rides on the Essence (%) facet
work the_film [%Film]
char nick_fury [+Spy]
// a structural slot on the work's skeleton (credits, prologue, the stinger)
concept post_credits
// the scene OCCUPIES the slot — the @ presence edge rides in the scene head
scene the_stinger @ post_credits {
beat 1 { evt tease [&Recruits(agent=nick_fury)] }
}
Two senses of “target” that don't collide: @ places a node at a slot or
location (presence — where a thing sits), while target= names an event's
patient (&Betrays(agent=vale, target=renn) — whom an act lands on).
Levels: home plane, loud crossings
Declare a node on its home plane; reference it from another plane via a
loud crossing <<home|name>> — never a bare, silent reference.
A plane change is always visible in the source. See diegetic levels.
POV planes follow a naming convention. A character's point of view lives on a plane
named pov_<charname> — e.g. <<pov_renn : non_canon>>. It's
typically non_canon (a perception, not ground truth), and the
perspective(renn) lens surfaces it by name. Use it to keep a character's possibly-false
world-model on its own plane, apart from canon.
Before you ship
A good file validates clean, carries no undeclared referents (front-load), and — for a story — fires the recognitions it means to.