From 64106c89facc244f79f3211d919d061d022e6c04 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 30 Jun 2026 09:26:30 +0000 Subject: [PATCH] =?UTF-8?q?plan:=20design=20parameterised=20relations=20?= =?UTF-8?q?=E2=80=94=20Slice=206=20(role=20signature:=20a+b+c)=20&=20Slice?= =?UTF-8?q?=207=20(algebra:=20d)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture the Relation<…> design from the discussion. The reframe: the parameters split into two halves — the role SIGNATURE (shape of a tuple: per-role type a, arity b, cardinality c) and the relation's ALGEBRA (behaviour: transitivity/symmetry/inverse/ sub-relations d). A relation is Relation; today's binary typed relations are the degenerate 2-role case. Slice 6: generalise :rel to a :roles signature; (a) per-role type = the declares-anchor made explicit, (b) arity needs reification (instance-posts) for n-ary, (c) cardinality by counting. Nominal variance, JIT caveat for n-ary role iteration. Slice 7: declared algebraic properties with GENERIC closure (retires the hardcoded is-a/subtype closure — OWL property characteristics); real inverse relations; sub-relations. Decidable core stops here; defined-by-rule + cross-role predicates fenced behind the predicate-language decision. Co-Authored-By: Claude Opus 4.8 --- plans/relations-as-posts.md | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/plans/relations-as-posts.md b/plans/relations-as-posts.md index c3eec126..501ed6fc 100644 --- a/plans/relations-as-posts.md +++ b/plans/relations-as-posts.md @@ -116,6 +116,58 @@ relation-subtype closure when relations get subtyped; the boot title cache above - FUTURE: arbitrary predicate constraints (not just required blocks); constraints as their own posts; relation cardinality (`is-a` single-valued?) as a declared constraint. +## Parameterised relations (DESIGN — Slices 6 & 7) + +The next axis: `Relation<…>`. The key reframe is that the obvious parameters aren't separate +``s — they split into **two halves**, and they compose into one coherent thing: + +1. **The role SIGNATURE** (the *shape* of a tuple) — Slice 6 (a + b + c). +2. **The relation's ALGEBRA** (how it *behaves*) — Slice 7 (d). + +A relation is `Relation`, where a signature is an ordered list of **roles**, each +role carrying a **type** and a **cardinality**; the signature's length is the **arity**. +Today's binary typed relations are the degenerate 2-role case — backward-compatible, nothing +gets thrown away. Prior art to borrow (and stay decidable within): Codd / ER reified +relationships (signature), OWL property characteristics (algebra), Datalog / relation algebra +(derived relations — the undecidable frontier; fence it). Decidability rule of thumb: concrete ++ algebraic role-types and counts stay decidable; arbitrary predicates / recursive rules don't. + +### Slice 6 — the role signature (a + b + c) +Generalise the relation-post's `:rel` slot from `{:symmetric :label}` to a `:roles` list — +`{:roles [{:name :type :card} …]}` — driving picker candidates, validation, and arity per-role: +- **(a) per-role type** — each role's `:type` is a type-expr (so it can be algebraic: + `Relation`). The object-role's type IS today's `declares`-anchor — make it + explicit. `valid-object?` becomes per-role `is-a-expr?` against `:type`. +- **(b) arity** = `(len roles)`. Binary stays the fast `src|kind|dst` edge path; **n-ary needs + reification**: a relation *instance* becomes its own post with role edges (`subject→X`, + `object→Y`, `recipient→Z`) — on-brand (we made relation *kinds* posts; now *instances* too), + but a SECOND representation alongside the binary edges, not a tweak. Qualifiers (Wikidata- + style) then come free as extra roles. +- **(c) cardinality** — `:card` per role (min/max; functional = max 1, required = min 1), + enforced on relate by counting. Composes with Slice 5 validation. No model change for binary. +- Siblings: ordered roles (set vs list), keys/identity (which roles identify a tuple). +- **Layering (cheapest → deepest):** (c) cardinality on the binary object-role → (a) explicit + role-type + the 2-role signature abstraction → (b) reified n-ary (the real lift). +- **Variance: nominal, none initially** — no structural subtyping of `Relation<…>` (covariance + of parameterised types is a research project). JIT caveat: 2-role signatures are unrollable; + n-ary role-iteration with per-role reads needs the cache/unroll treatment (Slice 2/5 lesson). + +### Slice 7 — relation algebra / characteristics (d) +The behaviour half — and (d) **transitivity** is special because we ALREADY hardcode it +(`is-a`/`subtype-of` closure via lib/relations); declaring it generically *removes* code. +- **Algebraic properties** declared on the relation-post (`:transitive :symmetric :reflexive + :antisymmetric :irreflexive`), with the closure **derived generically** from them — OWL's + property characteristics. `subtype-of` becomes "a declared transitive + antisymmetric + relation" (a partial order), not a special case. `:symmetric` (already stored) folds in here. +- **Inverse relations** — a real `:inverse` (not just the `:inverse-label` display hint): + relating one auto-derives the converse, the way `:symmetric` writes both directions. +- **Sub-relations** — relations subtyping relations (`wrote subPropertyOf created`): X wrote Y + ⟹ X created Y. Same `subtype-of` machinery, over the `relation` root — meta-circular. +- **Decidable core stops here.** Beyond-d, FENCED: defined-by-rule relations (composition, + `grandparent = parent ∘ parent` — straight onto the Datalog substrate, but gate to + stratified/bounded rules) and cross-role refinement predicates (`start < end`) — both need + the predicate-language-vs-embedded-code decision first. + ## 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