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.2] 2026-09-06

Patch release: a fan-out whose children settle at the same time assembles with every child's donedata present. A settlement used to read a sibling's status as terminal while that sibling's answer was still in flight, and assembled a completed child with a nil donedata; a settlement now records its own answer under the parent's exclusion and waits for every child's recorded answer, not only for every child's terminal status.

Fixed

  • A fan-out invocation whose children settle concurrently no longer answers with a nil donedata for a child that completed: a settlement waits for every child's answer to be recorded, not only for every child's status to be terminal, and records its own answer under the parent's exclusion.

[0.7.1] 2026-09-05

Patch release: hosts that are not on Postgres can apply the package DDL. V03 creates its metadata GIN index only on Postgres, and the Ecto adapter now answers supports_metadata?/1 by adapter, so a store that could not settle a durable subchart or a fan-out is refused at open rather than crashing partway through.

Fixed

  • V03 of the Ecto migration helper is adapter-aware: the metadata GIN jsonb_path_ops index is created (and dropped) only when the migration's repo runs on Ecto.Adapters.Postgres. On any other adapter it is skipped and the migration runs to completion, so a host on another backend can apply the package DDL at all - in 0.7.0 the index raised, the whole migration rolled back, and the outcome_blob column went with it. The column itself is still created on every adapter, and StatifierPersistence.Storage.Ecto.supports_run_outcome?/1 is still true everywhere.
  • StatifierPersistence.Storage.Ecto.supports_metadata?/1 now answers false off Postgres, because both metadata queries the adapter issues are jsonb containment SQL. StatifierPersistence.Storage's child_listing_supported?/1 and run_states_supported?/1 consult it, so a durable subchart or a fan-out over such a store is refused at open (:child_listing_unsupported) rather than started and left with children nothing could settle. Behavior on Postgres is unchanged.

[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 child i of N for a parent's <invoke>, records the count and the aggregation policy (:all or :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/6 and fan_out?/1: a child's linkage can now carry its invocation's child_count and aggregation policy (:all or :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_record gains a nullable outcome_blob: a run's own answer, written once when it reaches a terminal status through StatifierPersistence.Storage.update_run_status/4's new outcome_blob: option. update_run/2 carries 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?/1 and list_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_blob column and a GIN jsonb_path_ops index on metadata. A host already on V02 picks it up with StatifierPersistence.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 plain CREATE INDEX that blocks writes to the runs table for the length of the build.
  • StatifierPersistence.Driver takes a child_canceller: option: how a :first_error settlement 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/3 routes a fan-out child the same way and returns :ok for it. A child with no child_count on 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 with driver: :persistence, so opentelemetry_statifier produces 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/0 carries :invoke, the whole Statifier.Effect.Invoke being dispatched, so a :dispatch fun can read the element's src - the document id a subchart handler resolves its child chart by - along with content, autoforward, and the step counters.
  • StatifierPersistence.Telemetry emits the fourteen [:statifier_persistence, ...] events ADR-0009 specifies - the durable step as a :start/:stop pair, the per-run lock wait, every storage-adapter call, identity refusals, the run lifecycle, executor failures, and the durable-subchart seam - and events/0 returns every name for a bridge to attach to.
  • Adds a direct :telemetry dependency (already present transitively through statifier, 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

Changed

  • StatifierPersistence.Storage.Adapter.run_status/0 gains 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.Run gains donedata, set only on the step that completes a run and nil everywhere 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

Changed

  • The context handed to a :dispatch fun carries invoke_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.Driver drives a durable run to quiescence over StatifierPersistence.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.Driver builds an invocation's answer events - done.invoke.<id> and error.communication.invoke.<id>, origin and origintype included - field for field from Statifier.Session's own done_invocation/3 and failed_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/4 and StatifierPersistence.Storage.insert_run/5 accept an optional metadata: map of string keys, stored opaquely beside the run record and returned by fetch_run/2 unchanged (ADR-0006). Host identities only, never personal data: blob encryption does not reach this column.
  • StatifierPersistence.Storage.Adapter gains the optional supports_metadata?/1 callback and a metadata field on run_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?/1 and check_metadata/2 report whether a store's adapter can hold metadata, without writing anything.
  • StatifierPersistence.Storage.Ecto.list_runs_by_metadata/2 lists the runs whose metadata contains every given key/value pair.
  • Migration V02 adds a nullable jsonb metadata column to the runs table, and StatifierPersistence.Ecto.Migrations.up/1 accepts from: so a host already on V01 applies later versions in its own second migration.
  • StatifierPersistence.Testing.StorageConformance gains metadata cases: a conformant adapter either round-trips the map or refuses it at open, and never silently drops it.
  • use StatifierPersistence.Ecto accepts a :blob_type option 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.1 rather 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 sibling done.state.* events are still queued now persists as completed, 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/4 passes only its metadata: pair to StatifierPersistence.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 for create/4 that accepted no executor: 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_extras entry are removed, so the published docs are the README, this changelog, and the restart-demo guide.
  • ex_doc is pinned to ~> 0.40, and CHANGELOG.md is listed in skip_undefined_reference_warnings_on; mix docs now 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.Ecto no 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 checking Code.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, and update_run/2 callbacks with run_record/run_status types and the :run_exists / :run_not_found error arms; StatifierPersistence.Storage.InMemory is the reference implementation.
  • Guarded run access on the facade: StatifierPersistence.Storage.insert_run/5, update_run/5, fetch_run/2, and load_run_position/3 (identity-guarded, with the :run_position_missing arm 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/4 and step/5 drive the load -> re-stamp -> step -> execute -> persist loop over durable run records, handing effects to a host-supplied StatifierPersistence.Executor (behaviour or arity-2 fun) and returning the host-facing StatifierPersistence.Run struct; 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.communication events (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 :failed run (position untouched) and returns {:error, {:budget_exhausted, payload}}.
  • StatifierPersistence.Runs.fail/4, the host-driven abandonment: marks an active run :failed with a reason, leaves the stored position untouched, and discards on a terminal run - backed by the status-only writer StatifierPersistence.Storage.update_run_status/4.
  • Pluggable per-run serialization: the StatifierPersistence.Serialization behaviour (with_run/3), selected per lifecycle call with serialization: {module, config} on Runs.create/4, step/5, and fail/4. The default strategy, StatifierPersistence.Serialization.AdapterLock, delegates to the optional adapter callback StatifierPersistence.Storage.Adapter.lock_run/3 (implemented by InMemory, 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 defines Chart, Position, and Run schema modules and exposes the resolved config via __statifier_persistence__/1. Requires the optional ecto_sql dependency.
  • StatifierPersistence.Ecto.KeyGenerator: the behaviour a surrogate-key scheme implements, with :uxid (default), :uuid (UUIDv7), :bigserial, and {module, opts} resolved through resolve/1.
  • StatifierPersistence.Ecto.Migrations: the versioned migrations helper (up/1, down/1, taking for: HostModule or the same literal options use takes) that creates the charts/positions/runs tables from the same resolved config the schemas use.
  • StatifierPersistence.Storage.Ecto: the Postgres storage adapter over the schemas a host generates with use 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_exists enforced atomically by the unique index.
  • Storage.Ecto.isolate/1: with sandbox: true, wraps each test in its own Ecto.Adapters.SQL.Sandbox checkout - 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-scoped pg_advisory_xact_lock plus a SELECT ... FOR UPDATE row lock (ADR-0004 as amended), consumed by Serialization.AdapterLock.
  • uxid is a required dependency (the default key scheme works out of the box); ecto_sql is optional and the package compiles without it.