package checks import ( "fmt" "io/fs" "os" "path" "regexp" "runtime" "sort" "strings" "sync" "testing" "testing/fstest" "time" "unicode" ) // One word per thing, checked (words.py until ADR 0527; ADR 0244). The glossary (00-META/glossary.md) is the // authority on the mesh's words. It names the words it retired on one fixed kind of line, so a program can read them: // // *Not:* ~~control plane~~, ~~overlay~~ (hq) // // A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only; `(tools)` // in the descriptions of the mesh's tools only. And it names code that still carries an old name until it is renamed: // // *Identifier until renamed:* `mesh-host` - the repository, binary and service unit // // An identifier is allowed inside a code span and nowhere else. // // What is enforced: // // retired no retired word of scope hq (or none), and no identifier, in running prose of a checked document. // Running prose is what is left once code blocks, code spans, block quotes, text in quotation marks, // struck-through text, link targets, HTML comments and frontmatter are taken out. A word's plural is the word. // unique no head word heads two entries, and none is also struck through on a *Not:* line. // lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09): the operator types `nox`. // copy with MESH_CATALOG_DIR set to a checkout of the catalogue, its copies of the retired words and the // daemons' lines equal this glossary's and this list (skipped, with its reason, when unset). // // Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened on or after // wordsFrom (on or after linesFrom for the lines). A document that cannot be reworded in the change that found it is // named, with a date and a reason, in words-allowed.md beside this file; an entry past its date fails like the word // itself, and so does an entry for a document that no longer needs it. const ( glossaryFile = "00-META/glossary.md" allowedFile = "00-META/checks/words-allowed.md" // The day the rule began (ADR 0244): issues opened from it on are held to it. wordsFrom = "2026-10-07" // The day the daemons' lines rule began. linesFrom = "2026-10-09" ) var wordsAlways = []string{"00-META/", "03-DESIGN/", "AGENTS.md", "README.md"} // daemonLines are the daemons' lines (ADR 0272): each daemon's commands. "alone" is a command that is a line by // itself; "with an argument" one that is a line only when written as one ("the mesh-controller module" is prose; // `mesh-controller module check ...` is a line). The node-engine's install-time subcommands are not lines here. var daemonLines = []struct { daemon, alone, withArgument string }{ {"mesh-controller", "adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act hand-acts healers identity kill licence " + "migrate operator pause pin plan prepare push rebuild rehearse replay resume retire rollout rotate secret settings status token " + "unassign unpin upgrade", "artifacts ask builds bus collection data delivery images mirrors module node overlay plans queue seat seats take version"}, {"mesh-host", "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version", ""}, {"nox-mesh-host", "hand-over", ""}, } var ( notLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Not:\*(.*)$`) struckRx = regexp.MustCompile(`~~([^~]+)~~(?:\s*\((hq|tools)\))?`) identLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$`) spanRx = regexp.MustCompile("`([^`]+)`") headRx = regexp.MustCompile(`(?m)^\s*-\s+\*\*([^*]+)\*\*`) openedRx = regexp.MustCompile(`(?m)^opened:\s*(\S+)`) ) type retiredWord struct{ word, scope string } // parseGlossary reads the retired words with their scope, the identifiers and the head words from the glossary. func parseGlossary(text string) (retired []retiredWord, identifiers, heads []string) { for _, m := range notLineRx.FindAllStringSubmatch(text, -1) { for _, s := range struckRx.FindAllStringSubmatch(m[1], -1) { scope := s[2] if scope == "" { scope = "all" } for _, one := range strings.Split(s[1], " / ") { retired = append(retired, retiredWord{strings.TrimSpace(one), scope}) } } } for _, m := range identLineRx.FindAllStringSubmatch(text, -1) { for _, s := range spanRx.FindAllStringSubmatch(m[1], -1) { identifiers = append(identifiers, strings.TrimSpace(s[1])) } } for _, m := range headRx.FindAllStringSubmatch(text, -1) { for _, h := range strings.Split(m[1], " / ") { heads = append(heads, strings.TrimSpace(h)) } } return } func isWordRune(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' } // findBounded returns the start offsets of the matches of rx that are whole words: no word character or hyphen // before or after (the lookarounds of the Python check, which RE2 has not). func findBounded(rx *regexp.Regexp, text string) []int { var out []int pos := 0 for pos <= len(text) { loc := rx.FindStringIndex(text[pos:]) if loc == nil { break } start, end := pos+loc[0], pos+loc[1] before, after := true, true if start > 0 { r := []rune(text[max(0, start-4):start]) before = !isWordRune(r[len(r)-1]) } if end < len(text) { r := []rune(text[end:min(len(text), end+4)]) after = !isWordRune(r[0]) } if before && after { out = append(out, start) pos = end if end == start { pos++ } } else { pos = start + 1 } } return out } // wordRegexp matches a word whole, case-insensitively; a space in the word matches a space, a line break or a hyphen. // The plural is the word too: its last part may end in s or es ("alerts", "control planes"). func wordRegexp(word string) *regexp.Regexp { var parts []string for _, p := range strings.Fields(word) { parts = append(parts, regexp.QuoteMeta(p)) } return regexp.MustCompile(`(?i)` + strings.Join(parts, `[\s-]+`) + `(?:e?s)?`) } // blank replaces every byte of s but the line breaks, so offsets and line numbers hold. func blank(s string) string { b := []byte(s) for i, c := range b { if c != '\n' { b[i] = ' ' } } return string(b) } func blankFrontmatter(text string) string { if strings.HasPrefix(text, "---\n") { if end := strings.Index(text[4:], "\n---"); end != -1 { end += 4 return blank(text[:end+4]) + text[end+4:] } } return text } func blankAll(text string, steps []*regexp.Regexp) string { for _, rx := range steps { text = rx.ReplaceAllStringFunc(text, blank) } return text } func compileAll(patterns ...string) []*regexp.Regexp { var out []*regexp.Regexp for _, p := range patterns { out = append(out, regexp.MustCompile(p)) } return out } // What is not the document's own words, in the order it is taken out. const ( stepComment = `(?s)` stepBlockQuote = `(?m)^\s*>.*$` stepLinkTarget = `\]\([^)\s]*\)` stepStruck = `~~[^~\n]+~~` stepQuoted = `"[^"\n]*"` stepCurlyDouble = `\x{201c}[^\x{201d}]*\x{201d}` stepCurlySingle = `\x{2018}[^\x{2019}\n]*\x{2019}` ) var ( proseSteps = compileAll("(?ms)^\\s*```.*?^\\s*```", stepComment, stepBlockQuote, "`[^`\\n]+`", stepLinkTarget, stepStruck, stepQuoted, stepCurlyDouble, stepCurlySingle) // In the glossary the *Not:* and *Identifier until renamed:* lines name the retired words, so they are taken out first. glossaryProseSteps = append(compileAll(notLineRx.String(), identLineRx.String()), proseSteps...) // The daemons' lines are looked for in the document's own words: a code block and a code span are kept, since a // line to type is written in one. ownWordsSteps = compileAll(stepComment, stepBlockQuote, stepLinkTarget, stepStruck, stepQuoted, stepCurlyDouble, stepCurlySingle) ) // prose is the text with everything that is not running prose blanked, keeping offsets and line numbers. func prose(text string, glossaryDoc bool) string { text = blankFrontmatter(text) if glossaryDoc { return blankAll(text, glossaryProseSteps) } return blankAll(text, proseSteps) } func ownWords(text string) string { return blankAll(blankFrontmatter(text), ownWordsSteps) } // issueDocuments lists the issue reports' files opened on or after since. func issueDocuments(fsys fs.FS, since string) []string { var out []string entries, _ := fs.ReadDir(fsys, "04-ISSUES") for _, e := range entries { if !e.IsDir() { continue } head := readFile(fsys, path.Join("04-ISSUES", e.Name(), "00-report.md")) front := "" if strings.HasPrefix(head, "---\n") { if end := strings.Index(head[4:], "\n---"); end != -1 { if m := openedRx.FindStringSubmatch(head[4 : 4+end]); m != nil { front = m[1] } } } if front == "" || front < since { continue } files, _ := fs.ReadDir(fsys, path.Join("04-ISSUES", e.Name())) for _, f := range files { if strings.HasSuffix(f.Name(), ".md") { out = append(out, path.Join("04-ISSUES", e.Name(), f.Name())) } } } return out } // perDocument runs fn over the documents on all processors (the unit level is short) and joins the problems in // the documents' order. func perDocument(docs []string, fn func(doc string) []string) []string { results := make([][]string, len(docs)) var wg sync.WaitGroup sem := make(chan struct{}, runtime.NumCPU()) for i, d := range docs { wg.Add(1) sem <- struct{}{} go func() { defer wg.Done() results[i] = fn(d) <-sem }() } wg.Wait() var out []string for _, r := range results { out = append(out, r...) } return out } func checkedDocuments(fsys fs.FS, since string) []string { var docs []string for _, entry := range wordsAlways { if strings.HasSuffix(entry, "/") { _ = fs.WalkDir(fsys, strings.TrimSuffix(entry, "/"), func(p string, d fs.DirEntry, err error) error { if err == nil && !d.IsDir() && strings.HasSuffix(p, ".md") { docs = append(docs, p) } return nil }) } else if _, err := fs.Stat(fsys, entry); err == nil { docs = append(docs, entry) } } return append(docs, issueDocuments(fsys, since)...) } var dateRx = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) // parseAllowed reads words-allowed.md: one row per document, | `document` | until | why |. Until is a date, after // which the allowance fails like the words themselves. The clock is the caller's: today is YYYY-MM-DD. func parseAllowed(text, today string) (allowed map[string]string, problems []string) { allowed = map[string]string{} for _, line := range strings.Split(text, "\n") { cells := strings.Split(strings.Trim(strings.TrimSpace(line), "|"), "|") if len(cells) != 3 { continue } for i := range cells { cells[i] = strings.TrimSpace(cells[i]) } if !strings.HasPrefix(cells[0], "`") { continue } doc, until, why := strings.Trim(cells[0], "`"), cells[1], cells[2] if why == "" { problems = append(problems, fmt.Sprintf("%s: the allowance for %s gives no reason", allowedFile, doc)) } if !dateRx.MatchString(until) { problems = append(problems, fmt.Sprintf("%s: the allowance for %s has no date", allowedFile, doc)) continue } if until < today { problems = append(problems, fmt.Sprintf("%s: the allowance for %s ran out on %s — reword it", allowedFile, doc, until)) continue } allowed[doc] = until } return } func checkRetired(fsys fs.FS, retired []retiredWord, identifiers []string, today string) []string { allowance, out := parseAllowed(readFile(fsys, allowedFile), today) type matcher struct { word string rx *regexp.Regexp why string } var words []matcher for _, r := range retired { if r.scope == "all" || r.scope == "hq" { words = append(words, matcher{r.word, wordRegexp(r.word), "retired"}) } } for _, i := range identifiers { words = append(words, matcher{i, wordRegexp(i), "an identifier, allowed only in a code span"}) } var mu sync.Mutex used := map[string]bool{} out = append(out, perDocument(checkedDocuments(fsys, wordsFrom), func(d string) []string { var found []string text := prose(readFile(fsys, d), d == glossaryFile) lower := strings.ToLower(text) for _, w := range words { // Every match holds the word's first part literally, so a text without it is not searched (the unit level is short). if !strings.Contains(lower, strings.ToLower(strings.Fields(w.word)[0])) { continue } for _, start := range findBounded(w.rx, text) { if _, ok := allowance[d]; ok { mu.Lock() used[d] = true mu.Unlock() continue } m := w.rx.FindString(text[start:]) found = append(found, fmt.Sprintf("%s:%d: %q is %s (glossary)", d, lineOf(text, start), strings.ReplaceAll(m, "\n", " "), w.why)) } } return found })...) var unused []string for d := range allowance { if !used[d] { unused = append(unused, d) } } sort.Strings(unused) for _, d := range unused { out = append(out, fmt.Sprintf("%s: %s no longer uses a retired word — remove its allowance", allowedFile, d)) } return out } func checkUnique(retired []retiredWord, heads []string) []string { var out []string seen := map[string]bool{} for _, h := range heads { k := strings.ToLower(h) if seen[k] { out = append(out, fmt.Sprintf("%s: %q heads two entries", glossaryFile, h)) } seen[k] = true } for _, r := range retired { if seen[strings.ToLower(r.word)] { out = append(out, fmt.Sprintf("%s: %q is a head word and also retired", glossaryFile, r.word)) } } return out } // linesList is the daemons' lines as the catalogue's copy lists them: " ", with " …" when an // argument must follow. func linesList() []string { var out []string for _, d := range daemonLines { for _, c := range strings.Fields(d.alone) { out = append(out, d.daemon+" "+c) } for _, c := range strings.Fields(d.withArgument) { out = append(out, d.daemon+" "+c+" …") } } sort.Strings(out) return out } func checkLines(fsys fs.FS) []string { type pat struct { daemon, command string rx *regexp.Regexp bounded bool } var pats []pat for _, d := range daemonLines { dq := regexp.QuoteMeta(d.daemon) for _, c := range strings.Fields(d.alone) { pats = append(pats, pat{d.daemon, c, regexp.MustCompile(dq + `\s+` + regexp.QuoteMeta(c)), true}) } for _, c := range strings.Fields(d.withArgument) { // A line only when written as one: at the start of a line or a code span, a prompt allowed, an argument after. pats = append(pats, pat{d.daemon, c, regexp.MustCompile("(?m)(?:^|`)[ \\t]*(?:\\$[ \\t]*)?(" + dq + `\s+` + regexp.QuoteMeta(c) + "\\s+[^\\s`])"), false}) } } return perDocument(checkedDocuments(fsys, linesFrom), func(d string) []string { var out []string text := ownWords(readFile(fsys, d)) for _, p := range pats { if !strings.Contains(text, p.daemon) { continue } var starts []int if p.bounded { starts = findBounded(p.rx, text) } else { for _, loc := range p.rx.FindAllStringSubmatchIndex(text, -1) { starts = append(starts, loc[2]) } } for _, s := range starts { out = append(out, fmt.Sprintf("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)", d, lineOf(text, s), p.daemon, p.command)) } } return out }) } func toolsList(retired []retiredWord) []string { seen := map[string]bool{} var out []string for _, r := range retired { if (r.scope == "all" || r.scope == "tools") && !seen[r.word] { seen[r.word] = true out = append(out, r.word) } } sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i]) < strings.ToLower(out[j]) }) return out } func readList(file string) ([]string, error) { b, err := os.ReadFile(file) if err != nil { return nil, err } var out []string for _, l := range strings.Split(string(b), "\n") { if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") { out = append(out, l) } } return out, nil } // checkCopy compares the catalogue's copies of the retired words and the daemons' lines with this glossary's. A // difference names what the copy should list, so regenerating it is copying that from the failure. func checkCopy(catalogue string, retired []retiredWord) []string { var out []string copyWords, err := readList(path.Join(catalogue, "retired-words")) if err != nil { return []string{"the catalogue's copy of the retired words cannot be read: " + err.Error()} } sort.Slice(copyWords, func(i, j int) bool { return strings.ToLower(copyWords[i]) < strings.ToLower(copyWords[j]) }) want := toolsList(retired) if strings.Join(copyWords, "\n") != strings.Join(want, "\n") { out = append(out, "the catalogue's retired-words differs from the glossary: it should list "+strings.Join(want, ", ")) } copyLines, err := readList(path.Join(catalogue, "retired-lines")) if err != nil { return append(out, "the catalogue's copy of the daemons' lines cannot be read: "+err.Error()) } sort.Strings(copyLines) if strings.Join(copyLines, "\n") != strings.Join(linesList(), "\n") { out = append(out, "the catalogue's retired-lines differs from this check's list: it should list, one per line:\n"+strings.Join(linesList(), "\n")) } return out } // The real tree. func today() string { return time.Now().Format("2006-01-02") } func TestWordsGlossaryNamesRetiredWords(t *testing.T) { retired, identifiers, heads := parseGlossary(readFile(rootFS(), glossaryFile)) if len(retired) == 0 { t.Fatal("the glossary names no retired word on a *Not:* line — the check would pass on anything") } if len(identifiers) == 0 || len(heads) == 0 { t.Errorf("the glossary names %d identifiers and %d head words; both are expected", len(identifiers), len(heads)) } } func TestWordsEachHeadWordHeadsOneEntry(t *testing.T) { retired, _, heads := parseGlossary(readFile(rootFS(), glossaryFile)) expectNone(t, checkUnique(retired, heads)) } func TestWordsNoRetiredWordInRunningProse(t *testing.T) { retired, identifiers, _ := parseGlossary(readFile(rootFS(), glossaryFile)) expectNone(t, checkRetired(rootFS(), retired, identifiers, today())) } func TestWordsNoDaemonLineWritten(t *testing.T) { expectNone(t, checkLines(rootFS())) } func TestWordsCatalogueCopyAgrees(t *testing.T) { catalogue := os.Getenv("MESH_CATALOG_DIR") if catalogue == "" { t.Skip("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalogue's copy of the list was not read") } retired, _, _ := parseGlossary(readFile(rootFS(), glossaryFile)) expectNone(t, checkCopy(catalogue, retired)) } // The checks themselves, against broken trees (H5a: the glossary). const glossaryFixture = `# Glossary - **control plane** — the retired one's replacement is below. *Not:* ~~control plane~~, ~~overlay~~ (hq), ~~alert~~ (tools), ~~a / b~~ - **controller** — decides. *Not:* ~~control plane~~ - **Controller** — again. - *Identifier until renamed:* ` + "`mesh-host`, `nox-old`" + ` — the repository ` func TestGlossaryReadsNotLinesScopesIdentifiersAndHeads(t *testing.T) { retired, identifiers, heads := parseGlossary(glossaryFixture) want := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}, {"a", "all"}, {"b", "all"}, {"control plane", "all"}} if fmt.Sprint(retired) != fmt.Sprint(want) { t.Errorf("retired = %v, want %v", retired, want) } if fmt.Sprint(identifiers) != "[mesh-host nox-old]" { t.Errorf("identifiers = %v", identifiers) } if fmt.Sprint(heads) != "[control plane controller Controller]" { t.Errorf("heads = %v", heads) } got := checkUnique(retired, heads) expectProblem(t, got, `"Controller" heads two entries`) expectProblem(t, got, `"control plane" is a head word and also retired`) } func TestToolsListHoldsTheWordsOfScopeAllAndTools(t *testing.T) { retired, _, _ := parseGlossary(glossaryFixture) if got := fmt.Sprint(toolsList(retired)); got != "[a alert b control plane]" { t.Errorf("toolsList = %s", got) } } // H5b: prose blanking and matching. func TestProseBlanksWhatIsNotRunningProse(t *testing.T) { text := "---\nfm: overlay\n---\n" + "bare overlay one\n" + "```\noverlay in a block\n```\n" + "span `overlay` x\n" + "> quoted overlay\n" + "say \"overlay\" and “overlay” and ‘overlay’\n" + "~~overlay~~ struck\n" + "[link](overlay.md)\n" + "\n" + "bare overlay two\n" got := prose(text, false) if strings.Count(got, "overlay") != 2 { t.Errorf("only the two bare uses stay: %q", got) } if strings.Count(got, "\n") != strings.Count(text, "\n") || len(got) != len(text) { t.Error("blanking keeps offsets and line numbers") } } func TestProseOfTheGlossaryBlanksNotAndIdentifierLines(t *testing.T) { got := prose("- **x** — y\n *Not:* control plane, plain\n- *Identifier until renamed:* mesh-host\nthe control plane\n", true) if strings.Count(got, "control plane") != 1 || strings.Contains(got, "mesh-host") { t.Errorf("only the running use stays: %q", got) } } func TestWordMatchesWholeWordsPluralsAndLineBreaks(t *testing.T) { rx := wordRegexp("control plane") count := func(s string) int { return len(findBounded(rx, s)) } for s, want := range map[string]int{ "the control plane runs": 1, "Control Planes": 1, "control-plane and control\nplane": 2, "uncontrol plane": 0, "control planet": 0, "control plane-x": 0, "x-control plane": 0, "control planexs": 0, } { if got := count(s); got != want { t.Errorf("%q: %d matches, want %d", s, got, want) } } } func TestRetiredCatchesAWordInProseAndNamesDocumentAndLine(t *testing.T) { retired := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}} broken := mapOf(map[string]string{ "AGENTS.md": "fine\nthe control plane here\n", "03-DESIGN/01-to-be/a.md": "an overlay and \"a control plane quoted\" and `overlay` alert\n", "README.md": "mesh-host runs; `mesh-host` too\n", }) got := checkRetired(broken, retired, []string{"mesh-host"}, "2026-10-23") expectProblem(t, got, `AGENTS.md:2: "control plane" is retired`) expectProblem(t, got, `03-DESIGN/01-to-be/a.md:1: "overlay" is retired`) expectProblem(t, got, `README.md:1: "mesh-host" is an identifier, allowed only in a code span`) if len(got) != 3 { t.Errorf("a quotation, a code span and a word of scope tools are not problems: %q", got) } } func TestIssuesAreHeldToTheRuleFromItsDay(t *testing.T) { retired := []retiredWord{{"overlay", "all"}} fsys := mapOf(map[string]string{ "04-ISSUES/1-old/00-report.md": "---\nopened: 2026-10-06\n---\noverlay\n", "04-ISSUES/2-new/00-report.md": "---\nopened: 2026-10-07\n---\nfine\n", "04-ISSUES/2-new/01-notes.md": "an overlay\n", "04-ISSUES/3-nodate/00-report.md": "overlay\n", }) got := checkRetired(fsys, retired, nil, "2026-10-23") expectProblem(t, got, "04-ISSUES/2-new/01-notes.md:1") if len(got) != 1 { t.Errorf("only the issue opened from 2026-10-07 on is read: %q", got) } } // H5c: allowances, with an injected clock. const allowedFixture = `# Words allowed | Document | Until | Why | |---|---|---| | ` + "`a.md`" + ` | 2026-12-01 | quotes what was | | ` + "`b.md`" + ` | 2026-10-01 | ran out | | ` + "`c.md`" + ` | soon | no date | | ` + "`d.md`" + ` | 2026-12-01 | | ` func TestAllowedReadsRowsAndTheInjectedClock(t *testing.T) { allowed, problems := parseAllowed(allowedFixture, "2026-10-23") if _, ok := allowed["a.md"]; !ok || len(allowed) != 2 { t.Errorf("a.md and d.md are in date: %v", allowed) } expectProblem(t, problems, "the allowance for b.md ran out on 2026-10-01") expectProblem(t, problems, "the allowance for c.md has no date") expectProblem(t, problems, "the allowance for d.md gives no reason") // The same text on an earlier day: b.md is still in date. if early, _ := parseAllowed(allowedFixture, "2026-09-30"); len(early) != 3 { t.Errorf("on 2026-09-30 a, b and d are in date: %v", early) } } func TestAllowanceHoldsAWordBackUntilItsDateAndShrinks(t *testing.T) { retired := []retiredWord{{"overlay", "all"}} allowedRow := "| `AGENTS.md` | 2026-12-01 | quotes what was |\n" tree := func(agents, allowed string) map[string]string { return map[string]string{"AGENTS.md": agents, allowedFile: allowed} } expectNone(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-10-23")) expectProblem(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-12-02"), "ran out on 2026-12-01") expectProblem(t, checkRetired(mapOf(tree("clean\n", allowedRow)), retired, nil, "2026-10-23"), "AGENTS.md no longer uses a retired word") expectProblem(t, checkRetired(mapOf(tree("an overlay\n", "")), retired, nil, "2026-10-23"), `AGENTS.md:1: "overlay" is retired`) } // H5d: the daemons' lines and the catalogue comparison. func TestLinesCatchesADaemonsLineButNotProse(t *testing.T) { fsys := mapOf(map[string]string{ "AGENTS.md": "the mesh-controller module is prose\n" + "run `mesh-controller module check x` now\n" + "or:\n```\n$ mesh-controller push\n```\n" + "a quotation \"mesh-controller status\" stays\n" + "> mesh-controller doctor in a block quote\n" + "a mesh-controller pushed is another word\n", "04-ISSUES/1/00-report.md": "---\nopened: 2026-10-08\n---\nmesh-host apply\n", "04-ISSUES/2/00-report.md": "---\nopened: 2026-10-09\n---\nmesh-host apply\n", }) got := checkLines(fsys) expectProblem(t, got, "AGENTS.md:2: `mesh-controller module` is a daemon's line") expectProblem(t, got, "AGENTS.md:5: `mesh-controller push` is a daemon's line") expectProblem(t, got, "04-ISSUES/2/00-report.md:4: `mesh-host apply`") if len(got) != 3 { t.Errorf("prose, quotations, block quotes and issues before 2026-10-09 are not problems: %q", got) } } func TestLinesListNamesEachLineOnce(t *testing.T) { list := linesList() seen := map[string]bool{} for _, l := range list { if seen[l] { t.Errorf("%q listed twice", l) } seen[l] = true } if !seen["mesh-controller push"] || !seen["mesh-controller module …"] || !seen["nox-mesh-host hand-over"] { t.Errorf("the list lacks a known line: %v", list[:5]) } } func TestCatalogueCopyDifferenceIsCaughtAndNamesTheList(t *testing.T) { retired := []retiredWord{{"overlay", "all"}, {"alert", "tools"}, {"hq only", "hq"}} dir := t.TempDir() write := func(name, content string) { if err := os.WriteFile(path.Join(dir, name), []byte(content), 0o644); err != nil { t.Fatal(err) } } write("retired-words", "# copy\nalert\noverlay\n") write("retired-lines", strings.Join(linesList(), "\n")+"\n") expectNone(t, checkCopy(dir, retired)) write("retired-words", "alert\n") write("retired-lines", "mesh-controller push\n") got := checkCopy(dir, retired) expectProblem(t, got, "retired-words differs from the glossary: it should list alert, overlay") expectProblem(t, got, "retired-lines differs from this check's list") expectProblem(t, checkCopy(t.TempDir(), retired), "cannot be read") } func mapOf(files map[string]string) fs.FS { m := fstest.MapFS{} for k, v := range files { m[k] = doc(v) } return m }