Files
rose-ash/plans/commerce-on-sx.md
giles cda35a1ed8
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m7s
commerce: record Phase 3 flow-integration design + gotchas for next iteration
Settled design for order flow (checkboxes 1-2): Scheme flow carries only the
order-id, SX driver does all ledger IO. Key gotcha captured: never return
flow-make-env from eval (serializer hangs on the cyclic env); run the flow
suite single-process like flow's own conformance with a long timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:59:22 +00:00

168 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# commerce-on-sx: Catalog, cart, pricing & orders on miniKanren
> **DRAFT outline.** The revenue vertical. Depends on `persist-on-sx` (durable
> orders) and `flow-on-sx` (checkout as a durable flow). Don't start before
> persist-on-sx Phase 1 is green.
rose-ash's revenue engine — market (catalog), cart (checkout), orders (SumUp
payment, reconciliation) — has no SX subsystem. The hard part of commerce isn't
CRUD; it's **pricing**: discounts, bundles, tax, membership rates, promotions that
stack (or don't). These are relations, and a relational engine can run them in
multiple directions — forward ("what's the total?") and backward ("what promo code
yields this total?", "which line item triggered the discount?").
That's a miniKanren fit. Pricing/promotion rules are relational; cart and order
*lifecycle* (reserve → pay → fulfil → reconcile) is a durable `flow`; the order
ledger is a `persist` stream. Commerce is the first real **composition** subsystem.
End-state: a catalog model, a relational pricing/promotion engine, a cart with
deterministic totals, and an order lifecycle flow with payment-webhook
reconciliation — all auditable via the event log.
## Status (rolling)
`bash lib/commerce/conformance.sh`**132/132** (8 suites: catalog, cart, price, api, promo, stack, quote, ledger) — Phases 1-2 done; Phase 3 ledger done
## Ground rules
- **Scope:** only `lib/commerce/**` and `plans/commerce-on-sx.md`. May **import**
from `lib/minikanren/`, and (once they exist) `lib/persist/` + `lib/flow/`. Do not
edit substrates.
- **Architecture:** prices/promotions are miniKanren relations over catalog facts;
a cart total is a *deterministic* query result (first solution under a fixed rule
order). Order lifecycle is a `flow` that suspends at the payment IO boundary.
Money is integer minor units — never floats.
- **Determinism:** promotion stacking must have explicit, tested precedence;
totals must be reproducible from the cart + catalog snapshot.
- **Commits:** one feature per commit. Progress log + tick boxes.
## Architecture sketch
```
Catalog + cart Total / order
product(id,price,tags) {:subtotal :discounts :tax :total}
│ ▲
▼ │
lib/commerce/catalog.sx lib/commerce/price.sx
— product / variant / stock facts — miniKanren pricing relations
│ — promo stacking, membership rates
▼ ▲
lib/commerce/cart.sx lib/commerce/order.sx (flow + store)
— line items, quantities — reserve→pay→fulfil→reconcile
│ — SumUp webhook = flow resume
▼ │
lib/commerce/api.sx ── (commerce/add) (commerce/total) (commerce/checkout) ──┘
```
## Phase 1 — Catalog + cart + deterministic totals
- [x] `catalog.sx` — product/variant/stock as facts
- [x] `cart.sx` — line items, add/remove/qty
- [x] `price.sx` — base pricing relation, subtotal; tax
- [x] `api.sx` + tests + scoreboard + conformance.sh
## Phase 2 — Promotions (relational)
- [x] promo rules: percentage, fixed, bundle, member rate
- [x] explicit stacking precedence; "best price" backward query
- [x] tests: stacking order, mutually-exclusive promos, member vs guest
## Phase 3 — Order lifecycle (flow + store)
- [ ] order flow: reserve stock → await payment → fulfil
- [ ] payment webhook resumes the suspended flow
- [x] order ledger as a `persist` stream; idempotent reconciliation
## Phase 4 — Reconciliation + federation
- [ ] mismatch detection (paid≠ordered) as queries over the ledger
- [ ] cross-instance catalog (federated marketplace) — out-of-scope stub
- [ ] tests: webhook replay, partial refund, double-charge guard
## Progress log
- 2026-06-07 — `ledger.sx` (Phase 3 piece, checkbox 3): order ledger as a
persist event stream "order/<id>". Status/total/paid/recon are projections
(folds) over events — ledger is the single source of truth. `order-pay`/
`order-refund` are idempotent via `persist/append-once` keyed on the payment
ref, so a replayed SumUp webhook records once (no double-charge). `order-recon-of`
classifies :unpaid/:ok/:underpaid/:overpaid on net (paidrefunded) vs total;
`ledger-mismatches` finds genuine paid≠ordered across all streams. Verified
minikanren+scheme/flow+persist all coexist in one sx_server process. ledger
suite 20/20; total 132/132. Next: order flow (reserve→pay→fulfil) as a Scheme
flow-on-sx flow with webhook resume (checkboxes 1-2) — needs SX↔Scheme quote
marshalling.
- 2026-06-07 — `quote.sx` (pricing capstone, bridges Phase 2→3): `cart-quote`
composes price+promo+stacking into the deterministic priced quote
`{:subtotal :discount :tax :total :codes}` with `total = subtotal - discount
+ tax`. Explicit tax policy: tax on GROSS per-line amounts (discount reduces
payable, not tax base) — documented for the determinism contract. This quote
is the value the Phase-3 order flow will carry. quote suite 13/13; total
112/112.
- 2026-06-07 — `stack.sx` (**Phase 2 complete**): stacking precedence as a
separate selection layer (precedence NOT in the rules, per the miniKanren
design rule). Exclusivity = unordered code pairs; `valid-stackings` enumerates
every legal subset of applicable promos (powerset excluded combos);
`best-stacking` is the deterministic max-total-discount selection (stable on
ties). `stacking-by-totalo` is the best-price backward query ("which legal
stacking yields total D?"). Member vs guest falls out of applicable-promos.
stack suite 16/16; total 99/99.
- 2026-06-07 — `promo.sx` (Phase 2 piece 1): four promo types as tagged tuples
`(:percent code class bps)`/`(:fixed code threshold amount)`/`(:bundle code sku
n)`/`(:member code class bps)`. Per-promo discount is pure integer arithmetic;
`promo-discounto`/`promo-applieso` enumerate (code, amount) relationally —
forward ("which apply?") and backward ("which code yields 2000?" → run* over
applieso). `applicable-promos`/`promo-amount-for` deterministic helpers. promo
amounts via `project` to ground the membero-bound promo. promo suite 17/17;
total 83/83. Next: stacking precedence + best-price (stack.sx).
- 2026-06-06 — `api.sx` (**Phase 1 complete**): session facade
`{:ctx :cart}` with `commerce-add`/`-remove`/`-set-qty`/`-total`/`-count`/
`-lines`, `commerce-can-add?` catalog validation, `commerce-explain` per-line
audit breakdown ({:sku :variant :qty :unit :extended :tax}), and a
`commerce-checkout` Phase-3 stub. api suite 12/12; total 66/66.
- 2026-06-06 — `price.sx`: deterministic `cart-subtotal` (Σ unit×qty, variant
delta defaults 0) + jurisdiction-relational tax. `taxo` facts indexed by
(jurisdiction, product-class, customer-class)→bps, queried multidirectionally;
`apply-bps` rounds half-up with integer arithmetic only. `cart-total` returns
`{:subtotal :discounts :tax :total}` (discounts 0 until Phase 2), reproducible
from (context, cart). `=` does structural dict equality (order-independent), so
total dicts compare directly. price suite 20/20; total 54/54.
- 2026-06-06 — `cart.sx`: cart as an ordered list of (sku variant qty) lines.
Pure ops `cart-add` (merges same line / appends), `cart-set-qty` (0 removes),
`cart-remove`, plus `cart-qty`/`cart-count`/`cart-skus`/`cart-empty?`.
`cart-lineo` is the relational view (membero over the cart) — forward and
backward. cart suite 18/18; total 34/34.
- 2026-06-06 — `catalog.sx`: catalog snapshot (products/variants/stock as fact
tuples) + multidirectional accessor relations (`producto`/`varianto`/`stocko`,
derived `priceo`/`classo`/`unit-priceo`) + deterministic `catalog-price`/
`-class`/`-has?` helpers. `conformance.sh` harness + scoreboard. catalog suite
16/16. Gotcha: minikanren `run-n` macro binds `s` internally — query vars must
avoid `s`; tests compare reified results with `=` (not `equal?`, which fails on
reified lists). Money = integer minor units throughout.
## Phase 3 flow-integration notes (for the next iteration)
Order flow = checkboxes 1-2 (reserve→pay→fulfil as a flow-on-sx flow + webhook
resume). Design is settled; the remaining work is mechanical but slow to iterate.
- **flow is the Scheme-on-SX guest layer**, not the SX/minikanren host. Load
order: `lib/guest/{lex,reflective/env,reflective/quoting}` + `lib/scheme/{parser,
eval,runtime}` + `lib/flow/{spec,store,remote,host,api}`. Confirmed it coexists
with the minikanren + persist stacks in one sx_server process.
- **Driver API (SX side):** `(flow-make-env)` builds the env once; `(flow-run-in
env "<scheme-src>")` evaluates a Scheme program string. Flows/driving are all
Scheme: `(flow/start flow input)`, `(flow/resume id val)`, `(flow/pending)`,
`(flow/status id)`, `(flow/result id)`. Host ABI (host.sx): `(request kind
payload)` suspends with a typed envelope; `(flow-host-requests)` lists pending.
- **Settled design:** the Scheme flow carries ONLY the order-id (a string) and is
pure orchestration: `(defflow ordf (lambda (oid) (begin (request 'reserve oid)
(request 'payment oid) (request 'fulfil oid))))`. All IO/ledger work stays in
SX — the SX driver services each request by appending to the persist ledger
(ledger.sx) and resuming with a marker. Payment stays suspended until the
webhook calls flow/resume. Marshalling is trivial (just strings).
- **GOTCHA (cost me a turn):** `flow-make-env` returns a large/likely-cyclic env
object; returning it from `(eval "...")` makes the harness serializer hang (got
exit 0 with NO epoch-2 output). NEVER return the env — wrap as `(begin (define
env (flow-make-env)) :ok)`. Structure the flow suite like `lib/flow/conformance.sh`:
load once, build env once, run all assertions in ONE process returning small
count values. Budget a long timeout (flow's own suite uses 540s); env build is
~150s CPU and balloons under sibling-agent CPU contention.
## Blockers
(none)