feat: add docs checker for ADRs and implementation plans (#1)
Add cmd/docs, the first corestack component: `docs new adr|plan "Title"` writes records from embedded templates, and `docs check` validates them against the public format spec in docs/docs-format.md (timestamp IDs, frontmatter keys and enums, separate decision and implementation status, supersession symmetry, MADR 4 layout with required Consequences, plan sections, and repo-root/durable references), reporting path:line errors. Also adds the module skeleton (go.auvem.com/corestack, BSD-3-Clause, README, AGENTS.md, CLAUDE.md), Gitea CI running tests and docs check, a tag-triggered goreleaser release for darwin/linux binaries, and the accepted ADR adopting the format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit was merged in pull request #1.
This commit is contained in:
1 parent
37b0516fd2
commit
cc482d9a9b
27 files changed
+2464
No files matched your search
@@ -0,0 +1,130 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLinkDestinations covers the section 7 rules through one body link.
|
||||
func TestLinkDestinations(t *testing.T) {
|
||||
tests := []struct{ dest, want string }{ // want "" means valid
|
||||
{"/docs/spec.txt", ""},
|
||||
{"/docs/spec.txt#frag", ""},
|
||||
{"/docs/sp%20ace.md", ""},
|
||||
{"#anchor", ""},
|
||||
{"mailto:a@example.com", ""},
|
||||
{"https://example.com/a?b=1#c", ""},
|
||||
{`/docs/spec\.txt`, ""},
|
||||
{"/docs/spec.txt", ""},
|
||||
{"/docs/spec.txt", ""},
|
||||
{`/docs\\spec.txt`, "backslash"},
|
||||
{"/docs/SPEC.txt", "does not exist"},
|
||||
{"/Docs/spec.txt", "does not exist"},
|
||||
{"foo.md", "relative"},
|
||||
{"./foo.md", "relative"},
|
||||
{"../docs/spec.txt", "relative"},
|
||||
{"/docs/../docs/spec.txt", `".."`},
|
||||
{"/docs/%2e%2e/docs/spec.txt", `".."`},
|
||||
{`/docs\spec.txt`, "backslash"},
|
||||
{`C:\spec.txt`, "backslash"},
|
||||
{"//example.com/x", "protocol-relative"},
|
||||
{"/docs/spec.txt?x=1", "queries"},
|
||||
{"/docs/%zz", "percent-encoding"},
|
||||
{"/docs/missing.txt", "does not exist"},
|
||||
{"/Users/me/spec.txt", "does not exist"},
|
||||
{"/docs", "not a regular file"},
|
||||
{"/docs/out/f.md", "outside the repository"},
|
||||
{"file:///etc/passwd", "scheme"},
|
||||
{"ftp://example.com/x", "scheme"},
|
||||
{"javascript:alert(1)", "scheme"},
|
||||
{"https://u:p@example.com/x", "credentials"},
|
||||
{"https:///nohost", "host"},
|
||||
{"<>", "empty"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.dest, func(t *testing.T) {
|
||||
dest := tt.dest
|
||||
if strings.ContainsAny(dest, " ") {
|
||||
dest = "<" + dest + ">"
|
||||
}
|
||||
doc := adrBase + "\nSee [x](" + dest + ").\n"
|
||||
res, err := Check(tree(t, withSupport(map[string]string{aPath: doc}), map[string]string{"docs/out": "@outside"}))
|
||||
must(t, err)
|
||||
var want []string
|
||||
if tt.want != "" {
|
||||
want = []string{aPath + ":35: " + tt.want}
|
||||
}
|
||||
expect(t, res.Problems, want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinkForms covers which Markdown constructs are link destinations.
|
||||
func TestLinkForms(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, body string
|
||||
want []string
|
||||
}{
|
||||
{"image", "", []string{":35: relative"}},
|
||||
{"autolink", "<ftp://example.com/x>", []string{":35: scheme"}},
|
||||
{"reference link and definition", "[x][r]\n\n[r]: foo.md", []string{":35: relative", ":37: relative"}},
|
||||
{"duplicate definition is checked", "[a]: /docs/spec.txt\n\n[a]: ../bad.md", []string{":37: relative"}},
|
||||
{"definition in a blockquote", "> [Zed]: ../bad.md", []string{":35: relative"}},
|
||||
{"definition-like line in code", "```\n[a]: x\n```\n\n[a]: ../bad.md", []string{":39: relative"}},
|
||||
{"definition-like paragraph continuation", "Some text\n[a]: ../x.md\n\n[a]: ../bad.md", []string{":36: relative", ":38: relative"}}, // [a] in the text is a reference link
|
||||
{"definition-like line in an HTML comment", "<!--\n[a]: x\n-->\n\n[a]: ../bad.md", []string{":39: relative"}},
|
||||
{"autolink on a later line", "Para one\nline two\n<ftp://example.com/x>", []string{":37: scheme"}},
|
||||
{"image without alt on a later line", "Para one\nline two\n", []string{":37: relative"}},
|
||||
{"unused definition", "[r]: foo.md", []string{":35: relative"}},
|
||||
{"link in a list", "* [x](foo.md)", []string{":35: relative"}},
|
||||
{"bare URL and raw HTML are not checked", "http://u:p@example.com and @user #123 abc1234\n\n<a href=\"foo.md\">x</a>", nil},
|
||||
{"link in code is not a link", "`[x](foo.md)`\n\n```\n[x](foo.md)\n```", nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res, err := Check(tree(t, withSupport(map[string]string{aPath: adrBase + "\n" + tt.body + "\n"}), nil))
|
||||
must(t, err)
|
||||
var want []string
|
||||
for _, w := range tt.want {
|
||||
want = append(want, aPath+w)
|
||||
}
|
||||
expect(t, res.Problems, want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpecExamples keeps the minimal examples in docs/docs-format.md valid.
|
||||
func TestSpecExamples(t *testing.T) {
|
||||
spec, err := os.ReadFile("../../docs/docs-format.md")
|
||||
must(t, err)
|
||||
root := t.TempDir()
|
||||
parts := strings.Split(string(spec[strings.Index(string(spec), "## 9."):]), "```markdown\n")
|
||||
if len(parts) != 3 {
|
||||
t.Fatalf("expected two examples, got %d", len(parts)-1)
|
||||
}
|
||||
// The path of each example is the backticked name on the line before its fence.
|
||||
heads := strings.Split(string(spec[strings.Index(string(spec), "## 9."):]), "\n")
|
||||
var paths []string
|
||||
for _, l := range heads {
|
||||
if strings.HasPrefix(l, "`docs/") {
|
||||
paths = append(paths, strings.Trim(l, "`:"))
|
||||
}
|
||||
}
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("expected two example paths, got %v", paths)
|
||||
}
|
||||
for i, part := range parts[1:] {
|
||||
body, _, _ := strings.Cut(part, "```")
|
||||
full := filepath.Join(root, paths[i])
|
||||
must(t, os.MkdirAll(filepath.Dir(full), 0o755))
|
||||
must(t, os.WriteFile(full, []byte(body), 0o644))
|
||||
}
|
||||
res, err := Check(root)
|
||||
must(t, err)
|
||||
expect(t, res.Problems, nil)
|
||||
if res.Records != 2 {
|
||||
t.Fatalf("records = %d", res.Records)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user