Hold what the tools say to the glossary's retired words (hq ADR 0244)
mesh/merge-gate pass: builds docker, gitea, lab, nftables, slack, systemd → ace, g14, novox, shanks; no bus step; 4 wait(s) for a person; every machine com…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

An agent meets the mesh's words most often in tool descriptions, and
nothing compared them with the glossary: several still said "the host"
for the node-engine and the forge's pull request comment was headed
"Change plan", a word retired twice over. retired-words is the copy of
the words the glossary retires for the tools, and checks/words fails the
repository check when any string a module's code can show, or any
manifest description, uses one. Those found are reworded here.
This commit is contained in:
jochen
2026-10-07 20:02:09 +02:00
parent c74f407abd
commit 73bb597c16
16 changed files with 367 additions and 16 deletions
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/words
go 1.22
+327
View File
@@ -0,0 +1,327 @@
// Command words holds what the catalogue's modules say to an agent to the glossary's words (novox/hq ADR
// 0244): no word the glossary retired for the tools' descriptions — the copy in retired-words at the
// catalogue's root — in any text a module's tools can show. That text is every string literal in a module's
// own code that is not a test (a tool's description, the notes and errors it answers with) and every
// description in its manifest. Comments are not read: an agent never sees them.
//
// A word is matched whole and in any case, a space in it matching any run of white space. A vendor word is
// never on the list (the glossary does not retire one for the tools), so a wrapped program's own objects —
// an identity provider's users, a media manager's releases — are never findings.
//
// go run . <catalogue root>
//
// Exits 1 on a finding, 2 when the list cannot be read.
package main
import (
"bufio"
"encoding/json"
"fmt"
"go/scanner"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
// text is one piece of text an agent can be shown, and where it starts.
type text struct {
file string
line int
s string
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
words, err := readList(filepath.Join(root, "retired-words"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
}
if len(words) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-words lists no word — the check would pass on anything")
os.Exit(2)
}
var texts []text
files := 0
err = filepath.WalkDir(filepath.Join(root, "modules"), func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case "node_modules", "dist", "vendor", "test", "tests", "testdata", ".git":
return filepath.SkipDir
}
return nil
}
rel, _ := filepath.Rel(root, path)
name := d.Name()
var found []text
switch {
case strings.HasSuffix(name, "_test.go"), strings.Contains(name, ".test."), strings.Contains(name, ".spec."):
return nil
case strings.HasSuffix(name, ".go"):
found, err = goStrings(path, rel)
case strings.HasSuffix(name, ".ts"), strings.HasSuffix(name, ".js"), strings.HasSuffix(name, ".mjs"):
found, err = scriptStrings(path, rel)
case name == "module.json":
found, err = jsonStrings(path, rel)
default:
return nil
}
if err != nil {
return fmt.Errorf("%s: %w", rel, err)
}
files++
texts = append(texts, found...)
return nil
})
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
}
findings := check(words, texts)
for _, f := range findings {
fmt.Println(f)
}
if len(findings) > 0 {
fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings))
os.Exit(1)
}
fmt.Printf("words: %d retired words, none in the %d strings of %d files\n", len(words), len(texts), files)
}
// readList reads retired-words: one word or phrase a line; blank lines and lines starting with # are not words.
func readList(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var words []string
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
words = append(words, line)
}
return words, sc.Err()
}
func pattern(word string) *regexp.Regexp {
parts := strings.Fields(word)
for i, p := range parts {
parts[i] = regexp.QuoteMeta(p)
}
return regexp.MustCompile(`(?i)(?:^|[^\w-])(` + strings.Join(parts, `\s+`) + `)(?:$|[^\w-])`)
}
func check(words []string, texts []text) []string {
var out []string
for _, w := range words {
rx := pattern(w)
for _, t := range texts {
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
line := t.line
if line > 0 {
line += strings.Count(t.s[:m[2]], "\n")
}
out = append(out, fmt.Sprintf("%s:%d: %q is retired — in %q", t.file, line, w, excerpt(t.s, m[2], m[3])))
}
}
}
sort.Strings(out)
return out
}
func excerpt(s string, from, to int) string {
a, b := from-50, to+50
if a < 0 {
a = 0
}
if b > len(s) {
b = len(s)
}
return strings.Join(strings.Fields(s[a:b]), " ")
}
// goStrings returns a Go file's string literals, adjacent ones joined across `+` as the program joins them.
func goStrings(path, rel string) ([]text, error) {
src, err := os.ReadFile(path)
if err != nil {
return nil, err
}
fset := token.NewFileSet()
file := fset.AddFile(rel, -1, len(src))
var s scanner.Scanner
var bad error
s.Init(file, src, func(pos token.Position, msg string) { bad = fmt.Errorf("%s: %s", pos, msg) }, 0)
var out []text
var cur *text
joining := false
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
switch {
case tok == token.STRING:
v, err := strconv.Unquote(lit)
if err != nil {
v = lit
}
if cur != nil && joining {
cur.s += v
} else {
out = append(out, text{file: rel, line: fset.Position(pos).Line, s: v})
cur = &out[len(out)-1]
}
joining = false
case tok == token.ADD && cur != nil:
joining = true
default:
cur, joining = nil, false
}
}
return out, bad
}
// scriptStrings returns a TypeScript or JavaScript file's string literals, comments skipped, adjacent ones
// joined across `+`. A template literal's `${…}` parts are left out of the text around them.
func scriptStrings(path, rel string) ([]text, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
src := string(b)
var out []text
line := 1
lastWasString, joining := false, false
for i := 0; i < len(src); i++ {
c := src[i]
switch {
case c == '\n':
line++
case c == '/' && i+1 < len(src) && src[i+1] == '/':
for i < len(src) && src[i] != '\n' {
i++
}
line++
case c == '/' && i+1 < len(src) && src[i+1] == '*':
end := strings.Index(src[i+2:], "*/")
if end < 0 {
end = len(src) - i - 2
}
line += strings.Count(src[i:i+2+end], "\n")
i += end + 3
case c == '"' || c == '\'' || c == '`':
start := line
var sb strings.Builder
depth := 0
j := i + 1
for ; j < len(src); j++ {
d := src[j]
if d == '\n' {
line++
}
if depth > 0 {
if d == '\n' {
sb.WriteByte('\n')
}
if d == '{' {
depth++
} else if d == '}' {
depth--
}
continue
}
if d == '\\' && j+1 < len(src) {
j++
sb.WriteByte(src[j])
continue
}
if c == '`' && d == '$' && j+1 < len(src) && src[j+1] == '{' {
depth = 1
j++
sb.WriteByte(' ')
continue
}
if d == c || (c != '`' && d == '\n') {
break
}
sb.WriteByte(d)
}
if lastWasString && joining && len(out) > 0 {
out[len(out)-1].s += sb.String()
} else {
out = append(out, text{file: rel, line: start, s: sb.String()})
}
lastWasString, joining = true, false
i = j
continue
case c == '+' && lastWasString:
joining = true
continue
case c == ' ' || c == '\t' || c == '\r':
continue
default:
lastWasString, joining = false, false
}
if c == '\n' {
continue
}
}
return out, nil
}
// jsonStrings returns the descriptions in a manifest — every string under a key named "description", at any
// depth. The rest of a manifest is names, paths and the contents of files it places, none of which a tool
// shows an agent.
func jsonStrings(path, rel string) ([]text, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var v any
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
var out []text
var walk func(any, bool)
walk = func(v any, described bool) {
switch x := v.(type) {
case string:
if described {
out = append(out, text{file: rel, line: lineOf(string(b), x), s: x})
}
case []any:
for _, e := range x {
walk(e, described)
}
case map[string]any:
for k, e := range x {
walk(e, k == "description")
}
}
}
walk(v, false)
return out, nil
}
func lineOf(src, s string) int {
q, _ := json.Marshal(s)
if i := strings.Index(src, string(q)); i >= 0 {
return strings.Count(src[:i], "\n") + 1
}
return 0
}
+7 -1
View File
@@ -14,9 +14,15 @@
# long-running resources without `health` held to the number in health-undeclared, which only goes down;
# 2. the Go tests of every module the change touches that has them, under the race detector when the
# toolchain has a C compiler — and a module whose dependencies cannot be fetched here, or that is
# written in TypeScript, is said as not tested, never passed silently.
# written in TypeScript, is said as not tested, never passed silently;
# 3. the words (novox/hq ADR 0244): no word the glossary retired for the tools, as copied in
# retired-words, in any text a module's tools can show an agent — every string in its own code that is
# not a test, and every description in its manifest. Run over every module, not only the touched ones,
# so a word newly retired is found everywhere it stands.
set -eu
(cd checks/words && go run . ../..)
if [ -n "${MESH_GATE:-}" ]; then
checked=$("$MESH_GATE" module check modules/*/module.json) || { printf '%s\n' "$checked"; exit 1; }
# **The count only goes down** (novox/hq ADR 0240 rule 8): the long-running resources that do not say how
+1 -1
View File
@@ -527,7 +527,7 @@ func (c *Client) Act(ctx context.Context, verb, ref string) (map[string]any, err
answer := map[string]any{"container": s.Name, "verb": verb, "ok": true, "state": s.State, "mesh_held": s.MeshHeld}
if s.MeshHeld {
answer["held_by"] = s.HeldBy
answer["note"] = fmt.Sprintf("the mesh holds this container (%s): the host restores what its declaration says at its next apply", s.HeldBy)
answer["note"] = fmt.Sprintf("the mesh holds this container (%s): the node-engine restores what its declaration says at its next apply", s.HeldBy)
}
return answer, nil
}
@@ -188,7 +188,7 @@ func TestActingOnAMeshContainerSaysTheHostRestoresIt(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !f.ran("docker stop --time 10 mesh-web") || got["mesh_held"] != true || !strings.Contains(got["note"].(string), "host restores") {
if !f.ran("docker stop --time 10 mesh-web") || got["mesh_held"] != true || !strings.Contains(got["note"].(string), "node-engine restores") {
t.Fatalf("%v %+v", got, f.calls)
}
got, _ = client(f, 1000).Act(context.Background(), "start", "dev-db")
+2 -2
View File
@@ -140,8 +140,8 @@ func tools(c *Client) []stdio.Tool {
return map[string]any{"count": len(stats), "containers": stats}, nil
},
},
act("start", "Start one container. A container the mesh holds is started too, and the answer says the host restores what its declaration says at its next apply."),
act("stop", "Stop one container (ten seconds, then killed). For a container the mesh holds, the answer says the host will start it again at its next apply if its declaration says running."),
act("start", "Start one container. A container the mesh holds is started too, and the answer says the node-engine restores what its declaration says at its next apply."),
act("stop", "Stop one container (ten seconds, then killed). For a container the mesh holds, the answer says the node-engine will start it again at its next apply if its declaration says running."),
act("restart", "Restart one container (ten seconds to stop, then killed); the answer says whether the mesh holds it."),
{
Name: "docker_top",
+2 -2
View File
@@ -68,9 +68,9 @@ function builds(plan?: ChangePlan): boolean {
return !!plan && ((plan.moved?.length ?? 0) > 0 || (plan.new?.length ?? 0) > 0);
}
/** A change plan as a person reads it on the pull request. */
/** A delivery plan (the controller's ChangePlan) as a person reads it on the pull request. */
export function planText(plan: ChangePlan): string {
const lines = [`**Change plan** — ${plan.summary}`];
const lines = [`**Delivery plan** — ${plan.summary}`];
(plan.tiers ?? []).forEach((tier, i) => lines.push(`- tier ${i}: ${tier.join(", ")}`));
for (const m of plan.machines ?? []) {
const parts: string[] = [];
+1 -1
View File
@@ -137,7 +137,7 @@ test("a change plan is the gate's result: said on the status and, when it builds
summary: "every machine composes", gate: { verdict: "pass", summary: "every machine composes", modules: ["gitea"] }, plan };
assert.equal(statusFor(c).description, "pass: builds gitea → anchor; no bus step; every machine composes");
const said = commentFor(c) ?? "";
assert.match(said, /Change plan\*\* — builds gitea → anchor/);
assert.match(said, /Delivery plan\*\* — builds gitea → anchor/);
assert.match(said, /- anchor: receives gitea/);
// A plan that builds nothing, passing: the statuses say it, no comment.
const nothing = { ...c, plan: { ...plan, moved: [], tiers: [], machines: [], summary: "builds nothing" } };
+1 -1
View File
@@ -100,7 +100,7 @@ node -e ${shellQuote(
`const fs=require("fs");const f=${JSON.stringify(join(dir, "status.json"))};const s=JSON.parse(fs.readFileSync(f,"utf8"));s.commits=Object.fromEntries(fs.readFileSync(${JSON.stringify(join(dir, "commits.txt"))},"utf8").trim().split("\\n").map(l=>l.split(" ")));fs.writeFileSync(f,JSON.stringify(s,null,2))`,
)}
${setState("building")}
# The @novox scope resolves from the mesh's own package registry on the forge, as the build machine
# The @novox scope resolves from the mesh's own package registry on the forge, as the builder
# resolves it; nothing else is asked of it.
printf '%s\n' ${shellQuote(`@novox:registry=${forge}/api/packages/novox/npm/`)} > ${shellQuote(join(dir, ".npmrc"))}
export NPM_CONFIG_USERCONFIG=${shellQuote(join(dir, ".npmrc"))}
+1 -1
View File
@@ -186,7 +186,7 @@ export class FirewallClient {
}
return { where, did };
}
throw new Error(`${JSON.stringify(where)} is not a rule set as the host reports one: ` +
throw new Error(`${JSON.stringify(where)} is not a rule set as the node-engine reports one: ` +
"`chain X (iptables-legacy)` or `table <family> <name>, chain X`");
}
+1 -1
View File
@@ -63,7 +63,7 @@ test("what is not the operator's to remove is refused by name", async () => {
await assert.rejects(c.remove("chain DOCKER (iptables-legacy)"), /container runtime's own/);
await assert.rejects(c.remove("chain FORWARD (iptables-legacy)"), /built in/);
await assert.rejects(c.remove("chain ufw6-docker-logging-deny (ip6tables-legacy)"), /found firewall, which is in force/);
await assert.rejects(c.remove("something else"), /not a rule set as the host reports one/);
await assert.rejects(c.remove("something else"), /not a rule set as the node-engine reports one/);
// Retired, a front end's leftover is nobody's and goes.
const retired = await new FirewallClient(fake(false).run, undefined, () => true).remove("chain ufw6-docker-logging-deny (ip6tables-legacy)");
assert.ok(retired.did.includes("ip6tables-legacy -X ufw6-docker-logging-deny"));
+1 -1
View File
@@ -594,7 +594,7 @@ func (m *Machine) Check() (CheckAnswer, error) {
return a, err
}
if v == "" {
add("Slack ("+packageFor+") is not installed", "install it from the AUR: the module does not install it, because the host installs packages from the official repositories only")
add("Slack ("+packageFor+") is not installed", "install it from the AUR: the module does not install it, because the node-engine installs packages from the official repositories only")
}
mine := 0
for _, st := range m.starts() {
+1 -1
View File
@@ -283,7 +283,7 @@ func (m *Manager) Act(scope Scope, verb, unit string) (map[string]any, error) {
answer := map[string]any{"unit": unit, "scope": string(scope), "verb": verb, "ok": true,
"active": after["ActiveState"], "boot": after["UnitFileState"], "mesh_declared": after["mesh_declared"]}
if after["mesh_declared"] == true {
answer["note"] = "the mesh declares this unit: the host restores its declared state at its next apply"
answer["note"] = "the mesh declares this unit: the node-engine restores its declared state at its next apply"
}
return answer, nil
}
@@ -155,7 +155,7 @@ func TestTheRestoreNoteIsOnlyOnAUnitTheMeshDeclares(t *testing.T) {
return Ran{}
}, nil))
r, _ := m.Act(System, "stop", "showcase.service")
if r["mesh_declared"] != true || !strings.Contains(r["note"].(string), "host restores its declared state") {
if r["mesh_declared"] != true || !strings.Contains(r["note"].(string), "node-engine restores its declared state") {
t.Fatalf("%v", r)
}
}
+2 -2
View File
@@ -92,8 +92,8 @@ func tools(m *Manager) []stdio.Tool {
}
return m.Status(scope, unit)
}},
act("start", "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at its next apply."),
act("stop", "Stop one unit; for a unit the mesh declares, the answer says the host will restore its declared state."),
act("start", "Start one unit. For a unit the mesh declares, the answer says the node-engine will restore what its declaration says at its next apply."),
act("stop", "Stop one unit; for a unit the mesh declares, the answer says the node-engine will restore its declared state."),
act("restart", "Restart one unit."),
act("enable", "Make one unit start at boot (or at the account's login, in user scope)."),
act("disable", "Stop one unit starting at boot (or at login, in user scope)."),
+15
View File
@@ -0,0 +1,15 @@
# The words the glossary retired for the descriptions of the mesh's tools (novox/hq ADR 0244): its
# *Not:* words with no scope or with (tools). A copy, so a catalogue merge needs nothing else; novox/hq's
# words.py compares it with the glossary. Regenerate with: python3 00-META/checks/words.py --list tools
build machine
change plan
control plane
flavor
host agent
mesh-console
node host
node tools
release plan
substrate
tool bridge
tools-sdk