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>
237 lines
7.1 KiB
Plaintext
237 lines
7.1 KiB
Plaintext
;; lib/relations/engine.sx — recursive reachability + roots/leaves + cycles +
|
|
;; shape queries.
|
|
;;
|
|
;; 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
|
|
;;
|
|
;; Trust is a body literal, re-checked every query, so revoking trust (or a peer
|
|
;; link) takes effect on the next saturation. Trust is NOT transitive — only a
|
|
;; peer's own links, under a local trust(P) fact, bind. With no peer_rel/trust
|
|
;; facts, erel ≡ rel, so non-federated behaviour is unchanged.
|
|
;;
|
|
;; Reachability is the bottom-up transitive closure acl-on-sx uses for
|
|
;; inheritance, parameterised by Kind so closures never leak across kinds:
|
|
;;
|
|
;; 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) 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
|
|
(quote
|
|
((erel S D K <- (rel S D K))
|
|
(erel S D K <- (peer_rel P S D K) (trust P))
|
|
(reach K X Y <- (erel X Y K))
|
|
(reach K X Y <- (erel X Z K) (reach K Z Y))
|
|
(reach_any X Y <- (erel X Y K))
|
|
(reach_any X Y <- (erel X Z K) (reach_any Z Y))
|
|
(rnode K X <- (erel X Y K))
|
|
(rnode K Y <- (erel X Y K))
|
|
(has_parent K Y <- (erel X Y K))
|
|
(has_child K X <- (erel X Y K))
|
|
(root K X <- (rnode K X) {:neg (has_parent K X)})
|
|
(leaf K X <- (rnode K X) {:neg (has_child K X)}))))
|
|
|
|
;; Pull one column (by keyword key) out of a list of substitution dicts.
|
|
(define
|
|
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
|
|
(fn
|
|
(db node kind)
|
|
(relations-pluck
|
|
(dl-query db (list (quote erel) node (quote Dst) kind))
|
|
:Dst)))
|
|
|
|
;; Direct parents: every Src with an effective edge erel(Src, node, kind).
|
|
(define
|
|
relations-parents-of
|
|
(fn
|
|
(db node kind)
|
|
(relations-pluck
|
|
(dl-query db (list (quote erel) (quote Src) node kind))
|
|
:Src)))
|
|
|
|
;; Directly related: neighbours in either direction under kind.
|
|
(define
|
|
relations-related
|
|
(fn
|
|
(db node kind)
|
|
(append
|
|
(relations-children-of db node kind)
|
|
(relations-parents-of db node kind))))
|
|
|
|
;; Transitive descendants of node under kind (everything reachable forward).
|
|
(define
|
|
relations-descendants
|
|
(fn
|
|
(db node kind)
|
|
(relations-pluck
|
|
(dl-query db (list (quote reach) kind node (quote Y)))
|
|
:Y)))
|
|
|
|
;; Transitive ancestors of node under kind (everything that reaches node).
|
|
(define
|
|
relations-ancestors
|
|
(fn
|
|
(db node kind)
|
|
(relations-pluck
|
|
(dl-query db (list (quote reach) kind (quote X) node))
|
|
:X)))
|
|
|
|
;; Is b reachable from a under kind (transitive)?
|
|
(define
|
|
relations-reachable?
|
|
(fn
|
|
(db a b kind)
|
|
(> (len (dl-query db (list (quote reach) kind a b))) 0)))
|
|
|
|
;; Mixed-kind: descendants reachable from node over edges of ANY kind.
|
|
(define
|
|
relations-descendants-any
|
|
(fn
|
|
(db node)
|
|
(relations-pluck
|
|
(dl-query db (list (quote reach_any) node (quote Y)))
|
|
:Y)))
|
|
|
|
;; Mixed-kind: is b reachable from a over edges of ANY kind?
|
|
(define
|
|
relations-reachable-any?
|
|
(fn
|
|
(db a b)
|
|
(> (len (dl-query db (list (quote reach_any) a b))) 0)))
|
|
|
|
;; Roots: nodes touched by kind with no incoming edge.
|
|
(define
|
|
relations-roots
|
|
(fn
|
|
(db kind)
|
|
(relations-pluck (dl-query db (list (quote root) kind (quote X))) :X)))
|
|
|
|
;; Leaves: nodes touched by kind with no outgoing edge.
|
|
(define
|
|
relations-leaves
|
|
(fn
|
|
(db kind)
|
|
(relations-pluck (dl-query db (list (quote leaf) kind (quote X))) :X)))
|
|
|
|
;; Is node on a cycle under kind (reachable from itself)?
|
|
(define
|
|
relations-cycle?
|
|
(fn
|
|
(db node kind)
|
|
(> (len (dl-query db (list (quote reach) kind node node))) 0)))
|
|
|
|
;; Has the kind any cycle at all? (no node reaches itself)
|
|
(define
|
|
relations-acyclic?
|
|
(fn
|
|
(db kind)
|
|
(=
|
|
(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))))))
|