# `StatifierPersistence.Storage`
[🔗](https://github.com/riddler/statifier_persistence/blob/v0.18.0/lib/statifier_persistence/storage.ex#L1)

The guarded entry point from a storage adapter to a
`Statifier.MachineState.t()`.

Every load runs through `load_position/3` or `load_execution_position/3`, and
every load is checked against the exact chart revision that produced the
stored position (ADR-0003 decision 2). No adapter callback ever holds
both the stored identity and a caller-supplied `Statifier.Machine.t()` at
the same time (`StatifierPersistence.Storage.Adapter`'s moduledoc), so
the guard cannot be skipped or weakened per adapter - it lives here,
above every one of them, and nowhere else in this package decodes a
position blob.

Every writer taking a machine or machine state - `save_chart/3`,
`save_position/3`, `insert_execution/5`, `update_execution/5` - derives a chart's
`content_hash` and `identity_blob` from `Machine.identity/1` on the
machine it is given - never from a caller-supplied value - and refuses an
unidentified machine with `{:error, :unidentified_chart}` rather than
writing a row a later load has no way to check.

A chart is keyed by that `content_hash` and by nothing else. No tenant,
namespace, or host scope takes part in the key, so two tenants that store
byte-identical charts share one chart row: the hash is a content address,
saving the same one twice is `:ok`, and it does not duplicate the row
(`StatifierPersistence.Storage.Adapter`'s `save_chart/2` contract). The
hash answers which chart these bytes are, never who stored them.

A multi-tenant host therefore tenant-qualifies its own per-chart rows, in
its own tables, rather than expecting this package to do it. The package
stores nothing per tenant; an execution's opaque `metadata` map (ADR-0006) is
where a host tags an execution with the scope it already keys its own tables by,
and any narrower scoping stays the host's.

Folding a namespace into the hash would change what a chart's identity
is, and that is `Statifier.Machine.identity/1`'s question - statifier-ex's
contract, not this package's. Nothing here proposes it, and this package
offers no option to do it.

Every execution writer - `insert_execution/5`, `update_execution/5`,
`update_execution_status/4` - stamps `ended_at` on the record it writes
when the status it writes is terminal (`:completed`, `:failed` or
`:cancelled`), with `DateTime.utc_now/0`, and leaves it `nil` otherwise.
The adapter keeps a stamp already stored
(`c:StatifierPersistence.Storage.Adapter.update_execution/2`), so the
stamp an execution carries is the time of the first terminal write its
row took while it had none, and no later write moves it. A write that
is not terminal - `write_tree_migration/2`'s re-pins and parks, and an
`update_execution/5` or `update_execution_status/4` putting a row back
to `:active` - never clears one, so a row can carry a stamp while its
status is not terminal.

Every function here returns an error tuple instead of throwing; nothing
in this module ever downgrades a failure to a default value.

This module is also the storage seam's telemetry site (ADR-0009 decision
3): every adapter callback below is timed and reported as
`[:statifier_persistence, :adapter, :call]`, and every identity refusal
- the guard's own and every writer's - as
`[:statifier_persistence, :identity, :refused]`. Both are built by
`StatifierPersistence.Telemetry`; see `docs/telemetry.md` for the
contract.

# `error`

```elixir
@type error() ::
  StatifierPersistence.Storage.Adapter.error()
  | :not_a_statifier_blob
  | :unidentified_chart
  | :execution_position_missing
  | :metadata_unsupported
  | :child_listing_unsupported
  | :execution_outcome_unsupported
  | :execution_states_unsupported
  | :content_hash_query_unsupported
  | :chart_retirement_unsupported
  | :tree_migration_unsupported
  | :execution_pruning_unsupported
  | {:unsupported_format_version, term()}
  | {:identity_mismatch, Statifier.Machine.Identity.t(),
     Statifier.Machine.Identity.t() | nil}
```

This facade's error vocabulary: an adapter's own arms plus `Statifier`'s
own unflattened blob-decode and identity arms (ADR-0003 decision 4).
Every arm is returned, none is collapsed into a default.

# `execution_write_opt`

```elixir
@type execution_write_opt() ::
  {:failure, String.t() | nil}
  | {:position, :persist | :skip}
  | {:metadata, StatifierPersistence.Storage.Adapter.metadata()}
  | {:outcome_blob, binary() | nil}
```

Options the execution writers (`insert_execution/5`, `update_execution/5`) accept:

- `failure:` - the short reason stored on a `:failed` execution; defaults to
  `nil`.
- `position:` - `:persist` (the default) encodes the given machine state
  with `Position.to_binary/1` and stores it as the execution's `position_blob`;
  `:skip` stores `nil` on insert and carries the currently stored blob
  forward verbatim on update.
- `metadata:` - `insert_execution/5` only: the optional opaque map of host
  identities stored beside the execution (ADR-0006 decision 1), defaulting to
  `%{}`. It is write-once - `update_execution/5` and `update_execution_status/4`
  carry the stored map forward and accept no `metadata:` of their own.
- `outcome_blob:` - `update_execution_status/4` only: the execution's own opaque
  answer, written once when it reaches a terminal status. Defaults to
  `nil`, which carries the stored value forward rather than clearing it,
  so every other writer leaves an answer already recorded alone.

# `input`

```elixir
@type input() :: %{
  execution_id: StatifierPersistence.Storage.Adapter.execution_id(),
  seq: StatifierPersistence.Storage.Adapter.seq(),
  door: StatifierPersistence.Storage.Adapter.door(),
  event: Statifier.Event.t() | nil
}
```

One decoded input log entry (ADR-0010 decision 2), as `list_inputs/2`
returns it: the adapter's stored record with its opaque `input_blob`
decoded back into the `%Statifier.Event{}` the interpreter saw.

A `nil` `event` is the closed marker the cap wrote (decision 6) and
nothing else. `door` is one of `t:StatifierPersistence.Executions.entry/0`'s
seven atoms, as its string - the key decision 8's replay mapping is
written against.

# `t`

```elixir
@type t() :: %StatifierPersistence.Storage{
  adapter: module(),
  opts: StatifierPersistence.Storage.Adapter.opts()
}
```

# `tree_write`

```elixir
@type tree_write() ::
  {:repin, StatifierPersistence.Storage.Adapter.execution_id(),
   Statifier.MachineState.t(),
   StatifierPersistence.Storage.Adapter.content_hash() | nil}
  | {:park, StatifierPersistence.Storage.Adapter.execution_id()}
```

One write of a tree migration, as `write_tree_migration/2` takes it
(ADR-0015 decision 3): a re-pin of an execution onto the machine state
it is handed, with its linkage pin's new `content_hash` or `nil` when it
carries no linkage, or a park.

# `append_input`

```elixir
@spec append_input(
  store :: t(),
  execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  door :: atom(),
  event :: Statifier.Event.t()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.seq()}
  | :not_supported
  | {:error, error()}
```

Appends `event` to `execution_id`'s input log at `door`, returning the ordinal
the adapter assigned (ADR-0010 decisions 2 and 3).

This is the encode site: the `%Statifier.Event{}` the interpreter was
handed crosses the adapter seam as an opaque `input_blob`, encoded here
with `:erlang.term_to_binary/1` in exactly `outcome_blob`'s established
shape. ADR-0003 decision 1 therefore stays true word for word - an
adapter sees binaries, strings and an ordinal, never a `Statifier`
struct - and this package mints no serialization format of its own.

`:not_supported` for an adapter that keeps no log, without calling the
adapter at all. `{:error, :input_log_full}` once the execution's log has
closed itself at the host's cap (decision 6): that arm refuses the
append and never the step, and the caller carries on.

`door` is a `t:StatifierPersistence.Executions.entry/0` atom - the fixed
vocabulary of public doors, stored as its string.

# `chart_retirement_supported?`

```elixir
@spec chart_retirement_supported?(store :: t()) :: boolean()
```

Whether a chart can be tombstoned in `store` (ADR-0012 decision 6).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.supports_chart_retirement?/1`
and it answers `true` for these opts - the same shape
`metadata_supported?/1` checks, asked of the store rather than of the
adapter module, because migration V07 makes the two chart blob columns
nullable only on Postgres.

Public for `content_hash_query_supported?/1`'s reason: a host plans a
retirement before it asks for one, and learning that this store cannot
carry a tombstone is worth learning then rather than at the refusal.

# `check_chart_retired`

```elixir
@spec check_chart_retired(store :: t(), machine :: Statifier.Machine.t()) ::
  :ok | {:error, error()}
```

Answers the retired arm for `machine`'s own content hash without
writing anything: `:ok`, or `{:error, {:chart_retired, info}}`
(ADR-0012 decision 6).

The hash is derived from `Machine.identity/1`, never supplied by the
caller, exactly as `save_chart/3` derives it. A machine carrying no
identity is `:ok`: there is no hash to have retired, and the writers'
own `:unidentified_chart` refusal is the one that belongs to that
case.

Every answer other than the retired arm is `:ok`. A hash never stored
is not a retired hash - `:chart_not_found` keeps the meaning decision
6 gave it - and an adapter failure is the write's to report, not this
check's, which is why this function narrows to the one arm it exists
to see rather than forwarding whatever it read.

Public for the reason `check_metadata/2` is: a caller with work to do
*before* the write must not do it for a create that will be refused.
`StatifierPersistence.Executions.create/4` runs it ahead of
`Statifier.Interpreter.initialize/2`, so an execution on a retired
chart fires no effect on its way to the refusal.

The read does not transfer the chart's bytes when the adapter declares
the optional
`c:StatifierPersistence.Storage.Adapter.supports_retired_info?/1`: the
check then asks
`c:StatifierPersistence.Storage.Adapter.fetch_retired_info/2`, which
reads the tombstone alone, so its cost does not grow with the chart.
An adapter that does not declare it is answered through
`fetch_chart/2` instead - the same answer, reading the whole row.

# `check_metadata`

```elixir
@spec check_metadata(store :: t(), opts :: [execution_write_opt()]) ::
  :ok | {:error, error()}
```

Validates a writer's `metadata:` option against `store`'s adapter without
writing anything: `:ok`, or `{:error, :metadata_unsupported}` for a
non-empty map an adapter cannot store (ADR-0006 decision 3).

`insert_execution/5` runs this check itself, so a caller writing through the
facade alone never needs it. It is public for the caller that has work to
do *before* the write and must not do it for a create that will be
refused: `StatifierPersistence.Executions.create/4` runs it ahead of
`Statifier.Interpreter.initialize/2` so no effect is executed for an execution
whose metadata cannot be stored - the same reason `insert_execution/5`'s
identity refusal runs before the position encode. Raises `ArgumentError`
on a malformed option, exactly as the writers do.

# `child_listing_supported?`

```elixir
@spec child_listing_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter can list executions by a metadata match (ADR-0008
decision 5).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.list_executions_by_metadata/2` **and**
`metadata_supported?/1` holds for this store. This is the predicate
`list_executions_by_metadata/2` consults for its own refusal-at-open, exposed
because a caller with work to do before the query - such as
`StatifierPersistence.Driver` refusing to start a child it could never
enumerate for cancellation - wants the answer before it acts.

The metadata conjunct is not belt and braces. A metadata match is a
query over stored metadata, so an adapter that declares it cannot
support metadata (ADR-0006 decision 3) cannot answer one either, and an
adapter whose declaration is conditional - the shipped Ecto adapter says
`false` off Postgres, where the containment SQL does not parse - is
otherwise reported as able to list purely because the function is
compiled into it. Exported and able are different questions and this is
the one callers ask (sp-11w).

# `content_hash_query_supported?`

```elixir
@spec content_hash_query_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter can answer the drained query (ADR-0012
decision 3).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.supports_content_hash_query?/1`
and it answers `true` - the same shape `metadata_supported?/1` checks.

Public because a host asks it before it plans a retirement: a chart is
retired against the counts this query takes, so a store that cannot
answer it cannot retire a chart, and that is worth learning before the
sweep rather than at the refusal.

# `count_executions_by_content_hash`

```elixir
@spec count_executions_by_content_hash(
  store :: t(),
  content_hash :: StatifierPersistence.Storage.Adapter.content_hash()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.execution_counts()}
  | {:error, error()}
```

Counts the executions on `content_hash`, per stored arm (ADR-0012
decision 3).

Delegates to the adapter's optional
`c:StatifierPersistence.Storage.Adapter.count_executions_by_content_hash/2`
when `content_hash_query_supported?/1` is true; returns
`{:error, :content_hash_query_unsupported}` otherwise, without calling
the adapter at all.

A hash this store has never seen answers zeros, not a not-found arm:
the question is how much traffic a chart carries, and none is a
number.

The five arm keys - `active`, `needs_migration`, `completed`, `failed`
and `cancelled`, the stored statuses of
`t:StatifierPersistence.Storage.Adapter.execution_status/0` - count
execution rows on the hash; `needs_migration` is not terminal
(ADR-0014 decision 4). `children` counts the durable-child linkage pins
naming it whose parent execution is `:active` or `:needs_migration`
(ADR-0012 decision 1, as ADR-0014 decision 4 reads it), which is a
different population and can be non-zero for a hash with no execution
row of its own in any arm.

The answer is not a retirability test (ADR-0012's consequences say so
in full): it reports the three terminal arms, which never block a
retirement, and it leaves out the position rows and the host's own pin
sources, which do.

# `execution_outcome_supported?`

```elixir
@spec execution_outcome_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter can store an execution's own `outcome_blob` (sp-t57,
ruling C3).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.supports_execution_outcome?/1` and it
answers `true` - the same shape `metadata_supported?/1` checks. This is
half of `StatifierPersistence.Driver.start_child_at/6`'s refusal at
open: a fan-out child whose answer could never be read back could never
settle its invocation.

# `execution_pruning_supported?`

```elixir
@spec execution_pruning_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter can prune finished executions (ADR-0016).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.supports_execution_pruning?/1`
and `c:StatifierPersistence.Storage.Adapter.prune_executions/3` and the
first answers `true` - the shape `tree_migration_supported?/1` checks.

# `execution_states_supported?`

```elixir
@spec execution_states_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter can answer the indexed status projection
(sp-t57, ruling C5).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.list_execution_states_by_metadata/2`
and `metadata_supported?/1` holds - the same pair
`child_listing_supported?/1` checks, and for the same reason: the
projection is the same metadata match, narrowed to three columns. The
other half of `start_child_at/6`'s refusal at open.

# `fetch_chart`

```elixir
@spec fetch_chart(
  store :: t(),
  content_hash :: StatifierPersistence.Storage.Adapter.content_hash()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.chart_record()} | {:error, error()}
```

Fetches the chart stored under `content_hash`.

A tombstoned hash answers `{:error, {:chart_retired, info}}` carrying
who retired it and when, never `:chart_not_found` (ADR-0012 decision
6): a host that asked for the removal reads its own decision back
rather than a miss it would debug as data loss. `:chart_not_found`
keeps its meaning exactly - never stored, as against stored and
retired.

# `fetch_execution`

```elixir
@spec fetch_execution(
  store :: t(),
  execution_id :: StatifierPersistence.Storage.Adapter.execution_id()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.execution_record()}
  | {:error, error()}
```

Fetches the execution record stored under `execution_id`.

# `input_log_supported?`

```elixir
@spec input_log_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter keeps an execution's input log (ADR-0010 decision 1).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.supports_input_log?/1` and it
answers `true` - the same shape `metadata_supported?/1` checks. Nothing
refuses on it: an adapter that keeps no log runs every chart exactly as
it did before ADR-0010, and this predicate is public so a host that
needs a replayable execution can find out whether it will get one *before* it
drives one.

# `insert_execution`

```elixir
@spec insert_execution(
  store :: t(),
  execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  machine_state :: Statifier.MachineState.t(),
  status :: StatifierPersistence.Storage.Adapter.execution_status(),
  opts :: [execution_write_opt()]
) :: :ok | {:error, error()}
```

Inserts an execution record for `execution_id`, keyed by the content hash and identity
envelope of `machine_state.machine`'s own `Machine.identity/1` - never a
caller-supplied hash - and refusing an unidentified machine with
`{:error, :unidentified_chart}` before calling the adapter.

Writers always take a `MachineState`: even an execution failed at creation has
one, because `Statifier.Interpreter.initialize/2` cannot fail (ADR-0004
decision 1). Under `position: :persist` (the default) the state is
encoded with `Position.to_binary/1` and stored as the execution's
`position_blob`; under `position: :skip` the blob is stored `nil` - the
arm for an execution with no quiescent position to store. Uniqueness comes from
the adapter's `insert_execution/2` `:execution_exists` refusal, not a pre-check here.

`metadata:` is the optional opaque map of host identities ADR-0006
decision 1 grants, defaulting to `%{}`. Create is the only place it is
set. This function is where the refusal-at-open lives: a non-empty map
for an adapter that does not export
`c:StatifierPersistence.Storage.Adapter.supports_metadata?/1` (or whose
answer is `false`) returns `{:error, :metadata_unsupported}` before any
write, and an empty or absent map is never refused. A map that is not a
map of string keys raises `ArgumentError`: the shape is the one thing
ADR-0006 decision 1 does validate, and a malformed option is a caller
bug, not a storage event.

The map carries host identities only, never personal data (ADR-0006
decision 2). `:blob_type` encryption reaches the three blob columns and
not this map, so a name, an email address, or a card number filed here is
at rest in the clear. Nothing in this package can enforce that - the map
is opaque - so the contract states it and the host keeps it.

# `list_active_execution_ids_by_content_hash`

```elixir
@spec list_active_execution_ids_by_content_hash(
  store :: t(),
  content_hash :: StatifierPersistence.Storage.Adapter.content_hash()
) ::
  {:ok, [StatifierPersistence.Storage.Adapter.execution_id()]}
  | {:error, error()}
```

Lists the ids of the `:active` executions on `content_hash` (ADR-0012
decision 4).

Delegates to the adapter's optional
`c:StatifierPersistence.Storage.Adapter.list_active_execution_ids_by_content_hash/2`
under the same capability `count_executions_by_content_hash/2` checks;
returns `{:error, :content_hash_query_unsupported}` otherwise, without
calling the adapter at all.

It is public because it is what a host building a pin source's context
by hand would otherwise have no way to ask for, and
`StatifierPersistence.Executions.retire_chart/4` asks it on a host's
behalf on every retirement. A hash this store has never seen answers
`{:ok, []}`.

# `list_execution_states_by_metadata`

```elixir
@spec list_execution_states_by_metadata(
  store :: t(),
  metadata :: StatifierPersistence.Storage.Adapter.metadata()
) ::
  {:ok, [StatifierPersistence.Storage.Adapter.execution_state()]}
  | {:error, error()}
```

The indexed status projection over the same match
`list_executions_by_metadata/2` takes (sp-t57, ruling C5).

Delegates to the adapter's optional
`c:StatifierPersistence.Storage.Adapter.list_execution_states_by_metadata/2`
when `execution_states_supported?/1` is true; returns
`{:error, :execution_states_unsupported}` otherwise, without calling the
adapter at all.

This is the read a fan-out's settlement uses to ask whether every child
of an invocation has reached a terminal status. It exists so that
question does not cost N whole records - blobs included - once per
child.

# `list_executions_by_metadata`

```elixir
@spec list_executions_by_metadata(
  store :: t(),
  metadata :: StatifierPersistence.Storage.Adapter.metadata()
) ::
  {:ok, [StatifierPersistence.Storage.Adapter.execution_record()]}
  | {:error, error()}
```

Lists the executions whose stored `metadata` contains every key/value pair in
`metadata` (ADR-0008 decision 5).

Delegates to the adapter's optional
`c:StatifierPersistence.Storage.Adapter.list_executions_by_metadata/2` when
`child_listing_supported?/1` is true; returns
`{:error, :child_listing_unsupported}` for an adapter that does not
export it, without calling the adapter at all.

# `list_inputs`

```elixir
@spec list_inputs(
  store :: t(),
  execution_id :: StatifierPersistence.Storage.Adapter.execution_id()
) ::
  {:ok, [input()]} | :not_supported | {:error, error()}
```

Lists `execution_id`'s whole input log in ascending `seq`, decoded (ADR-0010
decision 2).

The decode half of `append_input/4`: each stored `input_blob` comes back
as the `%Statifier.Event{}` that was delivered, equal to the one the
interpreter saw, `caller_context` and all. An entry whose `event` is
`nil` is the closed marker the cap wrote (decision 6) and nothing else -
a reader that maps this log onto a replay refuses on it rather than
replaying an execution that never happened.

`:not_supported` for an adapter that keeps no log;
`{:error, :execution_not_found}` for an execution that does not exist; `{:ok, []}`
for an execution with no inputs.

# `load_execution_position`

```elixir
@spec load_execution_position(
  store :: t(),
  execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  machine :: Statifier.Machine.t()
) :: {:ok, Statifier.MachineState.t()} | {:error, error()}
```

Fetches the execution stored under `execution_id` and rebuilds its position into a
`Statifier.MachineState.t()` walking `machine`, refusing a chart-revision
mismatch instead of silently resuming the wrong configuration.

Runs in `load_position/3`'s order, with one extra arm: the same cheap
identity pre-check against the stored `identity_blob`, then
`{:error, :execution_position_missing}` for an execution whose `position_blob` is
`nil` (an execution that failed at creation stores none - ADR-0004 decision 1),
then `Position.from_binary/2` as the authoritative check, its result
returned unchanged.

Like `load_position/3`, the returned state carries `nil` for `routes`,
`invoke_types` and `send_types` (st-ADR-0064); re-stamping them before
the next drive is the stepper's job, not this function's.

# `load_position`

```elixir
@spec load_position(
  store :: t(),
  session_id :: StatifierPersistence.Storage.Adapter.session_id(),
  machine :: Statifier.Machine.t()
) :: {:ok, Statifier.MachineState.t()} | {:error, error()}
```

Fetches the position stored for `session_id` and rebuilds it into a
`Statifier.MachineState.t()` walking `machine`, refusing a chart-revision
mismatch instead of silently resuming the wrong configuration.

Runs in this order, and the order is the contract:

1. `fetch_position/2` on the adapter. `:position_not_found` and
   `{:adapter, term()}` pass straight through.
2. The cheap pre-check: decodes the stored `identity_blob` and compares
   it against `Machine.identity(machine)` with `Identity.matches?/2` -
   never `==/2` (st-ADR-0052 decision 1). A mismatch returns
   `{:error, {:identity_mismatch, stored, supplied}}` without paying the
   position decode. `machine` carrying no identity returns
   `{:error, :unidentified_chart}`. A stored `identity_blob` that does
   not decode returns `{:error, :not_a_statifier_blob}` (or
   `{:error, {:unsupported_format_version, version}}`).
3. `Position.from_binary/2` - the authoritative check. Its result is
   returned unchanged, all four error arms included.

Step 2 is an optimization that must never disagree with step 3: both
reuse `Identity.matches?/2` and produce the same
`{:identity_mismatch, expected, actual}` arm, whichever check fires.

The returned `MachineState.t()` carries `nil` for `routes`,
`invoke_types` and `send_types`. None survives the round trip:
`Position.to_binary/1` drops all three alongside `:machine` when it
encodes the payload, and `from_binary/2` drops all three from the
decoded payload before it rebuilds the struct - unconditionally, so a
blob written by an older encoder decodes to `nil` too (st-ADR-0064,
which amends st-ADR-0052 in part).

A caller must therefore stamp all three before the next drive, via
`MachineState.put_routes/2`, `MachineState.put_invoke_types/2` and
`MachineState.put_send_types/2`: each is a per-drive or per-session
snapshot (st-ADR-0048, st-ADR-0051, st-ADR-0069), and a persisted
position is not the place they live. Stamping is the stepper's job
(sp-4an.2), not this function's; this function is documented here as
the place a reader learns the snapshot does not come back.

# `metadata_supported?`

```elixir
@spec metadata_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter can store an execution's opaque `metadata` map
(ADR-0006 decision 3).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.supports_metadata?/1` and it
answers `true` for this handle. This is the predicate `insert_execution/5`'s
refusal-at-open consults, exposed because a host choosing between an
adapter's scope query and its own side table wants the answer before it
writes, and because the conformance suite branches on it.

# `new`

```elixir
@spec new(adapter :: module(), opts :: StatifierPersistence.Storage.Adapter.opts()) ::
  {:ok, t()} | {:error, error()}
```

Initializes `adapter` with `opts` and returns the handle every other
function in this module takes as its first argument.

# `prune_executions`

```elixir
@spec prune_executions(store :: t(), cutoff :: DateTime.t(), limit :: pos_integer()) ::
  {:ok, StatifierPersistence.Storage.Adapter.prune_counts()} | {:error, error()}
```

Prunes one batch of at most `limit` finished executions that ended
before `cutoff` (ADR-0016, the facade half of
`c:StatifierPersistence.Storage.Adapter.prune_executions/3`).

Each execution in the batch keeps its row and loses its position blob
and its input log. The callback's documentation says which executions
a batch takes. `StatifierPersistence.Retention.prune/3` calls this
until nothing is left, and is the door a host uses.

`{:error, :execution_pruning_unsupported}` for a store whose adapter
does not declare the capability, without calling it.

# `retire_chart`

```elixir
@spec retire_chart(
  store :: t(),
  content_hash :: StatifierPersistence.Storage.Adapter.content_hash(),
  opts :: keyword()
) ::
  {:ok, StatifierPersistence.Storage.Adapter.retired_info()} | {:error, error()}
```

Retires the chart on `content_hash` over this package's own tables
(ADR-0012 decisions 5 and 6).

The facade half of the split decision 5 names: this function owns the
counts this package can take and the tombstone write, and
`StatifierPersistence.Executions.retire_chart/4` above it owns
everything that reaches outside the package. A host calls that one;
this one is here for a host that keeps its own pin accounting and has
already done the outside half itself.

Two refusals happen at open, before anything is counted and before a
transaction is opened:

- `{:error, :content_hash_query_unsupported}` for a store that cannot
  answer the drained query, because a chart must not be retired
  against a count that was never taken (decision 3);
- `{:error, :chart_retirement_unsupported}` for a store that cannot
  carry a tombstone - the two chart blob columns are still `NOT NULL`,
  or the two tombstone columns are absent. Migration V07 makes them
  nullable on Postgres only, so this is the answer on a store V07
  could not finish arranging, and it names the backend limit instead
  of surfacing a constraint violation.

Everything after that is one atomic unit in the adapter: the counts,
the refusal, and the tombstone. A non-zero count anywhere in ADR-0012
decision 1's blocking set answers `{:error, {:pinned, counts}}` and
writes nothing, and the counts it carries include the three terminal
execution arms, which are reported and never block. A hash with no
chart is `{:error, :chart_not_found}`; a hash already retired is
`{:error, {:chart_retired, info}}` and never a second tombstone.

## Options

- `retired_by:` (required) - the opaque host string recorded on the
  row as who asked. This package does not interpret it.
- `now:` - the `t:DateTime.t/0` written as `retired_at`. Defaults to
  `DateTime.utc_now/0`; there is no clock in this package and nothing
  here decides *when* a chart should be retired (decision 7).
- `source_counts:` - the pin-source counts already collected, keyed by
  source module. Defaults to `%{}`. They are folded into the same
  atomic unit as this package's own counts, so a source count that
  blocks blocks inside the unit that would otherwise write.

# `save_chart`

```elixir
@spec save_chart(
  store :: t(),
  machine :: Statifier.Machine.t(),
  chart_blob :: binary()
) ::
  :ok | {:error, error()}
```

Stores `chart_blob` under the content hash and identity envelope of
`machine`'s own `Machine.identity/1` - never a caller-supplied hash.

`chart_blob` is stored verbatim and is the one thing this function does
not derive or inspect (ADR-0003 decision 1). Returns
`{:error, :unidentified_chart}` when `machine` carries no identity,
without calling the adapter at all.

A hash a retirement has tombstoned is refused with
`{:error, {:chart_retired, info}}` and is not revived (ADR-0012
decision 6): a content-addressed save looks identical to an ordinary
idempotent re-save, so it is the one call with no way to say "yes, I
mean to undo that". A host that still wants the chart re-authors the
document and saves the result under its new hash.

# `save_position`

```elixir
@spec save_position(
  store :: t(),
  session_id :: StatifierPersistence.Storage.Adapter.session_id(),
  machine_state :: Statifier.MachineState.t()
) :: :ok | {:error, error()}
```

Encodes `machine_state` with `Position.to_binary/1` and stores it under
`session_id`, keyed by the content hash and identity envelope of
`machine_state.machine`'s own `Machine.identity/1` - never a
caller-supplied hash, so a caller cannot store a position under a hash
that disagrees with its blob.

`{:error, :unidentified_chart}` from `Position.to_binary/1` is returned
unchanged: st-ADR-0052 decision 4's structural guarantee that no
unverifiable blob can be written arrives intact at this layer too.

# `tree_migration_supported?`

```elixir
@spec tree_migration_supported?(store :: t()) :: boolean()
```

Whether `store`'s adapter can write a tree migration as one unit
(ADR-0015 decision 3).

True when the adapter exports the optional
`c:StatifierPersistence.Storage.Adapter.supports_tree_migration?/1` and
`c:StatifierPersistence.Storage.Adapter.write_tree_migration/2` and the
first answers `true` - the shape `chart_retirement_supported?/1` checks.

# `update_execution`

```elixir
@spec update_execution(
  store :: t(),
  execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  machine_state :: Statifier.MachineState.t(),
  status :: StatifierPersistence.Storage.Adapter.execution_status(),
  opts :: [execution_write_opt()]
) :: :ok | {:error, error()}
```

Overwrites the execution stored under `execution_id` with a full record derived the
same way `insert_execution/5` derives one: identity always from
`machine_state.machine`'s own `Machine.identity/1`, refusal of an
unidentified machine, `position_blob` encoded under `position: :persist`
(the default).

Under `position: :skip` the currently stored `position_blob` is carried
forward verbatim: because the adapter's `update_execution/2` is a full-record
overwrite, this function fetches the current record and reuses its blob
bytes unchanged, so a status-only update (a failed step, an abandonment)
never touches the stored position. Returns `{:error, :execution_not_found}`
when no execution exists for the id.

An execution's `metadata` is write-once (ADR-0006 decision 1 grants a map at
create and no way to change it), so this function accepts no `metadata:`
option and passes `%{}` in the record; the adapter carries the stored map
forward verbatim, as
`c:StatifierPersistence.Storage.Adapter.update_execution/2` records.

# `update_execution_status`

```elixir
@spec update_execution_status(
  store :: t(),
  execution_id :: StatifierPersistence.Storage.Adapter.execution_id(),
  status :: StatifierPersistence.Storage.Adapter.execution_status(),
  opts :: [execution_write_opt()]
) :: :ok | {:error, error()}
```

Overwrites only the status and failure of the execution stored under `execution_id`,
carrying every other stored field - both blobs included - forward
verbatim.

This is the writer for a host-driven terminal transition that has no
`MachineState` in hand (`StatifierPersistence.Executions.fail/4`, ADR-0004
decision 6): nothing is derived, decoded, or re-encoded, so the identity
guard is preserved by construction - the stored `identity_blob` and
`position_blob` bytes never change. `opts` accepts `failure:` and
`outcome_blob:` (both default `nil`). Returns
`{:error, :execution_not_found}` when no execution exists for the id.

`outcome_blob:` is the one writer of an execution's own answer, and this is the
right writer for it: an answer is recorded exactly when an execution reaches a
terminal status, which is the transition this function exists for, and
nothing else about the record is touched. A `nil` (the default) carries
the stored blob forward, so a status-only update never erases an answer.

# `write_tree_migration`

```elixir
@spec write_tree_migration(store :: t(), writes :: [tree_write()]) ::
  :ok | {:error, error()}
```

Writes a tree migration's re-pins and parks as one unit: every write
lands or none does (ADR-0015 decision 3, the facade half of
`c:StatifierPersistence.Storage.Adapter.write_tree_migration/2`).

A re-pin's record is derived as `update_execution/5` derives one: the
identity and content hash from the machine state's own machine, the
position blob encoded from it, the status `:active` and a `nil` failure;
the stored metadata and answer are carried forward, and the linkage
pin's `content_hash` is rewritten when the write names one (ADR-0008's
2026-09-23 Amendment). A park writes `:needs_migration` and a `nil`
failure and nothing else. Every record is derived before the adapter is
called, so an unidentified machine refuses with nothing written.

`{:error, :tree_migration_unsupported}` for a store whose adapter does
not declare the unit, without calling it.
`StatifierPersistence.Executions.migrate_tree/4` is the caller; nothing
else in this package writes a linkage pin after create.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
