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:
@@ -536,6 +536,23 @@
|
||||
(concat acc (list (str "‘" k "’ blocks are not allowed in :" field)))))
|
||||
(else acc))))
|
||||
(list) (host/blog--comp-nodes slug field))))
|
||||
;; ── Part B: RELATIONS are governed by the type too — related / is-a / subtype-of / tagged are
|
||||
;; part of the object's composition (external — NOT in the CID), and the type declares which
|
||||
;; relation kinds its instances may use (:type-relations). Absent -> all kinds (back-compat). --
|
||||
(define host/blog--all-rel-kinds (fn () (map (fn (s) (get s :kind)) host/blog-rel-kinds)))
|
||||
(define host/blog--type-relations (fn (type) (get (host/blog-get type) :type-relations)))
|
||||
(define host/blog--set-type-relations!
|
||||
(fn (type kinds)
|
||||
(let ((r (host/blog-get type))) (when r (host/blog--write! type (merge r {:type-relations kinds}))))))
|
||||
;; the relation kinds a post may use = the union its types declare (:type-relations); if no
|
||||
;; type declares any, every registered kind (so metamodel types keep full freedom by default).
|
||||
(define host/blog--allowed-relations
|
||||
(fn (slug)
|
||||
(let ((declared (reduce (fn (acc t) (let ((r (host/blog--type-relations t)))
|
||||
(if r (host/blog--uniq (concat acc r)) acc)))
|
||||
(list) (host/blog-types-of slug))))
|
||||
(if (empty? declared) (host/blog--all-rel-kinds) declared))))
|
||||
(define host/blog--relation-allowed? (fn (slug kind) (contains? (host/blog--allowed-relations slug) kind)))
|
||||
;; ── Part C: the TYPE DEFINITION is itself editable — a type's :fields (each with, for a
|
||||
;; Composition field, its block grammar) are displayed + edited on the type's own edit page.
|
||||
;; is this post a TYPE? (declares fields, or is subtype-of "type" transitively). --
|
||||
@@ -1232,6 +1249,10 @@
|
||||
;; above the body. (field "subtitle") resolves to the instance's value at render.
|
||||
(host/blog--set-template! "article"
|
||||
"(p :style \"font-style:italic;color:#555;margin:0 0 1em;font-size:1.1em\" (field \"subtitle\"))")
|
||||
;; article's allowed RELATIONS (Part B): an article instance may be related, typed, and
|
||||
;; tagged — but NOT subtyped (subtype-of is for types). The relation editors + relate
|
||||
;; handler read this; the metamodel types declare none, so they keep every kind.
|
||||
(host/blog--set-type-relations! "article" (list "related" "is-a" "tagged"))
|
||||
;; ── cards-as-types: the blog content block vocabulary (kg-cards / content-on-sx
|
||||
;; block kinds) as metamodel types. "card" is the root; each card kind is a subtype
|
||||
;; with its own fields. These define the editor's card palette + the radar migrator's
|
||||
@@ -1678,11 +1699,12 @@
|
||||
;; section, generated by ITERATING the registry (add a kind -> it gets an editor).
|
||||
(define host/blog--relation-editors
|
||||
(fn (slug)
|
||||
(cons (quote div)
|
||||
;; false: the initial edit page renders empty pickers (the load trigger fills
|
||||
;; each), keeping this render cheap. The relate/unrelate FRAGMENT passes true.
|
||||
(map (fn (spec) (host/blog--relation-editor slug (get spec :kind) false))
|
||||
host/blog-rel-kinds))))
|
||||
(let ((allowed (host/blog--allowed-relations slug)))
|
||||
(cons (quote div)
|
||||
;; only the relation kinds the post's TYPE permits (Part B). false: the initial edit
|
||||
;; page renders empty pickers (the load trigger fills each), keeping this render cheap.
|
||||
(map (fn (spec) (host/blog--relation-editor slug (get spec :kind) false))
|
||||
(filter (fn (spec) (contains? allowed (get spec :kind))) host/blog-rel-kinds))))))
|
||||
|
||||
;; ── block editor: edit the post :body composition (insert/reorder/remove blocks) ─────
|
||||
;; A row per block (type + preview + up/down/remove + a link to edit the card's fields) and
|
||||
@@ -1847,8 +1869,25 @@
|
||||
(unquote (host/blog--checkbox "allow-cond" "conditional (or)" (contains? ctrl "cond")))
|
||||
(unquote (host/blog--checkbox "allow-each" "repeater (each)" (contains? ctrl "each")))
|
||||
(br) (button :type "submit" :style "margin-top:0.4em" "save grammar"))))))))))
|
||||
;; the whole type-definition editor: a row per field (scalar shown as name:type; Composition
|
||||
;; shown with its grammar form).
|
||||
;; the RELATION checklist for a type (id #type-relations): which relation kinds its instances
|
||||
;; may be linked by (Part B — relations are type-governed composition too).
|
||||
(define host/blog--relations-form
|
||||
(fn (slug)
|
||||
(let ((cur (or (host/blog--type-relations slug) (host/blog--all-rel-kinds)))
|
||||
(all (host/blog--all-rel-kinds))
|
||||
(url (str "/" slug "/relations")))
|
||||
(cons (quote form)
|
||||
(append
|
||||
(quasiquote (:method "post" :action (unquote url) :sx-post (unquote url)
|
||||
:sx-target "#type-relations" :sx-swap "outerHTML"
|
||||
:id "type-relations" :style "margin:0.5em 0;padding:0.5em;border:1px dashed #bbb"
|
||||
(div :style "font-weight:bold;font-size:0.9em;margin-bottom:0.3em"
|
||||
"instances may be linked by these relations —")))
|
||||
(append
|
||||
(map (fn (k) (host/blog--checkbox (str "rel-" k) k (contains? cur k))) all)
|
||||
(quasiquote ((br) (button :type "submit" :style "margin-top:0.4em" "save relations")))))))))
|
||||
;; the whole type-definition editor: the field/grammar rows + the relation checklist. "It's
|
||||
;; just more composition" — the inline block grammar AND the external relations, in one place.
|
||||
(define host/blog--type-def-editor
|
||||
(fn (slug)
|
||||
(let ((fields (host/blog-fields-of slug)))
|
||||
@@ -1862,7 +1901,7 @@
|
||||
(append
|
||||
(quasiquote (:id "type-def" :style "margin-top:1.5em;border-top:2px solid #999;padding-top:1em"
|
||||
(h3 :style "font-size:1em;margin:0 0 0.4em" "Type definition — what this type's instances may contain")))
|
||||
rows))))))
|
||||
(append rows (list (host/blog--relations-form slug)))))))))
|
||||
|
||||
;; ── read handlers ───────────────────────────────────────────────────
|
||||
;; Post body is rendered per-block (a guarded HTML string) then injected raw.
|
||||
@@ -2219,7 +2258,8 @@
|
||||
;; this enforces the same schema against crafted/API requests; an invalid
|
||||
;; relate is a silent no-op (consistent with the other guards here).
|
||||
(when (and other (not (= other "")) (not (= other slug))
|
||||
(host/blog--kind-spec kind) (host/blog-exists? other)
|
||||
(host/blog--kind-spec kind) (host/blog--relation-allowed? slug kind)
|
||||
(host/blog-exists? other)
|
||||
(host/blog--valid-object? kind other))
|
||||
(host/blog-relate! slug other kind))
|
||||
;; AJAX (the picker's sx-post, carries SX-Target): return the re-rendered
|
||||
@@ -2312,6 +2352,17 @@
|
||||
(if (host/blog--editor-swap-req? req)
|
||||
(dream-html (render-page (host/blog--grammar-form slug (host/blog--own-field slug fname))))
|
||||
(dream-redirect (str "/" slug "/edit")))))))
|
||||
;; POST /<type>/relations — set which relation kinds the type's instances may use (Part B).
|
||||
(define host/blog-relations-submit
|
||||
(fn (req)
|
||||
(let ((slug (dream-param req "slug")))
|
||||
(begin
|
||||
(when (and (host/blog-exists? slug) (host/blog--is-type? slug))
|
||||
(host/blog--set-type-relations! slug
|
||||
(filter (fn (k) (not (nil? (host/field req (str "rel-" k))))) (host/blog--all-rel-kinds))))
|
||||
(if (host/blog--editor-swap-req? req)
|
||||
(dream-html (render-page (host/blog--relations-form slug)))
|
||||
(dream-redirect (str "/" slug "/edit")))))))
|
||||
|
||||
;; GET /<slug>/edit — edit form pre-filled with the post's current title, raw
|
||||
;; sx_content (in a textarea — render-to-html escapes the text child, so the
|
||||
@@ -2450,6 +2501,7 @@
|
||||
(dream-post "/:slug/blocks/:idx/move" (host/blog--protect-html resolve host/blog-block-move-submit))
|
||||
(dream-post "/:slug/blocks/:idx/cond" (host/blog--protect-html resolve host/blog-block-cond-submit))
|
||||
(dream-post "/:slug/grammar" (host/blog--protect-html resolve host/blog-grammar-submit))
|
||||
(dream-post "/:slug/relations" (host/blog--protect-html resolve host/blog-relations-submit))
|
||||
(dream-post "/:slug/relate" (host/blog--protect-html resolve host/blog-relate-submit))
|
||||
(dream-post "/:slug/unrelate" (host/blog--protect-html resolve host/blog-unrelate-submit))
|
||||
(dream-post "/meta/new-type" (host/blog--protect-html resolve host/blog-meta-new-type))
|
||||
|
||||
Reference in New Issue
Block a user