# Composition objects — a content-addressed, data-driven UI model

Everything the system stores is an **object**: typed, content-addressed (`:cid`), in one graph.
"Post" was the blog's word; the unit is an object. A *document* is an object whose **body** is a
composition over other objects' CIDs. This is the cards-as-objects decision, generalised.

## One mechanism: ordered, labelled forks

An object forks into children via **labelled, ordered edges** (the relations engine + `order` on
the edge value + an optional `when`). There is no separate "composition system" — relations *are*
the forks. The **label** says what a fork means:
- **structural** (`contains`) → ordered, part of identity, rendered;
- **cross-cutting** (`tagged`, `related`, `author`) → loose links, not structural.

Multiple relations from an object *are* its fork. No "multiple DAGs per object" — fork immediately;
differently-labelled forks (`body` vs `aside`) give named slots. **Join** = a child CID referenced
by two forks — free, because content-addressed. The whole structure is a **Merkle DAG** (git trees
/ IPFS / artdag): `:cid` = hash over `fields + contains-forks (child-CID + order + when)`.

## The body is a tiny UI language (the render-fold is its interpreter)

A body is a composition node. Four combinators + leaves + references:

| node | meaning | strategy |
|------|---------|----------|
| `(seq …)` | **sequence** | render all (block), in order |
| `(row …)` / `(grid …)` | **layout** (par) | render all, side-by-side |
| `(alt (when P n) … (else n))` | **conditional** (or) | render the FIRST child whose `when` holds |
| `(each src tmpl)` | **iteration** (loop) | eval `src` → items; render `tmpl` per item (item bound) |
| `(ref CID)` | transclude | fetch object by CID, render its body |
| `(card TYPE fields)` | leaf | render via the card-type's `:template` (host/blog--instantiate) |
| `(tmpl NAME)` | **recursion** | a named template, may reference itself |

`seq/row` = render-**all** passing children; `alt` = render-**first** passing child. So **and/or/choice
all come from one axis (`when` on forks) × the container's all/first strategy** — `Alt` isn't a new
node kind, it's "first" instead of "all".

## The two fundamentals we designed IN

1. **Recursion** — `(tmpl NAME)` may reference itself; `(each (children) (tmpl NAME))` renders trees
   (comment threads, nested nav, the `/meta` type hierarchy itself). Terminates naturally when a
   query runs dry; a **depth guard** in the context backstops it.
2. **The context is an environment, not a flat dict.** `when` reads it; `each` *extends* it
   (`:item`). Make it extensible + reactive-ready and the two non-composition axes plug in with NO
   new combinators:
   - **Behaviour / interactivity** (Slice 9 lifecycles/effects) — a button references a behaviour;
   - **Reactivity / local state** (the reactive runtime) — `alt(when local-state=active-tab)` is a
     tabset, `alt(when accordion-open)` an accordion; a *live* `each` re-renders on data change.
   The static render-fold becomes a live, interactive UI purely by making the context live.

## The unifying property

**The object's CID is its *definition* (the query, the template, every `when`-variant). The
*rendering* is the *execution* (which items, which branch, which context).** The object is the
program; the render is the run. One immutable content-addressed object encodes its whole
responsive/personalised/variant space; rendering picks the path. Render-fold and the Slice-9
behaviour interpreter are the **same shape** — interpreters over content-addressed objects + the
decidable-core predicate set + the graph. The system converges on: objects + small interpreters.

## Beyond content — composition is universal; a fold per domain

The render-fold isn't "the content renderer" — it's **fold #1**. The composition DAG is a
**universal algebra** (`seq/par/alt/each` over content-addressed objects); *content* is just one
*interpretation*. Same structure, a different **fold** per domain — what changes is what the
combinators and leaves *mean*:

| domain | the fold | `seq` | `par` | `alt`+`when` | `each` | substrate |
|--------|----------|-------|-------|-----------|--------|-----------|
| **content** | render → HTML | block order | layout/columns | choose variant | map items | `compose.sx` (done) |
| **behaviour** | execute → effects | steps in order | concurrent | branch (if/cond) | for-each | `[[project_flow_on_sx]]` |
| **query** | eval → results | join/chain | union | conditional | iterate/quantify | `[[project_relations_on_sx]]` (Datalog) |
| **pipeline** | reduce → data | dataflow stages | parallel ops | choose path | fan-out | `[[project_artdag_on_sx]]` (content-addressed DAG) |
| **types** | extent → set | — | ∧ intersection | — | ∨ union | the type algebra (`make-and!`/`make-or!`) |

So **"relations just a fork" generalises**: a `contains` fork folded by *render* is a document; a
`then` fork folded by *execute* is a workflow step; a `depends-on` fork folded by *eval* is a
dependency graph. **The relation kind + the fold = the domain.** This isn't aspirational — the
repo's `X-on-sx` loops ALREADY ARE these folds (flow = execute, Datalog = eval, artdag = a
content-addressed composition DAG); we just hadn't seen them as one shape. The composition DAG is
the **convergence point** the whole fleet has been circling.

The payoff is concrete: **build the composition machinery ONCE** (forks + ordered edges + the four
combinators + a fold framework) → reuse for every domain by writing one interpreter. **The block
editor edits *any* composition** — author a workflow like a document, same structure, one editor.
The whole system collapses to four ideas: **content-addressed objects + a composition algebra +
per-domain folds + the decidable-core predicates (`when`).** The render-fold's shape (walk the
composition, dispatch combinators, recurse, read the context) is the *template* for every other fold.

## What lives elsewhere (not composition primitives)

Transclusion = a `ref` leaf. Sort/filter/limit/group = the *source query* language (Datalog).
`each` reconciliation keys = the item's CID (free). Empty / missing-CID = render-fold robustness
(the per-block guard). Async/streaming, events, local state = the behaviour + reactive axes.

## Build roadmap

1. **Keystone (this):** `lib/host/compose.sx` — the render-fold interpreter over seq/row/alt/each/
   ref/card/tmpl, with the context-as-environment, `when` predicates, and recursion + depth guard.
   Self-contained proof: render one composed object two ways (auth on/off) + a recursive tree.
2. Wire it to objects: a document's `:body` is a composition node; `contains` forks carry order;
   `host/blog-render` dispatches to the render-fold when `:body` is present (else the legacy
   `sx_content` path). Card leaves render via the existing card-type `:template`.
3. **(done)** `each` source = a graph query: `(query is-a TYPE)` resolves via a `query`
   resolver injected into the render context (`host/blog--comp-ctx` binds
   `host/blog--comp-query` → `host/blog-instances-of` → records). compose.sx stays
   self-contained — it asks the context for the data; the host supplies graph access. The
   list isn't baked into the body; it's whatever is-a TYPE *right now*. (`/compose-demo`
   each is now a live query over seeded `compose-item` instances.)
4. **(done)** Live context: `host/blog--comp-ctx` routes auth + device (User-Agent) + locale
   (Accept-Language) — read purely from the request — into the render context, so the SAME
   object renders a responsive/personalised variant (`(alt (when (eq "device" "mobile") …) …)`).
   Reactive values plug into the same context later with no new combinators.
5. **(done)** The typed importer decomposes content into card OBJECTS + a `contains` body
   (cards-as-objects), instead of one `sx_content` string. `host/blog--decompose!` splits an
   `(article …)` into one stored card object per block (is-a a card-type + field-values),
   linked by ordered `contains` edges, with `:body = (seq (ref c0) (ref c1) …)`. Card types
   carry a render `:template`, so the `ref` combinator transcludes each card via the existing
   typed-block path. `/import` wired; home filtered to published so `"block"` cards stay hidden.
   The `val` (raw value) leaf added for attribute interpolation. (Perf: typing now reads direct
   KV `subtype-of` edges via a host-side BFS, not lib/relations — no Datalog re-saturation.)
6. **(done, server-side)** The block editor edits the body: `host/blog-block-add!` /
   `-remove!` / `-move!` operate on the `:body` ref-seq + ordered `contains` edges;
   `host/blog--block-editor` renders a row per block (type + preview + ↑/↓/remove + a link
   to edit the card's fields) + an add-block form, injected into the edit page; routes
   `POST /:slug/blocks/{add,:cslug/remove,:cslug/move}` (guarded, SX-htmx outerHTML swap).
   Per-block field editing is free — a card is an object, edited via its own `/<cslug>/edit`.
   (Live SX-htmx swap still wants a Playwright check; `alt`/`each` block insertion deferred.)
7. **(done)** Prove universality with a second fold. `lib/host/execute.sx` is an `execute`-fold
   over the *same* `seq/alt/each` structure: leaves = effects, `seq` = steps in order, `alt`+`when`
   = branch, `each` = for-each; the fold returns an effect log. It REUSES compose.sx's shared
   machinery — `host/comp--pred?` (when), `host/comp--field` (field/value), `host/comp--source`
   (each source) — so only the leaf semantics + accumulator differ. KEYSTONE proven (tests): ONE
   `(alt (when …) …)` skeleton + ONE context folds two ways — render picks the branch → HTML,
   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. **(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.
