# Upgrading from 2.5 to 2.15.0: what a host changes

This page is for a host: the application that compiles charts, starts
executions and supplies the services a chart reaches through `<invoke>` and
`<send>`. For each release from 2.6.0 to 2.15.0 it says what a host must
change to take the release, and then what a host may start doing with it.
Where a host must change nothing, the page says NONE.
The [Changelog](../CHANGELOG.md) says what the library changed; this page
does not repeat it.

Of the releases in this range, only 2.14.0 has a Breaking entry in the
CHANGELOG. Every
"must change" from 2.6.0 to 2.10.0 is NONE. 2.11.0 changes some answers,
and its "must change" names the hosts each one reaches: a host whose
charts carry a `<send>` `<param>` with both `expr` and `location`, a host
that matched an error shape or rescued a raise the release replaces with a
named refusal, and a receiver that parsed a list or map parameter's text.
2.12.0 changes some answers too, and its "must change" names the hosts
each one reaches: a receiver of a Basic HTTP send whose `<content expr>`
evaluates to a struct, a host whose charts carry an `initial` on a
compound state with no `id` naming a target outside that state, a host
that reads the entries of a document already refused for a shared `id`,
a host that holds predicator at 9.0 to 9.3, and a chart with a `\u` or
`\U` escape in a string literal. 2.12.1 changes documentation only, and
its "must change" is NONE. 2.13.0 changes some answers, and its "must
change" names the hosts each one reaches: a host whose charts carry an
`initial` on a `<state>` with no `id` that names a missing state or sits
on a state with no children, a host that reads the entries of a document
already refused for a shared `id`, and a host that waited for a further
halted message after reporting a budget-halted session's failed send.
2.14.0 changes some answers, and its "must change" names the hosts each
one reaches: a host whose documents write one attribute twice on one
element, or that matches the validator's reasons exhaustively (the
Breaking entry); a host that cancels a budget-halted session with events
queued; and a host that waited for a further halted message after a
delayed send fired into a budget-halted session.
2.15.0 changes one answer, and its "must change" names the hosts it
reaches: a host whose charts call a function from `cond` or `expr` that
is not `In`, a predicator builtin or a function the host declares, and a
host that matches the compiler's error reasons exhaustively. 2.15.0's
CHANGELOG lists that refusal under Changed, not as a Breaking entry: it
refuses only a chart that calls a function no evaluation context holds,
a call that raised `error.execution` whenever it was evaluated.
A dependency requirement of `{:statifier,
"~> 2.5"}` already accepts every version on this page. Raise it only when
your host calls a function a later release added: each section names the
lowest requirement its functions need.

## 2.6.0

**A host must change:** NONE. A session started without `:send_types`
behaves as it did in 2.5: a `<send>` whose `type` is not built in still
raises `error.execution`.

**A host may start:** handing `<send>` types to its own processors
(ADR-0069). Requires `{:statifier, "~> 2.6"}`.

1. Write a module per type that implements `Statifier.Send.Processor`:
   `deliver/3` and `cancel/2` are required and pure; the optional
   `perform/2` does the delivery, and the optional `ioprocessors_entry/1`
   fills the type's `_ioprocessors` entry. `perform/2` may be called more than once for the same send, so
   make it idempotent. A delayed send is your processor's timer: the
   session schedules nothing for it.
2. Pass the map to `Statifier.Session.start_link/2` as
   `send_types: %{"my-type" => MyProcessor}`, and add
   `inherit_send_types: true` if the children a chart starts through
   `<invoke>` should get the same map. A map that names a built-in type
   (`"scxml"`, the SCXML Event I/O Processor URI, or `nil`) makes the
   session refuse to start with
   `{:error, {:send_types, {:built_in_types, types}}}`.
3. When your processor cannot deliver a send and the sender still exists,
   call `Statifier.Session.failed_send/3`; the chart then sees
   `error.communication` with the send's id. When the sender has finished,
   that call writes nothing and recording the miss is yours. A host
   without a session process makes the same write with
   `Statifier.Interpreter.deliver_internal/5`.
4. At publish time, refuse a chart that names a type you never registered:
   `Statifier.Send.Types.unsupported_sends/2`, called with the compiled
   chart and `Statifier.Send.Types.from_send_types/1` of the map you will
   start it with, lists each such `<send>` with its location.
5. A resumed session does not remember which processor holds a delayed
   send. Routing a `<cancel>` for a send handed over before the position
   was saved is yours, as re-arming timers after a resume already is.

The guide is the section "The `<send>` half: host-registered send types" of
[How to extend Statifier: `<invoke>` handlers and `<send>` types](extending.md).

## 2.6.1

**A host must change:** NONE. The release adds conformance cases under
`conformance/`, which is not in the Hex package.

**A host may start:** nothing new. A sibling implementation that vendors
the corpus re-vendors it at the `v2.6.1` tag; that is not a host step.

## 2.7.0

**A host must change:** NONE.

**A host may start:** checking a chart's events at publish time
(ADR-0071). Requires `{:statifier, "~> 2.7"}`.

- `Statifier.Chart.events/1` gives a compiled chart's event vocabulary:
  every event descriptor on a transition from a state some path can enter,
  as authored.
- `Statifier.Chart.check_accepts/2` compares a declared list of the event
  names the chart accepts with that vocabulary and answers
  `%{unreachable: [...], undeclared: [...]}`. `unreachable` holds the
  declared names the chart can never select on; `undeclared` holds the
  descriptors the declaration leaves out. With `nil` for the declaration
  both lists are empty. To ask whether one event name reaches the chart,
  call `check_accepts(machine, [name])` and read `unreachable`.

Both functions report and refuse nothing: which list refuses a publish is
your decision.
[Publish-time checks: every runtime refusal and its twin](publish-time-checks.md)
lists the runtime refusal each one stands in front of.

## 2.8.0

**A host must change:** NONE.

**A host may start:** comparing two compiled charts before publishing the
second, and checking whether one execution's position survives the edit
(ADR-0072). Requires `{:statifier, "~> 2.8"}`.

- `Statifier.Chart.diff/3` takes the published chart, the candidate and
  an optional `mapping:` of renamed state ids (old id to new id), and
  answers `%{class: class, reasons: reasons}` with a class of
  `:identical`, `:compatible`, `:mapped` or `:breaking`. It raises
  `ArgumentError` on an option other than `mapping:` or an invalid
  mapping.
- `Statifier.Position.compatible_at?/3` takes the two charts and one
  execution's `Statifier.Position.export/1` map and answers `true` only
  when that execution's position is untouched by the edit. Both charts
  must carry their source, which `Statifier.compile/2` keeps; a chart
  built without it answers `false`.

Neither function moves an execution, and the library calls neither.
Whether an execution moves to the new chart, and when, stays your
decision.

## 2.8.1

**A host must change:** NONE.

An `<invoke>` whose type is `http://www.w3.org/TR/scxml`, the SCXML type
URI without its trailing slash, now starts an SCXML child session; before
2.8.1 it raised `error.execution`. A handler your host registered under
`:invoke_handlers` for that exact string still receives it, since a
registered handler takes precedence over the built-in one. If a chart of
yours carries that spelling and your host relied on the
`error.execution`, that chart now starts a child instead. A host that
needs the new behaviour requires `{:statifier, "~> 2.8 and >= 2.8.1"}`.

**A host may start:** nothing new. The corpus changes follow 2.6.1's
rule: a sibling implementation re-vendors at the `v2.8.1` tag.

## 2.9.0

**A host must change:** NONE. `Statifier.MachineState` gains a
`last_selection` field, which a position blob does not carry, so a
position saved under 2.8.1 restores as before.

**A host may start:** running every publish-time check in one call.
Requires `{:statifier, "~> 2.9"}`.

- `Statifier.Publish.findings/2` takes a compiled chart and a declaration
  of the host's `send_types:`, `invoke_types:` and `accepts:`, and answers
  a list of findings, each naming its row of
  [Publish-time checks: every runtime refusal and its twin](publish-time-checks.md)
  with a `kind`, a location and its data. Like 2.7.0's functions, it
  refuses nothing: which finding refuses a publish is your decision.
- `Statifier.MachineState`'s `last_selection` reads `:selected` when the
  last external event selected a transition, `:none` when it selected
  none, and `nil` before any external event.

## 2.10.0

**A host must change:** NONE. A session that registers no send type sees
nothing new: no request is made, nothing is started, and `_ioprocessors`
holds the SCXML processor's entry alone. A `{:statifier, "~> 2.9"}`
requirement already accepts 2.10.0.

**A host may start:** the W3C Basic HTTP Event I/O Processor (ADR-0075).
Requires `{:statifier, "~> 2.10"}`.

- Register `{Statifier.Send.BasicHTTP, base_url: base}` in `:send_types`
  under both of its type strings,
  `"http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"` and
  `"basichttp"`. Each string's `_ioprocessors` entry holds one
  `"location"`: the base URL, `/`, and the session's `_sessionid`. A
  registration without `:base_url` refuses the session's start with an
  `ArgumentError`. The POSTs go through OTP's `:httpc` by default, and a
  `:transport` option names your own `Statifier.Send.BasicHTTP.Transport`;
  the package adds no dependency.
- The processor receives nothing on its own: your front answers at the
  base URL and hands each request to `Statifier.Send.BasicHTTP.decode/1`,
  which answers the event to enqueue or the reason to refuse the request.
- Every POST carries the send's deduplication key in an `scxml-send-key`
  header. A front that enqueues a request only when it has not already
  enqueued one with the same value delivers each send once; one that
  ignores the header sees at-least-once delivery.
- For a processor of your own, a `:send_types` value may now be
  `{module, opts}`: the options reach `deliver/3` and `cancel/2` under the
  plan context's `:opts` key. A processor may implement the optional
  `ioprocessors_entry/2`, which receives the type string and a context
  carrying the session id and the options, so its entry can address one
  session. A bare-module registration behaves as it did in 2.9.0.

The guide is [The Basic HTTP Event I/O Processor](basichttp.md). The
conformance corpus claims the processor's W3C documents and its case
schema gains `host.event_io_processors`; a sibling implementation
re-vendors the corpus at the `v2.10.0` tag, which is not a host step.

## 2.11.0

**A host must change:** only where one of these reaches it. A session that
registers no send type sees nothing new, and a `{:statifier, "~> 2.10"}`
requirement already accepts 2.11.0.

- A chart whose `<send>` carries a `<param>` with both `expr` and
  `location` no longer compiles: `Statifier.Validator.validate/2`, and so
  `Statifier.compile/2`, refuses it as `{:param_expr_and_location, name}`.
  Before 2.11.0 it compiled and the parameter took `location`. Drop one of
  the two attributes; keeping `location` keeps what the chart sent. A
  `<send>` `<param>` with neither attribute is refused as
  `{:param_no_value, name}`, where `Statifier.compile/2` raised
  `FunctionClauseError`.
- A fresh `Statifier.Session.start_link/2` whose `Statifier.Send.BasicHTTP`
  registration has no usable string `:base_url` answers
  `{:error, {:send_types, {:invalid_registration, type, {:missing_option, :base_url}}}}`,
  refused before any session process is spawned and so with no crash
  report. It answered `{:error, {%ArgumentError{}, stacktrace}}`. If your
  host matched the `ArgumentError` shape, match the named refusal instead.
  Every registration the processor can build its entry from still starts.
  A resume is not refused and does not change: its position carries the
  `_ioprocessors` entries it started with. `ioprocessors_entry/2` still
  raises `ArgumentError` for a direct caller.
  Corrected 2026-10-04: the named refusal is decided before the session
  process starts, so a start that also had another fault answers the
  named refusal too, where the start answered that other fault: a
  `:name` already registered (`{:already_started, pid}`), an `:invoke_handlers` value that is not a
  map (`{:function_clause, stacktrace}`), or another processor's
  `_ioprocessors` entry that is not a map (that processor's
  `ArgumentError`). Beside another processor whose `check_registration/2`
  breaks its contract (it raises, throws, exits or answers anything other
  than `:ok` or `{:error, reason}`), the registration is not refused by
  name and the start answers the `ArgumentError` shape as before.
  ADR-0069's Note of 2026-10-04 records this.
- `Statifier.Lowering.lower/2`, and so `Statifier.compile/2`, answers
  `{:error, [%Statifier.Lowering.Error{reason: {:unexpected_root, name}}]}`
  for a document whose root is any SCXML element other than `<scxml>` (a
  `<state>`, a `<parallel>`, a `<final>`, a `<history>`, a `<transition>`,
  ...), where it raised `BadMapError`, and `{:unsupported_element, "scxml"}`
  for an `<scxml>` nested below the root, where it raised
  `FunctionClauseError`. If your host rescued either raise, match the error
  tuple instead.
- `Statifier.Validator.validate/2` no longer raises `FunctionClauseError`
  on a `<history>` whose compound parent has no `id`: a default target
  outside the parent is reported as `{:initial_not_descendant, target, nil}`.
  If your host matches that reason's parent id as a binary, handle `nil`
  too.
- `Statifier.Send.BasicHTTP` writes a parameter value that is a list or a
  map, and a `<content>` body that is a list, as JSON text
  (`["Dune",2]`, `{"title":"Dune"}`), where it wrote the value's
  `inspect/1` text; an undefined value inside such a list or map is
  written as JSON `null`. A list or map holding anything with no JSON form
  (a key that is not a string, a struct such as a `Date`, a tuple, a
  string that is not UTF-8) keeps its whole `inspect/1` text, and every
  scalar keeps its text. A `<content>` body that is a list is still sent as
  `text/plain`. If a receiver of your POSTs parsed the `inspect/1` text,
  read JSON instead. The inbound decoder is unchanged.

Three changes to a `Statifier.Send.BasicHTTP` delayed send ask nothing of a
host, but a chart sees them. A delayed send whose transport raises now
reaches the sender as `error.communication`, through
`Statifier.Session.failed_send/3` with the reason `{:raised, exception}`;
before, nothing reached the chart. A delayed send of a live session that is
busy when the delay passes is now POSTed; before, it was dropped. A cancel
the send's timer has received before its POST now always wins. An
immediate send whose transport raises is unchanged.

**A host may start:** refreshing a session's registered `_ioprocessors`
entries, refusing a registration in its own processor, and telling a
processor's own process that its session halted. Requires
`{:statifier, "~> 2.11"}`.

- `Statifier.MachineState.refresh_ioprocessors/1` recomputes a position's
  registered `_ioprocessors` entries from the registration it is stamped
  with, and answers `{:ok, machine_state}` or a registration's
  `{:error, reason}` with nothing changed; an entry that raises raises
  here. A host whose Basic HTTP base URL moved across a resume re-stamps
  the position with `Statifier.MachineState.put_send_types/2`, refreshes
  it, and then passes it as `:resume`. `Statifier.Session.refresh_ioprocessors/1`
  refreshes a live session and answers `:ok`, or, changing nothing,
  `{:error, reason}` for a registration its processor rejects
  (`{:error, {:missing_option, :base_url}}` for Basic HTTP without
  `:base_url`), `{:error, :not_running}` once the session has halted,
  `{:error, :recorded_session}` for a session started with `record: true`,
  or `{:error, {:ioprocessors_entry, type, exception}}` when an entry
  raises, the session running on at the position it held. The SCXML entry
  and an entry from a processor exporting only `ioprocessors_entry/1` are
  untouched, and a resume still reads the entries its position carries. A
  session that registers nothing answers success and sees nothing change.
  ADR-0075's Amendments of 2026-10-02 record both calls.
- A processor of yours may implement the optional
  `Statifier.Send.Processor` callback `check_registration/2`, which answers
  `:ok` or `{:error, reason}` for a registration's type string and options.
  A fresh start asks it for every registration whose module exports it and
  refuses the first one rejected with
  `{:error, {:send_types, {:invalid_registration, type, reason}}}`, before
  any session process is spawned; a resume does not ask it. A processor
  that does not implement it is not asked. ADR-0069's Amendment of
  2026-10-02 records the callback.
  Corrected 2026-10-04: `Statifier.Send.BasicHTTP` implements the
  callback, so 2.11.0 also adds the public function
  `Statifier.Send.BasicHTTP.check_registration/2`, which a host may call
  to check a Basic HTTP registration's options before a start. It answers
  `:ok` exactly when `ioprocessors_entry/2` would build an entry from the
  options (a string `:base_url`), and `{:error, {:missing_option, :base_url}}`
  for any other registration, a `:base_url` that is present but not a
  string included.
- A processor that holds a delay in a process it starts from `perform/2`
  can hand that process to `Statifier.Session.HaltNotice.watch/2`; the
  session sends it `{:statifier_halted, session, reason}` when it halts
  (`:done`, `:cancelled` or `:budget_exhausted`), at once when it already
  has. `HaltNotice.take/1` removes and returns the processes watched under
  a key. Outside a session `watch/2` keeps nothing and answers
  `:not_a_session`. The delay stays the processor's (ADR-0069 decision 4).

The conformance corpus changes too. The W3C Basic HTTP documents test518,
test519, test520 and test534 now check the decoded event their transition
takes, not its name alone, and a `statifier` case's step may carry an
optional `expect_position`, the position the chart holds after that step
(ADR-0076), with three new library cases. The corpus hash in
`conformance/manifest.json` moves, so a sibling implementation that
vendors the corpus re-vendors it at the `v2.11.0` tag, which is not a host
step.

## 2.12.0

**A host must change:** only where one of these reaches it.

- `Statifier.Send.BasicHTTP` sends a `<content expr>` that evaluates to a
  struct as the body, its `inspect/1` text as `text/plain`. Before
  2.12.0 such a content took the form arm. A struct that does not
  enumerate as parameter pairs (a `Date`, for example) made planning the
  send raise (`Protocol.UndefinedError` for a `Date`), the session
  performing it exited, and no request was made. A struct that does
  enumerate as pairs (a `MapSet` of two-element tuples, for example) was
  sent as an `application/x-www-form-urlencoded` body of those pairs; a
  receiver of such a send now gets the struct's `inspect/1` text as
  `text/plain` instead, the text it already reads for a struct inside a
  list or a map. A session that registers no send type sees nothing new.
- `Statifier.Validator.validate/2`, and so `Statifier.compile/2`, refuses
  a chart whose compound `<state>` has no `id` and carries an `initial`
  attribute or an `<initial>` element naming a target outside that state:
  it reports `{:initial_not_descendant, target, nil}`. Before 2.12.0 such
  a chart validated and `Statifier.compile/2` raised `KeyError`. A target
  inside the state is accepted as before. If your host rescued the raise,
  match the refusal instead, and if it matches that reason's parent id as
  a binary, handle `nil` too, as 2.11.0 asked for a `<history>` parent.
- `Statifier.Validator.validate/2`, and so `Statifier.compile/2`, decides
  whether a `<history>` default target sits under the history's parent by
  the document's structure for every parent, as it already did for a
  parent with no `id`. A document where two states share an `id` and a
  history under one of them names a target under the other was already
  refused for that `id` (`{:duplicate_id, id}`, or `{:empty_id}` for an
  empty one); it now also reports `{:initial_not_descendant, target,
  parent_id}`. No document that validated before is refused, and none is
  accepted that was refused.
- `statifier` requires `{:predicator, "~> 9.4"}`, where it required
  `~> 9.0`. A host that holds predicator at 9.0 to 9.3, in its own
  `mix.exs` or in its `mix.lock`, moves to 9.4 with it (`mix deps.update
  predicator`). With it, a string literal holding a non-ASCII character
  in an expression reaches `_event.data` and a `Statifier.Send.BasicHTTP`
  text body as the UTF-8 string written. Before 2.12.0 each such
  character became one byte, the low eight bits of its code point, so a
  text body carrying one was not the literal and was usually not valid
  UTF-8.
- With the same move, a string literal holding a `\u` or `\U` escape
  no longer reads as its letters with the backslash dropped. Before
  2.12.0 `'caf\u00e9'` read as `cafu00e9`; write the character itself
  instead. What a chart sees depends on where the literal sits. In an
  expression the compiler checks at load (a transition's `cond`, a
  `<log expr>` or a `<send>` `<content expr>`, for example),
  `Statifier.compile/2` refuses the document with an
  `:expression_compile_error`. In a `<data expr>`, an `<assign expr>`, a
  `<script>` or a `namelist` entry, where the compiler defers the
  failure, it raises `error.execution` when it runs, and
  `Statifier.Publish.findings/2` reports it under row S13. Wherever text
  is read as a value, through `Statifier.Compiler.Expressions.inline_value/1`
  or `Statifier.EventData.coerce/1`, it no longer folds to a string and is
  kept as its text, quotes and backslash included: the text of a `<data>`
  or an `<assign>` element; a `<content>` element's text under `<send>`,
  `<invoke>` or `<donedata>`; an inbound Basic HTTP request's text body or
  form parameter values.

**A host may start:** nothing new.

The conformance corpus changes too. `conformance/schema/case.json` now
refuses a step's `expect_position` whose `history_values` gives a
history state anything but an array of unique, non-empty state ids, and
the library case `loan_lost_after_timer` states the position after the
step that stops its chart: `running` false, an empty configuration and
`lost` still in `states_to_invoke`. The corpus hash in
`conformance/manifest.json` moves, so a sibling implementation that
vendors the corpus re-vendors it at the `v2.12.0` tag, which is not a
host step.

The conformance runner compares a position's numbers by JSON value: a
float the chart holds agrees with an expected integer of the same value
(`2.0` with `2`), where before it disagreed and its message named no
member, and a number that differs in value still disagrees, naming the
member. A sibling implementation's runner compares the same way; this is
not a host step either.

## 2.12.1

**A host must change:** NONE. The release changes documentation only: no
file under `lib/` differs from 2.12.0.

**A host may start:** nothing new. The conformance corpus does not change,
so a sibling implementation that vendored it at the `v2.12.0` tag has
nothing to re-vendor.

## 2.13.0

**A host must change:** only where one of these reaches it. A
`{:statifier, "~> 2.12"}` requirement already accepts 2.13.0.

- `Statifier.Validator.validate/2`, and so `Statifier.compile/2`, refuses
  two documents whose `<state>` has no `id`: one whose `initial` names a
  missing state, reported as `{:unresolved_initial, missing_id}`, and one
  with no child states that carries an `initial`, reported as
  `{:initial_on_atomic_state, nil}`. Before 2.13.0 both validated and
  `Statifier.compile/2` raised `KeyError`. If your host rescued the raise,
  match the refusal instead, and if it matches
  `initial_on_atomic_state`'s id as a binary, handle `nil` too, as 2.12.0
  asked for `initial_not_descendant`'s parent id.
- `Statifier.Validator.validate/2`, and so `Statifier.compile/2`, decides
  whether a `<state>`'s `initial` target is its descendant by the
  document's structure for every state, as 2.12.0 began doing for a
  `<history>` default target. A document where two states share an `id`
  and one's `initial` names a state under the other was already refused
  for that `id` (`{:duplicate_id, id}`); it now also reports
  `{:initial_not_descendant, target, id}`. No document that validated
  before is refused, and none is accepted that was refused.
- A session halted `:budget_exhausted` that is handed a miss through
  `Statifier.Session.failed_send/3` drops it without stepping, as a
  session halted `:done` or `:cancelled` does. Before 2.13.0 it stepped
  the halted chart again and sent its subscribers another
  `{:halted, :budget_exhausted}` for every miss. A host that waited for
  that further message after reporting a miss no longer receives it; the
  miss is the host's dead letter. A session halted `:done` or `:cancelled`
  is unchanged.

`Statifier.Testing.HandlerCase`'s check that a handler exception
propagates now waits up to ten seconds for the session's exit instead of
one, so a host's generated handler test no longer fails on a loaded
machine; a passing run returns as soon as the exit arrives. This is not a
host step.

**A host may start:** replacing a running session's registration for one
send type. Requires `{:statifier, "~> 2.13"}`.

- `Statifier.Session.replace_send_type/3` takes a session, a send type the
  session already registers and a new registration (a module or
  `{module, opts}`, as a `:send_types` value of `start_link/2` is), and
  recomputes that type's `_ioprocessors` entry in the same call. A host
  whose front moved a registration's options while the session runs (a
  new `:base_url` for `Statifier.Send.BasicHTTP`, or a rotated location)
  hands the session the new registration, the chart reads the new entry
  from its next event, and later sends of the type go through the new
  registration. It answers `:ok`, or changes nothing and answers
  `{:error, :not_running}` for a halted session,
  `{:error, :recorded_session}` for a session started with
  `record: true`, `{:error, {:unknown_send_type, type}}` for a type the
  session does not register,
  `{:error, {:send_types, {:invalid_registration, type, reason}}}` when
  the processor's `check_registration/2` refuses the registration (as
  `start_link/2` spells that refusal), and
  `{:error, {:ioprocessors_entry, type, exception}}` when the entry
  raises, the session keeping its position. ADR-0069's Amendment of
  2026-10-09 records the call.

The conformance corpus's hash does not move: no case changes. The case
schema's descriptions and the corpus README now say that a step's
expected datamodel keeps `_name` for a document whose `<scxml>` carries a
`name` attribute, and that the corpus writes each array of state ids
sorted while the schema does not check the order; no verdict changes.

## 2.14.0

**A host must change:** only where one of these reaches it. A
`{:statifier, "~> 2.13"}` requirement already accepts 2.14.0.

- **Breaking:** `Statifier.Validator.validate/3`, and so
  `Statifier.compile/2`, refuses a document that writes one attribute
  twice on one element, with the error reason
  `{:duplicate_attribute, element, attribute}` (code
  `:duplicate_attribute`), located at the repeated occurrence. Before
  2.14.0 such a document compiled using the first occurrence's value. XML
  does not allow the repeat: remove it from the document. If your host
  matches `Statifier.Validator.Error`'s reasons exhaustively, add a clause
  for the new reason.
- `Statifier.Session.cancel/1` sent to a session halted
  `:budget_exhausted` with events queued (by
  `Statifier.Session.send_event/2`, or by a delayed send to the session
  itself fired after the halt) reaches it and halts it `:cancelled`,
  discarding the queued events, so `{:halted, :cancelled}` follows the
  budget halt as the session's documentation promises. Before 2.14.0 the
  cancel stayed queued behind those events and the session stayed
  `:budget_exhausted`.
- A delayed send fired while a session is halted `:budget_exhausted`, to
  an internal target (a `targetexpr` evaluating to `#_internal`) or to a
  route unreachable at fire time, is dropped without stepping: the core
  is not driven, nothing is recorded, and no further message reaches a
  subscriber, as a budget-halted sender's miss reported through
  `Statifier.Session.failed_send/3` is dropped since 2.13.0. Before 2.14.0
  it drove the halted core again and sent another
  `{:halted, :budget_exhausted}`. A host that waited for that further
  message no longer receives it. A fired send to the session itself is
  still enqueued on its inbox, and one to another live session is still
  delivered to it.

A session halted `:done` or `:cancelled` is unchanged by either session
change.

**A host may start:** nothing new.

The conformance corpus changes too. The statifier suite gains two
library cases, `loan_renew_refused_by_standing` and
`loan_dispute_restores_late_shallowly`, and `conformance/registry.json`
claims both. The corpus hash in `conformance/manifest.json` moves, so a
sibling implementation that vendors the corpus re-vendors it at the
`v2.14.0` tag, which is not a host step. No case repeats an attribute on
one element, so the new refusal moves no case. The case schema's
descriptions now say that a step's `expect_position` members are compared
as JSON values, and that the corpus writes `active_invocations` sorted
while the schema does not check the order; no verdict changes.

## 2.15.0

**A host must change:** only where this reaches it. A
`{:statifier, "~> 2.14"}` requirement already accepts 2.15.0.

- `Statifier.compile/2` refuses a chart whose expressions call a function
  that is not `In`, a predicator builtin or a function the host declares,
  with a `Statifier.Compiler.Error` whose reason is
  `{:unknown_function, name, source}`, located at the element that holds
  the call. Before 2.15.0 such a chart compiled, and every evaluation of
  the call raised `error.execution`. Register the function with
  `:function_providers`, or declare its name with `:function_names`. If
  your host matches `Statifier.Compiler.Error`'s reasons exhaustively, add
  a clause for the new reason. This includes a chart a host stored before
  2.15.0 and recompiles under it, from its source or from a
  `Statifier.Chart` blob.

**A host may start:** registering named functions a chart may call from
`cond` and `expr` (ADR-0078). Requires `{:statifier, "~> 2.15"}`.

1. Write a `Predicator.FunctionProvider` module per family of functions.
   A function reads only its arguments and the host term: it does no I/O,
   reads no clock and no random source, and keeps no state, so a replay or
   a resume evaluates it the same.
2. Pass the list as `:function_providers` to `Statifier.compile/2`,
   `Statifier.MachineState.new/2` and `Statifier.Session.start_link/2`.
   A provider may not replace `In` or a predicator builtin. A list that
   does not resolve makes `Statifier.Session.start_link/2` refuse with
   `{:error, {:function_providers, reason}}` before it starts anything,
   and makes `Statifier.compile/2` and `Statifier.MachineState.new/2`
   raise `ArgumentError`.
3. A tool that checks a chart without loading the modules passes
   `:function_names`, the names alone, to `Statifier.compile/2`.
4. A session started with `:resume` stamps the list it is given, and a
   recording keeps it for `Statifier.Replay`. A host that resumes a
   position itself re-stamps the list with
   `Statifier.MachineState.put_function_providers/2`.
5. A child session started for an `<invoke>` does not inherit the list,
   and a chart given as `<invoke>` content cannot call a host function.

The guide is the section "Host functions for `cond` and `expr`" of
[How to extend Statifier: `<invoke>` handlers, `<send>` types and host functions](extending.md).

The conformance corpus does not change in 2.15.0.
