# 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. `each` source = a graph query (`(query is-a Event)` → `host/blog-instances-of`) — data-driven. 4. Live context: route auth/device/locale into the context; reactive values later. 5. The typed importer decomposes Ghost Lexical into card objects + a `contains` body (cards-as- objects), instead of one `sx_content` string. 6. The block editor edits the body (insert/reorder/`alt`/`each`) — the metamodel editor for content. 7. **Prove universality with a second fold.** Write a tiny `execute`-fold over the *same* `seq/alt/each` structure that *runs* a workflow (leaves = effects; `seq` = steps in order, `alt` = branch, `each` = for-each) — the way the recursive tree proved recursion, this proves the composition algebra is domain-agnostic. Then the *behaviour* model (Slice 9) is "an `execute`-fold over a composition object", not a separate system. 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.