All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Entries for unreleased work are not written here directly. Each issue drops a
fragment in changelog.d/; the fragments are assembled
into a version section at release. See that README for the format and for when a
change warrants an entry at all.
[0.7.0] 2026-09-05
Feature release: a scheduler can start a Tier A fan-out through a public door, and the N children settle once as a dense, index-ordered list instead of each answering the parent. Hosts on the Ecto adapter run a new V03 migration.
Added
StatifierPersistence.Driver.start_child_at/6: the public start-with-index door a scheduler drives a Tier A fan-out through. Starts childiofNfor a parent's<invoke>, records the count and the aggregation policy (:allor:first_error) on the child's linkage, and is idempotent on the derived child run id, so a re-delivered start adopts rather than duplicating. Refuses at open on a store that could not settle the invocation afterwards.StatifierPersistence.Run.Linkage.new/6andfan_out?/1: a child's linkage can now carry its invocation'schild_countand aggregation policy (:allor:first_error), which is what marks it as one of a fan-out's N rather than an ordinary durable subchart.
Changed
- The storage-adapter
run_recordgains a nullableoutcome_blob: a run's own answer, written once when it reaches a terminal status throughStatifierPersistence.Storage.update_run_status/4's newoutcome_blob:option.update_run/2carries a stored payload forward when the record it is given carries none, so an ordinary step never erases one. Adapters gain two optional callbacks alongside it -supports_run_outcome?/1andlist_run_states_by_metadata/2, the indexed status projection - and an adapter that exports neither is conformant unchanged. - The Ecto adapter's V03 migration adds the
outcome_blobcolumn and a GINjsonb_path_opsindex onmetadata. A host already on V02 picks it up withStatifierPersistence.Ecto.Migrations.up(for: MyApp, from: 3). Run it before deploying 0.7.0, and read the README's "Upgrading to V03 before deploying 0.7.0" first: the index build is a plainCREATE INDEXthat blocks writes to the runs table for the length of the build. StatifierPersistence.Drivertakes achild_canceller:option: how a:first_errorsettlement asks the scheduler to cancel the start jobs of a fan-out's not-yet-started children, which have no run record for the cascade to reach.- A fan-out child's completion now settles instead of answering its
parent's door: its answer is stored on its own run record, and only the
settlement that finds all N indices terminal assembles the dense,
index-ordered list and answers the invocation once.
StatifierPersistence.Driver.answer_parent/3routes a fan-out child the same way and returns:okfor it. A child with nochild_counton its linkage - every child created before this release - is unaffected.
[0.6.0] 2026-09-02
Feature release: a durably-stepped run is observable through statifier's own session telemetry, so the OpenTelemetry bridge produces the same spans and effect events for a durable run as for a session-hosted one.
Added
- Durably-stepped runs now emit statifier's own
[:statifier, :session, ...]telemetry withdriver: :persistence, soopentelemetry_statifierproduces the same macrostep spans and effect events for a durable run as for a session-hosted one, with no bridge change.
[0.5.0] 2026-09-01
Feature release: the durable step is observable, and a :dispatch fun can
see the whole <invoke> it is being handed.
Added
StatifierPersistence.Driver.dispatch_context/0carries:invoke, the wholeStatifier.Effect.Invokebeing dispatched, so a:dispatchfun can read the element'ssrc- the document id a subchart handler resolves its child chart by - along withcontent,autoforward, and the step counters.StatifierPersistence.Telemetryemits the fourteen[:statifier_persistence, ...]events ADR-0009 specifies - the durable step as a:start/:stoppair, the per-run lock wait, every storage-adapter call, identity refusals, the run lifecycle, executor failures, and the durable-subchart seam - andevents/0returns every name for a bridge to attach to.- Adds a direct
:telemetrydependency (already present transitively throughstatifier, so no lock file grows).
[0.4.0] 2026-09-01
Feature release: durable subcharts (ADR-0008) - an <invoke> may start a
child chart as an ordinary durable run, the parent rests holding no process
while the child runs, and leaving the invoking state cancels the child
subtree.
Breaking for storage adapters: run_status/0 gains a fourth terminal
value, :cancelled. An adapter that encodes run statuses by an exhaustive
match must add a clause for it before upgrading, or cancelled runs will fail
to persist. The two adapters in this package already handle it.
Added
- Durable subcharts (ADR-0008): a
<invoke>whose:dispatchfun answers{:start_child, invoke, {:invoke, invoke}}now starts the subchart as an ordinary durable run instead of being refused, and the parent rests holding no process for as long as the child takes. StatifierPersistence.Run.Linkagerecords a child's parent run id, invocation id, and a mandatory pin of the child's chart identity under a reserved key in the child's runmetadata.StatifierPersistence.Runs.create/4takeslinkage:, and raisesArgumentErrorwhen a host's ownmetadata:writes into the reserved key.StatifierPersistence.Driver.new/3takeschart_resolver:, which lets a finished child answer its parent through the existingdone_invocation/5andfailed_invocation/5doors.StatifierPersistence.Runs.cancel/3andcascade_cancel/3cancel a parent's child subtree when it leaves the invoking state, retaining every record and position.- Storage adapters may export the optional
list_runs_by_metadata/2, reached throughStatifierPersistence.Storage.list_runs_by_metadata/2andchild_listing_supported?/1; a store whose adapter does not export it refuses a durable subchart before any write. StatifierPersistence.Storage.InMemoryimplementslist_runs_by_metadata/2, whichStatifierPersistence.Storage.Ectoalready supported.
Changed
StatifierPersistence.Storage.Adapter.run_status/0gains a fourth terminal value,:cancelled. An adapter that encodes statuses by an exhaustive match must add a clause for it, or cancelled runs will fail to persist.StatifierPersistence.Rungainsdonedata, set only on the step that completes a run andnileverywhere else.
[0.3.0] 2026-09-01
Feature release: an asynchronous invocation seam on the durable driver - a
dispatch may answer :pending and the run rests holding no process, with
public doors that answer the invocation later from any process or node.
Added
StatifierPersistence.Driver's:dispatchfun may answer:pending: the call was started asynchronously and the run rests durably with the invocation live, holding no process (ADR-0007).StatifierPersistence.Driver.done_invocation/5andStatifierPersistence.Driver.failed_invocation/5answer a pending invocation later, from any process or node, building the samedone.invoke/error.communication.invokeevents a liveStatifier.Sessionbuilds. An answer for an invocation the chart has cancelled is{:discarded, run}, decided from the persisted position inside the run's serialization strategy.StatifierPersistence.Runs.step/5accepts an event builder - a fun over the loaded position returning{:ok, event}or:discard- anywhere it accepts aStatifier.Event.
Changed
- The context handed to a
:dispatchfun carriesinvoke_id, the invocation id an asynchronous host keys its work by and hands back to the re-entry doors.
[0.2.0] 2026-08-31
Feature release: a durable run-to-quiescence driver, opaque run metadata, and a custom blob type for encryption at rest.
Added
StatifierPersistence.Driverdrives a durable run to quiescence overStatifierPersistence.Runs: it performs each<invoke>through a host-supplied dispatch fun inside the step that emitted it, then steps every answer back in until the chart rests. Hosts that hand-rolled this loop can delete it.StatifierPersistence.Driverbuilds an invocation's answer events -done.invoke.<id>anderror.communication.invoke.<id>,originandorigintypeincluded - field for field fromStatifier.Session's owndone_invocation/3andfailed_invocation/3, so a chart sees the same event in a session and out of storage. A conformance test answers one document both ways and compares what each chart saw.StatifierPersistence.Runs.create/4andStatifierPersistence.Storage.insert_run/5accept an optionalmetadata:map of string keys, stored opaquely beside the run record and returned byfetch_run/2unchanged (ADR-0006). Host identities only, never personal data: blob encryption does not reach this column.StatifierPersistence.Storage.Adaptergains the optionalsupports_metadata?/1callback and ametadatafield onrun_record. An adapter that does not export it refuses a non-empty map at create with{:error, :metadata_unsupported}; an empty or absent map is never refused, so every existing adapter stays conformant unchanged.StatifierPersistence.Storage.metadata_supported?/1andcheck_metadata/2report whether a store's adapter can hold metadata, without writing anything.StatifierPersistence.Storage.Ecto.list_runs_by_metadata/2lists the runs whose metadata contains every given key/value pair.- Migration V02 adds a nullable
jsonbmetadatacolumn to the runs table, andStatifierPersistence.Ecto.Migrations.up/1acceptsfrom:so a host already on V01 applies later versions in its own second migration. StatifierPersistence.Testing.StorageConformancegains metadata cases: a conformant adapter either round-trips the map or refuses it at open, and never silently drops it.use StatifierPersistence.Ectoaccepts a:blob_typeoption to put a custom Ecto type on the three blob columns (identity_blob,chart_blob,position_blob), enabling encryption at rest with no wrapping adapter.
Changed
- Requires
statifier~> 2.2 and >= 2.2.1rather than~> 2.0: 2.2.1 is the first release carrying the queue-discard-on-exit fix the completion conformance cases need. (An interim git-ref pin served between 2.2.0 and that release.)
Fixed
- A run whose top-level
<final>is reached while siblingdone.state.*events are still queued now persists ascompleted, instead of raising "loop bug: non-quiescent MachineState reached the persist tail". The same holds for a top-level<final>whose<donedata>expression fails. StatifierPersistence.Runs.create/4passes only itsmetadata:pair toStatifierPersistence.Storage.check_metadata/2, whose contract is the narrower[Storage.run_write_opt()]. Handing the whole option list over made dialyzer derive a success typing forcreate/4that accepted noexecutor:at all, so an embedder had to suppress "will never return" on every correct call; that suppression can now be deleted.
[0.1.3] 2026-08-27
Docs release: README and guide refresh onto the family's canonical example domains. No library code changes.
Changed
- The README now walks a full worked run in the card-processing domain - load, step, execute effects, persist - and continues it across a restart, with a new module map; the examples are executed by a test so they cannot drift from the real API.
- Example domains follow the family rule: card processing and the signup wizard with A/B testing only.
- Agent tooling: gate attestation points at
mix quality.verify(shipped by ex_quality 0.14) instead of a retired local task.
[0.1.2] 2026-08-24
Docs release: the hexdocs/README overhaul from PR #20. No library code changes.
Changed
- Hexdocs no longer publishes the ADRs: the ADR extras and their
groups_for_extrasentry are removed, so the published docs are the README, this changelog, and the restart-demo guide. ex_docis pinned to~> 0.40, andCHANGELOG.mdis listed inskip_undefined_reference_warnings_on;mix docsnow completes with zero warnings.- The README gains the standard badge row (CI, hex.pm version/downloads, hexdocs, license) and a documentation index line linking the published restart-demo guide.
[0.1.1] 2026-08-24
Patch release: the key-generator compile-race fix from PR #18.
Fixed
- Custom key-generator validation in
use StatifierPersistence.Ectono longer fails spuriously when the generator module is still being compiled by the host's parallel compiler; validation now waits for in-flight compilation (Code.ensure_compiled/1) instead of checkingCode.ensure_loaded?/1.
[0.1.0] 2026-08-22
First release: the persistence-first execution loop for the statifier statechart engine - load a persisted position, step it, execute the effects, persist - packaged as a storage-adapter behaviour with an identity guard, an in-memory reference adapter, a run lifecycle, and an Ecto/Postgres layer, all covered by one conformance suite downstream adapters inherit.
Added
StatifierPersistence.Storage.Adapter, the storage contract, including run records:insert_run/2,fetch_run/2, andupdate_run/2callbacks withrun_record/run_statustypes and the:run_exists/:run_not_founderror arms;StatifierPersistence.Storage.InMemoryis the reference implementation.- Guarded run access on the facade:
StatifierPersistence.Storage.insert_run/5,update_run/5,fetch_run/2, andload_run_position/3(identity-guarded, with the:run_position_missingarm for a run persisted without a position). - Run-record conformance tests in
StatifierPersistence.Testing.StorageConformance, so downstream adapters inherit the same contract checks. - The run lifecycle as a library:
StatifierPersistence.Runs.create/4andstep/5drive the load -> re-stamp -> step -> execute -> persist loop over durable run records, handing effects to a host-suppliedStatifierPersistence.Executor(behaviour or arity-2 fun) and returning the host-facingStatifierPersistence.Runstruct; events to a terminal run come back as{:discarded, run}. - Failure semantics on the loop: executor failures on actionable effects
re-enter the chart as
error.communicationevents (single wave per step, observational failures discarded); effect execution is at-least-once, with a failed persist re-driving the same event and re-emitting the same effects under identical deterministic keys; budget exhaustion persists a:failedrun (position untouched) and returns{:error, {:budget_exhausted, payload}}. StatifierPersistence.Runs.fail/4, the host-driven abandonment: marks an active run:failedwith a reason, leaves the stored position untouched, and discards on a terminal run - backed by the status-only writerStatifierPersistence.Storage.update_run_status/4.- Pluggable per-run serialization: the
StatifierPersistence.Serializationbehaviour (with_run/3), selected per lifecycle call withserialization: {module, config}onRuns.create/4,step/5, andfail/4. The default strategy,StatifierPersistence.Serialization.AdapterLock, delegates to the optional adapter callbackStatifierPersistence.Storage.Adapter.lock_run/3(implemented byInMemory, conformance-tested when exported) and refuses with{:error, {:serialization, :not_supported}}when the adapter does not export it. use StatifierPersistence.Ecto: compile-time configuration on the host's module (repo:,key:,table_prefix:,tables:,prefix:) that definesChart,Position, andRunschema modules and exposes the resolved config via__statifier_persistence__/1. Requires the optionalecto_sqldependency.StatifierPersistence.Ecto.KeyGenerator: the behaviour a surrogate-key scheme implements, with:uxid(default),:uuid(UUIDv7),:bigserial, and{module, opts}resolved throughresolve/1.StatifierPersistence.Ecto.Migrations: the versioned migrations helper (up/1,down/1, takingfor: HostModuleor the same literal optionsusetakes) that creates thecharts/positions/runstables from the same resolved config the schemas use.StatifierPersistence.Storage.Ecto: the Postgres storage adapter over the schemas a host generates withuse StatifierPersistence.Ecto(Storage.new(Storage.Ecto, persistence: MyApp.Persistence)). Passes the same conformance suite as the in-memory reference adapter; engine identities stored verbatim;:run_existsenforced atomically by the unique index.Storage.Ecto.isolate/1: withsandbox: true, wraps each test in its ownEcto.Adapters.SQL.Sandboxcheckout - the hook host test suites (and this package's conformance suite) isolate through.Storage.Ecto.lock_run/3: per-run mutual exclusion as a transaction-scopedpg_advisory_xact_lockplus aSELECT ... FOR UPDATErow lock (ADR-0004 as amended), consumed bySerialization.AdapterLock.uxidis a required dependency (the default key scheme works out of the box);ecto_sqlis optional and the package compiles without it.