// Package checks holds hq's four checks as Go tests (ADR 0527): records, index, cycle and words. // // The package has tests only. Each check is a function over an fs.FS that returns its problems, so // the test of the real tree reads the repository's root, and the test of the check itself reads a // deliberately broken tree built in memory and must see the problem named (every check here failed on // a broken fixture before it passed). The shared reading of Markdown and frontmatter is in this file. package checks import ( "io/fs" "os" "regexp" "sort" "strings" "testing" ) // rootFS is hq's tree: the checks run from 00-META/checks, two folders under the root. func rootFS() fs.FS { return os.DirFS("../..") } // skipDirs are folders no check reads: git's, the agent tooling's and a session's scratch folder. var skipDirs = map[string]bool{".git": true, ".claude": true, ".work": true} // markdownFiles lists every Markdown file of the tree, sorted, in slash paths from the root. func markdownFiles(fsys fs.FS) []string { var out []string _ = fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() { if p != "." && skipDirs[d.Name()] { return fs.SkipDir } return nil } if strings.HasSuffix(p, ".md") { out = append(out, p) } return nil }) sort.Strings(out) return out } func readFile(fsys fs.FS, p string) string { b, err := fs.ReadFile(fsys, p) if err != nil { return "" } return string(b) } // Front is a document's frontmatter: a scalar field holds a string, a list field a []string. type Front map[string]any func (f Front) scalar(key string) string { s, _ := f[key].(string) return s } func (f Front) list(key string) []string { switch v := f[key].(type) { case []string: return v case string: if strings.TrimSpace(v) != "" { return []string{v} } } return nil } var ( fmItem = regexp.MustCompile(`^\s+-\s*(.+?)\s*$`) fmPair = regexp.MustCompile(`^([A-Za-z_-]+):\s*(.*)$`) ) // frontmatter reads the block between the first two --- lines. It is not a YAML parser on purpose: a // dependency to read a few scalar fields and one list would cost more than it returns. It returns nil // when the document has no frontmatter. func frontmatter(text string) Front { if !strings.HasPrefix(text, "---\n") { return nil } end := strings.Index(text[4:], "\n---") if end == -1 { return nil } out := Front{} key := "" for _, line := range strings.Split(text[4:4+end], "\n") { if m := fmItem.FindStringSubmatch(line); m != nil && key != "" { list, _ := out[key].([]string) out[key] = append(list, m[1]) continue } m := fmPair.FindStringSubmatch(line) if m == nil { continue } key = m[1] value := strings.TrimSpace(m[2]) switch { case strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]"): list := []string{} for _, v := range strings.Split(value[1:len(value)-1], ",") { if v = strings.TrimSpace(v); v != "" { list = append(list, v) } } out[key] = list case value == "": out[key] = []string{} default: out[key] = value } } return out } func unquote(v string) string { v = strings.TrimSpace(v) if len(v) >= 2 && v[0] == v[len(v)-1] && (v[0] == '"' || v[0] == '\'') { return v[1 : len(v)-1] } return v } // lineOf is the 1-based line of byte offset i in text. func lineOf(text string, i int) int { return strings.Count(text[:i], "\n") + 1 } // expectNone fails the test with every problem a check found in the real tree. func expectNone(t *testing.T, problems []string) { t.Helper() for _, p := range problems { t.Error(p) } } // expectProblem fails unless some problem contains want: the broken fixture must be caught, by name. func expectProblem(t *testing.T, problems []string, want string) { t.Helper() for _, p := range problems { if strings.Contains(p, want) { return } } t.Errorf("no problem mentions %q; got %q", want, problems) }