All notable changes to this project are documented in this file.

v1.4.2 — October 6, 2026

  • Lock: update AshPostgres from 2.14.2 to 2.14.3 after the dependency-currency gate identified the new patch release. Published dependency requirements are unchanged.
  • Documentation: the getting-started, errors, and recipe examples preserve unhandled typed errors for the application’s handler instead of raising on an unlisted code. Release checks exercise every documented code against all four expressions, alongside the complete, safe HTTP mapping in the Phoenix guide.

v1.4.1 — October 6, 2026

Security dependency-floor backport for the 1.4 line. There is no DSL, public API, database schema, persisted response, token wire-format, or migration change.

  • Raise the Ash requirement to >= 3.34.3 and < 4.0.0. EEF-CVE-2026-94201 affects Ash >= 3.5.1 and < 3.34.3; 3.34.3 is the first release outside that range.
  • Raise optional Mint to ~> 1.10 and >= 1.10.2. Mint 1.10.2 backports EEF-CVE-2026-91043, EEF-CVE-2026-92103, and EEF-CVE-2026-94194.
  • Repair the three Livebooks to use explicit release requirements, real Ed25519 verification, and a real independently committed PostgreSQL receipt ledger. The 1.4 notebook behavior remains the behavior of that line; no later runtime feature is backfilled.
  • Contributor setup uses macOS or Linux; Windows developers use WSL2. See CONTRIBUTING.md for the supported setup and executable gates.

v1.4.0 — 2026-09-24

Breaking behavior change (the reason this is a minor, not a patch): a concurrent same-key retry of an external-effect action that previously waited out the original and replayed may now return :request_in_progress within the configured wait. Clients already treat that code as wait-or-retry; consumers running check-then-act peers are the direct beneficiaries.

Added

  • Pre-peer claim lock for external effects (ADR-0010). Before any peer call, the package takes the claim row FOR UPDATE in the caller's open transaction with a bounded wait — the new per-protection external_lock_timeout_ms option (default 2000, validated 1..25_000 — 5s of headroom under the committed-claim worker kill, requires external_effect). The same-key retry's committed-claim worker arms the same bound. A lock timeout maps to :request_in_progress (the local path's existing concurrent-retry semantic) and is terminal. The lock is generation-safe: it binds the claim id alongside the logical key, so a reaped-and- reinserted row fails closed (:store_invariant) instead of being adopted. Motivation, observed on live PostgreSQL: without the lock, an honest in-flight retry truthfully recovered :absent and executed under the same operation key while the original's execute was still in flight — and a check-then-act peer (conforming to the contract as published at v1.3.2) double-spends in exactly that window. With the lock, two executes under one key cannot overlap at the peer; dead-caller recovery is unchanged (test/ash_onetime/external_contention_test.exs, test/ash_onetime/store/contention_test.exs). The mutation battery pins the lock twice: effect-lock-generation (id bind defeated while kept, reds on generation adoption) and pre-peer-lock-removed (the lock call removed, reds on the unlocked race — B's red-proof). The lock_timeout GUC is scoped to the lock: captured before, restored after — it never caps the adapter's own writes. The internal AshOnetime.Store grows lock_for_effect/3 and an options-taking claim_committed/3 (the 2-arity facade is unchanged).

Decisions (ADRs 0009-0011, each adversarially reviewed and judged)

  • ADR-0009 (Rejected): reap-surviving derived operation key. A stateless derivation survives retention too — a post-retention retry would replay the prior generation's peer result instead of executing (a disclosed duplicate traded for an undisclosed no-op); a stateful one is correct only under an unknowable peer key-retention horizon; and the operation key is published callback API, so a derived key breaks every conforming adapter's claim join. The operator-side defense stays: business-level reconciliation before enabling the reaper on external-effect actions.
  • ADR-0011 (Rejected for wall-clock): library-side callback bounding. The only in-package bounding mechanism (timed_callback/4's Task) runs the callback outside the caller's transaction, silently breaking transactional adapter writes. The DB-resource bound (opt-in idle_in_transaction_session_timeout) and a configurable immediate post-ambiguous recover/3 are recorded as candidates with named decision criteria.

Documentation

  • External-effect contract corrected (design review 2026-09-24; decision records under .kimosabe/specs/):
    • ADR 0001's claim that a duplicate side effect requires BOTH a lying adapter AND a failing peer is corrected by a dated amendment: an honest in-flight retry alone produced concurrent same-key executes (now prevented by the lock; the peer-side residue — a sequential lying :absent — keeps key idempotency a MUST, with atomic key claims a SHOULD).
    • The adapter execution environment is normative: both effect callbacks run inside the caller's open PostgreSQL transaction (observed per callback, including the caller's backend idle in transaction on pg_stat_activity) and the package applies no timeout to them; adapters MUST bound their own peer call, and an ambiguous execute is followed by recover in the same transaction (up to two adapter timeouts of hold).
    • Reaping an external-effect claim documented honestly: the operation key is the claim UUID, so a post-reap retry runs under a new key and no key-based defense remains at either layer (external-effects guide, operations guide, ADR 0002 amendment).
    • New :absent vs :unknown mapping table for real HTTP peers (the 404-under-replica-lag double-spend trap) plus a worked bounded HTTP adapter in the external-effects guide.
    • New Recipe 4 — external effect via a transactional outbox (the outbox row as the ExternalEffect peer, keyed by the operation key, committing with the action), including the acceptance-receipt classifier rule, the race-absorbing on_conflict key claim, and the receiver-side cross-generation deduplication boundary.
    • :request_in_progress documented on the external path; the previously unlisted :worker_timeout added to the error table.

v1.3.2 — 2026-09-20

Security

  • Optional Mint floor raised to ~> 1.10 and >= 1.10.1 following ADR 0004: mix hex.audit flagged EEF-CVE-2026-82672 against the locked 1.10.0 — an unvalidated chunk-size line tail in Mint's HTTP/1 client enabled response smuggling against strict intermediaries on pooled connections. 1.10.1 (2026-09-19) carries the fix. Mint stays optional: true and runtime-excluded: the floor binds only hosts that already carry the installer closure, and no core consumer pulls it. The package check, optional-integration matrix, and dependency-security tripwires move together. OBSERVED: mix hex.audit clean after the raise.

  • Lock: ash 3.33.4 → 3.33.8 and ash_sql 0.7.5 → 0.7.6, riding the dependency-currency gate (resolver-updatable drift). The published floors are unchanged — >= 3.33.4 and < 4.0.0 for Ash remains the CVE-derived minimum; mix hex.audit reports no advisories against the new lock.

Added

  • AshOnetime.Verified.new/1 — the sanctioned constructor for the opaque Verified type. t stays opaque — a Dialyzer-enforced construction convention: consumer code that builds the struct literal violates the opacity contract (contract_with_opaque, OBSERVED at 1.2.3 in a consumer sweep; suppressed there pending this release). Opacity is not the runtime anti-forgery boundary — that remains the admission path (DSL-configured verifier/minter callbacks under a bounded context, reserved-input rejection). new/1 validates at mint time what every internal consumer holds a Verified to: key and verifier_id non-empty binaries (verifier_id at most 128 bytes), issued_at and the optional expires_at well-formed DateTimes with expires_at at or after issued_at; unknown, duplicate, and missing keys are rejected, and the constructor never raises. Purely additive: no existing export changed (the export census grew new: 1), the Elixir requirement and dependency floors are unchanged. The nonce livebook, its walkthrough mirror, and the transaction-owned admission guide now demonstrate the constructor; CI gained a test-env dialyzer leg so the consumer-shaped opacity fixture stays analyzed.

v1.3.1 — 2026-09-17

No consumer-facing changes: the published Elixir requirement (~> 1.20), every dependency floor, and the public API are identical to v1.3.0. This release hardens how the project itself builds and verifies — on every platform it supports.

Changed

  • Toolchain identity is enforced in-repo, before anything compiles. The Elixir support window stays ~> 1.20; config/config.exs now refuses any Erlang/OTP build outside the supported set (28, 29). System.version/0 does not encode the OTP build, so a same-Elixir foreign build previously compiled silently and poisoned shared _build/PLT state. The guard never ships — config/ is excluded from the package — and its set grows only in the same commit that adds a CI leg: a dedicated otp-28 CI job now proves the 28 end, while .tool-versions (Elixir 1.20.4 on OTP 29.0.3) and the primary CI runtime hold the 29 end.
  • macOS/Linux/Windows capability is a hard requirement, not a preference. Every POSIX-only tool in the developer surface is gone: the dependency-currency gate moved from a bash script to scripts/check_deps_currency.exs (same exit-code contract; all four outcomes proven — green, resolvable drift, deliberate pin, unparsable table), every System.cmd("mix", ...) call site in tests and battery scripts spawns through AshOnetime.Portable (cmd /c wrapping for Windows' .cmd shims), and .gitattributes (* text=auto eol=lf) keeps a Windows checkout byte-exact for mix format --check-formatted. CI proves the weakest platform continuously: a windows-build lane (clone, deps, format, compile, audits, credo, currency gate, hex.build) and a windows-db-battery lane that installs PostgreSQL 18 natively via chocolatey (service containers are a Linux-only feature) and runs the full mix test. The heavier release gates (mutation battery, unpacked-package check, optional-integration matrix) remain ubuntu-proven and are named as such in the workflow.
  • Test database substrate: docker compose up -d provisions the exact PostgreSQL 18 the harness requires, with the host port taken from .env (PGPORT, default 18841 — the same port CI pins) so the dedicated instance never collides with another local listener; .env.example documents the PG* variables, the fail-closed DATABASE_URL, and HEX_API_KEY. The harness still fails closed — on the dedicated shape rather than one string: ecto scheme, postgres user, loopback host, ash_onetime_test name, any port except the shared default 5432.
  • Dependency currency is mechanical: scripts/check_deps_currency.exs exits nonzero on resolver-updatable drift (mix hex.outdated "Update possible") and prints blocked packages with the requirement chains holding them. OBSERVED: dialyxir 1.4.7 → 1.4.8, ex_doc 0.40.3 → 0.40.4, and oban 2.23.1 → 2.24.1 moved to latest; mix hex.audit reported "No retired or security advisory packages found" before and after the move.

v1.3.0 — 2026-09-16

Security

  • Breaking dependency change: raise the consumer floors to Ash 3.33.4, AshPostgres ~> 2.13, AshSql 0.7.1, Igniter 0.8.4, and Mint 1.10.0, following ADR 0004. OBSERVED: mix hex.audit on September 15, 2026 reported 26 advisories against the previous lock; the OSV ranges and Hex release metadata identify these patched minimums. EEF-CVE-2026-86338 (published September 16, fixed only in Ash 3.33.4) moved the Ash floor above the initial 3.33.0 choice before release — CI's floor-cell audit caught it. The doctor, CI floor, and package check move together. Mint is optional and excluded from this package's runtime application list; existing optional integrations retain their startup boundaries. Ash 3.33 additionally requires the consumer config config :ash, default_string_length_count: :codepoints (or :mixed) — without it an application's own resources fail to compile; see the upgrading guide for the full consumer steps, including this one.
  • Lock: ash 3.32.0 → 3.33.4, ash_postgres 2.11.0 → 2.13.1, ash_sql 0.6.6 → 0.7.5, igniter 0.8.3 → 0.8.4, mint 1.9.3 → 1.10.0, with req 0.7.4, spark 2.7.3, and spitfire 0.4.1 riding the resolution.

Changed

  • Test harness: Ash 3.33's mandatory string-length counting mode is set to the recommended :codepoints (config/config.exs), and the compile-fixture subprocess runners start Mix and mirror the mode (the fresh VMs read no config). The dynamic-repo transaction test's readiness receives and the test pool's checkout queue were widened to bounded patient windows — the default 100ms/6s ceilings flaked under scheduler and host load; no fail-closed semantics changed. The optional-integration matrix script now also observes the resolved Igniter and Mint versions against the security floors, gains conflict proofs (exact advised pins must fail to resolve), and joins the release-gate battery.

v1.2.3 — 2026-08-27

Changed

  • Runtime application closure of the package's .app spec — plug, oban, igniter, and stream_data now carry runtime: false, so none of them appear in the generated applications list anymore. Before this release, every host that carried one of the optional integrations in its own dependency closure inherited it as a runtime application of ash_onetime (the extension's app spec claimed its boot and release closure), and every host inherited stream_data unconditionally. The opt-in contract is unchanged: the Plug module, the Oban workers, and the Igniter installer compile exactly when the host itself lists the dependency (the optional-dependency matrix passes unchanged), and the published Hex requirements still mark the three integrations optional: true. stream_data remains a published requirement because Ash itself requires it transitively in every environment — Mix rejects an only: :test restriction alongside Ash's unrestricted entry — but it no longer contributes to this package's runtime applications. Dev-side, the dialyzer PLT now names :oban, :plug, and :igniter explicitly, since runtime: false deps are no longer seeded into the PLT automatically while the guarded integration modules still analyze against them.

v1.2.2 — 2026-08-25

Added

  • AshOnetime.Transaction.claim_id/1 — the one sanctioned accessor for a fresh admission's claim UUID (Admission stays opaque by contract). The UUID is the durable address of the claim inside the store: persisting it lets a host correlate its own invocation records with the claim and hand the unchanged UUID to an external execute/recover peer. A pure read (no process/transaction/store interaction); test-pinned against the stored claim row, and the architecture export inventory is updated.

v1.2.1 — 2026-08-22

Changed

  • README: a mutation-battery badge (130 sentinels, each proven RED under its own mutation before it is trusted) and an explicit test-assurance statement — every library module at 100% line coverage, with the mutation battery as the assurance bar. Documentation only; no code, DSL, or contract change.

v1.2.0 — 2026-08-22

Additive operations-preflight and hardening release. No breaking change, no DSL/contract change, no migration.

Added

  • mix ash_onetime.doctor --live — opt-in schema-currency preflight: connects to the database read-only and fails when the installed schema is not current for the running package (missing logical_partition columns, response-payloads table or its _default partition, cleanup/reap functions by exact arity, or delete-guard triggers) — the upgrade-without-migrating failure mode, caught at preflight instead of at the first admission after deploy.
  • Backup/restore runbook (documentation/operations.md) — the restore-from-backup procedure: per-strategy exposure of a rewound admission store (bounded nonce windows, fresh re-execution of post-restore-point idempotent effects), reconciliation scoping, and the post-restore partition roll-forward.
  • PostgreSQL floor documentation (README, operations.md) — the SQL surface requires PostgreSQL 11+ (declarative hash/range partitioning with default partitions, SKIP LOCKED, pg_advisory_xact_lock(bigint)); CI exercises PostgreSQL 18 and versions below it are unverified.

Fixed

  • Store digest comparisons unified to constant-time — the last two plain-== digest comparisons in the store (the stored-payload digest check on replay load and the caller-digest check at completion) now use a size-guarded :crypto.hash_equals helper, one convention for every digest comparison site in the library. No behavior change: neither site compares attacker-secret material, and both operands were already 32-byte-constrained.
  • Test suite: the RollContentionTest full-suite flake (~10%/run) is eliminated at the mechanism — Sandbox.start_owner!/2's asynchronous teardown could leave the caller's ownership allowance in place long enough for a back-to-back second owner in the same process to crash with {:already, :allowed}. Same-process sequential owner sites now use an in-process checkout/checkin helper (AshOnetime.Test.RealConnection), with a 100-cycle tripwire pinning the class; the timing-dependent telemetry-flush and ETS-TTL sleeps are deterministic. No library code changed.
  • Test suite: direct unit coverage for the admission decision functions — every resolve/5 arm (all execution_class/2 mappings, transaction-mode/locator/identity/ claim-state invariant rejects, :complete replay and fingerprint mismatch, :processing per mode, :collision match/malformed, the exact-shape :execute_untracked escape with a fail-closed test per guard dimension, the store_error fallthrough) plus the request/claim sanitizers, exposed as @doc false test seams and pinned by the architecture census. The dev-build seam-absence tripwire was also de-vacuated: its subprocess probe now loads the module before function_exported?/3 (an unloaded module probes false unconditionally).

v1.1.0 — 2026-08-20

Additive transaction-owned admission for applications whose effect boundary is an existing Ecto transaction rather than an Ash action.

Added

  • AshOnetime.Transaction — public idempotency/2, nonce/2, and complete/2 operations that require an existing PostgreSQL READ COMMITTED transaction and never start or commit one. Idempotency replays exact response bytes; nonce collisions reject; typed errors and all claims remain bound to the caller transaction.
  • Logical partitions — a bounded UTF-8 authority component included in every claim, collision identity, response payload locator, cleanup path, and cache key. Identical operation/scope/key triples in distinct partitions are independent without importing a host application's tenant types.
  • mix ash_onetime.gen.logical_partitions — reversible upgrade migration for 1.0 installs. Existing rows backfill to global; rollback refuses while non-global authority exists.

Compatibility

  • Existing Ash resource protections continue in the global logical partition and retain their existing public behavior.
  • Fresh generated installs include logical partitions. Existing installs run the new upgrade task once before admitting non-global transaction-owned work.

Documentation

  • Updated Getting Started, Upgrading, README status, and every runnable Livebook to select the 1.1 package line. All three notebooks now link the transaction-owned admission guide, and a documentation-currency tripwire keeps their install requirements aligned with mix.exs.

v1.0.0 — 2026-08-19

The 1.0 stability release: the surface contract is frozen (published in full in upgrading.md — docs are the contract; breaking changes to the documented surface now happen only at 2.0, with reserved break-rights for security, behavior-correcting bug fixes, and new compiler/linter warnings). Pre-1.0 (0.x) releases were explicitly non-binding; 1.0.0 is the compatibility baseline. The persisted response format (ADR-0007) and the token wire format (ADR-0008, window-bounded) are frozen compatibility surfaces.

Fixed

  • Ash requirement pin validation (release tooling): ASH_ONETIME_ASH_VERSION is now compile-time validated — it must parse as a version AND lie within the published requirement range; pre-release and build suffixes are rejected; a release exported with the variable unset is asserted by the package gate to carry the floating requirement in the archive metadata.
  • Telemetry contract (AshOnetime.Telemetry): the default attach/0 router now covers the full closed surface — all 13 events including the :uncertain_exception diagnosis event (previously silently dropped); the moduledoc was rewritten to the router reality (the phantom metrics/0 reference is gone); the diagnosis event's closed atoms-only metadata shape (%{strategy, phase, exception}) is validated before emission.
  • Admission test seam: the Process-dictionary test-store redirect in the internal Admission module is no longer compiled under Mix.env() == :test — it is gated on Application.compile_env(:ash_onetime, :allow_admission_override, false), default-off in every build environment (mirroring the token verify-clock gate). A consumer building deps with MIX_ENV=test no longer ships the redirect; calling the seam without the explicit opt-in now raises a named error.

Documentation

  • ADR-0008 records the token wire-format compatibility rule (window-bounded: a 1.x reader verifies tokens minted by any earlier 1.x writer while inside their acceptance window; no permanent freeze) alongside the persisted response-format rules (ADR-0007).
  • The operations runbooks' diagnostics are all executable (each snippet verified against a live database): the vacuous default-partition count became a stranded-row count, the non-runnable IEx backlog snippet was replaced by its SQL path, and the pool diagnostic is a checkout-queue watch on the repo query event.
  • Worker-backoff records corrected (ADR-0005 and every prose carrier): Oban's default is ~36–40 s at three attempts, not hours; the custom backoff's rationale is re-derived honestly (spaced DDL-class retries, wide jitter, 120 s cap).
  • ADR batch: ADR-0001 (external-recovery :absent trust boundary), ADR-0003 (live committed-claim failure taxonomy), ADR-0004 (forward-compatible floor posture) amended; ADR-0006 (point-events observability) and ADR-0007 (persisted response-format compatibility for 1.x) added.
  • SECURITY.md publishes the support and disclosure policy (supported-versions table, 7-day ack / weekly updates / coordinated disclosure); CI battery claims name the per-cell battery precisely; the ROADMAP is reconciled (portable status column, all eleven rows verified).

v0.7.0 — 2026-08-18

Security release: raises the Ash floor to 3.31.3 after EEF-CVE-2026-67579. Breaking dependency change — consumers on Ash 3.29.3–3.31.2 must bump Ash to ≥ 3.31.3 (the library is pre-1.0, so the minor version carries the break per the documented convention).

  • Ash floor raised to 3.31.3 (from 3.31.1). EEF-CVE-2026-67579 (filter expression injection via a forged keyset pagination cursor — HIGH, CVSS 7.5, unauthenticated, no application-side workaround) affects Ash below 3.31.3 and is fixed only in 3.31.3; Hex additionally retired 3.31.1. The published >= 3.31.1 requirement let consumers resolve a retired or advised Ash, and mix hex.audit — a required gate — failed on the 3.31.2 lock. The floor was derived from the complete OSV advisory inventory for hex ash (11 advisories; maximum fixed-version 3.31.3; 3.31.3 not retired), not from the absence of a Hex label. See ADR-0004 (Security-driven Ash floor, amended). The CI matrix moves from [3.31.1, latest] to [3.31.3, latest] — transiently both cells resolve to 3.31.3 until the next Ash release.
  • Doctor floor guard directly tested: the doctor's Ash-floor verdict is extracted as a pure floor_status/1 seam with reject tests for the retired 3.31.1 and the advised 3.31.2 (red-proven before the floor bump), plus a mutation-battery entry (doctor-ash-floor) that re-proves the guard red-capable on every release run. The Oban queue advisory verdict is likewise a pure seam (oban_queue_status/2) with both arms — Oban loaded and not loaded — covered. Doctor output is unchanged; the three strict-Credo findings that red-barred the latest-Ash CI cell are fixed.
  • Lock: ash 3.31.2 → 3.31.3 (with ecto 3.14.2 and reactor 1.0.6 riding the resolution).

v0.6.0 — 2026-08-10

Minor bump: two additive enhancements (new Mix task + Phoenix guide) plus internal perf/cleanliness gap closures. No breaking change, no consumer upgrade action.

  • mix ash_onetime.doctor — a read-only upgrade-preflight that checks the Ash security floor (fatal if below 3.31.1), Oban queue configuration (advisory — warns on a missing :ash_onetime_partitions queue that strands the retention-safety path), and prefix validity. Mirrors the runtime Mix task family; run after install and on each upgrade.
  • Phoenix integration guide (documentation/phoenix.md) — a runnable Phoenix controller recipe wiring the Plug, the replayed?/1 → 200/201 + Idempotent-Replayed header signal, and the error-code → HTTP-status mapping into a complete controller pattern. Closes the gap where every existing recipe stopped at plain Elixir tuples.
  • Cleanup delete-guard probe partition-scoped — the :complete-branch probe in the install migration now constrains partition_date = OLD.response_partition (the H1 read-path tail), turning an O(partitions) scan into a point lookup on every completed-claim delete. Same property shift as H1 (partition pruning vs cross-partition detection are mutually exclusive); the write path remains the authoritative guard.
  • Dead trusted_context parameter removed from the internal scope/key resolution path (admission.ex prepare_resolved/scope_hash/scope_component/key_hash/key_component). No behavior change — the parameter was threaded but never read after M1's collapse.
  • L4 monitoring blind spot closed — a stuck-available PartitionWorker query added to operations.md (the discard-alert watched discarded only; an unconfigured queue leaves jobs in available forever with no signal).

v0.5.1 — 2026-08-10

Patch: test-only hardening. No consumer-visible change; no upgrade action required.

  • Invariant tripwire hardening (test-only): the five test-coverage residuals the v0.5.0 closeout accepted as documented gaps are closed. Each tripwire now observes the production function it claims to pin, and every one is proven RED-capable by mutation (mutate the production line, watch the test fail). The Oban worker error-tuple tests force a REAL :lock_timeout through database lock contention rather than reading source text. No production behavior change.
  • Internal test seams: three pure helpers are now @doc false public callables so their contracts can be pinned directly — Store.Postgres.roll_advisory_key/1, Cache.key/1, Resource.Verifier.verify_required_shape/2. Undocumented, not a supported API.
  • Lint: mix credo --strict now passes clean (implicit-try and cond→if refactors in the ETS cache adapter and the telemetry default handler — behavior-identical).
  • Dependency: bumped ash from the retired 3.31.1 to 3.31.2 (within the existing >= 3.31.1 and < 4.0.0 pin). 3.31.1 was retired upstream for a breaking bug.

v0.5.0 — 2026-08-10

Minor bump: sixteen findings (M1–M5, L1–L11) from the v0.4.0 independent code review. Four are CONSUMER-VISIBLE (read documentation/upgrading.md before upgrading); the rest are internal hardening.

  • Independent code-review fixes (M1–M5, L1–L11): sixteen findings from the v0.4.0 independent code review, landed in four surface-cohesive clusters:

    • M1 — bounded callback context: verifier/mint/scope callbacks now receive exactly %{resource:, action:} (the prior code ran a dead Map.take(trusted_context, [:keys, :now]) and threaded caller actor/tenant that no callback read; both dead paths removed). Least-privilege contract pinned by an admission test.
    • M2 — reserved-input compile check now matches the runtime guard (CONSUMER-VISIBLE): a protected resource declaring a reserved-named attribute (:key/:issued_at/...) with no accept now fails to compile (previously caught only at runtime by reject_reserved/1). Rename any such attribute (e.g. :idempotency_key).
    • M3 — @reserved single-source: the reserved-input list is now AshOnetime.reserved_verification_inputs/0, shared by the transformer (compile-time) and Admission.reject_reserved/1 (runtime); the two copies can no longer drift.
    • M4 — verifier defense-in-depth: the Spark verifier now re-asserts strategy in [:idempotency, :one_time_nonce], non-nil scope, non-nil key (mirroring the transformer), catching a transformer regression before runtime.
    • M5 — clock override gate: @allow_test_clock_override (Mix.env() == :test, deployment-fragile) replaced by Application.compile_env(:ash_onetime, :allow_clock_override, false) — the verify-side :clock override is off by default in every build; the test suite opts in via config/test.exs.
    • L1 — constant-time comparator consolidated onto :crypto.hash_equals/2 (the hand-rolled XOR-reduce is gone); the verify/3 rescue is documented as load-bearing for wrong-length signatures (hash_equals/2 raises on unequal length — verified empirically).
    • L2 — reap floor enforced at the guard: reap/3 rejects sub-86_400 callers with :invalid_request (no DB round-trip) instead of the migration's 22023 misclassified as :store_invariant.
    • L3 — per-tenant advisory lock: the partition-roll lock key is derived per-prefix, so distinct tenants roll concurrently (within-tenant serialization preserved). Cannot reintroduce a CREATE PARTITION OF race (per-schema partitions, distinct OIDs).
    • L4 — dedicated :ash_onetime_partitions Oban queue (CONSUMER-VISIBLE): PartitionWorker moves off the shared cleanup queue so forward partition creation (retention-safety) does not compete with routine cleanup under saturation. Operators must configure the queue. documentation/operations.md updated.
    • L5 — worker error tuples carry the inner reason (CONSUMER-VISIBLE): {:error, :tag} → {:error, {:tag, reason}} so the distinguishable store cause survives Oban exhaustion. Consumers pattern-matching the old bare atom must update to the 2-tuple inner shape.
    • L6 — store-transaction exception telemetry: committed_claim_transaction rescue emits [:ash_onetime, :uncertain_exception] with the exception class before collapsing to :dispatched_unknown. Telemetry-only posture preserved (no Logger).
    • L7 — cache key length-prefix: defense-in-depth framing for the cache key (today's fixed-32-byte components have no ambiguity; the framing future-proofs against a variable-length change).
    • L8 — roll_forward OID scoping: the _default partition OID subquery is now namespace-scoped (pg_namespace), matching the schema-scoped existence checks.
    • L9 — change/generic_action dedup: dispatch_reservation/trusted_context/ unavailable_error hoisted to Admission (the near-verbatim duplicates removed).
    • L10 — ensure_callbacks cycle diagnostic: Code.ensure_compiled now distinguishes the :unavailable/:module_unavailable cycle case with clear ordering guidance.
    • L11 — prepare/3 spec dead arm removed (the Result.t() arm never matched); dialyzer confirmed the downstream store_error/1 was dead code and it was removed.
  • Replay read prunes to the claim's payload partition (H1): the idempotency replay read path (load_payload/2) now constrains its ash_onetime_response_payloads query by partition_date as well as claim_id, turning a replay of a completed claim into a primary-key point lookup on the single monthly child partition instead of a scan of every monthly partition. The cost of a replay no longer grows with partition count (retention age). No behavior change on the authoritative path — the returned payload and the digest/partition-mismatch failure arms are unchanged. One incidental property changes: the read no longer detects a stray payload row in a different partition from the claim's authoritative one (this is necessarily dropped by partition pruning); cross-partition cardinality remains enforced at write time (update_complete) and re-asserted by the cleanup delete guard. ADR-0001 gains a read-path performance subsection. The production predicate is pinned by a registered mutation test; an EXPLAIN-based mechanism proof documents the pruning directly.

v0.4.0 — 2026-08-09

Hardening, ops-readiness, and enhancement release. No breaking contract change for existing consumers — the minor bump carries new public observability surfaces (the telemetry default attach helper, the :worker_timeout result class, the ETS cache reference adapter) and the span-events-out-of-scope decision. Existing consumers are unchanged; all new surfaces are opt-in.

  • External-recovery adversarial-absence proof (H10): a test proving the re-execution invariant against a lying-:absent adapter, plus a normative section in documentation/external-effects.md stating the adapter MUST prove absence and the peer MUST enforce idempotency by operation key. No runtime guard — the trust is inherent to the design (ADR-0001). The library's :absent trust is safe against a correct peer.
  • Worker timeout distinguished from disconnect (H11): the committed-claim worker's 30s timeout now surfaces as a distinct :worker_timeout result_class on [:ash_onetime, :store_uncertainty], separate from :disconnected and :unknown. All three fail closed; the distinction is operational triage (pool/lock contention vs network partition).
  • Oban worker backoff + discard alert (H20, ADR-0005): the three maintenance workers (Cleanup, Partition, Reap) declare a bounded, jittered backoff/1 (30–120 s) instead of the default exponential, so transient failures retry within the retention window. A documented discard-alert SQL names the operational signal for a stranded partition roll.
  • Telemetry default attach handler (H21): AshOnetime.Telemetry.attach/0 — an opt-in helper routing the closed event surface into a downstream :metric stream for a consumer's own aggregator. No telemetry_metrics dependency; the value-free invariant is preserved.
  • Telemetry span structure (H22): documented that the library emits point events only (never span events), with the reason (:telemetry.span/3 cannot preserve the value-free invariant — it force-injects telemetry_span_context and fires :exception inside the span before any caller rescue) and a recommended consumer-applied :telemetry.span/3 wrapper.
  • Operations runbook (H23): three named procedures (backlog-stuck, partition-discard-detected, pool-saturated) with exact SQL/telemetry queries.
  • ETS cache reference adapter (H30): AshOnetime.Cache.Ets — bounded, TTL-aware, supervised, no third-party dependency. Makes the cache-degradation path reachable without a Redis dependency.
  • Admission unit tests (H31), key_source/claim property tests (H32): direct test coverage for the pure decision functions and the security-boundary invariants (the suite grows from 500 to 553 tests).
  • Runtime security-surface docs (H33): @doc on token.ex, key_source.ex, fingerprint.ex, telemetry.ex public functions.
  • CI-matrix-asserted compatibility documented (H34): CONTRIBUTING names the CI matrix as the guard against transitive semantic drift (not the dep bounds).

v0.3.0 — 2026-08-09

Security release: raises the Ash floor to the CVE-patched 3.31.1 and makes the architecture census robust to dependency framework-injection drift. Breaking dependency change — consumers on Ash 3.29.3–3.31.0 must bump Ash to ≥ 3.31.1 (the library is pre-1.0, so the minor version carries the break per the documented convention).

  • Ash floor raised to 3.31.1 (from 3.29.3). EEF-CVE-2026-70395 (predicate injection in manage_relationship belongs_to lookup disclosing secret lookup keys) and EEF-CVE-2026-69659 (memory exhaustion via unbounded keyset-cursor deserialization) affect Ash below 3.31.1; both are patched in 3.31.1. mix hex.audit — a required gate — now fails on the 3.29.3 lock. Consumers pinned to Ash 3.29.3–3.31.0 must bump Ash to ≥ 3.31.1. See ADR-0004 (Security-driven Ash floor). The CI matrix narrows from [3.29.3, 3.30.1, latest] to [3.31.1, latest].
  • Export census version-tolerance (test harness): the documented public-export census no longer breaks on Splode/Ash framework-injection drift. It derives each module's project-authored exports from its own source AST, so framework-injected functions (e.g. Splode 0.3.2's keyword_list_options?/0) are auto-classified rather than hand-maintained, and the census guards exactly the project-owned public surface. This fixed a CI failure present since v0.1.0 across all Ash-matrix cells. Also fixes a seed-dependent flake in the ARCH-1 Splode-membership assertion (force-loads the module before the check).

v0.2.0 — 2026-08-09

Feature release: the DPoP replay fence, forward response-partition maintenance (SEC-5/6), and notebook-per-concern docs. New user-facing capabilities (minor bump); no contract break since v0.1.1 — existing consumers are unchanged, all new surfaces are opt-in.

  • DPoP replay fence (admission): add commit: :independent (default :with_action) on :one_time_nonce protections. When set, the nonce claim commits in its own transaction before the action body runs (via the existing claim_committed worker), so a body failure cannot make the proof reusable — RFC 9449 §11.1 request-attempt scope. A reused proof within the acceptance window is rejected with :nonce_already_used via the existing collision path. Opt-in: existing nonce consumers are byte-for-byte unchanged. Rejected (compile error) on :idempotency. No new table, migration, error code, or telemetry event. See ADR-0003 (Independent-commit nonce). Operational note: the claim_committed worker uses a +1 connection checkout per in-flight protected request and a 30s timeout (fails closed); size the pool accordingly.
  • SEC-5 (data-layer): add forward monthly response_payloads partition creation (Store.roll_partitions/2, mix ash_onetime.roll_partitions, AshOnetime.Oban.PartitionWorker) plus a one-time forward migration (mix ash_onetime.gen.roll_forward). The install migration generates a fixed 13-month window; once retention exceeds it, payloads routed to _default and were never dropped (the drop path excludes _default), silently defeating bounded retention (ADR-0001:65 "reuse after retention is a new execution"). The roll keeps the window ahead of retention; the forward migration back-fills elapsed partitions, adds the index, and drains past-retention payloads stranded in _default via a claim-scoped delete. The roll is idempotent and concurrency-safe (advisory-locked, bounded lock_timeout). Existing installs run the forward migration once.
  • SEC-6 (data-layer): add an index on ash_onetime_idempotency_claims(response_partition). Cleanup's partition-empty check was a full scan per partition per cycle.
  • Telemetry: add :partitions_created to the [:ash_onetime, :cleanup] event's result-class enum (closed-schema extension, pinned by a mutation fixture). See Telemetry.

v0.1.1 — 2026-08-08

Adoption polish: a runnable Livebook walkthrough, richer Igniter installer, and adoption docs. No contract change; safe minor bump from v0.1.0.

  • Add runnable Livebook notebooks — one per concern (idempotency, one-time nonces, external recovery) — covering fresh execution, replay, fingerprint conflict, nonce spend/reuse, and the external-effect protocol end-to-end against a real PostgreSQL. Each notebook's code is regression-pinned by test/ash_onetime/livebook_walkthrough_test.exs.
  • Extend the Igniter installer with a repeatable --resource MyApp.MyResource flag that wires AshOnetime.Resource into a resource and scaffolds a starter onetime block. Non-resource targets and missing modules are rejected loudly instead of silently no-op'ing.
  • Add adoption docs: Recipes, Telemetry, Upgrading, and FAQ. Add a README "When to use this vs. hand-rolled idempotency" section and a "Try it" livebook pointer.
  • Reorder Getting started to lead with the consumer quickstart (install → protect → handle the result); move the test-DB harness to CONTRIBUTING. Fix the stale "not published yet" line (the package is live on Hex).

v0.1.0 — 2026-08-08

  • Breaking (DSL): collapse the dual limits surface into a single protect-level vocabulary. Response-size limits can no longer be declared on the response entity (response ..., limits: [...]); declare them on protect instead. The protect limits: option now accepts the full 11-key union vocabulary: max_key_bytes, max_token_bytes, max_scope_components, max_fingerprint_bytes, verifier_timeout_ms, max_cache_entry_bytes (key/verification/cache paths), and max_response_bytes, max_response_depth, max_response_nodes, max_response_entries, max_response_scalar_bytes (response payload). All keys are validated at compile time. To migrate, move any response ..., limits: [max_response_*: ...] keys onto protect ..., limits: [...].
  • Make AshOnetime.Error a Splode error of class :invalid so Ash recognizes it and preserves the typed :code through the action pipeline. Before, a protected-action failure was wrapped as Ash.Error.Unknown.UnknownError and the code (e.g. :nonce_already_used, :key_reused_with_different_request) was lost before it could reach the caller. Add AshOnetime.Error.code/1 to recover the code from a leaf or class wrapper. See documentation/errors.md for the code→HTTP table.
  • Add a caller-visible replayed-vs-fresh signal. After Ash.create/2 / Ash.run_action/2 returns, AshOnetime.replayed?/1 reports whether the result was a stored replay (true), a fresh execution (false), or carries no signal (nil — untracked execution, primitive-return action, or unprotected). The signal rides __metadata__[:ash_onetime] for tracked admission classes; :untracked is deliberately not stamped to preserve untracked transparency. See documentation/replay.md.
  • Broaden the Ash dependency requirement from ~> 3.29.0 (only 3.29.x) to >= 3.29.3 and < 4.0.0, so the package installs across the whole Ash 3.x line. The floor is 3.29.3, not 3.29.0: EEF-CVE-2026-55736 (private action arguments settable by user input) affects Ash 3.29.0–3.29.2 and is fixed in 3.29.3.
  • Add a CI compatibility matrix (.github/workflows/ci.yml), configured to run the full gate battery — including mix hex.audit — against the 3.29.3 floor, each intermediate minor, and a floating latest Ash 3.x cell on every push and pull request once the repository is pushed to a GitHub remote.
  • Establish the standalone Mix package, PostgreSQL 18 test harness, package boundary checks, accepted architecture decision, and project documentation.
  • Add the per-action Spark resource DSL, normalized introspection, precompile rejection boundary, fail-closed runtime stubs, compile-fixture battery, and mutation proofs.
  • Add PostgreSQL-authoritative idempotency and one-time nonce admission with exact operation, scope, key, fingerprint, transaction, and failure-direction invariants.
  • Add transactional CRUD and generic-action execution, classified typed response persistence, digest-bound replay, and replay-safe lifecycle enforcement. The response contract digest binds the codec options, so stored bytes cannot be reinterpreted under changed options; replayed results carry the same selected/tenant metadata as a first execution.
  • Enforce the configurable max_scope_components, max_fingerprint_bytes, and max_response_bytes limit overrides at their declared values, not only their package ceilings.
  • Add bounded canonical encoding, HMAC-SHA-256 and Ed25519 signing, trusted verification facts, self-identifying tokens, and inclusive nonce windows.
  • Add committed external-effect recovery points, stable peer operation keys, conservative ambiguous-outcome handling, and crash recovery.
  • Add unpartitioned and operation-hash-partitioned claim layouts, date-partitioned response payloads, strict bounded cleanup, deletion guards, the prune task, and optional Oban cleanup.
  • Add PostgreSQL-gated cache degradation, optional Plug header extraction, closed value-free telemetry, deterministic Igniter installation, and migration generation.
  • Add system, architecture, mutation, documentation, exact Hex archive, unpacked consumer, and dependency audit release gates, run on the pinned Elixir 1.20.2 / Erlang/OTP 29 runtime.