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:
- 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. - Read-time re-derivation is the drift guard. Readers use the stored column, but a read-time
getCanonicalStatusover the persisted Size Vector asserts== stored; a mismatch throwsFINANCIAL_INTEGRITY(a missed write can never silently show a wrong status). This is ADR-0011's reconcile invariant, repurposed. - Netting/settlement still never depend on display wording — they route on the same canonical
status enum the classifier emits (drives #950's
transitionOrderStateT1–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'sstates.tsevaluators (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
getCanonicalStatusis NOT dead code — it becomes the classifier #950 consumes (the intended handshake). Wiring location: ADR-0013.- The function +
SizeVector+CanonicalStatustype move to PAL core (exchanges/core) so the write machine, the read serializer, and every adapter import one definition. CanonicalOrdergainssizeVoided(missing today) so the classifier sees the full vector.- One-way data migration: coordinate ordering with #950's
order_status_literalsso we don't ship two conflictingorders.statusrewrites. - Drift-guard input (guardian MEDIUM — FIXED in #953): the drift guard is only as good as the
provider
sizeRemainingit reconciles against.BetfairMapperpreviously masked a realsizeRemaining === 0toundefined, makingcanonicalOrderToSizeVectorSKIP reconciliation for terminal orders. Fixed by unmasking (sizeRemaining: order.sizeRemaining) — Betfair always reports it, so terminal orders now carry a truthful0and reconcile. When #950 makes the guard throwing, it has correct input for the common case.