# `Capstan.SnapshotStore`
[🔗](https://github.com/baselabs/capstan/blob/v1.3.1/lib/capstan/snapshot_store.ex#L1)

The durable snapshot-progress persistence contract (C2), the sibling of
`Capstan.CheckpointStore`.

A snapshot store persists **exactly one value** per pipeline identity: the whole
`Capstan.Snapshot.State` (Pinned decision #3) — the phase, the pipeline-global floor `p0`,
and the per-`{schema, table}` progress (fingerprint, PK shape, cursor, done flag). This
mirrors `CheckpointStore`'s one-durable-value model, so a caller advancing the backfill
never hand-rolls the persistence and the two stores stay structurally parallel.

The snapshot state is **orthogonal auxiliary state**, never a replication position: it
carries no GTID that could become a `Capstan.Gtid.member?/2` dedup floor (ADR-0001). The
processed-GTID checkpoint stays the sole authoritative replication position; this store only
records "how far the backfill has read".

## Behaviour

  * `c:read/1` — `{:ok, %Capstan.Snapshot.State{} | nil}` (`nil` = never written) or a
    value-free `{:error, term()}`.
  * `c:write/2` — durably store the `%State{}` (idempotent: writing the same value twice is
    indistinguishable from writing it once) or `{:error, term()}`.

A read/write fault **propagates** through `read/2`/`write/3` — it is never collapsed to
`{:ok, nil}`. A store that faulted must fail closed: masquerading as "never snapshotted"
would make a re-bootstrap re-scan the whole backfill from zero (a silent duplicate).
Callers budget the fault with the SHARED `Capstan.CheckpointStore.retry_decision/2` +
`permanent_reason?/1` (this store does NOT re-derive the counter), then halt fail-closed
(`:snapshot_state_read_failed` / `:snapshot_state_write_failed`).

## Rule 1 (Ch5) — binding on every implementation

The `%State{}` passed to `write/2` holds **USER DATA**: each table's `pk_cursor` is a row
value and its `fingerprint` is a column-derived hash. An implementation must **never log**,
telemeter, or otherwise emit the `write/2` argument (nor the value returned by `read/1`).
Persist it to the durable store and nowhere else. `%Capstan.Snapshot.State`'s derived
`Inspect` elides the per-table map, but a store that stringifies or logs the struct through
another path would defeat that — so this binding is on the STORE, not only the struct.

`Capstan.SnapshotStore.InMemory` is a process-lifetime reference implementation for tests
and ephemeral pipelines — it is **NOT** durable across a restart, so it must never be used
as a production snapshot store.

# `store`

```elixir
@type store() :: term()
```

A per-pipeline store handle (e.g. the pid/name of a store process).

# `read`

```elixir
@callback read(store()) :: {:ok, Capstan.Snapshot.State.t() | nil} | {:error, term()}
```

Read the durable snapshot state for this pipeline: `{:ok, %State{} | nil}` (`nil` = never
written) or a value-free error.

# `write`

```elixir
@callback write(store(), Capstan.Snapshot.State.t()) :: :ok | {:error, term()}
```

Durably persist the whole `%State{}` for this pipeline. Idempotent — re-writing the same
value is a no-op-equivalent. Returns `:ok` or a value-free error. Rule 1: never log or
telemeter the argument.

# `read`

```elixir
@spec read(module(), store()) ::
  {:ok, Capstan.Snapshot.State.t() | nil} | {:error, term()}
```

Read the snapshot state through `impl`, propagating a fault fail-closed.

Never collapses `{:error, _}` to `{:ok, nil}` — a faulted store must not read as
"never snapshotted". `impl` is the callback module; `store` its handle.

# `write`

```elixir
@spec write(module(), store(), Capstan.Snapshot.State.t()) :: :ok | {:error, term()}
```

Persist the snapshot state through `impl`. `impl` is the callback module; `store` its handle.

---

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