host: cards-as-objects import + typing reads direct KV edges (composition step 5 + perf)

STEP 5 (cards-as-objects). The importer no longer carries a Ghost body as one opaque
sx_content string: host/blog--decompose! splits an (article …) into one stored card OBJECT
per top-level block (is-a the mapped card-type + its field-values), links each by an ordered
`contains` edge, and sets the post :body = (seq (ref c0) (ref c1) …). Card types now carry a
render :template, so the new `ref` combinator (compose.sx) transcludes each card via the
SAME typed-block path articles use. /import wired to decompose; the home index filtered to
published so the "block"-status card objects stay hidden. Added the `val` leaf (raw field
value, no <span>) for attribute interpolation in templates (href/src). The post page renders
the transcluded cards — verified end-to-end (conformance 157/159; the 2 fails are the
pre-existing relate-picker pagination pair, unrelated).

PERF (the conformance-speed fix). host/blog typing — types-of / instances-of / type-defs —
computed the subtype closure via lib/relations descendants/ancestors, and EVERY such call
re-saturates the whole CEK-interpreted Datalog ruleset (~seconds each). Typing is the hottest
path (is-a?/types-of/instances-of run per post, per picker, per render), so this dominated
both the blog suite and live page latency. Now the closure is a host-side BFS over the DIRECT
subtype-of edges (the edge:* KV rows, via host/blog--subtype-closure) — one snapshot per
closure, O(edges), cycle-safe, Datalog-free. Same transitive set (KV == relations for direct
edges, host/blog-relate! writes both), so exact, not approximate. Drops Datalog out of the
typing hot path entirely — speeds conformance AND the live site (/tags etc.).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 22:20:38 +00:00
parent a25427cb79
commit 14a6bd6411
4 changed files with 201 additions and 43 deletions

View File

@@ -100,6 +100,12 @@
((= h "val") (host/comp--field (first args) ctx)) ;; raw value, no markup — for attributes
((= h "text") (str (first args)))
((= h "card") (host/comp--card (str (first args)) (first (rest args))))
;; ref: TRANSCLUDE another object by id/CID — fetch it and render its body. Like
;; `query`, this delegates to a resolver bound in the context (the host supplies
;; graph access) so compose.sx stays self-contained. A join in the Merkle DAG is
;; free: two bodies can (ref) the same child id (content-addressed).
((= h "ref") (let ((rfn (get ctx "ref")))
(if rfn (rfn (str (first args)) ctx) "")))
((= h "tmpl") (host/comp--render (get host/comp--tmpls (str (first args))) ctx))
(else ""))))))