host: Part B — relations are type-governed composition too

related / is-a / subtype-of / tagged are part of an object's composition (external — NOT in the
CID), and the TYPE declares which relation kinds its instances may use (:type-relations; absent
-> all kinds, so metamodel types keep full freedom). host/blog--{all-rel-kinds, type-relations,
set-type-relations!, allowed-relations, relation-allowed?}. The relation editors filter to the
permitted kinds; relate-submit ENFORCES it. article declares (related is-a tagged) — an article
instance can't be subtyped. The type-def editor (Part C) gains a relation CHECKLIST + POST
/<type>/relations, so the type's inline block-grammar AND external relations are edited in one
place: "it's just more composition."

blog 189/189 (+ Part B tests: allowed-relations excludes subtype-of for article, editors filter,
relate rejects a forbidden kind, checklist renders, POST /relations sets it). Full conformance
deferred — the sibling OTel loop is contending on the shared warm-conf dir; Part B touches only
blog.sx, so the other 7 suites are unaffected. Verifying live instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 14:22:41 +00:00
parent 30a23d4dae
commit 7838e45aea
2 changed files with 95 additions and 9 deletions

View File

@@ -1006,6 +1006,40 @@
(list (contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/article/edit" "Bearer good" "" ""))) "Type definition")
(contains? (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/my-first-post/edit" "Bearer good" "" ""))) "Type definition"))
(list true false))
;; -- Part B: RELATIONS are type-governed composition — the type declares which relation kinds
;; its instances may use; the editors + relate handler honour it; editable on the type page. --
(host-bl-test "allowed-relations reads the type's :type-relations (article excludes subtype-of)"
(begin
(host/blog-put! "artinst" "AI" "(article (h1 \"x\"))" "published")
(host/blog-relate! "artinst" "article" "is-a")
(let ((allowed (host/blog--allowed-relations "artinst")))
(list (contains? allowed "related") (contains? allowed "is-a") (contains? allowed "tagged")
(contains? allowed "subtype-of"))))
(list true true true false))
(host-bl-test "the relation editors show only the type's permitted kinds"
(let ((html (render-page (host/blog--relation-editors "artinst"))))
(list (contains? html "rel-editor-related") (contains? html "rel-editor-tagged")
(contains? html "rel-editor-subtype-of")))
(list true true false))
(host-bl-test "relate-submit rejects a relation kind the type forbids"
(begin
(host/blog-seed! "sometype" "ST" "(p)" "published") (host/blog-relate! "sometype" "type" "subtype-of")
(host-bl-wapp (host-bl-send "POST" "/artinst/relate" "Bearer good"
"application/x-www-form-urlencoded" "kind=subtype-of&other=sometype"))
(contains? (host/blog-out "artinst" "subtype-of") "sometype"))
false)
(host-bl-test "the type-def editor includes the relation checklist"
(let ((html (render-page (host/blog--type-def-editor "article"))))
(list (contains? html "may be linked by") (contains? html "rel-related") (contains? html "rel-tagged")))
(list true true true))
(host-bl-test "POST /<type>/relations sets the allowed relations from the checklist"
(begin
(host/blog-seed! "rtype" "RT" "(p)" "published") (host/blog-relate! "rtype" "type" "subtype-of")
(host-bl-wapp (host-bl-send "POST" "/rtype/relations" "Bearer good"
"application/x-www-form-urlencoded" "rel-related=on&rel-tagged=on"))
(let ((r (host/blog--type-relations "rtype")))
(list (contains? r "related") (contains? r "tagged") (contains? r "is-a"))))
(list true true false))
;; the editor renders a HAND-AUTHORED composition (text/row/alt-with-text) WITHOUT falling
;; through to "(unknown block)" — every node kind gets a labelled row (the compose-demo case).
(host-bl-test "the block editor renders text/layout/inline-alt nodes (no unknown block)"