# `StatifierPersistence.Storage`
[🔗](https://github.com/riddler/statifier_persistence/blob/v0.12.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 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
  | {: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()
}
```

# `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.

# `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).

# `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_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`.

# `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_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.

Executions 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 both
`routes` and `invoke_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.

Executions 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 both `routes` and
`invoke_types`. Neither survives the round trip: `Position.to_binary/1`
drops both alongside `:machine` when it encodes the payload, and
`from_binary/2` drops both 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 both before the next drive, via
`MachineState.put_routes/2` and `MachineState.put_invoke_types/2`: both
are per-drive/per-session snapshots (st-ADR-0048, st-ADR-0051), 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.

# `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.

# `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.

# `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.

---

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