SD-M1 sdk: a seat's family is the shared events stream's, 7 days, no cap of its own

The lead's ruling of 13:0x replaces 12:5x's said-subject: mesh-broker's events follow every seat, on
mesh.seat.mesh-broker.event.<event> in the shared events stream, 7 days; the bus module keeps a longer history on a
stream of its own module. So every seat family is that stream filtered to the seat's subjects: a definition says 7
days and no cap of its own, and the reader refuses anything else.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 12:53:19 +01:00
parent fc50486190
commit 293df49377
3 changed files with 21 additions and 94 deletions
+4 -38
View File
@@ -1,16 +1,10 @@
package buscompose
import (
"strings"
"testing"
"git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
"git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry"
"git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/membership"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// The names this package keeps for the grants part's composer (mesh-grants internal/buscompose, ADR 0593 §1) agree with
@@ -39,38 +33,10 @@ func TestTheBusNamesAgree(t *testing.T) {
if err := ledger.Valid(); err != nil || ledger.OwnSecret() != "ledgers" {
t.Fatalf("a wanted ledger account: %v, held in %q", err, ledger.OwnSecret())
}
// mesh-broker's events are the bus module's own stream (design 25 § 12.3, 30 days): no subject of the shared
// streams may take them, or the bus refuses the two streams as overlapping.
// A seat's family is the shared events stream filtered to its subjects: a definition says the stream's age.
for _, st := range MeshStreams() {
for _, pattern := range st.Subjects {
if subjectTakes(pattern, meshbroker.SubjectCreated) {
t.Errorf("the stream %s takes mesh-broker's %s by %s", st.Name, meshbroker.SubjectCreated, pattern)
}
}
}
// A seat family on the shared events stream is kept as long as the stream keeps it: its contract says no more.
for family, days := range map[string]int{operatorchannel.Family: operatorchannel.FamilyMaxAgeDays,
npmpackageregistry.Family: npmpackageregistry.FamilyMaxAgeDays, gopackageregistry.Family: gopackageregistry.FamilyMaxAgeDays,
containerimageregistry.Family: containerimageregistry.FamilyMaxAgeDays,
ociartifactregistry.Family: ociartifactregistry.FamilyMaxAgeDays} {
for _, st := range MeshStreams() {
if st.Name == EventsStream && int64(days)*24*60*60 != int64(st.MaxAge) {
t.Errorf("the family %s says %d days, the shared events stream keeps %d s", family, days, st.MaxAge)
}
if st.Name == EventsStream && int64(st.MaxAge) != seatdef.SharedEventsMaxAgeDays*24*60*60 {
t.Errorf("the shared events stream keeps %d s, a seat's definition says %d days", st.MaxAge, seatdef.SharedEventsMaxAgeDays)
}
}
}
// subjectTakes says whether a subject pattern (`*` one token, `>` the rest) takes a subject.
func subjectTakes(pattern, subject string) bool {
p, s := strings.Split(pattern, "."), strings.Split(subject, ".")
for i, tok := range p {
switch {
case tok == ">":
return len(s) > i
case i >= len(s) || (tok != "*" && tok != s[i]):
return false
}
}
return len(p) == len(s)
}
+11 -37
View File
@@ -67,8 +67,7 @@ type Definition struct {
Verbs []Verb `json:"verbs,omitempty"`
// Accepts are what may be sent to the seat's holder on `mesh.seat.<seat>.accept.<name>`.
Accepts []Message `json:"accepts,omitempty"`
// Events are what is published on the seat's own subject, `mesh.seat.<seat>.<token>.<name>` (EventSubject): the
// token is `event`, unless the family names its own.
// Events are what is published on the seat's own subject, `mesh.seat.<seat>.event.<name>`.
Events []Message `json:"events,omitempty"`
// Family is where the seat's events are kept, when the definition fixes it.
Family *Family `json:"family,omitempty"`
@@ -118,24 +117,17 @@ type HolderEvent struct {
Body map[string]any `json:"body,omitempty"`
}
// Family is where a seat's events are kept.
// Family is where a seat's events are kept: the shared events stream filtered to the seat's subjects, so its age is
// the stream's (SharedEventsMaxAgeDays) and it has no cap of its own (the lead's ruling of 2026-11-04 13:0x: a
// holder that keeps a longer history keeps it in a stream of its own module).
type Family struct {
Name string `json:"name"`
MaxAgeDays int `json:"max-age-days"`
MaxMiB int `json:"max-mib,omitempty"`
// Token is the subject's word after the seat's name, `event` when absent. A family kept in a stream of its own
// names another, so its subjects stay outside the shared events stream's `mesh.seat.*.event.>`: the bus refuses
// two streams whose subjects overlap (mesh-broker's `said`, design 25 § 12.3).
Token string `json:"token,omitempty"`
}
// EventToken is the token after the seat's name on the subjects of its events.
func (f *Family) EventToken() string {
if f == nil || f.Token == "" {
return "event"
}
return f.Token
}
// SharedEventsMaxAgeDays is how long the shared events stream keeps a seat's events (mesh-sdk buscompose's
// EventsStream, whose test holds the two equal).
const SharedEventsMaxAgeDays = 7
// Record is one state the holder keeps for the seat: its key's form and its value's schema.
type Record struct {
@@ -387,16 +379,9 @@ func (d Definition) check() Problems {
if d.Family.Name == "" {
ps = append(ps, Problem{"family.name", "missing"})
}
if d.Family.MaxAgeDays < 1 {
ps = append(ps, Problem{"family.max-age-days", "missing, or under one day"})
}
switch t := d.Family.Token; {
case t == "":
case !wordName.MatchString(t):
ps = append(ps, Problem{"family.token", fmt.Sprintf("%q is not a lower-case word", t)})
case sharedTokens[t]:
ps = append(ps, Problem{"family.token", fmt.Sprintf("%q is a token every seat shares; "+
"a family of its own names another, or none", t)})
if d.Family.MaxAgeDays != SharedEventsMaxAgeDays {
ps = append(ps, Problem{"family.max-age-days", fmt.Sprintf("missing, or not %d: a seat's events ride the "+
"shared events stream, which keeps them %d days", SharedEventsMaxAgeDays, SharedEventsMaxAgeDays)})
}
}
for i, e := range d.HolderEvents {
@@ -517,20 +502,9 @@ func Resolve(d Definition, parts map[string]Part) (Definition, error) {
return out, nil
}
// EventSubject is where the seat's event is published on the shared events stream's token; by a caller, the
// caller's name follows. A definition whose family names a token of its own is asked with Definition.EventSubject.
// EventSubject is where the seat's event is published; by a caller, the caller's name follows.
func EventSubject(seat, event string) string { return "mesh.seat." + seat + ".event." + event }
// EventSubject is where this seat's event is published, under its family's token; by a caller, the caller's name
// follows.
func (d Definition) EventSubject(event string) string {
return "mesh.seat." + d.Seat + "." + d.Family.EventToken() + "." + event
}
// sharedTokens are the words after a seat's name that every seat's subjects share (its verbs, its work queue, the
// shared events stream's events): a family's own token is none of them.
var sharedTokens = map[string]bool{"event": true, "accept": true, "tool": true, "call": true, "proof": true}
// AcceptSubject is where a message the seat accepts is sent.
func AcceptSubject(seat, accept string) string { return "mesh.seat." + seat + ".accept." + accept }
+6 -19
View File
@@ -13,7 +13,7 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
"verbs":[{"name":"add","input":{"type":"object"}}]}`
const whole = `{"format":1,"seat":"mesh-broker","scope":"mesh","grain":"whole","decision":"ADR 1",
"verbs":[{"name":"list","input":{"type":"object"},"answer":{"type":"object"}}],
"events":[{"name":"created","body":{"type":"object"}}],"family":{"name":"broker.events","max-age-days":30}}`
"events":[{"name":"created","body":{"type":"object"}}],"family":{"name":"broker.events","max-age-days":7}}`
cases := []struct {
name, raw string
refused string // a text the refusal names, empty when it parses
@@ -35,13 +35,12 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
"verbs[0].input: missing"},
{"a whole event with no body", strings.Replace(whole, `"name":"created","body":{"type":"object"}`,
`"name":"created"`, 1), "events[0].body: missing"},
{"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":30}`,
{"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":7}`,
"", 1), "family: missing"},
{"a family's own token parses", strings.Replace(whole, `"max-age-days":30}`, `"max-age-days":30,"token":"said"}`, 1), ""},
{"a family's token shared by every seat", strings.Replace(whole, `"max-age-days":30}`,
`"max-age-days":30,"token":"event"}`, 1), "family.token: \"event\" is a token every seat shares"},
{"a family's token not a word", strings.Replace(whole, `"max-age-days":30}`,
`"max-age-days":30,"token":"sa.id"}`, 1), "family.token"},
{"a family kept longer than the shared events stream", strings.Replace(whole, `"max-age-days":7`,
`"max-age-days":30`, 1), "family.max-age-days: missing, or not 7"},
{"a family with a cap of its own", strings.Replace(whole, `"max-age-days":7`, `"max-age-days":7,"max-mib":256`, 1),
"max-mib: a field the format does not know"},
{"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`},
{"records at the seat list's grain", strings.Replace(listGrain, `"decision":"ADR 1",`,
`"decision":"ADR 1","records":[{"name":"asks"}],`, 1), "grain: seat-list holds"},
@@ -94,18 +93,6 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
t.Errorf("a type defined twice: %v", err)
}
// An event's subject takes its family's token: the shared events stream's `event`, or the family's own.
for _, c := range []struct{ token, want string }{{"", "mesh.seat.mesh-broker.event.created"},
{"said", "mesh.seat.mesh-broker.said.created"}} {
d, err := Parse([]byte(strings.Replace(whole, `"max-age-days":30}`, `"max-age-days":30,"token":"`+c.token+`"}`, 1)))
if err != nil {
t.Fatal(err)
}
if got := d.EventSubject("created"); got != c.want {
t.Errorf("token %q: subject %q, want %q", c.token, got, c.want)
}
}
// Fixtures name what they are an instance of.
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list.input","given":{}},{"name":"b","of":"event:created",
"subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil {