Self-referential many-to-many: acyclicity and a materialized transitive closure #151

Open
opened 2026-08-02 06:30:29 +00:00 by PerishFire · 0 comments
Owner

K1 — Self-referential many-to-many: acyclicity and a materialized transitive closure

Status: draft, ready to file on PerishLab/keel. Unblocks Phase 1 of the
codehull identity seam. Two clauses, nothing else — this is the third
version of this issue; the two earlier ones were wrong in ways recorded at the
bottom, and both went wrong by widening.

Motivation

Ensign is about to model organisations as a single self-referential
many-to-many on Actor: an org's members may themselves be orgs. Teams stop
being their own unit and become nested orgs. A token then carries the closure
of the orgs its subject belongs to, and the consuming forge evaluates ordinary
grants against that claim.

Two engine properties are missing for that to be sound.

1. An acyclicity invariant on a self-referential many-to-many

A tie that would close a cycle must be refused at write time.

This has to be the engine's, not the caller's. An invariant that arrives later
can only reject new cycles; it cannot repair the ones already stored. A
ceremony-level check in one caller is also not the same guarantee: any other
writer through any other Face bypasses it.

2. A materialized transitive closure

A declared self-referential many-to-many gains an engine-owned closure
relation, refreshed as part of the same write that changes a tie. Grants then
read it as an ordinary pred over a normal relation — no new scope grammar,
no new capability verb.

It must be maintained inside the write transaction, and must not be a
trigger consumer.
docs/run/trigger.md is post-commit and observe-only and
forbids consumer participation in the write path — correctly. But a grant that
evaluates against an asynchronously refreshed closure makes authorization
eventually consistent: a revoked membership stays effective for the length of
the delivery lag. Consistency here is not a performance preference.

This would be the engine's first push-side derived structure. Today the engine
is pull-only and depth-1: bond bags are the live closure of the current root
bag, forward bonds only, and deep graphs are explicitly told to use another
top-level query (docs/model/edge.md).

Explicitly not asked for

  • Reachability as a scope. Evaluating authorization by walking the graph
    per request is exactly why nested-group ACLs in LDAP are famously slow. The
    closure is a table to be read, not a traversal to be run.
  • Any change to grant grammar, capability verbs, or the edge DSL.
  • A general derived-relation facility. That is a separate issue (K2); keeping
    it out of this one is deliberate.

Two earlier versions of this issue, both wrong

  • "An authorized operation implies disclose on its operands." This patched
    permissions onto the wrong operation. The answer was a consent model in the
    product, not a widening in the engine.
  • "birth targeted at a named subject." Already solved: run/face/grant.rs
    mint() grants authority to the creating operator in the same transaction.

Recording these so a fourth version does not rediscover them.

# K1 — Self-referential many-to-many: acyclicity and a materialized transitive closure Status: draft, ready to file on PerishLab/keel. Unblocks Phase 1 of the codehull identity seam. **Two clauses, nothing else** — this is the third version of this issue; the two earlier ones were wrong in ways recorded at the bottom, and both went wrong by widening. ## Motivation Ensign is about to model organisations as a single self-referential many-to-many on `Actor`: an org's members may themselves be orgs. Teams stop being their own unit and become nested orgs. A token then carries the closure of the orgs its subject belongs to, and the consuming forge evaluates ordinary grants against that claim. Two engine properties are missing for that to be sound. ## 1. An acyclicity invariant on a self-referential many-to-many A tie that would close a cycle must be refused at write time. This has to be the engine's, not the caller's. An invariant that arrives later can only reject **new** cycles; it cannot repair the ones already stored. A ceremony-level check in one caller is also not the same guarantee: any other writer through any other Face bypasses it. ## 2. A materialized transitive closure A declared self-referential many-to-many gains an engine-owned closure relation, refreshed as part of the same write that changes a tie. Grants then read it as an ordinary `pred` over a normal relation — no new scope grammar, no new capability verb. **It must be maintained inside the write transaction, and must not be a trigger consumer.** `docs/run/trigger.md` is post-commit and observe-only and forbids consumer participation in the write path — correctly. But a grant that evaluates against an asynchronously refreshed closure makes *authorization* eventually consistent: a revoked membership stays effective for the length of the delivery lag. Consistency here is not a performance preference. This would be the engine's first push-side derived structure. Today the engine is pull-only and depth-1: bond bags are the live closure of the current root bag, forward bonds only, and deep graphs are explicitly told to use another top-level query (`docs/model/edge.md`). ## Explicitly not asked for - **Reachability as a scope.** Evaluating authorization by walking the graph per request is exactly why nested-group ACLs in LDAP are famously slow. The closure is a table to be read, not a traversal to be run. - Any change to grant grammar, capability verbs, or the edge DSL. - A general derived-relation facility. That is a separate issue (K2); keeping it out of this one is deliberate. ## Two earlier versions of this issue, both wrong - *"An authorized operation implies `disclose` on its operands."* This patched permissions onto the wrong operation. The answer was a consent model in the product, not a widening in the engine. - *"`birth` targeted at a named subject."* Already solved: `run/face/grant.rs` `mint()` grants authority to the creating operator in the same transaction. Recording these so a fourth version does not rediscover them.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
PerishLab/keel#151
No description provided.