Beat Composition¶
Status: Current contract Authority: This note names the journal contribution pipeline alongside
JOURNAL_COMPOSE_CONTRACT.md. The worked example is thecomposed_beat_demoworld bundle and its loader test.
A journal step is a small syuzhet problem: the engine knows a set of facts and consequences (the fabula), and the step’s fragments are one deliberate telling of them. The runtime already provides every channel that telling needs; this note names them so mechanics and worlds compose beats the same way instead of hand-rolling prose paths.
The pipeline¶
One step’s journal output is assembled in three moves:
Gather. do_gather_ns builds the scoped namespace: entity-local
get_ns() layers from the cursor and its ancestors (closest scope first),
then merged gather_ns dispatch contributions (later dispatch layers win).
Block content is rendered against this namespace with format_map, so any
named value — a chunk — is directly authorable as a {placeholder}.
Enrich. Extra fragments join the merged batch from two directions:
render_journal handlers contribute conditionally at render time, and
earlier phases (canonically UPDATE) stage delayed consequences through
ctx.injected_journal_fragments, which drain at the head of the merge.
Compose. compose_journal handlers fold over the merged batch in
dispatch order, each receiving the previous handler’s output. This is where
the telling is shaped: reorder into slots, substitute what the viewpoint
cannot see, bind the result into a retrievable overlay.
The override ladder¶
A named chunk can be defined — and overridden — at every scope, cheapest first. Resolution order for a chunk read at the cursor:
Block
locals:(authored YAML, no code)Ancestor
locals:— scene, then story containers, closest firstgather_nsdispatch contributions, merged later-wins across layers: AUTHOR beats APPLICATION beats SYSTEM; story-graphlocals:beat worldlocals:within the story layer
Data overrides sit above handler overrides: an author writing
locals: {dock_mood: ...} on a block beats every registered handler. Use
data scopes for authored variation (skins, per-node mood) and handler
scopes for computed or stateful chunks.
A narrative skin is this ladder applied at presentation scale: one
selector chunk (logic_skin in worlds/logic_demo) chooses a sparse prose
overlay while the underlying machine stays untouched. Skins are a gather
concern (what the chunks say); beats are a compose concern (how the telling
is ordered). The two layers are orthogonal.
The blessed stanzas¶
tangl.journal.compose names the recurring compose moves:
replace_first— substitute the first fragment matching a predicate (the visibility/suppression move).assemble_slots— reorder the batch into named slots (setting → incident → reaction → REST_SLOTin the demo).beat_overlay— emit aGroupFragment(group_type="beat")whosemember_idsbind the composed fragments and whosecontentnames the beat. Segmentation-aware retrieval (current_beatstyle queries) slices on this overlay rather than re-deriving membership.
Worked example¶
worlds/composed_beat_demo exercises every channel in one five-block
scene; engine/tests/loaders/test_composed_beat_demo_world.py pins one
channel per assertion. Mapping:
Channel |
Demo element |
|---|---|
data-scope chunk override |
|
handler-scope chunk override |
|
conditional render enrichment |
Maro’s reaction, gated on |
cross-phase enrichment |
manifest incident injected during UPDATE |
composition |
slot ordering, fog substitution, beat overlay |
Boundaries¶
Everything in JOURNAL_COMPOSE_CONTRACT.md applies unchanged: composition
shapes the telling, it does not mutate state, dereference media, or make
client-format decisions. Chunks are ordinary namespace entries — they are
visible to predicates, media facets, and dialog binding for free, so no
journal-private expression system should be introduced.