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

81 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 B``is-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-circularity**`is-a` needs `is-a`; seed relation-posts + `Type is-a
Type`(?) idempotently, as the type seed already is.
4. **Cost** — `reach-down` is a BFS of direct-edge scans; fine for a small blog, revisit with
a `lib/relations` transitive query if the graph grows.