Beat Composition

Status: Current contract Authority: This note names the journal contribution pipeline alongside JOURNAL_COMPOSE_CONTRACT.md. The worked example is the composed_beat_demo world 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:

  1. Block locals: (authored YAML, no code)

  2. Ancestor locals: — scene, then story containers, closest first

  3. gather_ns dispatch contributions, merged later-wins across layers: AUTHOR beats APPLICATION beats SYSTEM; story-graph locals: beat world locals: 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_SLOT in the demo).

  • beat_overlay — emit a GroupFragment(group_type="beat") whose member_ids bind the composed fragments and whose content names the beat. Segmentation-aware retrieval (current_beat style 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

dock_mood in story vs block locals:

handler-scope chunk override

porter_greeting, APPLICATION vs AUTHOR gather_ns

conditional render enrichment

Maro’s reaction, gated on reputation

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.