# `Statifier`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.0.0/lib/statifier.ex#L1)

The public entry point for statifier-ex.

This module is the library's four-function surface (ADR-0006), the only
place a caller needs to import: compile a document, initialize a state
chart, send an event synchronously, and read the active leaf states. Every
layer beneath it - `Statifier.Document`, `Statifier.Machine`,
`Statifier.MachineState` - stays reachable for tooling, but a caller who
only wants to run a state chart never has to name them.

Two boundaries hold at every function in this module:

- **String ids only at this boundary, and nowhere deeper** (ADR-0005).
  Below `Statifier`, states and transitions are addressed by interned
  integer index; this module is where a caller's string ids go in and
  where they come back out.
- **Effects are returned, never interpreted** (ADR-0003). Every function
  that can produce `Statifier.Effect.t()` values hands them back as data;
  none of them is inspected, logged, or executed here. A caller that wants
  to act on `:log`, `:done`, or a `:trace` effect does so itself.

All four functions land in this module: `compile/2` (with `compile/1`'s
default `opts \ []`) runs the parse pipeline; `initialize/2`,
`send_event/2`, and `active_leaf_states/1` wrap `Statifier.Interpreter`.

# `error`

```elixir
@type error() ::
  Statifier.Parser.ParseError.t()
  | Statifier.Lowering.Error.t()
  | Statifier.Validator.Error.t()
  | Statifier.Compiler.Error.t()
```

The union of every error struct any pipeline stage `compile/2` runs can
produce.

# `active_leaf_states`

```elixir
@spec active_leaf_states(machine_state :: Statifier.MachineState.t()) ::
  MapSet.t(String.t())
```

The active leaf states of `machine_state`, as a `MapSet` of string ids.

`Statifier.MachineState.active_leaf_states/1` returns interned integer
indexes; this is the boundary ADR-0005 reserves for translating them to
the ids a caller wrote in the document, and nothing beneath `Statifier`
ever returns a string id. `Statifier.Machine.id/2` returns `nil` for the
root and for every nameless state - a state with no id cannot be named by
any caller's expectation either, so it is dropped rather than raised or
given a synthetic name that no document actually wrote.

# `compile`

```elixir
@spec compile(source :: binary(), opts :: keyword()) ::
  {:ok, Statifier.Machine.t()} | {:error, [error()]}
```

Compiles SCXML source into a `Statifier.Machine`.

Runs the full pipeline - `Statifier.Parser.parse/1`,
`Statifier.Lowering.lower/2`, `Statifier.Validator.validate/3`,
`Statifier.Compiler.compile/1` - in that order, stopping at the first stage
that fails. `Statifier.Parser.parse/1` is the one stage that reports a
single error rather than a list; this function wraps it so every failure
from every stage has one shape: `{:error, [error()]}`.

`Validator.validate/3` returns three elements on both arms (ADR-0033). On
success its warnings ride onto the returned `Machine.t()`'s `warnings`
field rather than a third element of this function's own return, so a
document with warnings still compiles and the caller finds the findings on
the machine (`Statifier.Machine`'s moduledoc explains why they live there).
Its error arm's extra element is collapsed back to this function's own
`{:error, [error()]}` shape so this stage's failure looks like every other
stage's.

`opts` defaults to `[]`, so every existing `compile/1` call keeps its exact
behavior. The one recognized option today is `invoke_content_markup:
true` (ADR-0042): it relaxes `Statifier.Validator.Checks.Boilerplate`'s
root-namespace check (spec 3.2.1's `xmlns` requirement) to accept a root
that declares no namespace at all, the same leniency
`Statifier.Lowering.Namespace.scxml_vocabulary?/1` already applies to
lowering dispatch. It exists to compile the verbatim source slice
`Statifier.Invoke.Source.resolve/2` extracts from an `<invoke><content>`
element - G.6 (informative) places an undeclared-namespace child of
`<content>` in the SCXML namespace by ordinary XML scoping, a fact the
slice's standalone compile can no longer see for itself. It is not a
general validation off-switch: a root that declares a namespace other than
SCXML's still fails, in this mode and without it alike, and `version` must
still be `"1.0"` either way.

Two more recognized options, `:chart_name` and `:chart_version`, ride
straight through to `Statifier.Machine.Identity.of_source/2` and are
ignored by every pipeline stage before it: `Validator.Context.build/3`
reads only `:invoke_content_markup` from this same `opts`, so the two
identity options pass through the pipeline untouched and no stage needs a
change to carry them. They stamp the returned `Machine.t()`'s `identity`
field (`Statifier.Machine.identity/1`) alongside the content hash
`compile/2` always takes over `source`.

A successful compile also stamps `source` (the exact bytes passed in) and
`compile_opts` (`opts` filtered through the closed
`@persisted_compile_opts` allowlist, in the allowlist's own order) onto the
returned `Machine.t()`, so it can reproduce itself later - see
`Statifier.Machine`'s moduledoc for the invariant this keeps with
`identity`.

# `initialize`

```elixir
@spec initialize(machine :: Statifier.Machine.t(), opts :: keyword()) ::
  {Statifier.MachineState.t(), [Statifier.Effect.t()]}
```

Initializes `machine` into its starting `Statifier.MachineState`, running
the initialization macrostep to quiescence.

A straight pass-through to `Statifier.Interpreter.initialize/2`, mirroring
that function's own untagged `{machine_state, [effect]}` pair rather than
wrapping it in an `{:ok, _, _}` this facade would have to invent: a
`%Machine{}` is valid by construction, so initialization cannot fail.
`opts` is `Statifier.MachineState.new/2`'s own option set (`:trace`,
`:datamodel`, `:max_macrostep_rounds`), passed straight through and
interpreted by neither this function nor `Interpreter.initialize/2`
itself.

The returned effects are data, never performed here (ADR-0003) - a caller
that wants the initialization log/trace effects has them; a caller that
does not is free to discard them.

# `send_event`

```elixir
@spec send_event(
  machine_state :: Statifier.MachineState.t(),
  event :: Statifier.Event.t() | String.t()
) ::
  {:ok, Statifier.MachineState.t(), [Statifier.Effect.t()]}
  | {:error, :not_running}
```

Sends one event to `machine_state`, running a macrostep to quiescence and
returning the resulting position.

A straight pass-through to `Statifier.Interpreter.handle_event/2`:
`{:error, :not_running}` comes back unchanged when `machine_state` has
already terminated, rather than being reinterpreted into some other
shape. As with `initialize/2`, the returned effects are handed back as
data and never inspected, logged, or executed here (ADR-0003).

`event` may be a `Statifier.Event.t()` or a plain name string; the string
clause is a convenience over `Statifier.Event.external/2` and carries no
data of its own - a caller who needs event data builds the `%Event{}`
directly.

# `start_session`

```elixir
@spec start_session(machine :: Statifier.Machine.t(), opts :: keyword()) ::
  {:ok, pid()} | {:error, term()}
```

Starts a `Statifier.Session` for `machine` on `Statifier.SessionSupervisor`,
under the ADR-0027 session runtime.

`opts` is `Statifier.Session.start_link/2`'s own option set, passed
through unchanged. This is the runtime-placed alternative to calling
`Statifier.Session.start_link/2` directly: a session started here lands
on `Statifier.SessionSupervisor` (`restart: :temporary`, so a crash is
never silently restarted into a fresh, unrelated session id - ADR-0027
decision 4) and registers itself under `Statifier.Registry` during
`init/1`, so it becomes reachable by other sessions as
`#_scxml_<sessionid>` (ADR-0027 decision 2). A session started instead by
a bare `Statifier.Session.start_link/2` call is legal but stays
unregistered, and therefore unreachable by id from any session but
itself.

Requires `Statifier.Supervisor` to already be placed somewhere in the
embedder's own supervision tree - this library ships no application
callback (ADR-0027 decision 1), so nothing starts one automatically. The
return value is `DynamicSupervisor.start_child/2`'s own
`{:ok, pid} | {:error, term}` (see its docs for what `term` can be, most
commonly `Statifier.SessionSupervisor` itself not being started).

---

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