diff --git a/go/catalogue/seat_invokes.go b/go/catalogue/seat_invokes.go new file mode 100644 index 0000000..583ad4e --- /dev/null +++ b/go/catalogue/seat_invokes.go @@ -0,0 +1,89 @@ +package catalogue + +import ( + "fmt" + "slices" + "strings" +) + +// A seat verb a module invokes is one some holder serves (issue 774, sweep item 1). messenger invoked +// `seat:node-engine.root-free`, which nothing in the mesh answered, so every ask that offers an approval was held on +// Telegram, silently. A manifest names what it calls (novox/hq ADR 0152); that a seat by that name exists, promises +// that verb, and has a holder is only known over the whole catalogue, so it is judged here, beside the seats' other +// rules (CatalogueProblems). + +// UnheldSeat ends the problem of a `seat:.` whose seat no module given claims: over the whole catalogue a +// refusal, over some manifests alone a seat whose holder was not given (IsUndeclaredSeat says both). +const UnheldSeat = "which no module claims, so nothing answers it" + +// SeatInvoke splits an invoke `seat:.` into its seat and verb; ok is false for anything else (a module's +// tool, `*`). +func SeatInvoke(invoke string) (seat, verb string, ok bool) { + rest, isSeat := strings.CutPrefix(invoke, "seat:") + if !isSeat { + return "", "", false + } + seat, verb, ok = strings.Cut(rest, ".") + return seat, verb, ok && seat != "" && verb != "" +} + +// seatInvokeProblems are the `seat:.` a module or one of its processes invokes that nothing answers: a seat +// no module declares and the mesh does not define, a verb the seat does not promise, or a seat with no holder (a +// mesh seat no module claims, unless its node's engine holds it, ADR 0563 ยง1). +func seatInvokeProblems(shelf Shelf, declared map[string]SeatDeclaration) []string { + claimed := map[string]bool{} + for _, m := range shelf { + for _, c := range m.Claims { + claimed[c.Name] = true + if s, ok := SeatNamed(c.Name); ok { + claimed[s.Name] = true + } + } + } + var problems []string + for _, module := range shelfOrder(shelf) { + m := shelf[module] + for _, invoke := range invokesOf(m) { + seat, verb, ok := SeatInvoke(invoke) + if !ok { + continue + } + var verbs []Verb + held := claimed[seat] + if s, isMesh := SeatNamed(seat); isMesh { + verbs, held = s.Serves, held || claimed[s.Name] || s.HeldByTheEngine + } else if d, isModule := declared[seat]; isModule { + verbs = d.Serves + } else { + problems = append(problems, fmt.Sprintf("%s invokes %s, a verb of the seat %q, "+UndeclaredSeat, module, invoke, seat)) + continue + } + if !slices.ContainsFunc(verbs, func(v Verb) bool { return v.Name == verb }) { + problems = append(problems, fmt.Sprintf("%s invokes %s, and the seat %s serves no verb %q", module, invoke, seat, verb)) + continue + } + if !held { + problems = append(problems, fmt.Sprintf("%s invokes %s, a verb of the seat %q, "+UnheldSeat, module, invoke, seat)) + } + } + } + return problems +} + +// invokesOf is everything a module says it invokes: its own and each process's bus, once each. +func invokesOf(m Manifest) []string { + out := append([]string(nil), m.Invokes...) + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) != "process" { + continue + } + if _, present := r["bus"]; !present { + continue + } + if b, _, err := processBusOf(r); err == nil { + out = append(out, b.Invokes...) + } + } + slices.Sort(out) + return slices.Compact(out) +} diff --git a/go/catalogue/seats_declared.go b/go/catalogue/seats_declared.go index b8342e7..94f0ffa 100644 --- a/go/catalogue/seats_declared.go +++ b/go/catalogue/seats_declared.go @@ -272,7 +272,9 @@ type Shelf map[string]Manifest const UndeclaredSeat = "which no module declares and the mesh does not define" // IsUndeclaredSeat says whether a problem CatalogueProblems gave is a seat no manifest given declares. -func IsUndeclaredSeat(problem string) bool { return strings.HasSuffix(problem, UndeclaredSeat) } +func IsUndeclaredSeat(problem string) bool { + return strings.HasSuffix(problem, UndeclaredSeat) || strings.HasSuffix(problem, UnheldSeat) +} func CatalogueProblems(shelf Shelf) []string { var problems []string @@ -349,6 +351,9 @@ func CatalogueProblems(shelf Shelf) []string { } } + // A seat verb a module invokes is one some holder serves (issue 774). + problems = append(problems, seatInvokeProblems(shelf, declared)...) + // Which seats name each path (novox/hq ADR 0287): a module's seat naming one another seat names loses. named, pathProblems := pathSeats(shelfOrder(shelf), shelf, declaredBy) problems = append(problems, pathProblems...) diff --git a/go/catalogue/seats_declared_test.go b/go/catalogue/seats_declared_test.go index 25a8d0b..fdef894 100644 --- a/go/catalogue/seats_declared_test.go +++ b/go/catalogue/seats_declared_test.go @@ -51,12 +51,39 @@ func TestTwoModulesCannotDeclareTheSameSeat(t *testing.T) { } // Where ADR 0110's guarantee lands under a derived set: a typo is refused, not resolved to -// nothing at runtime. +// nothing at runtime. And a seat verb a module invokes is one a holder serves (issue 774, sweep item 1: messenger +// invoked a verb nothing answered, and Telegram held every ask silently). func TestUsingASeatNobodyDeclaresIsRefused(t *testing.T) { - shop := Manifest{Module: "shop", Uses: []string{"telegram-sendr"}} - got := problemsFor(t, Shelf{"telegram": telegram(), "shop": shop}) - if !strings.Contains(got, "telegram-sendr") || !strings.Contains(got, "no module declares") { - t.Fatalf("a misspelled seat was accepted: %s", got) + unclaimed := telegram() + unclaimed.Claims = nil + for _, row := range []struct { + name, want string + shop Manifest + seat Manifest + note bool // over some manifests alone it is a note (IsUndeclaredSeat) + }{ + {"a misspelled seat used", `uses the seat "telegram-sendr", ` + UndeclaredSeat, + Manifest{Module: "shop", Uses: []string{"telegram-sendr"}}, telegram(), true}, + {"a declared verb invoked of a held seat", "", Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.status"}}, telegram(), false}, + {"a verb the seat does not promise", `the seat telegram-sender serves no verb "resend"`, + Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.resend"}}, telegram(), false}, + {"a verb of a misspelled seat", `a verb of the seat "telegram-sendr", ` + UndeclaredSeat, + Manifest{Module: "shop", Invokes: []string{"seat:telegram-sendr.status"}}, telegram(), true}, + {"a verb of a declared seat nobody claims", `a verb of the seat "telegram-sender", ` + UnheldSeat, + Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.status"}}, unclaimed, true}, + {"a verb of a mesh seat nobody claims", `a verb of the seat "mesh-controller", ` + UnheldSeat, + Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-free"}}, telegram(), true}, + {"the engine's verb: every node's engine holds its seat", "", + Manifest{Module: "shop", Invokes: []string{"seat:node-engine.root-free"}}, telegram(), false}, + {"a verb the engine's seat does not promise", `the seat node-engine serves no verb "root-freed"`, + Manifest{Module: "shop", Invokes: []string{"seat:node-engine.root-freed"}}, telegram(), false}, + } { + got := CatalogueProblems(Shelf{"telegram": row.seat, "shop": row.shop}) + joined := strings.Join(got, "; ") + if row.want == "" && joined != "" || row.want != "" && (len(got) != 1 || !strings.Contains(joined, row.want) || + IsUndeclaredSeat(got[0]) != row.note) { + t.Errorf("%s: %s", row.name, joined) + } } }