Main reverted G's half merge (#189, reverted by8afc3ebin #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 headf30dee54only 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 atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
107 lines
3.9 KiB
Go
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
|
|
}
|
|
}
|
|
}
|