Files
rose-ash/plans/relations-as-posts.md
giles b3804ce712
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 42s
host: relations-as-posts slice 1 — declaration-driven candidate pools
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>
2026-06-29 21:40:27 +00:00

4.7 KiB
Raw Blame History

Relations as posts — declared, inherited, and eventually algebraic

Principle

Everything is a post in one graph: content-posts, type-posts, relation-posts, and (later) constraint-posts. Nothing about typing is hardcoded — a type-post declares which relations it anchors, declarations are inherited down the type closure, and every candidate set / validation is a transitive graph query (lib/relations). This closes the meta-circular loop the typing plan gestured at: the type system describes itself in its own graph.

Supersedes the hardcoded :candidates "types"/"tags"/"all" field of host/blog-rel-kinds.

Why (the wrinkle that started this)

Candidates for is-a/subtype-of were instances-of("type") — the instances that are types, but NOT the type-defining posts themselves (type, tag, article are wired with subtype-of, no is-a edge, so they're not instances of type). So the picker offered tutorial (is-a tag) but never tag/article/type — the things you most want to say a post is-a. The fix is to ask the right question: a candidate is anything that inherited the relation's object-end declaration from the anchor, which includes the roots.

Model

  • A declaration is an edge T --declares--> R: type-post T anchors relation R at its object end ("you may point at T with R"). Seed: type declares is-a, type declares subtype-of, tag declares tagged. related has no declaration.
  • Candidate set for relating under R = the down-closure of R's anchors through inverse(is-a) inverse(subtype-of) (a post is a candidate iff it is, transitively, an instance-or-subtype of an anchor — or IS one). No anchors ⇒ every post (related).
    • is-a/subtype-of: anchors {type} ⇒ the whole type closure (roots + subtypes + instances). Wrinkle fixed.
    • tagged: anchors {tag} ⇒ the tags.
    • related: no anchor ⇒ all posts.

Roadmap

Slice 1 — declarations + candidate-by-inheritance — DONE

  • Seed declares edges; add host/blog--reach-down (down-closure) and rewire host/blog--candidate-pool to be declaration-driven. :candidates becomes vestigial.
  • Wrinkle fixed: the type roots now appear as is-a candidates.

Slice 2 — relations as first-class posts

  • Seed is-a/subtype-of/tagged/related as posts that own their metadata (:symmetric, :label, :inverse-label, cardinality, end roles). The registry host/blog-rel-kinds melts into reads off these posts. A relation can declare its subject-end anchor too (who may be the source), not just object.

Slice 3 — typed relations (target-type constraints)

  • A declaration carries a target-type constraint: the other end must be (an instance of) some type. is-a's object must be a type; a hypothetical wrote's object must be a Work. Validation on relate (and on save) = is-a? against the constraint. This is the jump from "candidate set" to a real relation schema. Picker candidates and validation read the same constraint.

Slice 4 — type algebra

Types are posts + subtype-of is a partial order ⇒ a lattice, and is-a? is transitive set-membership ⇒ extents have set semantics. So algebra is expressible as posts:

  • Intersection A ∧ B — a type-post whose membership predicate is is-a? A ∧ is-a? B (meet / GLB in the lattice). Union A Bis-a? A is-a? B (join / LUB).
  • Refinement {x : T | φ(x)} — a type-post with a :constraint predicate over a post (generalises today's article schema "must have a heading"). Gradual: declaring the type adds the obligation; the next save must satisfy it.
  • Algebraic types are themselves posts with edges to their operands — is-a? recurses on the expression. Meta-circular: the algebra lives in the graph it describes.

Slice 5 — constraints as posts + validation

  • Promote the schema/:constraint slot to constraint-posts (a predicate expr + message), attachable to any type. Save-time validation evaluates the constraints of a post's full (transitive) type set. Relation cardinality (is-a single-valued? tagged many?) becomes a declared constraint too.

Open design questions (track as we go)

  1. Subject-end declarations — who may be the source of a relation (a root Thing?).
  2. Inheritance path — through is-a AND subtype-of downward (current choice); revisit if instances-of-instances as candidates surprises.
  3. Bootstrap / meta-circularityis-a needs is-a; seed relation-posts + Type is-a Type(?) idempotently, as the type seed already is.
  4. Costreach-down is a BFS of direct-edge scans; fine for a small blog, revisit with a lib/relations transitive query if the graph grows.