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>
72 lines
1.9 KiB
Go
72 lines
1.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|