host: factor the shared composition CORE — one fold, N domains (composition step 8)

The roadmap's capstone: now that two folds exist (render, execute), extract the machinery
they share. host/comp-fold (compose.sx) is the reusable core — the seq/alt/each combinator
dispatch + the `when` predicate set (host/comp--pred?) + the context-environment + the `each`
source (host/comp--source) + recursion + the depth guard, ALL in one place. A domain plugs in
via a small dict {:empty :combine :leaf :overflow}; only its leaves and how results combine
differ:
  render  = {:empty ""     :combine str    …}  leaf -> markup (+ row/grid layout combinators)
  execute = {:empty (list) :combine concat …}  leaf -> effect

host/comp-render and host/exec-run are now one-liners over host/comp-fold with their domain.
execute.sx shed its own seq/alt/each dispatch — it's just a dict + a leaf. A THIRD domain
(eval/reduce/extent over the same algebra) is now only a new dict + leaf, no new control flow.

Both folds went through the core with ZERO behaviour change: new tests/compose.sx exercises
the core + render domain directly (17/17 — leaves, seq, row, alt+when (has/eq/not), each
(items/query/empty), tmpl recursion over a (children) tree + depth guard, ref transclude, one
object two contexts); execute 13/13; blog 162/164 (2 pre-existing relate-picker fails). Full
host conformance 388/390. Wired tests/compose.sx into conformance.

plans/composition-objects.md roadmap steps 1-8 COMPLETE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 23:53:56 +00:00
parent ed68b9883d
commit e12e314bc3
5 changed files with 190 additions and 119 deletions

View File

@@ -134,8 +134,17 @@ Transclusion = a `ref` leaf. Sort/filter/limit/group = the *source query* langua
execute picks the SAME branch → effect. A publish workflow (validate→branch→notify-each) runs as
one execute-fold. The behaviour model (Slice 9) is "an execute-fold over a composition object",
not a separate system. 13/13 (execute suite). Wired into conformance + serve.
8. **Factor out the shared machinery** once two folds exist: the fork model (ordered, labelled,
`when`), the combinator dispatch, the context-environment, and recursion become a reusable
`compose` core; each domain (`render`, `execute`, `eval`, …) supplies only its leaf + combinator
semantics. The block editor + the metamodel UI then generalise to *every* fold — one composition
editor authors documents, workflows, queries, and pipelines alike.
8. **(done)** Factor out the shared machinery. `host/comp-fold` (compose.sx) is the reusable
core: the seq/alt/each combinator dispatch + the `when` predicate set + the context-environment
+ the `each` source + recursion + the depth guard, ALL in one place. A domain plugs in via a
dict `{:empty :combine :leaf :overflow}` — only its leaves and how results combine. render =
`{:empty "" :combine str …}` (leaf → markup, + row/grid layout combinators); execute =
`{:empty (list) :combine concat …}` (leaf → effect). Both folds went through the core with zero
behaviour change (compose suite 17/17, execute 13/13, blog 162/164 — the 2 fails pre-existing).
A third domain (`eval`/`reduce`/`extent`) is now just a new dict + leaf. The block editor +
metamodel UI generalise to *every* fold — one composition editor for documents, workflows,
queries, pipelines alike.
## Status: roadmap COMPLETE (steps 1-8). Remaining polish: Playwright live-swap check for the
block editor; `alt`/`each` block insertion in the editor; a live workflow object executed via the
execute-fold (the way `/compose-demo` shows the render-fold); a third domain to exercise the core.