host: the adapter seam for business-logic-as-composition (design-first)

lib/host/behavior.sx — the substrate-independent seam every runner/transport/registry/driver
plugs into. An engine bundles four dict-of-functions adapters (trigger-registry, runner,
transport, driver); behavior/process folds an ACTIVITY through the pipeline: emit → match
triggers → run each behavior DAG → dispatch each effect-as-data → recurse on new activities
(loop closure, depth-guarded at 8). Every stage injected, so the same DAG + engine run over the
synchronous op-table runner / Erlang durable / celery-sx / fed-sx transport unchanged.

Reference tests (mock adapters) prove the contract: publish→trigger→runner→effect flows; a
non-matching activity fires nothing (log complete, execution precise); an effect that emits a new
activity re-triggers (loop closes); an unbounded loop is depth-guarded (terminates). Wired into
conformance.sh + serve.sh MODULES. behavior 4/4; full host conformance 575/575.

Next: P0 supplies the REAL adapters (publish activity ← host/blog--publish-activity, local-SX
trigger, sync op-table runner over a publish-DAG, host driver) — same engine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 13:42:04 +00:00
parent f240c46fa8
commit 5d04da748a
5 changed files with 172 additions and 1 deletions

View File

@@ -72,7 +72,47 @@ transports swap trivially.
blog_publish_digest flow (urgent/newsletter-suspend/draft-skip/guard-reject/dedup). This is the
reference P0 wires the live host onto.
## P0 — publish workflow, end-to-end (spike)
## The ADAPTER SEAM (design first — the contract every substrate plugs into)
The invariant is an **activity** (state-change event) + a **behavior DAG**. Everything between is
a swappable adapter — each a dict-of-functions (SX-native, like the fold domains). Six contracts:
1. **Activity** (content-addressed event): `{:verb "create"|"update"|"add"|"remove"|"delete"
:actor :object <cid> :object-type :delta :prev <cid> :ts}`.
2. **Behavior binding** (declared on a TYPE): `{:on {:verb :object-type :guard} :dag <ref> :runner
<hint>}`. The type carries content-grammar + allowed-relations + these bindings.
3. **Trigger-registry adapter** — `{:register! (fn spec dag hint) :match (fn activity -> [binding])}`.
Impls: a local SX matcher / next/'s Erlang trigger_registry.
4. **Runner adapter** — `{:run (fn dag env -> {:status "done"|"suspended"|"failed" :results :effects
:resume})}`; env = `{:activity :actor :ctx :effects}`. Impls: sync op-table (artdag/run) / Erlang
durable (flow_dispatch, may suspend) / celery-sx (queue+workers). Durability = the runner's, not
the DAG's.
5. **Transport adapter** — `{:emit (fn activity) :deliver (fn -> [activity])}`. Impls: in-process /
fed-sx (next/) / internal HTTP / IPFS. Content-ids are global → results move by id.
6. **Effect driver** — `{:dispatch (fn effect -> [activity])}`. Perform the effect-as-data; may emit
NEW activities (loop closure).
**Engine** (orchestrator): `behavior/make-engine {:triggers :runner :transport :driver}` →
`behavior/process(engine, activity)`:
```
emit(transport, activity)
for b in match(triggers, activity):
r = run(runner, b.dag, {:activity activity …})
for eff in r.effects:
for a in dispatch(driver, eff): process(engine, a) ; loop closes
```
Every stage injected → swap runner (sync→Erlang→celery-sx), transport (in-proc→fed-sx), trigger
registry, driver — DAG + engine unchanged.
**Existing pieces implement the contracts:** activity ← host/blog--publish-activity (P0.1 ✓) ·
trigger-registry ← next/ trigger_registry OR a local SX matcher · runner ← artdag/run+op-table
(sync) / next/ flow_dispatch (durable) / celery-sx · transport ← artdag/federation (transport
injected) / next/ delivery / in-process · driver ← host writes / email / append-activity.
**Build order:** (a) DONE — the seam as SX contracts + a reference engine wired to MOCK adapters, tested
(process a mock activity → effect flows → loop closes). (b) P0 then supplies the REAL adapters
(publish activity, local-SX trigger, sync op-table runner over a publish-DAG, host driver).
## P0 — publish workflow, end-to-end (spike) — on the seam
Prove: live host publishes a post → fed-sx activity → on-publish trigger → blog_publish_digest.
- [x] **P0.1 — the publish-activity contract (SX side).** host/blog--publish-activity(slug):