Files
mesh-controller/internal/conditions/plain.go
T
jochen 1fce541023
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 fail: its merge-check.sh failed: --- FAIL: TestTheForgesOwnAddressFollowsThePortTheNodeGaveIt (0.00s)
mesh/delivery-group group feat/plain-notifications rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
Give every condition a headline, an explanation and a resolved line in plain words
The operator could not read the desktop notifications: they carried plan ids,
commits, keys and verb syntax. The words the operator reads now travel with the
condition, so every channel says them (hq ADR 0253).
2026-10-08 13:22:27 +02:00

274 lines
9.4 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").
//
// 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.
type Words struct {
Headline string
Explanation string
Resolved string
}
// Bounds of the plain words: a headline fits a notification's title line, an explanation two sentences.
const (
HeadlineMax = 60
ExplanationMax = 360
)
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]`)
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
}
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{o.Headline, o.Explanation, o.Resolved}
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 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.Explanation, o.Resolved = w.Headline, w.Explanation, w.Resolved
return o
}
// Escalated is the sentence a condition's explanation ends with once a healer gave up on it.
const Escalated = "The mesh tried to repair it and could not: it needs you now."
// escalatedWords says in the explanation that a healer gave up, whatever the words said before.
func escalatedWords(c *Condition) {
if c.Escalated() && !strings.HasSuffix(c.Explanation, Escalated) {
c.Explanation = strings.TrimSpace(c.Explanation + " " + Escalated)
}
}
// 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. %s", what, thing, ResolverWords(o)),
Resolved: "Resolved: " + thing + " is fine again",
}
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. " + ResolverWords(o),
Resolved: "Resolved: the mesh is fine again"}
}
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"
}
// ResolverWords says whether the operator needs to act, from who resolves it.
func ResolverWords(o Observation) string {
switch {
case o.Resolver == ResolverOperator:
return "It needs you: the mesh does not repair this by itself."
case strings.HasPrefix(o.Resolver, "healer:"):
return "A healer is working on it; nothing to do unless it stays."
case o.Severity == Urgent:
return "It clears by itself once it is fixed, but needs a look now."
}
return "Nothing to do yet: it clears by itself once it is fixed."
}
// 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:]
}