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:
1 parent
b0428256bd
commit
750b7449b0
15 files changed
+1856
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user