Files
hq/00-META/checks/words_test.go
T

757 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: "<daemon> <command>", 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" +
"<!-- overlay\nover lines -->\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
}