Grants part: the unread seat list never holds the part's own module or the holder of the seat it is read through (ADR 0636 §1; issue 1966)

This commit is contained in:
2026-11-05 16:10:48 +01:00
parent e2deb78caf
commit ead87c4c90
2 changed files with 37 additions and 1 deletions
+27
View File
@@ -2256,6 +2256,33 @@ func TestGrants(t *testing.T) {
}
})
// What the part needs to hear the seat and read the list is never held by the unread list (ADR 0636 §1; the lead's ruling
// of 2026-11-05): its own module (its consumer on EVENTS, its grant of the seat's verb) and the module that holds the seat
// the list is read through, whatever other seats their manifests name.
t.Run("the seat list unread never holds the part's own module or the holder of the seat it is read through", func(t *testing.T) {
b := fixture()
own, err := os.ReadFile("module.json")
if err != nil {
t.Fatal(err)
}
b.manifestOf(grants.Module, string(own))
b.manifestOf("cat-holder", `{"module":"cat-holder","version":"1","claims":[{"name":"mesh-catalog"}],"invokes":["seat:mesh-vault.account-issue"]}`)
b.assign("a", grants.Module)
b.assign("a", "cat-holder")
p := partOn(b)
p.SeatList = &seatGate{} // nothing is bootstrapped: only the exemption lets these two through
p.Compose()
refused := strings.Join(p.Last().Refused, "\n")
for _, module := range []string{grants.Module, "cat-holder"} {
if strings.Contains(refused, "manifest of "+module+":") {
t.Errorf("%s is held while the seat list is unread: %v", module, p.Last().Refused)
}
}
if !strings.Contains(refused, "manifest of mod-x: it waits for the seat list") {
t.Errorf("another module is not held: %v", p.Last().Refused)
}
})
// DLV-G1, 1966/AC32-AC33 (novox/hq ADR 0595 §1, §7; design 25 § 12.2): the bus module's created writes the binding into
// the module's membership on every machine it is assigned, one assigned later included; recreated rewrites it; dropped
// removes it; before created, on a refused, or from an emitter holding no mesh-broker, none.
+10 -1
View File
@@ -17,6 +17,7 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -219,6 +220,14 @@ type SeatGate interface {
MayAct(seats ...string) bool
}
// readsTheList says whether a module is what the part needs to hear the seat and read the list: the part's own module (its
// consumer on EVENTS, its grant of the seat's verb and events) and the holder of the seat the list is read through. The unread
// list never holds these, whatever other seats their manifests name: held, the seat's event could not reach the part and
// the list would never be read (ADR 0636 §1).
func readsTheList(module string, m catalogue.Manifest) bool {
return module == Module || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == meshcatalog.Seat })
}
// SeatsOf are the seats a manifest claims or invokes, which the seat list must name before the part composes the module.
func SeatsOf(m catalogue.Manifest) []string {
var seats []string
@@ -422,7 +431,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
if err == nil {
m, err = cache.get(r, v.Manifest)
}
if err == nil && gate != nil && !gate.MayAct(SeatsOf(m)...) {
if err == nil && gate != nil && !readsTheList(module, m) && !gate.MayAct(SeatsOf(m)...) {
err = errors.New("it waits for the seat list: the list has not been read yet and the manifest names seats the bootstrapped list does not (novox/hq ADR 0636 §1)")
}
if err != nil {