Files
mesh-catalog/checks/words/main.go
T
jschoubben 0bfc72d230 Make the controller checks strict: nothing names the placed-nowhere controller
controllerdeps refuses any manifest naming the controller's seat, states or
buckets, the lease and the entries once kept to its write among them, and
any module file naming one of its buckets (ADR 0590 §5). The words check
refuses a controller address in any text a module shows. The handacts check
and noplacement's list of assignments readers are folded into it (issue 1126 B10).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00

423 lines
12 KiB
Go

// 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. And no daemon's line
// (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to
// type of the node-engine's or the controller's binary, where the operator's command is `nox`. And no address of the
// controller (novox/hq ADR 0590 §5): it is not placed, so a text naming one of its verbs, states or buckets sends its
// reader nowhere.
//
// A word is matched whole and in any case, a space in it matching any run of white space, and its plural
// (`s` or `es` on its last part) is matched as the word. 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]
}
os.Exit(run(root))
}
// run reads every text a module can show under root and answers the command's exit code: 0 clean, 1 a finding, 2 a list or a
// file unreadable. The command prints what it found; a test of the real catalogue reads the code.
func run(root string) int {
words, err := readList(filepath.Join(root, "retired-words"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
return 2
}
if len(words) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-words lists no word — the check would pass on anything")
return 2
}
texts, files, err := collect(root)
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
return 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))
return 1
}
// And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line.
lines, err := readLines(filepath.Join(root, "retired-lines"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
return 2
}
if len(lines) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything")
return 2
}
if findings := checkLines(lines, texts); len(findings) > 0 {
for _, f := range findings {
fmt.Println(f)
}
fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n",
len(findings))
return 1
}
// And no address of the controller (novox/hq ADR 0590 §5): it is not placed, so a text that sends an agent or the
// operator to one of its verbs, states or buckets sends them nowhere.
if findings := checkAddresses(texts); len(findings) > 0 {
for _, f := range findings {
fmt.Println(f)
}
fmt.Printf("words: %d address(es) of the controller in what a module shows — name the seat that holds the job now (novox/hq ADR 0590)\n",
len(findings))
return 1
}
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
len(texts), files)
return 0
}
// controllerAddress is an address of the controller: its seat's verb or state (`mesh-controller.<name>`, with or without
// `seat:`) or one of its buckets (`mesh-controller_<bucket>`). The module's name in prose is no address.
var controllerAddress = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller[._][A-Za-z0-9<]`)
// checkAddresses is every text that names an address of the controller (novox/hq ADR 0590 §5).
func checkAddresses(texts []text) []string {
var out []string
for _, t := range texts {
if !strings.Contains(t.s, "mesh-controller") {
continue
}
if m := controllerAddress.FindStringIndex(t.s); m != nil {
line := t.line
if line > 0 {
line += strings.Count(t.s[:m[0]], "\n")
}
out = append(out, fmt.Sprintf("%s:%d: names an address of the controller, which is not placed — in %q", t.file, line,
excerpt(t.s, m[0], m[1])))
}
}
sort.Strings(out)
return out
}
// collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of
// its manifests.
func collect(root string) ([]text, int, error) {
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
})
return texts, files, err
}
// 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)
}
// The plural is the word too: `s` or `es` on its last part, so "control planes" is found as "control
// plane" is. Anything else joined on is another word.
return regexp.MustCompile(`(?i)(?:^|[^\w-])(` + strings.Join(parts, `\s+`) + `(?:e?s)?)(?:$|[^\w-])`)
}
func check(words []string, texts []text) []string {
var out []string
// A text can hold a word only if it holds the word's first part, in any case: the regular expression is run on those alone,
// which keeps the check of the whole catalogue within the unit level's bound.
lower := make([]string, len(texts))
for i, t := range texts {
lower[i] = strings.ToLower(t.s)
}
for _, w := range words {
rx := pattern(w)
first := ""
if parts := strings.Fields(w); len(parts) > 0 {
first = strings.ToLower(parts[0])
}
for i, t := range texts {
if !strings.Contains(lower[i], first) {
continue
}
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]), " ")
}
// modulePath matches a string that is only a module or import path ("git.example.org/org/repo/go/pkg").
var modulePath = regexp.MustCompile(`^[a-z0-9.-]+\.[a-z]+/[A-Za-z0-9_./-]+$`)
// 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 modulePath.MatchString(v) {
// An import path or another module path names code, not words an agent reads.
cur, joining = nil, false
continue
}
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
}