Files
rose-ash/plans/events-on-sx.md
giles 80a2dee22f
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s
events: capacity-safe transactional booking on persist + 24 tests
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>
2026-06-07 01:44:43 +00:00

128 lines
7.2 KiB
Markdown

# 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) and `flow-on-sx` (reminders, retrying
> delivery). Pairs with `commerce-on-sx` for 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
`flow`s. 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/**` and `plans/events-on-sx.md`. May **import** from
`lib/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 `persist` event (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
- [x] `calendar.sx` — event facts, RRULE expansion in a window (DAILY/WEEKLY)
- [x] `calendar.sx` — MONTHLY (bymonthday + nth-weekday byday)
- [x] `availability.sx` — free/busy rules (busy/conflict/busy_in on Datalog)
- [x] `availability.sx` — next-free slot search (same rules, different bindings)
- [x] `api.sx` — public entry points (schedule/agenda/book/free/next-free/conflicts)
- [x] tests + scoreboard + conformance.sh (73/73)
## Phase 2 — Ticketing + booking
- [x] capacity rules; transactional booking → `persist` (no overbooking)
- [ ] wire `booking.sx` into `api.sx` (persist-backed `ev/book`)
- [ ] cancellation (tombstone events) + seat release
- [ ] paid tickets compose with `commerce` order flow
- [x] 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 a
`delivery-on-sx` once 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 via
`persist/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`: immutable `store`
({: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/book`
becomes capacity-safe via a persist append at that point.
- 2026-06-07 — `next-free` slot search: earliest start ≥ after where
[s,s+duration) is free and ends ≤ horizon, else nil. Candidates are `after`
plus each busy-interval end (interval-packing); each probe reuses the
`busy_in` Datalog rule via `ev-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 over `occurrence`/`booking` EDB. Rules `busy(A,S,E)`,
`conflict(A,O1,O2)` (canonical `O1<O2`, half-open overlap `S1<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 shared
`ev-emit-occs` per-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-all` merges + sorts. Wired
conformance harness (conf + thin wrapper reusing `lib/guest/conformance.sh`),
scoreboard. 24/24 green. MONTHLY deferred to next commit.
## Blockers
- None. Substrates present: `lib/datalog` (276/276), `lib/persist`, `lib/flow`
all exist — Phase 2/3 unblocked when reached.