Commit Graph

4207 Commits

Author SHA1 Message Date
125d9f1398 sx-git Phase 4: worktree — tree materialization, index overlay, status (TDD)
Worktree is a value (path->data dict). tree-from-files/tree-files round-trip
through real tree objects (cid-identical to hand-built trees); index =
{:base tree-cid :staged overlay} in kv with add!/rm!/unstage!/index-tree!;
status = three-way dict diff (HEAD vs index vs worktree) with
staged/unstaged/untracked. 26/26, total 132/132.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:11:04 +00:00
eda6806989 sx-git Phase 3: commit DAG — log/ancestry/merge-base SX-side + Datalog bridge (TDD)
Topo log = reverse DFS postorder over parent edges from commit objects;
reflexive is-ancestor?, all-LCA merge-bases (criss-cross verified). Datalog
bridge exports (git-parent child parent) facts under a minimal 2-rule
ancestor closure, cross-checked against the SX walk. 30/30, total 106/106.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:07:06 +00:00
74c2521926 sx-git Phase 2: refs — branches/tags/HEAD over persist kv, CAS + reflog (TDD)
Ref value = {:cid} | {:symref}; atomic moves via persist/kv-cas old-value
expect, create-only branches via kv-put-new; bounded symref resolution;
per-ref append-only reflog on the persist log facet. 38/38, total 76/76.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:03:36 +00:00
9a85b52d1a sx-git Phase 1: blob/tree/commit/tag as content-addressed typed objects (TDD)
Objects are plain dicts over persist kv, addressed by sx1:<sha256> of the
artdag/canon canonical form (sorted dict keys) — native CIDs, extensible
fields participate in identity. 38/38.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:01:11 +00:00
f561deede3 plan: log hardening pass H1-H7 (TDD) + the commerce arc next steps
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:56:58 +00:00
99401ae21e H7: adjacency streams — per-(node,kind) edge reads, no more full kv scans (TDD)
Failing tests first (2 red: relate! wrote no adjacency streams). Every edge write now maintains
per-pair event streams (rel:src|kind ← {:dst}, rin:dst|kind ← {:src}); host/blog-out/-in/--out-raw
(+ new --in-raw) fold ONLY the pair's stream — O(edges of that node under that kind) instead of
O(all kv keys) per read. Append-only ⇒ no read-modify-write race (duplicate :adds fold to a set).
The edge:* kv rows remain (whole-graph consumers: subtype-closure, relations admin block) and feed
host/blog-reindex-edges! — the idempotent boot migration serve.sh now runs, so pre-H7 live stores
read correctly. Collapsed host/blog--add-edge-kv! into add-edge! (the type-algebra conj/disj edges
were bypassing the streams — caught by the existing algebra tests going red).

blog suite 256/256 (+6); FULL conformance 658/658.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:53:32 +00:00
f8b96b3d81 H6: durable activity dedup — same :id processed at most once, ever (TDD)
Failing tests first (3 red: a redelivered activity reran its behavior — behavior/process starts
from an empty trace, so dedup evaporated per call). host/blog--process-local! now atomically
claims the :id on persist stream 'activities:processed' via ev/book! (the same append-expect
acquire as seats/votes) and returns a :deduped trace on duplicates. Store-backed → survives outbox
retries AND restarts. Prerequisite for non-idempotent effects (payment). Id-less activities process
unchecked.

blog suite 250/250 (+3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:45:55 +00:00
edbb2d4a37 H5: two-phase buy — hold → guarded mint → confirm/release (TDD, seat-leak fix)
Failing tests first (red: a failed mint left the seat consumed — the cross-domain leak; a RAISING
mint escaped the handler entirely, proving no guard). host/blog--mint-ticket is now an injectable
seam (default: signed HTTP to the shop). buy-ticket: ev/hold! reserves (capacity-counted, atomic) →
mint runs inside (guard (e (true "")) ...) → 'ticket:' ⇒ ev/confirm! + sold edges + a 'sell'
activity (H4's missing emission); anything else ⇒ ev/release! frees the seat. Held seats count
toward capacity, so a pending mint can't be oversold either.

blog suite 247/247 (+3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:43:26 +00:00
e5fd4f8e0b H4: P2 restored — cinema/poll mutations emit activities (TDD)
Failing tests first (6 red: no cinema/poll handler emitted anything — films/showings/votes were
invisible to federation and other peers' behaviors). Now: new-film→create(film),
new-showing→schedule(showing), offering-add→offer, offering-update→update (id carries new values so
distinct changes federate, replays dedup), offering-remove→retract, add-poll→create(poll),
new-event→schedule(event), vote→vote(poll) — voter kept OFF the wire (seat number makes the id
unique; pinned by test). All through host/blog--emit! (engine + durable activity log + outbox).
buy-ticket's sell emission lands with H5's injectable mint.

blog suite 244/244 (+6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:16:40 +00:00
f1238c1a38 H3: votes are atomic claims on the persist stream (TDD)
Failing tests first (2 red: the vote wasn't on the stream, and dedup vanished if the projection
edge was removed — proving it was an edge-scan). host/blog-vote now acquires on stream
'vote:<poll>' via ev/book! (append-expect, retry-on-conflict — the same atomicity as seats);
the option --voted--> edge is a projection recorded only on :booked. Removed the read-check-write
host/blog--voted-in-poll?. Governance-grade: no double vote under concurrency, dedup survives
projection wipes + restarts (store-backed).

blog suite 238/238 (+4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:12:32 +00:00
eb54d17df9 H2: auth-gate cinema/poll admin ops (TDD)
Failing tests first (6 red: unauth /new-film created a film, etc). new-film / new-showing /
offering-add|update|remove / add-poll / new-event moved from the public route list into
host/blog-write-routes behind protect-html — same gate as every blog write. /vote, /buy-ticket,
/buy stay public (voters + customers) with explicit tests pinning that.

blog suite 234/234 (+9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:09:58 +00:00
8e0f06aa28 H1: HMAC-gate internal endpoints /ticket /order /person (TDD)
Failing tests first (4 red: unsigned POSTs returned 200 and minted objects), then the gate:
host/blog--int-verify? checks x-int-sig = sess-sig(fed-secret, request TARGET) (params live in the
query, body is empty); host/blog--protect-internal wraps the three routes → 403 unsigned. Secret
unset = open (dev/tests). Callers (events→shop /ticket + /order, shop→identity /person) sign via
host/blog--int-headers. Closes the live capacity-bypass (anyone could mint tickets directly).

blog suite 225/225 (218 + 7 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:07:40 +00:00
a7533b26b1 polls on blog posts: a non-commercial Claim (vote), proving the grammar off the commerce axis
Demonstrates a DIFFERENT configuration on the same substrate: a post carries polls; a vote is the
same 'acquire, deduped by actor' shape as a booking, with money + capacity turned OFF.

- host/blog-add-poll: a poll is-a poll (field question), post --has-poll--> poll, options as option
  posts (is-a option, field label), poll --option--> opt.
- host/blog-vote: one vote per voter per poll (host/blog--voted-in-poll? checks all options), records
  option --voted--> voter. No capacity, no payment — a Claim with those axes off.
- host/blog--post-polls / --poll-view / --poll-form: results (per-option counts) + a vote form per
  option + an Add-a-poll form, shown on every post page.

LIVE on blog.rose-ash.com/welcome: Dune 2 / Oppenheimer 1 / Barbie 0 (a repeat voter refused). Same
dedup as ev/book!, zero new mechanism. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:48:23 +00:00
ab058147fc wire lib/events capacity-safe booking under the cinema buy path (first subsystem integration)
The events peer's ticket purchase now goes through lib/events' ATOMIC booking instead of a
read-check-write race — real domain logic dropping in under the same clickable cinema, exactly as
the seam promised.

- MODULES (serve.sh + conformance.sh): load lib/persist/concurrency.sx (append-expect/conflict?) +
  lib/events/booking.sx. host/blog-store (persist/open) is stream-capable, same backend lib/events
  tests use.
- host/blog-buy-ticket: replaced '(< (len sold) capacity)' with (ev/book! host/blog-store <showing>
  <capacity> <actor>); proceed only on :status :booked. occ-key = showing slug, capacity =
  host/blog--showing-capacity, actor = email + current roster len (unique per seat, collapses
  double-clicks, allows a person to hold several seats). persist append-expect retries on conflict —
  no oversell even under concurrent buys.
- Per-offering cap + the sold-edge display are unchanged (render-safe); ev/book! is the authoritative
  gate in the handler.

VERIFIED LIVE on events.rose-ash.com: cap-2 showing → 3 buys, exactly 2 booked (3rd refused);
cap-1 showing → 10 CONCURRENT buys → exactly 1 sold, SOLD OUT. Sandbox: ev/book! returns
booked/booked/full/already for a cap-2 occ. blog 218/218 (with the two new modules loaded).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:12:36 +00:00
ee4dbf3be9 offering editor: per-showing ticket types with prices + per-offering caps
Each showing's offerings are now independently editable — the 'some / all / extra + special offer,
different prices, different caps' from the cinema model.

- host/blog--offering-editor: a collapsible '⚙ Manage offerings' panel on the showing page — per
  offering an inline price+cap Save form and a Remove button, plus an Add-offering form.
- host/blog-offering-update: edit an offering's price + cap.
- host/blog-offering-remove: unlink an offering from the showing (sold tickets keep their record).
- host/blog-offering-add: add an offering, CREATING the ticket type first if new (e.g. special-offer
  → seeds the ticket-type + is-a). host/blog--offering-showing resolves the parent showing.
- Per-offering CAP enforcement: host/blog--offering-available? (offering sold < its cap, else only the
  showing capacity limits it). buy-ticket checks it and tallies offering --sold--> ticket per offering;
  the tickets section shows 'type — £price (sold/cap)'.

This covers the layout-style variable caps too (seated / tables / standing = per-offering caps).

LIVE: on the Dune showing — set adult £12 cap 2, added special-offer £5 cap 1, removed u18; buying the
special-offer twice yields 1/1 sold (second blocked). blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:43:31 +00:00
2037ae45d1 identity: /people view (Persons, email-keyed) for id.rose-ash.com
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:07:28 +00:00
78d8fd54c5 cinema tickets: capacity-enforced buy across events→shop→identity (4 domains)
People can now buy tickets, from the web UI, with capacity enforcement — the heart of the model.

- Showing page (events): a 🎟 Tickets section (host/blog--showing-extras) shows capacity/sold + a Buy
  form per Offering (ticket type + price). host/blog--showing-capacity = the showing's override else
  its calendar's screen's default (via on-calendar → has-calendar reverse).
- host/blog-buy-ticket (events): CAPACITY-CHECKED (sold < capacity), then POSTs to shop /ticket and
  records showing --sold--> ticket. Sold out → the Buy form is replaced by 'sold out'.
- host/blog-ticket (shop): issues a Ticket (is-a ticket, for showing, bought-as offering, owned-by
  the person's email) + registers the person on the identity peer.
- host/blog-person (identity): find-or-create a Person keyed by email (login-optional) → person:<id>.
- IDENTITY is a new 4th fed-sx peer (sx_identity, SX_DOMAIN=identity, id.rose-ash.com-ready); shop
  gets SX_IDENTITY_BASE. serve.sh gains shop 'ticket' type + identity 'person' type seeds.

LIVE end-to-end: events.rose-ash.com/<showing> → Buy adult (alice@example.com) → sold 0→1, a ticket
on market.rose-ash.com, a person on identity. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:02:38 +00:00
7aaf2c9b17 cinema model (events core): Cinema/Screen/Calendar/Film/Showing/Offering + /cinema admin
The Rose Ash Cinema object model, operable on events.rose-ash.com/cinema.

- host/blog-seed-cinema!: seeds the type-posts (cinema/screen/calendar/film/ticket-type/showing/
  offering) + Rose Ash Cinema with two screens (each capacity 100 + a calendar) + ticket types
  (adult/u18/concession/standing). Idempotent. Called from serve.sh's events block.
- /cinema page: screens (with capacity) → their calendars → showings; a Films list (each with its
  ticket types); an Add-film form; a Book-a-showing form.
- host/blog-new-film: creates a film is-a film + default ticket types (adult, u18).
- host/blog-new-showing: books a Film onto a Calendar at a time (showing of-film / on-calendar,
  calendar --scheduled--> showing), with an optional per-showing capacity override, and SNAPSHOTS
  the film's ticket types as Offerings (offering of-type, showing --offers--> offering, price field).
- Views: host/blog--screens-view / --calendar-view / --films-view (all via out-raw for federated refs).

LIVE: events.rose-ash.com/cinema shows the two screens + calendars; add 'Dune' → film with adult/u18;
book Dune on cal-screen-1 Fri 8pm → a showing with offerings, listed under the calendar. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:56:30 +00:00
12b4e15569 shop: /orders view on market.rose-ash.com (ticket orders per event)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:17:41 +00:00
f28f960481 cross-domain UX: link from blog's allocate form to events.rose-ash.com/calendars
The allocations live on the events PEER (events.rose-ash.com), not on blog — blog federates them
away. Repointed Caddy events.rose-ash.com → sx-dev-sx_events-1 (in /root/caddy/Caddyfile, external
to this repo; needed a 'docker service update --force caddy_caddy' because a single-file bind mount
is inode-pinned — editing swaps the inode). Added a '→ view on events' link on the blog allocate
form so the workflow is navigable. events.rose-ash.com/calendars now shows allocated posts +
scheduled events + ticket sales, publicly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:07:25 +00:00
a5a6698772 cross-domain slices 2-3: events + shop peers, full workflow operable from the web UI
The whole vision, clickable end-to-end across THREE fed-sx peers (blog / events / shop), each a
lib/host instance with SX_DOMAIN-selected types + behaviors.

- EVENTS peer: a 'calendar' type (on-allocate → real allocated relation, via the driver now
  PERFORMING relate effects) + an 'event' type. /calendars UI: allocated posts, scheduled events
  (each with its featured post + a Buy-ticket button + sold count), and a Schedule-an-event form.
  host/blog-new-event schedules an event on main, optionally featuring an allocated post.
- SHOP peer: an 'order' type. POST /order?event= creates an order (is-a order, related to the event)
  → 'order:<id>'. Replaces the Python shop service.
- BUY: events POSTs a cross-domain order to shop (host/blog--http-order), then links event--sold-->
  order. host/blog--out-raw reads cross-domain edges (host/blog-out filters to local slugs, which
  would drop federated refs — the bug that hid allocated posts + sold counts).
- BLOG: every post page shows an 'Allocate to a calendar' form.
- serve.sh: SX_DOMAIN gates blog/events/shop seeds; SX_EVENTS_BASE / SX_SHOP_BASE wire the chain.
  docker-compose: sx_events + sx_shop peers (own stores, shared fed secret, externalnet-ready).

LIVE, all via the browser: allocate 'welcome' on blog → events /calendars shows it → schedule
'Summer Gig' featuring welcome → Buy ticket → shop order → tickets sold increments. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:14:51 +00:00
4a0d53ac43 cross-domain UI: allocate operable from the web + real relations + /calendars view
Makes slice 1 clickable and real. The effect DRIVER now PERFORMS action-effects (closing the loop):
a 'relate' effect {:args (src kind dst)} mutates the relation graph. The events calendar behavior's
allocate-link DAG emits (effect relate (field target) 'allocated' (field slug)), so an allocated post
becomes a real main--allocated-->post edge on events (not just a log line).

UI: every blog post page shows an 'Allocate to a calendar' form (host/blog--allocate-form, shown when
a peer is configured) → POST /:slug/allocate reads the form field. events gets a /calendars page
listing posts allocated to 'main'. serve.sh seeds a 'main' calendar (is-a calendar) on events.

LIVE: submit the allocate form on blog.rose-ash.com/welcome → events /calendars shows 'welcome'
allocated to main, a real federated relation. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:56:06 +00:00
355bcbefdc 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>
2026-07-02 20:25:37 +00:00
43c085e8e8 federation production layer: actor model + follower graph + delivery timer + signatures (LIVE)
The full fed-sx production layer, live-verified across A (blog.rose-ash.com) and B (sx_host_b).

ACTOR MODEL + FOLLOWER GRAPH: activities carry a real :actor (SX_ACTOR); delivery targets FOLLOWERS,
not a static peer list. A peer subscribes by POSTing {verb:follow, actor, base} to /inbox
(host/blog--add-follower!); B follows A at boot (SX_FOLLOW) so A delivers to B. host/blog--{actor,
self-base, followers, follow!, delivery-bases} + durable followers store.

BACKGROUND DELIVERY TIMER: serve.sh's detached _fed_delivery_loop hits GET /fed-tick every 15s
(over /dev/tcp) → re-follow (idempotent, recovers a target that was down at boot) + flush the durable
outbox. Federation is eventually-consistent, not best-effort-at-emit.

SIGNATURE VERIFICATION: every federated POST is signed (host/blog--fed-sign = dr/sess-sig shared-secret
MAC over the body, SX_FED_SECRET); /inbox rejects a bad/missing signature with 403 (empty secret =
open). Applies to both follows and activity delivery.

PUBLIC DOMAIN: B joins externalnet so Caddy CAN reverse_proxy a subdomain to it — the DNS + Caddy
route itself is external ops config (no local Caddyfile).

LIVE PROOF: B follows A (followers:1); publish on A → SIGNED delivery to follower B → B verifies +
fires validate+notify; a forged POST (bad x-fed-sig) → 403; B down → publish queues → the background
timer auto-delivers the backlog when B returns (no manual flush). blog 218/218, full conformance green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:03:35 +00:00
afb9ce5e90 TA-live: real A→B federation over HTTP + a durable outbox (LIVE-VERIFIED)
Step 3 — federation, live-verified with TWO real host instances.

- host/ta.sx: host/ta--post/make-http-wire/federate (POST a serialized activity to a peer's /inbox
  over real HTTP). host/blog.sx: POST /inbox (host/blog-inbox → receive! → process locally, does NOT
  re-federate — no loops).
- DURABLE OUTBOX (fed-sx reliability, after the user asked 'if B is down does it still work?'):
  emit! processes locally (always succeeds), QUEUES per-peer to a persisted outbox, delivers
  best-effort. A peer being DOWN no longer fails the publish — delivery is GUARDED (SX guard catches
  the http-request connection error), failed items stay queued and retry on next emit / on boot /
  manual /flows?flush=1. /flows shows the outbox depth.
- serve.sh: SX_PEERS → peers; boot load+flush of the outbox. docker-compose: a 2nd host sx_host_b
  (peer B, own store, no peers).

LIVE PROOF: (1) a peer POSTs create/article to blog.rose-ash.com/inbox → A fires validate+notify.
(2) publish on A → federates to B → B fires ITS behaviors on A's activity (B's /flows + /activities).
(3) RESILIENCE: publish with B DOWN → A returns 303 (was 500) + queues; start B + flush → B receives
the backlog + fires. blog 218/218 (+TA receive test), full host conformance green.

A = blog.rose-ash.com (public/Caddy); B = sx_host_b (internal docker DNS only, no public domain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:47:07 +00:00
cb0d866002 RA-live: durable business logic in production — host drives the kernel service (LIVE)
Steps 1+2 of RA-live/TA-live, live-verified end-to-end on blog.rose-ash.com.

(1) DEPLOY: docker-compose.dev-sx-host.yml gains an sx_kernel service running next/kernel/serve.sh
(the durable-execution kernel), SX_HTTP_HOST=0.0.0.0 so the host container reaches it at
http://sx_kernel:8930.

(2) HOST AS CLIENT: lib/host/ra.sx gains a KERNEL runner — host/ra--make-kernel-runner drives the
kernel over HTTP (http-request, native primitive; returns {status headers body}). It advertises
{effect,branch,each,suspend}, so select-runner routes a durable DAG to it. host/blog.sx: the DAG
registry + runner fleet are now mutable (register-dag!/add-runner!); emit! records SUSPENSIONS in a
durable pending log; /flows shows suspended instances with a resume link (?resume=<id>) driving
host/ra--kernel-resume. serve.sh wires it: set kernel-base, add the kernel runner, register the
durable 'blog-digest' DAG, declare a DURABLE behavior on article (create→publish SYNC, update→
blog-digest DURABLE), add a 'category' field.

LIVE PROOF: editing a published newsletter article → Update → routes to the kernel runner → POST
/flow/start/newsletter → kernel SUSPENDS (instance 5, shown pending on /flows) → /flows?resume=5 →
host re-drives the kernel → DONE → digest-sent effect + pending cleared. Durable suspend/resume
across separate HTTP requests, on a deployed persistent kernel. urgent edits complete immediately
(digest). http-request works in the serving context. blog 217/217, full conformance green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:31:33 +00:00
c0d9cb3cf4 plan: record the kernel service build (RA-live substrate)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:12:06 +00:00
22e2835bdb next: the real durable-execution KERNEL SERVICE (host_kernel) — RA-live substrate
Promotes the persistent-kernel spike into a real service. next/kernel/host_kernel.erl: boots
flow_store, registers named behavior flows (blog_digest), then blocks in http:listen so the
er-scheduler + gen_server stay alive across requests. Parameterised flow routes (paths matched by
byte prefix — binary =:= is buggy): GET /flow/start/<category> starts the flow with that category and
returns '<InstanceId>:<status>' (suspended|done); GET /flow/resume/<id> resumes that instance. Path
plumbing (starts_with / last_seg / field) is byte-level for portability.

next/kernel/serve.sh: the persistent service launcher (container entrypoint / local) — loads the
runtime + next/flow + the kernel, then host_kernel:start(); sleep infinity holds stdin so
the listener serves forever. next/tests/host_kernel.sh: drives it over HTTP — 4/4: newsletter →
instance 1 SUSPENDED, urgent → 2 DONE, draft → 3 DONE (skipped), resume 1 in a SEPARATE request →
DONE (durable state persists across requests). serve.sh launcher verified live (bind + start +
resume).

This is the RA-live substrate: a working durable-execution service the host drives over HTTP.
Remaining for RA-live: deploy it (a container/placement), point host/ra.sx's real-eval at it (POST
/flow instead of in-process erlang-eval-ast), route a durable binding to RA. TA-live adds inbox/
outbox routes on the same kernel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:11:47 +00:00
836d32474f spike: PERSISTENT next/ kernel is viable — unblocks RA-live + TA-live
The shared prerequisite for both live steps was: does a next/ kernel process hold gen_server state
(flow_store) across HTTP requests? Confirmed yes. plans/ra_kernel.erl is a minimal kernel
(flow_store + register the publish-digest flow, then a blocking http:listen that keeps the
er-scheduler + gen_server alive); plans/ra-kernel-spike.sh boots it as a background sx_server and
drives it with two SEPARATE curls: GET /start suspends instance 1, GET /resume resumes that SAME
live instance → done. So durable suspend→resume across requests works on a persistent kernel.

Design decision (per the discussion): chose the persistent-kernel path (B) over host-side replay-log
(A). B serves BOTH durability (RA) and federation (TA) on one fed-sx-native substrate and exposes the
full next/ kernel (projections, outbox, actor model); A only solves flow durability and mixes Erlang
into the host process. The er-scheduler-context bug (which kills an in-process kernel, option C) does
NOT bite a separate-process kernel — er-bif-http-listen spawns each handler in-scheduler, so
gen_server:call completes. Gotchas recorded: a blocking listener hangs any in-process
erlang-eval-ast (the kernel must be a dedicated TCP-driven process), and binary =:= is buggy (always
true) so routes must pattern-match paths as byte-list binaries.

RA-live + TA-live are now BUILD work (a real kernel service + the host as HTTP client + the actor
model), not research — the prerequisite is proven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:00:32 +00:00
39e5f906f2 host TA: the fed-sx transport adapter — federation loop proven at the seam
lib/host/ta.sx — a seam transport {:emit :deliver} over a DIRECTIONAL wire (out = outbox→followers,
in = inbox←follows). The transport is the SERIALIZATION boundary: activities cross the wire as
SX-source strings (host/ta--serialize/deserialize map the keyword-keyed activity ↔ a flat
string-keyed wire form of the P2 activity fields). host/ta--make-transport(out-wire, in-wire) +
host/ta--make-mem-wire (an in-memory directional queue for tests).

Proven (ta 5/5): content + relation activities round-trip through the wire; the FEDERATION LOOP —
instance A emits an activity → the wire carries it → instance B's behavior/pump delivers + processes
it → B's engine fires ITS behavior on A's activity; DIRECTIONAL (B re-emits to its own outbox, not
back into the inbox — no loop). 'Everything works over fed-sx', proven at the seam.

TA-live (deferred, same shape as RA-live): swap the mem-wire for the real next/ delivery wire —
needs a PERSISTENT next/ kernel (gen_servers don't survive across erlang-eval-ast calls) + the ACTOR
MODEL (peer_actors/follower_graph decide who the out-wire delivers to) + pushing /activities onto it.

Full host conformance green (+ta 5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:48:30 +00:00
d13c4dd5fe host P2: all state changes emit canonical activities (LIVE-VERIFIED)
Generalizes emission beyond publish to the full event source. TWO ActivityPub-faithful classes:
- CONTENT (host/blog--content-activity): Create on first publish, Update on a subsequent published
  edit. object-type is DERIVED from the post's is-a (host/blog--post-type), not hardcoded 'article'.
- RELATION (host/blog--relation-activity): Add/Remove, carrying :relation + :target (the edge).

host/blog--emit! runs any activity through behavior/process (logged + matched). emit-content-change!
(create/update) wired into form-submit + edit-submit; emit-relation! (add/remove) wired into
relate-submit + unrelate-submit.

DEBT #1 FIXED — per-EVENT :id (not the bare CID): content = create:/update:+cid; relation =
add:/remove:+src:kind:dst (EDGE-based, because a relation change doesn't shift the CID, so a
CID-based id would false-dedup different edges on one object).

The activity log is now the DURABLE EVENT SOURCE (string-keyed records under 'activitylog',
boot-loaded), surfaced at /activities — what TA will push to peers.

LIVE PROOF (blog.rose-ash.com): publish → /activities 'create article <cid>'; relate → 'add article
p2-events — add welcome related'; unrelate → 'remove …'. blog 217/217 (+4 P2, reframed P0.3 fire
tests for Update semantics), full host conformance 614/614.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:43:57 +00:00
9d29295820 host P1: types DECLARE behavior, runner DERIVED (LIVE-VERIFIED)
Generalizes the hardcoded publish trigger into declared, capability-routed behavior.

- Types carry :behavior — flat string-keyed bindings {"verb" "type" "dag"} on the type-post
  (persist-safe, like :type-relations). The "article" type declares on-create → the "publish" DAG.
- host/blog--load-behaviors! gathers ALL posts' declarations into a registry at boot (serve.sh); the
  trigger match (host/blog--triggers :match = host/blog--match-behaviors) consults it. Hardcoded
  create+article trigger removed.
- Runner DERIVED (DEBT #2 fixed): match resolves :dag via host/blog--dag-registry and picks the
  runner via host/flow--select-runner over host/blog--runner-fleet ([exec-runner]; RA joins at
  RA-live). Each binding carries its :runner; behavior/-run-binding now uses the binding's runner
  (else the engine default) — so the capability model drives the LIVE engine.
- The type-def view shows each behavior + its derived runner (host/blog--behavior-lines).

LIVE PROOF: /article shows 'on create → publish DAG · needs {effect, branch} · runner: synchronous
(exec-fold)'; publishing on blog.rose-ash.com fired /flows validate+notify via the DECLARED path.
blog 213/213 (+3 P1), full host conformance 610/610. FINDING: load-behaviors! scans all posts, not
is-type?-filtered (article failed is-type? on the durable store though it passed in-memory).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:34:39 +00:00
c21be815f3 host RA: the Erlang durable runner adapter — built + tested (module + integration)
lib/host/ra.sx — a PURE-SX seam runner (advertises {effect,branch,each,suspend}) with an INJECTED
erl-eval (real = er-to-sx-deep ∘ erlang-eval-ast; mock in unit tests), so it loads in the plain host
(Erlang refs resolve lazily inside lambdas) and is unit-testable without the Erlang runtime.
host/ra--{atom,bin,erl-src,start-expr,resume-expr,parse,make-runner,resume,real-eval}: marshals our
canonical activity → Erlang source (CID as <<"…">> binary, atoms single-quoted), starts a named
next/ flow via flow_store, parses (ok Id (flow_done V))→{:status done :effects V :flow-id} /
(ok Id (flow_suspended T))→{:status suspended :resume {:id :tag}}.

DUAL-RUNNER ROUTING (flows.sx): host/flow--required-caps now handles a {:erl-flow :needs} DAG
(declared caps, since a foreign flow can't be introspected); host/flow--select-runner picks the
cheapest runner whose capabilities cover the DAG's needs. The capability model is now REAL with two
runners — an {effect,branch} composition lands on exec-runner; a {suspend} DAG routes to RA.

Verified: ra 9/9 (mock erl-eval) + plans/ra-integration.sh 4/4 (the REAL module driving live
flow_store: urgent→done, newsletter→suspended with a resume handle, digest_sent effect-as-data).
Full host conformance 607/607; next/tests/triggers_e2e.sh 10/10 baseline intact.

FINDING → RA-LIVE deferred: gen_servers don't persist across separate erlang-eval-ast calls (flow
README), so true cross-call suspend/resume needs a PERSISTENT next/ kernel process. The runner +
marshalling + suspend/resume mechanics are proven; RA-live is process lifecycle + wiring, documented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:20:36 +00:00
17602e597f RA spike — the Erlang durable runner is VIABLE (4/4)
Narrow spike (plans/ra-spike.sh) de-risking the durable/federated half before building P1/P2. From
the SX side it proves the whole RA path: (1) our canonical activity dict serializes to a valid
Erlang activity-proplist source (the marshaller shape = host/blog--activity->erl); (2) it drives
pipeline:apply_triggers → blog_publish_digest → done + 3 emails (urgent sync branch); (3) the
newsletter activity SUSPENDS on the morning timer (status =:= {ok,{suspended,morning}}); (4)
flow_store:resume completes it → 3 emails (the async cycle closes); (5) NO er-scheduler deadlock —
flow-on-erlang's railway threading holds when driven from SX.

Findings recorded in the plan for the full build: erlang-eval-ast returns Erlang TERMS directly
(integers raw, atoms as {:tag atom :name …}) so the runner must parse results (not assume :name);
flow_store start→{done,V}|{suspended,Tag} + resume(Id,Res) maps 1:1 onto the seam's runner contract
{:status done|suspended :effects :resume}; the instance Id is the resume handle. Remaining for full
RA: load the Erlang runtime into the serving process (or out-of-process), the async dispatch
boundary (DEBT #3), CID→binary marshalling, structured result parsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:33:48 +00:00
a5d43246e0 host: P0 review — fix edit-submit ordering bug + record carried-forward debt
REVIEW at the P0-complete milestone found one live bug and several forward prerequisites.

FIX (was live): edit-submit ran maybe-publish! BEFORE set-field-values!, so an edit that set a
category and published in one submit fired the publish activity on the STALE category (wrong branch).
Reordered — fields land before the transition fires. Regression test added (fields-first →
newsletter→digest, not stale→notify). blog 210/210.

Recorded carried-forward debt in the plan: activity identity (DEBT #1, blocks P2 — :id=CID false-
dedups relation events), capability bind not wired into the live engine (DEBT #2, P1), synchronous-
in-request dispatch (DEBT #3, RA needs the async boundary + background pump), the 'urgent' default
smell (DEBT #4). Sequencing note: P1's runner-derivation is vacuous until RA adds a 2nd runner, and
RA is the load-bearing risk — recommend a narrow RA spike next to de-risk the durable/federated half.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:17:48 +00:00
77e89a9965 host P0.4: canonical seam activity shape + RA marshaller (LIVE-VERIFIED) — P0 COMPLETE
host/blog--publish-activity now emits the CANONICAL seam shape {:verb :actor :object <cid>
:object-type :slug :category :delta :id}: :object is a content-addressed REFERENCE (the CID, not an
inlined dict), :id the dedup identity, :slug+:category the domain fields the DAG reads. Consumers
reconciled — the on-publish trigger matches :verb+:object-type; publish-ctx reads top-level
:category+:slug. Added host/blog--activity->erl: marshals the canonical activity → next/'s Erlang
proplist for the Erlang runner adapter (RA) — defined + tested, unused until RA so the reconcile is
complete and RA's bridge is ready. (:ts/:prev omitted — no clock primitive in the host; deferred.)

LIVE PROOF: published on blog.rose-ash.com → /flows fired validate+notify with the canonical
activity. blog 209/209, full host conformance 597/597.

P0 COMPLETE: the synchronous publish workflow runs end-to-end on the live host through the
substrate-agnostic seam, durably, in the canonical shape, with the RA marshaller staged. RA (Erlang
runner) + TA (fed-sx transport) plug in next without touching the DAG or the wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:07:58 +00:00
5b6a5e4f19 plan: fix P0.3b conformance count (596, not 599)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:01:25 +00:00
67d2fad8d8 host P0.3b: durable flow log — survives restart (LIVE-VERIFIED)
The driver now persists each effect record to the blog store (string-keyed to dodge the keyword/
persist top-level split), and host/blog-load-flowlog! rebuilds the in-memory log on boot (wired into
serve.sh after load-edges!). So /flows survives a restart — closing the P0.3 gap.

LIVE PROOF: published a post on blog.rose-ash.com → /flows showed validate+notify → RESTARTED the
container (in-memory log lost) → /flows STILL showed them, reloaded from the durable store.
Round-trip also covered by a conformance test (persist → clear → reload → identical). blog 208/208,
full host conformance 599/599. Note: whole-list rewrite per effect — fine at P0 volume, cap/rotate later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:01:10 +00:00
9ac6a8afd5 host P0.3: wire the seam into the live publish path (LIVE-VERIFIED)
Publishing a post now fires the on-publish behavior DAG through the seam. host/blog--{transport
(activity log), triggers (on-publish: create+article → publish-DAG), driver (records each effect in
the flow log), publish-engine (behavior/make-engine over the four adapters + the execute-fold runner
+ publish-ctx), fire-publish!, maybe-publish!}. Both write handlers (form-submit POST /new,
edit-submit POST /:slug/edit) detect the draft→published TRANSITION (fire-once) in the handler body
and run behavior/process. GET /flows renders the flow log (the effect-as-data the driver dispatched).

LIVE PROOF: logged in + POST /new on blog.rose-ash.com → /flows shows 'validate' + 'notify' (the
publish-DAG branched on the default urgent category), driven end-to-end by the real behavior engine.
Every piece is a seam adapter — swapping the runner for Erlang (RA) or the transport for fed-sx (TA)
federates this same wiring unchanged.

blog 207/207 (+4 P0.3), full host conformance 595/595. GAP: flow log is in-memory (P0.3b = persist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:56:00 +00:00
564fa7dd7d plan: don't calcify P0.2 — artdag may grow to contain business logic (phase AX)
Per the user: the execute-fold-vs-artdag split from P0.2 is a capability SNAPSHOT, not a permanent
boundary. artdag MAY grow +{effect,branch,each} node-kinds; business logic then migrates onto it to
inherit the DAG-engine superpowers — content-addressed memoization (recompute only on input-CID
change), optimize (fuse/dedup/dce), schedule, and above all FEDERATION (a flow result reused across
peers by content-id — the federation vision, for free). The capability model makes the migration
seamless (same DAGs + seam; the runner just advertises more). Named the real design work: dynamic
control in a static DAG (branch prunes a path); effect nodes non-cacheable vs pure nodes memoized.
Demand-driven (phase AX); execute-fold stays the lean default for cheap synchronous flows. Annotated
the P0.2 finding + flows.sx header so the finding doesn't harden into dogma.

Doc/comment-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:38:36 +00:00
e38a8381d4 host P0.2: publish-DAG + execute-fold runner + capability check (hypothesis confirmed)
The hypothesis test. FINDING: a synchronous business flow expresses NATURALLY as an EXECUTE-FOLD
composition (host/execute.sx: seq/effect/alt — the category branch IS 'alt'), NOT an artdag
DATAFLOW DAG (which has no control flow). So 'business logic = art-dag' holds at the ABSTRACTION
(both content-addressed op-DAGs) and is REFINED at the vocabulary: the synchronous control-flow
runner is the execute-fold (caps {effect,branch,each}); artdag is the dataflow sibling. Two
instances of one thing, run very differently — exactly the framing.

lib/host/flows.sx: capability typing (host/flow--node-cap/required-caps derive a DAG's capability
set from its node vocabulary; effect→effect, alt→branch, each→each, wait→suspend), the execute-fold
seam runner (advertises {effect,branch,each}), and host/flow--bind (required ⊆ advertised → derive
the runner, else fail-fast). host/blog--publish-dag (the publish workflow) + publish-ctx.

Verified: publish-DAG required-caps = {effect,branch} → binds to the sync runner; runs →
newsletter→[validate,digest] / urgent→[validate,notify] / other→[validate,skip]; a  node →
{suspend} → binds FAIL-FAST against the exec-runner (would need the Erlang runner, RA). Runner is
DERIVED, not chosen. flows 7/7, blog 203/203, full host conformance 591/591.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:18:08 +00:00
8c48cac46f plan: capability-typed nodes + capability-advertising runners (derived runner)
Folds in the sharpest refinement: business logic and art-dag are the SAME op-DAG structure,
differing only in the CAPABILITIES their nodes require — so the runner is DERIVED, not chosen.
A node declares :needs (wait→suspend, fan-out→parallel, heavy→offload); a runner advertises
:capabilities (op-table {effect,branch,each}; Erlang +suspend; celery-sx +parallel,retry,offload);
artdag/analyze computes a DAG's required set → its minimum runner; the binder checks required ⊆
runner-caps (fail fast). The sync/durable/distributed split falls out of the DAG (a {effect}-only
DAG runs with zero ceremony; a wait node auto-requires Erlang) — turning 'simple in SX / complex
in Erlang' from a judgment call into a derivable property. Removed the :runner hint from the type
binding; P0.2 gains the hypothesis test (natural-as-a-DAG? + flip-to-wait fails fast); runner
contract gains :capabilities; type-def editor can show the derived classification.

Doc-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:09:24 +00:00
689d4bd363 plan: whole-plan coherence review — align the middle with the artdag+seam reframe
The reframe updated the vision but left the P0 section stale + contradictory: P0.2/P0.3 still
described the Erlang-bridge-first path the reframe deferred; P0.1's activity ({:type :object-dict})
didn't match the seam's canonical activity ({:verb :object-cid}); the seam-contract section
predated the 2 enrichment passes (no status/dedup/pump). Coherence fixes:
- P0 rewritten around the seam + SX op-table runner (all-SX publish-DAG, local-SX registry,
  in-process transport, host driver) — no Erlang/fed-sx.
- Erlang/fed-sx DEMOTED to explicit adapter phases: RA (durable Erlang runner wrapping next/
  flow_dispatch) + TA (fed-sx transport wrapping next/ delivery). P3-federation folded into TA.
- canonical seam activity shape defined; P0.4 reconciles P0.1's next/-shaped activity + a marshaller.
- seam contract refreshed to behavior.sx (result {:status :effects :resume :error}, dedup
  per-invocation, pump/async-completion, behavior 10/10); stray fragments + 9/9→10/10 cleared.

Doc-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:00:02 +00:00
6ed523623b host: correct the seam's async-completion contract + prove it (2nd review)
Second review of the (core) seam caught a subtle one — and that my first 'fix' was itself wrong.
The async completion of a SUSPENDED durable flow happens AFTER the synchronous process call has
returned, so an :emit captured in the run env would be stale. The correct seam is construction-
wiring: a durable runner is wired to the transport's INBOUND channel at construction and injects
its completion activity there, out-of-band; a later behavior/pump drains it → effects flow. So the
engine code was already right (pump is the async re-entry seam); only the contract comment was
wrong — corrected. New test proves the loop: process(wait) suspends (no effect), then pump drains
the out-of-band completion → the flow's digest effect flows. Also clarified: dedup is per-
invocation (global idempotency = emitter fire-once + durable inbox); retry is flow-level; the
engine-facing runner result is {:status :effects :resume :error} (:results is runner-internal).

behavior 10/10 (+ async-completion). No engine change — comment + test only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:55:32 +00:00
e4fc66bfeb host: enrich the adapter seam to be substrate-agnostic (review fixes)
After review, the seam was only synchronous-complete; the durable/celery-sx runners couldn't
plug in cleanly. Additive fixes (pipeline unchanged): (1) :status branch in run-binding — 'done'
dispatches effects, 'suspended' records the flow + :resume (a durable runner holds it; completion
re-enters as a new activity via pump), 'failed' records + :error for retry/dead-letter. (2) richer
runner env — :ctx (per-activity, via engine :ctx-of) + injected :effects (external-read interfaces,
e.g. a deterministic fetch_followers). (3) dedup by content :id — a cycle is caught by identity,
not just the depth guard. (4) behavior/pump — drain transport.deliver for inbound (peer activities
+ async runner completions), sharing one trace so dedup spans the batch.

behavior 9/9 (+ suspended/failed/dedup/env/pump); full host conformance 580/580.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:50:41 +00:00
5d04da748a host: the adapter seam for business-logic-as-composition (design-first)
lib/host/behavior.sx — the substrate-independent seam every runner/transport/registry/driver
plugs into. An engine bundles four dict-of-functions adapters (trigger-registry, runner,
transport, driver); behavior/process folds an ACTIVITY through the pipeline: emit → match
triggers → run each behavior DAG → dispatch each effect-as-data → recurse on new activities
(loop closure, depth-guarded at 8). Every stage injected, so the same DAG + engine run over the
synchronous op-table runner / Erlang durable / celery-sx / fed-sx transport unchanged.

Reference tests (mock adapters) prove the contract: publish→trigger→runner→effect flows; a
non-matching activity fires nothing (log complete, execution precise); an effect that emits a new
activity re-triggers (loop closes); an unbounded loop is depth-guarded (terminates). Wired into
conformance.sh + serve.sh MODULES. behavior 4/4; full host conformance 575/575.

Next: P0 supplies the REAL adapters (publish activity ← host/blog--publish-activity, local-SX
trigger, sync op-table runner over a publish-DAG, host driver) — same engine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:42:04 +00:00
f240c46fa8 plan: account for celery-sx as the distributed/durable runner adapter
celery-sx = one more runner on artdag/op-table-runner, not a Celery port: broker=persist KV,
workers=er-scheduler, result backend=content-addressed (dedup free), retries/replay=flow-on-
erlang, fan-out=artdag/schedule. ~few hundred lines of glue, zero packages, 'Celery the way it
should have been' on erlang-on-sx. DEMAND-DRIVEN (RX) — build when a DAG needs heavy compute /
long-running-retryable / cross-machine fan-out; the synchronous op-table runner covers P0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:35:44 +00:00
c147020059 plan: elevate — business logic IS art-dag; substrates are adapters
Reframe after the user's insight, confirmed in code: artdag-on-sx already IS the substrate-
independent behavior engine — artdag/run injects the RUNNER (execution adapter: SX op-table /
Erlang / Celery), federation.sx injects the TRANSPORT (communication adapter: fed-sx / HTTP /
IPFS). Business logic = a content-addressed DAG; durability is a RUNNER capability (same DAG runs
eager or durable); deployment (subdomain service / peer / L1 worker) is placement. fed-sx+Erlang
is ONE adapter set, not the architecture. The type carries content-grammar + allowed-relations +
a behavior DAG. The prior fed-sx/Erlang framing is kept as one concrete first slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:31:10 +00:00
5b46a18c61 plan: review corrections to business-logic-fed-flows
Three framing fixes after review: (1) the event source is object-level state changes, NOT just
CID deltas — relations write edge:* rows so they don't shift the CID; content/status → Create/
Update, relations → Add/Remove (ActivityPub-faithful). (2) verbs are TRANSITIONS (on-publish =
draft→published, fire-once, not every delta of a published post). (3) the hybrid flow split is by
DURABILITY not complexity — the execute-fold is eager/synchronous (no wait); suspend/timer/human
flows are the Erlang escape hatch. Plus: effects-as-data need a DRIVER (host, for P0); P0.2 must
gate on the transition + run in the handler body (VmSuspended/er-scheduler risk); P0.3 gets an
acceptance criterion; P3 flags the fed-sx delivery M2 blocker + the deferred actor model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:02:59 +00:00
3675d059b5 host P0.1: publish-activity contract for federated composition-flows
Business logic as federated composition-flows (plans/business-logic-fed-flows.md). P0.1: the
host describes a published post as a fed-sx activity — host/blog--publish-activity(slug) →
{:type "create" :actor "site" :id <CID> :object {:type "article" :slug :category}} — the
exact shape next/'s trigger machinery consumes (verified: next/tests/triggers_e2e.sh 10/10).
category (drives the flow branch: newsletter suspends / urgent fires / else skip) comes from
the "category" field-value, else the first tag, else "urgent". + host/blog--post-category.

Design decided: activity log = every CID delta (event source); triggers = declared subscriptions
(DefineTrigger); flows hybrid (SX composition for simple via the execute-fold, named Erlang flows
for complex); federated execution = Erlang (next/); the type carries content+relations+behavior.

blog 200/200 (+3: contract, category fallback, missing-post nil).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:55:08 +00:00