host: relations-as-posts slice 1 — declaration-driven candidate pools
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 42s

Types declare which relation they anchor (type declares is-a/subtype-of, tag
declares tagged) via a 'declares' edge; the picker's candidate set is the
down-closure of a relation's anchors through is-a ∪ subtype-of. So is-a/subtype-of
now offer the WHOLE type closure — the roots (type/tag/article) AND instances —
fixing the wrinkle where only instances showed and you could never pick 'tag' or
'article' as a type. 'related' has no anchor → every post.

Replaces the hardcoded :candidates "types"/"tags"/"all" with graph queries
(host/blog--reach-down + the declares edges). Design + roadmap (relations as
first-class posts, typed relations, type algebra, constraints) in
plans/relations-as-posts.md.

host conformance 283/283 (+5: is-a pool includes type roots, excludes plain posts,
tagged anchored by tag, related = all, is-a relate-options offers Article).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 21:40:27 +00:00
parent ad556c3e31
commit b3804ce712
3 changed files with 142 additions and 10 deletions

View File

@@ -457,6 +457,30 @@
(host-bl-test "type-valid? is vacuously true with no schemas (gradual)"
(host/blog-type-valid? "ppost" "(p \"anything\")") true)
;; -- relations-as-posts: declaration-driven candidate pools (plans/relations-as-posts.md) --
;; The picker's candidate set is the down-closure of a relation's anchors. is-a/subtype-of
;; are anchored by `type`, so they offer the WHOLE type closure — the roots (type/tag/
;; article) AND the instances — fixing the wrinkle where only instances showed.
(host-bl-test "is-a candidates = the type closure: roots (type/tag/article) AND instances"
(let ((pool (host/blog--candidate-pool "is-a")))
(list (contains? pool "type") (contains? pool "tag")
(contains? pool "article") (contains? pool "ocaml"))) ;; ocaml is-a tag
(list true true true true))
(host-bl-test "is-a candidates exclude a plain content post (not is-a/subtype-reachable to Type)"
(contains? (host/blog--candidate-pool "is-a") "ppost") false)
(host-bl-test "tagged candidates are anchored by tag (tag + its instances)"
(let ((pool (host/blog--candidate-pool "tagged")))
(list (contains? pool "tag") (contains? pool "ocaml")))
(list true true))
(host-bl-test "related candidates = every post (no declaration anchors it)"
(let ((pool (host/blog--candidate-pool "related")))
(list (contains? pool "type") (contains? pool "ppost")))
(list true true))
;; and it flows through to the live picker endpoint: the is-a picker now offers a type root
(host-bl-test "is-a relate-options offers the type roots (Article)"
(contains? (dream-resp-body (host-bl-app (host-bl-req "/ppost/relate-options?kind=is-a"))) "Article")
true)
;; -- Phase 3: tags as posts -- (ocaml is-a tag, from the seed-types test above)
(host-bl-test "is-tag?: a post that is-a tag is a tag; others are not"
(list (host/blog-is-tag? "ocaml") (host/blog-is-tag? "ppost"))