Merge branch 'feat/47-G-build-bus' into feat/47-G-build

This commit is contained in:
2026-11-11 14:32:02 +01:00
4 changed files with 131 additions and 0 deletions
@@ -0,0 +1,57 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"nats-tools/internal/keeper"
)
// The keeper reads a module's asks from the module's entry in the catalog half of its revision view, not from the
// indexer's event (design 47 G §344, §433).
// Asks is what one catalog graph entry says of a module for the keeper: what its manifest asks of the bus as an accepted
// verdict, the seats it claims, and whether it declares an event of its own (the facts keeper.Sends reads).
type Asks struct {
Accepted keeper.Accepted
Claims []string
EmitsOwn bool
}
// AsksOf reads one entry. An entry whose manifest is not accepted asks nothing of the bus now, as an accepted verdict of
// no entries does; the digest it carries is of the entries it asks, so an entry changed in nothing the bus module reads
// is not applied again.
func AsksOf(e catalog.CatalogEntry) (Asks, error) {
if e.Module == "" {
return Asks{}, fmt.Errorf("a catalog entry names no module")
}
raw, err := json.Marshal(e.Manifest)
if err != nil {
return Asks{}, fmt.Errorf("the manifest of %s is not readable: %w", e.Module, err)
}
var m catalogue.Manifest
if err := json.Unmarshal(raw, &m); err != nil {
return Asks{}, fmt.Errorf("the manifest of %s is not readable: %w", e.Module, err)
}
state := m.BusState
if e.Verdict != "" && e.Verdict != "accepted" {
state = nil
}
sum := sha256.Sum256(mustJSON(state))
out := Asks{Accepted: keeper.Accepted{Module: e.Module, Digest: hex.EncodeToString(sum[:]), BusState: state},
EmitsOwn: len(m.Emits) > 0 || len(e.Streams) > 0}
for _, c := range m.Claims {
out.Claims = append(out.Claims, c.Name)
}
return out, nil
}
func mustJSON(v any) []byte {
b, _ := json.Marshal(v)
return b
}
@@ -0,0 +1,28 @@
package main
import (
"testing"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
func TestAsksOf(t *testing.T) {
m := map[string]any{"module": "m", "claims": []any{map[string]any{"name": "seat-a"}}, "emits": []any{"done"}}
a, err := AsksOf(catalog.CatalogEntry{Module: "m", Verdict: "accepted", Manifest: m})
if err != nil {
t.Fatal(err)
}
if len(a.Claims) != 1 || a.Claims[0] != "seat-a" || !a.EmitsOwn || a.Accepted.Module != "m" || a.Accepted.Digest == "" {
t.Errorf("read %+v", a)
}
b, _ := AsksOf(catalog.CatalogEntry{Module: "m", Manifest: map[string]any{"module": "m"}})
if b.EmitsOwn || len(b.Claims) != 0 {
t.Errorf("an entry with neither: %+v", b)
}
if b.Accepted.Digest != a.Accepted.Digest {
t.Errorf("the digest is of the bus-state asked alone: %s vs %s", b.Accepted.Digest, a.Accepted.Digest)
}
if _, err := AsksOf(catalog.CatalogEntry{}); err == nil {
t.Error("an entry with no module was read")
}
}
@@ -14,6 +14,7 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// fakeBus holds objects by stream, as a bus would, and refuses what a test tells it to.
@@ -794,3 +795,31 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) {
})
}
}
func TestSends(t *testing.T) {
defs := map[string]seatdef.Definition{
"verbed": {Verbs: []seatdef.Verb{{Name: "go"}}},
"evented": {Events: []seatdef.Message{{Name: "done"}}},
"included": {Includes: []string{"part"}},
"bare": {},
}
for _, c := range []struct {
name string
claims []string
own bool
want bool
}{
{"a seat with a verb", []string{"verbed"}, false, true},
{"a seat with an event", []string{"evented"}, false, true},
{"a seat that includes parts", []string{"included"}, false, true},
{"a seat with neither", []string{"bare"}, false, false},
{"a seat the list does not hold", []string{"unknown"}, false, false},
{"an event of its own", nil, true, true},
{"nothing claimed", nil, false, false},
{"one of two claims sends", []string{"bare", "verbed"}, false, true},
} {
if got := Sends(c.claims, c.own, defs); got != c.want {
t.Errorf("%s: Sends = %v, want %v", c.name, got, c.want)
}
}
}
+17
View File
@@ -16,6 +16,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/membership"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// The kinds of bus object an entry is made of: a stream, a key-value bucket (stream KV_<name>) and an object store
@@ -48,6 +49,22 @@ const (
SentAgeDays = 7
)
// Sends says whether a module sends (design 47 G §344): it claims a seat whose definition has a verb or an event, or it
// declares an event of its own (emitsOwn). claims are the seats its catalog graph entry claims, defs the seat list's
// definitions by seat; a claimed seat the list does not hold counts for nothing. A definition that includes parts takes
// their verbs and events whole (seatdef.Resolve), so one counts as having some.
func Sends(claims []string, emitsOwn bool, defs map[string]seatdef.Definition) bool {
if emitsOwn {
return true
}
for _, c := range claims {
if d, ok := defs[c]; ok && (len(d.Verbs) > 0 || len(d.Events) > 0 || len(d.Includes) > 0) {
return true
}
}
return false
}
// Settings are one bus object's settings, as asked and as held: what check compares.
type Settings struct {
// MaxBytes is its cap in bytes; MaxAgeSeconds its age, 0 for none.