# `DrinkMeAlice.Outcome`

Classifies raw SurrealDB replies for possibly-submitted mutations and
reads into *facts*.

The core rule: once a request may have entered the write path, a lost
or malformed reply is `{:unknown, request}` — never silently `rejected`
or `committed`. `{:refused, …}` facts exist only where the server gave
*qualified* evidence the request could not have executed through a
request-level or session-level RPC refusal. Incomplete replies remain
inconclusive.

SurrealDB 3.0.5 statement-result semantics used here (pinned
`dbs/executor.rs:675-1035` — review-44 correction):

- A `query` reply's `result` is a statement array. COMMIT has its own
  slot ONLY on success: the inner executor pushes the COMMIT result
  after `txn.commit()` returns ok (~:917), and consumes COMMIT with
  NO result entry on every failure path. A failed reply is therefore
  one entry SHORTER than the script — the trailing `RETURN` executes
  at the nominal COMMIT index. The classifier keys on the expected
  total length (`ctx.len` from `DrinkMeAlice.Tx`), never on the
  COMMIT index alone.
- Full-length reply (`len == ctx.len`): `results[ci]` is the real
  COMMIT slot. `OK` is observed commit success — a post-commit error
  cannot un-commit it, so a missing/failed decision is
  `:committed_no_decision`, never a false rejection. Any `ERR` at
  `ci` in a full-length array is an unqualified shape (the pinned
  executor never pushes a COMMIT error) — inconclusive.
- Shortened reply (`len == ctx.len - 1`): the COMMIT entry was
  omitted; `results[ci]` is the *shifted RETURN* and is NEVER commit
  evidence. Qualified shapes over the in-tx entries at 1..ci-1
  (statement errors carry `kind`/`details` at ENTRY level with
  `result` a message STRING — pinned `dbs/response.rs:78-90,113-164`,
  confirmed by the live capture `memory-reply-shapes-live-02.txt`):
    * statement abort: BEGIN `{status: "OK", result: nil}` at 0
      (outside the rewrite's `[start_results..]` range), entries
      before the failing statement are `NotExecuted` markers
      (`{status: "ERR", kind: "Query", details: {kind: "NotExecuted"},
      result: <string>}`), the failing statement carries the real
      error (a non-marker `ERR` with a message-string result), and
      every statement after it is a `Cancelled` marker — exactly ONE
      real error, marker order NotExecuted-before / Cancelled-after →
      definite non-commit (`{:rejected, _}`).
    * homogeneous cancellation: ALL in-tx entries are `Cancelled`
      markers (ctx-done path) or ALL are `NotExecuted` markers
      (tx-creation failure) — definite non-commit. Mixed marker sets
      are not produced → unqualified.
  A shortened reply needs ≥2 in-tx entries (ci ≥ 3) for the
  single-error abort qualification: with one in-tx statement the
  commit-failure rewrite produces exactly one `Internal` error —
  indistinguishable from a lone abort error without message
  substrings.
- Commit failure (`txn.commit()` error, ~:930): every in-tx entry is
  rewritten to `Internal("Query not executed: {e}")` — MULTIPLE real
  errors — and `surrealkv/mod.rs` commits the storage transaction
  before `commit_coordinator.wait_for_sync`, so the write may already
  be durable. No rollback certificate exists on this wire shape —
  always inconclusive. Earlier "real" errors in that reply are
  rewrites, not abort causes.
- A full-length reply must have `OK` status on every pre-COMMIT
  entry, `result: nil` (NONE) at BEGIN and COMMIT — a contradictory
  pre-commit error or a non-nil COMMIT cannot establish success.
- `THROW` surfaces as entry-level `kind: "Thrown"` with the thrown
  tag inside the message-string `result`.
- Any other length, an `OK` mid-tx entry inside a shortened reply, a
  non-OK BEGIN, or any silhouette not listed above is unsupported —
  inconclusive. A shifted `RETURN` is never read as COMMIT success.

Every statement entry must match the `{status: "OK"|"ERR", result: _}`
shape — unknown statuses, scalars or malformed entries make the whole
reply inconclusive rather than letting a valid-looking decision
overshadow corrupt neighbours.

# `fact`

```elixir
@type fact() ::
  {:committed, map()}
  | {:existing, map()}
  | {:rejected, DrinkMeAlice.Error.t()}
  | {:session_refused}
  | {:refused, integer()}
  | {:inconclusive, term()}
```

# `classify_read`

```elixir
@spec classify_read(map()) :: {:ok, map()} | {:error, atom()}
```

Classify a read reply — single `RETURN` statement. Returns
`{:ok, map}` | `{:error, :session | :rpc_error | :bad_shape}`.

# `classify_tx`

```elixir
@spec classify_tx(map(), map(), map() | nil) :: fact()
```

Classify a mutation reply.

`verdicts` maps the script's decision strings to outcome atoms used by
the caller. `ctx` carries `%{commit: i, decision: j, len: n}` — the
nominal COMMIT index, the trailing RETURN index, and the script's
statement count, emitted by `DrinkMeAlice.Tx` builders (`len`
defaults to `j + 1`: RETURN is the last statement). When `ctx` is
absent (tests, migrations-side replies), the strict fallback runs:
a decision is honoured only if *no* entry carries any error and the
complete array is well-shaped.

# `session_code?`

```elixir
@spec session_code?(integer()) :: boolean()
```

True when the error CODE ALONE proves a session refusal — only
`-32002`. `-32000` requires structured evidence; use
`session_refusal?/1` on the whole error object.

# `session_refusal?`

```elixir
@spec session_refusal?(map()) :: boolean()
```

True when the wire error object is a session refusal: `-32002`
InvalidAuth outright, or `-32000` carrying the exact expired-session
wrapper chain the pinned 3.0.5 server emits — `kind: "NotAllowed"`,
`details.kind: "Auth"`, `details.details.kind: "SessionExpired"`.
A `SessionExpired` kind under any other wrapper, a malformed chain,
or a bare `-32000` is generic internal evidence: NOT a session
refusal (review-13).

---

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