feat: add docs checker and record templates

docs new creates ADRs and plans from embedded templates; docs check reports
path:line: message problems per docs/docs-format.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
endandClaude Sonnet 5.5 committed 2026-10-23 14:46:56 -07:00
1 parent b0428256bd
commit 750b7449b0
15 files changed
+1856

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
package docs
import (
"slices"
"strings"
)
// graph checks cross-record rules: unique IDs and supersession.
func (c *checker) graph(recs []*record) {
byStem := map[string][]*record{}
byPath := map[string]*record{}
for _, r := range recs {
byStem[r.stem] = append(byStem[r.stem], r)
byPath["/"+r.path] = r
}
for _, group := range byStem {
for _, r := range group {
for _, o := range group {
if o != r {
c.add(r.path, 1, "duplicate ID %s, also used by %s", r.stem, o.path)
}
}
}
}
adrs := recs[:0:0]
for _, r := range recs {
if r.kind == "adr" && r.fm != nil {
adrs = append(adrs, r)
}
}
for _, r := range adrs {
if len(r.lists["supersedes"]) > 0 && !slices.Contains([]string{"accepted", "superseded", "deprecated"}, r.status) {
c.add(r.path, r.fm.line("supersedes"), "a record that supersedes another must be accepted, superseded or deprecated, not %s", r.status)
}
for _, e := range [][2]string{{"supersedes", "superseded_by"}, {"superseded_by", "supersedes"}} {
for _, it := range r.lists[e[0]] {
if o := byPath[it.v]; o != nil && o.kind == "adr" && !slices.ContainsFunc(o.lists[e[1]], func(x item) bool { return x.v == "/"+r.path }) {
c.add(r.path, it.line, "%s lists %s, which must list this record in %s", e[0], it.v, e[1])
}
}
}
}
// Cycles over supersedes edges: report once, on the first record (in path order) that closes one.
const (
visiting = 1
done = 2
)
state := map[*record]int{}
var visit func(r *record)
visit = func(r *record) {
state[r] = visiting
for _, it := range r.lists["supersedes"] {
o := byPath[it.v]
if o == nil || o.kind != "adr" {
continue
}
switch state[o] {
case visiting:
c.add(r.path, it.line, "supersession cycle through %s", strings.TrimPrefix(it.v, "/"))
case 0:
visit(o)
}
}
state[r] = done
}
for _, r := range adrs {
if state[r] == 0 {
visit(r)
}
}
}