Files
mesh-sdk/go/buscompose/events.go
T
jschoubben 5fa64be894 Revert the revert of G's mesh-sdk merge (#191): G's sdk stands again over reader release 3
Main reverted G's half merge (#189, reverted by 8afc3eb in #191) and then took the reader release's S1-S5, which
copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the
release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's
empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's head f30dee54 only by those and
the release's two tests (objects cap option, seat states and stream kinds).

Builds: design 47 G (the milestone's sdk part, as at f30dee54)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 12:49:56 +01:00

107 lines
3.9 KiB
Go

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
}
}
}