Skip to content

Repository files navigation

Statifier

CI Hex.pm Version Hex Downloads Hex Docs License

A W3C SCXML-conformant statecharts engine for Elixir. Ground-up rewrite of statifier v1.x, built against the SCION and W3C conformance corpora from day one.

Why a rewrite

v1 works, but its interpreter re-derived the SCXML semantics instead of porting the spec's algorithm, and the divergences account for nearly all of its remaining conformance failures. v2 is:

  • a literal port of W3C SCXML Appendix D - same functions, same names
  • a pure functional core returning effects - one semantics for every API, sessions and timers layered on top
  • predicator as the datamodel - safe, non-evaluative expressions; no ECMAScript, no eval
  • built corpus-first - 281 generated SCION/W3C conformance tests (119 SCION + 162 W3C) behind a forward-only regression ratchet inherited from v1, with the generator committed this time

Installation

Add statifier to your dependencies:

def deps do
  [
    {:statifier, "~> 2.2"}
  ]
end

Releases follow SemVer; CHANGELOG.md is the upgrade briefing, and its [2.0.0] section is written as a migration document for 1.x users. (The pre-release SHA-pinning contract ended with 2.0.0 - ADR-0066.) Persisted position and recording blobs refuse with a typed error on a format-version or chart-identity mismatch rather than misreading.

Quick start

Compile an SCXML document, initialize it, and send it events. Here is a card authorization that checks the amount against a budget before capturing it:

source = """
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="predicator" initial="authorizing">
  <datamodel>
    <data id="amount" expr="4200"/>
    <data id="budget_remaining" expr="10000"/>
  </datamodel>

  <state id="authorizing">
    <transition event="card.approved" cond="amount &lt;= budget_remaining"
                target="capturing"/>
    <transition event="card.approved" target="over_budget"/>
    <transition event="card.declined" target="declined"/>
  </state>

  <state id="capturing">
    <invoke type="myapp:capture" id="capture">
      <param name="amount" expr="amount"/>
    </invoke>
    <transition event="done.invoke.capture" target="settled"/>
    <transition event="error.communication" target="needs_attention"/>
  </state>

  <state id="over_budget"/>
  <state id="declined"/>
  <state id="needs_attention"/>
  <final id="settled"/>
</scxml>
"""

{:ok, machine} = Statifier.compile(source)
{machine_state, _effects} = Statifier.initialize(machine)

Statifier.active_leaf_states(machine_state)
#=> MapSet.new(["authorizing"])

{:ok, machine_state, effects} = Statifier.send_event(machine_state, "card.approved")

Statifier.active_leaf_states(machine_state)
#=> MapSet.new(["capturing"])

The guard is a predicator expression evaluated against the chart's own datamodel - no ECMAScript and no eval. Because 4200 <= 10000 held, the run took the first arrow.

Effects come back as data; the engine never performs them for you. Entering capturing did not call your payment service, it described the call:

effects
#=> [
#=>   invoke: %Statifier.Effect.Invoke{
#=>     invoke_id: "capture",
#=>     type: "myapp:capture",
#=>     params: %{"amount" => 4200},
#=>     ...
#=>   }
#=> ]

Performing that effect is your host's job, and so is telling the chart how it went: Statifier.Session.done_invocation/3 delivers done.invoke.capture, and Statifier.Session.failed_invocation/3 delivers error.communication.invoke.capture once your retry policy is exhausted - which is the arrow that parks this run in needs_attention instead of waiting in capturing forever. See Extending.

That four-function surface (compile/2, initialize/2, send_event/2, active_leaf_states/1) is the whole entry point; sessions, durable timers, persistence, and telemetry layer on top of it.

Documentation

Published guides on hexdocs:

  • Architecture - the layered design and the decisions behind it
  • Datamodel - predicator expressions, <data>, <assign>, and <script>
  • Extending - registering your own <invoke> handlers, and reporting completion or permanent failure back to the chart
  • Persistence - chart identity, persisted positions, and resuming sessions
  • Durable timers - scheduling delayed sends outside the session process
  • Observability - trace effects and what to do with them
  • OpenTelemetry - span topology and the OTel bridge
  • Testing charts - testing your own state charts
  • Chart patterns - patterns for external-resource verdicts (park/retry, fail-fast)
  • Family reference - what the statifier sibling repos copy from here

Architecture Decision Records live in the repository at docs/adr/.

Development

mix deps.get
mix quality --profile loop   # fast inner loop
mix quality                  # full gate (required green before commit)

Issue tracking is beads (bd ready to find work). Workflow, model roles, and worktree conventions: docs/workflow.md.

License

MIT - see LICENSE.

About

StateCharts for Elixir with W3C compliance

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages