Say a secret given in plain words, and log words the keeper refuses
mesh/delivery superseded: a newer head of the same pull request
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

The secret-given condition's explanation carried a clock time, which the
plain rule refuses, so the operator read the scope's fallback ("needs a
look") instead of what changed. Its words now carry no time and say the
secret's name as words; a test holds them to the rule through a keeper.
With no test hook set, the keeper logs a refused or missing wording once
per kind and reason, so a fallback is never silent again (hq issue 359).
This commit is contained in:
jochen
2026-10-09 23:41:22 +02:00
parent 19eefb66c6
commit 988e501ccc
4 changed files with 103 additions and 13 deletions
+21 -7
View File
@@ -27,6 +27,7 @@ package conditions
import (
"fmt"
"log"
"regexp"
"strings"
"sync"
@@ -135,9 +136,26 @@ func Worded(kind string) bool {
// 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.
// fail the producer. Unset, as in the serving controller, it is a line in the log (unworded).
var Unworded func(o Observation, why string)
// loggedUnworded holds each kind and why already logged: once per process, since a condition observed
// every minute would otherwise say the same line every minute.
var loggedUnworded sync.Map
// unworded tells Unworded, or else logs once, that an observation is said in borrowed words — so words
// that fell back are never silent (hq issue 359: a secret given reached the operator as "needs a look",
// and nothing said why).
func unworded(o Observation, why string) {
if Unworded != nil {
Unworded(o, why)
return
}
if _, seen := loggedUnworded.LoadOrStore(o.Kind+"\x00"+why, true); !seen {
log.Printf("the condition kind %q is said in the scope's words, not its own: %s", o.Kind, why)
}
}
// The plain rule's shapes.
var (
hexID = regexp.MustCompile(`\b[0-9a-f]{7,40}\b`)
@@ -263,14 +281,10 @@ func plainly(o Observation) Observation {
// An explanation too long is cut, never refused: what it says first is what matters.
w.Explanation = cut(w.Explanation, ExplanationMax-len(Verdict(w.Needs, ""))-1)
if from == "" {
if Unworded != nil {
Unworded(o, "the kind "+o.Kind+" has no plain words")
}
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)
}
unworded(o, from+" is not plain: "+why)
// The scope's words, but never a weaker verdict: what needed the operator still does, and its
// answers are kept where they are themselves sound.
needed, actions := w.Needs != "", soundActions(w.Actions)
+26
View File
@@ -1,7 +1,10 @@
package conditions
import (
"bytes"
"encoding/json"
"log"
"os"
"strings"
"testing"
)
@@ -245,3 +248,26 @@ func TestAChangeOfWordsIsSaid(t *testing.T) {
t.Fatalf("%+v", said)
}
}
// **Words that fall back are never silent** (hq issue 359): with no test listening, the keeper logs which
// kind is said in borrowed words and why — once, however often the condition is observed.
func TestBorrowedWordsAreLogged(t *testing.T) {
k, _, _, _ := keeper(t)
before := Unworded
Unworded = nil
t.Cleanup(func() { Unworded = before })
var out bytes.Buffer
log.SetOutput(&out)
t.Cleanup(func() { log.SetOutput(os.Stderr) })
for i := 0; i < 3; i++ {
if _, err := k.Observe(t.Context(), Observation{Scope: ScopeMachine, ID: "ace", Kind: "test-clock", Machine: "ace",
Severity: Warning, Source: "test", Summary: "s", Headline: "ace was given a secret",
Explanation: "It was given at 23:32.", Resolved: "Seen"}); err != nil {
t.Fatal(err)
}
}
if got := out.String(); strings.Count(got, "\n") != 1 || !strings.Contains(got, `"test-clock"`) ||
!strings.Contains(got, "a clock time or date") {
t.Errorf("the log said %q", got)
}
}