# Checks are a transact op kind, and a failed transact reports every op

`transact` accepts a fifth op kind alongside `insertOp`/`getAndUpdateOp`/`deleteOp`/`restoreOp`: a **check op**, built from an entity or key with no read of its own (`unchangedOp`, `existsOp`, `notExistsOp`; single entities get `unchangedOp` only). It asserts an **item condition** about an item the batch does not write — overwhelmingly "this is still at the `_u` I read" — and commits the batch only if every assertion holds. Checks share the ops array, the 100-item limit, and the duplicate-target guard, because DynamoDB's `ConditionCheck` shares all three with `Put`; a check yields `null` at its position in the result, keeping the returned tuple aligned one-to-one with the ops given. A batch may be checks only. This is the read-then-decide-then-write path: read an item, evaluate whatever business rule you like in your own code, then make the transaction conditional on that item not having moved since.

Conditions stay at existence and `_u`, never predicates over `data`. An adapter-independent predicate would compile to a DynamoDB condition expression on one adapter and run as in-process JavaScript on the other three, and those disagree on missing attributes, `null`, and numeric coercion — the same program would give different answers per database, which is exactly what the **StdTable surface** promises it will not do. `_u` is the portable predicate: it says nothing the caller read has changed, which subsumes every predicate the caller could have evaluated. Existence is physical for the same reason `not-exists` on an insert must be — a tombstoned item exists, and `unchangedOp` catches soft deletion for free since a delete bumps `_u`.

The duplicate-target guard is kept and extended to checks even though DynamoDB would catch the collision itself. It is a portability guarantee, not a correctness one: DynamoDB rejects a repeated target with a `ValidationException` (surfacing as an opaque `OperationFailed`), while SQLite, IDB, and Memory would apply both operations and commit. Without the guard the same program passes on the three adapters people develop against and fails only in production. The cost is a string-equality lookup in a `Set` the transact path already builds.

A failed `transact` now fails with `TransactFailed`, carrying one **transact outcome** per submitted op in order — entity, key, kind, the op itself, and a status of `passed`, `failed`, or `not-evaluated` — rather than a bare `ConditionFailed` naming a single entity. `passed` deliberately is not "success": a failed transact writes nothing, and the word would read as "the insert happened". `not-evaluated` records a genuine divergence instead of papering over it, since DynamoDB evaluates every item and can report several failures at once while the other adapters abort at the first. This obliges the contract's `ConditionFailure` to carry positional outcomes, `SQLiteChangesMismatch` to carry its statement index, and the DynamoDB adapter to map `CancellationReasons` positionally instead of collapsing them to a boolean. `ConditionFailed` stays as-is for single-item writes, where one condition needs no report.

Rejected: a separate `checks` argument to `transact` (splits a channel DynamoDB keeps unified, and hides the shared 100-item budget and duplicate-target rule); returning the checked entity at a check's position (DynamoDB's `ConditionCheck` returns nothing, so it would mean an extra read to fabricate a value); filtering checks out of the result (breaks positional correspondence, forcing callers to count non-check ops to find their own entities); a `check` on `get` — a token returned alongside the read (`_u` is already on `Entity Meta`, so the check is a derivation from information the caller holds, and making it a return value would deny it to entities obtained from `query`, a broadcast, or a cache); reporting only the failing index and inferring the rest (discards the multiple, non-condition cancellation codes DynamoDB already provides); a pure, non-`Effect` check constructor (correct, but one op constructor that must not be `yield*`-ed inside an `Effect.gen` block is a papercut at every call site).

Amended by ADR 0006: `getAndCheckOp` no longer reads at construction time. The read-then-decide-then-write path is now expressed as an **entity invariant** that `transact` evaluates against fresh data, and `TransactOutcome.status` splits `failed` into `stale`, `refused`, and `missing`.
