Statifier.Chart (Statifier v2.0.0)

Copy Markdown View Source

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

Summary

Functions

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.

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

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

Functions

format_version()

@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(blob)

@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(machine)

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