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>
This commit is contained in:
@@ -128,6 +128,22 @@
|
||||
:id (host/blog-cid slug) ;; the object's content CID
|
||||
:object {:type "article" :slug slug
|
||||
:category (host/blog--post-category slug)}}))))
|
||||
;; P0.2: the publish WORKFLOW as an EXECUTE-FOLD composition (host/execute.sx) — the SYNCHRONOUS
|
||||
;; business flow. Validate, then BRANCH on category (newsletter → build a digest, urgent → notify
|
||||
;; now, else skip). Content flow (effect/alt), NOT dataflow — so it's the execute-fold, not artdag.
|
||||
;; Its required capabilities are {effect, branch} (host/flow--required-caps) → binds to the sync
|
||||
;; execute-fold runner (which advertises {effect, branch, each}). A `wait` node would add {suspend}
|
||||
;; and fail-fast against that runner (requiring the Erlang runner, RA). Runs against a ctx built
|
||||
;; from the activity's object.
|
||||
(define host/blog--publish-dag
|
||||
(quote (seq
|
||||
(effect validate (field "slug"))
|
||||
(alt (when (eq "category" "newsletter") (effect digest (field "slug")))
|
||||
(when (eq "category" "urgent") (effect notify (field "slug")))
|
||||
(else (effect skip))))))
|
||||
;; the ctx a publish activity presents to the publish-DAG (string keys — preds read ctx by key).
|
||||
(define host/blog--publish-ctx
|
||||
(fn (activity) (let ((o (get activity :object))) {"category" (get o :category) "slug" (get o :slug)})))
|
||||
|
||||
;; ── render ──────────────────────────────────────────────────────────
|
||||
;; A post's sx_content is SX element markup -> HTML via render-page (which supplies
|
||||
|
||||
@@ -95,6 +95,7 @@ MODULES=(
|
||||
"lib/host/compose.sx"
|
||||
"lib/host/execute.sx"
|
||||
"lib/host/behavior.sx"
|
||||
"lib/host/flows.sx"
|
||||
"lib/host/htmlsx.sx"
|
||||
"lib/host/blog.sx"
|
||||
"lib/host/page.sx"
|
||||
@@ -114,6 +115,7 @@ SUITES=(
|
||||
"blog host-bl-tests-run! lib/host/tests/blog.sx"
|
||||
"htmlsx host-ht-tests-run! lib/host/tests/htmlsx.sx"
|
||||
"behavior host-be-tests-run! lib/host/tests/behavior.sx"
|
||||
"flows host-fl-tests-run! lib/host/tests/flows.sx"
|
||||
"compose host-cp-tests-run! lib/host/tests/compose.sx"
|
||||
"execute host-ex-tests-run! lib/host/tests/execute.sx"
|
||||
"session host-se-tests-run! lib/host/tests/session.sx"
|
||||
|
||||
44
lib/host/flows.sx
Normal file
44
lib/host/flows.sx
Normal file
@@ -0,0 +1,44 @@
|
||||
;; lib/host/flows.sx — behavior DAGs + CAPABILITY-typed nodes / capability-advertising runners
|
||||
;; (plans/business-logic-fed-flows.md). P0.2 finding: a SYNCHRONOUS business flow is an EXECUTE-FOLD
|
||||
;; composition (host/execute.sx: effect/alt/each — content-addressed control flow), NOT an artdag
|
||||
;; DATAFLOW DAG (which has no branch). Both are "content-addressed op-DAGs" — two instances of one
|
||||
;; abstraction, run very differently: the execute-fold runner (control flow, synchronous) vs the
|
||||
;; artdag runner (dataflow, memoized/parallel). The DIFFERENCE is which capabilities their nodes
|
||||
;; need. A node declares its capability; a runner ADVERTISES what it supports; the binder checks
|
||||
;; required ⊆ advertised (fail fast); so the sync/durable/distributed choice is DERIVED from the DAG.
|
||||
|
||||
;; ── capability typing: a node kind → the capability it needs ──────────
|
||||
(define host/flow--node-cap
|
||||
(fn (h)
|
||||
(cond ((= h "effect") "effect")
|
||||
((= h "alt") "branch")
|
||||
((= h "each") "each")
|
||||
((= h "wait") "suspend") ;; a timer/suspend node — the execute-fold canNOT run it
|
||||
(else nil))))
|
||||
(define host/flow--uniq-concat
|
||||
(fn (a b) (reduce (fn (acc x) (if (contains? acc x) acc (concat acc (list x)))) a b)))
|
||||
;; the capability SET a composition requires — the union of its nodes' caps (walked recursively).
|
||||
(define host/flow--required-caps
|
||||
(fn (node)
|
||||
(if (not (= (type-of node) "list")) (list)
|
||||
(let ((self (host/flow--node-cap (str (first node))))
|
||||
(kids (reduce (fn (acc c) (host/flow--uniq-concat acc (host/flow--required-caps c)))
|
||||
(list) (rest node))))
|
||||
(if (nil? self) kids (host/flow--uniq-concat (list self) kids))))))
|
||||
(define host/flow--subset? (fn (a b) (reduce (fn (ok x) (and ok (contains? b x))) true a)))
|
||||
|
||||
;; ── the SYNCHRONOUS op-table runner = the execute-fold ────────────────
|
||||
;; a seam runner {:capabilities :run}. It ADVERTISES {effect, branch, each} — the execute-fold
|
||||
;; vocabulary. run: fold the composition (dag) against the env's :ctx → the effect log (as data).
|
||||
(define host/flow--exec-runner
|
||||
{:capabilities (list "effect" "branch" "each")
|
||||
:run (fn (dag env) {:status "done" :effects (host/exec-run dag (or (get env :ctx) {}))})})
|
||||
|
||||
;; DERIVE the runner: bind a DAG to a runner iff its required capabilities ⊆ the runner's advertised.
|
||||
;; Fails fast (a {:bind-error …}) rather than mysteriously at run time. This is where "simple in SX
|
||||
;; / durable in Erlang / distributed in celery-sx" becomes a checkable property of the DAG.
|
||||
(define host/flow--bind
|
||||
(fn (runner dag)
|
||||
(let ((need (host/flow--required-caps dag)) (have (get runner :capabilities)))
|
||||
(if (host/flow--subset? need have) {:ok true :runner runner}
|
||||
{:ok false :bind-error {:needs need :has have}}))))
|
||||
@@ -110,6 +110,7 @@ MODULES=(
|
||||
"lib/host/compose.sx"
|
||||
"lib/host/execute.sx"
|
||||
"lib/host/behavior.sx"
|
||||
"lib/host/flows.sx"
|
||||
"lib/host/htmlsx.sx"
|
||||
"lib/host/blog.sx"
|
||||
"lib/host/server.sx"
|
||||
|
||||
@@ -1182,6 +1182,24 @@
|
||||
(list "urgent" "urgent"))
|
||||
(host-bl-test "publish-activity of a missing post is nil"
|
||||
(host/blog--publish-activity "nope-nope-nope") nil)
|
||||
;; 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"
|
||||
(begin
|
||||
(host/blog-put! "pdag1" "P" "(article (h1 \"x\"))" "published")
|
||||
(host/blog--set-field-values! "pdag1" {"category" "newsletter"})
|
||||
(let ((act (host/blog--publish-activity "pdag1")))
|
||||
(map (fn (e) (get e :verb))
|
||||
(get ((get host/flow--exec-runner :run) host/blog--publish-dag {:ctx (host/blog--publish-ctx act)}) :effects))))
|
||||
(list "validate" "digest"))
|
||||
(host-bl-test "publish-DAG: urgent→notify now, other→skip"
|
||||
(list (map (fn (e) (get e :verb)) (get ((get host/flow--exec-runner :run) host/blog--publish-dag {:ctx {"category" "urgent" "slug" "s"}}) :effects))
|
||||
(map (fn (e) (get e :verb)) (get ((get host/flow--exec-runner :run) host/blog--publish-dag {:ctx {"category" "draft" "slug" "s"}}) :effects)))
|
||||
(list (list "validate" "notify") (list "validate" "skip")))
|
||||
(host-bl-test "publish-DAG requires {effect,branch} and binds to the sync runner (derived)"
|
||||
(list (host/flow--required-caps host/blog--publish-dag)
|
||||
(get (host/flow--bind host/flow--exec-runner host/blog--publish-dag) :ok))
|
||||
(list (list "effect" "branch") true))
|
||||
|
||||
(define
|
||||
host-bl-tests-run!
|
||||
|
||||
50
lib/host/tests/flows.sx
Normal file
50
lib/host/tests/flows.sx
Normal file
@@ -0,0 +1,50 @@
|
||||
;; lib/host/tests/flows.sx — P0.2: capability-typed nodes + the execute-fold (synchronous) runner.
|
||||
;; A composition's required capabilities are DERIVED from its node vocabulary; a runner advertises
|
||||
;; what it supports; the binder checks required ⊆ advertised (fail fast) — so the runner is derived.
|
||||
|
||||
(define host-fl-pass 0)
|
||||
(define host-fl-fail 0)
|
||||
(define host-fl-fails (list))
|
||||
(define host-fl-test
|
||||
(fn (name actual expected)
|
||||
(if (= actual expected)
|
||||
(set! host-fl-pass (+ host-fl-pass 1))
|
||||
(begin (set! host-fl-fail (+ host-fl-fail 1))
|
||||
(append! host-fl-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
;; ── required-caps: the node vocabulary → the capability set ──
|
||||
(host-fl-test "required-caps: effect + alt → {effect, branch}"
|
||||
(host/flow--required-caps (quote (seq (effect a) (alt (when (eq "k" "v") (effect b)) (else (effect c))))))
|
||||
(list "effect" "branch"))
|
||||
(host-fl-test "required-caps: each adds :each; wait adds :suspend"
|
||||
(list (host/flow--required-caps (quote (each (query is-a t) (effect x))))
|
||||
(host/flow--required-caps (quote (seq (effect a) (wait morning)))))
|
||||
(list (list "each" "effect") (list "effect" "suspend")))
|
||||
(host-fl-test "required-caps: a plain effect-only DAG needs only {effect} (zero ceremony)"
|
||||
(host/flow--required-caps (quote (effect notify (field "to"))))
|
||||
(list "effect"))
|
||||
|
||||
;; ── the binder DERIVES the runner: required ⊆ advertised, or fail fast ──
|
||||
(host-fl-test "subset?: required ⊆ advertised"
|
||||
(list (host/flow--subset? (list "effect" "branch") (list "effect" "branch" "each"))
|
||||
(host/flow--subset? (list "suspend") (list "effect" "branch" "each")))
|
||||
(list true false))
|
||||
(host-fl-test "bind: an {effect,branch} DAG binds to the exec runner; a {suspend} DAG fails FAST"
|
||||
(let ((ok (host/flow--bind host/flow--exec-runner (quote (alt (when (eq "k" "v") (effect a)) (else (effect b))))))
|
||||
(bad (host/flow--bind host/flow--exec-runner (quote (seq (effect a) (wait m))))))
|
||||
(list (get ok :ok) (get bad :ok) (get (get bad :bind-error) :needs)))
|
||||
(list true false (list "effect" "suspend")))
|
||||
|
||||
;; ── the execute-fold runner folds a composition against the env :ctx → effect-as-data ──
|
||||
(host-fl-test "exec runner: run a composition → {:status done :effects […]}"
|
||||
(let ((r ((get host/flow--exec-runner :run) (quote (effect notify (field "to"))) {:ctx {"to" "alice"}})))
|
||||
(list (get r :status) (map (fn (e) (get e :verb)) (get r :effects)) (get (first (get r :effects)) :args)))
|
||||
(list "done" (list "notify") (list "alice")))
|
||||
(host-fl-test "exec runner advertises {effect, branch, each}"
|
||||
(get host/flow--exec-runner :capabilities)
|
||||
(list "effect" "branch" "each"))
|
||||
|
||||
(define host-fl-tests-run!
|
||||
(fn ()
|
||||
{:total (+ host-fl-pass host-fl-fail)
|
||||
:passed host-fl-pass :failed host-fl-fail :fails host-fl-fails}))
|
||||
Reference in New Issue
Block a user