booking.sx: per-occurrence append-only stream, roster = replay. Booking decided against an observed (roster, last-seq) snapshot, committed via persist/append-expect — atomic check+append, no overbooking, no lock. Explicit last-seat race test: two bookers, one booked, one conflict, roster capped. Idempotent per actor. 97/97 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.2 KiB
events-on-sx: Calendar, ticketing & notification delivery on Datalog
DRAFT outline. The events vertical + the shared notification-delivery edge. Depends on
persist-on-sx(bookings ledger) andflow-on-sx(reminders, retrying delivery). Pairs withcommerce-on-sxfor paid tickets.
rose-ash's events domain is calendar + ticketing: recurring events, availability,
capacity, bookings. Scheduling is constraint reasoning — "is this slot free given
recurrence, capacity, and the attendee's other bookings?" — which is rule
evaluation over facts. Datalog expresses availability, recurrence expansion, and
capacity as rules; a booking is a transaction; reminders and digests are durable
flows. Notification delivery (email/push) — needed here and by feed/notify —
is folded in as an injected transport, extractable later.
End-state: a Datalog-on-SX events layer with recurrence expansion, availability + capacity rules, transactional booking, and a flow-driven notification dispatcher (reminders, digests, retries) over an injected transport.
Status (rolling)
bash lib/events/conformance.sh → 97/97 (Phase 1 complete + Phase 2 capacity-safe booking)
Ground rules
- Scope: only
lib/events/**andplans/events-on-sx.md. May import fromlib/datalog/, and (once they exist)lib/persist/+lib/flow/. Do not edit substrates. - Architecture: events/availability/capacity are Datalog facts + rules;
recurrence expands to occurrence facts within a window; a booking checks rules
then appends a
persistevent (idempotent, capacity-safe). Notifications are flows that suspend on transport IO and retry on failure. - Determinism: recurrence expansion + availability must be reproducible for a fixed window + ruleset; capacity checks must be race-safe (no overbooking).
- Commits: one feature per commit. Progress log + tick boxes.
Architecture sketch
Event + booking Result
event(id,start,rrule,capacity) {:booked | :full | :conflict} + reminders
│ ▲
▼ │
lib/events/calendar.sx lib/events/availability.sx
— event facts, recurrence (RRULE) — free/busy + capacity rules (Datalog)
— expand occurrences in window │
│ ▲
▼ │
lib/events/booking.sx lib/events/notify.sx (flow)
— transactional, capacity-safe — reminders / digests, retry on fail
— bookings → persist ledger — injected transport (email/push)
│ │
▼ ▼
lib/events/api.sx ── (events/schedule) (events/book) (events/agenda) ──────┘
Phase 1 — Calendar + recurrence
calendar.sx— event facts, RRULE expansion in a window (DAILY/WEEKLY)calendar.sx— MONTHLY (bymonthday + nth-weekday byday)availability.sx— free/busy rules (busy/conflict/busy_in on Datalog)availability.sx— next-free slot search (same rules, different bindings)api.sx— public entry points (schedule/agenda/book/free/next-free/conflicts)- tests + scoreboard + conformance.sh (73/73)
Phase 2 — Ticketing + booking
- capacity rules; transactional booking →
persist(no overbooking) - wire
booking.sxintoapi.sx(persist-backedev/book) - cancellation (tombstone events) + seat release
- paid tickets compose with
commerceorder flow - tests: capacity edge, double-book guard, conflict detection
Phase 3 — Notification delivery (flow)
notify.sx— reminder/digest flows over injected transport- retry/backoff on transport failure (flow suspend/resume)
- tests: delivery success, retry path, idempotent re-send
- NOTE: shared with
feed/notify— candidate for later extraction to adelivery-on-sxonce a second consumer is real
Phase 4 — Federation
- cross-instance events (peer calendar) — trust-gated stub
- tests: federated agenda merge
Progress log
- 2026-06-07 — Phase 2 start: capacity-safe booking.
booking.sx: one append-only persist stream per occurrence; roster = stream replayed. Booking decisions made against an OBSERVED (roster, last-seq) snapshot, committed viapersist/append-expect— append only if last-seq unchanged, else a conflict value the booker retries. This makes capacity-check + append atomic at the persist boundary: no overbooking, no lock.ev/book!(retrying),ev/book-with-observed,ev/roster,ev/seats-left. Idempotent per actor (:already). Explicit last-seat race test: two bookers on the same snapshot → one :booked, one :conflict, roster never exceeds capacity; loser retry → :full (or next seat when room remains). 24 tests, 97/97 green. - 2026-06-07 — Phase 1 complete.
api.sx: immutablestore({:events :bookings}) facade over calendar + availability.ev/schedule,ev/book,ev/agenda,ev/agenda-for,ev/free?,ev/next-free,ev/conflicts. Availability queries auto-widen the expansion window back by the longest event so any overlapping occurrence is captured. 14 tests, 73/73 green. Phase 2 (transactional booking on persist) is next —ev/bookbecomes capacity-safe via a persist append at that point. - 2026-06-07 —
next-freeslot search: earliest start ≥ after where [s,s+duration) is free and ends ≤ horizon, else nil. Candidates areafterplus each busy-interval end (interval-packing); each probe reuses thebusy_inDatalog rule viaev-free?. Finds gaps between bookings, skips too-short gaps, half-open at edges. +6 tests, 59/59. - 2026-06-07 —
availability.sx: free/busy + conflict detection as forward- chained Datalog overoccurrence/bookingEDB. Rulesbusy(A,S,E),conflict(A,O1,O2)(canonicalO1<O2, half-open overlapS1<E2 ∧ S2<E1),busy_in(A,QS,QE)for window queries. API:ev-busy,ev-conflicts,ev-has-conflict?,ev-free?(transient qwindow assert/retract). Integrates with calendar expansion (book expanded occurrences). 16 tests, 53/53 green. - 2026-06-06 — MONTHLY recurrence.
ev-days-in-month,ev-add-months, BYMONTHDAY (incl. negative = from month end), ordinal BYDAY ({:ord N :wd W}, ord<0 = nth-from-last), default day-of-month (skips months too short, e.g. day-31 monthly skips Feb/Apr). Refactored weekly+monthly onto a sharedev-emit-occsper-period emitter. 37/37 green (+13). - 2026-06-06 — Phase 1 scaffold + calendar recurrence.
calendar.sx: integer epoch-minute datetimes, Hinnant civil<->day-number conversion, DAILY/WEEKLY RRULE expansion in a bounded (start,end) window with INTERVAL, COUNT (window- independent), UNTIL, BYDAY (weekly).ev-expand-allmerges + sorts. Wired conformance harness (conf + thin wrapper reusinglib/guest/conformance.sh), scoreboard. 24/24 green. MONTHLY deferred to next commit.
Blockers
- None. Substrates present:
lib/datalog(276/276),lib/persist,lib/flowall exist — Phase 2/3 unblocked when reached.