EE-S1: name the mesh's event subjects in one place (issue 1966)
buscompose gains EveryEvent, EventSubjects, EventName and Overlaps, so the grants part and the tool runner never spell the event namespace themselves. Builds: design 34 every-event §1, §25, §26, §34, §37 (Overlaps rows) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The one place the mesh's event subjects are named (design 34 every-event §1, §25, §26), so neither the grants part
|
||||
// nor the tool runner spells the event namespace itself.
|
||||
|
||||
// The namespaces an event lands in, as `from` names them (design 34 every-event §13, §26).
|
||||
const (
|
||||
fromSeat = "seat"
|
||||
fromModule = "module"
|
||||
fromStream = "stream"
|
||||
)
|
||||
|
||||
// eventNamespaces are the three, each with the prefix its subjects start with, in EveryEvent's order.
|
||||
var eventNamespaces = []struct{ from, prefix string }{
|
||||
{fromSeat, "mesh.seat."},
|
||||
{fromModule, "mesh.mod."},
|
||||
{fromStream, moduleStreamPrefix},
|
||||
}
|
||||
|
||||
// EveryEvent is the subjects the tool runner's account hears: every seat's events, every module's, and every module
|
||||
// stream's, a restricted one included, in that order (novox/hq ADR 0670 §1, ADR 0671 §1).
|
||||
func EveryEvent() []string {
|
||||
out := make([]string, 0, len(eventNamespaces))
|
||||
for _, ns := range eventNamespaces {
|
||||
out = append(out, ns.prefix+"*.event.>")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// eventToken is the form of each token of an event name.
|
||||
var eventToken = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
|
||||
// EventSubjects is the subjects one event name lands on. A name is `<emitter>.<event>`, optionally followed by further
|
||||
// tokens (`operator-channel.decided.claude-code`), each token of the form `[A-Za-z0-9_-]+`. With from empty it answers
|
||||
// six subjects, per namespace (seat, module, stream) `mesh.<ns>.<emitter>.event.<rest>` and the same followed by `.>`;
|
||||
// with from `seat`, `module` or `stream` only that namespace's two. It refuses, naming the rule, a name of fewer than two
|
||||
// tokens, an empty token or a token outside that form (so `*` and `>` are refused), and a from of any other word.
|
||||
func EventSubjects(name, from string) ([]string, error) {
|
||||
tokens := strings.Split(name, ".")
|
||||
if len(tokens) < 2 {
|
||||
return nil, fmt.Errorf("event name %q has fewer than two tokens: a name is <seat or module>.<event>", name)
|
||||
}
|
||||
for _, t := range tokens {
|
||||
if !eventToken.MatchString(t) {
|
||||
return nil, fmt.Errorf("event name %q has the token %q: each token of a name matches [A-Za-z0-9_-]+, "+
|
||||
"so no token is empty and none is * or >", name, t)
|
||||
}
|
||||
}
|
||||
emitter, rest := tokens[0], strings.Join(tokens[1:], ".")
|
||||
var out []string
|
||||
for _, ns := range eventNamespaces {
|
||||
if from != "" && from != ns.from {
|
||||
continue
|
||||
}
|
||||
subject := ns.prefix + emitter + ".event." + rest
|
||||
out = append(out, subject, subject+".>")
|
||||
}
|
||||
if out == nil {
|
||||
return nil, fmt.Errorf("from %q is none of %s, %s or %s", from, fromSeat, fromModule, fromStream)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// EventName is the inverse of EventSubjects for a heard subject: for `mesh.seat.<emitter>.event.<rest>`,
|
||||
// `mesh.mod.<emitter>.event.<rest>` or `mesh.stream.<emitter>.event.<rest>` it answers the name `<emitter>.<rest>` and
|
||||
// its namespace (`seat`, `module` or `stream`); any other subject is answered as not an event (ok false).
|
||||
func EventName(subject string) (name, from string, ok bool) {
|
||||
for _, ns := range eventNamespaces {
|
||||
after, found := strings.CutPrefix(subject, ns.prefix)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
emitter, rest, found := strings.Cut(after, ".event.")
|
||||
if !found || emitter == "" || rest == "" || strings.Contains(emitter, ".") {
|
||||
return "", "", false
|
||||
}
|
||||
return emitter + "." + rest, ns.from, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Overlaps says whether some subject matches both a and b, a wildcard on either side: `*` stands for one token and
|
||||
// `>`, last, for one token or more (design 34 every-event §34).
|
||||
func Overlaps(a, b string) bool {
|
||||
at, bt := strings.Split(a, "."), strings.Split(b, ".")
|
||||
for i := 0; ; i++ {
|
||||
switch {
|
||||
case i == len(at) && i == len(bt):
|
||||
return true
|
||||
case i == len(at) || i == len(bt):
|
||||
return false
|
||||
case at[i] == ">" || bt[i] == ">":
|
||||
return true
|
||||
case at[i] == "*" || bt[i] == "*" || at[i] == bt[i]:
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The mesh's event subjects are named in one place: EveryEvent, EventSubjects, EventName and Overlaps (design 34
|
||||
// every-event §1, §25, §26, §34, §37).
|
||||
func TestTheEventSubjectsAreNamedInOnePlace(t *testing.T) {
|
||||
// EE-S1/AC1 (E §1, §25): the three subjects, in order.
|
||||
if got, want := EveryEvent(), []string{"mesh.seat.*.event.>", "mesh.mod.*.event.>", "mesh.stream.*.event.>"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("EveryEvent is %v, want %v", got, want)
|
||||
}
|
||||
|
||||
// EE-S1/AC2, AC3 (E §1, §26): a name's subjects, all six or one namespace's two, and the refusals naming the rule.
|
||||
subjects := []struct {
|
||||
name, from string
|
||||
want []string
|
||||
refused string
|
||||
}{
|
||||
{name: "operator-channel.decided.claude-code", want: []string{
|
||||
"mesh.seat.operator-channel.event.decided.claude-code", "mesh.seat.operator-channel.event.decided.claude-code.>",
|
||||
"mesh.mod.operator-channel.event.decided.claude-code", "mesh.mod.operator-channel.event.decided.claude-code.>",
|
||||
"mesh.stream.operator-channel.event.decided.claude-code", "mesh.stream.operator-channel.event.decided.claude-code.>",
|
||||
}},
|
||||
{name: "mesh-delivery-view.delivered", from: "seat", want: []string{
|
||||
"mesh.seat.mesh-delivery-view.event.delivered", "mesh.seat.mesh-delivery-view.event.delivered.>"}},
|
||||
{name: "mesh-delivery-view.delivered", from: "module", want: []string{
|
||||
"mesh.mod.mesh-delivery-view.event.delivered", "mesh.mod.mesh-delivery-view.event.delivered.>"}},
|
||||
{name: "mesh-mail.mail", from: "stream", want: []string{
|
||||
"mesh.stream.mesh-mail.event.mail", "mesh.stream.mesh-mail.event.mail.>"}},
|
||||
{name: "mesh-delivery-view", refused: "fewer than two tokens"},
|
||||
{name: "mesh-delivery-view..delivered", refused: "[A-Za-z0-9_-]+"},
|
||||
{name: "mesh-delivery-view.*", refused: "[A-Za-z0-9_-]+"},
|
||||
{name: "mesh-delivery-view.>", refused: "[A-Za-z0-9_-]+"},
|
||||
{name: "mesh-delivery-view.delivered", from: "bench", refused: "none of seat, module or stream"},
|
||||
}
|
||||
for _, c := range subjects {
|
||||
got, err := EventSubjects(c.name, c.from)
|
||||
if c.refused != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), c.refused) {
|
||||
t.Errorf("EventSubjects(%q, %q) answered %v, %v; want refused naming %q", c.name, c.from, got, err, c.refused)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil || !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("EventSubjects(%q, %q) answered %v, %v; want %v", c.name, c.from, got, err, c.want)
|
||||
}
|
||||
// EE-S1/AC5 (E §1): every subject it answers is covered by one of EveryEvent's.
|
||||
for _, s := range got {
|
||||
covered := false
|
||||
for _, e := range EveryEvent() {
|
||||
covered = covered || Overlaps(e, s)
|
||||
}
|
||||
if !covered {
|
||||
t.Errorf("%s is covered by none of EveryEvent", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EE-S1/AC4 (E §1, §26): a heard subject's name and namespace, and any other subject not an event.
|
||||
names := []struct{ subject, name, from string }{
|
||||
{"mesh.seat.operator-channel.event.decided.claude-code", "operator-channel.decided.claude-code", "seat"},
|
||||
{"mesh.mod.mesh-delivery-view.event.delivered", "mesh-delivery-view.delivered", "module"},
|
||||
{"mesh.stream.mesh-delivery-checker.event.verdict", "mesh-delivery-checker.verdict", "stream"},
|
||||
{"mesh.seat.intake.call.accept.ace", "", ""},
|
||||
{"mesh.mod.netcheck.call.netcheck_dns", "", ""},
|
||||
{"$KV.views.x", "", ""},
|
||||
{"mesh.seat..event.x", "", ""},
|
||||
}
|
||||
for _, c := range names {
|
||||
name, from, ok := EventName(c.subject)
|
||||
if name != c.name || from != c.from || ok != (c.name != "") {
|
||||
t.Errorf("EventName(%q) answered %q, %q, %v; want %q, %q", c.subject, name, from, ok, c.name, c.from)
|
||||
}
|
||||
}
|
||||
|
||||
// EE-S1/AC6 (E §34, §37): overlapping subjects, a wildcard on either side.
|
||||
overlaps := []struct {
|
||||
a, b string
|
||||
want bool
|
||||
}{
|
||||
{"mesh.stream.mail.event.mail.>", "mesh.stream.mail.event.mail.*.message", true},
|
||||
{"mesh.mod.mail.event.x", "mesh.stream.mail.event.x", false},
|
||||
{"mesh.mod.mail.event.x.>", "mesh.mod.mail.event.x", false},
|
||||
{"mesh.mod.mail.event.*", "mesh.mod.*.event.x", true},
|
||||
{"mesh.mod.mail.event.x", "mesh.mod.mail.event.x", true},
|
||||
{"mesh.mod.*.event.>", "mesh.mod.mail.event.x.ace", true},
|
||||
{"mesh.mod.mail.event.x.ace", "mesh.mod.mail.event.x", false},
|
||||
}
|
||||
for _, c := range overlaps {
|
||||
if Overlaps(c.a, c.b) != c.want || Overlaps(c.b, c.a) != c.want {
|
||||
t.Errorf("Overlaps(%q, %q) is not %v both ways", c.a, c.b, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user