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

The versioned binary contract for a *chart* - a `Statifier.Machine.t()`
reduced to the inputs that reproduce it: its SCXML source, the persisted
subset of the options it was compiled with, and its
`Statifier.Machine.Identity.t()`. No compiled term is written - `from_binary/1`
rebuilds a `Machine.t()` by recompiling the stored source with the stored
options through `Statifier.compile/2`, the same pipeline any other caller
runs, rather than by deserializing compiler output directly.

This is boundary work, not core work (`docs/architecture.md` principle 2),
and it could not live on `Machine` even if that boundary argument were set
aside: `from_binary/1` calls `Statifier.compile/2` to rebuild its result,
and `Statifier.compile/2` itself builds a `Machine.t()` (ADR-0003's
layering - the thing produced does not call back into its own producer).
Putting the pair here instead keeps the dependency pointing one direction:
`Statifier.Chart` depends on `Statifier` and `Statifier.Machine`, never the
reverse. It also keeps `lib/statifier/machine.ex`'s moduledoc - already
carrying the full 100% Doctor burden for the compiled struct itself - free
of a second concern (persisting a chart across a process or storage
boundary) that has nothing to do with what the struct means once compiled.

`to_binary/1` refuses to encode a `Machine` carrying no `identity` or no
`source` (`{:error, :unidentified_chart}`): a `Machine` built without
either has nothing for a future `from_binary/1` to recompile from or check
against, so no blob is produced for it at all. `from_binary/1` decodes
safely, checks the envelope's tag and shape, checks its format version,
recompiles the stored source under the stored options, and only then
compares the recompiled `Machine`'s identity against the blob's - in that
order, for the same reason `Statifier.Position` checks version before
identity: a future format whose identity representation changed should
report the version mismatch, not a confusing identity one.

Neither function performs I/O; encoding and decoding a binary in memory,
and recompiling source already held in memory, are not effects a caller
has to route around (ADR-0003 does not apply here, and this module is not
listed in `@effect_interpreter_paths`).

# `format_version`

```elixir
@spec format_version() :: pos_integer()
```

The version tag `to_binary/1` writes and `from_binary/1` checks. A bare
integer, so a future format change is a version bump here rather than an
inference from the blob's shape.

# `from_binary`

```elixir
@spec from_binary(blob :: binary()) ::
  {:ok, Statifier.Machine.t()}
  | {:error, :not_a_statifier_blob}
  | {:error, {:unsupported_format_version, term()}}
  | {:error, {:compile_failed, [Statifier.error()]}}
  | {:error,
     {:identity_mismatch, expected :: Statifier.Machine.Identity.t(),
      actual :: Statifier.Machine.Identity.t() | nil}}
```

Decodes a `to_binary/1` envelope and recompiles it into a `Machine.t()`.

Checks run in this order, and the order matters: decode safely, then check
the envelope's tag and shape, then its format version, then recompile the
stored source under the stored options through `Statifier.compile/2`, then
compare the recompiled `Machine`'s identity against the blob's own. Version
before recompile before identity, because the identity being checked is
the *recompiled* `Machine`'s - there is no identity to compare until the
recompile has run, and a version this build cannot read at all should
report as a version mismatch rather than failing to compile for reasons
that have nothing to do with the source.

`{:error, {:compile_failed, errors}}` carries `Statifier.compile/2`'s own
`[Statifier.error()]` list unchanged - a blob whose source no longer
compiles under this build (for instance a validator check tightened across
a library upgrade) is a real, distinct failure and must not be flattened
into `:not_a_statifier_blob`.

`{:error, {:identity_mismatch, expected, actual}}`'s `expected` is the
blob's own stored identity and `actual` is the recompiled `Machine`'s -
Position's own argument order. Both are compared with
`Statifier.Machine.Identity.matches?/2`, never `==/2` on the struct
(ADR-0052 decision 1): a future identity field addition should not
silently change what "the same chart" means at this call site either.

Returns `{:error, :not_a_statifier_blob}` for anything that is not this
module's tagged envelope - a foreign `term_to_binary` blob, garbage bytes,
or a well-formed envelope whose source is not a binary or whose opts are
not a keyword list.

# `to_binary`

```elixir
@spec to_binary(machine :: Statifier.Machine.t()) ::
  {:ok, binary()} | {:error, :unidentified_chart}
```

Encodes `machine` as a tagged, versioned binary envelope carrying its SCXML
`source`, its persisted `compile_opts`, and its `Statifier.Machine.Identity.t()`
- never a compiled term.

Returns `{:error, :unidentified_chart}` when `machine.identity` or
`machine.source` is `nil` - a `Machine` built without either (for instance
one that came straight from `Statifier.Compiler.compile/1` rather than
`Statifier.compile/2`) has nothing for `from_binary/1` to recompile from or
check a future load against, so no blob is produced for it at all.

The payload is `machine.source` and `machine.compile_opts` verbatim, never
`machine` itself - the whole point of this module is that a chart's binary
form holds nothing `Statifier.compile/2` cannot reproduce, which is what
keeps the blob far smaller than `term_to_binary(machine)` for the same
chart: the compiled states, transitions, and expressions are the
overwhelming majority of a `Machine`'s bytes.

---

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