# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Entries for unreleased work are not written here directly. Each issue drops a
fragment in
[`changelog.d/`](https://github.com/riddler/statifier_blocks/blob/v0.35.1/changelog.d/README.md);
the fragments are assembled into a version section at release. See that README
for the format and for when a change warrants an entry at all.

## [0.44.1] 2026-10-20

Docs release: 0.44.1 changes documentation only. The README is cut to a
short introduction with a Basic usage block and a Documentation map, the
guides and reference pages that map links are published on HexDocs, and
the teaching examples are re-cut to patron registration. No function
body, typespec or option changes; apart from the `@compiler_version`
string, `lib/` and the stylesheet change only in `@moduledoc` and `@doc`
text, doctest literals and comments. No function answers differently
from 0.44.0, except that `StatifierBlocks.Compiler.compiler_version/0`
answers the new package version and a compiled record carries it as its
compiler version. The package gains no Hex dependency.

### Added

- New how-to pages: build a block document and compile it to a chart,
  register your own block types, publish a document, validate a document
  against the schema from any language, embed the editor in a LiveView
  host, and mount the Map. Each is in the HexDocs "How-to guides" group.
- New reference pages: the config fields and the handlers this package
  ships, and the editor assigns and host seams. The seam table also
  lists `Edit.Targets.admits_at?/5` and `accepted_types_at/5`,
  `Edit.Session.refusal/1`, the 32-character cap on badges and summary
  chips, and the `event` attr on `config_form/1`.
- A cheatsheet for the calls a host makes (the document, the palette,
  compile, publish and the editor mount), in the "Reference" group.
- An explanation page on documents, revisions, charts and executions, and
  why a document of blocks compiles to a chart, in the "Explanation"
  group.
- The existing pages for flow patterns, migrating documents from 0.27 to
  0.34, host validators, profiles, theming, typing a palette and the
  block-level flow graph are now published on HexDocs, in groups, and the
  package carries them.

### Changed

- The README is now an introduction: what the package is, why it exists,
  how to install it, one executed Basic usage block, and a Documentation
  map that links every page under four questions (learn, do, look up,
  understand). The sections it held moved to the pages above, with the
  design records folded into the map's Understand list.
- The publish page corrects what it moved from the README: it names the
  compile stages step 1 runs, says why a refused document is not
  compiled, says the blank await event's `:error` appears twice in the
  list, names `Graph.consumers_broken/2` as the call that pairs each
  finding with the parent's document id, says an undeclared datamodel
  path is `:error` when the document declares its own datamodel roots and
  `:info` when only the host declares, and names the release that added
  `check_accepts/2`.
- The describing-a-document page opens with a small patron registration
  document and takes the rewording of one edge by its endpoints.
- The host validators, theming, profiles and typing-a-palette pages, and
  the `InvokeStep` moduledoc and `Environment` doctests, teach in patron
  registration instead of card processing. The typing-a-palette page also
  carries the field-signature snippet and the `StatifierDatamodel.Types.satisfies/3`
  read check.
- The `StatifierBlocks` moduledoc points to the build-and-compile guide
  instead of the README's worked example.

## [0.44.0] 2026-10-19

0.44.0 adds two optional surfaces and checks more before publish. Under
Added: `core.await` takes an optional `capture` map with
`core.on_event`'s semantics, and the editor takes an optional
`advisories` assign through which a host raises findings of its own. The
changes to existing answers are under Changed:
`StatifierBlocks.Publish.findings/3` answers an undeclared datamodel path
at `:error` in a document that declares its own datamodel roots; a
`StatifierBlocks.Graph` finding on a reference a composite's expansion
placed is anchored on the composite block, where the compiled
`interface` now records that reference with `expanded_from`; a
`core.map` whose `collect_type` the supplied `:datamodel` does not
declare is reported unchecked, with a new `unresolved_reason`; and the
Map's how-to-read paragraph, its abandon end, its Finish edge and its
key's dashed-ring line read new sentences. Under Fixed: a face finding
on a selected card spans the card, the Expand gesture writes members at
their type's current version, the read-only config form renders a
passed `class`, and a drafts shelf that fails to lay out no longer
blanks the map. 0.44.0 is a minor, because it adds surface and what
existing functions answer changes. The package gains no Hex dependency.

### Added

- `StatifierBlocks.Editor` takes an optional `advisories` assign: a one-argument function the editor calls with the document on every rebuild, returning `{anchor, message}` or `{anchor, message, opts}` as a `DocumentValidator` does, and each becomes a `:lint` finding at `:warning` unless its `opts` say otherwise, so a host that holds a `core.map`'s child chart can raise a `:warning` on `collect_type` when `BlockType.agrees?/3` says the pair disagrees; `findings_count/3` takes the same function as its `:advisories` option. With the assign unset the editor draws exactly what it drew before.
- `core.await` takes an optional `capture` map with `core.on_event`'s semantics: each pair writes a value out of the awaited event's `_event.data`, or a `["const", value]` literal, into a datamodel path on the transition the event selects, and the editor draws the same "Capture from the event" rows for it. An await with no `capture` compiles to the same bytes as before; an await that already stored a `capture` key, which was ignored until now, compiles to its assigns, and a malformed one is refused. Since the editor draws those rows for every await, the first time its form posts an edit to an await that holds no `capture`, the block's config gains an empty `capture` map, which compiles to the same bytes as an absent one.

### Changed

- The map's how-to-read paragraph now says a caption sits on a branch's band, or under a group's rules column with its explanation beneath, in place of "a one-line caption".
- The map's abandon end, the Finish edge into it and the key's dashed-ring line now say an interrupt rule abandons "a group at the end of its flow" in place of "its last step", since a group deeper in the document's tail draws its own abandon end.
- `StatifierBlocks.Publish.findings/3` answers an undeclared datamodel path at `:error`, not `:info`, when the document declares its own datamodel roots (its `datamodel` key holds an entry), so a host's publish step that refuses on `:error` now refuses such a document; the finding keeps the advisory's anchor, `:lint` source and message, and the editor still shows it at `:info`. A document that declares no roots is unchanged: with only a host-supplied `:datamodel` or `:declare` roots the advisory stays `:info` and refuses nothing, and with nothing declared anywhere there is no advisory.
- A `StatifierBlocks.Graph` finding on a `core.subchart` or `core.map` that a composite's expansion placed is now anchored `{:block, composite_id}` on the composite block the stored document holds, and the compiled `interface` records that reference under the composite with the expanded member's id as `expanded_from`, in place of the member's id, which the stored document does not hold.
- A `core.map` whose `collect_type` names a type the compile's `:datamodel` does not declare is now marked `unresolved` in the compiled `interface`, with a new `unresolved_reason` (`:not_declared`, or `:no_datamodel` for a compile without `:datamodel`), and `StatifierBlocks.Graph.check/2` and `consumers_broken/2` report its done-data read as unchecked with the same `:graph` `:warning` a compile without `:datamodel` gets, its message saying the supplied datamodel does not declare the name; before, the read passed without a finding. It is never an `:error`, so a publish step refusing on errors does not refuse on it.

### Fixed

- A drafts shelf that fails to lay out on its own no longer replaces the whole map with the error pane: the drawer is left out and the path is drawn.
- `Editor.ConfigForm.config_form/1` now renders a `class` a host passes on its read-only form as well as on the editable one, beside the form's own `sb-form sb-form--readonly`.
- A face finding on a selected card now spans the card's full inner width below the title row, where it had been squeezed to a column about one character wide once the card's controls were revealed.
- The editor's Expand gesture now writes each member of a composite at its block type's current version, so a member past version 1 (`core.send`, `core.wait`) is stored as authored at that version, instead of being stored at version 1 and migrated on the next compile: with a duration in an accepted spelling the expanded document compiles to the same chart the composite did, and with one in a retired spelling the composite and the expanded document both refuse the same field with the same message.

## [0.43.1] 2026-10-19

0.43.1 widens the `predicator` requirement so that it admits 9.5: a host
moving to a `statifier` that requires `predicator` 9.5 takes this release.
No function answers differently from 0.43.0, except that a compiled
record carries the new package version as its compiler version, and the
package gains no Hex dependency.

### Changed

- The `predicator` requirement now admits 9.5 (`~> 9.4 and >= 9.4.1`), keeping the 9.4.1 floor and the 9.x cap, so a host that moves to a `statifier` requiring `predicator` 9.5 can resolve this package beside it.

## [0.43.0] 2026-10-10

0.43.0 changes what the Map draws and says, so that a reader can follow a
document from its start to its ends. The start dot comes first, at the
top, marked Start; every end ring is marked End; a legend beside the
drawing says what each of its marks means; a drafts shelf moves into a
drawer under the drawing; and a group's caption is its type's whole first
sentence. The changes to existing answers are under Changed: the graph
`StatifierBlocks.Map.graph/2` answers gains the start dot at its top
level, the Start and End labels, a `legend`, a `drafts` field, abandon
ends deeper in the tail, a rules column caption and indented arm entries;
the descriptions `StatifierBlocks.Map.Info` answers gain a "Finishes with"
fact and a "Legend" fact and read new sentences; and a list of datamodel
paths gets the advisory and the suggestions a single path gets. Under
Fixed: the Map hook draws the first layout when its second fails, and a
blank timer or interrupt event reads "its event". 0.43.0 is a minor,
because what existing functions answer changes. The package gains no Hex
dependency.

### Changed

- The Map's start edge description reads as two sentences, what starts the document and then the step that runs first: `StatifierBlocks.Map.Info.elements/6` answers the start edge's `sentence` as "Starts when the system is told to. First: Wait 21 days." where it answered "Starts when told to, Wait 21 days". The first step's line closes with one full stop, which a line already ending in one keeps. The edge's facts ("What starts it", "First step") and the caption drawn on the edge are unchanged.
- The Map draws a drafts shelf in a drawer under the drawing rather than as a box in the path's column, and lays the path out as though the shelf were not there. The drawer is a native disclosure element whose row reads "Drafts: N steps not wired in" ("1 step" for one), holding the shelf's own drawing: closed on a page that cannot edit, opening in place on a click on its row, and drawn open on a page that can edit. The shelf's steps in an open drawer select and arm inserts as the drawing's do. `StatifierBlocks.Map.graph/2` marks the shelf's node with a new `drafts` field, `%{"count" => count, "label" => label}`, and its `legend` gains a last `drafts` entry whose line is that row; the node stays where it was, so `StatifierBlocks.Map.nodes/1` is unchanged. A host drawing the graph itself finds the shelf by that field, and a host that matches the idle description's "Legend" fact reads the new line on a document with a shelf.
- On a document whose root body holds only a drafts shelf, the graph's start edge now runs into the root's box rather than into the shelf.
- `StatifierBlocks.Map.graph/2` places the start dot first in reading order: the dot is now the graph's first node, ahead of the document's root box rather than inside it, and its start edge is the graph's own, so the map draws the dot and its caption at the top, above the root box's title and every box label. A host that reads the graph's shape finds the dot and its edge at the graph's top level, beside the root box; the edge still runs into the first step of the document's flow, and the type names stay on the boxes. `StatifierBlocks.Map.Info.elements/6`, which lists elements in the order a walk of the graph meets them, now lists the start dot before the root box and the start edge after the root's own elements and edges.
- `StatifierBlocks.Map.Info.idle/4`'s `explanation`, the paragraph on how to read the map, opens with "A plain box is a step, drawn inside the step that holds it, with its type's name on top and what it does under it." in place of a sentence that called every box a step. The sentence on shaded boxes no longer names a group's interrupt rules; a new sentence after it says "A column beside a group holds the rules that watch it, or the step that runs when it fails." A host that matches the explanation text reads the new sentences.
- A timer edge's explanation in `StatifierBlocks.Map.Info.elements/6` now says the delayed event arrives at the interrupt rule or the hourglass step that hears it, the words the Map's key uses for the dotted arrow, where it said the rule or the wait it points at hears it.
- The Map draws a key of its marks beside the drawing, at its top right: the start dot, the end rings, solid, dashed and dotted arrows, the fork, the join, the hourglass and the clock, each with the drawing's own glyph and one line saying what it means, and only the marks the drawing uses. `StatifierBlocks.Map.graph/2` answers it as a new top-level `legend` list, one `%{"mark" => mark, "line" => line, "lines" => lines}` per mark, which the Map hook draws; the hook widens the SVG to hold it. A host drawing the graph itself finds the key there.
- `StatifierBlocks.Map.Info.idle/4` answers the key's lines as a new last fact, `{"Legend", lines}`, since the map is hidden from assistive technology, and its `explanation`, the paragraph on how to read the map, is shorter: what each mark means moves to the key, and the paragraph points at it. `StatifierBlocks.Editor.MapRegions.description_region/1` shows the fact with a map beside it and leaves it out with `map={false}`, as it leaves out the paragraph. A host that matches the idle description's facts or its explanation text reads the new values.
- The Map's key of its marks is titled "Legend", and `StatifierBlocks.Map.Info.idle/4` answers the key's lines as its last fact under the label "Legend" rather than "Key"; `StatifierBlocks.Editor.MapRegions.description_region/1` with `map={false}` leaves that fact out as before. The key's lines and the paragraph on how to read the map are unchanged. A host that matches the idle description's facts by label reads "Legend".
- The Map hook now holds the SVG wide enough for a timer edge between two steps of one column, the run it draws past their right sides and its delay, when the drawing has no key; before, only a key beside the drawing widened the SVG for it.
- `StatifierBlocks.Map.caption/1` now answers the whole first sentence of the type's explanation for a branch and the two group types, never cut to one line and ended in "..." as a group's was; a host that drew `caption/1` itself should expect the longer text.
- A group's rules column in `StatifierBlocks.Map.graph/2` now carries `caption` "Rules: watch the steps while they run" in place of the group's sentence, and a new `explanation_lines` field, that sentence wrapped to a box's line; the Map hook draws the caption under the column's label and the lines under it, and the column grows to hold them.
- The Map hook draws a resume edge clear of its own rule's box when the middle of the head of the group's body is not well above the rule's top: the edge still leaves the top of the rule and ends on the head's side, but it now goes up a short way, left past the rule's side and down to the head's height before it turns in, where it ran straight down through the rule's box. A resume edge whose head stands well above the rule is drawn as before. Only the hook's drawing moves; `StatifierBlocks.Map.graph/2` answers the same graph.
- A `{:list, {:path, opts}}` field's rows now get what a `{:path, opts}` field gets: `StatifierBlocks.Datamodel.findings/4` returns the `:info` undeclared-path advisory for each undeclared row, anchored on the list field's key, and the editor binds each row's input to a `<datalist>` of `path_candidates`; `BlockType.datamodel_path?/1` still answers false for a list.
- `StatifierBlocks.Map.graph/2` sets a wrapped arm entry apart on a branch's header: every line after the entry's first now starts with no-break spaces as wide as the entry's number (`"1. "`, `"10. "`), so the map draws it indented under the arm's label and a wrapped condition no longer reads as an entry of its own. An entry that fits on one line, and every other box's lines, are unchanged. A host that reads a branch node's `lines` finds the indent at the start of each continuation line.
- The Map's start dot now carries the word Start and every end mark the word End, whichever outcome it ends with; the outcome stays the caption on the arrow into the mark. In `StatifierBlocks.Map.graph/2` the start node and each end node carry the word as their one ELK `labels` entry, with `layoutOptions` asking the layout to place it outside the mark on its right, and the Map hook draws it there; a host drawing the graph itself finds the word in that label. The Map's legend lines name the dot marked Start and the ring marked End.
- The Map's start caption names who starts the document: `StatifierBlocks.Map.start_text/0` answers "Starts when the system is told to" where it answered "Starts when told to", the same sentence on every document, so the caption on the start edge `StatifierBlocks.Map.graph/2` answers, the "What starts it" fact and the start edge's description sentence `StatifierBlocks.Map.Info` answers ("Starts when the system is told to. First: Wait 21 days.") read it too. A host drawing the graph itself finds the longer caption, and its wider label, on the start edge.
- The Map draws the dashed abandon end for a group deeper in a document's tail, not only for a group that is the root's last step: a group that is the last step of an arm of the root's last step, or of the body of a group that is, finishes the document when one of its rules abandons it. In `StatifierBlocks.Map.graph/2` such a document gains the `abandon` end node, and an `abandon` edge into it from each group in that position whose rules abandon it, so one abandon end can have several edges into it; the `done` end and its edge are unchanged. `StatifierBlocks.Map.Info.elements/6` describes each new edge as it describes the root-level one.
- `StatifierBlocks.Map.Info.idle/4` answers a "Finishes with" fact right after "What starts it": the document's ends in the words its end marks and the arrows into them carry, in order (each outcome, and "abandons" for an abandon end), for example `["done", "abandons"]`. A document whose map draws no end, such as one with no step yet, has no such fact.

### Fixed

- The Map hook draws the first layout, its line bent at the group, when the second layout of a graph whose group body holds a container fails, instead of drawing the error pane.
- `StatifierBlocks.Describe.render/2` reads a timer or interrupt edge whose event is blank as "its event", as it already did for an edge with no event, instead of leaving a gap in the line.

## [0.42.1] 2026-10-05

0.42.1 changes documentation only: no function's behaviour changes, and the
package gains no Hex dependency.

### Changed

- The hexdocs sidebar groups the guides by the kind of page: the guide to describing a document sits under How-to guides, and the README and the CHANGELOG stay ungrouped at the top. The module groups are unchanged.
- Three guides read on GitHub take titles that say what they help you do: "How to build common flow patterns", "How to mount the editor for one audience" and "How to theme the editor". No file is renamed, so links to them still resolve.
- `StatifierBlocks.Editor.Inspector`'s module documentation now places the Note section beside the Config tab's two sections and says why it alone appears only with a block selected.

## [0.42.0] 2026-09-30

0.42.0 changes three things a host of the editor or the Map can see, and
fixes what raised or dropped out on documents and classes a host passes.
The changes to existing answers are under Changed: `map_region/1` no
longer stamps `data-info-region` on the map's element; the editor stores a
browser's CRLF note line breaks as LF, while the note command a host issues
keeps its bytes; and a host's `:phrase` words reword an event name only in
event position. Under Fixed: a note-free block kept as a stored Erlang term
no longer raises in the note command, the Map's descriptions or the
editor's note field; `description_region/1` degrades instead of raising on
a composite whose declaration cannot expand; and the host's `class` reaches
the description region's hover layer. 0.42.0 is a minor, because what
existing functions answer changes. The package gains no Hex dependency.

### Changed

- `StatifierBlocks.Editor.MapRegions.map_region/1` no longer stamps `data-info-region` on the map's element, which the `StatifierBlocksMap` hook has not read since 0.40.0 (it finds its hover layer by `data-info-hover` and its store by `data-info-store`), so the hover is unchanged. A host that stamps the attribute on its own element has nothing to do; a host that read it off the element finds it gone and takes the region's id from the `description` it passes.
- The editor's note field now stores each CRLF line break a browser posts as LF, so a note written in the editor holds the same bytes as the same note written by a host, and a change of line endings alone is no longer an undo entry; a lone CR is stored as posted, and the `{:update_note, id, note}` command a host issues itself still stores the note as written.
- A host's `:phrase` words for its event names (`StatifierBlocks.Map.graph/2` and the Map's description region) now reword a name only in event position - after "Send" or a delayed send's "send", after "Wait for", or after a rule's "When" - and leave a known name anywhere else as written, such as a path a step sets, a name after a send or wait word glued to a dotted prefix, or the event a timer edge's line names before "reaches"; a host that relied on a name reading as words elsewhere now sees the name.

### Fixed

- The `class` a host passes to `StatifierBlocks.Editor.MapRegions.description_region/1` now goes on the region's hover layer as well as on the region, so styling the host gives the region through it no longer drops out while a hover is shown.
- A document whose blocks were built before blocks had a note, and that a host kept as a stored Erlang term rather than as JSON, now also goes through the note edit command `{:update_note, id, note}` (its inverse carrying the empty note), `StatifierBlocks.Map.Info`'s descriptions and the editor's note field as a note-free document, instead of raising `KeyError` or `CaseClauseError`.
- `StatifierBlocks.Editor.MapRegions.description_region/1` no longer raises when a composite in the document has a declaration that cannot expand: the region renders with no description in it instead of raising out of the host's render, as the editor already survives such a document.

## [0.41.0] 2026-09-29

0.41.0 changes how the Map draws three things and how Describe names a
container. The Map draws the happy path straight through a group whose
body holds a container, the Map hook laying such a document out a second
time; it draws the clock mark on a timed wait; and its how-to-read text
names every mark it draws. The changes to existing answers are under
Changed: `StatifierBlocks.Describe.render/2` names a container in its edge
lines by a noun rather than by its sentence, and sets an embedded delayed
send's sentence in double quotation marks, so a host that matched the old
edge lines reads the new words; and the Map hook warns once in the
browser console when its element has no `data-map-canvas` child.
`StatifierBlocks.Describe.Node` gains the `noun` field. 0.41.0 is a
minor, because what an existing function answers changes and a public
struct gains a field. The package gains no Hex dependency.

### Added

- `StatifierBlocks.Describe.Node` carries `noun`, the name an edge line gives the block when it is the edge's container: its title, else a short noun for its type, and `nil` for a block with no slots or one the palette cannot resolve.

### Changed

- The Map draws the happy path straight through a group whose body holds a
  container, such as a branch: the Map hook lays that document out twice,
  the second time with the group's edges attached where its body's steps
  stand, instead of at the middle of the group.

- The paragraph on how to read the map, in the idle description, names
  every mark the Map draws once each: the shaded slot boxes, the band and
  its fork mark, the captions, the rejoin's dot and heavier arrow, the
  selection outline and the "+" on a page that can edit, beside the marks
  it already named. It says a dotted timer arrow runs to the interrupt
  rule or the hourglass step that hears it.

- `StatifierBlocks.Describe.render/2` names a container by a noun in its edge lines rather than by its sentence: its title where it has one, else a short noun for its type (`The steps start with Wait 14d`, `Send loan.overdue (done) ends the steps`, `abandons the group`); a host's container reads `the` and its palette label in lower case. Node lines, and a step named in an edge line, keep their sentence. A host that matched the old edge lines reads the new words or rewords them through `StatifierBlocks.Describe.Phrasing`.

- `StatifierBlocks.Describe.render/2` sets a delayed `core.send`'s sentence in double quotation marks wherever an edge line embeds it, so its own comma no longer reads as the line's: `The steps start with "In 7 days, send loan.overdue"`, `After "In 7 days, send loan.overdue" (done), Wait 14d`. The send's node line, an undelayed send and every other step read as before. A host that matched the old edge lines reads the new words or rewords them through `StatifierBlocks.Describe.Phrasing`.

- The Map draws the clock mark on a timed wait (`core.wait`), as it does on
  a delayed send: the clock says time passes, and the hourglass on an
  await says a step waits for an event. The mark is a mark only; a wait
  still takes no part in a timer edge. The paragraph on how to read the
  map, in the idle description, says so.

- The Map hook warns once per mount in the browser console when its element
  has no `data-map-canvas` child. It still draws into the element itself, as
  before, but that is not supported: LiveView patches the element, so a
  patch can take the drawing away. `map_region/1` always renders the child;
  a host that attaches the hook to its own element gives it one.

## [0.40.0] 2026-09-29

0.40.0 lets an author write a block's note in the editor, and shows the
selected block's description under the editor's canvas. The new edit
command `{:update_note, id, note}` writes the note with the previous note
as its inverse, so undo and redo move it, and the inspector's Note field
issues it; `StatifierBlocks.Edit.t()` gains that member, so a host that
matches every member of the union by hand has one more to handle. The
editor draws the Map's description region,
`StatifierBlocks.Editor.MapRegions.description_region/1`, under its canvas
with no map beside it, and the region takes new `map` and `label` attrs
whose defaults render what it rendered before. The changes to existing
answers are under Changed: the region no longer announces a hover, which
shows in a hover layer beside it that the Map hook finds by
`data-info-hover`, and `map_region/1` and `description_region/1` refuse a
`phrase` that is not a function of one argument with an `ArgumentError`
naming the attr. Under Fixed, a block kept as a stored Erlang term from
before blocks had a note encodes and validates, and the npm manifest names
both licences. 0.40.0 is a minor, because it adds to the public surface,
a member of the edit union included. The package gains no Hex dependency.

### Added

- The editor draws a description region under its canvas: the selected
  block's description, its note first, or the document's name, description,
  what starts it and its counts when nothing is selected. It is the Map's
  `StatifierBlocks.Editor.MapRegions.description_region/1`, live and showing
  values only; the editor still draws no map. It is drawn in a read-only
  mount too, and no `profile` key hides it.
- `description_region/1` takes a `map` attr, `true` by default, saying
  whether a map is mounted beside the region; `false` renders no hover
  layer, no store and no how-to-read paragraph in the idle description. A
  host that passes nothing gets the region it had.

- The edit command `{:update_note, id, note}` writes a block's author note,
  with the previous note as its inverse, so undo and redo move it like any
  other edit; an empty note removes it. `StatifierBlocks.Edit.t()` gains this
  member, so a host that matches every member of the union by hand has one
  more to handle.
- The editor's inspector opens its Config tab on a Note field for the
  selected block, which writes the note through that command; a read-only
  mount shows the note as text and does not edit it.

- `StatifierBlocks.Editor.MapRegions.description_region/1` takes a `label`
  attr, the live region's accessible name, rendered as its `aria-label`;
  it defaults to `"Description"`, so a host that passes nothing gets the
  region it had.

### Changed

- `StatifierBlocks.Editor.MapRegions.map_region/1` and `description_region/1` refuse a `phrase` that is neither `nil` nor a function of one argument with an `ArgumentError` naming the attr, where they raised a `FunctionClauseError` from a private function; `nil` and a one-argument function render as before.

- The Map's description region no longer announces a hover: what `description_region/1`'s `aria-live="polite"` region holds changes only when the host renders a new selection, and a hover over the map shows its description in a new hover layer beside the region instead (`aria-hidden`, `hidden` until the `StatifierBlocksMap` hook fills it, left alone by LiveView), which the package stylesheet draws in the region's place. The region now sits with the layer inside a `.sb-map__description-frame` wrapper, and `map_region/1` also stamps the layer's id in `data-info-hover`; a host that set the `--sb-*` palette on `.sb-map__description` alone sets it on `.sb-map__description-frame` so the layer reads it too.
- The `StatifierBlocksMap` hook now finds its hover layer by `data-info-hover` and no longer reads `data-info-region`, so a host that stamps the hook's ids on its own element and names only the region and the store gets no hover: stamp `data-info-hover` with the id of an `aria-hidden`, `hidden` element beside the region for the hook to fill, or mount `description_region/1` and `map_region/1`, which render and stamp it.

### Fixed

- A document whose blocks were built before blocks had a note, and that a
  host kept as a stored Erlang term rather than as JSON, now encodes and
  validates as a note-free document instead of raising `KeyError`.

- The npm package manifest names its licence as `(MIT AND EPL-2.0)`, since it ships the vendored elkjs file under EPL-2.0.

## [0.39.0] 2026-09-28

0.39.0 ships the Map, a second view of a block document drawn beside the
host's own list. `StatifierBlocks.Map` is a pure reader of the view model,
and nothing about the map is stored in the document. A draw-only hook,
`StatifierBlocksMap`, lays that graph out and draws it from its own entry
point, `statifier_blocks/map`, with elkjs 0.9.3 vendored (about 1.6 MB,
loaded only by a host that imports that entry point; the default export is
unchanged). `StatifierBlocks.Map.Info` answers the description region's
data, and `StatifierBlocks.Editor.MapRegions` gives a host the map region
and the description region to mount beside its list. The one change to an
existing answer is under Changed: Decode admits an optional `note` string on
a block, a key it refused before, and refuses a note that is not a string
with the new reason `{:malformed_block, id, {:note, :not_a_string}}`. Under
Fixed, a card whose control strip wraps now holds that wrapped height at
rest. The vendored elkjs file is the package's one file under the Eclipse
Public License 2.0, so the package metadata lists `EPL-2.0` beside `MIT`.
0.39.0 is a minor, because it adds to the public surface and changes no
existing signature. The package gains no Hex dependency.

### Added

- `StatifierBlocks.Map.Info` answers the description region's data: `elements/6` describes every element `StatifierBlocks.Map.graph/2` draws, keyed by the id the map draws it under - a block or a rule with its title, sentence, type's explanation, settings, place, outcomes, interrupt rules, timer edges and findings, a slot, an arm, an empty marker, the start and end marks, a connector, an interrupt edge and a timer edge - and `idle/4` describes the document for when nothing is selected. A block's author-written `note` is its description's `note`, shown above the built-in text; the host's `:phrase` reads event names as words, as the Map's does.
- The `StatifierBlocksMap` hook swaps the description region, in the browser, to the description of the element under the pointer and puts the region back when the pointer leaves, reading the region and a hidden store of rendered descriptions by the ids the host stamps on the map's element (`data-info-region`, `data-info-store`). Hover pushes nothing and changes no selection; a host that stamps neither id gets no hover.

- `StatifierBlocks.Map.graph/2` builds the graph a layout hook draws the block document from, as boxes in boxes read off the view model: the start dot, an end mark per way the document finishes, a branch's arms under one band, a group's rules in a side column, a marker per empty slot, every edge the outline describes including the dotted timer edge, the host's selection marked and the host's words for its event names read into the sentences; with `nodes/1`, `interrupts/1`, `timers/1`, `caption/1`, `empty_text/0` and `start_text/0` beside it.

- `StatifierBlocksMap`, a draw-only LiveView hook at its own entry point `statifier_blocks/map` (`assets/js/statifier_blocks_map.js`), lays out the graph `StatifierBlocks.Map.graph/2` answers with elkjs and draws it as plain SVG: model order kept, the happy path straight through a group's fixed ports, an error pane in place of a map that could not be laid out, a moved selection re-marked without a second layout, and a click on a box, a gap or an empty slot's marker sent as the host list's own select or insert event, under the names the host stamps on the element (`data-select-event`, `data-insert-event`). It is not in the default export, so a host that never mounts the Map never bundles the layout library.
- elkjs 0.9.3 ships vendored and unmodified as `assets/vendor/elk.bundled.js` (1,606,238 bytes, 466,990 gzipped at the highest level, loaded whole by any bundle that imports the Map hook), with its Eclipse Public License 2.0 text beside it as `assets/vendor/elkjs-LICENSE.md`.

- `StatifierBlocks.Editor.MapRegions`, the two function components a host mounts to show the Map beside its own list. `map_region/1` renders the element the `StatifierBlocksMap` hook draws into, stamped with the graph `StatifierBlocks.Map.graph/2` answers for the host's view model and selection, the host list's own select and insert event names, whether the page can edit, and the description region's ids for hover; the region is `aria-hidden` and its scroll box `tabindex="-1"`, so the list stays the keyboard and screen-reader path. `description_region/1` renders the `aria-live="polite"` region the list's rows name with `aria-describedby`, with the selected block's `StatifierBlocks.Map.Info` description (its note first) or the document's idle one rendered on the server, and beside it the hidden store of every element's description the hook's hover reads.
- The package stylesheet styles both regions under `sb-map__` classes and declares the nine `--sb-map-*` tokens the Map hook's drawing reads (`--sb-map-block-fill`, `--sb-map-block-stroke`, `--sb-map-slot-fill`, `--sb-map-slot-stroke`, `--sb-map-empty-fill`, `--sb-map-empty-stroke`, `--sb-map-edge`, `--sb-map-mark`, `--sb-map-band-fill`) on `.sb-map__region`, mapped onto the `--sb-*` palette.

### Changed

- Decode admits an optional `note` string on a block, a key it refused
  before: `StatifierBlocks.Block` gains a `note` field (default `""`, the
  absent note) that `Block.new/2` takes as `:note`, canonical form omits an
  empty note, the shipped schema describes it, and a non-string note is
  refused as `{:malformed_block, id, {:note, :not_a_string}}`. A note
  changes the document hash, not the compiled chart.

### Fixed

- A card whose text is shorter than its wrapped control strip no longer grows taller on hover, selection or keyboard focus: the strip wraps its controls at rest too, so the card holds that height in every state.

## [0.38.0] 2026-09-28

Upgrading a host: a block type's declared field types now bind, so a host
config its declared field types refuse is now refused before
`validate_config/1` runs, at compile, at the edit gate and in the view
model. After taking this version and before deploying it, run
`StatifierBlocks.Palette.preflight/1` over your palette, and
`StatifierBlocks.Palette.preflight/2` over the documents you store, and
correct each declaration or stored block it lists; an empty list means you
are ready. That is the one change to an existing answer, under Changed.
0.38.0 is a minor, because it adds to the public surface and changes no
existing signature: `StatifierBlocks.Schema.for_palette/1` answers a
draft-07 schema for one palette, and `preflight/1` and `preflight/2` are
new. The package gains no dependency.

### Added

- `StatifierBlocks.Schema.for_palette/1` answers a draft-07 schema for a palette: the shipped root with every block also judged by its own type's definition, a `core.*` type's from `definitions/core` and a host type's generated from its declared `config_schema/1` and `slots/1`, each declared field typed from its declared field type.

- `StatifierBlocks.Palette.preflight/1` lists every host type in a palette whose own defaults, or the block `Palette.new_block/2` builds from them, disagree with their declared field types, each finding naming the type, the field's key, its declared type and the value; `preflight/2` also lists every block of the given documents whose config its type's declared field types refuse, by block id. A type whose callbacks raise is a finding, never a raise.

### Changed

- A block type's declared field types now bind: a config value whose declared field type in `config_schema/1` does not admit it (an integer in a `:string` field, a value outside a `{:select, choices}`) is refused before `validate_config/1` runs, at compile, at the edit gate and under the field in the view model, with a finding naming the field's key and its declared type; `validate_config/1` still runs and its findings follow. An absent key and a `null` are still the type's own to judge. After taking this version and before deploying it, run the palette pre-flight (`StatifierBlocks.Palette.preflight/1`, or `preflight/2` with your stored documents) over your host types and correct each declaration or stored block it lists, since a host type whose documents disagree with its declared field types will see new `:config` findings.

## [0.37.0] 2026-09-28

0.37.0 is a minor, because it adds to the public surface and changes no
existing signature. A block type can now say what every block of it does:
`StatifierBlocks.BlockType` gains an optional `explain/0` callback and the
resolver `explain/1`, and every `core.*` type declares one.
`StatifierBlocks.Describe` draws a new edge kind, `:timer`, from a delayed
`core.send` to the rule or await that names its event, carrying the delay
in `StatifierBlocks.Describe.Edge`'s new `delay` field, which a host
rewords through the new optional `Phrasing.timer/2` callback. The one
change to an existing answer is `core.send`'s card line for a delayed
send, which now names the delay, under Changed. Upgrading a host: nothing
needs migrating, the package gains no dependency, and a document with no
delayed `core.send` reads and describes exactly as it did in 0.36.1.

### Added

- `StatifierBlocks.BlockType` gains an optional `explain/0` callback, one short paragraph saying what every block of the type does, and the resolver `StatifierBlocks.BlockType.explain/1`, which answers that paragraph, else the palette entry's `description`, else `nil`, through the palette so a `{module, state}` type resolves the same way; every `core.*` type declares one.

- `StatifierBlocks.Describe.outline/3` answers a new edge kind, `:timer`, from a delayed `core.send` to every `core.on_event` and `core.await` in the document that names its event, after every other edge and carrying the delay in `StatifierBlocks.Describe.Edge`'s new `delay` field; `render/2` writes it as `In 24 hours, registration.deadline reaches ...`, and a phrasing module rewords it through the new optional `StatifierBlocks.Describe.Phrasing.timer/2` callback. A document with no delayed send describes exactly as before.

### Changed

- A `core.send` block with a delay names that delay on its card line: `sentence/1` answers `In 7 days, send loan.fines_notice` where it answered `Send loan.fines_notice`, the short duration form put in words and any other stored spelling shown as stored, and the `StatifierBlocks.Describe` lines built from that sentence change with it; a send with no delay reads exactly as before.

## [0.36.1] 2026-09-27

0.36.1 is a patch, because it changes no public function, option or
callback and adds nothing to the public surface. It fixes
`StatifierBlocks.Describe`: the outline now draws the interrupt edge for an
abandon handler that names `finish_as`, which 0.36.0 left out. Upgrading a
host: nothing needs migrating, and the package gains no dependency.

### Fixed

- `StatifierBlocks.Describe.outline/3` draws the interrupt edge to the group's end for an abandon handler that names `finish_as`, keying the edge on the handler's `outcome` select rather than on the name it finishes with.

## [0.36.0] 2026-09-26

0.36.0 is a minor, because it adds to the public surface and changes no
existing signature. A host gains two things. The block document's JSON
Schema (draft-07) now ships in the package, and `StatifierBlocks.Schema`
reads it, so a host can check a document with its own validator before it
reaches the package. `StatifierBlocks.Describe` describes a document in
words: a deterministic outline of its nodes and flow-graph edges, and one
sentence per node and per edge, which a host rewords through a phrasing
behaviour. The release also gives `core.sequence`, `core.group` and
`core.await` card lines of their own in place of their labels. Upgrading a
host: nothing needs migrating, the package gains no runtime dependency,
and a host that calls neither new module sees no change from 0.35.1 other
than those three card lines.

### Added

- `StatifierBlocks.Describe` describes a block document in words: `outline/3` answers one node per block and the flow-graph edges between them, read from the document's structure without compiling it, and `render/2` writes one deterministic English line per node and per edge, which a host rewords through the `StatifierBlocks.Describe.Phrasing` behaviour.

- The block document's JSON Schema (draft-07) ships in the package at `priv/schemas/block-document.schema.json`, with `StatifierBlocks.Schema.path/0` and `json/0` to read it: its root never refuses a document `StatifierBlocks.Document.from_json/1` accepts, and typed `config` and `slots` definitions for the `core.*` types sit under its `definitions/core`, which the root does not apply.

### Changed

- `core.sequence` declares `sentence/1`: its card line, and its name in `StatifierBlocks.Describe`'s lines, is "Run its steps in order" where it was the label "Sequence".
- `core.group` declares `sentence/1`: its card line, and its name in `StatifierBlocks.Describe`'s lines, is "Run interruptible steps" where it was the label "Group".
- `core.await` declares `sentence/1`: its card line, and its name in `StatifierBlocks.Describe`'s lines, is "Wait for <event>", followed by ", giving up after <timeout>" when a deadline is set, where it was the label "Wait for event".

## [0.35.1] 2026-09-25

0.35.1 is a patch, because it changes no public function, option or
callback and adds nothing to the public surface. It fixes how a card lays
out its title: a long title on a card that carries several controls no
longer breaks mid-word. Upgrading a host: nothing needs migrating, the
package gains no dependency, and the fix is in the shipped stylesheet
only.

### Fixed

- A card's title no longer breaks mid-word when the card carries several
  controls: the title column keeps a minimum of 6.5 times `--sb-text-md`, the
  hidden controls yield their width to it at rest, and on hover or selection
  they wrap inside the space left rather than squeezing the title.

## [0.35.0] 2026-09-24

0.35.0 is a minor, because it adds to the public surface and changes no
existing signature. A host can now check a whole document against the
palette it is about to mount before an author opens it:
`StatifierBlocks.Plan.expressible/3` answers `:ok`, or `{:no, reasons}`
with one reason per refusal, each naming the rule and the block, and
`StatifierBlocks.Plan.expressible?/3` answers the same question as a
boolean. The release also carries a host upgrade page for moving from 0.31
to 0.34 one minor at a time, and a guide for moving a document off the 0.27
vocabulary onto declared outcomes. Upgrading a host: nothing needs
migrating, the package gains no dependency, and a host that calls neither
new function sees no change from 0.34.0.

### Added

- `StatifierBlocks.Plan.expressible/3` checks a whole document against a
  palette before it reaches the editor: `:ok` when every block's type resolves,
  every block sits in a slot the editor would let an author drop it into
  (declared, with room, admitting its kinds), and every empty required slot is
  one the palette can fill; otherwise `{:no, reasons}` with one reason per
  refusal naming the rule and the block. A read type mismatch is not a reason,
  since the editor flags it rather than refusing the drop. An optional third
  argument is the assignability context, defaulting to `%{}` as
  `Edit.Targets.admits_at?/5` defaults it. `StatifierBlocks.Plan.expressible?/3`
  asks the same question with the same arguments and answers a boolean, `true`
  exactly when `expressible/3` answers `:ok`.

## [0.34.0] 2026-09-23

0.34.0 is a minor, because it adds to the public surface and changes no
existing signature. A host can now generate the migration plan between two
compiled revisions of one document: `StatifierBlocks.Migration.plan/2` maps
the old revision's states onto the new one's through their block ids and
answers the mapping as plain data, reporting every state it could not map.
The editor can show the host's publish line: a new `publish_status` assign
carries the count of live executions on the previous revision and the class
of the change, drawn beside the header. Upgrading a host: nothing needs
migrating, the package gains no dependency, and an editor mounted without
`publish_status` renders exactly what it rendered in 0.33.0.

### Added

- `StatifierBlocks.Migration.plan/2` maps the states of one compiled revision
  of a document onto another's through their block ids, roles included, and
  answers plain string-keyed data in the migration plan's `states`, `history`
  and `invocations` fields. Every old state with no counterpart - each state of
  a deleted block, and each final of an outcome a block no longer declares -
  is listed under `unmapped` and mapped nowhere. Two artifacts of different
  documents are refused with `{:error, :different_documents}`. It moves no
  execution and reads no timer.
- `StatifierBlocks.Editor` takes a `publish_status` assign, `nil` or
  `%{live: n, class: class}` with `class` one of the four classes
  `Statifier.Chart.diff/3` returns, and draws it as one line beside the
  `:header` slot, such as "3 live executions on the previous revision; this
  change is compatible". The host computes both values and the editor makes
  no query; `nil`, the default, renders exactly what the editor rendered
  before.

## [0.33.0] 2026-09-22

0.33.0 is a minor, because it adds to the public surface and changes no
existing signature. A document can declare the external events it accepts,
as a new `accepts` key on its envelope. Two new public modules serve a host's
publish step: `StatifierBlocks.Publish.findings/3` returns every finding the
host judges a document by before it compiles, and `StatifierBlocks.Graph`
judges a parent's compiled artifact against the children it names, and a
child's next revision against the parents that name it. Upgrading a host
that stores documents: `accepts` defaults to `[]`, a stored document decodes
unchanged and keeps its `content_hash/1`, and nothing needs migrating; a
document saved with a non-empty `accepts` list cannot be read by 0.32.0 or
earlier. Beside that, this release carries a minted-id collision finding
at the Resolve stage, a Document-stage refusal where the compile raised,
and clearer wording on the `:undeclared_slot` finding.

### Added

- A document can declare the external events it accepts, as an `accepts` list
  of event names on the envelope beside `datamodel`. `Document.validate/1`
  refuses a value that is not a list, an entry that is not a string, an empty
  name and a repeated name, each as a `{:malformed_envelope, {:accepts, _}}`
  reason. An empty or absent list encodes to the same bytes as before, so no
  existing document's `content_hash/1` changes; a reader from an earlier
  release refuses a document that declares one.
- `Compiler.compile/3` carries the document's `accepts` list, exactly as
  written, onto `%Compiled{}` and `%CompilationRecord{}` as a new `accepts`
  field. The compile reads nothing from it and the SCXML and chart identity do
  not change with it; whether each name is one the chart can take is for a
  host's publish step to check.
- The edit command `{:set_accepts, names}` replaces a document's whole
  `accepts` list, with the previous list as its inverse, and the editor's
  Declarations tab gains an accepted-events row where an author adds, renames,
  reorders and removes them.
- `StatifierBlocks.Declarations.add_accepted/1` appends a freshly named
  accepted event, `event_1` or the first `event_N` the list does not already
  hold, and `Declarations.put_accepted/3` writes the name at an index, or
  returns the list unchanged when no name sits there. `Declarations.refusal/1`
  phrases a refused `{:set_accepts, names}` as one sentence for the panel.
- `StatifierBlocks.Editor.Findings.anchor_tag/1` tags a finding anchored on
  the document `document`, with no id.
- `StatifierBlocks.Graph.check/2` and `StatifierBlocks.Graph.consumers_broken/2` return a `:warning` finding on a `core.map` block's `collect_type` field when that field names a type and the parent was compiled without `:datamodel`, saying the done-data keys read there are unchecked; each `interface` reference records the name in an `unresolved` field. Pass `:datamodel` to the compile, or write the `collect_type` inline, to have the keys checked.
- `Compiler.structure_findings/3` returns the findings the compile's Document,
  Resolve, Config and Structure stages refuse a document for, without emitting
  anything: exactly the list `Compiler.compile/3` refuses with when it refuses
  at one of those stages, and `[]` otherwise. It takes `compile/3`'s own
  options and never raises.
- `StatifierBlocks.Publish.findings/3` returns every finding a host's publish
  step judges a document by before it compiles, in the order the editor shows
  them: the structure findings above, then the editor's own findings for the
  same document and the same `:datamodel`, `:declare` and `:chart_outcomes`. A
  document the Document stage refuses comes back as that one finding. The
  host refuses the publish on an `:error`; an undeclared datamodel path stays
  an `:info` advisory.
- `StatifierBlocks.Finding` gains the anchor `:document` for a finding about
  the document rather than a block. `Finding.from_compiler/2` adapts the
  compile's Document-stage finding to it instead of refusing it as
  `:unanchorable`, and the editor lists it first on both findings surfaces,
  under `Document`, with nothing to select. `Shell.findings_groups/3`'s groups
  gain a `document?` key, and the inspector stamps `data-document` on each
  group.
- `Compiler.compile/3` records the document's parent/child interface on
  `%Compiled{}` as a new `interface` field: the outcomes and done-data keys its
  root block declares, and for each `core.subchart` or `core.map` block the
  document it names, the child outcomes it routes on and the child done-data
  keys it reads. The SCXML and chart identity do not change with it, and
  `%CompilationRecord{}` does not carry it.
- `StatifierBlocks.Graph.check/2` judges a parent's compiled artifact against
  each child it names, through a resolver the host supplies, and
  `StatifierBlocks.Graph.consumers_broken/2` judges a child's next revision
  against the parents that name it, for a host's publish step to refuse on. A
  child that is not published, an outcome the parent routes on that the child
  does not declare, and a done-data key the parent reads that the child does
  not declare are each an `:error` finding on the parent's referencing block.
  The reverse direction pairs each finding with the parent's document id.
- `StatifierBlocks.Finding` gains the source `:graph` for those findings.

### Changed

- `StatifierBlocks.Shell.drawer_view/1` takes the document's accepted events
  under an optional `accepts` key, and the Declarations tab's count adds them
  to the datamodel roots it counted before.
- A recipe's `insert/2` may return `{:set_accepts, names}` beside the blocks
  it inserts, as it already could `{:set_datamodel, entries}`:
  `Recipe.within_reach?/2` answers `true` for a list holding it, and
  `Edit.Targets.recipe_inserts/4` no longer refuses such a recipe as
  `{:recipe_out_of_reach, name}`. Every command that names a position is
  still held to the armed position and the block that encloses it.
- A block placed in a composite's pass-through slot, or in a declaring
  composite's `on_<name>` slot, whose id equals one the composite's expansion
  mints for its own members now draws a `:minted_id_collision` finding at the
  Resolve stage, against the composite block and naming the placed block;
  before, such a document was refused at the Chart stage with `:duplicate_id`
  findings when both blocks emitted a state, and a block in a pass-through
  slot could hide an `:outcome_not_raisable` finding. Give that block another
  id.

### Fixed

- `Compiler.compile/3` returns its Document-stage refusal for a document whose
  `root` is not a block, or whose `slots` value is not a map of block lists,
  where it raised.
- The `:undeclared_slot` finding message no longer calls a single stray block
  "they": at both the Structure and the Resolve stage it now says the slot's
  contents are dropped, and the Resolve message says to move them into a slot
  "this block type declares" instead of an ambiguous "it declares".

## [0.32.0] 2026-09-18

0.32.0 is a minor, because a public function's arity changes. A composite
block type may now implement an optional `declared_outcomes/1` callback and
have its outcomes read from each instance's own config, so one type can
stand for blocks that finish in different ways. Breaking:
`StatifierBlocks.Composite.unraisable_outcomes/4` is now
`unraisable_outcomes/5`, and a caller passes the config of the block it
expanded, between the type ref and the expansion members. Beside that, this
release carries fixes to the compiler's slot findings and their wording, to
a `capture` literal that is not valid UTF-8, to the refusals a handler that
finishes with `done` while it resumes reports, and to an editor card that
reflowed when it was selected.

### Added

- `StatifierBlocks.BlockType.finishing_outcome_name/2` answers the outcome a
  block in a declared slot finishes with: the name it carries under
  `finish_as` when that name is well formed, and the outcome its config
  declares at the given key otherwise.
- A composite block type may implement an optional `declared_outcomes/1`
  callback and have its outcomes read from each instance's own config, so one
  type can stand for blocks that finish in different ways.

### Changed

- The editor view model reads a `core.on_event` handler that names the
  outcome it finishes with under `finish_as` as that name; a handler naming
  none still reads as its `outcome` select value.
- Every reader of a composite's declared outcome list now reads the list of the
  block in hand. A composite declaring `outcomes:` statically, and one
  declaring nothing at all, behave exactly as before.
- **Breaking:** `StatifierBlocks.Composite.unraisable_outcomes/4` is now
  `unraisable_outcomes/5`, taking the block's config as its third argument;
  a caller passes the config of the block it expanded, between the type ref
  and the expansion members.

### Fixed

- A declared slot whose child count violates its declared arity now draws a
  `:slot_arity_violated` finding even when the composite carrying it declares
  no outcomes; it used to compile green with the declaration's refusal lost.
- Selecting an editor card no longer reflows it: the control strip reserves
  the widest set of controls the card can show, so the "Save as a step"
  control arriving on selection no longer narrows the title column or
  re-wraps the title.
- The compiler's slot findings no longer read "holds 1 blocks": the
  undeclared-slot and slot-arity messages now count in blocks or in one
  block, to agree with the number they report.
- A child placed in a slot a composite's type does not declare now draws an
  `:undeclared_slot` finding even when that composite declares no outcomes;
  it used to compile green and be dropped with nothing to read.
- `StatifierBlocks.Core.OnEvent.validate_config/1` answers `:ok` instead of
  raising `UnicodeConversionError` when a direct caller passes a `capture`
  literal that is not valid UTF-8; the encoding refusal stays with the
  compiler, which already refuses such a literal before the walk runs.
- A `core.on_event` handler that names a finishing outcome of `done` while
  it resumes its group now reports both of the refusals that apply, instead
  of only the first.

## [0.31.0] 2026-09-14

0.31.0 is a minor, because a core block type takes a new public key and
what a named handler compiles to changes. A `core.on_event` handler may
now name the outcome it finishes its group with, so an enclosing
composite can declare that outcome and route it through its derived
`on_<name>` slot (ADR-0002 C8). Beside that, a slot written on a
declaring composite under the name the compiler reserves for that
composite's expansion members is now reported instead of silently
dropping the children.

### Added

- `core.on_event` takes an optional `finish_as` key naming the outcome the
  handler finishes with when it abandons its group, so an enclosing composite
  can declare that outcome and route it through its derived `on_<name>` slot.

### Changed

- A slot written on a composite that declares outcomes, under the name the
  compiler keeps that composite's expansion members under, is reported as a
  compile finding instead of being replaced with its children silently
  dropped.

## [0.30.0] 2026-09-13

0.30.0 is a minor, because what a composite compiles to changes. A
composite that declares `outcomes` now compiles to a state of its own
with one `<final>` per declared name, and the enclosing body routes its
completion through a derived `on_<name>` slot per declared outcome
(ADR-0002 C6 and C7). Beside that, a `core.on_event` `capture` literal
carrying a raw control character is now refused at compile.

### Changed

- A `core.on_event` `capture` literal carrying a raw control character other
  than tab, line feed or carriage return is now refused at compile, on the
  `capture` key, because the value reaches an XML attribute raw and XML 1.0
  admits no such character there.
- A composite that declares `outcomes` now compiles to a state of its own with
  one `<final>` per declared name, so an enclosing body can route its
  completion, and it derives one `on_<name>` slot per declared outcome for the
  blocks that run when it finishes that way.
- A composite declaration whose `slots:` names a pass-through slot after one of
  its own declared outcomes is now refused, because the two kinds of slot
  cannot share a name; rename one of them.

## [0.29.0] 2026-09-13

0.29.0 is a minor, and its one consumer-visible requirement comes first:
**the `predicator` floor moves from `~> 9.0` to `~> 9.4.1`**. That is the
version whose string lexer reads a literal back whole, and it is what the
release's one behaviour change needs - a `core.on_event` capture pair's
literal source now accepts a string carrying any character, so the interim
restriction that kept a literal inside printable ASCII is lifted and a
handler may capture a label such as `"Café inscrit"`.

Beside that, this release is about an editor that tells the truth about
what it will accept. A literal capture pair draws a read-only row rather
than going missing from the form that cannot author it, and editing a
`core.on_event` block no longer drops one. A read-only mount withholds the
card controls it refuses and names the arm behind an empty slot. And two
composite-facing reports are corrected: a member the palette cannot
resolve no longer lends the composite a raisable outcome, and a summary
chip that is both translated and over the cap still carries its declared
event name.

### Added

- A literal capture pair - `["const", value]` - draws a read-only row in the
  editor's capture section, so a pair the two controls cannot author is
  visible rather than absent.

### Changed

- A `core.on_event` capture pair's literal source - `["const", value]` - now
  accepts a string carrying any character. The interim restriction that a
  literal string stay inside printable ASCII, tab, newline and carriage
  return is lifted: it was earned only while predicator 9.4.0's string lexer
  wrote a literal's codepoints back one byte at a time, and 9.4.1 fixed that
  lexer. A handler may now capture a label such as `"Café inscrit"` and the
  datamodel reads it back whole.

- Dependency floor: `predicator` moves from `~> 9.0` to `~> 9.4.1`. That is
  the version whose string lexer reads a literal back whole, which is what
  the lifted restriction above needs; 9.4.0 and below are excluded.

### Fixed

- A read-only editor mount no longer draws the card controls it refuses:
  "Replace with its steps" and Delete are withheld, the way the palette
  column and the gap "+" already were.

- A composite member whose block type the palette cannot resolve no longer
  contributes the default `done` outcome to the composite's raisable set, so a
  declared outcome name is no longer accepted on the strength of a missing
  palette entry.

- A summary chip that is both translated and over the presentation cap now
  carries its declared event name on the chip's `title`, rather than the
  clipped chip's own full translated text, so the generated event name is
  still on the card for a reader debugging against the compiled chart.

- Editing a `core.on_event` block no longer drops its literal capture pairs:
  a pair the form could not draw was previously replaced away by the next
  change the form posted.

- A read-only editor mount's empty slot now names the arm it draws the
  placeholder for, so a screen reader hears which arm is empty instead of
  hearing the slot header and nothing.

## [0.28.0] 2026-09-12

0.28.0 is a minor, and its two behaviour changes come first. **The compiled
chart of a document with a failure-classed final moves**: the reserved
`<donedata>` param the compiler mints on such a final is now named
`statifier_persistence:execution_status` rather than
`statifier_persistence:run_status`, so a durable host that reads that key
needs `statifier_persistence >= 0.12`, which is the floor for the new name.
And **a `core.on_event` capture pair whose source is absent from the event
payload no longer writes `:undefined`** - the destination is left unwritten,
so a document that captures by a path compiles to a different chart and a
stored compiled document has to be recompiled. A document with no
failure-classed final, and a handler that captures nothing or captures only
literals, compile byte for byte to what they compiled to before.

Beside those, this release is about what a capture and a composite may say
for themselves. A capture pair may take a literal source, written
`["const", value]`, so a handler records a value the document states rather
than one the event's payload has to carry. A composite may declare its own
`outcomes`, checked at Resolve against what its expansion can raise. And the
editor's `profile` assign takes a `run?` key that seats no run at all when it
is false, rather than merely hiding the run pane.

### Added

- A composite declaration may carry `outcomes`, a list of outcome names it
  declares for itself, on both the `use` form and the data form. Present, the
  list replaces the expansion root's derived outcomes - each name labelled by
  the member that raises it - and the compiler checks at Resolve that the
  expansion can raise every declared name, reporting an
  `:outcome_not_raisable` finding against the composite block when it cannot.
  Absent, nothing changes: a document that writes no `outcomes` key compiles
  to the same bytes as before.

- The editor's `profile` assign takes a `run?` key: `run?: false` mounts an
  editor that seats no run at all, so there is no run pane, no run marks on the
  canvas and no **Held here** column, whatever the host passes in `run` and
  `run_session`. It defaults to `true`, and it does not touch the marks a host
  paints itself through `active_marks` and `invoke_mark`.

- A `core.on_event` capture pair may take a literal source, written
  `["const", value]` in the block document, so a handler records a value the
  document states rather than one the firing event's payload has to carry; a
  string source is still the payload path it has always been, and the two are
  told apart by shape.

### Changed

- The compiled chart of a document with a failure-classed final changes: the
  reserved `<donedata>` param the compiler mints on such a final is now named
  `statifier_persistence:execution_status` instead of
  `statifier_persistence:run_status`. Upgrade a durable host to
  `statifier_persistence >= 0.12`, which is the floor for reading the new key;
  0.12 reads both keys for one release and 0.13.0 reads only the new one. A
  document with no failure-classed final compiles byte for byte to what it
  compiled to before. Both names stay refused for a `donedata_type/1`
  declaration while the old one is still read.

- A `core.on_event` capture pair whose source is absent from the event payload
  no longer writes `:undefined`; the destination is left unwritten, so "not
  answered" and "answered with nothing" are different values and a reader tests
  a captured path by asking whether it is there. A document that captures by a
  path compiles to a different chart - each such pair's `<assign>` is now
  wrapped in an `<if>` that tests the path - so recompile stored documents; a
  handler that captures nothing, and a pair whose source is a literal, compile
  byte-identically. A reader that tested a captured path for `:undefined`
  should test it for presence instead.

## [0.27.0] 2026-09-08

0.27.0 is a minor, and its two breaking changes come first. **The compiled
chart of every document holding an interruptible group moves**: a group's two
interrupt transitions and the `<raise>` of the interrupt pair inside its rail
now carry that group's own emitted state id, so a stored compiled chart or a
pinned chart identity for such a document must be re-captured. The spelling an
author configures and the pair a host block type raises do not change - the
salt is applied at emit, and a raise outside any rail is emitted unchanged.
And **`StatifierBlocks.Composite.expand/2` answers a tuple**,
`{:ok, {blocks, param_map}} | {:error, reason}` instead of the bare
`{blocks, param_map}` pair, and no longer raises; `expand!/2` is the raising
spelling the compiler's Resolve and the editor call, and a caller that wants
the old return and the old raise adds one character.

Beside those, this release is about **admission** and **the card face**.
Admission now resolves a composite's members through the palette it is asked
about - `Assignability.kinds/3`, `slot_accepts/4` and `admits?/4` take the
palette first - so a host-rooted composite is admitted on the kinds that host
declares and a drop is refused exactly where a compile would refuse it. Beside
the `Edit.Targets.accepted_types/4` sweep there is now a per-target fast path,
`admits_at?/5` and `accepted_types_at/5`: one probe and one check at the
slot's append gap, which is the call a "+" chooser at a gap should make.
`Assignability.assignable?/5` takes a `strict: true` that refuses a pair
either side of which resolves to `:unknown`.

On the card, the summary-chip presentation cap is 32 characters rather than
24, and a chip past it is drawn clipped with an ellipsis and its full text on
the chip's `title` instead of being dropped. A presentation-cap `:lint`
finding is read in the drawer's Findings tab rather than on the card face, and
a finding that does draw on a face is laid out inside the card's own box. The
card's controls moved into a reserved strip beside the title, so a title that
wrapped to two lines is never covered and nothing truncates; "Save as a step"
and its marking tray are no longer drawn on a mount that registered no
`on_collapse`. Two class contracts moved with those:
`.sb-node__chrome > .sb-node__strip > ...` for the controls, and
`.sb-node__chrome > .sb-finding` for a face finding.

For a host drawing its own surface, `Editor.ConfigForm.config_form/1` takes an
`event` attr defaulting to `"config-change"` and posts the block id as a
hidden input, so one call replaces a hand-written field pair, and
`Edit.Session.refusal/1` says what a refused gesture refused in one sentence -
the same sentence the editor now draws under its own toolbar.

### Added

- `StatifierBlocks.Composite.expand!/2` is the raising spelling of the
  expansion: it keeps the return and the broken-declaration raises
  `expand/2` had, and it is what the compiler's Resolve, the environment
  walk and this module's own derivations call.

- A data composite draws the summary chips its `use`-composite twin draws:
  one chip per param the config gives a value to, minus those declared
  `hidden?: true`.

- `StatifierBlocks.Edit.Session.refusal/1` says what is in a session's
  `last_error` as one sentence, so a host driving the same commit funnel
  draws the same words for the same refusal.

- `StatifierBlocks.Edit.Targets.admits_at?/5` answers whether one block type
  would be accepted at one `{parent_id, slot}` target, with one probe and one
  `StatifierBlocks.Assignability.check/5` at the slot's append gap instead of
  the whole-document walk `accepted_types/4` runs per candidate - the call a
  "+" chooser at a gap should make.
- `StatifierBlocks.Edit.Targets.accepted_types_at/5` asks that question over a
  candidate list, defaulting to the palette's own types, so a surface with a
  shortlist pays for the shortlist. `accepted_types/4` is unchanged and stays
  the sweep; the module's moduledoc says which of the three to call.

- `StatifierBlocks.BlockType.summary_refusal_message?/1` says whether a
  message is one `summary_refusal_message/4` wrote, which is how a surface
  tells a presentation-cap diagnostic from every other `:lint` finding.

- `StatifierBlocks.Assignability.assignable?/5` takes an options keyword whose
  `strict: true` refuses a pair either side of which resolves to `:unknown`,
  for the caller that must not admit a value nothing has typed. The host
  relation is not asked about an unknown side.
- The option defaults to `strict: false`, so `assignable?/3` and
  `assignable?/4` decide exactly what they decided before.

- `StatifierBlocks.Assignability.kinds/3`, `slot_accepts/4` and `admits?/4`
  take a palette as their first argument and resolve a composite's members
  through it, so a composite whose expansion is rooted at a host type is
  admitted on the kinds that host declares rather than on the `[:step]`
  fallback the core-only `kinds/2`, `slot_accepts/3` and `admits?/3` reach.
  The `c:StatifierBlocks.BlockType.io/1` callback is unchanged and gains no
  palette argument.

- `StatifierBlocks.Editor.ConfigForm.config_form/1` takes an `event` attr,
  defaulting to `"config-change"`, which is written to both `phx-change` and
  `phx-submit`, so a host can draw one block's fields under its own
  `handle_event/3` instead of hand-writing the field pair. The block the
  params are about still arrives as the hidden `block-id` input the form
  already posted.

### Changed

- The card's controls moved into a `.sb-node__strip` element inside
  `.sb-node__chrome`. A host stylesheet or test selecting
  `.sb-node__chrome > .sb-node__remove`, `> .sb-node__expand`,
  `> .sb-node__save-step`, `> .sb-node__fold` or `> .sb-node__offer` should
  select `.sb-node__chrome > .sb-node__strip > ...` instead. The classes, the
  `data-reveal` contract and the events are unchanged, and the strip is drawn
  on every card. Within it the controls are in left-to-right order, so a
  keyboard now reaches Save before Expand.
- `StatifierBlocks.Editor.Canvas.canvas/1`, `.Slot.slot/1` and
  `.BlockNode.block_node/1` take a `collapsible` attr, threaded the way
  `expandable` is; it is `false` by default and the editor passes whether its
  `on_collapse` assign is a one-arity function.

- **Breaking.** `StatifierBlocks.Composite.expand/2` now answers
  `{:ok, {blocks, param_map}} | {:error, reason}` instead of the bare
  `{blocks, param_map}` pair, and it no longer raises on a broken
  declaration. A caller that wants the old return and the old raise calls
  `expand!/2` and needs no other change; a caller that would rather be told
  than raised at matches on the tuple.

- A read-check `:type_mismatch` whose disagreeing writer is a block a
  composite minted for its own expansion now names the composite the author
  placed, by that composite's sentence, instead of naming an id the author
  cannot see, select or edit. A writer the author did place - including a
  block dropped into a composite's pass-through slot - is still named by its
  own id.
- The finding's reason tuple is unchanged: it still carries the minted id, so
  a fixture run and the Source tab still say which member of the expansion
  wrote the entry.

- `StatifierBlocks.Composite.expand/2` blames a member carrying more than one
  param's value on the first param in declaration order, instead of answering
  `nil` for it; a finding re-anchored onto the composite now names a field to
  open. A member carrying no param value still answers `nil`.

- The summary-chip presentation cap is 32 characters, up from 24. Every
  message that quotes the cap follows the number; `--sb-card-width` is
  unchanged, because the cap is a legibility number rather than a
  measurement of the card.
- A summary chip past the cap is now drawn clipped, with an ellipsis in its
  last position and its full text on the chip's `title`, instead of being
  dropped. A blank, multiline or non-string chip is still refused. The
  `:lint` finding that reports the length stays, and its sentence now ends
  "so it is drawn clipped".
- `StatifierBlocks.BlockType.badge/1` is unaffected: an over-long badge is
  still dropped rather than clipped.
- A presentation-cap `:lint` finding no longer draws on the card face; it is
  read in the drawer's Findings tab, and still counts toward a card's
  findings rollup. Every other finding a card carries draws where it did.
- A finding that does draw on a card face is laid out inside the card's own
  box. On a container it previously drew full-width between the card and the
  slot label below it, where it read as belonging to the slot. A host
  stylesheet targeting `.sb-node > .sb-finding` should target
  `.sb-node__chrome > .sb-finding` instead.

- The compiled chart of every document holding an interruptible group changes:
  a group's two interrupt transitions, and the `<raise>` of the interrupt pair
  inside its rail, now carry that group's own emitted state id
  (`statifier_blocks.interrupt.resume.<group state id>`), so a stored compiled
  chart or a pinned chart identity for such a document must be re-captured. The
  spelling an author configures and the pair a host block type raises do not
  change: the salt is applied at emit, and a raise outside any rail is emitted
  unchanged. A railed composite can now be nested inside a resumable group's
  body without the two rails contending for one event name.

- `StatifierBlocks.ViewModel.overlay_findings/2` enforces the findings shape
  `validate_config/1` declares: a block type answering with anything but a list
  of `{key, message}` string pairs is refused with an error naming that type,
  rather than failing a clause head that named the view model.

- Kind admission inside `check/5`, `valid_targets/4` and `validate/3`, and the
  editor's expansion check, now route through the palette-carrying arities, so
  a drop is refused exactly where a compile would be refused. A host-rooted
  composite that was wrongly admitted at a step slot, or wrongly refused at an
  interrupts slot, now gets the verdict the compiler gives it.

- `config_form/1`'s `target` attr is optional and defaults to `nil`, which
  renders no `phx-target` anywhere - the case of a host whose form posts to
  the LiveView it is mounted in. Every present caller passes a target and is
  unchanged.

### Fixed

- The assigns map an `:expression` field hands a host's `expression_component`
  carries a `debounce` key - the field's own `debounce` value, and `nil` when
  the caller named none - so an override can rate-limit the way every other
  control in the same form does instead of posting per keystroke. Overrides
  that ignore the key are unchanged.

- An `:expression` field served through the `expression_component` seam is
  handed the values the host offered for that field ahead of the document's
  declared paths - one de-duplicated `candidates` list - instead of the
  declared paths alone. A host that named values for the field now sees them
  in its own control, the way the plain input this package renders itself has
  always shown them; a host that named none sees the declared paths exactly as
  before.

- The "Save as a step" control and its marking tray are no longer drawn on a
  mount that registered no `on_collapse` callback, and the four events the
  gesture is made of are answered with the socket unchanged there. The
  gesture's only outcome is that callback, so a mount without one offered a
  marking step, a Save, and then silence. "Replace with its steps" is
  unaffected in both directions: it commits an edit the editor makes itself.
- The card's controls no longer draw on top of a title that wrapped to two
  lines. They sit in a reserved control strip beside the title - a grid
  column of its own, held whether or not the controls are revealed - so a
  long name wraps beside them and nothing truncates.

- The edit algebra's config gate refuses a `{:update_config, id, config}`
  whose value for a `{:type_expr, opts}` field is no arm that field admits,
  so the editor reports it where the edit is made rather than only at
  compile.

- An empty slot on a read-only editor mount now draws a non-interactive
  placeholder. The mark that says "nothing is here yet" was styled on the gap's
  "+" button, which a read-only mount does not draw, so an empty arm rendered as
  a slot header and nothing else.

- A read-only editor mount now refuses the Expand gesture. "Replace with its
  steps" reached the document and fired `on_change` on a mount whose profile
  said `read_only?: true`, which every other gesture that changes the document
  already refused.

- A gesture the editor refuses now draws the refusal as one sentence in the
  canvas column, under the toolbar. Every refusal already recorded its reason
  and nothing read it, so a gesture the editor refused looked on screen
  exactly like a gesture that did nothing.

- A composite's expansion is compiled at each member type's **current**
  version, so a config value a subtree writes reaches the chart as written
  rather than through that type's `migrate_config/2`. A subtree naming
  `core.send` or `core.wait` - the two core types past version 1 - used to
  have its `delay` or `duration` read as a stored value at version 1 and
  silently rewritten; such a value is now the author's to write in the
  accepted spelling, and one that is not is refused by name. A block the
  document stores is untouched: it still migrates.

- `StatifierBlocks.Composite.Collapse.replacement/4` gives the composite it
  inserts the collapsed arrangement's own block id instead of a freshly
  minted one, so the state ids of the chart a host gets back after committing
  the swap are the ones the document implies rather than a new UXID each time.

- `use StatifierBlocks.Composite` refuses an option it does not recognize,
  naming the key, instead of dropping it and compiling the composite with the
  default the declaration was trying to replace.

## [0.26.0] 2026-09-07

0.26.0 is about what a host does **around** a composite.
`StatifierBlocks.Composite.Collapse` reads a selected arrangement back as the
`Composite.Data` declaration that stands for it, and the editor's "Save as a
step" control hands that declaration to a new `on_collapse` callback - the
gesture edits no document and this package persists nothing, because which
table a saved step lives in, and whether it is saved at all, is the host's. A
composite may now declare **pass-through slots**, so an author puts children
into one instead of only filling in params, and `expand/2` splices them into
the mapped member's slot with their ids unchanged. A data declaration may
carry a `"migrations"` chain, so a block saved against an older revision of a
saved composite resolves instead of being refused. And all nine field types
now have a JSON spelling, so a declaration held as data reaches the same
field-type set a module composite declares.

Beside that, this release publishes what a host drawing its own surface over a
document was otherwise writing privately: `Composite.io/2` and `outcomes/2`
resolved through a palette you supply, a derived `summary/1`,
`Edit.Targets.accepted_recipes/4` and `recipe_inserts/4` beside the type half,
`Environment.with_writes/4`, `ViewModel.transparent?/2`, `effective_parent/3`,
`end_of_list_target/3` and `core_containers/0`, `ViewModel.overlay_findings/2`
over the session's own `draft_findings`, `ViewModel.order_palette_groups/2`, a
`variant: :inline` on `Editor.Field.field/1`, a `debounce` assign on the
editor mount, and `use StatifierBlocks.InvokeStep, failure_outcomes: [...]`.

It is a minor, and no compiled chart bytes move: a document holding a
composite with a pass-through slot compiles to the bytes of the same document
with that composite expanded in place, exactly as one with no slot does. Two
things answer differently from 0.25.0 for a host that does nothing:
`StatifierBlocks.Palette.new/2` now runs the duplicate-`order` refusal that
`from_modules/2` already ran, so a palette built by merging onto `core_types/0`
raises where it used to build; and a composite's `slots/1` answers its
declared pass-through slots rather than always `[]`.

### Added

- `StatifierBlocks.Environment.with_writes/5` is public: `env` with one
  block's own writes applied, member expansion included. The declarations
  argument carries a default, so `with_writes/4` is the spelling a caller
  writes.

- `StatifierBlocks.Editor.Field.field/1` takes a `variant` attribute
  (`:block`, the default and byte-identical to what it rendered before, or
  `:inline`), so a host placing one field inside a sentence of its own gets
  the same control, the same posted params and a label that is off the screen
  but still announced, without the editor gaining a layout mode.

- `core.on_event` answers `sentence/1` - "When card.authz_timed_out, abandon" -
  so a host list view draws the handler's own line instead of the palette
  label "On event".

- `StatifierBlocks.Edit.Targets.accepted_recipes/4` answers which of a palette's
  recipe names would land at a target, the recipe half of `accepted_types/4`, so
  a host drawing its own insert picker offers recipes beside block types.
- `StatifierBlocks.Edit.Targets.recipe_inserts/4` answers the command list that
  inserts a named recipe's arrangement at a target, or the refusal, without
  committing anything.

- `ViewModel.transparent?/2`, `effective_parent/3` and `end_of_list_target/3`
  answer where a row sits and where an append lands once a host has flattened
  its transparent containers, taking the transparent type names as an argument.
- `ViewModel.core_containers/0` names the three core containers a host most
  often flattens, as the documented default for those readers rather than a
  built-in policy.

- `StatifierBlocks.Edit.Session` carries a `draft_findings` map: the per-field
  findings each refused `change_config/3` was handed, keyed by block id.
- `StatifierBlocks.ViewModel.overlay_findings/2` routes those findings onto a
  node's form fields, with a finding naming no field landing in
  `form.unrouted` - the findings half of `overlay_draft/2`.

- `StatifierBlocks.Composite.io/2` and `outcomes/2` answer a composite block's
  io and outcomes with every member resolved through a palette you supply, so a
  composite rooted at a host block type is exact wherever a palette is in hand.
- A composite derives `summary/1`: one card chip per declared param that is not
  `hidden?: true`, drawn as `"<label>: <value>"` and overridable.
- `StatifierBlocks.BlockType.outcome_names/1` answers the names in an outcome
  list a caller already holds.

- `use StatifierBlocks.InvokeStep, failure_outcomes: [...]` declares a step
  family's failure class once at the `use` site, so a host whose steps all
  fail the same way no longer writes `failure_outcomes/1` on every member.

- `ViewModel.order_palette_groups/2` puts palette groups in a reading order a
  caller names, keeping the groups it did not name after them by name.

- `StatifierBlocks.Editor` takes a `debounce` assign and passes it to the
  inspector's config form, so a host that mounts the editor can say how often
  its controls post without composing `ConfigForm.config_form/1` itself.

- A `StatifierBlocks.Composite.Data` declaration may carry a `"migrations"` list: ordered `rename` / `drop` / `default` steps, each keyed by the `type_version` it migrates from. `migrate_config/3` walks every step at or above the stored version and below the declaration's `"version"` in one call, so a block saved against an older revision of a saved composite resolves instead of being refused.
- The whole chain is validated at `declaration/1`, entry-build time: a step of the wrong shape, a step carrying none of the three parts, a key named by both `"drop"` and `"default"`, a duplicate, out-of-order or gapped `"from"`, a chain not ending at `"version" - 1`, and a step naming a key the declaration's own params do not account for are each refused before the entry reaches a palette.

- A composite may declare **pass-through slots**: `use StatifierBlocks.Composite, slots: [%{name: ..., to: {local_id, inner_slot}}]` on a module composite and a declaration-level `"slots"` key on a `Composite.Data` row. The composite's card draws an interior for each declared slot, and `expand/2` splices the children the author put there into the mapped slot of the mapped member, with their ids unchanged.
- `StatifierBlocks.Composite.pass_through/2` answers a composite block's declared slots resolved to the minted id of the member each maps into.
- `StatifierBlocks.Composite.mapping_errors/2` answers the three refusals a pass-through mapping can earn against its own subtree: an unknown local id, an inner slot the subtree does not write, and a mapped inner slot the subtree also fills. A module composite raises them at its first expansion; a data composite answers them from `declaration/1`.

- `StatifierBlocks.Composite.Collapse.propose/3` reads a selected arrangement back as the `Composite.Data` declaration that stands for it, without naming it - naming the type stays the host's act.
- `StatifierBlocks.Composite.Collapse.replacement/4` answers the compound that swaps an arrangement for a composite of the name a host registered; the host commits it, and nothing in the package does.
- The editor's "Save as a step" control on the selected card, with a marking tray for the values the saved step should ask for, handing the proposal to a new `on_collapse` callback. The gesture edits no document and the package persists nothing.
- All nine field types now have a JSON spelling in a `Composite.Data` declaration: `select`, `path`, `list` and `type_expr` are the type's name plus an optional `"options"` key.

### Changed

- A surface drawing a refused config draft reads the findings from the session
  instead of calling `validate_config/1` again to re-derive them.

- `StatifierBlocks.Assignability.produces/4` and the editor's `core.on_event`
  event candidates read a composite's io and outcomes through the palette they
  already hold rather than through the core-only fallback. The
  `c:StatifierBlocks.BlockType.io/1` and `outcomes/1` callbacks are unchanged
  and keep that fallback.

- `StatifierBlocks.Palette.new/2` refuses two entries of one palette-browser
  group that declare the same `order`, raising the `ArgumentError` naming both
  entries that `from_modules/2` already raised, so a palette built by merging
  onto `core_types/0` meets the same refusal; the fix is to renumber one of the
  two entries, and the moduledoc's "Ordering a group" states the convention.
- Both builders now admit one pair at a single `order`: a composite's `types`
  entry beside that same composite's own derived `<Module>.Recipe`, whose
  `palette_entry/0` is the block type's. Registering both stays the host's
  choice to show two entries; `from_modules/2` used to refuse it.

- The palette column draws its groups in the order a profile's
  `palette_groups` list gives them, rather than by name; a mount that names no
  list draws them by name as before.

- A block stored below a declaration's earliest migration step still answers `{:error, {:no_migration_from, from}}`, and so does every stored version of a declaration that writes no `"migrations"` key - the previous behaviour, unchanged. A module composite is untouched: `use StatifierBlocks.Composite` gains no `migrations:` option and still writes `migrate_config/2` itself.

- A composite's `slots/1` answers its declared pass-through slots instead of always `[]`, and `io/1`'s `slot_accepts` answers each declared slot at the mapped inner slot's own accepted kinds.
- The environment walk descends a declared pass-through slot's children at the mapped inner position - the environment they would have seen had the expansion been placed by hand - and reaches nothing through a slot key the declaration does not declare. A read that fails inside such a child is reported against the child, not against the composite.

- The editor's config form draws `StatifierBlocks.ViewModel.shown_fields/1`'s
  list, so a host surface drawing its own form reads the same filter the
  package's own form reads rather than a second copy of it.

### Fixed

- The editor's drop-check preview no longer under-reports a member mismatch
  the drop would introduce. It applied a candidate's writes without the
  member expansion the walk runs, so a read of `record.member` was answered
  with an advisory before the drop and an error the moment it landed. The
  preview and the walk are now one codepath, and they agree.

- A read-only editor mount no longer draws the "+" button on the gaps between
  blocks; the gaps themselves stay, so an empty arm still reads as one.

- The editor's Expand gesture refuses a composite whose declaration is too broken to expand - an empty or non-block `subtree/1`, a duplicated, `blk_`-prefixed or `__`-minting local id, or a pass-through slot mapped at something the subtree does not hold - naming the declaration error, instead of raising out of the author's LiveView. The compiler already answered the same case with a `:composite_expansion_failed` finding.
## [0.25.0] 2026-09-07

0.25.0 is about **composites**: a block type derived from params plus a pure
subtree, so an arrangement a host writes over and over becomes one thing an
author puts down and one thing they read back. `use StatifierBlocks.Composite`
declares one from a module, and `StatifierBlocks.Composite.Data` derives one
from a JSON-shaped declaration, so a host whose *users* save block types can
register a block type held as data - which is what a `{module, state}` palette
entry is for. The compiler replaces a composite with its expansion at the
Resolve stage, so a document holding one compiles to bytes identical to the
same document with that composite expanded in place, and the editor's
"Replace with its steps" performs exactly that expansion as one compound edit
a single undo puts back.

Beside that, this release publishes what a host drawing its own surface over a
document was otherwise writing privately: `StatifierBlocks.ViewModel`'s
readers, `StatifierBlocks.Edit.Session` as a commit funnel with no socket in
it, `StatifierBlocks.Edit.Targets.accepted_types/4`, `Recipe.members/2` with a
delete path that offers the whole arrangement, a `selected_id` a host may
write, `field_candidates` honoured on path and expression fields, and a
`debounce` attr on the config form.

It is a minor, and no compiled chart bytes move. Two things answer differently
from 0.24.0 for a host that does nothing: `StatifierBlocks.Palette.from_modules/2`
now refuses a palette whose browser group holds two entries declaring the same
`order`, which is a mount-time raise for a host that carried a duplicate and
built at 0.24.0; and `StatifierBlocks.Palette.manifest/1` now answers `nil` as
a type's version where it used to raise.

### Added

- `StatifierBlocks.ViewModel` answers the readers a host writing its own surface
  over a document would otherwise write privately: `find_node/2`, `parent_of/2`,
  `positions/1`, `sentence/1`, `shown_fields/1`, `fields_for/2`,
  `overlay_draft/2` and `drafted_field/2`.
- `StatifierBlocks.Document.committed_config/2` and `effective_config/2` and
  `/3` answer what the document holds for a block, and what an unaccepted draft
  says instead.

- The editor's `field-list-add` and `field-list-remove` events carry
  `block-id` beside `key`, so a host handling them knows which block the
  gesture came from.

- A palette entry may now be `{module, state}` beside a bare module, so a host
  whose *users* save block types can register a declaration held as data.
  `StatifierBlocks.Palette.call/4` is the one seam every callback on an entry
  goes through: it prepends the state, asks about the arity a stateful module
  actually exports, and answers a caller-supplied default when the entry
  declares nothing. `StatifierBlocks.Palette.declares?/3` answers declaredness
  alone for the two presentation branches that need it rather than a value.
- `StatifierBlocks.Composite.Data` derives a composite block type from a
  JSON-shaped declaration - params, a subtree template with one `"$param"`
  placeholder arm and one `"$literal"` escape, and an optional sentence and
  palette entry - through the same `StatifierBlocks.Composite.expand/2` a
  `use StatifierBlocks.Composite` module goes through. A data composite and
  the module composite of the same shape expand to the same blocks and compile
  to the same bytes. `Composite.Data.declaration/1` refuses a malformed
  declaration before it reaches a palette, which is the last moment a callback
  can still be pure and total.

- `StatifierBlocks.Recipe` gains an optional `members/2` callback: asked about
  a block, a recipe answers the ids of the arrangement that block belongs to,
  or `[]`. A recipe that does not implement it is unchanged.
- The core `"deadline"` recipe implements `members/2`, recognising its
  `core.send` and `core.on_event` pair structurally - a hand-built pair is
  claimed, a pair renamed apart is not, and no block outside the enclosing
  group is ever named.

- `selected_id` is a documented input assign. A host that draws a selection
  surface of its own - an outline pane, a plan view - moves the editor's
  selection by passing the id through `send_update/3`, and hears the result
  back on `on_select` like any other selection. It is honoured only on an
  update that carries it, so a re-render the host made for a reason of its own
  leaves the author's selection where it was, and an id the open document does
  not hold clears the selection rather than naming a block that is not there.
  A selection is not a document edit: no command, nothing serialized, and
  nothing on the undo stack.

- The editor replaces a selected composite block with the blocks it stands
  for. The control sits on the composite's own card, reads "Replace with its
  steps", and commits the removal and the expansion's inserts as one compound
  edit: one undo puts the composite back whole, and the document it writes is
  the one `StatifierBlocks.Composite.expand/2` answers, so the chart compiles
  to the same bytes on both sides of the gesture.
- The gesture is refused, and nothing is written, when the slot the composite
  sits in will not admit the blocks that come out of it.

- `StatifierBlocks.Edit.Session` is the commit funnel as a value: `commit/2`,
  `change_config/3`, `step/2`, `update_list/4` and `apply_gesture/2` over a
  document, a history and the drafts, with no socket in them. Each answers
  `{:ok, session}` or `{:error, session}`, and the tag says whether the
  document moved - which is when a host stores or notifies. Holding a refused
  config as that block's draft, and dropping every draft across an undo, are
  the package's decisions rather than each surface's.
- `StatifierBlocks.Edit.Targets.accepted_types/4` answers which of a palette's
  block types fit a `{parent_id, slot}` target, and `probe/2` builds the block
  it asks about: `Palette.new_block/2`'s, with the type's `palette_entry/0`
  `default_config` merged over it. A surface filtering a palette without that
  merge answers differently, for any type whose read depends on its config,
  than the canvas's own drag stamp does.
- `StatifierBlocks.Assignability.context/1` builds the assignability context
  from a host's `:datamodel`, so the drop check, the environment walk and a
  datamodel view are handed the same one.

- `ConfigForm.config_form/1` and `Field.field/1` take a `debounce` attr,
  written as `phx-debounce` onto every control the form draws, so a host
  that persists what the form posts can ask for something slower than one
  write per keystroke. It takes what LiveView takes - milliseconds, or
  `:blur` - and defaults to no attribute, which is what every existing
  caller already renders. Controls drawn by an `expression_component`
  override are that component's own and are not covered.

- A `:composite_expansion_failed` finding at the `:resolve` stage reports a
  composite whose declaration cannot be expanded, in place of the raise.

- A host's `field_candidates` list is honoured on `{:path, opts}` and
  `:expression` fields as well as `:string` ones: either spelling draws a
  `<datalist>` the input is bound to, ahead of the declared datamodel paths,
  and the value stays typed by the control.

- `use StatifierBlocks.Composite` declares a block type from params plus a
  pure `subtree/1`, deriving `config_schema/1`, `slots/1`, `io/1`,
  `outcomes/1`, `current_version/0`, `sentence/1`, `palette_entry/0` and a
  raising `emit/2`, and leaving only `sentence/1`, `palette_entry/0` and
  `validate_config/1` overridable.
- `StatifierBlocks.Composite.expand/2` answers the blocks a composite stands
  for, with ids minted deterministically from the composite block's own id,
  together with the param each expanded block is blamed on.
- A composite's declaration derives a `StatifierBlocks.Recipe` at
  `<Module>.Recipe` whose `insert/2` puts down one composite block.
- `StatifierBlocks.Environment.read_signatures/3` and `write_signatures/3`
  answer a composite with the union of its expansion's reads and writes, taken
  at the composite's one position in the document.

### Changed

- `StatifierBlocks.Palette.fetch/2` and `resolve/2` answer the entry **as
  stored**: neither normalizes a bare module into a pair nor unwraps a pair
  into its module, so a host that registered no stateful entry can be handed
  none and its existing `{:ok, module}` matches still match. `manifest/1`,
  `new_block/2` and `from_modules/2` read a stateful entry through the seam,
  including `from_modules/2`'s duplicate-`order` check - a stateful entry
  collides with a bare one at the same order and is refused, rather than
  silently dropping out of the check.
- `StatifierBlocks.Composite.expand/2` and `composite?/1` take a palette entry
  where they took a module. The existing module-only calls are unchanged.

- Deleting a block a recipe claims now offers to remove the whole arrangement:
  the card's delete control counts what the next click removes and a Keep sits
  beside it. Taking the offer commits one compound, so one undo puts the
  arrangement back whole. Deleting a block no recipe claims is unchanged.

- The compiler replaces a composite block with its expansion at the Resolve
  stage, so a document holding a composite compiles to bytes identical to the
  same document with that composite expanded in place.
- A finding raised inside an expansion is reported against the composite
  block, carrying the key of the param that produced it or no key at all, and
  the provenance map still owns every span by the expanded block that emitted
  it.

- `nil` is refused as the `default:` of a `hidden?: true` field for every
  field type, not only `{:type_expr, opts}`.

- **Breaking for a palette with a duplicate `order`.** `StatifierBlocks.Palette.from_modules/2`
  now raises `ArgumentError` when two entries of one palette-browser group
  declare the same `order`, naming both. A host whose palette carried such a
  pair built at 0.24.0 and does not build here: renumber one of the two. The
  check skips an entry whose module is not loaded, exports no
  `palette_entry/0`, or declares no `order`.
- `StatifierBlocks.Palette.manifest/1` answers `nil` as the version of a type
  whose module cannot be loaded or exports no `current_version/0`, where it
  used to raise, because it now reads every entry through
  `StatifierBlocks.Palette.call/4`.

### Fixed

- `core.invoke` and the core `deadline` recipe no longer both declare
  `order: 7`, so the Structure group of a palette browser has a stable
  order; `Palette.from_modules/2` refuses two entries of one group that
  declare the same `order`, naming both.

### Note

- `StatifierBlocks.Composite.Data` fixes no migration key, so bumping a
  declaration's `"version"` with stored blocks refuses them through ADR-0007's
  injected `migrate_config/2`, unchanged and deliberately not papered over.

## [0.24.0] 2026-09-07

0.24.0 is about drawing less of the editor, and saying more about what is
drawn. `StatifierBlocks.Editor` takes an optional `profile` assign naming
which drawer tabs, inspector tabs, palette groups and toolbar chips a mount
draws, with `read_only?: true` rendering a document that offers no way to
change it. A config field declaration gains `hidden?` and `readonly?`, so a
type can hold a value no form shows, or draw one as text beside its label.
Beside that: block types may declare `sentence/1`, one line of prose for a
config, which `StatifierBlocks.ViewModel.Node` carries beside the author's
title; `StatifierBlocks.ViewModel.outline/1` answers the whole document in
reading order as `{node, depth, kind}`; `StatifierBlocks.Palette.new_block/2`
builds a block from a palette entry, so a host view no longer reimplements
what the editor's insert does; and the Datamodel tab draws the values each
declared path enumerates.

It is a minor, and no compiled chart bytes move. Two changes can refuse a
document or a block type that was accepted at 0.23.0: a config field
declared without a `default:` key is now refused at compile whatever its
field type, where before only a `{:path, opts}` field was; and the typed
environment now puts an entry at every member beneath a record, shape or
inline-shape write, so a nested read that previously met the
nothing-is-known advisory is now checked against the written type.

### Added

- A config field declaration may carry `hidden?: true`, which keeps the field
  out of every form while the compiler, the Source tab and `validate_config/1`
  see its value entirely unchanged.
- A config field declaration may carry `readonly?: true`, which renders the
  field as its value beside its label rather than as an input.
- `StatifierBlocks.ViewModel.Field` carries both flags, so a host drawing its
  own surface filters on the same two booleans the package's own config form
  reads.

- `StatifierBlocks.Editor` takes an optional `profile` assign naming which of
  its surfaces a mount draws: `%{drawer_tabs:, inspector_tabs:, palette_groups:,
  toolbar:, read_only?:}`, every key optional and every list either `:all` or
  the ids it names. A mount that passes no profile renders exactly what it
  rendered before, and there is no arrangement of the map, `%{}` included, that
  removes a surface a host did not name.
- An id a profile lists that the package cannot resolve is dropped and the
  mount renders; a key whose value is neither a list nor `:all` resolves to
  that key's default. There is no `validate_profile/1` and a profile is never
  checked against the shell's ids at declaration.
- `profile: %{read_only?: true}` renders the document without offering any way
  to change it: no palette column, no drag hook on the canvas, config fields
  and declaration rows drawn as values rather than controls, Undo and Redo
  hidden rather than disabled, and every gesture that would reach the document
  answered with the socket unchanged, so `on_change` never fires. Selection,
  `on_select` and every findings surface are unchanged, and a document is never
  refused for being read-only.
- `StatifierBlocks.Shell.drawer_tabs/1` and
  `StatifierBlocks.Shell.inspector_tabs/1` answer the package's tabs a profile
  leaves, in the shell's own order; `StatifierBlocks.Shell.drawer_view/1` takes
  an optional `:profile` key and filters the host's contributed tabs by the
  same list.
- `docs/profiles.md` is the host-facing guide: the default, the ids each list
  draws from, the drop rule, a minimal mount and a read-only one.

- The editor's Datamodel tab draws a **Values** column beside each declared
  path: the `one_of` enumeration the ADR-0006 document declares there, cut at
  eight values with the remainder counted ("+3 more"). It reports the
  declaration and not a host's `value_candidates` override, so a reader can
  answer "where did this picklist come from" from the table rather than by
  opening a condition. A path that declares no enumeration draws an empty
  cell.

- A write signature whose type is a record, a shape, or an inline shape now
  puts an entry in the environment at every member beneath the path as well as
  at the path itself, recursively and to any depth, so a later block reading a
  nested path is checked rather than given the nothing-is-known advisory.

- Block types may declare an optional `sentence/1` callback, which answers the
  block as one line of prose for a given config. It is read through
  `StatifierBlocks.BlockType.sentence/2`, which is total: a type that declares
  none, and one whose callback raises, throws, exits or answers a non-string, a
  blank string or a multiline one, all read as the type's label. Unlike a chip
  it carries no length cap.
- `use StatifierBlocks.BlockType` injects an overridable `sentence/1` answering
  the type's own palette label, so a type that overrides nothing is
  indistinguishable from one that declares no `sentence/1` at all.
- `StatifierBlocks.ViewModel.Node` carries `sentence`: the type's own sentence
  where it declares a usable one, else the author's `title`, else the type's
  label falling back to the type name. A block whose type the palette cannot
  resolve carries the type name, so a list view never draws a blank line.
- `StatifierBlocks.ViewModel.outline/1` returns the document in reading order:
  one `{node, depth, kind}` per block, pre-order, `kind` in
  `:step | :arm | :rail | :tray`. It is pure and reads only the view model.
  Every block appears exactly once - arms, rails, trays and the drafts shelf
  are kinds and positions, never omissions - and `depth` is block nesting
  depth, so a slot never consumes a level.
- `core.wait`, `core.branch`, `core.subchart`, `core.foreach`, `core.parallel`,
  `core.send` and `core.assign` declare sentences of their own. Every other
  `core.*` type answers its label, which is what it answered before.

Cards are unchanged: `sentence` is not a chip, is never capped, and nothing new
is drawn on a block's card or in the palette browser.

- `StatifierBlocks.Palette.new_block/2` builds a block of a named type from a
  palette - the type's `config_schema/1` defaults as the config and its
  `current_version/0` as the stored version - so a host view that inserts from
  a palette no longer has to reimplement what the editor's insert does.

### Changed

- A config field declared without a `default:` key is now refused at compile
  whatever its field type; before, only a `{:path, opts}` field was. A block
  type that omitted the key on another type must add it - the declaration was
  never well formed, and the value it produced was `nil`.
- A `hidden?: true` field whose `default:` is its type's empty value is refused
  at compile: it can carry nothing and no form can ever give it a value. A
  hidden `:boolean` defaulting to `false` is legal, `false` being a decided
  value rather than an absence.
- A form ignores any posted value for a `hidden?` or `readonly?` field, so a
  crafted payload cannot reach a key the form withheld.

- A document may stop validating where a block reads a nested path the record
  written above it types differently; the fix is either the reading block's
  `expects` or the written record.

## [0.23.0] 2026-09-06

0.23.0 makes a block type's typed surface something an author writes rather
than only names. `StatifierBlocks.BlockType`'s field-type set gains
`{:type_expr, opts}`: a field holding the name of a type the datamodel
document declares, an inline unnamed shape written member by member, or
nothing at all, with one shared check behind it and an inspector control that
draws both arms. `core.map`'s `collect_type` and `core.on_event`'s `payload`
are the first two fields to take it, and the `statifier_datamodel`
requirement moves to `~> 0.4`, which is where a type expression admits an
inline shape. Beside that: the typed environment now **seeds** the declared
path types the datamodel document carries, so a read at a declared path meets
the declared type rather than an advisory; a compile refusal carries the
Config and Structure stages' findings together, so a mis-typed field on one
card no longer hides an unsatisfied read on another; a root block type
declares a typed child summary that a `child_use` compile emits as `<final>`
params; drop-time typing fires from a palette entry's new `default_config`
rather than from `config_schema/1`'s defaults alone; a `:type_mismatch`
finding carries the `config_key` of the field whose read declared the path;
and `StatifierBlocks.Palette.manifest/1` gives a host one assertion to pin
its palette with.

It is a minor, and compiled bytes move for one kind of document: one compiled
under `child_use: true` whose **root** block type declares a
`donedata_type/1`, whose top-level `<final>` elements now carry one `<param>`
per declared field after the two the compiler mints. Every other document
compiles to the bytes it compiled to at 0.22.0, and each entry below that
could have moved one says so in its own words. Two changes are worth reading
before upgrading, neither of them a byte: a stored document that validated
may refuse once its host supplies a datamodel, because the environment now
seeds declared path types and a read that found nothing before now meets a
declared one; and a host that compiles with `terminate:`, `child_use:`,
`known_invoke_types:` or `datamodel:` must pass its own `compile_options` to
the editor, or the Source tab, the fixture verdicts and the Run pane's marks
are each about a chart nobody ran.

### Added

- A palette entry may declare `default_config`, the config the editor's insert
  probe is built with over `config_schema/1`'s own defaults, so a block type
  whose read is declared on a path field says which path it will read before
  anyone has configured one.

- `StatifierBlocks.BlockType`'s closed field-type set gains a ninth member,
  `{:type_expr, opts}`: a field holding the name of a type the datamodel
  document declares, or an inline unnamed shape as a list of
  `"name"` / `"type"` / `"required?"` objects, or nothing at all. `opts`
  carries `arms:` (which of the two the field admits, both by default) and
  `allow_empty?:` (whether an empty value is admitted, true by default).
- `StatifierBlocks.BlockType.type_expr_findings/2` is the one shared check
  behind that field type, consulted by both the compiler's `:config` stage
  and the editor's view model: a value that is not an arm the declaration
  admits is a `:config` finding carrying the field's key, and an undeclared
  type name is not one.
- The editor draws a `{:type_expr, opts}` field in the inspector's Config
  tab: a text input bound to a `<datalist>` of the document's declared type
  names for the name arm, an ordered member-list form for the inline arm
  whose member types are the same control recursing, and a toggle when the
  field admits both. Switching arms replaces the value rather than
  translating it, and a value the control cannot read is drawn raw with its
  finding beneath it.
- `StatifierBlocks.Environment`'s type expression admits an inline unnamed
  shape, `{:shape, members}`, and `StatifierBlocks.Environment.inline_shape/1`
  reads a stored member list into one. The read check reaches it through
  `StatifierDatamodel.Types.satisfies/3` unchanged - this package defines no
  second one.

- `StatifierBlocks.Environment`'s context accepts `:skip_blocks`, a set of
  block ids whose declared writes the walk leaves out. Absent, as it is for
  every editor query, nothing is skipped.

- `StatifierBlocks.Palette.manifest/1` returns a palette as a sorted list of
  `{name, version}` entries - a block type beside its `current_version/0`, a
  recipe beside `:recipe` - so a host pins what its palette carries in one
  assertion that names the entry which moved instead of a count that does not.

- A worked example of the failure propagation 0.22.0 introduced, which the
  0.22.0 Notes stated as a rule without showing a run. A `core.sequence` body
  holds a `myapp:authorize` invoke followed by a `myapp:capture` invoke, and
  authorize ends on its `error` outcome. With the authorize invoke's own
  `on_error` slot filled - handling is declared by the failing block, never by
  the sequence around it - the slot's child runs, the invoke ends on `error`,
  the sequence advances, and the run ends at the document's ordinary completion
  final with capture having run. With `on_error` empty and the document
  compiled under `child_use: true` or `terminate: true`, the invoke's `error`
  final is emitted anyway, the root's transition on
  `done.outcome.<authorize state id>.error` is selected before the sequence's
  own `done.state`, and the run ends at the shared top-level failed final
  carrying the reserved `statifier_persistence:run_status` param with the value
  `failed` - capture never runs. Compiled under neither option there is no root
  catch, nothing selects the outcome event, and the sequence advances to
  capture exactly as it does on success. ADR-0002's amendment of 2026-09-06,
  section 4 ("The nested-to-root propagation rule") and section 6 ("What this
  costs a host, and what it does not"), carry the full walk-through.

- `StatifierBlocks.BlockType` declares an optional thirteenth callback,
  `donedata_type/1`: what a document's `<donedata>` carries when it is
  compiled for use as a child, as a list of `%{name, path, type}` fields
  typed in `statifier_datamodel`'s vocabulary.
  `StatifierBlocks.BlockType.donedata_type/2` is the resolver every consumer
  reads it through, and it is total - a type that does not export the
  callback, or exports one returning something else, declares nothing.
- Compiling with `child_use: true`, each top-level `<final>` now carries one
  `<param name="<name>" expr="<path>"/>` per entry the **root** block type
  declares, in declaration order, **after** the `outcome` param and the
  reserved `statifier_persistence:run_status` param. A root type that
  declares nothing compiles to exactly the bytes it compiled to before, and
  a `terminate` compile emits no declared field at all.
- A declared field name that collides with either of the two names the
  compiler mints, or that is not a bare lowercase identifier, is refused at
  compile with an `:invalid_donedata_field` Emit finding against the root
  block, rather than silently shadowing a param.
- `core.map` gains an optional seventh config field, `collect_type`: the type
  name a collected answer's `"donedata"` carries, read through
  `StatifierDatamodel.Types.parse/2` against the parent document's
  declarations. It is a type rather than a path, it produces no bytes, and it
  has no findings of its own. An absent or empty one means what every stored
  document means today.
- `StatifierBlocks.BlockType.agrees?/3` answers whether a child's declaration
  covers what a parent's `collect_type` expects of it, as
  `StatifierDatamodel.Types`' own read check and in that package's own reason
  vocabulary. The check is **dormant**: it is not a compile finding and
  changes no compiled byte, and it answers only where both documents are in
  hand.

### Changed

- The environment now **seeds** the declared path types the datamodel
  document carries: before the walk begins it holds an entry at every path
  the document declares, at the type it declares there, and a type a block
  writes replaces it from that position on. A read at a declared path that
  found nothing before, and was an `:info` advisory, now meets the declared
  type - so a stored document that validated may refuse once its host
  supplies a datamodel, and the fix is either the block's declaration or the
  document's. A caller that supplies no datamodel is unaffected in every
  particular.
- A seeded entry's writer is `:declaration`, which
  `StatifierBlocks.Assignability`'s `:type_mismatch` reports as its upstream
  ref. Such a refusal carries no `{:fixable_by, block_id}` reason - there is
  no block whose declaration an author would change - and its message says
  the datamodel document declares the type rather than naming a block.
- `core.map`'s `collect` writes `{:list, <the ADR-0009 envelope>}` rather
  than `{:list, :unknown}`: one collected element is a shape of `index`,
  `status`, an optional `donedata` typed by the block's own `collect_type`,
  and an optional `failure`. A block after a `core.map` learns this whether
  or not anything was declared, and no compiled byte moves.
- The `statifier_datamodel` requirement rises to `~> 0.4`, which is where a
  type expression admits an inline shape. Raising the floor alone also
  widens what a document projects: an entry whose `type` names a declaration
  contributes that declaration's fields as declared paths beneath it.

- `core.map`'s `collect_type` and `core.on_event`'s `payload` are
  `{:type_expr, opts}` fields admitting both arms, rather than `:string`
  fields carrying a type name. A stored string is the name arm: the same
  bytes, the same resolution, the same findings, and no compiled byte moves.
- `core.map`'s `collect_type` may now be written inline - a list of
  `"name"` / `"type"` / `"required?"` objects - and the inline shape becomes
  the `"donedata"` member of the type `collect` writes, one level inside the
  collected element's envelope.
- `core.on_event`'s `payload` may now be written inline, and the refusal it
  buys reads that arm the same way: a `capture` pair whose source path names
  a member the inline payload does not carry is the same `:config` finding on
  the `capture` key, walking below the first segment through a member typed by
  a declared name or by a nested inline shape.
- `StatifierBlocks.BlockType.agrees?/3` reads an inline `collect_type` as the
  shape it writes rather than as nothing. The check stays dormant in every
  other respect.
- `core.on_event`'s `validate_config/1` no longer reports a finding of its own
  for `payload`. What a value of a `{:type_expr, opts}` field may be is
  `StatifierBlocks.BlockType.type_expr_findings/2`'s one check, which the
  compiler's `:config` stage and the editor both already consult, so bytes that
  are neither arm are refused once rather than twice on the same key.

- `compile_options` is required of any host that compiles with `terminate:`,
  `child_use:`, `known_invoke_types:` or `datamodel:`. Leaving it unset is not
  an error and never was: the editor compiles a different chart than the host
  does, and a run against that chart is silently unmarked, the Source tab
  lists a chart nobody runs, and the fixture verdicts are about neither. Pass
  the host's own option list.
- `StatifierBlocks.SourceView.build/3` and
  `StatifierBlocks.Runtime.FixtureRuns.run/4` forward their whole `opts` to
  the compiler rather than `:declare` alone, less the one key each keeps for
  itself (`:previous` and `:view_model`).

- A compile refusal now carries the findings of the **Config and Structure
  stages together**, instead of the first of the two that failed. A mis-typed
  field on one card no longer hides an unsatisfied read on another, so an
  author sees both in one refusal rather than one per round trip. Refusal
  semantics are unchanged: such a document still does not compile, and every
  stage after Structure still stops the pipeline at its first failure.
- A block whose config the Config stage refused is skipped by id in the
  Structure stage: it reports no structure finding of its own, and its
  declared writes leave no entry in the typed environment, because a write
  signature is read off the config that was refused. The walk continues past
  it - its siblings and its children are checked exactly as before.

- The palette an armed "+" opens now filters recipes as well as block types. A
  recipe whose arrangement cannot land at the armed position - `insert/2`
  refuses it there, or the commands it answers with reach outside ADR-0005
  clause 3C's bound - is absent from the list rather than offered and then
  refused at the click, which wrote nothing and said nothing. The pick still
  runs both checks, so a stale pick is refused exactly as before.

### Fixed

- Dragging or picking a block type whose read is declared on a config field now
  greys the slots that would refuse it. The probe carried only the schema's
  defaults, and a path field defaulting to `""` names no path, so such a type
  declared no read at insert time and every slot accepted it.

- A block type that declares a datamodel-path field without a `default:` key is
  now refused at compile, as a `:config` finding naming the field, instead of
  raising a `FunctionClauseError` out of the view model when something rendered
  the block. The view model reads a declaration's `default:` permissively, so a
  field declared without one renders with no default rather than crashing the
  build.

- The editor's Source tab and its fixture runs now compile with the host's
  `compile_options`, as the provenance recompile already did. All three
  compiles pass one option list, so the listing, the fixture verdicts and the
  Run pane's marks are all about the chart the host actually compiled.

- A **root** block declaring an outcome named `failed` is now refused at
  compile with a `:config` finding naming the field the name was written in,
  rather than reaching `Statifier` as a duplicate state id reported against
  the package. The name is the one the shared final for an unhandled failure
  below the root already mints; rename the outcome. A block below the root is
  unaffected, and no other document's compiled bytes move.

- The editor toolbar's `Fit active` now works over a run the editor seated
  itself: with nothing selected it fits and reveals the innermost block the
  run's marks name, where before it was enabled only by the host's own
  `active_marks` list.

- A `:type_mismatch` compiler finding now carries the `config_key` of the field
  whose read signature declared the path, so an editor can put a refused read
  on the control the author has to change. Two path fields reading the same
  path were indistinguishable in a finding, which named the path only.

## [0.22.0] 2026-09-06

0.22.0 makes failure a first-class outcome across the block vocabulary, and
widens what a document can declare. `core.invoke` now declares the two
outcomes it has always emitted, `done` and `error`, and classes `error` as a
failure; every host type built on `StatifierBlocks.InvokeStep` classes its
own `error` the same way by default, and opts out with `failure_outcomes/1`
returning `[]`. `core.invoke`, `core.map` and `core.subchart` emit their
`error` outcome's `<final>` whether or not the failure slot is occupied, and
a failure-classed outcome a block below the document root leaves unhandled
now reaches the document's own ending, through one shared top-level `<final>`
carrying the reserved `statifier_persistence:run_status` donedata param.
Beside that, `core.on_event` takes an optional `payload` declaration naming a
type the datamodel document declares, and a `capture` pair reading a member
that payload does not carry is refused at compile; `core.map`'s `collect`
accepts any datamodel path rather than only a bare identifier; and the editor
takes a `compile_options` assign, so the recompile behind the Run pane's
marks resolves against the chart the run is a run of.

It is a minor, and a minor with notes. Compiled bytes move for three kinds of
document: one containing a `core.invoke`, `core.map` or `core.subchart` whose
failure slot is empty; one with a failure-classed outcome left unhandled
below its root, compiled under `child_use: true` or `terminate: true`; and
one containing a host type built on `InvokeStep`. Each of those is a new
chart revision. Every other document compiles to the bytes it compiled to at
0.21.0. ADR-0002's amendment of 2026-09-06, section 6, works through what
this costs a host and what it does not. The `statifier_datamodel` floor also
moves to `~> 0.3`; a host on 0.1 or 0.2 updates it with the rest of the
dependency tree.

### Added

- `core.on_event` takes an optional `payload` config key: the name of a type
  the datamodel document declares, saying what `_event.data` carries for the
  event that handler names. With one declared, a `capture` pair whose source
  path reads a member the payload does not carry is refused at compile - one
  `:config` finding on the `capture` key, naming the pairs and the payload -
  so the interpreter's unbound marker is never written for a captured path on
  a typed document. A handler with no `payload`, a `payload` naming a type the
  document does not declare, and a compile with no `:datamodel` are all
  unchanged: no new finding, and the same compiled bytes. `payload` itself
  emits nothing.

- The editor takes a `compile_options` assign: the option list the host
  compiled the open document with, forwarded to the recompile behind the Run
  pane's marks so a run of a `terminate: true` chart resolves against the
  chart it is a run of.

- A failure-classed outcome a block below the document root leaves unhandled
  now reaches the document's own ending: under `child_use: true` or
  `terminate: true` the compiler emits one shared top-level `<final>`
  carrying the reserved `statifier_persistence:run_status` donedata param,
  and one transition into it per unhandled pair.
- `core.invoke` declares the two outcomes it has always emitted, `done` and
  `error`, so a parent may wire `done.outcome.<state id>.error` and the
  editor offers both on the outcome-event candidate list.

### Changed

- `core.map`'s `collect` accepts any datamodel path, not only a bare lowercase
  identifier, so a fan-out can assemble its answers at `cards.batch` and not
  only at `batch`. All four fields this package writes an
  `<assign location="...">` from now read one grammar and one refusal wording.

- `core.invoke` classes its `error` outcome as a failure, and so does every
  host type built on `use StatifierBlocks.InvokeStep`. A host whose `error`
  is routine defines `failure_outcomes/1` returning `[]` beside its
  `outcomes/1` to keep the old classing.
- `core.invoke`, `core.map` and `core.subchart` emit their `error` outcome's
  `<final>` whether or not the failure slot is occupied; with the slot empty
  the failure transition targets that final directly instead of being
  selected by nothing. A document containing one of the three with an empty
  failure slot, one with an unhandled failure below its root under the two
  compile options, or a host `InvokeStep` type, compiles to different bytes
  and so is a new chart revision; every other document is byte-identical.

- The `statifier_datamodel` floor moves to `~> 0.3`, so a datamodel entry
  whose `type` names a declaration contributes the declaration's fields as
  declared paths beneath its own: they are offered as expression candidates,
  they carry their declared types in the Datamodel tab, and a block that
  writes one gets no undeclared-path advisory, exactly as an inlined `object`
  entry's `fields` do. A host on `statifier_datamodel` 0.1 or 0.2 updates it
  with the rest of the dependency tree.
- The Datamodel tab renders an entry typed by a declaration as the name it
  names, rather than raising on the `{:declared, name}` that release added.

## [0.21.0] 2026-09-06

0.21.0 makes the editor a debugger and widens what a block can say. The
drawer gains a Source tab - the compiled SCXML with every run of bytes
traced back to the block that emitted it, and a click on a config-emitted
span opening the Config tab on the field it came from - and the canvas
takes a seat in a run pane, with statifier-ui's status, scrubber and event
log around it and a send control that pushes a block type's fixture events
straight into a live session. `core.branch` declares a third slot,
`undecided`, for an arm whose condition cannot be decided (ADR-0012); a
block type may class one of its outcomes as a failure, and the compiler
stamps a reserved `<donedata>` param on that outcome's final so a durable
stepper can tell a chart that finished badly from one that merely finished
(ADR-0008's accepted amendment); a host can state a rule about a whole
document through `validate_document/1` and the palette's new `:validators`
list; `core.invoke`'s and `StatifierBlocks.InvokeStep`'s `assign_to` take
any datamodel path and declare the path they write; and `core.map` names
what a child run sees its item and its position under, through `item_as`
and `index_as`.

It is a minor, and nothing stored is invalidated by it: a `core.branch`
that leaves `undecided` empty, and every type that classes no outcome as a
failure, compile to the bytes they compiled to at 0.20.0. `statifier_ui`
is now an optional dependency at `~> 0.9`, the release that added the
state-id reads `StatifierBlocks.Runtime.Marks.from_trace/2` composes;
`statifier_datamodel` stays at `~> 0.1`.

### Added

- `core.branch` declares a third slot, `undecided`, labelled "Cannot be decided". A branch that puts children in it emits one extra transition, after every arm and before `otherwise`, taken when an arm's condition could not be decided - a comparison predicator answers with its undefined sentinel rather than `true` or `false`, such as a path missing under a bound datamodel root, or operands whose types do not match.

- The drawer has a Source tab: the compiled SCXML as numbered lines, with the block that emitted each run of bytes, and a click on any run selecting that block.
- `StatifierBlocks.SourceView` builds that listing from a document and a palette, without the editor.

- The editor takes a `run_session` - a `Statifier.Session.server()` - and the Run pane draws a send control over it: one button per event in the selected block's type's fixture sample, sending straight into the session through statifier-ui's `EventInjection`.
- The send control is enabled only over a live stream with a session supplied; over a persisted run, or with no session, every button renders disabled with a one-line note, and a send never writes to the document.

- `StatifierBlocks.Runtime.Marks.from_trace/2` turns a statifier-ui trace read model and a provenance map into the run marks the editor canvas already accepts, so a host observing a run marks the blocks a configuration is inside without naming them itself.

- A block type may class one of the outcomes it declares as a **failure** through the new optional `failure_outcomes/1` callback, and the compiler emits a reserved `<donedata>` param - `statifier_persistence:run_status` with the value `failed` - on that outcome's top-level `<final>` under both the `:child_use` and the `:terminate` compile options, so a durable stepper can tell that a chart finished badly rather than merely finished. `core.map` and `core.subchart` class their `error` outcome; every other type classes nothing.

- A host can state a rule about a whole document, not just about one block's config: `StatifierBlocks.DocumentValidator` is the behaviour, `validate_document/1` its one callback, and a module implementing it goes in the palette's new `validators` list (`StatifierBlocks.Palette.new/2`'s `:validators` option, defaulting to `[]`).
- A validator says where and what - `{anchor, message}` or `{anchor, message, severity: ...}` on decision 11's existing anchors - and the package stamps the source `:lint` and defaults the severity to `:warning`. The findings render in the editor's Findings drawer tab and count toward `StatifierBlocks.Editor.findings_count/3` like every other finding.

- `core.map` declares `item_as` and `index_as` - the names a child run sees its item and its position under, defaulting to `item` and to no position name - and carries both into its `<invoke>` beside the list's path, so a child recipe reads what the author named rather than whatever the fan-out handler chose.

- Clicking a config-emitted span in the Source tab opens the Config tab and focuses the field it came from; a span with no owning field still selects the block as before.

- The editor takes a `run` - statifier-ui's `StatifierUI.Live.State`, live or persisted - and seats the canvas in a run pane: statifier-ui's status and scrubber above it, its event log below, and no Mermaid diagram. Scrubbing or clicking a log entry moves the canvas's run marks, and clicking a log entry also selects the block whose state handled that step.
- While a run is seated the Datamodel drawer tab's "what is known here" table shows what the run held at each path, beside the type the position declares.
- `StatifierBlocks.Runtime.Handled.block/3` answers which block's state handled one macrostep of a run, and `StatifierBlocks.Runtime.RunValues.at/1` what a run held at its selection; both are pure and neither needs statifier-ui to be present.
- `StatifierBlocks.Runtime.Selection.scrub/2` and `select/2` move a run's selection, which is what the pane's two events do.

### Changed

- A `core.branch` that leaves `undecided` empty compiles to exactly the bytes it compiled to at 0.20.0, so every stored document is unaffected until its author wires the slot.

- `statifier_ui` is now an optional dependency at `~> 0.9`, the release that added the state-id reads `from_trace/2` composes; hosts on `0.8` upgrade the package to use the new module and are otherwise unaffected.

- A document whose root block is a `core.map` or a `core.subchart`, compiled with `child_use: true` or `terminate: true`, gains the reserved param on its `error` final. Its content hash changes with it, so it is a different chart revision under statifier-ex ADR-0052 - the same one-time choice opting into `terminate` already is. Every other document compiles to the bytes it compiled to.

- A palette entry's `singleton` declaration is now derived through the same document-rule path a validator's findings take, and runs first among them. Its findings are unchanged: still `:config`, still `:error`, still anchored at the root.

- A `core.map` compiled before this release gains an `item_as` param carrying the default name; a stored config that never had the key still validates and needs no migration.

- `core.invoke`'s `assign_to` is declared as a datamodel path field, so the editor offers the host's declared paths as candidates on it and a path it does not declare draws the usual advisory rather than a refusal.
- `core.invoke`'s and `StatifierBlocks.InvokeStep`'s `assign_to` accept any datamodel path, dotted or not, in place of a bare lowercase identifier - the rule `core.assign` and `core.subchart` already accept for the same `<assign>` element. `core.map`'s `collect` is unchanged.

### Fixed

- A path `core.invoke` writes is visible to the environment: it was emitted without being declared, so nothing downstream saw the write.

## [0.20.0] 2026-09-06

0.20.0 is about types. The datamodel document's declarations become
something the whole package reads: `StatifierBlocks.Environment` carries the
path-to-type map at any position in a document with one pre-order walk,
every `core.*` block type declares what it reads and writes at datamodel
paths, and the data-flow check is defined over that environment rather than
over the block immediately before this one. The path/type index itself moved
out to the `statifier_datamodel` package, taken from Hex at `~> 0.1`; the
drawer's Datamodel tab answers what is known at the selected block and lists
the declared records and shapes; and the condition editor is handed the
declared kinds, so a clause on a path declared `integer` offers the numeric
operators.

It is a minor with two breaking edges to read before upgrading.
`StatifierBlocks.Predicates.Datamodel` is gone: its functions are
`StatifierDatamodel.Index`'s, under the same names, so a caller swaps the
module name and nothing else. And a type expression spelled exactly
`unknown` now reads as the permissive `:unknown` rather than as an opaque
expression that happens to be spelled that way - it only ever admits, so
nothing that passed before is refused now. The signature changes under
Changed keep their arities; `{:type_mismatch, ...}` gains a sixth member
naming the datamodel path the read was checked at.

### Added

- The kinds a datamodel document declares reach the condition editor. With a
  document supplied, `StatifierBlocks.Datamodel.path_types/1` projects it to
  the expression language's own value kinds and the editor hands that map to
  statifier-ui's expression editor, so a clause on a path declared `integer`
  offers the numeric operators and one on a `date` path the date set, rather
  than whichever set its current source happens to imply. It draws a control
  and decides nothing: the operator the source carries is still offered, the
  value in it is still kept, a disagreement renders as an advisory beside the
  clause, and a path the document does not declare renders exactly as it did.
  A host that supplies its own `expression_component` is handed the same map.

- A `:type_mismatch` finding names a declared type by its **label**. A pair
  of names the datamodel document's `types` key declares reads as the two
  human names an author recognises; a pair of opaque spellings a host
  carries reads exactly as it did before there were declarations. The rule
  is one function, `StatifierBlocks.Environment.type_label/2`, so the
  finding and the editor cannot disagree about what a type is called.
- The drawer's Datamodel tab answers **what is known here**: the paths the
  environment holds at the selected block's position, with their types, as
  computed by the pre-order walk at that exact position - before the block's
  own writes land. Nothing selected and nothing known are two different
  states and the panel says which it is.
- The same tab lists the **declared records and shapes**, each with its
  ordered fields, their types and their required marks, through
  `StatifierBlocks.Datamodel.declared_types/1`. An author told that a record
  does not cover a shape can now read what that shape requires without
  leaving the editor.

- Write signatures across the `core.*` vocabulary, so the environment
  survives one. `core.assign` writes its `path` and `core.subchart` its
  `assign_to` as known-but-untyped; `core.map` writes `collect` as
  `{:list, :unknown}`, so the block after a fan-out knows it is looking at a
  list; `core.on_event` writes one path per `capture` pair on the interrupt
  path; `core.wait`, `core.send`, `core.raise` and `core.await` write nothing
  and leave the environment exactly as it reached them; and a container hands
  its children's writes out through the per-path merge.

- `StatifierBlocks.Environment` - the datamodel path to type map at any
  position in a document, carried by one pre-order walk. `at/3` answers it,
  `subject_path/2` names the path a document's subject lives at,
  `read_signatures/3` and `write_signatures/3` say what a block declares
  there, and `type_of/2` and `satisfies/3` read a declaration's spelling
  against the datamodel document's own type declarations.
- `palette_entry/0` gains an optional `subject` key: the datamodel path a
  document's subject lives at, read from the document's entry block.
- `StatifierBlocks.Assignability.context/0` gains an optional `:datamodel`
  key, and `StatifierBlocks.Compiler.compile/3` threads its existing
  `:datamodel` option into it, so the read check can consult the document's
  `record` and `shape` declarations.
- The reason vocabulary gains `{:shape_not_satisfied, missing}`: the
  environment holds a record at the path, the read expects a shape, and
  `missing` names the required fields the record does not cover.

- A `{:path, opts}` field declares what it reads and writes at its path:
  `expects: T` is a read signature the environment at the block's position
  must satisfy, and `writes: T` puts `T` there for every block after it.
  Both keys are optional, and a field carrying neither behaves exactly as it
  did - a write of `:unknown`, known but untyped, that refuses nothing.
- `field_candidates`, the values a host offers for one field, keyed
  `{type_name, field_key}`. A `:string` field with a closed list -
  `[{value, label}]` - renders a `<select>`; an open one -
  `{:open, [{value, label}]}` - renders the text input with a `<datalist>`;
  no list renders the input the field already had. It draws a control and
  decides nothing: `validate_config/1` is still the only authority on a
  value, and a stored value a closed list does not offer is drawn as its own
  option rather than silently rewritten. It is an editor assign and a
  `StatifierBlocks.Compiler.compile/3` option, where a value outside a
  closed list is a **warning** on the compiled artifact and never an error.
- `core.on_event`'s `capture` has an authoring surface: a repeated
  two-control row, one row per pair - the datamodel path written beside the
  path read inside the firing event's payload - with the source control
  offered the block type's own `fixtures/0` payload for the configured
  event. There is always one blank row at the end, which is what adds a
  pair; clearing both controls of a row removes one. The key was authored
  through the document before this and had no control at all.
- A capture's target paths reach the declared-path advisory, anchored on the
  `capture` key. They are datamodel paths that no field declaration names,
  so the pass covering every other datamodel path could not see them.

### Changed

- The optional `statifier_ui` dependency's floor moves to `~> 0.8`, which is
  the release that takes `:path_types`. The dependency is optional in the same
  sense it always was: a tree without it renders the plain source input, and
  nothing adds it for you.

- `statifier_datamodel` is a Hex dependency at `~> 0.1` rather than a pinned
  git revision, so a host resolves this package's dependencies from Hex alone.

- `StatifierBlocks.Datamodel` reads the datamodel document through
  `statifier_datamodel` rather than through an index of its own.
  `declared_paths/1`, `candidates/3`, `candidates_under/2`,
  `value_candidates/2` and `declared_view/3` keep their signatures and their
  behaviour; what changed is where the projection lives.
- The declared type set gains `date` alongside the eight it already carried,
  because the re-homed record widened it. A document that used no `date`
  entry is unaffected.

- **Breaking.** The data-flow check is no longer a question about the block
  before this one. A block declares what it reads and writes at datamodel
  paths, and `check/5`, `valid_targets/4`, `validate/3`, `inbound_type/4` and
  `seam_reason/4` are defined over the environment at a position. They keep
  their arities. `io/1`'s `consumes` and `produces` keep their meaning as
  sugar: `consumes` is a read at the document's subject path and `produces`
  is a write there. A palette that declares no `subject` on its entry block's
  palette entry has no subject path, so that sugar declares nothing and the
  document validates exactly as an untyped one always did.
- **Breaking.** `{:type_mismatch, block_id, ref, held, expected}` gains a
  sixth member, the datamodel path the read was checked at. A block may carry
  several read signatures on several paths, so a message that says which two
  types disagreed without saying where is one an author cannot act on.
  `{:kind_not_admitted, ...}` is unchanged.
- **Breaking.** A type expression spelled exactly `"unknown"` now reads as
  the permissive `:unknown` rather than as an opaque expression that happens
  to be spelled that way. It only ever admits: an opaque `"unknown"` compared
  by identity was already satisfied against another `"unknown"`, so nothing
  that passed before is refused now.
- The refusal a `{:type_mismatch, ...}` names is the block whose write
  signature put the type at the path, found by name rather than by adjacency.
  A refusal at index 0 of a slot that used to answer `:not_assignable` -
  because there was no previous sibling to name - now answers
  `{:fixable_by, block_id}` when a block upstream of the container did
  declare the type.
- The read check itself is `StatifierDatamodel.Types.satisfies/3`: unknown,
  then identity, then a record covering a shape's required set. The palette's
  host relation still runs, and now runs **last**, after that coverage step -
  so the floor a host cannot lower is higher than it was, and a host that was
  widening records into shapes by hand can delete that half of its module.
- A `core.branch`'s arms and a `core.parallel`'s lanes no longer blank
  everything downstream. What leaves a container is the per-path merge: a
  path every arm holds at one type keeps it, and only a path the arms
  disagree about drops to `:unknown`.

- `core.subchart`'s `assign_to` accepts a **datamodel path**, not only a
  bare lowercase identifier: any non-empty string with no whitespace in it,
  which is exactly what `core.assign` accepts for the path it writes. The
  validation and the emission widen together. It is a widening and nothing
  else - a bare identifier is a one-segment path, so every document that
  validated before still validates and compiles to the same bytes - and it
  settles the field offering dotted candidates its own validation refused.
  The identical refusal on an `<assign>` location elsewhere in the
  vocabulary is untouched.

### Removed

- `StatifierBlocks.Predicates.Datamodel` is gone. The datamodel document's
  path/type index moved to the `statifier_datamodel` package as
  `StatifierDatamodel.Index`, which carries the same functions under the same
  names (`index/1`, `declared_paths/1`, `sensitive_paths/1`, `datamodel/1`,
  `entries/1`, `fetch/2`, `type/2`, `declared?/2`, `under/2`); a caller
  swaps the module name and nothing else.

### Fixed

- The editor's drop check consults the datamodel document the editor already
  holds, instead of asking the data-flow question with an empty context. The
  coverage step - a record satisfying a shape by covering its required set -
  could not run there, so a placement the compiler accepts was drawn as
  refused. `StatifierBlocks.Edit.Targets.droppable_slots/3`,
  `droppable_slots_for/3` and `slot_verdicts/3` each take an optional
  context as a fourth argument; called with three, every one of them behaves
  exactly as it did.
## [0.19.0] 2026-09-05

0.19.0 is about what a chart does with the world outside it. `core.map`
(ADR-0009) runs another chart once per item of a datamodel list and waits
for the whole batch, compiling to exactly one `<invoke>` whose bytes do not
scale with the list; `core.await` holds until a named event arrives, with an
optional cancel-scoped deadline; and `core.on_event` gains a `capture` map
that writes values out of the firing event's payload into the datamodel. The
editor learns the datamodel's shape alongside them - a `{:path, opts}` field
type with declared-path candidates, event-name candidates read out of the
enclosing body, and a `chart_outcomes` assign the host fills so a subchart's
declared outcomes can be checked against the chart's own. The drawer's tab
strip answers the WAI-ARIA arrow keys.

### Added

- `core.on_event` takes an optional `capture` map: each pair writes one value
  out of the firing event's payload into the datamodel, the key naming the
  datamodel path written and the value the path inside `_event.data` it is
  read from. The pairs compile to one `<assign>` each on the handler's own
  transition, ahead of the raise that carries the outcome, ordered by their
  datamodel paths so a document compiles to one byte sequence. A handler
  without the key, or with an empty map, compiles exactly as it did before
  the key existed. A source path the payload does not carry is written as the
  interpreter's explicit unbound marker rather than `nil`, so a consumer of a
  captured path tests for that marker. The key has no editor control yet -
  the field-type set has no member describing a map - so it is authored
  through the document.

- `{:path, opts}` is the eighth value in a block type's closed field-type set.
  A field declared with it holds a path into the host's datamodel, and the
  editor reaches its control by the type alone: a text input bound to a
  `<datalist>` of the declared paths, and the `:info` advisory for a path the
  datamodel does not declare, anchored on the field. `opts` carries no defined
  key today, so a declaration writes `type: {:path, %{}}`. It suggests and
  never constrains - free text stays valid and `validate_config/1` remains the
  only authority on what a value may be.

- The editor offers value candidates on `core.on_event`'s `event` field: the
  completion events the blocks in the handler's enclosing body raise, written
  as the generated `done.outcome.<state id>.<outcome>` names the compiler
  mints and labelled by each block's own card label and outcome. The body is
  read through the enclosing type's `slot_accepts` declaration - a slot that
  admits ADR-0003's `:step` kind - so a host group is offered on the same
  terms a `core.group` is, and only blocks that implement `outcomes/1`
  contribute. The field is still a plain `:string`: the list is a
  `<datalist>` that suggests and never constrains, a free-typed event name
  validates exactly as it did, and a body with nothing to offer draws the
  plain input rather than an empty picker.

- `core.map`, the durable fan-out block type (ADR-0009): it runs another
  chart once per item of a datamodel list, all at once, and waits for the
  whole batch. The palette calls it "For every item, run a chart" - it sits
  beside `core.foreach`, which runs the blocks inside it one item at a time,
  and is not a mode of it. Four fields: `items` (the datamodel path holding
  the list), `chart` (the document id run for each item), `collect` (where
  the assembled answer is written, optional), and `on` (`all`, the default,
  or `first_error`). `items` and `collect` are `{:path, opts}` fields, so
  the editor offers the host's declared datamodel paths on both; `collect`
  accepts what `core.subchart`'s `assign_to` accepts, refused with the same
  wording. Any `on` outside the two permitted values is a config finding at
  authoring time, which is what reserves the word `quorum` for its own walk.
- It compiles to exactly **one** `<invoke>` of the constant type
  `"statifier_blocks:map"`, carrying the block's own id, the document id as
  `src`, and the four values as literal `<param>` children. The compiled
  bytes do not scale with the length of the list and cannot: the list is a
  runtime value the compiler never sees, so the params carry the *path* and
  the host's registered handler is what resolves it and starts the runs.
  Nothing about the size of a batch is validated here - a bound on it, if
  one is needed, is the fan-out runtime's to enforce and to refuse against.
  The type is deliberately a different string from
  `"statifier_blocks:subchart"`: a host that wired a single-child handler
  has not thereby wired a fan-out handler, and
  `StatifierBlocks.Compiler.InvokeTypes` reports that gap at deploy time.
- Two fixed outcomes, `done` and `error`, each with an optional slot
  (`on_done`, `on_error`), because N children report N outcomes and there
  is no branch target to be had by joining them - the per-child answers go
  to `collect` instead, and an author who wants to branch on them reads
  that list with a `core.branch` after the block. The collected answer is
  written once, on the success transition, in the shape `core.subchart`
  already uses.
- The core vocabulary is now seventeen types, and `core.map` is registered
  in `StatifierBlocks.Palette.core_types/0` beside the rest.

- `core.await` joins the core vocabulary: an in-flow leaf that holds until a
  named event arrives, with an optional deadline. It is valid wherever
  `core.wait` is, takes `event` (required) and `timeout` (an optional
  duration), and declares two outcomes, `received` and `timed_out`, so a
  parent can route the two ways it can end. A configured `timeout` arms a
  cancel-scoped delayed send, so leaving the await early - because the event
  arrived, because an interrupt fired, because a group was abandoned - leaves
  no timer behind. Both outcomes are declared whether or not a deadline is
  stored; with no `timeout`, no timer is armed and no `timed_out` final is
  emitted.

- A `chart_outcomes` editor assign, `%{document id => [outcome]}`: what the
  host says each of the documents it compiles with `:child_use` finishes
  with. A `core.subchart` names a chart by document id and declares its
  outcomes by hand - a block type cannot read the document it references - so
  the assign is where that knowledge enters. Two things read it: the
  `outcomes` field offers the named chart's finals as a `<datalist>`, and
  `StatifierBlocks.ViewModel.outcome_findings/3` reports a disagreement
  between that list and what the block declares, anchored on the `outcomes`
  key and also reachable through `StatifierBlocks.Editor.findings_count/3`'s
  new `:chart_outcomes` option.
- The disagreement is a `:warning`, never an error: the document compiles
  either way, and what a mismatch costs is a conditioned `done.invoke`
  transition that can never match. A chart the map does not name produces
  nothing at all - unknown is not disagreement - and so does an entry holding
  an empty list, so `%{}` (the default) leaves the editor drawing exactly
  what it drew before. The field is still a plain `:string`: the list
  suggests and never constrains, and a free-typed name validates as it did.

- The drawer's tab strip answers the WAI-ARIA tablist arrow keys. With the
  strip's one Tab stop focused, Left and Right move one tab and wrap, Home and
  End go to the ends, and the tab moved to is both selected and focused - so
  a tab past the clipped edge at the narrow breakpoint is reachable from the
  keyboard and is scrolled into view when it is reached. Host-contributed tabs
  are walked alongside the package's own, in the order the strip draws them.
  The movement is server-side and adds no JavaScript hook: the strip reports
  the key through `phx-keydown` and the editor picks the neighbour out of the
  same tab list a pointer pick reads.

### Changed

- `core.subchart`'s `assign_to` is declared `{:path, %{}}` rather than
  `:string`, so it offers the declared datamodel paths as candidates and gets
  the undeclared-path advisory. What it accepts is unchanged - a bare
  lowercase identifier, which is a one-segment path - and it compiles to the
  same `<assign location=...>` it always did.
- `StatifierBlocks.BlockType.datamodel_path?/1` answers `true` for a
  `{:path, opts}` field by construction. The `datamodel_path?: true` key is
  not withdrawn: it keeps its meaning, its control and its advisory, a
  declaration carrying both spellings says one thing twice rather than
  contradicting itself, and every declaration written before the type behaves
  exactly as it did.

### Fixed

- The palette's count line no longer counts a recipe as a block type. It
  reports the two kinds apart - "16 block types, 1 recipe" - and when a slot's
  acceptance set is narrowing the list it counts types alone, naming the
  recipes in a clause of their own ("2 of 16 block types fit here; 1 recipe
  also listed"), because a recipe is not a block type and no set of type names
  can answer whether one fits.

## [0.18.0] 2026-09-05

0.18.0 lets a palette put down more than one block at a time. A palette may
name **recipes** beside block types - the core palette ships one,
`"deadline"`, whose single pick writes the `core.send` / `core.on_event`
pair that spells a clock interrupt - and the edit vocabulary grows a
compound, so an arrangement like that undoes in one gesture. Alongside it a
palette entry may declare how many blocks of its type a document holds, the
two core timer types rewrite a stored duration written in the retired
spelling as the block resolves, and the editor toolbar's `Fit active`
follows the run marks a host paints.

### Added

- A palette may name **recipes** beside block types: arrangements an author
  picks the way they pick a type. A recipe is a module implementing
  `StatifierBlocks.Recipe` - `insert/2`, handed the armed position and the
  document, answering with the commands that build the arrangement, and
  `palette_entry/0`, which draws it in the palette browser exactly as a type
  draws. Register recipes with the new `:recipes` option on
  `StatifierBlocks.Palette.new/2` (a `name => module` map) or on
  `from_modules/2` (an ordered list, later entries winning), and resolve one
  with `fetch_recipe/2`. Recipe names and type names are two namespaces, not
  one: a recipe named `"deadline"` and a block type named `"deadline"` do not
  collide. A recipe may target the armed position and any slot of the block
  enclosing it, and nothing above that.

- The core palette registers one recipe, `"deadline"`
  (`StatifierBlocks.Palette.core_recipes/0`). One pick puts down the pair that
  spells a clock interrupt: a `core.send` carrying a generated deadline event
  and a delay, at the head of the enclosing group's `body`, and a
  `core.on_event` naming the same event on that group's `interrupts` rail. The
  pair compiles clean before the author types anything. Picked at a position
  whose enclosing block has no interrupts rail, the gesture is refused and
  nothing is written.

- `StatifierBlocks.Edit.t()` admits a composition, `{:compound, [t()]}`,
  carrying a non-empty list of the five commands. `Edit.apply/2` applies its
  members left to right and returns the compound of their inverses in reverse
  order, so `StatifierBlocks.Edit.History` remembers it as **one undo entry**:
  one gesture in, one gesture out. A member that refuses refuses the whole
  compound, with that member's own error term and no document at all. The set
  of edits is still five - a compound's leaves are drawn from it, and a list
  that is empty or holds a compound of its own is refused rather than
  flattened.

- A palette entry may declare `singleton: :head | :anywhere`, saying how many
  blocks of its type a document may hold and, for `:head`, that the one it
  holds is first in the root's first declared slot. A document that does not
  comply draws one `:config` finding per violating type, anchored on the root
  block, so a host gets "exactly one of this, at the top" without writing a
  validator of its own. Read it with `StatifierBlocks.BlockType.singleton/1`.
  Nothing is inserted, removed or moved on the author's behalf - the editor
  says what is wrong and the author acts. An entry that omits the key, or
  declares a value this package cannot read, is unconstrained exactly as it
  is today.

### Changed

- `StatifierBlocks.Editor.Toolbar.toolbar/1`'s `:selected?` attribute is now
  `:fittable?`, because a selection is no longer the only thing that gives
  `Fit active` something to fit. Rename the attribute at any direct call
  site; hosts rendering the editor pass nothing here.

- A `StatifierBlocks.ViewModel.PaletteGroup` entry now carries `:kind`
  (`:type` or `:recipe`) and `:name`, the name in whichever of the palette's
  two maps it came from. `:type_name` is unchanged on a type entry and absent
  on a recipe entry, which has no type name at all.

### Fixed

- The editor toolbar's `Fit active` follows the run marks a host paints: it
  is enabled whenever there is a selection or an active mark, and with
  nothing selected it fits and reveals the first marked block.

- A document saved before the duration pivot opens clean instead of showing a
  refusal on every timer: `core.wait` and `core.send` are at `type_version` 2
  and rewrite a stored duration written in the older spelling into the one the
  field reads. The rewrite happens in memory as the block resolves and nothing
  is written back, so persisting it stays the host's decision.

- The drawer's tab strip scrolls sideways at the narrow breakpoint instead of
  wrapping to a second row, so the bar stays one row tall however many tabs a
  host adds, and a fade marks the end the strip is clipped at.

## [0.17.0] 2026-09-05

0.17.0 makes a stored duration mean one thing. A `:duration` field reads the
duration strings `Predicator.Duration` parses - `30s`, `1h30m`, `2d`, `3d8h` -
and refuses every other spelling, which is what finally lets `500ms` and
`1.5s` through: the intermediate canonical form that stood between them and
the engine is gone, and with it two public functions. The editor surfaces
grow alongside it - a Fixtures tab on the inspector, a fixture-derived hint
beside a condition field, datamodel-derived value candidates, done-event
chips drawn as the block they name, and an `on_select` callback for a host
panel that follows the canvas.

### Added

- The inspector carries a fourth tab, **Fixtures**, about the selected block:
  one row per fixture row the block owns, with the slot the row expects, the
  slot the compiled chart took, and the verdict. It carries a count chip in
  the Findings tab's style, reading the selected block's row count.
  `StatifierBlocks.Shell.inspector_tabs/0` now answers four tabs, with
  `:fixtures` last, and `StatifierBlocks.Shell.inspector_tab/1` resolves the
  new name.

  The rows are the same `StatifierBlocks.Runtime.FixtureRuns` result the
  drawer's Fixtures tab renders - no second execution path - filtered to the
  selected block. Runs now refresh when the inspector's tab is picked and when
  the selection moves, as well as on the drawer's own tab, so switching blocks
  cannot leave the previous block's verdicts on screen.

  The drawer's Fixtures tab is unchanged: same rows, same verdicts, same
  count, same place in the tab order. With no selection, no fixtures source,
  or no rows for the block, the new tab reads its own empty state for that
  case; a document mid-edit reads as mid-edit rather than as a fixture
  failure.
- The editor accepts an `on_select` function and calls it with a
  `%{id:, type:, label:}` descriptor for each new selection, or `nil` when
  nothing is selected, so a host panel can follow the canvas.
- Sub-second and fractional durations are expressible in a `:duration` field
  for the first time: `500ms` and `1.5s` both validate and both compile to a
  `delay` the engine resolves. The recogniser that stood between them and the
  engine is gone.
- A condition's value picker now offers what the datamodel declares. A path
  whose ADR-0006 entry carries `one_of` gets those values by default, with no
  `value_candidates` map supplied; a host entry for a path replaces the
  derived list for that path and leaves every other path's default in place.
  `StatifierBlocks.Datamodel.value_candidates/2` is the derivation, and
  nothing validates against the list - `one_of` stays a hint.
- `StatifierBlocks.ViewModel.summary_chip_titles/1` gives the raw text behind
  each summary chip, `nil` where the chip is drawn as its type declared it.
- `StatifierBlocks.Compiler.StateId.undone_event/1` inverts a generated
  `done.state` or `done.outcome` event name back to the block it names, or
  answers `:error` for a name that does not invert unambiguously.
- A condition field now draws a hint beside its control, derived from the
  selected block's fixture rows. The exemplar is what the block's first
  fixture row in declaration order binds to the path the condition names, and
  every distinct value that path takes across the block's rows is listed on
  the hint's `title`. `StatifierBlocks.Shell.fixture_hint/3` is the
  derivation, over the `fixtures` the editor already holds.

  It is a hint and never an option: nothing reaches a picker, nothing is
  merged with `one_of` or with a host's `value_candidates`, and no value it
  shows can be selected. A block with no fixture rows, or a document with no
  fixtures source, renders exactly as it did before.

### Changed

- **Breaking.** A `:duration` field reads one grammar: the duration strings
  `Predicator.Duration` parses, such as `30s`, `1h30m`, `2d` and `3d8h`. Any
  other spelling is a format finding.

  *Migration: a `core.wait` `duration` or a `core.send` `delay` stored in
  ISO-8601 - `PT30S`, `PT1H30M`, `P1D` - no longer validates. Rewrite the
  value in the unit spelling above; `PT1H30M` becomes `1h30m`, `P1DT6H`
  becomes `1d6h`, and the compiled chart is byte-identical either way.*

- `StatifierBlocks.Core.Duration.to_delay/1` now takes the normalised
  duration `parse/1` returns rather than a canonical string.
- A summary chip whose text has the shape of a generated done-event name is
  drawn as the named block's label and the outcome, with the raw name on the
  chip's `title` attribute, so a card no longer shows the compiler's spelling
  of a fact the author stated (ADR-0005 decision 10w).
- The presentation cap measures the drawn chip rather than the generated one,
  so the cap lint no longer names a string the author cannot shorten. A
  translated chip that is still over the cap is refused exactly as before.
- `summary/2`, `summary_refusals/2` and `summary_refusal_message/3` take an
  optional trailing map of block labels. A call that passes none behaves
  exactly as it did: without labels nothing is translated.

### Removed

- `StatifierBlocks.Core.Duration.to_iso/1` and
  `StatifierBlocks.Core.Duration.predicator?/1`. With one grammar in and one
  attribute out there is no intermediate form to canonicalise to, and no
  narrower predicate to hold beside `duration?/1`. Callers that compiled a
  stored value should use `parse/1` and then `to_delay/1`.

## [0.16.0] 2026-09-04

0.16.0 fills the expression seam. A condition's `:expression` field now
renders statifier-ui's expression editor - picklists of field, operator and
value over the source it can round-trip, and the plain source input over the
rest - so a signup-wizard condition like `step in ['payment', 'review']` is
composed rather than typed, and a host can offer the values each datamodel
path accepts through a new `value_candidates` assign. `statifier_ui` joins
`phoenix_live_view` as an optional dependency: with it absent, nothing raises
and an `:expression` renders exactly what it rendered before.

### Added

- An `:expression` field renders statifier-ui's expression editor - picklists of field, operator and value over the source it can round-trip, a text input over the rest - when `statifier_ui` is on the load path, and the plain source input when it is not.
- A `value_candidates` editor assign, `%{path => [%{label:, value:} | binary]}`, carrying the values a host offers per datamodel path through to the expression control.

### Changed

- `statifier_ui` is a new optional dependency, resolved the way `phoenix_live_view` already is: absent, nothing raises and an `:expression` renders exactly what it rendered before.

## [0.15.0] 2026-09-02

0.15.0 is about seeing what a document actually declares and what its
fixtures actually do. The editor's drawer gains two more tabs: Fixtures,
which drives every attached fixture row through the compiled chart and
reports the outcome it took against the one it expected, and Datamodel, a
read-only grid of every declared path with the surfaces that declared it.
Compiling a `core.resumable_group` whose body opens with a delayed send now
warns that no deadline is re-armed after the first resume. The rest is
polish on the drawer and the truth table: keyboard reach, wrapping, pinned
case names, and refusals that read as sentences.

### Added

- The editor's drawer gains a fourth tab, Fixtures, that drives every attached fixture row through the compiled chart and reports each one's expected outcome against the one actually taken.
- Compiling a `core.resumable_group` whose `body` opens with a delayed `core.send` and whose `interrupts` rail carries a `resume` handler now produces an advisory warning: no deadline is re-armed after the first resume, and the two escapes are arming the deadline outside the group or using a `core.group`.
- The editor's drawer gains a fifth tab, Datamodel: a read-only grid of every declared datamodel path, with the surfaces that declared it and the type, scope and label the ADR-0006 projection carries. It shows exactly the vocabulary the undeclared-path advisories are decided against, and `StatifierBlocks.Datamodel.declared_view/3` is that projection on its own.

### Fixed

- The drawer's tab strip is now a single Tab stop instead of one per tab, matching the inspector's strip: Tab moves from the canvas to the active drawer tab and then on to the height slider, rather than stepping through all six tabs on the way.
- A drawer tab keeps its label and its count on one line at every width; a bar too narrow for all six tabs now wraps whole tabs onto a second row instead of breaking each tab into a three-line stack.
- A folded container carrying findings no longer draws its findings count underneath the fold control: the card reserves the corner the fold occupies whenever it renders a badge.
- A truth table wider than the drawer can now be scrolled from the keyboard. The table's scroll region is a Tab stop and a labelled `region` named for the table, so the arrow keys, Home and End reach the columns past the right edge; previously every cell in it was text and nothing in the panel was focusable, which left those columns reachable by pointer alone.
- A mismatched truth-table cell now says what the disagreement was - "expected yes, selected no" beside the status word and on the cell's tooltip - instead of only the word `mismatch`, and a row's `note` renders under the row name instead of being dropped.
- A truth table's case column stays pinned to the left edge while the table is scrolled sideways, so a status word keeps the row name (and its note) beside it instead of losing its row on any table wider than the drawer.
- A truth-table row whose bindings fail to build a context now reads as a sentence naming the binding and what went wrong, instead of the inspected error tuple; an unrecognised reason still shows its term.

## [0.14.0] 2026-09-01

0.14.0 is about a document declaring its own data, and about a subchart
that outlives the process running it. `core.subchart` gains a durable
runtime: the child runs as its own persisted run and re-enters the parent
through the driver's own door when it finishes. The editor's drawer gains
a third tab, Declarations, so the `datamodel` roots a document depends on
are authored in the document rather than supplied only by the host, and
every `:expression` field offers the declared paths as suggestions without
constraining what an author may type.

### Added

- `StatifierBlocks.Runtime.DurableSubchart` runs a `core.subchart` child as
  its own persisted `statifier_persistence` run: `dispatch_fun/1` builds the
  fun the driver's `:dispatch` option takes, and the child's completion
  re-enters the parent through the driver's own door.
- The editor's drawer carries a third tab, Declarations, where an author
  adds, edits, reorders and removes the document's own `datamodel` roots
  (ADR-0001 decision 11's entries: `id`, `expr`, `description`).
- `StatifierBlocks.Edit` gains a fifth command,
  `{:set_datamodel, entries}`, which replaces the document's whole
  declaration list and whose inverse is the list that was there before, so
  a declaration edit undoes and redoes like any other. It refuses a list
  `StatifierBlocks.Document.validate/1` would refuse, in the same
  `{:malformed_envelope, {:datamodel, reason}}` family.
- `StatifierBlocks.Declarations`, the pure list-to-list arithmetic behind
  the panel: `add/1`, `remove/2`, `move/3`, `put/4`, `change/3`,
  `count/1` and `refusal/1`.
- `StatifierBlocks.Datamodel.candidates/3` returns the declared datamodel
  paths, unioned from the host's datamodel, the compile call's `:declare`
  roots and the document's own `datamodel` key, sorted and deduplicated.
- `StatifierBlocks.Datamodel.candidates_under/2` narrows those candidates to
  a prefix, in the datamodel document's own order.
- An `:expression` config field offers the declared paths as a `<datalist>`,
  suggesting without constraining; supplying no datamodel renders the plain
  input unchanged.

### Changed

- The `expression_component` override now receives a `:candidates` key
  alongside `:field`, `:id`, `:name` and `:value`. Existing overrides read
  the keys they know and are unaffected.

### Fixed

- An arranged container whose slot ends in a drafts shelf drew its rejoin
  edge from the shelf's own outlet, asserting flow out of the one card
  nothing enters and nothing leaves. The rejoin now leaves the last step in
  the slot, and a slot holding nothing but a shelf rejoins from its header,
  the same as an empty one.

## [0.13.0] 2026-09-01

0.13.0 is a small release about what an editor shows first. The drafts
shelf 0.12.0 introduced opened expanded, so a document with parked work
opened showing its shelf rather than its flow; now a stocked shelf opens
folded and the flow is what the author sees. Nothing about the shelf's
placement rules, its compile output or the fold control itself moves.

### Changed

- The editor opens a non-empty drafts shelf folded, so a document with parked
  work opens showing its flow rather than its shelf. An empty shelf opens as
  before, the fold control is unchanged, and unfolding lasts for the session.

## [0.12.0] 2026-09-01

0.12.0 is the release where a document can hold work that is not finished
yet. `core.drafts` is the document's shelf - one per document, a direct
child of the root, holding fragments an author has built but not placed -
and it compiles to nothing, so shelving work never moves a byte of the
emitted SCXML. `core.placeholder` is the other half: an in-flow marker for
a step left unwritten on purpose, compiling to a state that completes on
entry so a preview walks straight through the gap. Both are visible to a
host rather than silent - misplacement and duplication are Structure
errors, and a non-empty shelf or a placeholder raises an Emit warning on a
compile that still succeeds. The editor draws the shelf as a tray: a
detached row at the foot of the canvas with no boundary box and no
connectors, because parked work is not part of the flow.

### Added

- `core.drafts`, the document's shelf: a container whose one `body` slot
  holds fragments an author has built but not placed. It is admitted as a
  direct child of the root block's `body` and nowhere else, a document
  carries at most one, and it compiles to nothing at all - a document with
  work on its shelf, the same document with the shelf emptied, and the same
  document with no shelf produce byte-identical SCXML.
- `core.placeholder`, an in-flow leaf marking a step left unwritten on
  purpose. One optional `note` field, and it compiles to a state that
  completes on entry, so a preview of a half-built workflow walks straight
  through the gap.
- Two Structure-stage errors: `:drafts_block_misplaced` and
  `:duplicate_drafts_block`, the second naming the second and every later
  shelf in document order rather than the first.
- Two Emit-stage warnings on a compile that succeeds:
  `:draft_blocks_present`, once per document on a non-empty shelf, and
  `:placeholder_block`, once per marker, carrying the author's note. What a
  host does with either - a publish gate, say - is the host's.
- `slot_style: :tray`, a fourth value for `palette_entry/0`'s `slot_style`
  map. A tray is a detached shelf: no boundary box, and no connector into
  it, out of it, or between one fragment and the next. `StatifierBlocks.ViewModel`
  gains `tray?/1`, `shelf?/1`, `flow_children/1` and `shelf_children/1`.
- `StatifierBlocks.Shelf`, the module owning the shelf's placement rules and
  the type-name predicates other layers ask about it.

## [0.11.0] 2026-08-31

0.11.0 is the release where declaring a block type stops being boilerplate.
`use StatifierBlocks.BlockType` fills in every callback but `emit/2`, and
`use StatifierBlocks.InvokeStep` goes further for the shape that keeps
recurring - a leaf step naming one host invoke type - so such a block type
is a handful of options rather than a module of callbacks. Beside it, the
`statifier_blocks:subchart` invoke type gains a canonical handler: `use
StatifierBlocks.Runtime.Subchart` generates the in-memory
`Statifier.Session` implementation a host would otherwise write by hand,
leaving the document resolver as the host's seam and putting a stated
reason on every refusal.

### Added

- `use StatifierBlocks.BlockType` declares the behaviour and injects an overridable default for every callback but `emit/2`.
- `use StatifierBlocks.InvokeStep` declares a leaf step that names one host invoke type, filling in every callback from an invoke type, an optional produced type, extra config fields, and palette keys.
- `use StatifierBlocks.Runtime.Subchart` declares the canonical
  `statifier_blocks:subchart` invoke handler for the in-memory
  `Statifier.Session` case: the host supplies a document resolver and a
  palette, and the generated module resolves the chart a `core.subchart`
  names, compiles it as a child, and starts it as a child session. Refusals
  surface on `error.communication.invoke` with a stated reason -
  `unknown_document`, `child_compile_findings`, or `cycle_refused`.
- `StatifierBlocks.Runtime.Subchart.handlers/1` builds the `:invoke_handlers`
  map for `Statifier.Session.start_link/2`.

## [0.10.0] 2026-08-31

0.10.0 is the release where a document declares its own data. A block
document carries a top-level `datamodel` key naming the `<data>` roots its
own guards and assigns read, so a document compiles on its own rather than
depending on whichever host happens to declare them; the compile call's
`:declare` option stays beside it as the host-wins surface, and the editor's
undeclared-path advisory reads both. Around that, `core.on_event` takes a
`cond`, so an interrupt rule fires only when its event arrives *and* the
condition holds; the `done` outcome of an invoke run mark gets theme tokens
of its own instead of borrowing the accent blue it was indistinguishable
from; and the canvas stops falling short of the tree it holds.

### Added

- The block document gains a top-level `datamodel` key: a list of
  `StatifierBlocks.Document.DatamodelEntry` structs, each an `id` plus an
  optional `expr` and an optional `description`, naming the `<data>` roots
  the document's own guards and assigns read. `Document.new/2` takes a
  `:datamodel` option (default `[]`); a document declaring none compiles
  and encodes byte-identically to one built before this key existed. The
  key is part of the document's canonical bytes, so it participates in
  `content_hash/1` and in compile determinism.
- `Compiler.compile/3`'s `:declare` compile option now leads a second
  declaration surface rather than being the only one: the compile call's
  roots emit first, the document's own `datamodel` roots follow, and
  block-declared roots follow those, all in one `<datamodel>`. A root
  both the compile call and the document declare is host-wins: the
  compile call's declaration is emitted, the document's is dropped, and
  the compile succeeds with a `:shadowed_document_root` warning on
  `Compiled.warnings` rather than refusing. A document root colliding
  with a block-declared root is still refused as `:duplicate_binding`.
- `core.on_event` takes an optional `cond`: an `:expression` config field
  that becomes the guard on the handler's transition, so an interrupt rule
  fires only when its event arrives *and* the condition holds. A handler
  whose `cond` is absent, empty, or whitespace emits exactly the bytes it
  emitted before the key existed, so the key is additive over every
  document authored without it. The condition is passed through to
  predicator verbatim - `validate_config/1` only asks that the stored value
  be a string - and the transition carries `"cond"` as its attribution key,
  so an upstream expression error lands on the field the author typed into.
  Recorded as ADR-0002's 2026-08-31 note.
- `StatifierBlocks.Editor` takes a `declare` assign, the `{id, expr}` roots
  the host will pass `StatifierBlocks.Compiler.compile/3` as `:declare`,
  defaulting to `[]`. `Editor.findings_count/3` takes the matching
  `:declare` option, so the host's number and the drawer's stay the same
  number. The document's own roots need no option - they are read off the
  document.
- `StatifierBlocks.Datamodel.declared_roots/1`, the total normalizer for a
  root set, beside `declared_paths/1`.
- Two tier-2 theme tokens, `--sb-run-done` and `--sb-run-done-bg`, for the
  `done` outcome of an invoke run mark. A host that themes the editor now
  sets the "came back" colour directly instead of inheriting whatever it set
  `--sb-info` to.

### Changed

- Decoding a document now refuses an envelope object carrying a key
  outside `id`, `revision`, `root`, `schema_version`, `metadata`,
  `datamodel`, and refuses a `datamodel` entry carrying a key outside
  `id`, `expr`, `description`, or carrying an explicit JSON `null` for
  `expr` or `description` - matching the round-trip discipline already
  applied to unrecognized block keys.
- The family's two worked examples - the ADR-0001 card authorization
  document and the signup wizard - now declare the `<data>` roots their
  own guards read through ADR-0001 decision 11's document `datamodel`
  key, rather than leaving them to whichever host compiles them. Each
  declares exactly what it reads and no more: `budget_remaining` and
  `amount` for the first, `variant` for the second, none of them carrying
  an `expr`, since all three are per-run values a host seeds or a step
  assigns. Either document now compiles on its own, with no `:declare`
  option, without its guard raising `error.execution` over a root nothing
  declared.
- Both documents' canonical bytes move with the key, and so do their
  document hashes and the worked example's pinned chart identity. The
  identity move was verified rather than accepted: deleting the one
  `<datamodel>` element from the new bytes reproduces the previously
  pinned hash exactly, so nothing else in either emission changed. Only
  the test fixtures ship these documents; no packaged code moved.
- The editor's undeclared-path advisory now reads three declaration
  sources rather than one. A config field a block type annotated
  `datamodel_path?: true` is declared when the host's datamodel holds its
  path, when the document's own `datamodel` key declares its root, or when
  the compile call's `:declare` option declares its root. A declared root
  covers every path beneath it; the datamodel's own paths are still matched
  whole. ADR-0005's decision 11 is amended as 11k-11m, taking the open
  question ADR-0001's 2026-08-31 amendment left to it in its clause 11g.
- The check's precondition widens to match: it runs when a datamodel was
  supplied *or* when either surface declares a root, and still produces
  nothing at all when nothing anywhere was declared. A host that passes no
  datamodel and no roots sees exactly what it saw before; a document that
  declares its own roots now lints its own paths with no host involved.
- An invoke mark whose outcome is `done` is drawn in `--sb-run-done` rather
  than `--sb-info`. In the shipped light palette `--sb-info` and
  `--sb-accent` are the same blue, so a block that was *active* and a block
  that had *come back* differed only in treatment - a halo outside the border
  against a border and a fill - and read as one state at a glance. They are
  now different hues in every theme the package ships or documents.

  This is a visual change for a host that did not override `--sb-info`, and
  a host that had retuned `--sb-info` specifically to colour its done marks
  should move that value to `--sb-run-done`. Nothing else reads the new
  tokens, and the `error` outcome still takes `--sb-error`.

  The complete host theme in `docs/theming.md` restates both tokens, as the
  theme audit requires of every colour token, and the accessibility
  discipline is unchanged: `--sb-run-done` is held to the 3:1 non-text
  threshold as a border that carries information, and `--sb-run-done-bg` is
  recorded as translucent and held to no ratio.

### Fixed

- A summary chip whose text is a single identifier - `signup.reminder_due`
  - no longer breaks mid-token onto a second line. The chip keeps one line
  and clips at its own edge with an ellipsis, which is the treatment the
  invoke type on the line below it already used. The card's title keeps its
  wrap: a title is prose an author wrote, not an identifier.
- The canvas stage is now at least as wide as the tree it holds
  (`min-width: min-content`). Its box took the panel's width, while the
  tree has a floor of its own - nesting paddings, the interrupt channel, a
  card that will not shrink past its token width - so a panel narrower than
  that floor left the stage short of its own content, and everything read
  off that box, the measurement hook's `offsetWidth` included, was short
  with it.
- `Connectors.fan_path/3`, `Connectors.join_path/3` and
  `Connectors.interrupt_path/4` now clamp an ascending edge level, as
  `flow_path/3` already did: the head is raised to the tail's own `y`
  rather than routed upward. An arrowhead is oriented along its path, so an
  ascending arm rendered an arrow pointing back at the block the flow just
  left - a loop the document does not contain.

## [0.9.0] 2026-08-31

0.9.0 is the release where the editor can watch a run. A host hands the
editor the block ids a run has activated and the block it is calling out to,
and the editor draws them on the cards themselves, in the accent and finding
tokens a theme already retunes. The same host seam widens the drawer: a host
contributes its own tabs beside the package's Truth tables and Findings, so a
run feed the host is appending to lives in the drawer rather than beside the
editor. Around that the chrome gives room back - the inspector folds to a
rail the way the palette already does - the config form's fields are dressed
from the same tokens as the palette's search box, and connectors stop
overshooting the cards they point at.

### Added

- The editor's inspector folds to a rail from a chevron on its own header, the
  way the palette already does, giving its width back to the canvas.
- `--sb-inspector-collapsed-width`, the theme token for how wide that rail is.
- The editor takes run marks from its host: `active_marks`, the block ids a run
  has activated, and `invoke_mark`, the block a run is calling out to together
  with how the call came back. Both are ordinary assigns, so a host pushes them
  with `Phoenix.LiveView.send_update/3` and needs no new API; both are held as
  editor state, so a re-render the host makes for its own reasons does not drop
  them; and both are cleared when the host opens a different document, because
  a mark addresses one block.
- The marks reach the markup as `data-run-active`, `data-run-invoking` and -
  only once a call has come back - `data-invoke-outcome` on the block's
  `.sb-node`, and the stylesheet draws them in tokens a host theme already
  retunes: the accent family for the active mark, the finding severities for
  the outcomes. A mark on a folded container stays visible.
- The editor accepts an optional `invoke_types` assign, and an `invoke_type`
  field then offers those types as a suggestion list; free text stays valid
  and an unknown type stays a lint rather than a refusal.
- `StatifierBlocks.Connectors.slots_anchor/1`, the anchor key for a
  container's body box. The editor stamps it on the element holding a
  container's slots, and an interrupt channel is now offset from that box
  rather than from the container's whole node box.
- The editor takes drawer tabs from its host: `drawer_tabs` is a list of
  `%{id:, title:, content:}` descriptors, with an optional `count:`, and each
  one is drawn beside the package's own Truth tables and Findings tabs and
  activates the same way. `content` is a function component, the same seam
  shape `icon` and `expression_component` use, called when its tab is the
  active one. The descriptors are ordinary editor state, so a host pushes them
  with `Phoenix.LiveView.send_update/3` and a feed the host is appending to
  redraws as it grows - a tab whose content changes under the host is what the
  seam exists for.
- The collapsed strip and the unchosen-tab resolution reach host tabs too: a
  document with no truth tables and no findings and a running feed opens on
  the feed rather than on an empty `Truth tables 0`, and a collapsed drawer
  names it.
- `StatifierBlocks.Shell.host_tabs/1` is the admission rule, and
  `StatifierBlocks.Shell.drawer_tab/2` resolves a tab name against the
  package's tabs and the host's together. A host tab named for one of the
  package's own, or repeating an id already used, is not drawn: the id is
  stamped into the tab's DOM id and its panel's. No tab name is ever turned
  into an atom, so a crafted `phx-value-tab` reaches at worst a tab the host
  declared.

The package's own tabs, the drawer's five states, its resize and its collapse
are unchanged, and a host that contributes no tabs gets the drawer it had.

### Changed

- The config form's fields are dressed the way the palette's search box always
  was - a token-built box with a border, a radius and the pane's own fill -
  instead of being left to whatever the host's browser paints a bare `<input>`
  or `<select>` as. The two are now one rule rather than two, so a host
  retuning `--sb-border`, `--sb-radius-sm` or `--sb-bg` moves every control in
  the editor together.
- Placeholder text in those fields takes `--sb-fg-subtle`, and a disabled field
  is muted with `--sb-disabled-opacity`, which is what a disabled `sb-button`
  has always used. Boolean fields, which render a checkbox, are excluded from
  the box by selector and are unchanged.

### Fixed

- Connector arrowheads keep one size at every stroke width and land their tip
  on the endpoint, rather than being scaled by the stroke of the path that
  references them and overshooting the card they point at.
- A flow edge whose head was measured above its tail is drawn level instead of
  ascending, so no arrowhead points back at the block the flow just left.
- An interrupt edge clears the container it exits instead of turning down
  inside it. A container is now as wide as what it holds rather than as wide
  as the box around it, so the box the routing is measured from means what it
  encloses.
- A gap's insertion marker masks the flow line it sits on, and its glyph stays
  legible when the canvas is zoomed out. The dashed placeholder ring stays on
  empty slots only.

## [0.8.0] 2026-08-30

0.8.0 is the release where a container folds shut. A container in the
editor now carries a fold control on its own card, and folding it does not
hide what is inside: a folded container wears a ring badge counting the
findings under it, so a problem stays visible from the outside. Alongside
that, an editor whose host swaps one document out for another fits the new
document the way it fitted the first - the `fit` attr is spent once per
open document rather than once per editor, so a swap now behaves exactly as
a mount does. Hosts: see Removed - the `--sb-fg-on-accent` theme token is
gone, and a theme that still sets it can drop the declaration.

### Added

- A container in the editor folds shut from a control on its own card, and a
  folded container carries a ring badge counting the findings inside it.

### Changed

- The editor's `fit` attr is now spent once per open document rather than once
  per editor: a document the host swaps in is armed from the attr passed in
  that same update and fitted by the next measurement, exactly as at mount. A
  host re-render carrying the document already open still never re-fits, and
  `:manual` or an absent attr still arms nothing.

### Removed

- The `--sb-fg-on-accent` theme token, which no rule in the stylesheet reads
  any more. A host theme that sets it can drop the declaration.

## [0.7.0] 2026-08-30

0.7.0 is the release where a finding looks like a finding wherever it is
read. One row anatomy - severity word, anchor tail, source chip, message -
now draws on the drawer's tab and on both inspector panels, which used to
show the bare message, and the two document-level surfaces carry a severity
pill row above the list. Alongside that, the 24-character presentation cap
on summary chips stops failing silently: a dropped chip raises a `:lint`
warning against its block and `summary_refusals/2` says which chip and why,
so a card with no second line can be told apart from a type that declared
none. And an editor opened with a fit no longer flashes at 100% before
snapping to it. Hosts: see Removed - `:no_presentation_source` leaves
`Finding.from_compiler/2`'s refusal type, and the canvas toolbar's dead
`inserting?` attribute is gone.

### Added

- `StatifierBlocks.Shell.severity_counts/1` cuts a findings list by severity,
  in `:error`, `:warning`, `:info` order and omitting any with nothing at
  them; it sums to `findings_count/1` for the same list.
- `StatifierBlocks.Editor.Findings.row/1` and `anchor_tag/1` are public, so a
  host rendering findings of its own gets the editor's row anatomy rather than
  re-deriving it.
- `StatifierBlocks.BlockType.summary_refusals/2` reports the summary chips the
  24-character presentation cap dropped, as `{index, reason}` with `reason` in
  `:too_long`, `:blank`, `:multiline` or `:not_a_string`, and
  `summary_refusal_message/3` puts one into the words an author reads.

### Changed

- A finding renders the same way everywhere: severity word, anchor tail
  (`config.duration`, `slot:body`, nothing for a block anchor), source chip
  and message. The inspector's two findings panels showed only the message
  before.
- Both document-level findings surfaces - the drawer's Findings tab and the
  inspector's with nothing selected - carry a severity pill row above the
  list. The list itself is still grouped by block.
- The view model raises a `:lint` warning against a block for every summary
  chip the presentation cap refused, so a card that draws no second line can
  be told apart from a type that declared none. A well-formed document gains
  no findings.

### Removed

- `StatifierBlocks.Finding.from_compiler/2` no longer declares the
  `:no_presentation_source` refusal in `from_compiler_error/0`; nothing has
  produced it since the adapter began mapping unplaced compiler findings to
  `:compile`, so a caller that matched on it can drop the clause and keep the
  `{:unanchorable, finding}` one.
- The canvas toolbar's dead `inserting?` attribute, left declared when
  0.6.0 removed its "Cancel insert" button. A host still passing it to
  `StatifierBlocks.Editor.Toolbar` gets an undefined-attribute warning;
  drop the assignment (statifier_blocks#172).

### Fixed

- An editor opened with `fit: :width` or `fit: :active` no longer paints its
  first frame at 100% and then snaps to the fit. While a fit is armed the
  root carries `data-fit-pending` and the stylesheet keeps the stage
  unpainted under it, so the first frame an author sees is the fitted one; a
  host that never imported the measurement hook, and so never spends the fit,
  is revealed by a CSS-only fallback half a second in rather than left blank.

## [0.6.0] 2026-08-30

0.6.0 is the release where the editor tells one story about findings. The
Findings tab reads the whole document when nothing is selected, the drawer's
strip and a host header can finally show the same number, and a container no
longer wears a badge on every uncollapsed face. Alongside that: a block type's
summary draws as a row of chips rather than one joined string, the palette's
search field wears the package's own chrome, and a host can open a document
already fitted to the canvas. Hosts: see Removed - `source: :arity` leaves
`StatifierBlocks.Finding` (pass `:assignability`) and `:compile` joins the enum;
`ViewModel.subtitle/1` now answers only a type label, with the chips behind the
new `summary_chips/1`; `.sb-badge` is rendered by nothing; and the canvas
toolbar's "Cancel insert" is gone, its job done by the palette's Cancel.

### Added

- `StatifierBlocks.Finding`'s `source` gains `:compile`, and
  `from_compiler/2` maps any compiler finding its by-stage rule cannot place
  onto it instead of refusing, so a compile error raised against generated
  SCXML or against the document envelope can be rendered by the editor.
- `StatifierBlocks.Shell.findings_groups/3` groups a document's findings by
  the block each is anchored to, unanchored ones last, without dropping any -
  the grouping behind that panel, and headless like the rest of `Shell`.
- The editor takes a `fit` attr - `:manual` (the default, unchanged
  behaviour), `:width` or `:active` - so a host can open a document already
  fitted to the canvas instead of leaving the author to press `Fit width` on
  every document. The first measurement performs the fit once; after it the
  editor behaves exactly as if the author had pressed the button, and an
  unknown value is refused into `:manual`.
- `StatifierBlocks.Editor.findings_count/3` returns the number of findings the
  editor's Findings tab reports for a document, from the same `document`,
  `palette`, `findings` and `datamodel` a host already passes the component, so
  a host header and the drawer cannot show two different numbers.

### Changed

- A block type's `summary/1` chips draw as a row of separate chips under the
  card's title (`.sb-node__summary`, one `.sb-node__chip` per chip) instead of
  one string joined with `", "`; the row wraps, and a type declaring no summary
  draws no row at all.
- `StatifierBlocks.ViewModel.subtitle/1` answers only the type label an author-
  named card carries, and `nil` otherwise. Read the chips from the new
  `StatifierBlocks.ViewModel.summary_chips/1` instead of the joined string.
- The inspector's Findings tab reads the whole document when nothing is
  selected: the count beside the tab is the document's findings number - the
  same one the drawer's strip and `StatifierBlocks.Editor.findings_count/3`
  report - and the panel lists those findings grouped by block, each row
  selecting its block. Findings anchored to a block the document no longer
  holds get an `Unanchored` group of their own, since they are inside the
  count. With a block selected the tab is that block's findings, unchanged.
  A host styling the panel has three new classes: `.sb-inspector__groups`,
  `.sb-inspector__group` (with `data-block-id` and `data-unanchored`) and
  `.sb-inspector__group-title`, plus `.sb-inspector__group-row` on the rows.
- The palette's search field is drawn by the package - a border, a surface, padding and a radius, all from `--sb-*` tokens - rather than left to whatever box the host's browser paints inside the pane.
- A gap's "+" wears the editor's button chrome at rest, so an insertion point reads as a control without being hovered first. Its hover, armed and drag states are unchanged.
- The finding count badge no longer renders on a container's face. ADR-0005
  places it on a collapsed subtree and the editor has no collapse command yet,
  so a badge on every container read as an error on every card while the counts
  multiplied up the tree. Every finding is still reachable: the node keeps its
  subtree rollup in `data-findings-count`, and the drawer's Findings tab and the
  inspector both list them. A host styling `.sb-badge` should know the class is
  now rendered by nothing.

### Removed

- `:arity` leaves `StatifierBlocks.Finding`'s `source` type. Nothing ever
  produced it: slot arity and undeclared-slot violations arrive through the
  compiler's `:structure` stage and have always carried `:assignability`.
  Pass `:assignability` where you passed `source: :arity`; the finding's
  `{:slot, block_id, slot_name}` anchor, and so where it renders, is
  unchanged.
- The canvas toolbar's "Cancel insert" button. Leaving an insert is the palette's Cancel, beside the line that names the slot the next pick fills, or Escape.

## [0.5.0] 2026-08-30

0.5.0 is the release where the editor behaves like the spike it was drawn
from. It bounds its own height and hands scrolling to the panes; every zoom
step and both fits scale the canvas for real; an arranged container's lanes
size to their own content, so connectors stop crossing sibling cards;
inserting visibly arms the gap the pick will land in, and a palette entry can
be dragged onto one; an unresolvable block's card is compact again, with its
findings and stored config moved to the inspector; the core block types
summarise themselves on a card's second line; nesting depth is banded across
the canvas; the plain controls render as buttons; and a host's `icon`
component is rendered as a function component. Hosts: see Changed -
`.sb-node__raw-config` is now `.sb-inspector__raw-config`, the editor's
buttons carry a new `sb-button` class, and two band tokens
(`--sb-band-even`, `--sb-band-odd`) join the tier-2 theming surface.

### Added

- A palette entry can be dragged onto a gap on the canvas to insert a block of
  that type there. The slots that accept the dragged type highlight as soon as
  the drag starts, exactly as they do when a card is dragged, and the drop
  produces the same insert a "+" and a pick produce.
- `--sb-editor-height` bounds the editor: set it to a length and the panes
  scroll in their own boxes while the drawer stays pinned at the bottom,
  instead of the whole document growing the host page. The default is `auto`,
  so an editor nobody bounds is unchanged.
- Two theming tokens for the editor's nesting bands, `--sb-band-even` and
  `--sb-band-odd` (tier 2). Both default to a surface the theme already
  carries, so a host that restates `--sb-bg` and `--sb-bg-sunken` in its own
  palette bands the canvas without setting either one.
- Block types may export an optional `summary/1`, returning `nil`, a short
  string, or a list of chips, which the editor draws as a card's second line
  when the author has not named the block. It is read through
  `StatifierBlocks.BlockType.summary/2`, which normalises every shape to a
  chip list and drops an over-long chip rather than truncating it.
- `core.parallel`, `core.wait`, `core.on_event`, `core.send` and `core.branch`
  summarise themselves on the card: lane names, `timer <duration>`, the outcome
  and the event, the event, and `N arms + otherwise`.
- `sb-button`, one class carrying the editor's button look, so a host restyling the family changes one selector rather than seven.

### Changed

- The measurement hook's payload carries the canvas panel's usable box under a `viewport` key, read from the element the editor stamps `data-sb-anchor="viewport"`. A host that registers the hooks from the package's default export needs no change; a host that reimplemented the hook against the documented payload should send the new key for the fits to resolve to a number.
- Every slot on the canvas carries `data-sb-depth`, its root-relative nesting
  depth, and the stylesheet paints a full-width band per nesting level,
  alternating by the depth's parity. Depth 0 is deliberately unbanded, so the
  canvas keeps its own dotted ground.
- The interrupt-rules rail has a ground of its own, in the warning family its
  dashed edge already uses.
- `StatifierBlocks.ViewModel.Node` carries a `summary` field, and
  `ViewModel.subtitle/1` returns it for a block whose title is its type's. A
  block the author has named still reads its type's label there.
- An unresolvable block's card reads its type and one short reason; its
  findings and its stored config moved to the inspector, so the card is the
  same width as its siblings.
- The inspector's Block section shows an unresolvable block's stored config
  as canonical JSON, wrapping mid-token rather than spilling past the pane.
- The stored-config `<pre>` moved from the card to the inspector, and its class
  with it: `.sb-node__raw-config` is now `.sb-inspector__raw-config`. A host
  styling the old class should restyle the new one.

### Fixed

- A slot that refuses the block being dragged no longer accepts a drop when it
  sits inside a slot that accepts it. The gaps in the refused slot were live
  targets, and dropping on one put the block in the slot that had said no.
- Every zoom step in the editor toolbar now scales the canvas, and the panel scrolls the scaled size rather than the unscaled one.
- `Fit width` resolves to the largest zoom step at which the chart fits the canvas panel, instead of only recording that the fit was asked for.
- `Fit active` resolves to the largest zoom step at which the selected block fits, and scrolls that block to the centre of the panel.
- A host's `icon` component is rendered as a function component rather than
  applied to a bare map, so it receives a tracked assigns map and may use
  `Phoenix.Component` helpers such as `assign/3` and `assign_new/3`. A host
  that worked around the old behavior by adding `__changed__` to the assigns
  itself no longer needs to; the component must still return a `~H` template,
  which it always had to.
- Connector edges no longer cross sibling cards: an arranged container's lanes
  size to their own content, so a nested arrangement wider than one lane no
  longer overflows into the lanes beside it (ADR-0005 decision 10b).
- Clicking a gap's "+" now visibly arms that gap, and the palette says which
  slot of which block the pick will land in, with a Cancel beside it and
  `Escape` as the way out.
- A palette pick made with nothing armed says why it did nothing instead of
  failing silently.
- A palette narrowed to the types a slot accepts now looks different from a
  palette that simply holds that many types.
- The editor's plain controls - Undo, Redo, Cancel insert, the zoom steps, Fit width, Fit active, and a list field's add/remove - render as buttons rather than as whatever the host's browser paints, with a hover, a muted disabled state and an accented pressed state.

## [0.4.0] 2026-08-30

### Added

- The inspector's Config tab is two labelled sections. **Block** states the
  selected block's type label, its id and the slot it sits in, and renders with
  nothing selected too - three rows in the same place, reading as a dash.
- `StatifierBlocks.Shell.slot_label/2` answers which slot a block sits in, by
  the slot's label rather than its name, with `"root"` for the document root.
- A block type may declare a `:string` config field keyed `label`, and the
  editor draws that value as the card's title with the type's own label as a
  subtitle underneath - so a host's steps read as the names an author gave
  them without the type ever being hidden.
- A card carrying `invoke_type` in its config draws it in mono on a third
  line, which is the fact an author checks most on a step that calls out to a
  handler.
- `StatifierBlocks.ViewModel.title/1` and `subtitle/1` answer what a card's
  two name lines say, and `ViewModel.Node` carries `title` and `invoke_type`
  for a host rendering its own cards.
- The compiler refuses a `core.subchart` whose `chart` names the document the
  block sits in, as an `:emit`-stage `:self_reference` finding against that
  block: a document cannot run itself. A cycle through two or more documents
  needs the host's document graph and stays the host resolver's to refuse.
- `StatifierBlocks.Shell.drawer_tabs/0`, `drawer_tab/1` and `drawer_title/1`,
  in the same shape as the inspector's tab helpers - an unknown tab from a
  crafted `phx-value-tab` resolves to the first one.
- `.sb-findings__row`, `__severity`, `__subject`, `__label`, `__id` and
  `__message`, the row's parts. The severity colour stays on `.sb-finding`
  and its severity modifier, so a host that had restyled one severity keeps
  that styling with no edit. No new custom property.
- A slot's header shows the condition it is subject to: the expression source,
  read-only, in a monospaced chip under the slot's name, clipped to one line
  with the whole of it in the chip's `title`. A branch's arms on the canvas now
  say what picks between them instead of only naming themselves.
- `StatifierBlocks.ViewModel.Slot.condition` carries that source. It is derived
  from the container's own `:expression` config field keyed by the slot's name,
  read through the field's declared `value_path`, so a host block type that
  guards a slot the way `core.branch` guards an arm gets the same chip without
  the editor learning either type's name.
- The palette and the inspector render as framed panes with a header row. The
  palette's names the pane and carries a chevron that folds it to a rail,
  giving its width back to the canvas; the inspector's names the pane and
  states its subject on the right - the selected block type's label, or
  `no selection`.
- `--sb-palette-collapsed-width`, the width the folded palette narrows to
  (tier 2, default `2.25rem`).
- The palette carries a count line under its search box: the size of the
  palette when nothing is filtering it, and how much of it is left plus what
  is doing the narrowing when something is - a query, or the acceptance set of
  the slot the palette was opened from.
- Each group header carries the number of rows currently under it, so a
  filtered section says how much of it survived the filter.
- The editor draws the join marker under a container whose slots sit side by
  side, reading the words the block type's `join_label` callback returns -
  `core.parallel` completing on its first lane says "continue at first" - and
  falling back to the editor's own word for a type that declares none.
- `StatifierBlocks.ViewModel.Node` carries `join_label`, the normalized words
  that callback returned for the block's config, or `nil` when it declared
  none.
- The editor's canvas is a named panel: the toolbar is its header row, with a
  `Canvas` label, a `nested tree` chip, one segmented zoom control, and the
  depth and block-count metrics as right-aligned chips.
- The canvas sits on a bordered, dotted ground, and `--sb-canvas-grid` is the
  tier-2 token a theme sets to change the dots' colour and spacing together.
- The editor draws a `ONE OF` pill above a container whose body slots are
  alternatives and an `ALL OF` pill above one whose lanes run concurrently,
  which is the only place that distinction is stated on the canvas.
- `StatifierBlocks.ViewModel.arrangement/1`, `body_slots/1` and `fan_label/1`
  derive how a container arranges its body slots, shared by the renderer and
  the connector geometry so the layout and the lines cannot disagree.
- `StatifierBlocks.Connectors.fan_anchor/1` and `join_anchor/1` name the two
  markers a fan leaves from and rejoins at, so an edge is no longer drawn
  through the words that say what it means.
- `--sb-card-width` sets the width of a block's card, which is what keeps the
  measured connector geometry from collapsing every edge onto one spine.

### Changed

- **Configuration**'s empty state is a box standing where the form stands,
  saying what selecting a block would let the author do, rather than the
  one-line sentence the other tabs use for having nothing to read.
- A required field is marked with the word `Required` beside its label instead
  of an asterisk on the end of it - the asterisk needed a legend the editor
  does not have and is read aloud as "star".
- The Findings tab's count is a pill in the error hue rather than a tinted
  rectangle, and it is still the block's own findings, never the subtree's.
- The delete control on a card is revealed on hover, on keyboard focus and on
  the selected card, and is hidden at rest. It is still in the DOM and still
  focusable, so the keyboard path is unchanged.
- The card title reads as a title rather than as a native button, and the
  count badge, the subtitle and the invoke line are placed by a grid on the
  card's chrome.
- The per-block-type accent stripe is drawn on cards whose type declared an
  `accent_token` and on no others. A type that declared nothing keeps a plain
  card; its icon tile is unchanged.
- The document-level findings list is the drawer's **Findings** tab, beside
  Truth tables, and no longer a block of text under the canvas (operator
  ruling, 2026-08-29, under ADR-0005 ruling 1A: a list of findings is a
  grid of rows about the whole document). Each row carries the finding's
  severity, the block it is about - label and id - and the message, and
  clicking one selects and reveals that block. The inspector's Findings tab
  is unaffected and stays the selected block's findings (3A), as do the
  per-card counts.
- The collapsed drawer strip reports the **active tab**. An author who has
  not picked a tab gets the first one that holds something, so a document
  with findings and no fixtures reads `Findings 4` rather than
  `Truth tables 0`; once a tab is picked the pick stands. A host swapping the
  open document resets the pick along with the drawer's open state.
- `StatifierBlocks.Shell.drawer_view/1` accepts `:tab`, `:findings` and
  `:orphan_findings`, and its result gains `tab`, `tabs`, `findings` and
  `orphans`. `title` and `count` now describe the active tab rather than the
  truth tables specifically; the truth-table `status` values are unchanged.
- `StatifierBlocks.Editor.Findings.findings/1` takes `findings`, `orphans`,
  `root` and `target` instead of `view_model`, and renders the tab's panel
  rather than a headed section: the tab is the heading.
- Slot labels are small, uppercased and letter-spaced - the treatment the fan
  pill and the join marker already carry for chrome that labels a structure.
  The transform is presentation only: the string a block type declares for a
  slot is unchanged, and every other reader still sees it as written.
- Concurrent lanes carry a rule in the block accent across the top of each
  lane's header, drawn off `data-arrangement="lanes"`. The pill above says
  `ALL OF` once; the rule is what carries that distinction down a document
  taller than one screen, where a set of lanes and a set of branch arms
  otherwise look alike.
- An interrupt rail's dashed edge and its heading take the colour the connector
  layer already draws an interrupt edge in, so the rail and the edge leaving it
  read as one thing.
- `StatifierBlocks.Editor.PaletteBrowser` takes a `collapsed` attribute, and
  the editor answers a `palette-collapse` event with one boolean and no hook,
  in the same shape as the shell amendment's other gestures. The fold is not
  reset when the host swaps the open document: it is a preference about the
  pane rather than state about the document.
- The narrow arrangement (ADR-0005 ruling 7A) is unchanged. Below a container
  width of 780 the strip and its sheet are still the palette's whole chrome
  and the pane header stands down, so the fold has nothing to do there.
- A container draws a box around its body only when it is a boundary - a
  container with a slot in the rail partition (ADR-0005 decision 10c, as
  amended by 10h). Every other container draws none: its own card stays at
  the head of its body and its children sit under it with the connectors,
  where a box around each of them turned a deep document into nested
  rectangles.
- A boundary's box is the border, the radius and the inset that enclose its
  body and its rail, rather than the border colour it was before.
- A container's card carries its own border, its accent stripe and its
  selection ring, so the block is still a card on the canvas when the box
  around its subtree is gone. A leaf card is unchanged.
- A palette row is now a tile, a name, and the type's description on a second
  line, at a row height that gives the description room to wrap. The tile is a
  slot rather than an icon: a block type that declared no icon still renders
  the box, so every name in the list lines up.
- A row's accent moved from a stripe down its leading edge onto the tile, which
  is where the card the pick produces carries it. The stamp itself is
  unchanged - a type that declares no accent token still gets neither the
  attribute nor the custom property.
- A `core.wait` mints its delayed send under the reserved `send` role, so a
  chart containing one now compiles to `s_<block id>__send` where it compiled
  to `s_<block id>__timer`.
- The package's default JavaScript export now carries both hooks, so
  `hooks: { ...StatifierBlocks }` registers `StatifierBlocksDrag` and
  `StatifierBlocksMeasure` together - a host that registered only the drag hook
  got an editor with no connectors and no error explaining it. Both names are
  still exported individually, and `statifier_blocks/measure` still resolves for
  a host that wants measurement alone.
- The canvas stage renders inside a `.sb-canvas-panel` element, which is now
  the scrolling box; `#sb-canvas` stays the drag hook's element, the stage
  anchor, and where the `theme` assign's declarations land.
- A container with more than one body slot now lays its slots out side by side
  and fans into them, as a container declaring `layout: :columns` already did -
  a branch's arms no longer stack full-width with each fan edge running down
  through the arm above its target.
- Columns are a CSS grid taking their natural heights, cards are a fixed width
  centred in the box they sit in, and a column's header is card-width and
  centred over the first card it governs.
- The "+" between two blocks is now the insertion marker: subtle at rest,
  highlighted on hover, on keyboard focus and for the whole of a drag, and
  drawn as a placeholder ring in a slot that is still empty. It is the same
  button with the same events, so nothing about the keyboard path changed.
- `StatifierBlocks.Editor.Slot` stamps `data-empty`, and
  `StatifierBlocks.Editor.BlockNode` stamps `data-container` and
  `data-arrangement`, on the markup a host may style against.
- The collapsed drawer's strip reads as a label and a quantity: a small-caps
  title with letter-spacing, and the count as a chip carrying a bare number
  rather than a parenthesised one inline with the title.

### Removed

- The stub exit tick a rail drew below itself in CSS. The exit edge is
  measured and drawn now, and a fixed-length mark beside it was a second claim
  about the same thing that pointed somewhere else.

### Fixed

- Leaving the scope around a `core.wait` cancels the wait's delayed timer, so
  a wait abandoned before its duration elapses no longer leaves an armed timer
  behind in a durable host.
- A `core.subchart` outcome-routing condition is no longer attributed to the
  `outcomes` config field, so a chart finding landing inside it reports
  `fault: :package` with no `config_key` rather than blaming the author for
  bytes the compiler composed.

## [0.3.0] 2026-08-29

Charts get more shapes to compile into. This release adds four emitters to the
`core.*` vocabulary - `core.subchart`, which runs another chart and routes on
the outcome the child reported; `core.foreach`, a container whose body runs
once per item of a datamodel list; scope-correct cancellation for a delayed
`core.send`; and `core.parallel`'s `complete: "first"`, which finishes the
block at the first lane's completion and exits the losing lanes. The compiler
gains two root-document options, `terminate:` and `declare:`, a typed
datamodel index that refuses a document reading a path the host declared
sensitive, and predicator duration strings wherever a duration is typed. In
the editor, connectors graduate out of the spike, a default icon set ships so
a host needs no asset pipeline to get one, and the shell is laid out as the
arrangement record describes it. This is a minor bump because every block's
conventional `<final>` moves from `s_<block>__done` to `s_<block>__o_done`:
the compiled bytes of every outcome-bearing chart move with it, so a host that
stores compiled charts or provenance maps recompiles them, and a chart-level
position saved against the old bytes no longer resolves.

Dependency floor: unchanged - `statifier ~> 2.2` and `predicator ~> 9.0`.

### Added

- A `core.subchart` block type: a step that runs another chart and waits,
  routing `done.invoke` on the outcome the child reported
  (`_event.data.outcome`) to one slot per declared outcome, with
  `core.invoke`'s `on_error` slot unchanged for a failed invocation.
- A `core.foreach` block type: a container whose `body` runs once for each
  item of a datamodel list, compiled as a plain SCXML loop - a per-loop cursor
  and a snapshot of the list taken once on entry, the `item_as` and `index_as`
  bindings re-assigned from that snapshot on each pass, and the body compiled
  once with an internal loop-back transition. Iteration ends on the
  out-of-bounds read, `snapshot[cursor] === undefined`.
- `core.parallel` accepts a `complete` config key choosing when the block is
  done: `"all"` (the default, and what a document stored before the key reads
  as) keeps the shipped rule, and `"first"` compiles the racing rule - one
  transition per lane on the `<parallel>` element itself, taken on that lane's
  own completion event and targeting the block's done final, so the block
  finishes at the first lane's completion and the engine exits the losing
  lanes with their `<onexit>` content and one `CancelInvoke` per live
  invocation.
- Block types may declare `outcomes/1`, an optional callback returning ordered
  `{name, label}` pairs for the ways a block can finish; a type that does not
  export it has exactly one outcome, `done`, and behaves as before.
- A child summary in the compiler context carries an `outcomes` field: the
  child's declared outcomes in declaration order, each with the `<final>` it
  compiled to and the completion event a parent wires on. It is never empty.
- `StatifierBlocks.Compiler.compile/3` accepts `terminate: true`, which emits
  one top-level `<final>` per root-block outcome with no `<donedata>`, so a
  compiled root document reaches `:done` when its root block completes;
  without it a root document never terminates, and passing it together with
  `child_use: true` is refused with an `:emit` finding.
- `StatifierBlocks.Compiler.compile/3` accepts `child_use: true`, which
  compiles a document for use as another chart's child: the emission gains one
  top-level `<final>` per outcome the root block declares, carrying that
  outcome name as done data, so a parent session can see which way the child
  finished.
- `StatifierBlocks.Compiler.compile/3` takes a `:declare` option - a list of
  `{id, expr}` pairs - so a host can declare the `<data>` roots its root
  document assigns to and guards on, hoisted ahead of block-declared roots
  into the chart's single `<datamodel>`.
- `Compiler.compile/3` takes a `:datamodel` option and refuses a document that
  reads a path the host declared `sensitive?: true` into a position the chart
  evaluates against the datamodel; with no datamodel supplied nothing is
  produced.
- A block type may now contribute declared `<data>` roots to the chart:
  `StatifierBlocks.Compiler.DeclaredRoots.declare/2` emits a declaration among
  the block's own children and the compiler lifts every one of them into a
  single top-level `<datamodel>`, in document order. A document that declares
  no roots emits no `<datamodel>` element, so charts compiled before this
  change are byte-identical.
- A new Emit-stage finding, `:duplicate_binding`: a declared root whose name a
  block it sits inside already declares is refused against the declaring block
  and the config field the name was typed into, because early binding makes
  both roots global and the inner one would silently overwrite the outer.
- The compiler refuses a block type that declares a malformed or duplicated
  outcome name with an `:invalid_outcome` Emit finding, against the block
  whose type declared it.
- A chart-stage finding for an expression the author typed carries
  `config_value_span`, the byte range of the offending sub-expression within
  that config value, so an editor can underline the sub-expression rather than
  the whole field.
- `StatifierBlocks.Predicates.Datamodel` indexes a datamodel document - the
  typed, three-scope declaration sb ADR-0006 defines - into a path/entry
  index: the type of a path, the entries under a prefix, whether a path is
  declared, and the record's one total derivation of the declared-path set.
  The index is advisory; an undeclared path is unknown, never wrong.
- `StatifierBlocks.Datamodel.declared_paths/1` accepts such a document as a
  fourth shape, alongside `nil`, a list and a `MapSet`, and projects it
  through that derivation. A document declaring no entries normalizes to the
  empty set - a host claim - rather than to `nil`, which stays reserved for no
  datamodel at all.
- The editor renders the arrangement ADR-0005's shell amendment records: a
  palette, canvas and inspector across three columns with a full-width drawer
  row beneath them, at container-query breakpoints of 1280, 1024, 900, 780 and
  640.
- A canvas toolbar with stepped zoom, Fit width, Fit active, and the
  document's block count and depth.
- The inspector is tabbed - Config, Findings and Condition - where Findings is
  the selected block's own and Condition reads the per-arm predicator source.
- A drawer that is never open-or-gone: collapsed it is a strip carrying a
  title and the document's table count, and opened on a block with no table it
  shows an index of the blocks that have one.
- `fixtures`, an assign carrying `%{block_id => [TruthTable.t()]}`, is what
  the drawer's truth-table tab reads; with none supplied the drawer is still
  present and reads 0.
- `drawer_height` and `on_drawer_resize` are the host's seam for the drawer's
  resizable height, which the host remembers per viewer.
- A `:header` slot the host fills with the outer header - document identity,
  the switcher, the theme control, compile and publish - which this package
  now explicitly does not draw.
- Below 780 the palette collapses to a strip that opens as a sheet.
- `StatifierBlocks.Shell` exposes the shell's arrangement as pure functions -
  the zoom ladder, the document metrics, the drawer's five states, and which
  of a block's fields are conditions.
- Three tier-2 theme tokens: `--sb-palette-width`, `--sb-inspector-width` and
  `--sb-drawer-height`.
- The editor draws connectors. Adjacency inside a slot, a container's entry,
  the fan and rejoin around a container arranged side by side, and a rail's
  exit are rendered as SVG in the LiveView tree, derived from the document's
  shape rather than authored.
- A second JavaScript entry point, `statifier_blocks/measure`, exporting the
  `StatifierBlocksMeasure` LiveView hook. Its whole job is measurement: after a
  render it reads the boxes the browser laid out for the anchors the server
  stamped and pushes them, and it issues no commands and mutates no DOM. A
  host that wants connectors adds one import; a host that does not gets the
  editor it had before, minus the drawn connectors.
- `StatifierBlocks.Connectors`: the connector geometry as pure functions from
  measured rectangles to SVG path data, outside the Phoenix guard, so a host
  can route its own connectors and a test can assert them without a browser.
- Three `--sb-*` tokens for the connector layer: `--sb-edge`,
  `--sb-edge-interrupt` and `--sb-edge-width`.
- `StatifierBlocks.Editor.Icons`, a default icon set the editor uses when the
  host passes no `icon` component. Inline SVG for the eleven names the core
  block types declare, with no font, no CDN and nothing to register in a
  host's asset pipeline. Every glyph paints with `currentColor` and fills its
  tile, so `--sb-block-accent` and `--sb-block-accent-tint` still decide the
  colour and a per-block-type `accent_token` still moves a type's tile with
  its stripe.
- Palette entries render their icon. The `icon` assign the editor passes the
  palette browser was declared and never rendered, so no host could put an
  icon on a palette row; a type now looks the same in the palette as on the
  card the pick produces.
- A slot declaring `slot_style: :failure` renders in its own vocabulary - a
  solid error-family edge, its own `sb-slot--failure` class, and an ordinary
  flow edge where an interrupt rail draws a dashed escape.
- `StatifierBlocks.ViewModel.exit_edge/1` says which edge vocabulary a slot's
  exit is drawn in, and the editor stamps it as `data-exit-edge`.
- The editor takes an optional `datamodel` assign - the paths the host
  declares - and reports a config field whose declared datamodel path is not
  among them as an `:info` finding in the findings panel; with no datamodel
  supplied nothing is produced.
- Block types may declare a config field with `datamodel_path?: true`, saying
  its value is a path into the host's datamodel; `core.assign`'s `path` field
  carries it.
- `StatifierBlocks.Core.Parallel.join_label/1`, declared on the type's palette
  entry, so a renderer draws "continue at first" or "continue when all" from
  the block's config without learning the type's name.
- `StatifierBlocks.DurationInput` reads a typed duration for that control,
  accepting exactly what `StatifierBlocks.Core.Duration` compiles and naming
  the limit a refused value hit.

### Changed

- Every block's conventional `<final>` moves from `s_<block>__done` to
  `s_<block>__o_done` and now raises `done.outcome.<state id>.done` on entry,
  so compiled SCXML moves for every document; a host that stores compiled
  charts or provenance maps recompiles them, and a chart-level position saved
  against the old bytes no longer resolves.
- `Compiler.compiler_version/0` (and every compilation record's
  `compiler_version`) moves to `0.3.0` with the package, per ADR-0004
  decision 6, and is the third input to the byte-determinism guarantee: this
  release is where the outcome-final byte movement is recorded.
- `core.send` now emits its `<send>` with `id="<the block's state id>__send"`,
  so a delayed send can be named after it is armed.
- A delayed `core.send` is now cancelled by its scope: the compiler emits
  `<cancel sendid="..."/>` in the `<onexit>` of the nearest enclosing
  `<state>`, so a pending send does not outlive the sequence, group, region or
  lane that armed it. Charts containing a delayed `core.send` change bytes;
  every other chart is unchanged, `core.wait` timers included.
- `core.wait` accepts a predicator duration string (`1h30m`, `2d`, `3d8h`) as
  well as ISO-8601, stores whichever spelling the author typed, and compiles
  it to the emitted `delay` attribute; its refusal message names both
  spellings.
- `core.wait`'s declared `duration` default is now the predicator string `1h`
  rather than the ISO-8601 `PT1H`, so a newly inserted block starts from the
  spelling an author types. Both spellings stay accepted and each compiles to
  the same `delay` attribute, so no chart's emitted SCXML changes and no
  stored document has to be retyped.
- A `:duration` config field renders as one text control taking predicator
  duration strings, with `30s`, `15m`, `1h30m`, `2d` and `3d8h` shown beside
  it; ISO-8601 is still accepted and the author's string is stored verbatim.
- An empty `:duration` field omits its config key rather than storing an empty
  string, so a cleared field and a never-set field are the same value.
- An icon entry that declares no icon renders no tile, rather than an empty
  one, and a host's `icon` component is never called with a `nil` name.
- An icon name the shipped set does not have renders a neutral mark with the
  name in `data-icon`.
- A `slot_style` this editor does not recognize renders as an ordinary body
  slot instead of reaching the markup unresolved; its children are still
  rendered, still selectable and still saved.

### Removed

- `StatifierBlocks.Editor.Field.units/0`, `format_duration/2` and
  `parse_duration/1`, which served the retired value/unit control. Call
  `StatifierBlocks.Core.Duration.to_iso/1` to canonicalise a stored duration.

### Fixed

- The editor no longer renders a `U+25A1` white square in every icon tile when
  the host passes no `icon`. Passing one still overrides every tile, on the
  canvas cards and the palette rows alike.
- A delayed `core.send` in the body of a `core.group` that carries interrupt
  rules is now cancelled when the group is abandoned. Its `<cancel>` is
  emitted in the body region's `<onexit>` rather than the group's own, and
  abandoning the group exits the region without exiting the group, so the old
  placement never fired.

### Known limits

- A `core.foreach` list holding a `nil` item iterates to its end rather than
  stopping at it: `===` is strict, so only an out-of-bounds read is
  `undefined`.
- Two `core.foreach` blocks in one document may not bind the same name, even
  when neither is inside the other; the second is refused with a duplicate-id
  finding on its `item_as` field.

## [0.2.0] 2026-08-29

The editor ships. This release graduated the authoring spike into the
package: `StatifierBlocks.Editor` renders from `assets/` with a documented
`--sb-*` theming surface, a drag marks the slots that accept a block and can
say why a slot refused, and a host registers its own block types through
`Palette.from_modules/2`. The `core.*` vocabulary grows by `core.invoke`,
`core.raise`, `core.assign` and `core.send`; the compiler now evaluates
predicator conditions, refuses slot-arity and undeclared-slot violations, and
adapts its findings into the shape the editor renders.

Dependency floor: `statifier ~> 2.2` (was `~> 2.0`); `predicator ~> 9.0` is
now a direct dependency.

### Added

- `core.raise` raises an event for an enclosing group's interrupt rules.
- `core.assign` writes a literal to a datamodel path.
- `docs/theming.md`: the theming guide - the three tiers of the `--sb-*`
  surface, the scheme token, per-block-type accents, and a complete host
  theme that sets custom properties and nothing else.
- `core.send` sends an event, now or after a delay.
- A block type may declare `slot_outcome_key` in its palette entry, naming the
  config key the blocks in one of its slots carry their outcome under, so a
  renderer can route an interrupt rule's escape without branching on a type
  name; the declaration reaches the view model as `Slot.outcome_key` and the
  resolved value as `Node.outcome`.
- `StatifierBlocks.BlockType.slot_outcome_key/2` and
  `StatifierBlocks.BlockType.outcome_name/2` read that declaration totally: a
  malformed declaration or value is refused rather than repaired, and reads as
  no declared outcome.
- A block type may declare `accent_token`, the NAME of a `--sb-*` property,
  and the editor stamps it on that type's cards and palette rows. Two rules
  in the stylesheet read it; no rule and no module names a block type
  (ADR-0005 amendment 14d, consumption side).
- `StatifierBlocks.Finding.severity_class/1`, and `:info` as a third
  severity for advisory findings (decision 11, amended 2026-08-29). Nothing
  emits one yet; only `:lint` may.
- A form whose config the gate has not accepted names the fields that are
  outstanding, says why nothing is stored, and offers "Discard edits". A
  draft was never a command, so it cannot be undone - it can only be thrown
  away, and that gesture had nowhere to live.
- `:expression` and `:duration` controls carry a placeholder. A bare
  `:string` still carries none: there is nothing a type that wide can
  suggest.
- A theme audit test over the stylesheet, failing in both directions: a
  `var(--sb-*)` with no declaration, and a declared token no rule reads (14e).
- `StatifierBlocks.SlotValidation`, a palette-aware whole-document check for
  a block's declared slots (`:undeclared_slot`) and each declared slot's
  arity (`:slot_arity_violated`).
- `StatifierBlocks.Predicates` evaluates a condition expression against a
  binding context through predicator, returning a boolean or a tagged error.
- `StatifierBlocks.Predicates.TruthTable` builds a checked truth table over
  fixture rows, applying first-match-wins arm ordering.
- `StatifierBlocks.Finding.from_compiler/2` and `from_compiler_all/2` adapt a
  compiler finding into the presentation shape the editor renders, so a host
  can route compile findings through `ViewModel.build/3`.
- `StatifierBlocks.Assignability.seam_reason/4`, `finding_reason/2` and
  `seam_reasons/3` name why a data-flow seam came out the way it did:
  `:not_assignable` and `{:fixable_by, block_id}` for a refusal, and
  `:source_untyped` / `:target_untyped` / `:both_untyped` for a seam that
  passed only because a block declared no type. `seam_reasons/3` is how a
  host finds the parts of its palette it has not typed yet.
- `StatifierBlocks.Assignability.target_verdicts/4` returns every position
  `valid_targets/4` enumerates with its full verdict, and
  `StatifierBlocks.Edit.Targets.slot_verdicts/3` projects those to slots -
  the accepting ones and the reason each refusing one gives.
- The editor stamps a refused slot's reason as `data-drop-reason` beside
  `data-drop`, so a hover affordance can explain a refusal with no
  round-trip and no JavaScript.
- A `core.invoke` block type: it names an invoke type for the host to run,
  sends datamodel values along as `<param>`s, writes the result where its
  `assign_to` names, and takes an optional `on_error` subtree entered on a
  permanent invoke failure.
- `StatifierBlocks.Compiler.Context.outcome_id/2` and `outcome_event/2`, for a
  block type with more than one way to finish: one `<final>` per outcome, and
  the `done.outcome.<state id>.<outcome>` event a parent wires on.
- `StatifierBlocks.Palette.from_modules/2`, the registration API a host uses
  to contribute its own block types: an ordered, explicit list of
  `{type_name, module}` registrations, with `core: true` to sit on top of
  the `core.*` vocabulary. Later entries win. It is still a value - no
  global registry, no application-configuration lookup, and no discovery
  pass.
- A palette entry may declare `badge`, a short chip for the card header, and
  `join_label`, a one-argument function of the block's config phrasing the
  join marker under a side-by-side arrangement (ADR-0002 amendment B).
  `StatifierBlocks.BlockType.badge/1` and `join_label/2` read them.
- Both readers are total and **refuse rather than repair**: a chip that is
  blank, carries a newline or tab, or runs past 24 characters is dropped,
  not clipped, and a `join_label` that raises degrades to the editor's own
  word rather than taking the canvas down.
- `accent_token`, `badge` and `join_label` are admitted keys of
  `t:StatifierBlocks.BlockType.palette_entry/0`.
- The README carries a worked host example - a `myapp.risk_hold` block type
  registered beside the core vocabulary, with a badge and an accent token -
  and it is executed on every build rather than trusted.

### Changed

- `Compiler.compiler_version/0` (and every compilation record's
  `compiler_version`) moves to `0.2.0` with the package, per ADR-0004
  decision 6: a record compiled by 0.1.0 identifies itself as such.
- `--sb-drop-ok-border` moves from `#2f9e5f` to `#2c945a`. The outline that
  says a slot accepts a drop was 2.93:1 on the sunken surface, under the 3:1
  a mark carrying information is held to; the tint follows it.
- `predicator` is now a direct dependency (`~> 9.0`), because
  `StatifierBlocks.Core.Duration` calls `Predicator.Duration.parse/1`. It
  already resolved transitively through `statifier`, so the resolved version
  does not move; naming it records the call.
- The editor's stylesheet carries a scoped reset, and every selector in it
  matches the container through `:where(.sb-editor)` so a component rule
  always wins (ADR-0005 amendment 14b).
- `--sb-color-scheme` is declared and read as `color-scheme` on the editor's
  own container, so the parts of a control the browser paints - a `<select>`'s
  drop-down, the scrollbars, the caret - follow the theme (14a).
- The `--sb-*` surface gains the space, type and shape scales, a third text
  step, a strong border, status tints, the drag seam's drag-time height, and
  the canvas sizing constants that were literals in rules.
- A `:secondary` and a `:failure` slot are both placed as attached rails,
  and a container declaring either is drawn as a boundary box - the rail
  partition, not the `:secondary` partition (amendments 10c and 10h).
- `--sb-drop-no-opacity` is **retired**. A drag now marks the slots that
  accept the block and leaves the rest alone rather than dimming them; the
  disabled-control opacity it doubled as is `--sb-disabled-opacity`.
- The compiler now refuses a document whose slots violate their declared
  arity or name a slot the block type does not declare, instead of silently
  dropping those children from the emission.
- Reasons change no verdict: `:unknown` stays permissive in both positions,
  `Assignability.validate/3` reports exactly the findings it did before, and
  neither finding tuple gained a field.
- `slot_style` admits a third value, `:failure`, for a slot whose children
  are an in-band continuation taken on a bad outcome; `core.invoke` declares
  it for `on_error`.
- The role namespace beginning `o_` is reserved for outcome finals;
  `Context.role_id/2` now refuses such a role with a `:reserved_role` finding.

### Fixed

- The editor's root rule sets `font-family` rather than the `font`
  shorthand, so `--sb-font` reaches the editor. `font: <family-list>` is not
  a valid shorthand, so the whole declaration was dropped and the editor's
  text did not inherit the host page's font as the token promised.

## [0.1.0] 2026-08-27

First release: the authoring layer above the
[statifier](https://hex.pm/packages/statifier) statechart engine. A block
document is the source of truth - a tree of typed blocks, each with a declared
shape - and it compiles one way to SCXML plus a provenance map that points a
runtime position back at the block that produced it. Block types are
host-pluggable: a host registers the types its own domain needs, and the
compiler and the editor work off that registry rather than a closed built-in
vocabulary. The `core.*` structural vocabulary, the compiler, the edit algebra,
and the LiveView editor shell all ship here.

The `Changed` entries below describe the shape of callbacks and metadata as
they stand at this first release; there is no earlier published version to have
changed from.

### Added

- `StatifierBlocks.Document.validate/1` checks a block document's structure:
  schema version, envelope shape, per-block shape, and document-wide id
  uniqueness.
- `StatifierBlocks.Document.to_json/1` encodes a document to ADR-0001's
  deterministic canonical JSON: sorted object keys, no insignificant
  whitespace, empty `slots`/`config`/`metadata` omitted, no floats.
- `StatifierBlocks.Document.content_hash/1` returns a `"sha256:" <> hex`
  document identity over `to_json/1`'s canonical bytes.
- `StatifierBlocks.Document.from_json/1` decodes canonical JSON back into a
  document, structurally and registry-free: unknown block types decode
  successfully, and every refusal is one of ADR-0001's typed error arms.
- `StatifierBlocks.BlockType` behaviour: the nine-callback authoring-time
  extension seam (ADR-0002), five required (`slots/1`, `config_schema/1`,
  `validate_config/1`, `current_version/0`, `emit/2`) and four optional
  (`io/1`, `migrate_config/2`, `fixtures/0`, `palette_entry/0`).
- `StatifierBlocks.Palette`: a caller-supplied `type_name => module` value
  (ADR-0002 decision 2), with `new/1` to build one and a total `fetch/2` that
  returns `{:ok, module}` or `{:error, {:unknown_block_type, type_name}}` and
  never raises (ADR-0002 decision 3).
- `StatifierBlocks.Palette.resolve/2`: resolves a block through the palette and
  migrates its config in memory when the stored `type_version` is below the
  type's `current_version/0` (ADR-0002 decision 8). Migration is applied to the
  returned struct only and never written back to a document; a stored version
  above `current_version/0` hard-errors as
  `{:error, {:block_type_too_new, id, version}}` rather than reading
  best-effort, and a failing or missing `migrate_config/2` surfaces as
  `{:error, {:migration_failed, id, reason}}`.
- The `core.*` structural block types (ADR-0002 decision 10), one
  `StatifierBlocks.BlockType` module each: `StatifierBlocks.Core.Sequence`,
  `.Group`, `.Branch`, `.Parallel`, `.Wait`, `.ResumableGroup` and `.OnEvent`.
  `core.branch` derives one slot and one `:expression` field per declared arm,
  `core.parallel` one slot per declared lane, and each type is the authority on
  its own config through `validate_config/1`.
- `StatifierBlocks.Palette.core/0` and `StatifierBlocks.Palette.core_types/0`:
  the core vocabulary as a palette, and as the plain `type_name => module` map
  a host merges its own entries into.
- Structural placement through ADR-0003 decision 3 kind tags: `core.on_event`
  declares `kinds: [:interrupt_handler]` and the group types accept only that
  kind in their `interrupts` slot, so an interrupt handler is admitted there
  and refused everywhere else, and an ordinary step is refused there - in both
  directions, from the declarations alone, with no special-cased rule.
- `fixtures/0` on `core.branch` (an arm condition evaluated against two
  datasets) and `core.on_event` (one example event payload). The bundle shape
  follows an amendment to ADR-0002 decision 9 that is not yet accepted, and is
  documented as provisional until it is.
- `StatifierBlocks.Assignability`: the one decision function for whether a
  block may land in a slot, checking structural admission by kind tag and
  data-flow compatibility by type-expression identity plus an optional
  host-supplied widening relation (ADR-0003). `check/5` decides a single
  candidate position; `valid_targets/4` lists every position a candidate may
  occupy in a document; `validate/3` reports every finding already present in a
  document; `inbound_type/4` and `assignable?/3` are the two primitives both
  are built from.
- `StatifierBlocks.Assignability.Relation`: the behaviour a host implements to
  widen data-flow compatibility beyond exact type-expression identity. A host
  module can only grow the accepted set, never shrink it.
- `StatifierBlocks.Palette` gains an `assignability` field naming the host's
  `Assignability.Relation` module, set via
  `Palette.new(types, assignability: MyApp.Blocks.Types)`. Defaults to `nil`,
  meaning no widening relation is declared; existing calls to `Palette.new/1`
  are unaffected.
- `StatifierBlocks.Compiler`: the one-way compile (ADR-0004 decisions 1-4,
  6-7). `compile/3` is a total function of `{document, palette}` returning
  `{:ok, %StatifierBlocks.Compiled{}}` or
  `{:error, [%StatifierBlocks.Compiler.Finding{}]}` - no process state, no
  clock, no IO, and no arm that raises. The pipeline runs Document, Resolve,
  Config and Emit, stopping at the first stage that produces errors and
  reporting every error from that stage.
- `StatifierBlocks.Emission`: the structural representation of one SCXML
  subtree a block type returns from `emit/2`, with `element/3` and the
  `child_ref/1` placeholder the compiler splices its children into.
- `StatifierBlocks.Compiler.Serializer`: the deterministic serializer.
  Attributes sorted, one canonical empty-element form, no incidental whitespace
  at all. It is identity-bearing code - chart identity hashes source bytes
  (st-ADR-0052) - and `serializer_test.exs` now enforces the whitespace
  sensitivity ADR-0004 decision 6 named and left unenforced.
- `StatifierBlocks.Compiler.StateId`: `state_id/1`, `state_id/2`,
  `unstate_id/1` and `done_event/1`. State ids derive from block ids
  (`"s_" <> block_id`, `"__" <> role` for an auxiliary state), so they are
  unique, invertible and total over generated states.
- `StatifierBlocks.Compiler.Context`: what a block type is entitled to know
  while emitting - its own ids, the document id, its children's summaries
  (block id, state id, done event) and the role-minting function. No palette,
  and no child's emitted SCXML.
- `StatifierBlocks.Compiled` and `StatifierBlocks.CompilationRecord`: the
  artifact, and the join between document identity and chart identity.
  `chart_name` carries the document id and `chart_version` stays `nil`, so a
  revision bump or a metadata-only edit still matches the identity a running
  session holds.
- `StatifierBlocks.Core.Emit`: the SCXML shapes the `core.*` vocabulary
  compiles to, and the builders a host block type follows to compose with them.
- `StatifierBlocks.Provenance`: the map from generated SCXML back to the blocks
  that produced it (ADR-0004 decision 5). Keyed by state id for highlighting a
  running session's configuration, and by byte span for routing findings that
  carry no element reference. `owner_at/2`, `owner_of_state/2`,
  `owners_of_states/2`, and canonical `to_json/1` / `from_json/1` so a host can
  store the map beside the chart.
- `StatifierBlocks.Compiled` now carries all five of ADR-0004 decision 1's
  fields: `provenance`, `invoke_types` and `warnings` join `scxml` and
  `record`.
- `invoke_types` publishes the sorted set of invoke types the chart emits,
  unconditionally, so a host can compare it against its `Statifier.Session`
  registration at deploy time (ADR-0004 decision 8).
- `Compiler.compile/3` accepts `:known_invoke_types`, an opt-in lint that
  **warns** - never errors - for every emitted invoke type absent from the set
  the caller believes will be registered.
- `Compiler.compile/3` accepts `:entry_type`, ADR-0003 decision 4's
  caller-supplied context, which the new Structure stage passes to
  `StatifierBlocks.Assignability.validate/3`.
- The compiler now runs a **Structure** stage (assignability) and a **Chart**
  stage (statifier's own pipeline over the generated bytes), and maps every
  upstream finding back to the block that caused it.
- `StatifierBlocks.Emission.attributed_to/2`, `from_config/2` and
  `attribute_from_config/3`: the hints a block type leaves so a finding lands
  on the block an author would recognise, and on the config field they typed
  into.
- `StatifierBlocks.Edit`: the editor's command algebra - insert, remove, move,
  and update-config - as a purely structural, invertible rewrite over a
  document with no palette involved (ADR-0005). `apply/2` applies one command
  and returns both the new document and the command that undoes it;
  `check_config/3` is the separate config-validity gate one layer up.
- `StatifierBlocks.Edit.History`: undo and redo over `Edit` commands.
  `commit/4` is the one funnel a host calls - it runs `Edit.check_config/3`
  before `Edit.apply/2`, then pushes the inverse and clears the redo stack, so
  invalid config never reaches the document on any path, undo and redo
  included.
- `StatifierBlocks.Edit.Targets`: `droppable_slots/3` and
  `droppable_slots_for/3`, which slots would accept a dragged block, at slot
  granularity rather than gap granularity, built as a reduction of
  `StatifierBlocks.Assignability.valid_targets/4`.
- `StatifierBlocks.Finding`: the presentation finding ADR-0005 specifies,
  anchored to a block, a slot, or a config field so the editor knows where to
  render it. Distinct from the existing `StatifierBlocks.Compiler.Finding`,
  which serves the compile pipeline.
- `StatifierBlocks.ViewModel`: the structure the editor actually renders,
  derived from a document, a palette, and a list of findings. Resolves and
  normalizes every block's slots, form fields, and palette presentation
  metadata, and routes every finding to the position that renders it.
- `StatifierBlocks.Editor`: the LiveView editor shell (ADR-0005). A
  `LiveComponent` a host embeds over a `%Document{}` and a `%Palette{}`; it is
  the only stateful module in the package's rendered half, and everything it
  does is translate a `phx-` event into one of `StatifierBlocks.Edit`'s four
  commands. Drag is two round-trips - one at `dragstart` to enumerate valid
  slots, one at `drop` - with zero per hover, because validity reaches the
  client as `data-drop` markup rather than as client-side logic.
- `StatifierBlocks.Editor.Canvas`, `.BlockNode`, `.Slot`, `.ConfigForm`,
  `.Field`, `.PaletteBrowser`, `.Findings`: the function components the shell
  renders, each independently renderable in a test. `BlockNode` and `Slot`
  recurse into each other, and there is no per-block-type component: a block
  type's `layout` and `slot_style` presentation metadata is the only thing that
  distinguishes a group from a set of lanes.
- `assets/js/statifier_blocks.js`: the package's entire client-side surface,
  one hook named `StatifierBlocksDrag`, shipped as source. A host adds
  `"statifier_blocks": "file:../deps/statifier_blocks"` to its
  `assets/package.json` and imports the hook in `app.js`; this repository
  bundles nothing and has no Node toolchain.
- `assets/css/statifier_blocks.css`: one stylesheet of structural CSS and no
  visual opinion beyond it. Every class is prefixed `sb-`, every color, space,
  radius and drag treatment is a `--sb-*` custom property with a default, and
  every top-level component takes a `class` attr appended to its own.
- A headless CI job that resolves the dependency tree with `phoenix_live_view`
  absent, compiles it with warnings as errors, and runs the non-LiveView suite
  - the acceptance property that makes the optional dependency's guard
  trustworthy rather than decorative. `STATIFIER_BLOCKS_HEADLESS=1` reproduces
  it locally without disturbing the ordinary build.
- `StatifierBlocks.BlockType`: a config field declaration may now carry an
  optional `value_path`, a list of keys and list indexes from the config root
  down to the value it edits (ADR-0002 decision 7, amended 2026-08-27). A
  declaration without one behaves exactly as before - its `key` addresses
  `config[key]`. The `key` remains the field's identity in both cases: the DOM
  id, the form param name, and what a `{:config, block_id, key}` finding
  anchors to.
- `StatifierBlocks.BlockType.value_path/1`, `fetch_value/2` and `put_value/3`:
  the reader and writer that resolve a declaration to a path and then read or
  write through it. `value_path/1` answers `[key]` for a declaration that
  declares none, so a caller never branches on which case it has.
  `fetch_value/2` is total and answers `:error` for a path that does not
  resolve. `put_value/3` writes the last segment whether or not a value was
  already there - an arm with no condition yet is exactly the one an author is
  about to type into - but never invents an intermediate map or list a block
  type did not write.
- `StatifierBlocks.ViewModel.Field` carries `value_path`, and
  `ViewModel.Field.value_path/1` reads it with the same `[key]` default.

### Changed

- `c:StatifierBlocks.BlockType.io/1`'s return type is
  `StatifierBlocks.Assignability.io/0` instead of `term()`. Every core block
  type already returns a value of this shape; a custom block type implementing
  `io/1` should confirm its return value conforms.
- All seven `core.*` block types implement `emit/2` for real; the
  `{:error, {:not_implemented, block_id}}` placeholder and
  `StatifierBlocks.Core.Config.emit_deferred/1` are gone.
- `c:StatifierBlocks.BlockType.emit/2` is narrowed from
  `(Block.t(), term()) :: {:ok, term()} | {:error, term()}` to
  `(Block.t(), StatifierBlocks.Compiler.Context.t()) :: {:ok, StatifierBlocks.Emission.t()} | {:error, StatifierBlocks.BlockType.emit_error()}`.
  A host block type that was returning something else now has a type to conform
  to.
- `StatifierBlocks.Compiler.Finding` gains `path`, `severity`, `fault` and
  `code`. `fault` is `:author` when a document edit fixes the finding and
  `:package` when it is a bug in this package or a host's block type - which is
  what lets an editor say "this cannot be fixed here" rather than blaming the
  author for a generated state id.
- Findings from every stage come back in document order over blocks rather than
  in the order a stage happened to collect them.
- A bad `:expression` config field now surfaces as an `:author` finding naming
  the arm's config key, rather than as an unrouted upstream error.
- `c:StatifierBlocks.BlockType.palette_entry/0`'s return type is
  `StatifierBlocks.BlockType.palette_entry/0` instead of `map()`. Every core
  block type already returns a value of this shape; a custom block type
  implementing `palette_entry/0` should confirm its return value conforms.
- `phoenix_live_view` is a declared optional dependency at `~> 1.0`, matching
  statifier_ui's floor. Every module under `StatifierBlocks.Editor.*` is
  compiled behind `Code.ensure_loaded?(Phoenix.LiveView)`, and no module
  outside that namespace references Phoenix - so a host that only compiles
  documents adds no Phoenix dependency and compiles no editor code.
- The hex package's `files:` list includes `assets`. The hook and the
  stylesheet ship as source, and source that is not in the tarball is not
  public API.
- Every illustrative example in the package - ADR worked examples, doc
  examples, and the shipped fixture bundles - uses one of the family's two
  canonical example domains: credit-card authorization and capture, or a signup
  wizard with A/B testing.
- `StatifierBlocks.Core.Branch.fixtures/0` ships budget-decision datasets
  (`"approved"` / `"declined"`) and the expression `"budget_remaining > amount"`.
  A host rendering the bundle in a palette panel sees those names.
- The arm-slot and lane-name validation messages on `core.branch` and
  `core.parallel` name `"arm_approved"` and `"capture"` as their exemplars.

### Fixed

- A `core.branch` arm's condition is now readable and editable in the editor.
  `Core.Branch.config_schema/1` keys one `:expression` field per arm by the
  arm's slot name, but the condition is stored at `config["arms"][i]["cond"]`;
  the form previously read and wrote the slot name as a top-level config key,
  so every branch condition rendered empty, no edit to one reached the arm, and
  a junk `config["arm_approved"]` accumulated beside it. Each per-arm field now
  declares `value_path: ["arms", i, "cond"]`, and `StatifierBlocks.ViewModel`
  and `StatifierBlocks.Editor.ConfigForm` read and write through it. `i` is the
  arm's index in the **stored** list rather than its index among the
  well-formed ones, so a good arm below a malformed one still addresses its own
  condition while an author is mid-edit.
- `StatifierBlocks.Editor`'s `field-list-add` and `field-list-remove` events
  read and write the rows through the field's `value_path` as well, rather than
  through the top-level key. A key naming no field in the selected block's
  schema now edits nothing, matching the guard `ConfigForm.decode/3` already
  applied.
- `StatifierBlocks.Assignability.check/5` and `valid_targets/4` no longer raise
  a `MatchError` when the candidate is the document root.
  `Document.fetch_path/2` answers `{:ok, []}` for the root, and the
  vacated-seam check now reads that as what it is - the root occupies no slot,
  so it leaves no seam behind - instead of calling `List.last/1` on the empty
  path. `StatifierBlocks.Edit.Targets.droppable_slots/3` answers `[]` for the
  root rather than crashing, so a caller no longer has to guard around it.

[0.44.1]: https://github.com/riddler/statifier_blocks/releases/tag/v0.44.1
[0.44.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.44.0
[0.43.1]: https://github.com/riddler/statifier_blocks/releases/tag/v0.43.1
[0.43.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.43.0
[0.42.1]: https://github.com/riddler/statifier_blocks/releases/tag/v0.42.1
[0.42.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.42.0
[0.41.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.41.0
[0.40.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.40.0
[0.39.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.39.0
[0.38.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.38.0
[0.37.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.37.0
[0.36.1]: https://github.com/riddler/statifier_blocks/releases/tag/v0.36.1
[0.36.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.36.0
[0.35.1]: https://github.com/riddler/statifier_blocks/releases/tag/v0.35.1
[0.35.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.35.0
[0.34.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.34.0
[0.33.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.33.0
[0.32.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.32.0
[0.31.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.31.0
[0.30.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.30.0
[0.29.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.29.0
[0.28.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.28.0
[0.27.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.27.0
[0.26.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.26.0
[0.25.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.25.0
[0.24.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.24.0
[0.23.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.23.0
[0.22.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.22.0
[0.21.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.21.0
[0.20.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.20.0
[0.19.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.19.0
[0.18.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.18.0
[0.17.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.17.0
[0.16.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.16.0
[0.15.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.15.0
[0.14.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.14.0
[0.13.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.13.0
[0.12.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.12.0
[0.11.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.11.0
[0.10.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.10.0
[0.9.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.9.0
[0.8.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.8.0
[0.7.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.7.0
[0.6.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.6.0
[0.5.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.5.0
[0.4.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.4.0
[0.3.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.3.0
[0.2.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.2.0
[0.1.0]: https://github.com/riddler/statifier_blocks/releases/tag/v0.1.0
