From aa73c319695c236767adf7f9b66c9204edb46eea Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sat, 22 Aug 2026 18:18:58 -0600 Subject: [PATCH 1/2] Prepares the statifier_persistence 0.1.0 Hex release Adds package/0 (statifier_persistence, MIT, lib + mix.exs + README + LICENSE + CHANGELOG in the tarball) and the docs/0 hexdocs configuration (README landing page, the restart demo, the ADR set). The version was already 0.1.0 and does not move. The engine dependency's default arm swaps from the git pin to {:statifier, "~> 2.0"} - statifier 2.0.0 is on Hex, ending the pin arrangement (st-ADR-0066) - with the STATIFIER_PATH override unchanged. The mix.exs comment explaining why the package could not ship goes with the pin. CHANGELOG.md is created with the [0.1.0] section assembled from the three changelog.d/ fragments, deleted here per the fragment protocol; README gains an install section and its Status paragraph drops the nothing-is-published caveat. Before merge, once statifier 2.0.0 is live on Hex, mix deps.get refreshes mix.lock's statifier entry from the git pin to the Hex release and the gate re-runs without the STATIFIER_PATH override. Tagging v0.1.0 and mix hex.publish stay the operator's actions after merge. Refs: sp-989 --- CHANGELOG.md | 89 +++++++++++++++++++++++++++++++++++++++ README.md | 16 ++++++- changelog.d/sp-02x.md | 22 ---------- changelog.d/sp-4an.2.1.md | 42 ------------------ changelog.d/sp-4an.3.1.md | 17 -------- mix.exs | 46 +++++++++++++++++--- 6 files changed, 143 insertions(+), 89 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 changelog.d/sp-02x.md delete mode 100644 changelog.d/sp-4an.2.1.md delete mode 100644 changelog.d/sp-4an.3.1.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fa735f0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,89 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Entries for unreleased work are not written here directly. Each issue drops a +fragment in [`changelog.d/`](changelog.d/README.md); 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.1.0] 2026-08-22 + +First release: the persistence-first execution loop for the +[statifier](https://hex.pm/packages/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. diff --git a/README.md b/README.md index 5ce4f0b..f8a3996 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,24 @@ span days or survive deploys should not need long-lived Session processes at all - but every host currently hand-rolls the loop, the storage guard, and the crash semantics. This package is that loop, packaged. +## Installation + +```elixir +def deps do + [ + {:statifier_persistence, "~> 0.1"}, + # Optional, for the Postgres adapter: + {:ecto_sql, "~> 3.10"} + ] +end +``` + ## Status -Pre-release, under active development. The storage-adapter behaviour with +Early, under active development. The storage-adapter behaviour with its identity guard, the in-memory reference adapter, the run lifecycle, and the Ecto layer (configurable keys/tables, versioned migrations, and -the Postgres adapter below) exist; nothing is published to Hex yet. +the Postgres adapter below) all exist and are conformance-tested. ## The Ecto adapter diff --git a/changelog.d/sp-02x.md b/changelog.d/sp-02x.md deleted file mode 100644 index 03724af..0000000 --- a/changelog.d/sp-02x.md +++ /dev/null @@ -1,22 +0,0 @@ -# sp-02x - -## Added - -- `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. - -## Changed - -- `uxid` is now a required dependency (the default key scheme works out of - the box); `ecto_sql` is an optional dependency and the package compiles - without it. diff --git a/changelog.d/sp-4an.2.1.md b/changelog.d/sp-4an.2.1.md deleted file mode 100644 index 0005a10..0000000 --- a/changelog.d/sp-4an.2.1.md +++ /dev/null @@ -1,42 +0,0 @@ -# sp-4an.2.1 - -## Added - -- Run records on the storage contract: `StatifierPersistence.Storage.Adapter` - gains `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` implements them. -- 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 new 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 new - 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. diff --git a/changelog.d/sp-4an.3.1.md b/changelog.d/sp-4an.3.1.md deleted file mode 100644 index 6623fc2..0000000 --- a/changelog.d/sp-4an.3.1.md +++ /dev/null @@ -1,17 +0,0 @@ -# sp-4an.3.1 - -## Added - -- `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`. diff --git a/mix.exs b/mix.exs index e486ce4..b43eb34 100644 --- a/mix.exs +++ b/mix.exs @@ -15,6 +15,8 @@ defmodule StatifierPersistence.MixProject do name: "StatifierPersistence", description: "Durable stepper and storage adapters for Statifier", source_url: @source_url, + docs: docs(), + package: package(), test_coverage: [tool: ExCoveralls], dialyzer: [plt_add_apps: [:ex_unit]], preferred_cli_env: [ @@ -34,6 +36,43 @@ defmodule StatifierPersistence.MixProject do defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] + # Hexdocs configuration. These paths are read off the publisher's disk at + # `mix docs` time and need no entry in package()'s files: list - the docs + # tarball hexdocs hosts is built separately from the package tarball + # `mix deps.get` fetches. + defp docs do + [ + name: "StatifierPersistence", + source_ref: "v#{@version}", + canonical: "https://hexdocs.pm/statifier_persistence", + source_url: @source_url, + main: "readme", + extras: + [ + "README.md", + "CHANGELOG.md", + "docs/restart-demo.md", + {"docs/adr/README.md", [title: "Architecture Decision Records", filename: "adr-index"]} + ] ++ Enum.sort(Path.wildcard("docs/adr/0*.md")), + groups_for_extras: [ + Guides: ~r{docs/(?!adr)}, + "Architecture Decision Records": ~r{docs/adr} + ] + ] + end + + defp package do + [ + name: "statifier_persistence", + licenses: ["MIT"], + files: ~w(lib mix.exs README.md LICENSE CHANGELOG.md), + links: %{ + "GitHub" => @source_url, + "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md" + } + ] + end + defp deps do [ statifier_dep(), @@ -50,11 +89,6 @@ defmodule StatifierPersistence.MixProject do ] end - # Statifier is not on Hex - it has no package/0 and no tags - so the default - # is a git dep whose SHA mix.lock pins. Note the consequence: Hex refuses to - # publish a package that carries a git dependency, so this package cannot - # ship until statifier is published. That is upstream's call, not ours. - # # The Ecto layer's dependency shape (ecto_sql optional here vs a separate # statifier_ecto package, uxid required, postgrex test-only) is decided in # ADR-0005 - see docs/adr/0005-ecto-in-package-and-postgres-test-harness.md. @@ -64,7 +98,7 @@ defmodule StatifierPersistence.MixProject do # so the override never lands in a commit by accident. defp statifier_dep do case System.get_env("STATIFIER_PATH") do - nil -> {:statifier, github: "riddler/statifier-ex", branch: "main"} + nil -> {:statifier, "~> 2.0"} path -> {:statifier, path: path, override: true} end end From 1a8f846e3c0e096a439e038b02bf3a390d32fa71 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Sat, 22 Aug 2026 18:54:47 -0600 Subject: [PATCH 2/2] Locks statifier 2.0.0 from Hex statifier 2.0.0 is live on Hex, so mix.lock's engine entry moves from the git pin to the Hex release, and the gate runs green against it with no STATIFIER_PATH override - the pre-merge step the release PR named. Refs: sp-989 --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 47c1d6d..e09433f 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,7 @@ "postgrex": {:hex, :postgrex, "0.22.4", "d271f595dfd25230b6398354e19d17bb5e2d20130fd2d9bdca7e15f125d43552", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "4aae45a2d60e35b04eea2602440be152fae332901f1fc7a60fc7cb7f0f9a9c5a"}, "predicator": {:hex, :predicator, "9.0.0", "c15685ce74e249195df9bbaedf7593c50339ac75b8700a3b13f56cfbcc837ff7", [:mix], [], "hexpm", "8e31739e3d448f9f5d91eb716498a0dfe26f3b27c8bcf96bc6b2ce93c0087e95"}, "saxy": {:hex, :saxy, "1.6.1", "742eff28f553c066d0b54e84662dbf384a1d1f38595472ed15f6e0a33038bbe1", [:mix], [], "hexpm", "8989d504424ba29460a61950f8968380651413fa05e63b6118084db057da1a6b"}, - "statifier": {:git, "https://github.com/riddler/statifier-ex.git", "68b814aad5ae0875881a4af05eefbaa81846a7ea", [branch: "main"]}, + "statifier": {:hex, :statifier, "2.0.0", "826cd183113f83cb91f90f41b9e711b7df735219783d996e8ff4a48a40d96c93", [:mix], [{:predicator, "~> 9.0", [hex: :predicator, repo: "hexpm", optional: false]}, {:saxy, "~> 1.6", [hex: :saxy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8026d01db517058c01cf289f428e9a5c3c4c52ae092ddf698aefd5c4be263a50"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "uxid": {:hex, :uxid, "2.9.0", "e61508fa4d0f997995fd3958d0033acceb70f23e5ec85ea3144485b8b9683c61", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "af4a43db3e9f25b7b182eb1870c712224e9a205f39676d98e6cfaaf3fe6029c1"}, }