# `StatifierBlocks.Describe`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/describe.ex#L1)

A block document described in words: an outline of its blocks joined by
the ways control passes between them, and one line of English per block
and per edge (ADR-0016).

    iex> alias StatifierBlocks.{Block, Describe, Document, Palette}
    iex> root =
    ...>   Block.new("core.sequence",
    ...>     id: "root",
    ...>     slots: %{
    ...>       "body" => [
    ...>         Block.new("core.wait", id: "wait", config: %{"duration" => "14d"}),
    ...>         Block.new("core.send", id: "send", config: %{"event" => "loan.overdue"})
    ...>       ]
    ...>     }
    ...>   )
    iex> root |> Document.new() |> Describe.outline(Palette.core(), []) |> Describe.render([])
    [
      "Run its steps in order",
      "Wait 14d",
      "Send loan.overdue",
      "The steps start with Wait 14d",
      "After Wait 14d (done), Send loan.overdue",
      "Send loan.overdue (done) ends the steps"
    ]

## Pure, and no model anywhere

`outline/3` is a function of the document, the palette and its options,
and `render/2` of the outline and its options. Neither compiles the
document, reads a clock or a random source, starts a process or sends a
message of its own, reaches a network or asks a language model, and
equal input answers byte-identical output. The one way either can reach
another process is not of its own making: a module is made sure of
(`Code.ensure_loaded?/1`) before it is asked - a palette's block-type
module by `StatifierBlocks.Palette.declares?/3`, the host's phrasing
module by `render/2` - and a module not yet loaded is loaded by the
runtime's code server. The only code either runs that this package does
not own is the palette's block-type callbacks, the host's phrasing
module and, because `outline/3` builds the document's view model with
`StatifierBlocks.ViewModel.build/3`, every
`StatifierBlocks.DocumentValidator` in the palette's `validators` list.
Each is held to be a pure function of its arguments by its own contract
(`StatifierBlocks.BlockType`, `StatifierBlocks.Describe.Phrasing`,
`StatifierBlocks.DocumentValidator`), and by contract only: nothing here
enforces it, so a host callback that reads a clock or sends a message
does so inside `outline/3` or `render/2`. A validator's findings take no
part in the outline: a document describes the same with or without its
palette's validators.

## Nodes

One `StatifierBlocks.Describe.Node` per block, every block exactly once,
in `StatifierBlocks.ViewModel.outline/1`'s order. A block whose type the
palette cannot resolve is described as its placeholder rather than
refused.

## Edges, from the document's structure

The edges are the block-level flow graph of the package's note
`docs/block-level-flow-graph.md`, read from the document's tree and the
block types' declarations; nothing is compiled. Edges are found inside a
resolved block of exactly the four types the note works through:

  * **`core.sequence`**: an `:entry` edge from its entry to its first
    child, a `:sequence` edge from each child to the next, and an `:exit`
    edge from the last child to its exit. An empty body is one `:entry`
    edge from its entry straight to its exit.
  * **`core.group`, `core.resumable_group`**: the same three over the
    `body` slot, then one `:interrupt` edge per handler in the
    `interrupts` slot whose `outcome` is `abandon` (to the group's exit)
    or `resume` (to the group's body, carrying a resumable group's
    `history`).
  * **`core.branch`**: per arm, in slot order, a `:branch` edge from the
    branch's entry to the arm's first child - or to the branch's exit
    for an empty arm - followed by that arm's `:sequence` edges and the
    `:exit` edge from its last child to the branch's exit, where the arms
    converge. `otherwise` always has its edge; `undecided` has one only
    when it holds a block.

A type is recognised here by the module the palette resolves the
block's type name to - `StatifierBlocks.Core.Sequence`,
`StatifierBlocks.Core.Group`, `StatifierBlocks.Core.ResumableGroup` or
`StatifierBlocks.Core.Branch` - not by the name itself; under
`StatifierBlocks.Palette.core/0` the two agree. A palette that registers
a host's own module under `core.sequence` gets that block described by
containment only, and one that registers `StatifierBlocks.Core.Sequence`
under a name of the host's own gets a sequence's edges. The timer edge
below recognises its sends, rules and awaits the same way.

A child here is one `StatifierBlocks.ViewModel.flow_children/1` answers
for the slot: a drafts shelf is a node and takes part in no edge.

A `:sequence` or `:exit` edge carries every outcome name the source
block's type declares, on the one edge. They are the declared outcomes,
not a compile's finals: `core.await` declares `received` and `timed_out`
whatever its config, so both ride its edge even with no timeout set.

Every other type - `core.parallel`, `core.foreach`, `core.map`,
`core.subchart`, `core.invoke`, a composite, a host type - is described
by containment and its node's `fan_label` alone, and draws no edge among
its children; a block of one still takes part in its parent's edges.

## The timer edge, the one edge an event name draws

An event name shared by a send and a handler is not an edge, with one
named exception (ADR-0016's amendment of 2026-09-27, ADR-0017 decision
3): a delayed `core.send`, one whose `delay` is a duration
`StatifierBlocks.Core.Duration.duration?/1` accepts, draws one `:timer`
edge to every `core.on_event` and `core.await` anywhere in the document
whose `event` is the send's `event`, the same string. The edge runs from
the send to the rule or await, its container is the send's parent, and
it carries the event and the delay as the send's config holds it. Only
blocks the palette resolved take part, and a block inside a drafts shelf
takes part in none. An undelayed send draws none. It is read from config,
never compiled, and it is not a transition: it says that the send arms an
event the other block waits for.

Timer edges follow every other edge, in the outline's order of their
sends and, for one send, of their targets. A document with no delayed
send describes exactly as it would without them.

## Lines

`render/2` answers one line per node, then one per edge, in the
outline's orders; ADR-0016 decision 2 gives each default line's words,
and a timer edge's line is `In <delay>, <event> reaches <target>`, the
delay written in the words `core.send`'s own sentence uses (`24h` reads
as `24 hours`).

An edge line names a step by its sentence and a container - the block
whose structure drew the edge, and that block's entry, exit or body -
by the container's node's `noun` (ADR-0016's amendment of 2026-09-29):
its title where it has one; else the noun its module is known by, `the
steps` for a sequence, `the group` for a group or a resumable group,
`the branch`, `the lanes` for a parallel and `the loop` for a for-each;
else `the` and its palette label in lower case. A noun that opens a
line with a lower-case `the`, a title included, is written with a
capital there, and a noun whose text is `the steps` or `the lanes`,
a title included, takes `start` where the others take `starts`, as in
`The steps start with Wait 14d` and `Send loan.overdue (done) ends the
steps`. A node that carries no noun is named by its sentence.

A step keeps its sentence in an edge line, first letter and all, and a
delayed send's sentence, `In 7 days, send loan.overdue`, is set there in
double quotation marks, because its comma would otherwise read as the
line's own: `The steps start with "In 7 days, send loan.overdue"`,
`After "In 7 days, send loan.overdue" (done), Wait 14d`. A delayed send
is read from its node alone - the type name `core.send` and the
sentence `core.send` writes for a delay - because `render/2` holds no
palette. Its own node line, and every other embedded sentence, is
written without the marks.

Every line is non-blank English with no newline, carriage return or tab,
and uncapped: a newline, carriage return or tab inside an author's text
is written as one space. A block's id never appears in a default line.

# `t`

```elixir
@type t() :: %StatifierBlocks.Describe{
  edges: [StatifierBlocks.Describe.Edge.t()],
  id: StatifierBlocks.Document.id(),
  nodes: [StatifierBlocks.Describe.Node.t()],
  revision: non_neg_integer()
}
```

# `outline`

```elixir
@spec outline(StatifierBlocks.Document.t(), StatifierBlocks.Palette.t(), keyword()) ::
  t()
```

The outline of `document` under `palette`: its id and revision, one node
per block and the edges between them, as the moduledoc describes.

`opts` is a keyword list; no key is read, and an unknown one is ignored.

# `render`

```elixir
@spec render(
  t(),
  keyword()
) :: [String.t()]
```

One line per node in the outline's node order, then one line per edge in
its edge order.

Every line is first written in the package's own words. With
`phrasing: module`, a module implementing
`StatifierBlocks.Describe.Phrasing`, each line is then offered to the
module's callback for the node's or edge's `kind`, where it declares one;
a usable answer replaces the line, and `:default` or a refused answer
keeps it (`Phrasing` gives the refusal set). Without the option every
line is the default.

The option's value is a module atom, and `phrasing: nil` is the same as
leaving it out. An atom that names no loadable module answers the
default lines; a value that is not an atom is outside this contract and
raises.

---

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