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:
2026-07-02 14:18:08 +00:00
parent 8c48cac46f
commit e38a8381d4
7 changed files with 151 additions and 9 deletions

44
lib/host/flows.sx Normal file
View 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}}))))