host: relations-as-posts slice 3 — typed relations (target-type constraint enforced)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 46s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 46s
A relation's declares-anchor IS its target-type constraint: is-a/subtype-of (anchored by type) require a type object; tagged (anchored by tag) a tag; related (no anchor) any post. host/blog--valid-object?(kind, other) = other ∈ the relation's candidate pool — the SAME set the picker offers — and relate-submit now enforces it (invalid target = silent no-op). The picker never offers an invalid target, so this guards crafted/API requests: the jump from candidate set to an enforced relation schema. A new typed relation needs only a relation-post + a '<TargetType> declares <rel>' edge. host/blog-relate! (direct/seed) stays unvalidated — validation is a handler boundary (the seed writes 'X is-a relation', and relation isn't under type). conformance 291/291 (+4: valid-object? accepts types/tags/any, relate-submit creates the edge for a type object and no-ops for a non-type). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -408,6 +408,16 @@
|
|||||||
(host/blog-slugs)
|
(host/blog-slugs)
|
||||||
(host/blog--reach-down anchors)))))
|
(host/blog--reach-down anchors)))))
|
||||||
|
|
||||||
|
;; Slice 3 — typed relations: a post is a valid OBJECT (target end) of `kind` iff it's
|
||||||
|
;; in the relation's declared candidate set (the down-closure of kind's declares-anchors
|
||||||
|
;; — the target-type constraint). The SAME set the picker offers, so the picker and
|
||||||
|
;; the relate endpoint agree by construction. A relation with no anchor (`related`)
|
||||||
|
;; accepts any existing post. This is what turns "candidate set" into an enforced
|
||||||
|
;; relation schema: is-a's object must be a type, tagged's must be a tag, etc.
|
||||||
|
(define host/blog--valid-object?
|
||||||
|
(fn (kind other)
|
||||||
|
(contains? (host/blog--candidate-pool kind) other)))
|
||||||
|
|
||||||
(define host/blog--relate-candidates
|
(define host/blog--relate-candidates
|
||||||
(fn (slug q kind)
|
(fn (slug q kind)
|
||||||
(let ((spec (host/blog--kind-spec kind)))
|
(let ((spec (host/blog--kind-spec kind)))
|
||||||
@@ -1006,8 +1016,14 @@
|
|||||||
(host/blog--page req "Not found"
|
(host/blog--page req "Not found"
|
||||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))
|
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))
|
||||||
(begin
|
(begin
|
||||||
|
;; …and `other` must satisfy the relation's TARGET-TYPE CONSTRAINT
|
||||||
|
;; (host/blog--valid-object?): you can only declare `slug is-a <a type>`,
|
||||||
|
;; tag with `<a tag>`, etc. The picker never offers an invalid target, so
|
||||||
|
;; 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))
|
(when (and other (not (= other "")) (not (= other slug))
|
||||||
(host/blog--kind-spec kind) (host/blog-exists? other))
|
(host/blog--kind-spec kind) (host/blog-exists? other)
|
||||||
|
(host/blog--valid-object? kind other))
|
||||||
(host/blog-relate! slug other kind))
|
(host/blog-relate! slug other kind))
|
||||||
;; AJAX (the picker's sx-post, carries SX-Target): return the re-rendered
|
;; AJAX (the picker's sx-post, carries SX-Target): return the re-rendered
|
||||||
;; editor for this kind so its sx-swap="outerHTML" replaces #rel-editor-KIND
|
;; editor for this kind so its sx-swap="outerHTML" replaces #rel-editor-KIND
|
||||||
|
|||||||
@@ -502,6 +502,31 @@
|
|||||||
(contains? kinds "subtype-of") (contains? kinds "tagged")))
|
(contains? kinds "subtype-of") (contains? kinds "tagged")))
|
||||||
(list true true true true))
|
(list true true true true))
|
||||||
|
|
||||||
|
;; -- relations are TYPED: the target-type constraint is enforced (slice 3) --
|
||||||
|
;; A valid object of a relation is one in its declared candidate set (the picker's
|
||||||
|
;; pool). So is-a's object must be a type, tagged's must be a tag, related's any post.
|
||||||
|
(host-bl-test "valid-object?: is-a accepts a type (article), rejects a plain post (ppost)"
|
||||||
|
(list (host/blog--valid-object? "is-a" "article") (host/blog--valid-object? "is-a" "ppost"))
|
||||||
|
(list true false))
|
||||||
|
(host-bl-test "valid-object?: tagged accepts a tag (ocaml); related accepts any post"
|
||||||
|
(list (host/blog--valid-object? "tagged" "ocaml") (host/blog--valid-object? "related" "ppost"))
|
||||||
|
(list true true))
|
||||||
|
;; the relate ENDPOINT enforces it: is-a to a type relates; is-a to a non-type no-ops.
|
||||||
|
(host/blog-unrelate! "alpha-post" "article" "is-a")
|
||||||
|
(host-bl-test "relate-submit: is-a to a type (article) creates the edge"
|
||||||
|
(begin
|
||||||
|
(host/blog-relate-submit (host-bl-relreq "alpha-post" "relate"
|
||||||
|
{:sx-request "true" :sx-target "#rel-editor-is-a"} "article" "is-a"))
|
||||||
|
(contains? (host/blog-out "alpha-post" "is-a") "article"))
|
||||||
|
true)
|
||||||
|
(host/blog-unrelate! "alpha-post" "article" "is-a")
|
||||||
|
(host-bl-test "relate-submit: is-a to a NON-type (beta-post) is rejected (no edge)"
|
||||||
|
(begin
|
||||||
|
(host/blog-relate-submit (host-bl-relreq "alpha-post" "relate"
|
||||||
|
{:sx-request "true" :sx-target "#rel-editor-is-a"} "beta-post" "is-a"))
|
||||||
|
(contains? (host/blog-out "alpha-post" "is-a") "beta-post"))
|
||||||
|
false)
|
||||||
|
|
||||||
;; -- Phase 3: tags as posts -- (ocaml is-a tag, from the seed-types test above)
|
;; -- 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"
|
(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"))
|
(list (host/blog-is-tag? "ocaml") (host/blog-is-tag? "ppost"))
|
||||||
|
|||||||
@@ -63,12 +63,17 @@ the relation's object-end declaration from the anchor**, which includes the root
|
|||||||
the picker O(1)-perform and cut the suspend/resume churn. Subject-end declarations + a
|
the picker O(1)-perform and cut the suspend/resume churn. Subject-end declarations + a
|
||||||
proper relation-subtype closure (when relations get subtyped) also belong here.
|
proper relation-subtype closure (when relations get subtyped) also belong here.
|
||||||
|
|
||||||
### Slice 3 — typed relations (target-type constraints)
|
### Slice 3 — typed relations (target-type constraints) — DONE
|
||||||
- A declaration carries a **target-type constraint**: the *other* end must be (an instance
|
- The declaration's `declares`-anchor IS the target-type constraint: `is-a`/`subtype-of`
|
||||||
of) some type. `is-a`'s object must be a type; a hypothetical `wrote`'s object must be a
|
(anchored by `type`) require a type object; `tagged` (anchored by `tag`) a tag. A new
|
||||||
`Work`. Validation on relate (and on save) = `is-a?` against the constraint. This is the
|
`wrote` relation needs only a `Work declares wrote` edge — fully data-driven.
|
||||||
jump from "candidate set" to a real relation schema. Picker candidates and validation
|
- `host/blog--valid-object?(kind, other)` = `other ∈ candidate-pool(kind)` — the SAME set
|
||||||
read the *same* constraint.
|
the picker offers, so picker and validation agree by construction. `relate-submit` now
|
||||||
|
enforces it (an invalid target is a silent no-op, like the other guards); `related`
|
||||||
|
(no anchor) accepts any post. The picker never offers an invalid target, so this guards
|
||||||
|
crafted/API requests — the jump from "candidate set" to an enforced relation schema.
|
||||||
|
- NOTE: `host/blog-relate!` (direct/seed) stays UNVALIDATED — the seed needs to write
|
||||||
|
`X is-a relation` where `relation` isn't under `type`. Validation is a *handler* boundary.
|
||||||
|
|
||||||
### Slice 4 — type algebra
|
### Slice 4 — type algebra
|
||||||
Types are posts + `subtype-of` is a partial order ⇒ a **lattice**, and `is-a?` is transitive
|
Types are posts + `subtype-of` is a partial order ⇒ a **lattice**, and `is-a?` is transitive
|
||||||
|
|||||||
Reference in New Issue
Block a user