From 29aa7cd70f3ae18ec568f585911ff3da23153cb8 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 30 Jun 2026 20:10:49 +0000 Subject: [PATCH] =?UTF-8?q?host:=20each-source=20=3D=20graph=20query=20?= =?UTF-8?q?=E2=80=94=20the=20data-driven=20each=20(composition=20roadmap?= =?UTF-8?q?=20step=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An object's `each` source can now be a GRAPH QUERY: `(query is-a TYPE)` resolves to whatever is-a TYPE *right now* — the list isn't baked into the body, it's the live graph. The object's `each` IS the query; the render is the run over current data (the unifying property, now over real data). compose.sx stays self-contained: the `query` source delegates to a resolver bound in the render context under "query" — it asks the context for data, never reaching into the graph itself. The host supplies graph access via host/blog--comp-query (`(query is-a TYPE)` -> host/blog-instances-of -> full records) injected by host/blog--comp-ctx (auth + resolver); the post handler renders :body against that context. Added a `val` leaf — the raw field value with no markup wrapper, for use inside attributes (href/src). `field` stays span-wrapped for display; `(val :slug)` makes a real link in the each template. /compose-demo's each is now a live (query is-a compose-item) over two seeded instances instead of a baked literal list. Verified end-to-end via a focused harness eval over the full relations+persist+blog stack (query iterates real instances; clean href via val; empty query -> empty, not an error). Blog suite 151/153 — the 2 fails ("relate-options load-more sentinel", "related picker offers all posts") are PRE-EXISTING (clean HEAD is 149/151 with the identical 2 fails, a relate-picker pagination-boundary issue) and unrelated to composition; my 2 new tests pass. Co-Authored-By: Claude Opus 4.8 --- lib/host/blog.sx | 33 +++++++++++++++++++++++++++++---- lib/host/compose.sx | 15 ++++++++++++--- lib/host/tests/blog.sx | 25 +++++++++++++++++++++++++ plans/composition-objects.md | 7 ++++++- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/lib/host/blog.sx b/lib/host/blog.sx index ea13c316..ad4d7608 100644 --- a/lib/host/blog.sx +++ b/lib/host/blog.sx @@ -527,11 +527,36 @@ (fn (slug body) (let ((r (host/blog-get slug))) (when r (host/blog--write! slug (merge r {:body body})))))) +;; The resolver for the composition `each` graph-query source (compose.sx asks the context +;; for "query"). `(query REL TYPE)` -> the objects related to TYPE by REL, as full records +;; so the per-item template can field them. Today the supported relation is is-a (TYPE's +;; transitive instances, via host/blog-instances-of); the dispatch leaves room for more. +;; This is the DATA-DRIVEN each source — the object's `each` is the query, the render is +;; the run over whatever the graph currently holds. +(define host/blog--comp-query + (fn (qargs ctx) + (let ((rel (str (first qargs))) (type (str (first (rest qargs))))) + (cond + ((= rel "is-a") (map host/blog-get (host/blog-instances-of type))) + (else (list)))))) +;; the render context for a :body: auth from the principal + the graph-query resolver. +(define host/blog--comp-ctx + (fn (principal) + (merge (if (nil? principal) {} {"auth" "yes"}) + {"query" host/blog--comp-query}))) ;; Seed a live demo of the composition fold: one object, rendered by host/comp-render, that ;; shows seq + alt(when auth) + row(par) + each — and renders DIFFERENTLY logged-in vs out. (define host/blog-seed-compose-demo! (fn () (begin + ;; a demo type + two instances, so the each(query …) below iterates REAL graph data — + ;; the list isn't baked into the body, it's whatever is-a compose-item right now. + (host/blog-seed! "compose-item" "Compose Item" "(article (h1 \"Compose Item\"))" "published") + (host/blog-relate! "compose-item" "type" "subtype-of") + (host/blog-seed! "compose-item-revel" "Revel Show" "(article (h1 \"Revel Show\"))" "published") + (host/blog-seed! "compose-item-pub" "Pub Night" "(article (h1 \"Pub Night\"))" "published") + (host/blog-relate! "compose-item-revel" "compose-item" "is-a") + (host/blog-relate! "compose-item-pub" "compose-item" "is-a") (host/blog-seed! "compose-demo" "Composition Demo" "(article (h1 \"Composition Demo\") (p \"Rendered via the composition fold.\"))" "published") (host/blog--set-body! "compose-demo" @@ -542,9 +567,9 @@ (text "

Two columns (par)

") (row (text "
Column A
") (text "
Column B
")) - (text "

A list (each)