# `StatifierBlocks.Core.Parallel`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/core/parallel.ex#L1)

`core.parallel`: one slot per lane, with no ordering between the lanes
(ADR-0002 decision 10).

The second config-parameterized type. `config` carries an ordered
`"lanes"` list of bare lane names, and each lane `name` becomes the slot
`"lane_" <> name`:

    config = %{"lanes" => ["capture", "receipt"]}

    slots(config)
    #=> [{"lane_capture", :any, "capture"}, {"lane_receipt", :any, "receipt"}]

A lane stores its **bare name** and the slot prefixes it, where
`StatifierBlocks.Core.Branch`'s arms store the whole slot name. The
asymmetry is not this type's choice: it is what the ADR-0001 worked
example stores, and the stored bytes win over any tidier scheme.

Lane slots are `:any` - a declared-but-empty lane is an ordinary
intermediate state, not a finding - and the *order* of the lanes in
config is presentation order only. Nothing about a parallel block gives
its lanes a sequence; that is the whole point of it, and it is why
`palette_entry/0` declares `layout: :columns` so the lanes render side by
side (ADR-0005 decision 10).

`produces` is `:unknown` for the same reason `core.branch`'s is: joining
the lanes' outputs is a type lattice, and ADR-0003 decision 4 refuses to
build one.

## `complete`: when the block is done

A second config key, `"complete"`, picks the completion rule
(ADR-0004's 2026-08-29 amendment, "`core.parallel` `complete: first`"):

  * `"all"` (the default, and what an absent key reads as) is the
    statifier-native rule - a `<parallel>` is done when every region is,
    so one transition on `done.state.<run>` needs no join logic.
  * `"first"` is the racing rule - the block is done at the *first*
    lane's completion, which is one transition per lane on the
    `<parallel>` element itself, each taken on that lane's own
    `done.state.<region id>`.

The key is read through its default everywhere, so every
`core.parallel` stored before it existed decodes, validates, and
compiles to the byte it did before (ADR-0001 decision 6: a stored
`null` is *not* an absent key and is still refused).

# `emit`

A compound state wrapping one `<parallel>` whose regions are the lanes
(ADR-0004 decision 2), each region sequencing its own steps and finishing
at its own `<final>`.

    <state id="s_PAR" initial="s_PAR__run">
      <transition event="done.state.s_PAR__run" target="s_PAR__done"/>
      <parallel id="s_PAR__run">
        <state id="s_PAR__lane_capture" initial="...">...<final id="s_PAR__done_lane_capture"/></state>
        <state id="s_PAR__lane_receipt" ...>
      </parallel>
      <final id="s_PAR__done"/>
    </state>

A `<parallel>` is done when every region is, so the outer state needs one
transition and no join logic of its own.

The two role families are `lane_<name>` and `done_lane_<name>`. They
cannot collide with each other whatever a lane is called, because they
differ in their first token rather than their last - a lane named
`capture_done` mints `lane_capture_done` and `done_lane_capture_done`, and neither is
any other lane's id.

A parallel with no lanes emits no `<parallel>` at all: an empty one is
not valid SCXML, and "run nothing concurrently" is done the moment it
starts, so the block compiles to a state whose `initial` is its `<final>`.
That is true of both completion rules: there is no first lane to win
and no last lane to wait for, so `complete` moves no byte of it.

## `complete: "first"`: one transition per lane, on the `<parallel>`

The racing rule replaces the single `done.state.<run>` transition with
one transition **per lane**, placed on the `<parallel>` element itself
(ADR-0004's 2026-08-29 amendment, P1):

    <state id="s_PAR" initial="s_PAR__run">
      <parallel id="s_PAR__run">
        <transition event="done.state.s_PAR__lane_authorize" target="s_PAR__done"/>
        <transition event="done.state.s_PAR__lane_fraud_check" target="s_PAR__done"/>
        <state id="s_PAR__lane_authorize" ...>
        <state id="s_PAR__lane_fraud_check" ...>
      </parallel>
      <final id="s_PAR__done"/>
    </state>

Three properties of that shape are decisions rather than accidents.

**The transitions come before the regions.** Document order among the
children of one element is the emitter's to fix, and this is the fixed
position: the joins ahead of the lanes they join, which is the order the
upstream ruling's worked chart is written in and the order the wrapper
already uses for its own transition. One position, chosen once, is what
ADR-0004 decision 6's determinism asks for; the transition set stays a
pure function of the ordered lane list either way.

**They are external.** The block's done `<final>` is a sibling of the
`<parallel>`, not a descendant of it, so there is nothing for
`type="internal"` to preserve - and exiting the `<parallel>`, with every
region still in it, is precisely the effect wanted. Appendix D then runs
each losing region's `<onexit>` and raises one `CancelInvoke` per live
invocation it owns (P2). None of that is this package's to emit.

**The `done.state.<run>` transition is dropped, not kept.** It could
never be taken: `done.state.<run>` is raised only once every region is
final, and the first region to reach its own `<final>` raises
`done.state.<region>` first and exits the `<parallel>` on it. Keeping it
would write bytes that cannot fire, which a reader of the chart - or of
the provenance map - would have to explain to themselves. P1 says the
transition set is per lane and that nothing joins, so it is per lane and
nothing joins.

# `join_label`

```elixir
@spec join_label(StatifierBlocks.Block.config()) :: String.t()
```

What the join marker under the lanes reads, given the block's config.

A `paletteEntry` callback rather than a case in a renderer: the type
owns its own completion rule and therefore its own words, and nothing
on the layout path learns the string `"core.parallel"` (ADR-0005
decision 10). A host type that fans into lanes with a rule of its own
declares its own callback and gets its own.

    iex> StatifierBlocks.Core.Parallel.join_label(%{"complete" => "first"})
    "continue at first"

    iex> StatifierBlocks.Core.Parallel.join_label(%{})
    "continue when all"

# `sentence`

This block as one line of prose (ADR-0002's 2026-09-07 amendment).

How many lanes, and that they run together. The count is the well-formed
lanes - the same list `summary/1` draws chips for and `slots/1` mints
slots from - so a malformed entry costs the count rather than putting a
broken name in a sentence.

    iex> StatifierBlocks.Core.Parallel.sentence(%{"lanes" => ["fraud", "credit"]})
    "Run 2 lanes at the same time"

    iex> StatifierBlocks.Core.Parallel.sentence(%{"lanes" => ["fraud"]})
    "Run 1 lane at the same time"

    iex> StatifierBlocks.Core.Parallel.sentence(%{})
    "Run its lanes at the same time"

# `slots`

One `:any` slot per well-formed lane, in config order.

Total for any config: a lane name that could not make a usable slot name
is skipped rather than raised on, keeping ADR-0002 decision 6's stability
rule true mid-edit.

# `summary`

The lane names, as a chip list (ADR-0002 amendment H6).

Read through the same filter `slots/1` reads them through, so a
malformed lane is absent from the card exactly as it is absent from the
slot list and the two cannot disagree about which lanes exist.

Chips, not one string: a lane name longer than the presentation cap
loses its own chip and no other, since
`StatifierBlocks.BlockType.summary/2` refuses each one on its own. That
is the whole reason this is the shape it is - the authoring spike wrote
the same line as `fraud_review, balance_chec...` and clipped it.

    iex> StatifierBlocks.Core.Parallel.summary(%{"lanes" => ["capture", "receipt"]})
    ["capture", "receipt"]

    iex> StatifierBlocks.Core.Parallel.summary(%{})
    []

# `validate_config`

The lane findings, then the `complete` one.

`complete` is read through its default, so an *absent* key validates
exactly as it did before the key existed and a config that carries only
lanes still answers with only lane findings. A stored `null` is not an
absent key: `Map.get/3` hands the `nil` straight to `one_of/2`, which
refuses it (ADR-0001 decision 6).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
