diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 1b69c235..e0afd1d2 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -4067,12 +4067,24 @@ func filepathDir(p string) string { } // invokeNameValid says a module or process's invoked entry names a tool: ., a role's -// seat:., or * for every tool on the mesh (novox/hq ADR 0152, ADR 0436 §1). +// seat:. or a kind the seat accepts (seat:., seat:.., design 47 G A-102), +// or * for every tool on the mesh (novox/hq ADR 0152, ADR 0436 §1). func invokeNameValid(t string) bool { if t == "*" { return true } - t = strings.TrimPrefix(t, "seat:") + if rest, isSeat := strings.CutPrefix(t, "seat:"); isSeat { + // A seat's verb, or a kind the seat accepts: ``, or `.` for one that rides a stream (design 47 G + // A-102), an asker's queue `accept.` among them (§601, A-231); seatInvokeProblems judges it against the seat. + seat, after, named := strings.Cut(rest, ".") + if !named || !name.MatchString(seat) { + return false + } + if stream, kind, onStream := strings.Cut(after, "."); onStream { + return toolName.MatchString(stream) && toolName.MatchString(kind) + } + return toolName.MatchString(after) + } first, second, named := strings.Cut(t, ".") return named && name.MatchString(first) && toolName.MatchString(second) } @@ -4586,7 +4598,12 @@ func seatInvokeProblems(shelf catalogue.Shelf, declared map[string]catalogue.Sea problems = append(problems, fmt.Sprintf("%s invokes %s, a verb of the seat %q, "+catalogue.UndeclaredSeat, module, invoke, seat)) continue } - if !slices.ContainsFunc(verbs, func(v catalogue.Verb) bool { return v.Name == verb }) { + // An asker's queue of a build seat, `seat:.accept.` (design 47 G §544, §601, A-231), read by mesh-sdk's + // one reader: judged against the kinds the seat's definition accepts. + if _, queue, isQueue := catalogue.QueueInvoke(invoke); isQueue { + verb = queue + } + if !slices.ContainsFunc(verbs, func(v catalogue.Verb) bool { return v.Name == verb }) && !acceptsKind(seat, verb, declared) { problems = append(problems, fmt.Sprintf("%s invokes %s, and the seat %s serves no verb %q", module, invoke, seat, verb)) continue } @@ -4598,6 +4615,15 @@ func seatInvokeProblems(shelf catalogue.Shelf, declared map[string]catalogue.Sea return problems } +// acceptsKind says the seat accepts the kind an invoke names after its seat (design 47 G A-102): ``, or +// `.` for a kind that rides a stream. A module's own seat names its accepts by kind alone. +func acceptsKind(seat, kind string, declared map[string]catalogue.SeatDeclaration) bool { + if s, isMesh := catalogue.SeatNamed(seat); isMesh { + return slices.Contains(s.Accepts, kind) || slices.Contains(s.AcceptedOn, kind) + } + return slices.Contains(declared[seat].Accepts, kind) +} + // SeatPathProblems is what is wrong with the paths a set of the mesh's own seats names (novox/hq ADR // 0287): only a node seat names one, each absolute and literal; every path has exactly one seat naming it // without stepping back; and a seat steps back only for another node seat naming the same path without diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index abc533d4..b7497f0a 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -5240,6 +5240,11 @@ func caseTwoModulesCannotDeclareTheSameSeat(t *testing.T) { func caseUsingASeatNobodyDeclaresIsRefused(t *testing.T) { unclaimed := telegram() unclaimed.Claims = nil + // A mesh seat that accepts a kind riding a stream (design 47 G A-102), beside the seat list's own. + was := catalogue.Seats() + t.Cleanup(func() { catalogue.UseSeats(was) }) + catalogue.UseSeats(append(catalogue.Seats(), catalogue.Seat{Name: "mesh-steps", Scope: catalogue.ScopeMesh, Decision: "test", + Accepts: []string{"step"}, AcceptedOn: []string{"check.step"}})) for _, row := range []struct { name, want string shop catalogue.Manifest @@ -5268,8 +5273,20 @@ func caseUsingASeatNobodyDeclaresIsRefused(t *testing.T) { {"what the engine reads of a manifest, and its event (ADR 0618 §3, §4)", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-engine.manifest-reads"}, Consumes: []string{"node-engine.manifest-reads-changed"}}, telegram(), false}, + // An asker names the queue it asks as `seat:.accept.`, checked against the kinds the seat's definition + // accepts (design 47 G §601, A-231); a kind the seat accepts, alone or on its stream, as `seat:.` or + // `seat:..` (A-102). + {"a queue a build seat accepts", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-build-go.accept.check"}}, telegram(), false}, + {"a queue a build seat does not accept", `the seat node-build-go serves no verb "other"`, + catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-build-go.accept.other"}}, telegram(), false}, + {"a queue a declared seat accepts", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.accept.send"}}, telegram(), false}, + {"a kind a seat accepts", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-steps.step"}}, telegram(), false}, + {"a kind a seat accepts on its stream", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-steps.check.step"}}, telegram(), false}, + {"a kind on a stream the seat does not take it on", `the seat mesh-steps serves no verb "delivery.step"`, + catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-steps.delivery.step"}}, telegram(), false}, } { - got := CatalogueProblems(catalogue.Shelf{"telegram": row.seat, "shop": row.shop}) + // The manifest's own invokes rule (at parse) takes the dotted kind as well; the seat's rule then judges it. + got := append(invokeProblems(row.shop), CatalogueProblems(catalogue.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) || catalogue.IsUndeclaredSeat(got[0]) != row.note) {