The guarded entry point from a storage adapter to a
Statifier.MachineState.t().
Every load runs through load_position/3 or load_run_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_run/5, update_run/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.
Every function here returns an error tuple instead of throwing; nothing in this module ever downgrades a failure to a default value.
Summary
Types
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.
Options the run writers (insert_run/5, update_run/5) accept
Functions
Fetches the chart stored under content_hash.
Fetches the run record stored under run_id.
Inserts a run record for run_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.
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.
Fetches the run stored under run_id and rebuilds its position into a
Statifier.MachineState.t() walking machine, refusing a chart-revision
mismatch instead of silently resuming the wrong configuration.
Initializes adapter with opts and returns the handle every other
function in this module takes as its first argument.
Stores chart_blob under the content hash and identity envelope of
machine's own Machine.identity/1 - never a caller-supplied hash.
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.
Overwrites the run stored under run_id with a full record derived the
same way insert_run/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).
Overwrites only the status and failure of the run stored under run_id,
carrying every other stored field - both blobs included - forward
verbatim.
Types
@type error() :: StatifierPersistence.Storage.Adapter.error() | :not_a_statifier_blob | :unidentified_chart | :run_position_missing | {: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.
@type run_write_opt() :: {:failure, String.t() | nil} | {:position, :persist | :skip}
Options the run writers (insert_run/5, update_run/5) accept:
failure:- the short reason stored on a:failedrun; defaults tonil.position:-:persist(the default) encodes the given machine state withPosition.to_binary/1and stores it as the run'sposition_blob;:skipstoresnilon insert and carries the currently stored blob forward verbatim on update.
@type t() :: %StatifierPersistence.Storage{ adapter: module(), opts: StatifierPersistence.Storage.Adapter.opts() }
Functions
@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.
@spec fetch_run(store :: t(), run_id :: StatifierPersistence.Storage.Adapter.run_id()) :: {:ok, StatifierPersistence.Storage.Adapter.run_record()} | {:error, error()}
Fetches the run record stored under run_id.
@spec insert_run( store :: t(), run_id :: StatifierPersistence.Storage.Adapter.run_id(), machine_state :: Statifier.MachineState.t(), status :: StatifierPersistence.Storage.Adapter.run_status(), opts :: [run_write_opt()] ) :: :ok | {:error, error()}
Inserts a run record for run_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 a run 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 run's
position_blob; under position: :skip the blob is stored nil - the
arm for a run with no quiescent position to store. Uniqueness comes from
the adapter's insert_run/2 :run_exists refusal, not a pre-check here.
@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:
fetch_position/2on the adapter.:position_not_foundand{:adapter, term()}pass straight through.- The cheap pre-check: decodes the stored
identity_bloband compares it againstMachine.identity(machine)withIdentity.matches?/2- never==/2(st-ADR-0052 decision 1). A mismatch returns{:error, {:identity_mismatch, stored, supplied}}without paying the position decode.machinecarrying no identity returns{:error, :unidentified_chart}. A storedidentity_blobthat does not decode returns{:error, :not_a_statifier_blob}(or{:error, {:unsupported_format_version, version}}). 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.
@spec load_run_position( store :: t(), run_id :: StatifierPersistence.Storage.Adapter.run_id(), machine :: Statifier.Machine.t() ) :: {:ok, Statifier.MachineState.t()} | {:error, error()}
Fetches the run stored under run_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, :run_position_missing} for a run whose position_blob is
nil (a run 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.
@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.
@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.
@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.
@spec update_run( store :: t(), run_id :: StatifierPersistence.Storage.Adapter.run_id(), machine_state :: Statifier.MachineState.t(), status :: StatifierPersistence.Storage.Adapter.run_status(), opts :: [run_write_opt()] ) :: :ok | {:error, error()}
Overwrites the run stored under run_id with a full record derived the
same way insert_run/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_run/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, :run_not_found}
when no run exists for the id.
@spec update_run_status( store :: t(), run_id :: StatifierPersistence.Storage.Adapter.run_id(), status :: StatifierPersistence.Storage.Adapter.run_status(), opts :: [run_write_opt()] ) :: :ok | {:error, error()}
Overwrites only the status and failure of the run stored under run_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.Runs.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: only
(default nil). Returns {:error, :run_not_found} when no run exists
for the id.