StatifierPersistence.Storage (StatifierPersistence v0.1.0)

Copy Markdown View Source

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

t()

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

error()

@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.

run_write_opt()

@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 :failed run; defaults to nil.
  • position: - :persist (the default) encodes the given machine state with Position.to_binary/1 and stores it as the run's position_blob; :skip stores nil on insert and carries the currently stored blob forward verbatim on update.

t()

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

Functions

fetch_chart(store, content_hash)

@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_run(store, run_id)

@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.

insert_run(store, run_id, machine_state, status, opts \\ [])

@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.

load_position(store, session_id, machine)

@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 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.

load_run_position(store, run_id, machine)

@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.

new(adapter, opts)

@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(store, machine, chart_blob)

@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(store, session_id, machine_state)

@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_run(store, run_id, machine_state, status, opts \\ [])

@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.

update_run_status(store, run_id, status, opts \\ [])

@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.