From 293df4937758834670905bff28aee4cf60c15111 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:53:19 +0100 Subject: [PATCH] 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. 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 --- go/buscompose/names_test.go | 42 ++++---------------------------- go/seatdef/seatdef.go | 48 +++++++++---------------------------- go/seatdef/seatdef_test.go | 25 +++++-------------- 3 files changed, 21 insertions(+), 94 deletions(-) diff --git a/go/buscompose/names_test.go b/go/buscompose/names_test.go index 6f4bd17..6a31678 100644 --- a/go/buscompose/names_test.go +++ b/go/buscompose/names_test.go @@ -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) -} diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index c0b37ae..50f0da4 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -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..accept.`. Accepts []Message `json:"accepts,omitempty"` - // Events are what is published on the seat's own subject, `mesh.seat...` (EventSubject): the - // token is `event`, unless the family names its own. + // Events are what is published on the seat's own subject, `mesh.seat..event.`. 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 } diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index 9c9a52f..7f27a8f 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -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 {