Skip to main content

Derive the canonical status from the Size Vector, then STORE it (align to #950)

Status: ACCEPTED (Bhargav, 2026-07-09). Reverses ADR-0001 and ADR-0002.

Pran's PR #950 (refactor/order-sync-new, plans/order-state-machine-v2.md) makes the 11 canonical statuses the new DB Order.status literal set (unmatched | matched | partially_matched | partially_matched_lapsed | partially_matched_cancelled | partially_matched_cancelled_lapsed | fully_lapsed | fully_cancelled | fully_cancelled_lapsed | settled | declined) via orderState/states.ts + a one-time order_status_literals data migration (pending|submitted→unmatched, accepted→matched, partially_accepted→partially_matched* by residue, lapsed→fully_lapsed, cancelled→fully_cancelled). The vocabulary is byte-identical to our CanonicalStatus — the status handshake with Pran landed; only the code seam did not.

Given #950 is the deeper, transaction-safe, tested write-time machine that is about to merge, we align to it: the rich status is STORED as the source of truth. But storing without a single deriver is how status drifts from the money, so:

  1. One pure classifier produces the stored value. getCanonicalStatus(SizeVector) → CanonicalStatus (provider-agnostic) is the ONLY thing that decides an order's match-axis status. #950's write path calls it; nothing hand-rolls a status literal.
  2. Read-time re-derivation is the drift guard. Readers use the stored column, but a read-time getCanonicalStatus over the persisted Size Vector asserts == stored; a mismatch throws FINANCIAL_INTEGRITY (a missed write can never silently show a wrong status). This is ADR-0011's reconcile invariant, repurposed.
  3. Netting/settlement still never depend on display wording — they route on the same canonical status enum the classifier emits (drives #950's transitionOrderState T1–T8) and on the raw Size Vector, never on FE phrasing. The ADR-0001 principle survives; only "never stored" is reversed.

What this reverses vs preserves

  • Reversed (ADR-0001): the status IS persisted (as the DB literal), not read-time-only.
  • Reversed (ADR-0002): the DB enum does NOT stay coarse — #950 replaces the coarse literals with the rich vocabulary. Our ORDER_STATUS_SETS (built on the old coarse literals) is therefore superseded by #950's states.ts evaluators (isTerminal/isSyncable/isSettleable/isCancellable) on the new vocabulary — see ADR-0013 (cleanup).
  • Preserved: single provider-agnostic pure classifier, no per-provider branches; money paths insulated from display wording; the Size Vector is the lossless source.

Consequences

  • getCanonicalStatus is NOT dead code — it becomes the classifier #950 consumes (the intended handshake). Wiring location: ADR-0013.
  • The function + SizeVector + CanonicalStatus type move to PAL core (exchanges/core) so the write machine, the read serializer, and every adapter import one definition.
  • CanonicalOrder gains sizeVoided (missing today) so the classifier sees the full vector.
  • One-way data migration: coordinate ordering with #950's order_status_literals so we don't ship two conflicting orders.status rewrites.
  • Drift-guard input (guardian MEDIUM — FIXED in #953): the drift guard is only as good as the provider sizeRemaining it reconciles against. BetfairMapper previously masked a real sizeRemaining === 0 to undefined, making canonicalOrderToSizeVector SKIP reconciliation for terminal orders. Fixed by unmasking (sizeRemaining: order.sizeRemaining) — Betfair always reports it, so terminal orders now carry a truthful 0 and reconcile. When #950 makes the guard throwing, it has correct input for the common case.