The SCXML shapes the core.* vocabulary compiles to (ADR-0004 decisions
2-4), and the small builders the seven types share.
The one convention everything else rests on
Decision 2 fixes that a block compiles to exactly one state and signals
completion with done.state.<state id>. SCXML raises that event when a
compound state's configuration enters a <final> child, so every core
type emits a compound state carrying a <final> under the role "done",
and arranges its own work to reach it. That is the whole of what a parent
needs: StatifierBlocks.Compiler.Context hands it a child's state id and
done event, never the child's SCXML.
Sequencing is therefore always the same shape - transitions on the parent's own state, one per adjacent pair:
<state id="s_SEQ" initial="s_c1">
<transition event="done.state.s_c1" target="s_c2"/>
<transition event="done.state.s_c2" target="s_SEQ__done"/>
...c1's subtree... ...c2's subtree...
<final id="s_SEQ__done"/>
</state>A transition on a compound state fires while any descendant is active, so
the parent can wire its children without any of them knowing they were
wired. An empty run degenerates cleanly: initial points straight at the
<final>, and entering the block completes it.
Interrupts: two regions and a two-event protocol
core.group and core.resumable_group have to run their body while
interrupt handlers watch for events. A handler is a block like any other
(decision 2 admits no exception), so it compiles to a state, and a state
only sees events while it is active - which means the handlers must run
concurrently with the body. The group therefore wraps both in a
<parallel>: one region for the body, one region per handler.
That leaves one problem. What a handler's arrival should do - abandon
the group, or resume it - is the handler's outcome config, and decision
4 deliberately keeps a child's config out of the parent's context. The
group cannot read it and must not try.
So the group wires both outcomes unconditionally and the handler picks one by raising an event:
<transition event="statifier_blocks.interrupt.abandon" target="s_G__done"/>
<transition event="statifier_blocks.interrupt.resume" target="s_G__run"/>The handler <raise>s whichever its config names. Nesting behaves the way
an author would expect for free: both groups carry the same two
transitions, the raise happens inside the inner group's region, and SCXML
selects the transition whose source is the deepest active state - the
inner group's. A host interrupt handler joins the protocol by raising the
same two events; interrupt_events/0 is where they are named.
core.group has nothing to remember, so its resume target is the
<parallel> itself and the body restarts. core.resumable_group targets
a <history> inside the body region instead, of the type its history
config names - which is the whole of what that config buys, and why
ADR-0002 decision 10 could leave it as one :select field.
Summary
Functions
Sequences summaries and lands on exit_target.
A <final>, the state whose entry raises the owning block's done.state.
The two events an interrupt handler raises to tell the group it sits in
what to do. A host handler that wants to work inside core.group raises
one of these; a host group that wants to admit core.on_event transitions
on both.
The interruptible shape: the body in one region, each handler in its own, and the two-event protocol wired on the group's own state.
The shape every plain ordered container emits: a compound state running
summaries in order and finishing at its own <final>.
A <state>; initial is dropped when nil, which is how an atomic state is written.
A <transition>. opts carries :event, :cond, :target and
:internal; an absent one is simply not written, so an eventless
conditional transition and an unconditional one are the same builder.
Functions
@spec chain([StatifierBlocks.Compiler.Context.child_summary()], String.t()) :: {String.t(), [StatifierBlocks.Emission.t()], [StatifierBlocks.Emission.node_t()]}
Sequences summaries and lands on exit_target.
Returns {initial, transitions, child_refs}: the state to enter first
(the first child, or exit_target for an empty run), one transition per
adjacent pair plus one from the last child to exit_target, and a
placeholder per child for the compiler to splice.
Each transition is attributed to the child it leaves, not to the container that emitted it (ADR-0004 decision 5). "What happens after the authorize step" is the fact an author would recognise, so a finding against that transition belongs on the authorize block than on the sequence around it. Attribution carries no bytes, so this changes nothing about the generated chart.
@spec final(String.t()) :: StatifierBlocks.Emission.t()
A <final>, the state whose entry raises the owning block's done.state.
The two events an interrupt handler raises to tell the group it sits in
what to do. A host handler that wants to work inside core.group raises
one of these; a host group that wants to admit core.on_event transitions
on both.
@spec interruptible(StatifierBlocks.Compiler.Context.t(), String.t() | nil) :: {:ok, StatifierBlocks.Emission.t()} | {:error, {:invalid_role, String.t(), String.t()}}
The interruptible shape: the body in one region, each handler in its own, and the two-event protocol wired on the group's own state.
history is nil for a group with nothing to remember, or "shallow" /
"deep" for one that resumes where it left off.
@spec ordered(StatifierBlocks.Compiler.Context.t(), [ StatifierBlocks.Compiler.Context.child_summary() ]) :: {:ok, StatifierBlocks.Emission.t()}
The shape every plain ordered container emits: a compound state running
summaries in order and finishing at its own <final>.
@spec state(String.t(), String.t() | nil, [StatifierBlocks.Emission.node_t()]) :: StatifierBlocks.Emission.t()
A <state>; initial is dropped when nil, which is how an atomic state is written.
@spec transition( keyword(), [StatifierBlocks.Emission.node_t()] ) :: StatifierBlocks.Emission.t()
A <transition>. opts carries :event, :cond, :target and
:internal; an absent one is simply not written, so an eventless
conditional transition and an unconditional one are the same builder.
internal: true writes type="internal", and every transition a
container puts on its own state targeting one of its descendants
needs it. A transition is external by default, and an external
transition exits and re-enters its source even when the target is inside
it - which for a <parallel>'s region means tearing down the other
regions mid-flight. That is not a subtlety a block-type author should
have to rediscover, so the builders below set it and this note says why.
cond_key names the config field the cond came from verbatim, which
is what makes an upstream expression error the author's typo rather than
a bug in the block type (ADR-0004 decision 9). Pass it whenever the
condition is an author's :expression field rather than something the
type composed.