package conditions // Plain words (novox/hq ADR 0253): **a condition carries what the operator reads, in plain words, beside // what an agent reads.** The summary is one line in the mesh's words for whoever looks closer — it names // plans, commits and the verbs that act, and stays so. The operator reads a notification between other // work: a popup that said "the walk of novox/mesh-catalog a6385479 has waited 56m0s for mesh-delivery's // word to start: `mesh-delivery.show` …" was a wall of identifiers nobody could act on. So every condition // also carries: // // - a **headline**: a few plain words naming the thing and what is wrong ("openrazer not running on // g14"), the title of every message about it; // - an **explanation**: one or two plain sentences — what happened, what it means for the operator, // and whether they need to do anything; // - a **resolved line**: the one short line said when it clears ("openrazer runs again on g14"); // - a **verdict**, which every explanation opens with: "Nothing for you to do." or "Needs you:" and one // concrete thing the operator can do themselves (needs). Never "have an agent …": the operator is not // asked to open a session to understand or answer a notification; // - **actions**: what the operator may answer from the notification itself (Release, Stop, Restart, // Silence), each the seat verb the operator channel calls when it is chosen, naming the operator. // // They are made here, where the condition is made, so every channel gets them — the desktop today, // others later — and none has to guess what a key means. A producer may say them itself; otherwise the // wording registered for its kind says them; otherwise a plain sentence made from its scope does, and a // test suite is told (Unworded) so the kind gets words of its own. Each is held to the plain rule (Plain): // no identifiers, hashes, keys, verb syntax, markup or clock times — a message names no time of its own, // since the channel says when, in the operator's time. import ( "fmt" "regexp" "strings" "sync" "github.com/novox/mesh-controller/internal/outward" ) // Words are what the operator reads of a condition. Explanation is said after the verdict, which // plainly writes from Needs: empty is "Nothing for you to do.", else "Needs you: " and Needs. type Words struct { Headline string Explanation string Resolved string Needs string Actions []Action } // Action is one answer the operator may give from a notification: a label, and the seat verb the // operator channel calls with these arguments (and a why naming the operator and the label) when it is // chosen. Machine is set for a seat every machine holds. An argument "why" given empty is the operator // channel's to fill: it names the operator, the channel and the label chosen. type Action struct { Label string `json:"label"` Verb string `json:"verb"` Machine string `json:"machine,omitempty"` Arguments map[string]string `json:"arguments,omitempty"` } // The two verdicts an explanation opens with. const ( NothingToDo = "Nothing for you to do." NeedsYou = "Needs you:" ) // SilenceAction is the action that stops a condition's messages for a week, with the operator's why: the // answer to a condition the operator decided to live with. func SilenceAction(key string) Action { return Action{Label: "Silence for a week", Verb: "mesh-controller.conditions", Arguments: map[string]string{"silence": key, "for": "7d", "why": ""}} } // Bounds of the plain words: a headline fits a notification's title line, an explanation two sentences. const ( HeadlineMax = 60 ExplanationMax = 420 NeedsMax = 120 ) var ( wordingsMu sync.RWMutex wordings = map[string]func(Observation) Words{} ) // Wording registers the plain words of one condition kind. func Wording(kind string, words func(Observation) Words) { wordingsMu.Lock() defer wordingsMu.Unlock() wordings[kind] = words } // Worded says whether a kind has words of its own. func Worded(kind string) bool { wordingsMu.RLock() defer wordingsMu.RUnlock() _, ok := wordings[kind] return ok } // Unworded is told of every observation said in borrowed words: its kind has none registered, or the // words it was given break the plain rule. The keeper says it plainly anyway; a test suite sets this to // fail the producer. var Unworded func(o Observation, why string) // The plain rule's shapes. var ( hexID = regexp.MustCompile(`\b[0-9a-f]{7,40}\b`) numberedID = regexp.MustCompile(`\b[a-z]+-[0-9]{6,}\b`) dotted = regexp.MustCompile(`\b[a-z][a-z0-9_-]*\.[a-z][a-z0-9_-]*\b`) goDuration = regexp.MustCompile(`\b[0-9]+(h[0-9]+m|m[0-9]+s|h[0-9]+m[0-9]+s)\b|\b[0-9]+(\.[0-9]+)?(ns|ms|µs)\b`) clockTime = regexp.MustCompile(`\b[0-9]{1,2}:[0-9]{2}\b|\b[0-9]{4}-[0-9]{2}-[0-9]{2}\b|\bUTC\b`) flag = regexp.MustCompile(`(^|\s)--?[a-z]`) agentWord = regexp.MustCompile(`(?i)\bagents?\b|\bby hand\b|\bsession\b`) markup = regexp.MustCompile("[`*<>{}\\[\\]|#]|(^|\\s)_|_(\\s|$)") ) // Plain says whether text is plain words the operator reads at a glance, and if not, what is not: an // identifier (a hash, a numbered id, a dotted name such as a key or a verb), a Go duration, a clock time // or date (the channel says when, in the operator's time), a command's flag, markup, a line break, or // anything the operator's channel withholds (ADR 0234 §6). machines are names that may appear. func Plain(text string, machines ...string) (string, bool) { if strings.TrimSpace(text) == "" { return "nothing is said", false } if strings.ContainsAny(text, "\n\r\t") { return "a line break", false } if m := markup.FindString(text); m != "" { return "markup (" + m + ")", false } for _, shape := range []struct { re *regexp.Regexp what string }{{hexID, "a hash"}, {numberedID, "a numbered id"}, {dotted, "a dotted name"}, {goDuration, "a duration in code"}, {clockTime, "a clock time or date"}, {flag, "a command's flag"}} { if m := shape.re.FindString(text); m != "" { if shape.re == hexID && !strings.ContainsAny(m, "0123456789") { continue // a word of letters a to f only } return shape.what + " (" + strings.TrimSpace(m) + ")", false } } if r, ok := outward.Check(text, machines...); !ok { return r.String(), false } return "", true } // PlainWords checks all three, with their bounds. func PlainWords(w Words, machines ...string) (string, bool) { if why, ok := Plain(w.Headline, machines...); !ok { return "headline: " + why, false } if len(w.Headline) > HeadlineMax { return fmt.Sprintf("headline: longer than %d characters", HeadlineMax), false } if why, ok := Plain(w.Explanation, machines...); !ok { return "explanation: " + why, false } if len(w.Explanation) > ExplanationMax { return fmt.Sprintf("explanation: longer than %d characters", ExplanationMax), false } for _, m := range agentWord.FindAllString(w.Explanation, -1) { if !strings.EqualFold(m, "by hand") { // "repaired by hand" is a fact; telling to act by hand is not return "explanation: sends the operator elsewhere (" + m + ")", false } } if w.Needs != "" { if why, ok := Plain(w.Needs, machines...); !ok { return "needs: " + why, false } if m := agentWord.FindString(w.Needs); m != "" { return "needs: not something the operator does themselves (" + m + ")", false } if len(w.Needs) > NeedsMax || !strings.HasSuffix(w.Needs, ".") { return fmt.Sprintf("needs: one sentence of at most %d characters, ending in a full stop", NeedsMax), false } } for _, a := range w.Actions { if a.Label == "" || len(a.Label) > 24 || a.Verb == "" { return fmt.Sprintf("action %q: a short label and a verb", a.Label), false } } if why, ok := Plain(w.Resolved, machines...); !ok { return "resolved: " + why, false } if len(w.Resolved) > HeadlineMax+20 { return fmt.Sprintf("resolved: longer than %d characters", HeadlineMax+20), false } return "", true } // plainly gives an observation its plain words: its own, its kind's, or its scope's. func plainly(o Observation) Observation { machines := append([]string{o.Machine}, o.Also...) given := Words{Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions} var w Words from := "" switch { case given.Headline != "": w, from = given, "its source" default: wordingsMu.RLock() fn := wordings[o.Kind] wordingsMu.RUnlock() if fn != nil { w, from = fn(o), "the wording of "+o.Kind if given.Explanation != "" { w.Explanation = given.Explanation } if given.Resolved != "" { w.Resolved = given.Resolved } if given.Needs != "" { w.Needs = given.Needs } if len(given.Actions) > 0 { w.Actions = given.Actions } } } if w.Headline != "" && w.Resolved == "" { w.Resolved = "Resolved: " + lowerFirst(w.Headline) } if from == "" { if Unworded != nil { Unworded(o, "the kind "+o.Kind+" has no plain words") } w = scopeWords(o) } else if why, ok := PlainWords(w, machines...); !ok { if Unworded != nil { Unworded(o, from+" is not plain: "+why) } w = scopeWords(o) } o.Headline, o.Resolved, o.Needs, o.Actions = w.Headline, w.Resolved, w.Needs, w.Actions o.Explanation = Verdict(w.Needs, w.Explanation) return o } // Verdict is an explanation opened by its verdict. func Verdict(needs, explanation string) string { if needs == "" { return strings.TrimSpace(NothingToDo + " " + explanation) } return strings.TrimSpace(NeedsYou + " " + needs + " " + explanation) } // Escalated is what a condition needs once a healer gave up on it, where its words needed nothing. const Escalated = "the mesh tried to repair this and could not; read the details to decide what to do." // escalatedWords turns "Nothing for you to do." into "Needs you:" once a healer gave up. func escalatedWords(c *Condition) { if c.Escalated() && c.Needs == "" { c.Needs = Escalated c.Explanation = Verdict(c.Needs, strings.TrimSpace(strings.TrimPrefix(c.Explanation, NothingToDo))) } } // scopeWords is what is said of a kind with no words of its own: what kind of thing, and which one // where its name is a name (a machine, a module), never an id. func scopeWords(o Observation) Words { thing := ThingWords(o) what := strings.ReplaceAll(o.Kind, "-", " ") w := Words{ Headline: Capital(thing) + " needs a look", Explanation: fmt.Sprintf("The mesh noticed a problem it calls %q with %s.", what, thing), Resolved: "Resolved: " + thing + " is fine again", Needs: ResolverNeeds(o), } if _, ok := PlainWords(w, append([]string{o.Machine}, o.Also...)...); !ok { w = Words{Headline: "Something in the mesh needs a look", Explanation: "The mesh noticed a problem it has no plain words for yet.", Resolved: "Resolved: the mesh is fine again", Needs: ResolverNeeds(o)} } return w } // ThingWords is what a condition is about, in plain words: "the machine ace", "openrazer on g14", "a // delivery". An id that is not a name (a plan's, a delivery's, a call's) is never said. func ThingWords(o Observation) string { switch o.Scope { case ScopeMachine: if o.Machine != "" { return o.Machine } return "a machine" case ScopeModule: module := strings.TrimSuffix(o.ID, "."+o.Machine) if o.Machine != "" && module != "" && !strings.Contains(module, ".") { return module + " on " + o.Machine } return "a module" case ScopeProvider: if module, _, _ := strings.Cut(o.ID, "."); module != "" && o.Machine != "" { return module + " on " + o.Machine } return "a provider" case ScopePlan: return "a walk" case ScopeDelivery: return "a delivery" case ScopeBuild: return "a build" case ScopeCall: return "a tool call" case ScopeMerge: return "a merge" case ScopeSeat: return "a seat's holder" case ScopeBus: return "the bus" case ScopeCore: return "the controller" case ScopeProbe: return "the self-check" } return "the mesh" } // ResolverNeeds is what the operator needs to do, from who resolves it: nothing, unless only a person can. func ResolverNeeds(o Observation) string { if o.Resolver == ResolverOperator { return "the mesh does not repair this by itself; read the details to decide what to do." } return "" } // Capital is s with its first letter upper case. func Capital(s string) string { if s == "" { return s } return strings.ToUpper(s[:1]) + s[1:] } func lowerFirst(s string) string { if s == "" || (len(s) > 1 && strings.ToUpper(s[:2]) == s[:2]) { return s } return strings.ToLower(s[:1]) + s[1:] }