package catalogue import ( "fmt" "regexp" "sort" "strings" ) // Seats a module declares of its own (novox/hq ADR 0118). // // The set of seats a mesh has is **derived**: the mesh's own, in seats.go, plus those declared by // every module it has registered. Still closed — a seat named nowhere is refused — but computed // from the catalogue rather than written in the controller, which is what ADR 0110 actually // needed and a hand-maintained table could not keep. Its own evidence: the enumeration done by // hand while that record was written reported eleven claims where there were thirteen. // // **What can be checked from one manifest and what cannot.** A declaration's shape, its scope, // and the reserved prefix are facts about the manifest in front of you. Whether a seat anybody // names actually exists, whether two modules declared the same one, and whether a holder // satisfies the protocol are facts about the *catalogue* — so they are checked at registration, // by CatalogueProblems, which is the last moment the mesh can still say no. // meshSeatPrefix is reserved to the mesh. The prefix *is* the reservation rule: no list of // reserved names to maintain, no way for the mesh's own namespace to be colonised by a manifest, // and nothing to keep in step when a mesh seat is added. const meshSeatPrefix = "mesh-" // retiredLoginShell is the one name outside the prefix a module may not declare: the login shell's, // from when a module declared it (novox/hq ADR 0176), before it became the mesh's (ADR 0204). const retiredLoginShell = "login-shell" // A SeatDeclaration is a role a module offers on the bus: what may be sent to it, what it says, // and what it answers. A caller declares that it uses the *seat*, never the module, so the // implementation can be replaced under it. type SeatDeclaration struct { Name string `json:"name"` Scope string `json:"scope,omitempty"` // Accepts are the verbs others may submit work on. Each becomes a work-queue subject, and // the holder is the only consumer — so exactly one worker does the job, by construction // rather than by how carefully somebody wrote a subscribe call. Accepts []string `json:"accepts,omitempty"` // Emits are the verbs the holder publishes: 1:many, nobody obliged to act. Emits []string `json:"emits,omitempty"` // Serves are the verbs the holder answers: request and reply, awaited. A bare name, or the // verb in full with its schema (novox/hq ADR 0132). Serves []Verb `json:"serves,omitempty"` // RetainSeconds is how long the inbound backlog survives with no holder, zero for the // mesh's default. Retention belongs to whoever owns the namespace (design 29 §3) — a seat // owns its own, which is why a seat is also the answer for a module that needs retention // its events cannot have. RetainSeconds int `json:"retain-seconds,omitempty"` // Kinded makes the seat a kinded bench (novox/hq ADR 0234 §2, ADR 0259 §3): its holders are different // modules, each claiming one kind, and each verb's subject carries the kind. Only the benches in // KindedBenches may be kinded; making another is a decision, recorded. Kinded bool `json:"kinded,omitempty"` // ByCaller are accepts and emits whose subject's last token names the calling module (ADR 0259 §3): a // user submits such an accept, and hears such an event, under its own name and no other. ByCaller []string `json:"by-caller,omitempty"` // Proofs are verbs carried as core request and reply, never on a stream: what travels on them (a code // the operator typed) is never kept (ADR 0259 §3). On a kinded bench a holder asks with its own kind and // the modules watching the seat answer. Proofs []string `json:"proofs,omitempty"` // Records are state buckets of the declaring module that each user reads under its own name — the // keys `.…` and no other (ADR 0259 §3). Records []string `json:"records,omitempty"` } // KindedBenches are the seats that may be kinded (novox/hq ADR 0234 §2): `channel` sends to the operator, // `intake` takes what the operator answers. Another is a decision, recorded, as ADR 0223 asks of a bench. var KindedBenches = map[string]bool{"channel": true, "intake": true} // NamedByCaller says whether one of the seat's verbs is named by its caller. func (s SeatDeclaration) NamedByCaller(verb string) bool { for _, v := range s.ByCaller { if v == verb { return true } } return false } // At is this declaration's scope, with the default applied. Mesh by default, because a seat // declared by a module is nearly always "there is one of these in the mesh" — a per-node worker // is the deliberate case, and says so. func (s SeatDeclaration) At() string { if s.Scope == "" { return ScopeMesh } return s.Scope } // verbs is everything the protocol names, for the checks that do not care which half. func (s SeatDeclaration) verbs() []string { out := append([]string{}, s.Accepts...) out = append(out, s.Emits...) return append(out, VerbNames(s.Serves)...) } // declaredSeatProblems is what one manifest can be judged on alone. func declaredSeatProblems(m Manifest) []string { var problems []string seen := map[string]bool{} for _, s := range m.DefinesSeats { switch { case s.Name == "": problems = append(problems, fmt.Sprintf("%s declares a seat with no name", m.Module)) continue case !name.MatchString(s.Name): problems = append(problems, fmt.Sprintf( "%s declares a seat named %q, which is not a usable name", m.Module, s.Name)) continue case strings.HasPrefix(s.Name, meshSeatPrefix): // The mesh's own code dereferences its seats by name — the resolver *is* the thing // that finds the store — so the prefix is not a convention, it is a namespace. problems = append(problems, fmt.Sprintf( "%s declares a seat named %q; %q is reserved to the mesh, which defines its own "+ "seats (novox/hq ADR 0118)", m.Module, s.Name, meshSeatPrefix+"*")) continue } if s.Name == retiredLoginShell { // The name ADR 0176 gave the login shell when the zsh module declared it. The seat is // the mesh's now, so a module declaring the old name would be a second login shell // beside it, with a protocol of its own (novox/hq ADR 0204). problems = append(problems, fmt.Sprintf( "%s declares a seat named %q; the login shell is the mesh's own seat %s, which a shell "+ "module claims and none declares (novox/hq ADR 0204)", m.Module, s.Name, LoginShellSeat)) continue } if seen[s.Name] { problems = append(problems, fmt.Sprintf( "%s declares the seat %q twice", m.Module, s.Name)) continue } seen[s.Name] = true if _, isMesh := SeatNamed(s.Name); isMesh { problems = append(problems, fmt.Sprintf( "%s declares %q, which is a seat the mesh already defines", m.Module, s.Name)) } switch s.At() { case ScopeNode, ScopeSite, ScopeMesh: default: problems = append(problems, fmt.Sprintf( "%s declares seat %s at scope %q; a seat is held per node, per site or per mesh", m.Module, s.Name, s.Scope)) } // A seat with an empty protocol is allowed, and is the mesh saying what a machine is: // which module is this node's packet filter, or its showcase. Design 26 calls it a seat // that delivers nothing, and that is most of the node-scoped ones. ADR 0126's "a declared // seat carries a protocol" governs what a holder must satisfy, not that every seat offers // something — a marker seat's protocol is satisfied by holding it. Nothing can reach this // state by accident: a mistyped field name is refused by the parser above, so an empty // protocol was written as one. for _, v := range s.verbs() { if !name.MatchString(v) { problems = append(problems, fmt.Sprintf( "%s declares %s.%s, which is not a usable verb", m.Module, s.Name, v)) } } problems = append(problems, trafficProblems(m, s)...) } for _, u := range m.Uses { if !name.MatchString(u) { problems = append(problems, fmt.Sprintf("%s uses %q, which is not a usable seat name", m.Module, u)) } } return problems } // trafficProblems is what one declaration of the rules of ADR 0259 §3 can be judged on alone. func trafficProblems(m Manifest, s SeatDeclaration) []string { var problems []string if s.Kinded && !KindedBenches[s.Name] { problems = append(problems, fmt.Sprintf( "%s declares %s as a kinded bench; only channel and intake are kinded, and another is a "+ "decision, recorded (novox/hq ADR 0234)", m.Module, s.Name)) } if s.Kinded && len(s.ByCaller) > 0 { problems = append(problems, fmt.Sprintf( "%s declares %s kinded and names verbs by their caller; a kinded bench's subjects carry the kind", m.Module, s.Name)) } for _, v := range s.ByCaller { inAccepts, inEmits := false, false for _, a := range s.Accepts { inAccepts = inAccepts || a == v } for _, e := range s.Emits { inEmits = inEmits || e == v } if !inAccepts && !inEmits { problems = append(problems, fmt.Sprintf( "%s names %s.%s by its caller, which the seat neither accepts nor emits", m.Module, s.Name, v)) } } for _, v := range s.Proofs { if !name.MatchString(v) || strings.Contains(v, ".") { problems = append(problems, fmt.Sprintf("%s declares the proof %s.%s, which is not a usable verb", m.Module, s.Name, v)) } } if len(s.Proofs) > 0 && !s.Kinded { problems = append(problems, fmt.Sprintf( "%s declares proofs on %s, which is not kinded; a proof is asked by a holder of a kind", m.Module, s.Name)) } for _, r := range s.Records { kept := false for _, st := range m.State { kept = kept || st.Name == r } if !kept { problems = append(problems, fmt.Sprintf( "%s says %s's users read its records %q, which it keeps no state of", m.Module, s.Name, r)) } } return problems } // A Shelf is every manifest the mesh has registered, by module name. type Shelf map[string]Manifest // CatalogueProblems are the rules no single manifest can be judged against. // // Run at registration, which is the last moment the mesh can still refuse: after it, a caller is // bound to a seat and a refusal is an outage rather than a conversation. func CatalogueProblems(shelf Shelf) []string { var problems []string // Who declares what, and who declared it first. declaredBy := map[string]string{} declared := map[string]SeatDeclaration{} for _, module := range shelfOrder(shelf) { for _, s := range shelf[module].DefinesSeats { if s.Name == "" { continue } if first, taken := declaredBy[s.Name]; taken { // The second loses. A seat name meaning two different protocols is the failure // nobody could diagnose afterwards — a caller would bind to whichever happened // to register first, and the symptom would appear in the other module. problems = append(problems, fmt.Sprintf( "%s declares the seat %q, which %s already declares; a seat name means one "+ "protocol", module, s.Name, first)) continue } declaredBy[s.Name] = module declared[s.Name] = s } } exists := func(seat string) bool { if _, isMesh := SeatNamed(seat); isMesh { return true } _, ok := declaredBy[seat] return ok } // Who claims each kind of a kinded bench, so a second claim of one kind is refused (ADR 0234 §2). kindsTaken := map[string]string{} for _, module := range shelfOrder(shelf) { m := shelf[module] for _, c := range m.Claims { if c.Kind != "" { if _, isModuleSeat := declared[c.Name]; !isModuleSeat { problems = append(problems, fmt.Sprintf( "%s claims %s of kind %q, and only a kinded bench takes a kind", module, c.Name, c.Kind)) } } } // A `uses` naming nothing is where ADR 0110's guarantee lands under a derived set: the // same refusal, at the same moment, from a set nobody maintains by hand. for _, u := range m.Uses { if !exists(u) { problems = append(problems, fmt.Sprintf( "%s uses the seat %q, which no module declares and the mesh does not define", module, u)) } } for _, c := range m.Claims { if !exists(c.Name) { problems = append(problems, fmt.Sprintf( "%s claims the seat %q, which no module declares and the mesh does not define", module, c.Name)) continue } s, isModuleSeat := declared[c.Name] if !isModuleSeat { // **A mesh seat is judged here and nowhere else** (novox/hq ADR 0122): the set is // the store's, and this is the only place that runs with the store's set loaded. // The parser cannot do it — it also runs on the build machine, against whatever // set that binary was compiled with. seat, _ := SeatNamed(c.Name) if err := CanHold(m, seat); err != nil { problems = append(problems, err.Error()) } continue } problems = append(problems, kindProblems(module, c, s, kindsTaken)...) if c.At() != s.At() { problems = append(problems, fmt.Sprintf( "%s claims %s at scope %q, and %s declares it at %s", module, c.Name, c.At(), declaredBy[c.Name], s.At())) } // A holder that does not answer what the seat promises is a caller's timeout, found // at assignment instead. if missing := unserved(m, c, s); len(missing) > 0 { problems = append(problems, fmt.Sprintf( "%s claims %s but does not serve %s, which that seat's protocol promises", module, c.Name, strings.Join(missing, ", "))) } } } // A read of a module's state that module does not keep (novox/hq ADR 0201) — said only where the // owner is on the shelf, as a consumer may be installed before its emitter. var manifests []Manifest for _, module := range shelfOrder(shelf) { manifests = append(manifests, shelf[module]) } problems = append(problems, StateReadsNothingDeclares(manifests)...) sort.Strings(problems) return problems } // ChannelCapabilities is the fixed vocabulary `channel-capabilities/1` (novox/hq ADR 0234 §2): a word // outside it is refused. `max-length:` takes a number. var ChannelCapabilities = map[string]bool{ "deliver": true, "reaches-away": true, "loud": true, "silent": true, "edit": true, "reaches-when-mesh-down": true, "private": true, "choice": true, "reply": true, "threads": true, "operator-first": true, "verified-sender": true, "exact-render": true, "code-factor": true, "key-factor": true, } var maxLength = regexp.MustCompile(`^max-length:[1-9][0-9]{0,6}$`) // capabilityProblems are the words of a claim outside the vocabulary, and capabilities on a claim of a // seat that is not kinded. func capabilityProblems(module string, c Claim, kinded bool) []string { if len(c.Capabilities) == 0 { return nil } if !kinded { return []string{fmt.Sprintf("%s claims %s with capabilities, and only a kinded bench's claim carries them", module, c.Name)} } var problems []string for _, w := range c.Capabilities { if !ChannelCapabilities[w] && !maxLength.MatchString(w) { problems = append(problems, fmt.Sprintf( "%s claims %s with the capability %q, which channel-capabilities/1 does not have", module, c.Name, w)) } } return problems } // kindProblems is a claim judged against a declared seat's kind: a kinded bench takes one claim per kind, // a usable name; any other seat takes none. func kindProblems(module string, c Claim, s SeatDeclaration, taken map[string]string) []string { if problems := capabilityProblems(module, c, s.Kinded); len(problems) > 0 { return problems } switch { case !s.Kinded && c.Kind != "": return []string{fmt.Sprintf("%s claims %s of kind %q, and only a kinded bench takes a kind", module, c.Name, c.Kind)} case !s.Kinded: return nil case c.Kind == "": return []string{fmt.Sprintf("%s claims the kinded bench %s and names no kind", module, c.Name)} case !name.MatchString(c.Kind) || strings.Contains(c.Kind, "."): return []string{fmt.Sprintf("%s claims %s of kind %q, which is not a usable name", module, c.Name, c.Kind)} } key := c.Name + "/" + c.Kind if first, ok := taken[key]; ok && first != module { return []string{fmt.Sprintf("%s claims %s of kind %q, which %s already claims; a kind has one holder", module, c.Name, c.Kind, first)} } taken[key] = module return nil } // unserved is what a seat's protocol promises and the claimant does not answer. Only the tools // are checked: `accepts` and `emits` are wired by the runtime from the declaration, while a tool // is code the module either has or has not written — under the claim's serves, or among its own. func unserved(m Manifest, c Claim, s SeatDeclaration) []string { has := map[string]bool{} for _, t := range c.ServesFor(m) { has[t] = true } var missing []string for _, t := range s.Serves { if !has[t.Name] { missing = append(missing, t.Name) } } return missing } // shelfOrder is the catalogue in a stable order, so two runs report the same problems in the same // sequence — a refusal that reorders itself is a refusal nobody can diff. func shelfOrder(shelf Shelf) []string { out := make([]string, 0, len(shelf)) for k := range shelf { out = append(out, k) } sort.Strings(out) return out }