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

@@ -789,6 +789,34 @@
(list (host/comp-render body (host/blog--comp-ctx nil mob))
(host/comp-render body (host/blog--comp-ctx nil desk))))
(list "M" "D"))
;; -- cards-as-objects: the importer decomposes content into card OBJECTS + a contains body
;; (not one opaque sx_content string). Each top-level block becomes a stored card object
;; (is-a a card-type + field-values), linked by ordered `contains` edges; the post :body is
;; (seq (ref c0) (ref c1) …) and the render-fold transcludes each card via its type template. --
(host-bl-test "import decomposes the body into typed card objects + a contains body"
(begin
(host/blog-import-post! {"slug" "imp-x" "title" "Imp X"
"sx_content" "(article (h1 \"Heading One\") (p \"Para text.\") (img :src \"p.jpg\" :alt \"alt\"))"
"status" "published"})
(list (len (host/blog-out "imp-x" "contains"))
(host/blog-is-a? "imp-x__b0" "card-heading")
(host/blog-is-a? "imp-x__b1" "card-text")
(host/blog-is-a? "imp-x__b2" "card-image")
(get (host/blog-field-values-of "imp-x__b0") "text")
(get (host/blog-field-values-of "imp-x__b2") "src")))
(list 3 true true true "Heading One" "p.jpg"))
(host-bl-test "a decomposed post :body is a (seq (ref …) …) composition"
(let ((body (host/blog-body-of "imp-x")))
(list (str (first body)) (len (rest body)) (str (first (first (rest body))))))
(list "seq" 3 "ref"))
;; the card objects are status "block" — stored but NOT listed as top-level posts.
(host-bl-test "decomposed card objects do not appear on the published home index"
(contains? (dream-resp-body (host-bl-app (host-bl-req "/"))) "imp-x__b0") false)
;; the post page renders the cards by TRANSCLUSION (ref -> card-type template).
(host-bl-test "decomposed post page renders the transcluded cards"
(let ((html (dream-resp-body (host-bl-app (host-bl-req "/imp-x/")))))
(list (contains? html "Heading One") (contains? html "Para text.") (contains? html "p.jpg")))
(list true true true))
(host-bl-test "a post with no schema'd type is vacuously valid"
(host/blog-type-valid? "ppost" "(p \"anything\")") true)
(host-bl-test "edit-submit rejects content violating the type schema (not saved)"