diff --git a/plans/business-logic-fed-flows.md b/plans/business-logic-fed-flows.md index 2f14e85f..dcc5babc 100644 --- a/plans/business-logic-fed-flows.md +++ b/plans/business-logic-fed-flows.md @@ -4,15 +4,29 @@ alongside content grammar + allowed relations. State changes federate over fed-sx; federated services execute the business logic as durable **flows**. -**Design (decided 2026-07-02):** -- **Activity log = every CID delta** — the federated event source of truth. Any content change - (new CID, per host/blog--cid-of: content in the record → CID; relations are external → not). +**Design (decided 2026-07-02; corrected after review):** +- **Activity log = every OBSERVABLE object-level state change** — the federated event source. + NOT just CID deltas: verified that relations write `edge:*` rows, NOT the record, so a relation + change does NOT shift the CID. So the log has TWO event classes (ActivityPub-faithful): + content/status change → a CID-carrying `Create`/`Update` (the record's canon incl. :status → the + CID); relation change (relate/unrelate/tag) → an `Add`/`Remove` activity referencing the edge. + (CID delta is one class, not the whole log — this is the fix to the original "every CID delta".) +- **Verbs are TRANSITIONS, not raw deltas.** `on-publish` = the draft→published transition + (fire-once), not every CID delta of a published post. The emitter picks the verb: `Create` on + first publish, `Update` on subsequent content edits, `Add`/`Remove` on relations, `Delete` on + unpublish/delete. Triggers are scoped to the transition, so re-editing doesn't re-fire on-publish. - **Triggers = declared subscriptions** — a type declares named triggers (on-publish, on-relate, …); flows fire only on matching ones. (fed-sx `DefineTrigger`: activity-type → flow-name + guard + actor-scope.) Log complete, execution precise. -- **Flows = hybrid** — simple logic authored as **SX composition** (the execute-fold: effect/alt/ - each — already live in /workflow-demo); an escape hatch to named **Erlang flows** - (next/flow/*.erl, effect-as-data, deterministic replay, suspend/resume) for the complex. +- **Flows = hybrid, split by DURABILITY not "complexity":** SYNCHRONOUS declarative logic authored + as **SX composition** (the execute-fold: effect/alt/each — eager, one-pass, NO suspend; live in + /workflow-demo). Anything needing a timer / suspend-resume / human-in-the-loop is a named + **Erlang flow** (next/flow/*.erl — flow_spec:sequence/branch/wait, effect-as-data, deterministic + replay). The execute-fold canNOT express `wait`; that's the escape-hatch boundary. +- **Effects are DATA; a DRIVER dispatches them.** Flows return effect descriptions (digest_sent, + a DigestSent activity) — they perform no IO (a blocking call deadlocks the er-scheduler). For P0 + the HOST is the driver (dispatch the effect → a durable record / append the follow-up activity / + show it). The driver closing the loop back to object state is P4. - **Federated execution = Erlang** (`next/` fed-sx Milestone-1 kernel: trigger_registry + flow_dispatch + pipeline post-append fan-out). Authoring stays SX; the fed-sx activity is the bridge. flow-on-sx (Scheme, lib/flow) remains for purely-local durable logic. @@ -30,20 +44,28 @@ Prove: live host publishes a post → fed-sx activity → on-publish trigger → a post record → the fed-sx activity {:type "create" :actor "site" :id :object {:type "article" :category … :slug …}}. category from field-value "category", else first tag, else "urgent". + host/blog--post-category. blog 200/200 (3 tests). DONE 2026-07-02. -- [ ] **P0.2 — the dispatch bridge.** On publish (host/blog-edit-submit status→published, or a - dedicated publish action), emit the activity into the trigger machinery. RUNTIME CHOICE for P0: - in-process — serve.sh loads next/ kernel + registers the on-publish trigger; host emits via the - erlang-on-sx bridge (erlang-eval-ast pipeline:apply_triggers). (P3 replaces this with real fed-sx - delivery over next/kernel/http_server.erl.) -- [ ] **P0.3 — the effect is visible.** The flow's digest_sent effect surfaces (a durable record / - a DigestSent activity back in the log / shown on the post or a /flows page). Close the local loop. +- [ ] **P0.2 — the dispatch bridge.** Detect the draft→published TRANSITION (edit-submit /a publish + action where prev status ≠ "published" and new = "published") — fire-once, not on every edit — + and emit host/blog--publish-activity into the trigger machinery. RUNTIME CHOICE for P0: in-process + — serve.sh loads next/ kernel + registers the on-publish trigger; host emits via the erlang-on-sx + bridge (erlang-eval-ast pipeline:apply_triggers). RISK: run this in the handler BODY (never a + render/quasiquote — VmSuspended), and the flow must stay effect-as-data (no blocking, or the + er-scheduler deadlocks). SPIKE the SX→erlang-on-sx call in isolation first. (P3 swaps this for + real fed-sx delivery over next/kernel/http_server.erl.) +- [ ] **P0.3 — the effect is dispatched (host = driver).** pipeline:apply_triggers returns the + flow's effect-as-data ({digest_sent, Emails, DigestObject}); the HOST driver dispatches it — + P0: append a DigestSent record / activity + show it on a /flows page (or on the post). ACCEPTANCE: + publish a post on the LIVE host → the DigestSent surfaces, driven by the real flow. (Marshalling: + the SX activity dict ↔ the erlang proplist term is the fiddly part — factor host/blog--activity->erl.) ## P1 — types declare behavior (generalize) - [ ] A Composition field / the type carries a :triggers list (on-publish → flow-name + guard) — edited in the type-def editor, like grammar + relations. - [ ] A fold turns a type's declared behavior into DefineTrigger + flow registrations at boot. -- [ ] Simple flows authored as SX composition (execute-fold) → compiled/dispatched to the engine; - complex ones reference named Erlang flows. +- [ ] SYNCHRONOUS flows authored as SX composition (execute-fold: effect/alt/each — one-pass, no + suspend) → dispatched to the engine; anything needing wait/suspend/human-gate references a named + Erlang flow. (Optional bigger: extend the SX flow vocabulary with a `wait` that compiles to + flow_spec — only if authoring durable flows in SX proves worth it.) ## P2 — state-change → activity emission (the CID-delta event source) - [ ] Every content mutation (new CID) appends a state-change activity to the log. Define the @@ -53,6 +75,11 @@ Prove: live host publishes a post → fed-sx activity → on-publish trigger → ## P3 — federation proper - [ ] Activities cross peers via next/ delivery (http_server / outbox / follower_graph). A remote service's trigger_registry fires the flow. Everything works over fed-sx. + RISK: next/ delivery had M2 blockers (http-listen handler runs off the er-scheduler context → + gen_server:call can't complete; project_fed_prims_http_listen_scheduler). Confirm delivery is + green before depending on it. Also: the ACTOR MODEL (:actor "site" is a P0 placeholder) is + foundational here — peer_actors / follower_graph / per-author identity underpin who federates to + whom. Deferred through P0–P2, but P3 needs it real. ## P4 — close the loop - [ ] Flow effects mutate objects back durably (a flow's DescribeEffects → host writes / new