diff --git a/lib/host/blog.sx b/lib/host/blog.sx index 761380a4..d5540609 100644 --- a/lib/host/blog.sx +++ b/lib/host/blog.sx @@ -2659,14 +2659,16 @@ (if (= (len issues) 0) (begin (host/blog-put! slug title sx-content status) - ;; P0.3: a draft→published transition fires the publish flow through the seam. - (host/blog--maybe-publish! slug (get r :status) status) - ;; store the typed field values from the generic, type-driven form (Slice 8b) + ;; store the typed field values FIRST — the publish activity reads :category from + ;; them, so field-writes must land before the transition fires (else it branches on + ;; the stale category on an edit that both sets a category and publishes). (host/blog--set-field-values! slug (reduce (fn (acc f) (assoc acc (get f :name) (or (host/field req (str "field-" (get f :name))) ""))) {} post-fields)) + ;; P0.3: a draft→published transition fires the publish flow through the seam. + (host/blog--maybe-publish! slug (get r :status) status) (dream-redirect (str "/" slug "/"))) (let ((issue-items (map (fn (i) (quasiquote (li (unquote i)))) issues))) (host/blog--resp req 400 diff --git a/lib/host/tests/blog.sx b/lib/host/tests/blog.sx index 21cfd980..dff3b823 100644 --- a/lib/host/tests/blog.sx +++ b/lib/host/tests/blog.sx @@ -1191,6 +1191,17 @@ (list (get e :type) (get (get e :object) :type) (get (get e :object) :slug) (get (get e :object) :category)))) (list "create" "article" "pub4" "newsletter")) +;; P0 review regression: field-writes must PRECEDE the transition, else the publish activity +;; branches on the stale category. (edit-submit now sets fields before maybe-publish!.) +(host-bl-test "publish reads the FRESH category (fields set before the transition fires)" + (begin + (set! host/blog--flow-log (list)) + (persist/backend-kv-put host/blog-store host/blog--flowlog-key (list)) + (host/blog-put! "p04r" "R" "(article (h1 \"r\"))" "draft") ;; a draft, no category yet + (host/blog--set-field-values! "p04r" {"category" "newsletter"}) ;; fields FIRST (the fix order) + (host/blog--maybe-publish! "p04r" "draft" "published") ;; then the transition + (map (fn (e) (get e "verb")) host/blog--flow-log)) + (list "validate" "digest")) ;; newsletter→digest, not stale→notify ;; P0.2: the publish WORKFLOW as an execute-fold DAG — branches on category, needs {effect,branch}, ;; binds to the synchronous execute-fold runner (derived, not chosen). (host-bl-test "publish-DAG: category branch (newsletter→digest) via the execute-fold" diff --git a/plans/business-logic-fed-flows.md b/plans/business-logic-fed-flows.md index 02ba5c74..9fdf385b 100644 --- a/plans/business-logic-fed-flows.md +++ b/plans/business-logic-fed-flows.md @@ -170,7 +170,35 @@ substrate-agnostic seam, durably, in the canonical activity shape, with the Erla staged. Every piece is a swappable adapter: RA (Erlang runner) + TA (fed-sx transport) plug in next without touching the DAG or the wiring. +### P0 REVIEW (2026-07-02) — findings + carried-forward debt +- **FIXED (was a live bug):** edit-submit ran maybe-publish! BEFORE set-field-values!, so an edit + that set a category AND published branched on the STALE category. Reordered (fields first); + regression test added. blog 210/210. +- **DEBT #1 (blocks P2): activity identity.** Dedup uses :id = the object CID. Relation Add/Remove + events don't change the CID → multiple relation activities would share :id → false dedup. P2 needs + a real activity id (verb+object+seq/edge), NOT the bare CID. +- **DEBT #2 (P1): the capability bind isn't wired into the live engine.** host/blog--publish-engine + calls exec-runner directly; host/flow--bind is tested but decorative in the live path. P1's engine + must DERIVE the runner via bind (required ⊆ advertised), not hardcode it. +- **DEBT #3 (RA): flow execution is synchronous in the request path.** Fine for 2 cheap effects, but + a durable/suspend runner CANNOT block a request — RA needs dispatch moved OFF the request path + (emit in-request → a background loop calls behavior/pump). The seam supports it (pump); the wiring + doesn't exist. This is structural, not just the marshaller. +- **DEBT #4 (minor): the "urgent" category default** notifies everyone for an uncategorised post — + demo-convenient, semantically wrong. Default to skip/none once the demo value is past. +- Acknowledged/known: unbounded flow log (cap/rotate), registry :register! is a stub (P1 makes it + real), :actor "site" placeholder (TA needs the actor model), marshaller unverified-until-RA. + +### Sequencing note (hidden prerequisites) +- **P1's "runner derived via caps" is near-vacuous until a SECOND runner exists** — with only + exec-runner, everything derives to it. The derivation only MEANS something once RA lands. +- **RA is where the real risk lives** (SX→erlang-on-sx dispatch + suspend→resume→pump + the + er-scheduler context bug) AND P1/P2 quietly depend on what RA forces (a 2nd runner, the async + boundary). RECOMMENDATION: a narrow **RA SPIKE** next — prove one dispatch + one suspend/resume/ + pump cycle in isolation — de-risks the whole durable/federated half before building P1/P2 on it. + ## P1 — types DECLARE behavior (generalize) + - [ ] The type carries :behavior = [{:on {:verb :object-type :guard} :dag }] — edited in the type-def editor beside grammar + relations. NO runner hint (the runner is DERIVED from the DAG's required capabilities). @@ -181,10 +209,12 @@ without touching the DAG or the wiring. to (the derived sync/durable/distributed classification, visible not hand-set). ## P2 — state-change → activity emission (ALL events, not just publish) + - [ ] Wire the host write path: put!/set-comp!/edit-submit → Create/Update; relate!/unrelate!/tag → Add/Remove. Emit canonical activities into the transport log. Define the delta summary. ## RA — the ERLANG (durable) RUNNER adapter ← the old "fed-sx spike", now an adapter + - [ ] A seam runner wrapping next/'s flow_dispatch + flow_store: marshal the canonical activity → next/'s proplist, dispatch to a named flow (blog_publish_digest), map the result back to {:status done|suspended :effects :resume}. On suspend, wire the resumed completion → the transport