cross-domain slice 1: events as a fed-sx peer + allocate-a-post-to-a-calendar (LIVE)

The first cross-domain federated workflow — behaviors defined by TYPES, across domains.

- events.rose-ash.com is now a fed-sx PEER: a lib/host instance with SX_DOMAIN=events whose 'calendar'
  TYPE declares an on-allocate behavior. Replaces the Python events service (no strangler). serve.sh
  gates domain types/behaviors on SX_DOMAIN (blog=article publish/digest; events=calendar+allocate).
- DIRECTED cross-domain delivery: an activity with :to <peer-base> is delivered to that peer's inbox
  (∪ followers). The wire gains 'to'. So 'allocate' targets the events peer specifically.
- host/blog--allocate-activity/allocate! + POST /:slug/allocate?calendar=<id>; the events calendar
  type's allocate-link DAG (an execute-fold effect) fires on receipt.
- docker-compose: the sx_events service (own store, shared SX_FED_SECRET, externalnet for a future
  events.rose-ash.com Caddy route).

LIVE PROOF: publish 'Gig Night' on blog.rose-ash.com → POST /gig-night/allocate?calendar=main → the
events peer RECEIVES the directed, signed activity (/activities: 'allocate article gig-night') and
its calendar type's on-allocate behavior FIRES (/flows: 'linked gig-night'). blog 218/218, full
conformance green.

NEXT: events runs lib/events (real calendars/recurrence/ticketing); link event→post; shop
(lib/commerce) sells tickets — same federated, type-declared shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 20:25:37 +00:00
parent 43c085e8e8
commit 355bcbefdc
5 changed files with 85 additions and 32 deletions

View File

@@ -148,6 +148,16 @@
:relation kind :target dst
:delta (str verb " " kind " " dst)
:id (str verb ":" src ":" kind ":" dst)}))
;; ── CROSS-DOMAIN (events): allocate a post to a calendar — a DIRECTED activity (:to the events peer).
;; It federates to events, whose calendar type declares an on-allocate behavior that links it.
(define host/blog--events-base "") ;; the events peer base URL (serve-set from SX_EVENTS_BASE)
(define host/blog--set-events-base! (fn (b) (set! host/blog--events-base b)))
(define host/blog--allocate-activity
(fn (post calendar)
{:verb "allocate" :actor host/blog--actor
:object post :object-type (host/blog--post-type post) :slug post
:target calendar :to host/blog--events-base
:delta (str "allocate to " calendar) :id (str "allocate:" post ":" calendar)}))
;; MARSHAL the canonical activity → next/'s Erlang proplist shape, for the Erlang runner adapter
;; (RA). The seam activity is canonical; each runner adapter maps it to its substrate. Unused until
;; RA, defined + tested here so the reconcile is complete and RA has its bridge ready.
@@ -173,7 +183,8 @@
;; the ctx a publish activity presents to the publish-DAG (string keys — preds read ctx by key).
;; Reads the canonical activity's top-level :category + :slug (P0.4).
(define host/blog--publish-ctx
(fn (activity) {"category" (get activity :category) "slug" (get activity :slug)}))
(fn (activity) {"category" (get activity :category) "slug" (get activity :slug)
"target" (get activity :target) "verb" (get activity :verb)}))
;; ── P1: types DECLARE behavior; the runner is DERIVED from the DAG's capabilities ──────
;; A type-post carries :behavior = a list of flat string-keyed bindings {"verb" "type" "dag"} (like
@@ -346,11 +357,13 @@
(define host/blog--outbox-persist! (fn () (persist/backend-kv-put host/blog-store host/blog--outbox-key host/blog--outbox)))
(define host/blog--enqueue-outbox!
(fn (a)
(begin
(for-each (fn (base) (set! host/blog--outbox
(concat host/blog--outbox (list {"peer" base "wire" (host/ta--serialize a)}))))
(host/blog--delivery-bases))
(host/blog--outbox-persist!))))
(let ((targets (host/flow--uniq-concat (host/blog--delivery-bases)
(if (and (get a :to) (not (= (get a :to) ""))) (list (get a :to)) (list)))))
(begin
(for-each (fn (base) (set! host/blog--outbox
(concat host/blog--outbox (list {"peer" base "wire" (host/ta--serialize a)}))))
targets)
(host/blog--outbox-persist!)))))
;; guarded, SIGNED delivery: POST the wire; a connection failure returns false (item kept), never raises.
(define host/blog--try-deliver
(fn (peer wire) (guard (e (true false)) (begin (host/blog--fed-post peer wire) true))))
@@ -386,6 +399,8 @@
;; a relation change → an Add/Remove activity (edge referenced, no CID shift).
(define host/blog--emit-relation!
(fn (verb src kind dst) (host/blog--emit! (host/blog--relation-activity verb src kind dst))))
;; CROSS-DOMAIN: allocate a post to a calendar on the events peer (directed :to → federates to events).
(define host/blog--allocate! (fn (post calendar) (host/blog--emit! (host/blog--allocate-activity post calendar))))
;; ── render ──────────────────────────────────────────────────────────
;; A post's sx_content is SX element markup -> HTML via render-page (which supplies
@@ -3022,10 +3037,19 @@
(host/require-login resolve)
(host/require-permission "edit" (fn (req) "blog")))
h)))
;; POST /<slug>/allocate?calendar=<id> — allocate a post to a calendar on the events peer. Emits a
;; directed "allocate" activity that federates to events, whose calendar type reacts (P1 behavior).
(define host/blog-allocate
(fn (req)
(let ((post (dream-param req "slug")) (calendar (or (dream-query-param req "calendar") "main")))
(begin
(when (not (= host/blog--events-base "")) (host/blog--allocate! post calendar))
(dream-redirect (str "/" post "/"))))))
(define host/blog-write-routes
(fn (resolve)
(list
(dream-post "/new" (host/blog--protect-html resolve host/blog-form-submit))
(dream-post "/:slug/allocate" (host/blog--protect-html resolve host/blog-allocate))
(dream-get "/:slug/edit" (host/blog--protect-html resolve host/blog-edit-form))
(dream-post "/:slug/edit" (host/blog--protect-html resolve host/blog-edit-submit))
(dream-post "/:slug/blocks/add" (host/blog--protect-html resolve host/blog-block-add-submit))