SD-M2b sdk: a seat's event family may name its own subject token

The lead's ruling of 12:5x (design 25 § 12.3): mesh-broker's events ride the bus module's own stream, 30 days,
on mesh.seat.mesh-broker.said.<event>, outside the shared events stream's mesh.seat.*.event.>, so the bus never
refuses the two as overlapping.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 12:42:16 +01:00
parent 3ff694e425
commit e1f4ac1050
3 changed files with 76 additions and 2 deletions
+25
View File
@@ -1,8 +1,10 @@
package buscompose
import (
"strings"
"testing"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"git.novox.be/novox/mesh-sdk/go/membership"
)
@@ -32,4 +34,27 @@ 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.
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)
}
}
}
}
// 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)
}
+34 -2
View File
@@ -67,7 +67,8 @@ 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>.event.<name>`.
// 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 []Message `json:"events,omitempty"`
// Family is where the seat's events are kept, when the definition fixes it.
Family *Family `json:"family,omitempty"`
@@ -122,6 +123,18 @@ 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
}
// Record is one state the holder keeps for the seat: its key's form and its value's schema.
@@ -377,6 +390,14 @@ func (d Definition) check() Problems {
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)})
}
}
for i, e := range d.HolderEvents {
where := fmt.Sprintf("holder-events[%d]", i)
@@ -496,9 +517,20 @@ func Resolve(d Definition, parts map[string]Part) (Definition, error) {
return out, nil
}
// EventSubject is where the seat's event is published; by a caller, the caller's name follows.
// 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.
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 }
+17
View File
@@ -37,6 +37,11 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
`"name":"created"`, 1), "events[0].body: missing"},
{"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":30}`,
"", 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 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"},
@@ -89,6 +94,18 @@ 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 {