relations: shape queries (siblings, in/out-degree, undirected connected?) computed in SX + 18 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 31s

Keep the Datalog ruleset minimal — every dl-query re-saturates, so shape
queries are SX BFS over erel, not extra closures. 110/110.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 12:56:35 +00:00
parent 1dacb0c8dd
commit 1c46fc2a69
7 changed files with 306 additions and 17 deletions

View File

@@ -1,8 +1,14 @@
;; lib/relations/engine.sx — recursive reachability + roots/leaves + cycles.
;; lib/relations/engine.sx — recursive reachability + roots/leaves + cycles +
;; shape queries.
;;
;; The whole engine is one Datalog ruleset, derived from the EFFECTIVE relation
;; `erel`, not raw `rel`. `erel` unions local edges with trust-gated federated
;; edges:
;; The Datalog ruleset is deliberately MINIMAL — every dl-query re-saturates it,
;; so each added recursive relation taxes every query in every suite. Reachability
;; (`reach`/`reach_any`), node membership (`rnode`) and root/leaf are the only
;; derived relations; the shape queries (siblings, undirected connectivity) are
;; computed in SX over the fast direct `erel` queries, NOT as extra closures.
;;
;; The ruleset derives from the EFFECTIVE relation `erel`, not raw `rel`. `erel`
;; unions local edges with trust-gated federated edges:
;;
;; erel(S,D,K) :- rel(S,D,K). ; local edge, always
;; erel(S,D,K) :- peer_rel(P,S,D,K), trust(P). ; peer edge, gated by trust
@@ -18,13 +24,11 @@
;; reach(K,X,Y) :- erel(X,Y,K). ; one hop
;; reach(K,X,Y) :- erel(X,Z,K), reach(K,Z,Y). ; transitive
;;
;; `reach_any` is the kind-agnostic closure (any edge, any kind) used for
;; mixed-kind reachability — distinct from single-kind `reach`.
;;
;; rnode collects the nodes touched by a kind; root/leaf are those with no
;; incoming / no outgoing edge (stratified negation over has_parent/has_child).
;; Cycles are ordinary data: `reach(K,X,X)` simply holds for nodes on a cycle —
;; cycle?/acyclic? are queries, not errors. Do not assume a DAG.
;; `reach_any` is the kind-agnostic closure (any edge, any kind) for mixed-kind
;; reachability. rnode collects the nodes touched by a kind; root/leaf are those
;; with no incoming / no outgoing edge (stratified negation). Cycles are ordinary
;; data: `reach(K,X,X)` simply holds for nodes on a cycle — cycle?/acyclic? are
;; queries, not errors. Do not assume a DAG.
(define
relations-rules
@@ -47,6 +51,44 @@
relations-pluck
(fn (substs key) (map (fn (s) (get s key)) substs)))
;; Membership without host-name clashes (schema.sx defines relations-member?,
;; but engine.sx may load before schema in ad-hoc sessions — keep a local copy).
(define
relations-eng-member?
(fn
(x xs)
(cond
((= (len xs) 0) false)
((= (first xs) x) true)
(else (relations-eng-member? x (rest xs))))))
(define
relations-concat-map
(fn
(f xs)
(if
(= (len xs) 0)
(list)
(append (f (first xs)) (relations-concat-map f (rest xs))))))
(define
relations-dedup
(fn
(xs)
(if
(= (len xs) 0)
(list)
(let
((r (relations-dedup (rest xs))))
(if
(relations-eng-member? (first xs) r)
r
(append (list (first xs)) r))))))
(define
relations-without
(fn (x xs) (filter (fn (e) (not (= e x))) xs)))
;; Direct children: every Dst with an effective edge erel(node, Dst, kind).
(define
relations-children-of
@@ -144,3 +186,51 @@
(=
(len (dl-query db (list (quote reach) kind (quote X) (quote X))))
0)))
;; Siblings: nodes sharing a parent with node under kind (excluding node).
;; Computed in SX over direct queries — no extra Datalog closure.
(define
relations-siblings
(fn
(db node kind)
(relations-without
node
(relations-dedup
(relations-concat-map
(fn (p) (relations-children-of db p kind))
(relations-parents-of db node kind))))))
;; Out-degree: number of direct children under kind.
(define
relations-out-degree
(fn (db node kind) (len (relations-children-of db node kind))))
;; In-degree: number of direct parents under kind.
(define
relations-in-degree
(fn (db node kind) (len (relations-parents-of db node kind))))
;; Undirected BFS frontier expansion: grow `visited` by neighbours (either
;; direction) until the frontier is empty. Reuses the fast `erel` queries.
(define
relations-ureach-bfs
(fn
(db kind frontier visited)
(if
(= (len frontier) 0)
visited
(let
((fresh (filter (fn (n) (not (relations-eng-member? n visited))) (relations-dedup (relations-concat-map (fn (n) (relations-related db n kind)) frontier)))))
(relations-ureach-bfs db kind fresh (append visited fresh))))))
;; Weakly connected: a and b joined by a path ignoring edge direction, under
;; kind. (Reflexive — a node is connected to itself.)
(define
relations-connected?
(fn
(db a b kind)
(or
(= a b)
(relations-eng-member?
b
(relations-ureach-bfs db kind (list a) (list a))))))