;; 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. ;; NOTE (plan phase AX): this execute-fold-vs-artdag split is a capability SNAPSHOT, not a boundary ;; — artdag MAY grow +{effect,branch,each} node-kinds and business logic then migrates onto it (to ;; inherit content-addressed memoization / optimize / FEDERATION). The capability model makes that ;; migration seamless; the execute-fold stays the lean default for cheap synchronous flows. ;; ── 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}}))))