StatifierBlocks.Describe (StatifierBlocks v0.41.0)

Copy Markdown View Source

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.

Summary

Functions

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

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

Types

t()

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

Functions

outline(document, palette, opts)

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(describe, opts)

@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.