feat(11.1-02): check links, commands, forbidden names and fence policy

- relative links and anchors resolve against the renderer's heading IDs
- summer and ./bin/<app> command names come from the real command
  constructors through docsite.Options.Commands; a nil set is a problem
- consuming-application names fail in page sources and built outputs
- go fences in docs/ pages need src=, callouts are NOTE, TIP or WARNING,
  docs/ headings are plain ASCII
- gate gains --claude and self-test plants for each new rule
This commit is contained in:
Jakub Zych
2026-09-30 21:40:46 +02:00
parent f4605292b5
commit 5d4c1e3046
12 changed files with 808 additions and 14 deletions

View File

@@ -34,6 +34,9 @@ func problemLines(problems []Problem) []string {
return out
}
// fixtureCommands is the command set the fixture trees are checked with.
var fixtureCommands = &Commands{Tool: []string{"docs:build", "make:plugin"}, App: []string{"serve", "migrate"}}
const fixtureSite = `title: Acme
description: Acme docs.
sections:
@@ -110,7 +113,7 @@ func TestReadmeIngestion(t *testing.T) {
t.Fatal(err)
}
out := filepath.Join(t.TempDir(), "site")
res, problems, err := Build(Options{Root: root, Out: out})
res, problems, err := Build(Options{Root: root, Commands: fixtureCommands, Out: out})
if err != nil || len(problems) > 0 {
t.Fatalf("Build: %v %q", err, problemLines(problems))
}
@@ -302,7 +305,7 @@ func TestSnippetConfinement(t *testing.T) {
if err := os.Symlink(outside, filepath.Join(root, "escape.txt")); err != nil {
t.Fatal(err)
}
problems, err := Check(Options{Root: root})
problems, err := Check(Options{Root: root, Commands: fixtureCommands})
if err != nil {
t.Fatal(err)
}
@@ -316,7 +319,7 @@ func TestSnippetConfinement(t *testing.T) {
t.Errorf("src=%s: no problem %q...%q in\n%s", tc.src, prefix, tc.want, strings.Join(got, "\n"))
}
}
if _, _, err := Build(Options{Root: root, Out: filepath.Join(t.TempDir(), "site")}); err != nil {
if _, _, err := Build(Options{Root: root, Commands: fixtureCommands, Out: filepath.Join(t.TempDir(), "site")}); err != nil {
t.Fatal(err)
}
}
@@ -327,7 +330,7 @@ func TestSyncRewritesDrift(t *testing.T) {
root := snippetTree(t, map[string]string{"docs/setup/start.md": doc})
path := filepath.Join(root, "docs/setup/start.md")
problems, err := Check(Options{Root: root})
problems, err := Check(Options{Root: root, Commands: fixtureCommands})
if err != nil {
t.Fatal(err)
}
@@ -336,7 +339,7 @@ func TestSyncRewritesDrift(t *testing.T) {
t.Fatalf("problems = %q, want [%q]", got, want)
}
out := filepath.Join(t.TempDir(), "site")
if _, problems, err := Build(Options{Root: root, Out: out}); err != nil || len(problems) != 1 {
if _, problems, err := Build(Options{Root: root, Commands: fixtureCommands, Out: out}); err != nil || len(problems) != 1 {
t.Fatalf("Build with drift: %v %v", err, problems)
}
if _, err := os.Stat(out); err == nil {
@@ -362,10 +365,10 @@ func TestSyncRewritesDrift(t *testing.T) {
if res, _, err := Sync(Options{Root: root}); err != nil || res != (SyncResult{}) {
t.Fatalf("second Sync = %+v, %v; want up to date", res, err)
}
if problems, err := Check(Options{Root: root}); err != nil || len(problems) > 0 {
if problems, err := Check(Options{Root: root, Commands: fixtureCommands}); err != nil || len(problems) > 0 {
t.Fatalf("Check after Sync: %v %q", err, problemLines(problems))
}
if _, problems, err := Build(Options{Root: root, Out: out}); err != nil || len(problems) > 0 {
if _, problems, err := Build(Options{Root: root, Commands: fixtureCommands, Out: out}); err != nil || len(problems) > 0 {
t.Fatalf("Build after Sync: %v %v", err, problems)
}
md, err := os.ReadFile(filepath.Join(out, "setup/start.md"))