Files
mesh-controller/internal/conditions/plain.go
T
jochen 1a4305213d
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/plain-notifications delivered: every member is delivered
Open every explanation with what the operator needs to do, and offer the answers
The operator could not tell from a notification whether to act, and was told to
have an agent do it. Each condition now says "Nothing for you to do." or
"Needs you:" with one thing they can do themselves, and carries the actions the
operator channel performs when chosen (hq ADR 0253).
2026-10-08 13:58:53 +02:00

342 lines
12 KiB
Go

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:]
}