package checks import ( "fmt" "io/fs" "path" "sort" "strings" "testing" "testing/fstest" ) // The development cycle, checked (cycle.py until ADR 0527; ADR 0080). Everything here reads frontmatter only, // because a status this repository holds lives in frontmatter and nowhere else. The decision records and research // efforts live in the knowledge base (ADR 0290), whose rules judge them. var designStatuses = map[string]bool{"proposed": true, "designed": true, "in-progress": true, "implemented": true, "abandoned": true} // The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275), and // `issue-tracker.move` enforces the cycle's rules at the write. A report carrying one is a status in two places. var trackerKeys = []string{"status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved"} // checkDesigns: every design parses, carries `layer:` matching its folder and a known `status:`; a to-be design // names a decision, and once in-progress or implemented names its owning code. As-is designs need no decisions. func checkDesigns(fsys fs.FS) []string { var out []string for _, layer := range []struct{ folder, name string }{{"00-as-is", "as-is"}, {"01-to-be", "to-be"}} { entries, _ := fs.ReadDir(fsys, "03-DESIGN/"+layer.folder) for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || e.Name() == "README.md" { continue } p := path.Join("03-DESIGN", layer.folder, e.Name()) front := frontmatter(readFile(fsys, p)) if front == nil { out = append(out, "cycle: "+p+": no frontmatter") continue } if front.scalar("layer") != layer.name { out = append(out, fmt.Sprintf("cycle: %s: layer is %q; this directory is %s", p, front.scalar("layer"), layer.name)) } status := front.scalar("status") if !designStatuses[status] { var known []string for s := range designStatuses { known = append(known, s) } sort.Strings(known) out = append(out, fmt.Sprintf("cycle: %s: status %q is not one of %v", p, status, known)) } if layer.name == "to-be" { if len(front.list("decisions")) == 0 { out = append(out, "cycle: "+p+": names no decisions -- no design without a decision") } if (status == "in-progress" || status == "implemented") && len(front.list("code")) == 0 { out = append(out, fmt.Sprintf("cycle: %s: status %s but code: names no owner -- no development without a design that says where", p, status)) } } } } return out } // checkIssues: no two issue folders share a number (issue 155: the number is how a record is cited), every report // has frontmatter with `opened:`, and none carries a key the tracker holds. func checkIssues(fsys fs.FS) []string { var out []string entries, _ := fs.ReadDir(fsys, "04-ISSUES") seen := map[string]string{} for _, e := range entries { if !e.IsDir() { continue } number := strings.SplitN(e.Name(), "-", 2)[0] if number == "" || strings.Trim(number, "0123456789") != "" { continue } if first, ok := seen[number]; ok { out = append(out, fmt.Sprintf("cycle: 04-ISSUES/%s: is numbered %s, and so is %s -- an issue number is how it is cited, and two records "+ "answering to one means a citation that resolves to whichever the reader opened. Take the next free number across main AND every open pull request", e.Name(), number, first)) } else { seen[number] = e.Name() } } for _, e := range entries { p := path.Join("04-ISSUES", e.Name(), "00-report.md") if !e.IsDir() { continue } if _, err := fs.Stat(fsys, p); err != nil { continue } front := frontmatter(readFile(fsys, p)) if front == nil { out = append(out, "cycle: "+p+": no frontmatter") continue } if front.scalar("opened") == "" { out = append(out, "cycle: "+p+": opened: says nothing -- the day the issue was opened, YYYY-MM-DD") continue } var carried []string for _, k := range trackerKeys { if _, ok := front[k]; ok { carried = append(carried, "`"+k+":`") } } if len(carried) > 0 { out = append(out, fmt.Sprintf("cycle: %s: carries %s, but an issue's status, owner, fix and replay live in the issue tracker (ADR 0275): "+ "move it with `issue-tracker.move`, and keep no copy here", p, strings.Join(carried, ", "))) } } return out } func TestCycleDesignsHoldTheChain(t *testing.T) { expectNone(t, checkDesigns(rootFS())) } func TestCycleIssuesHoldTheChain(t *testing.T) { expectNone(t, checkIssues(rootFS())) } func design(layer, status, extra string) *fstest.MapFile { return doc("---\nlayer: " + layer + "\nstatus: " + status + "\n" + extra + "---\nbody\n") } func TestCycleDesignsCatchesEachBreak(t *testing.T) { broken := fstest.MapFS{ "03-DESIGN/00-as-is/a-nofront.md": doc("no frontmatter"), "03-DESIGN/00-as-is/b-wronglayer.md": design("to-be", "implemented", ""), "03-DESIGN/00-as-is/c-badstatus.md": design("as-is", "done", ""), "03-DESIGN/01-to-be/d-nodecision.md": design("to-be", "designed", ""), "03-DESIGN/01-to-be/e-nocode.md": design("to-be", "in-progress", "decisions: [ADR 0001]\n"), "03-DESIGN/01-to-be/README.md": doc("not a design"), } got := checkDesigns(broken) expectProblem(t, got, "a-nofront.md: no frontmatter") expectProblem(t, got, `b-wronglayer.md: layer is "to-be"; this directory is as-is`) expectProblem(t, got, `c-badstatus.md: status "done" is not one of`) expectProblem(t, got, "d-nodecision.md: names no decisions") expectProblem(t, got, "e-nocode.md: status in-progress but code: names no owner") if len(got) != 5 { t.Errorf("exactly the five breaks, as-is needs no decisions and a README is no design: %q", got) } } func TestCycleDesignsAcceptsASoundPair(t *testing.T) { sound := fstest.MapFS{ "03-DESIGN/00-as-is/a.md": design("as-is", "implemented", ""), "03-DESIGN/01-to-be/b.md": design("to-be", "in-progress", "decisions: [ADR 0001]\ncode: [mesh-sdk]\n"), } expectNone(t, checkDesigns(sound)) } func TestCycleIssuesCatchesEachBreak(t *testing.T) { report := func(front string) *fstest.MapFile { return doc("---\n" + front + "---\nbody\n") } broken := fstest.MapFS{ "04-ISSUES/100-a/00-report.md": report("opened: 2026-10-01\n"), "04-ISSUES/100-b/00-report.md": report("opened: 2026-10-01\n"), "04-ISSUES/101-noopened/00-report.md": report("title: x\n"), "04-ISSUES/102-tracker/00-report.md": report("opened: 2026-10-01\nstatus: open\nreplay: none\n"), "04-ISSUES/103-nofront/00-report.md": doc("plain"), "04-ISSUES/notes/readme.md": doc("not an issue"), } got := checkIssues(broken) expectProblem(t, got, "04-ISSUES/100-b: is numbered 100, and so is 100-a") expectProblem(t, got, "101-noopened/00-report.md: opened: says nothing") expectProblem(t, got, "102-tracker/00-report.md: carries `status:`, `replay:`") expectProblem(t, got, "103-nofront/00-report.md: no frontmatter") if len(got) != 4 { t.Errorf("exactly the four breaks: %q", got) } }