Skip to content

[Adapters] Implement EmdashOrderStore creation, transitions and hold-adoption intent over one order document - #256

Merged
vedanshujain merged 1 commit into
feat/in-process-commercefrom
feat/emdash-order-store-core
Sep 14, 2026
Merged

vedanshujain merged 1 commit into
feat/in-process-commercefrom
feat/emdash-order-store-core

Conversation

@vedanshujain

Copy link
Copy Markdown
Contributor

What

Adds EmdashOrderStore, the domain's OrderStore port implemented over EmDash
plugin 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

  • The OrderStore port spans three increments; this PR is the first.
  • The 22 contract cases this slice owns pass on every tier (SQLite, Postgres, D1).
  • 46 contract cases belonging to the refund and list increments are registered
    as named test.todos, so the next increments must un-todo exactly them —
    enforced, not just asserted in prose.
  • The owned cases are held in a package-local copy of the domain's contract
    suites, because those suites register every case for the whole port and
    import test themselves, so no per-case filter exists. A drift guard reads
    the 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.
  • recordPayment and flagReconciliation landed here ahead of the refund
    increment 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

  • The line snapshot is a readonly array of readonly fields, written only by
    the creating write; every later write carries it by reference. A test asserts
    that editing a product after checkout leaves the order's line unchanged.
  • Each state transition is one compare-and-set: the flip guarded on the
    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.
  • Cross-aggregate work (hold adoption, commit, release) records an intent on
    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.
  • A global claim collection dedupes payment provider references across the
    whole store, not per order — a reference already claimed by another order is
    refused with a typed error rather than recorded twice.
  • Per-order notes are left out of the order document entirely and given their
    own child collection, since operator free text has no natural bound.
  • Expiry scanning refuses to truncate silently: a page that exhausts its
    budget throws a typed, retryable signal instead of returning a partial list.
  • Document size is measured directly: an order with three lines and a shipping
    address is about 2.2 KB at creation and about 4.1 KB after five transitions.

Crash seams proven

  • A create-claim lands with no order document yet — a replay finishes the
    create from the claim, same order id and same minted line ids.
  • An order document lands with its claim never promoted — the next read heals
    the claim in place and never mints a second order.
  • A hold-adoption intent is recorded but only some of several SKUs are
    adopted — the recorded intent lets a replay finish the rest.
  • A commit intent is recorded but only some reservation ids are committed —
    the singular per-id commit finishes cleanly even when one id is already done.
  • Adoption is retried against an order that has since been paid — it reports a
    no-op rather than a false "lost" set.
  • A commit completion is retried with an unknown reservation id — it is folded
    into "lost" instead of blocking on that one id.
  • The transition write itself is interrupted mid-flight — the flip, the audit
    event, and the outbox entry are proven absent together before, and present
    together after; never partial.
  • Expiry is interrupted right after the state flip lands — the owed hold
    release is completed exactly once, and the units are returned exactly once.

Verification

Check Result
Lint / typecheck / format / build clean
SQLite 177 passed, 92 todo
Postgres, run 1 366 passed, 1 pre-existing skip, 92 todo
Postgres, run 2 366 passed, 1 pre-existing skip, 92 todo — identical to run 1
D1 118 passed, 46 todo
Checkout races both hold exactly at the stock ceiling per loop, zero contention errors, max CAS attempts 7 and 9–10 of 12
Pre-existing suites unchanged

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

…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
@vedanshujain
vedanshujain merged commit 98b8879 into feat/in-process-commerce Sep 14, 2026
2 checks passed
@vedanshujain
vedanshujain deleted the feat/emdash-order-store-core branch September 14, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant