From 0ad95768b6e187c9154bc132669095365dd3e334 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 15:07:07 +0200 Subject: [PATCH] Run records.py's five checks as Go tests, each proven against a broken tree first (issue 1296) --- 00-META/checks/records_test.go | 206 +++++++++++++++++++++++++++++++++ 00-META/checks/walk_test.go | 1 - 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 00-META/checks/records_test.go diff --git a/00-META/checks/records_test.go b/00-META/checks/records_test.go new file mode 100644 index 00000000..7f8cd64a --- /dev/null +++ b/00-META/checks/records_test.go @@ -0,0 +1,206 @@ +package checks + +import ( + "fmt" + "io/fs" + "path" + "regexp" + "strings" + "testing" + "testing/fstest" +) + +// Structural checks over hq's own documents (records.py until ADR 0527). The decision records and research +// efforts live in the knowledge base (ADR 0290, issue 426); what stays here judges what this repository still +// holds, and keeps the records from coming back. + +var ( + linkRx = regexp.MustCompile(`\[[^\]]*\]\(([^)]+)\)`) + // A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three. + decisionEntryRx = regexp.MustCompile(`^ADR \d{4}$`) + adrMentionRx = regexp.MustCompile(`\bADRs?\s+(\d+)\b`) + researchMentionRx = regexp.MustCompile(`\b[Rr]esearch\s+(\d+)\b`) + codeSpanRx = regexp.MustCompile("`[^`]*`") + recordFolders = []string{"02-DECISIONS", "01-RESEARCH"} + recordFolderPoints = map[string]bool{"02-DECISIONS/README.md": true, "01-RESEARCH/README.md": true} +) + +// checkLinks: every relative link resolves to something that exists. +func checkLinks(fsys fs.FS) []string { + var out []string + for _, p := range markdownFiles(fsys) { + for n, line := range strings.Split(readFile(fsys, p), "\n") { + for _, m := range linkRx.FindAllStringSubmatch(line, -1) { + target := m[1] + if strings.HasPrefix(target, "http:") || strings.HasPrefix(target, "https:") || strings.HasPrefix(target, "mailto:") { + continue + } + target = strings.TrimSpace(strings.SplitN(target, "#", 2)[0]) + if target == "" { + continue + } + if _, err := fs.Stat(fsys, path.Join(path.Dir(p), target)); err != nil { + out = append(out, fmt.Sprintf("links: %s:%d: link does not resolve: %s", p, n+1, target)) + } + } + } + } + return out +} + +// checkNoRecords: the two folders the records left hold their pointer README and nothing else (ADR 0290 rule 9). +func checkNoRecords(fsys fs.FS) []string { + var out []string + for _, folder := range recordFolders { + if _, err := fs.Stat(fsys, folder); err != nil { + out = append(out, "no-records: "+folder+": the folder is gone; it keeps a README that points at the knowledge base") + continue + } + _ = fs.WalkDir(fsys, folder, func(p string, d fs.DirEntry, err error) error { + if err != nil || p == folder || recordFolderPoints[p] { + return nil + } + out = append(out, "no-records: "+p+": a decision record or research effort lives in the knowledge base, never here: "+ + "write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)") + return nil + }) + } + return out +} + +// checkDecisionsEntries: a design's `decisions:` names each record as "ADR NNNN". +func checkDecisionsEntries(fsys fs.FS) []string { + var out []string + for _, p := range markdownFiles(fsys) { + if !strings.HasPrefix(p, "03-DESIGN/") { + continue + } + for _, e := range frontmatter(readFile(fsys, p)).list("decisions") { + if !decisionEntryRx.MatchString(unquote(e)) { + out = append(out, fmt.Sprintf("decisions-form: %s: decisions: entry %q is not 'ADR NNNN' (four digits)", p, e)) + } + } + } + return out +} + +// checkMentions: in running text a decision record is "ADR NNNN" and a research effort "research NNN"; a code +// span is an example and is not read. +func checkMentions(fsys fs.FS) []string { + var out []string + for _, p := range markdownFiles(fsys) { + for n, line := range strings.Split(readFile(fsys, p), "\n") { + line = codeSpanRx.ReplaceAllString(line, "") + for _, m := range adrMentionRx.FindAllStringSubmatch(line, -1) { + if len(m[1]) != 4 { + out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a decision record is named 'ADR NNNN', four digits", p, n+1, m[0])) + } + } + for _, m := range researchMentionRx.FindAllStringSubmatch(line, -1) { + if len(m[1]) != 3 { + out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a research effort is named 'research NNN', three digits", p, n+1, m[0])) + } + } + } + } + return out +} + +var codeEntryRx = regexp.MustCompile(`\s*\(.*\)$`) + +// checkStatusAgainstCode: a to-be design naming specific code (a path after the repository) may not still be +// `designed`. Deliberately weak: it cannot tell whether the prose is true, only that a document stopped claiming +// to be unbuilt once it points at something. `code: [mesh-controller]` is a plan and stays `designed`. +func checkStatusAgainstCode(fsys fs.FS) []string { + var out []string + for _, p := range markdownFiles(fsys) { + if !strings.HasPrefix(p, "03-DESIGN/01-to-be/") || strings.HasSuffix(p, "README.md") { + continue + } + front := frontmatter(readFile(fsys, p)) + if front.scalar("status") != "designed" { + continue + } + for _, e := range front.list("code") { + named := strings.SplitN(strings.TrimSpace(codeEntryRx.ReplaceAllString(e, "")), " ", 2) + if len(named) > 1 { + out = append(out, fmt.Sprintf("status-vs-code: %s: `designed`, but names %q in %s. Naming a file claims it implements this "+ + "- use `in-progress`, or `implemented` once it is defensible from that repository's main branch.", p, strings.TrimSpace(named[1]), named[0])) + break + } + } + } + return out +} + +func doc(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} } + +// The real tree. + +func TestRecordsLinksResolve(t *testing.T) { expectNone(t, checkLinks(rootFS())) } +func TestRecordsFoldersHoldOnlyPointers(t *testing.T) { expectNone(t, checkNoRecords(rootFS())) } +func TestRecordsDecisionsEntriesForm(t *testing.T) { expectNone(t, checkDecisionsEntries(rootFS())) } +func TestRecordsMentionForm(t *testing.T) { expectNone(t, checkMentions(rootFS())) } +func TestRecordsStatusAgainstCode(t *testing.T) { expectNone(t, checkStatusAgainstCode(rootFS())) } + +// The checks themselves, each against a broken fixture, and against a sound one. + +func TestRecordsLinksCatchesABrokenLink(t *testing.T) { + broken := fstest.MapFS{ + "a/one.md": doc("see [two](two.md) and [gone](gone.md#x) and [web](https://example.com/x)\n"), + "a/two.md": doc("[up](../a/one.md#top) [anchor](#here)\n"), + } + got := checkLinks(broken) + expectProblem(t, got, "a/one.md:1: link does not resolve: gone.md") + if len(got) != 1 { + t.Errorf("only the broken link is a problem, got %q", got) + } +} + +func TestRecordsNoRecordsCatchesARecordPutBack(t *testing.T) { + broken := fstest.MapFS{ + "02-DECISIONS/README.md": doc("pointer"), + "02-DECISIONS/0001-x.md": doc("a record"), + "01-RESEARCH/README.md": doc("pointer"), + "01-RESEARCH/045/00-x.md": doc("an effort"), + } + got := checkNoRecords(broken) + expectProblem(t, got, "02-DECISIONS/0001-x.md") + expectProblem(t, got, "01-RESEARCH/045") + expectProblem(t, checkNoRecords(fstest.MapFS{"02-DECISIONS/README.md": doc("p")}), "01-RESEARCH: the folder is gone") +} + +func TestRecordsDecisionsFormCatchesAShortEntry(t *testing.T) { + broken := fstest.MapFS{ + "03-DESIGN/01-to-be/1.md": doc("---\nlayer: to-be\ndecisions:\n - ADR 0290\n - ADR 290\n---\n"), + "00-META/x.md": doc("---\ndecisions: [ADR 5]\n---\n"), + } + got := checkDecisionsEntries(broken) + expectProblem(t, got, `entry "ADR 290"`) + if len(got) != 1 { + t.Errorf("only a design's entries are read, and ADR 0290 is sound: %q", got) + } +} + +func TestRecordsMentionFormCatchesShortNumbers(t *testing.T) { + broken := fstest.MapFS{"x.md": doc("fine ADR 0290 and research 045\nbad ADR 12 and research 45\nexample `ADR 7` and `research 9`\n")} + got := checkMentions(broken) + expectProblem(t, got, "x.md:2: \"ADR 12\"") + expectProblem(t, got, "x.md:2: \"research 45\"") + if len(got) != 2 { + t.Errorf("code spans and sound forms are not problems: %q", got) + } +} + +func TestRecordsStatusAgainstCodeCatchesADesignedDesignNamingAFile(t *testing.T) { + broken := fstest.MapFS{ + "03-DESIGN/01-to-be/9.md": doc("---\nstatus: designed\ncode:\n - mesh-controller internal/x.go (the thing)\n---\n"), + "03-DESIGN/01-to-be/8.md": doc("---\nstatus: designed\ncode: [mesh-controller]\n---\n"), + "03-DESIGN/01-to-be/7.md": doc("---\nstatus: in-progress\ncode: [mesh-controller internal/x.go]\n---\n"), + } + got := checkStatusAgainstCode(broken) + expectProblem(t, got, "03-DESIGN/01-to-be/9.md: `designed`, but names \"internal/x.go\" in mesh-controller") + if len(got) != 1 { + t.Errorf("a plan without a path and an in-progress design are sound: %q", got) + } +} diff --git a/00-META/checks/walk_test.go b/00-META/checks/walk_test.go index af987431..f231bbe8 100644 --- a/00-META/checks/walk_test.go +++ b/00-META/checks/walk_test.go @@ -148,4 +148,3 @@ func expectProblem(t *testing.T, problems []string, want string) { } t.Errorf("no problem mentions %q; got %q", want, problems) } -