# 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. ## 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.