# Snapshot-bound read models and race-safe clients

Use these probes when a transactional inbox or ledger exposes global counts plus paginated rows while writes may occur concurrently.

## Canonical snapshot contract

- Compute a server-side `data_version` from mutation-complete durable evidence before deriving cursor scope.
- Never use `COUNT(*) + MAX(updated_at)` as the version. It misses non-max-row edits, same-resolution timestamp updates, and count-preserving replacements.
- Prefer a durable monotonic revision incremented by every relevant write boundary. If schema change is intentionally out of scope, compute a deterministic streaming digest over every value that can affect ordering, filtering, counts, joins, or rendered rows. Include lookup tables such as categories/accounts when their labels or active state affect the response.
- Bind cursor scope to filters, ordering, page-size contract, and `data_version`.
- Recompute the version after counts/facets/page rows are read. If it changed, return a conflict rather than a mixed snapshot.
- A client receiving a stale-cursor conflict discards accumulated rows and restarts from page one. Never append the new snapshot to the old page.
- Keep `total_all`, `filtered_total`, and visible page length as distinct fields. Counts are separate aggregate queries, not `len(items)` and not a 500-row fetch.
- If a content digest is the no-migration fallback, stream rows into the digest rather than materializing or returning them, expose only a short digest token, and record latency against productive scale so snapshot correctness does not silently undo bounded-page performance.

Adversarial mutation probes:

1. Read page one and capture `next_cursor` and `data_version`.
2. Ensure some unrelated row owns the maximum `updated_at`.
3. Change a page-two row's amount/category/merchant without changing its timestamp, and commit.
4. Request page two with the old cursor; require conflict/no rows.
5. Repeat with a same-second update and a count-preserving delete/insert replacement.
6. Request page one again and require a new version and internally consistent counts.

## Global facets versus page-local rows

- Source/account/category/status options must come from the complete authorized filtered scope, not the visible page.
- Keep facet derivation independent of page cursor and ordering.
- A filtered zero state should retain globally valid reset/filter options.

Probe with page one containing source A while source B appears only on a later page; the source selector must still offer both.

## Generation-bound client reads

Apply generation guards to all filterable and pageable reads, not only previews:

- Increment generation before each fresh/filter-changing request.
- Capture generation and requested cursor for append requests.
- Ignore completion, error, and `finally` state changes from obsolete generations.
- On a fresh request, clear stale rows immediately while optionally retaining stable server-provided facets.
- Disable or serialize repeated append actions.
- Invalidate or abort the generation on component unmount. A delayed response must not update component state or call global side effects such as `history.replaceState` against the destination route.

Deferred-promise probes:

1. Issue filter A, then filter B; resolve B first and A last. The UI must keep B's rows/counts and must not reintroduce A's stale result or error.
2. Mount a route component with a deferred read, unmount/navigate before it resolves, then resolve it. Require no row/error/loading update and zero browser-history or URL mutation. A request-generation check immediately before URL synchronization plus an unmount generation increment is a minimal safe pattern; cancellation is preferable when supported.

## Shared financial effects and bounded aggregation

- Define one canonical row-level financial-effects relation for income, expense, linked refunds, unlinked refunds, neutral transfers, duplicate suppression, effective category, and canonical-currency availability.
- Overview, categories, status, charts, forecasts, and export must aggregate that relation rather than restating formulas.
- For annual/chart endpoints, fetch the bounded year once and group by month/category in one pass. If one endpoint composes another, pass the already-loaded effect rows rather than scanning again.
- Carry partial canonical-currency availability through charts/status, not just transaction detail.

Regression gate: monkeypatch or instrument the effects loader and require one call for a composed yearly chart/status request while asserting output parity with month-level canonical summaries.

## Availability closure for excluded confirmed rows

Treat availability as a reconciliation output, not a missing-FX boolean. For every confirmed row, the canonical effects layer must explain whether it contributes income, expense, neutral volume, linked reversal, or an explicitly excluded/unknown effect.

Required public fields should include reason-specific counts (for example `unavailable_chf_count` and `unlinked_refund_count`) plus a canonical `data_status`. An aggregate is `partial` whenever any confirmed row is omitted from monetary totals pending evidence. Human-facing overview text must name all concurrent reasons rather than choosing only the first.

End-to-end probes:

1. Insert one confirmed foreign-currency expense without an accepted canonical-currency value. Require zero canonical effect, original-currency detail visibility, reason count `1`, and `partial` in monthly summary, overview, charts/status, and export.
2. Insert one confirmed unlinked refund. Require zero income/expense effect, `unlinked_refund_count=1`, `partial`, and explicit copy saying the refund has no effect until linked; reject `current` or “all confirmed data included.”
3. Combine open review rows, missing FX, and unlinked refunds. Require the overview to preserve all reason counts/messages and the financial equation to reconcile exactly.
4. Link the refund to a confirmed origin and provide accepted FX where applicable. Require reasons to clear and status to become `current` only when no other excluded confirmed rows remain.
