[Adapters] Implement EmdashOrderStore creation, transitions and hold-adoption intent over one order document - #256
Merged
vedanshujain merged 1 commit intoSep 14, 2026
Conversation
…adoption intent over one order document
`OrderStore` over EmDash plugin storage, on one aggregate document per order plus
two claim collections — ADR-0019 §3/§4/§7.9/§7.10 applied to the order aggregate.
First of three increments on the port: creation, the guarded transitions, the audit
spine, order expiry and the three cross-aggregate hold intents.
Creation is a claim, then a create-if-absent, then a promotion. `order_keys/{key}`
is claimed first and carries the WHOLE prepared document, so a replayer finishes an
interrupted create byte for byte — same order id, same minted line ids — and the
claim is promoted only after the order document exists, because a terminal key over
a missing order reads as "already minted" and would lose the checkout. Both halves
of that window are healed by ordinary calls, and both are fault-injected.
Snapshot immutability is structural: `items` is a `readonly` array of `readonly`
fields written only by the creating write, and every later write is `{ ...doc, … }`,
carrying the same array by reference. A product edit cannot reach it, and neither
can a future method without a compile error.
Each transition is ONE compare-and-set: the flip guarded on the revision and on
`state === fromState`, the appended audit event, and the first-wins outbox entry per
`(orderId, toState)`. Parking that single write proves all three are absent
mid-write and present after — the atomicity the SQL adapter got from a transaction.
Expiry adds the deadline to the guard and then releases the order's adopted holds;
if that release fails after a durable flip it is swallowed and left to the sweeper,
because the port's return means "did this call win the flip".
Adoption, commit and release span N inventory documents, so each is intent → per-id
idempotent write → completion, with the intent recorded on the order document by the
same write as the state change that implies it, and one declared index
(`holdsPendingAt`) carrying the earliest outstanding intent so the sweeper can find
it. Every completion is guarded on the order's state and closes the intent
stamp-only otherwise: re-adopting a paid order's committed holds would report every
id lost and invent a stock anomaly. The commit completion drives the SINGULAR
`commit` per id — `commitMany` skips an already-committed id and leaves its hold
live over spent units — and folds both a lost hold and an unknown reservation id
into `lost` rather than wedging the sweeper on one order.
Two corrections to ADR-0019 §4, for that ADR's next amendment. `payments.provider_ref`
UNIQUE was GLOBAL, so the dedupe is a claim document, `payment_refs/{providerRef}`,
and a reference held by another order is refused with a typed error rather than
recorded twice under a ceiling that reads the captured sum. And per-order NOTES do not
belong in this document — operator free text with no natural bound — so INC-B8 gets
`order_notes/{orderId}:{noteId}` indexed on `orderId` instead.
`recordPayment` and `flagReconciliation` land here although they sit in INC-B3's area:
both are on settle's path, so the races and the end-to-end flow cannot run without
them. `recordPayment` throws rather than silently no-op'ing on a missing order, and
`listExpirable` throws a typed, retryable signal rather than truncating a scan.
Methods INC-B3 and INC-B4 own throw a typed `NotImplementedInIncrementError` naming
their increment, and every contract case that needs one is a `test.todo` carrying the
same name (46 of them). Because the domain's suites register every case for the whole
port and import `test` themselves, the 22 cases this increment owns live in
`test/order-contract-b2.ts` as a semantically verbatim COPY (helper renames only) —
scaffolding scheduled for deletion when INC-B4 lands, and guarded meanwhile by
`test/order-contract-drift.test.ts`, which reads the domain suites as text and
requires the copy's titles plus its todo names to cover their case set exactly.
Verification: 22 B2-classified contract cases green on sqlite, Postgres and D1; both
checkout races green on Postgres with table-wide upper bounds restored (single-line
5/5 paid over 8 loops, max 7/12 attempts; multi-line 3 ids/order over 6 loops, no
half-commit, no paid order's hold left live, max 10/12); eight crash seams green on
both Node dialects (six inject a fault and read the state back; two are
completion-robustness cases that inject nothing); the order document held under an 8 KB cap by a test that prints
its size; every pre-existing file at its previous count. Also narrows
`stampHoldDeadline`'s `expiresAt` to non-null — a deadline-less hold is exactly the
one adoption classifies as lost.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NjdC8awspUte5wML6eY2X
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
EmdashOrderStore, the domain'sOrderStoreport implemented over EmDashplugin storage on one aggregate document per order — the first of three
increments on that port. This slice covers creation behind an idempotency-key
intent claim, the state machine as one guarded write that carries the flip, the
audit event, and the email-outbox entry together, order expiry, and the
hold-adoption / commit / release intents that any replayer — or the later
sweeper — completes. It is the eleventh increment of work order 02.
Scope split, stated honestly
OrderStoreport spans three increments; this PR is the first.as named
test.todos, so the next increments must un-todo exactly them —enforced, not just asserted in prose.
suites, because those suites register every case for the whole port and
import
testthemselves, so no per-case filter exists. A drift guard readsthe domain suites as text and pins the copy's titles — plus its todo names —
to cover that case set exactly; the copy is deleted once the whole suite can
be called directly.
recordPaymentandflagReconciliationlanded here ahead of the refundincrement because settlement's path calls both between marking an order paid
and committing holds, so neither the checkout races nor the end-to-end flow
can run without them.
Design points
readonlyarray ofreadonlyfields, written only bythe creating write; every later write carries it by reference. A test asserts
that editing a product after checkout leaves the order's line unchanged.
revision and the prior state, the appended audit event, and the first-wins
outbox entry for the target state all commit together. The outbox stays
once-only per (order, target state), and "flipped but no event" is
unreachable.
the order document before touching any inventory document, then completes
per reservation id under a guard on the order's current state. One declared
index carries the earliest outstanding intent's timestamp so a sweeper can
find pending work without scanning every order.
whole store, not per order — a reference already claimed by another order is
refused with a typed error rather than recorded twice.
own child collection, since operator free text has no natural bound.
budget throws a typed, retryable signal instead of returning a partial list.
address is about 2.2 KB at creation and about 4.1 KB after five transitions.
Crash seams proven
create from the claim, same order id and same minted line ids.
the claim in place and never mints a second order.
adopted — the recorded intent lets a replay finish the rest.
the singular per-id commit finishes cleanly even when one id is already done.
no-op rather than a false "lost" set.
into "lost" instead of blocking on that one id.
event, and the outbox entry are proven absent together before, and present
together after; never partial.
release is completed exactly once, and the units are returned exactly once.
Verification
Review
Two independent reviews over two rounds. Round 1 requested bracket guards, a
global payment-reference dedupe, restored race upper bounds, and record
accuracy; both reviewers approve. An independent verification run passed
twice on Postgres and once on D1.
🤖 Generated with Claude Code
https://claude.ai/code/session_011NjdC8awspUte5wML6eY2X