# Changelog

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

## Unreleased

## v1.5.0 — October 7, 2026

Security dependency release. The package DSL, public API, database schema, persisted
response format, and token wire format are unchanged; no migration is required.

**Breaking dependency change:** consumers pinned below Ash 3.34.3 or Mint 1.10.2
must update those dependencies. See [Upgrading](documentation/upgrading.md) for the steps.

### Security

- **Ash floor raised to `>= 3.34.3 and < 4.0.0`.**
  [EEF-CVE-2026-94201](https://cna.erlef.org/osv/EEF-CVE-2026-94201.html) affects Ash
  `>= 3.5.1 and < 3.34.3` through unsafe atom-attribute filters that can exhaust the BEAM
  atom table. Ash 3.33.11 includes the
  [EEF-CVE-2026-93477](https://cna.erlef.org/osv/EEF-CVE-2026-93477.html) fix but remains
  affected by EEF-CVE-2026-94201, so 3.33.11 cannot be the package floor.
- **Optional Mint floor raised to `~> 1.10 and >= 1.10.2`.** Mint 1.10.2 backports the fixes
  for [EEF-CVE-2026-91043](https://cna.erlef.org/osv/EEF-CVE-2026-91043.html),
  [EEF-CVE-2026-92103](https://cna.erlef.org/osv/EEF-CVE-2026-92103.html), and
  [EEF-CVE-2026-94194](https://cna.erlef.org/osv/EEF-CVE-2026-94194.html). Mint remains
  optional and runtime-excluded; the requirement binds only dependency graphs that already
  carry the installer HTTP closure.
- **Security patches for existing feature lines:**
  [1.4.2](https://hex.pm/packages/ash_onetime/1.4.2) and
  [1.3.4](https://hex.pm/packages/ash_onetime/1.3.4) carry the same dependency floors.
  Applications staying on those lines can take the patch without adopting a newer
  feature line. Neither patch changes its line's DSL, API, schema, or wire format.

### Changed

- Result-handling examples preserve unhandled typed errors for the application’s handler;
  release checks exercise every documented code against all four introductory expressions.

- **Lock:** `ash` 3.33.11 → 3.34.4, `ash_postgres` 2.13.1 → 2.14.3,
  `ash_sql` 0.7.6 → 0.8.1, `spark` 2.7.3 → 2.7.6, `mint` 1.10.1 → 1.11.0, with
  `ex_ast` 0.16.0, `finch` 0.24.0, and `req` 0.7.5 riding the resolution, for the
  dependency-currency gate. The development lock exercises current releases while the
  published ranges retain the patched Ash 3.x and Mint 1.x compatibility lines.
- **Actual Livebook source execution is now a release gate.**
  `elixir scripts/check_livebooks.exs --local` evaluates every Elixir cell and persisted
  output in all three `.livemd` files against PostgreSQL before packaging; the
  `--published` mode repeats the same notebooks from the Hex dependency after publication.
  The existing `LivebookWalkthroughTest` remains a supplemental mirror test for selected
  idempotency and nonce shapes. The v0.1.1 entry below records the historical claim that this
  mirror regression-pinned the notebooks; it did not read or execute notebook source, and
  this release corrects that verification boundary.
- **Developer database setup follows the current repository policy.** The repository no
  longer ships `docker-compose.yml`; contributors point `.env` at PostgreSQL 18 and source
  it before the gate battery. macOS and Linux are the supported developer platforms;
  Windows developers use WSL2, and CI runs on Linux only. This current guidance supersedes
  the v1.3.1 historical repository description without rewriting that release record.

## 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 non-Mint 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 non-Mint 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](documentation/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](documentation/telemetry.md).

## 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](documentation/livebooks/idempotency.livemd) — 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](documentation/recipes.md),
  [Telemetry](documentation/telemetry.md), [Upgrading](documentation/upgrading.md), and
  [FAQ](documentation/faq.md). Add a README "When to use this vs. hand-rolled idempotency"
  section and a "Try it" livebook pointer.
- Reorder [Getting started](documentation/getting-started.md) 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.
