Files
rose-ash/plans/relations-on-sx.md
giles c67aefa211
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m15s
relations: Phase 1 schema + direct relations (rel facts, relate/unrelate, children/parents/related) + 22 tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:32 +00:00

6.0 KiB

relations-on-sx: Cross-domain relationship graph on Datalog

rose-ash's internal relations service tracks parent/child and peer relationships across domains — a blog post's comments, a thread's replies, a product's variants, an order's line items, a resource's containment tree, a federated content's origin. The questions are graph questions: who are X's children? its ancestors? is A reachable from B? what's the chain that connects them? is there a cycle?

That is recursive Datalog in one rule — the same bottom-up reachability acl-on-sx uses for group/resource inheritance. Decisions come with a trace: not just "yes, related," but the path that proves it. relations is an internal-only service (no public URL); other domains call it to resolve hierarchy and linkage.

End-state: a Datalog-on-SX layer for typed relationship facts, with reachability, path explanation, cycle detection, and a federation extension for cross-instance links. Reuses lib/datalog/ — does not reimplement the engine.

Status (rolling)

bash lib/relations/conformance.sh22/22 (Phase 1 complete)

Ground rules

  • Scope: only lib/relations/** and plans/relations-on-sx.md. Do not edit spec/, hosts/, shared/, lib/datalog/**, or other lib/<lang>/. You may import from lib/datalog/ (public API in lib/datalog/datalog.sx); do not copy or modify Datalog.
  • Shared-file issues → "Blockers" with a minimal repro; do not fix here.
  • SX files: sx-tree MCP tools only; sx_validate after every edit.
  • Architecture: relationships are rel(Src, Dst, Kind) Datalog facts; reachability/ancestry are recursive rules; the proof tree is the connecting path; the lifecycle (assert/retract) is an SX layer over the db. Keep relations content-agnostic — a node is an opaque id string; domains own what ids mean.
  • Shared with acl-sx: both run on Datalog and both lean on the same recursive- reachability shape (reach(X,Y) :- edge(X,Y). / reach(X,Y) :- edge(X,Z), reach(Z,Y).). Watch for it; flag convergence in the Progress log, but do not extractplans/mod-on-sx.md records why cross-subsystem extraction waits for the architecture integrator with all consumers in view.
  • Commits: one feature per commit. Keep Progress log updated and tick boxes.

Architecture sketch

relate(src, dst, kind)                  query
        │                                  │
        ▼                                  ▼
lib/relations/schema.sx          lib/relations/engine.sx
  — rel(Src,Dst,Kind) facts         — children/parents/ancestors/descendants
  — kind vocabulary                 — reachable?(A,B), cycle?(X)
        │                                  ▲
        ▼                                  │
lib/relations/api.sx             lib/relations/explain.sx
  — relate / unrelate               — path(A,B): the connecting chain
  — registry over a live db           (from the Datalog derivation)
        │
        ▼
lib/relations/federation.sx
  — cross-instance links via fed-sx (replicated rel facts, peer-trust gated)

Phase 1 — Schema + direct relations

  • lib/relations/schema.sxrel(Src, Dst, Kind) fact projection; a small kind vocabulary (parent, member, reply, variant, origin, …) kept open
  • lib/relations/api.sx(relations/relate src dst kind) / (unrelate …) over a live Datalog db (assert/retract); (children-of db node kind), (parents-of db node kind), (related db node kind)
  • lib/relations/tests/direct.sx — assert/retract, direct children/parents, kind filtering, unknown node → empty
  • lib/relations/conformance.sh + scoreboard

Phase 2 — Reachability + cycles

  • recursive reachability rules: ancestors, descendants, reachable?(A,B) (transitive closure over a kind, the acl inheritance shape)
  • roots / leaves (no parents / no children) for a kind
  • cycle detection: cycle?(X)reachable(X, X); acyclic?(db, kind)
  • lib/relations/tests/reach.sx — deep chains, diamonds, disconnected nodes, self-loops, multi-kind isolation

Phase 3 — Typed relations + path explanation

  • multiple kinds coexisting; mixed-kind vs single-kind reachability
  • lib/relations/explain.sx(path db a b kind) returns the connecting chain (the relationship equivalent of acl's proof tree), nil if unreachable
  • (distance db a b kind) (hops) + shortest-path selection
  • lib/relations/tests/path.sx — path correctness, shortest among many, no-path

Phase 4 — Federation

  • cross-instance relationships — a peer asserts rel(local, remote, kind); replicate rel facts via fed-sx (mock the transport in tests)
  • trust gating — a peer's link binds locally only under a local trust fact (mirror acl's non-transitive trust/gate-in-engine model; do NOT copy acl code, re-derive the shape)
  • revocation — retract a replicated link; reachability re-saturates
  • lib/relations/tests/fed.sx — federated reachability chains, trust gating, revocation

Progress log

  • Phase 1 — schema + direct relations (22/22). schema.sx: rel(Src,Dst,Kind) fact constructor + accessors, open kind vocabulary (parent member reply variant origin link), relations-fact-valid?/relations-known-kind?. api.sx: db built via dl-program-data facts relations-rules (Phase 1 rules empty — direct queries need none); relations-children-of/-parents-of/-related are plain dl-query on the rel relation, plucking the bound column from substitution dicts; current-db convenience layer (relations/load!, relations/relate, relations/unrelate, relations/children/parents/related) over dl-assert!/ dl-retract!, mirroring lib/acl/api.sx. Tests cover direct children/parents, leaf/ root empties, kind isolation (parent query skips reply edge), retract, the api layer, and schema/constructor shape. Note: query result order is nondeterministic — tests use an order-insensitive set=?.

Blockers

(loop fills this in)