diff --git a/README.md b/README.md index 8c421c8..9ceb4c2 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,16 @@ go run ./tools/numericsurface --rbgo /tmp/rbgo --root content/en go run ./tools/upstreamlinks --root content/en ``` +A fifth directory, `tools/wired/`, is not a tool but the guard over the four +above: its test fails when a tool under `tools/` is invoked by no workflow. +It exists because the MkDocs-to-Hugo move ([#28](https://github.com/go-embedded-ruby/docs/pull/28)) +rewrote the workflows and dropped two of these guards while leaving them in the +tree — **a guard nothing calls is indistinguishable from a guard that passes, +since both are silent**, and the pages could have said anything for two +releases. It looks for the path `tools/`, not the bare name: `docs.yml` +mentions `docexample` in a comment as well as in its step, so against the bare +name, deleting that step would have left the tool reported as wired. + The authority column is the point. None of these holds the right answer itself: a table of right answers kept in **this** repository would repeat the page's mistake and agree with it. `upstreamlinks` asks `go.mod` precisely because a diff --git a/tools/wired/wired.go b/tools/wired/wired.go index b2738b1..4b4edb3 100644 --- a/tools/wired/wired.go +++ b/tools/wired/wired.go @@ -86,11 +86,34 @@ func WorkflowText(dir string) (string, error) { return b.String(), nil } -// Unwired returns the commands no workflow mentions. +// Unwired returns the commands no workflow invokes. +// +// ⛔ It looks for `tools/`, the PATH, not for the bare name. +// +// Measured 2026-10-31: `.github/workflows/docs.yml` names `docexample` twice — +// once on the `run:` line that invokes it, and once in a comment on line 108 +// explaining what it judges. Against the bare name, deleting the invocation and +// leaving the comment would have kept that tool reported as wired. The one +// defect this package exists to catch would have been invisible for the one +// tool whose step carries prose about it. +// +// This stays deliberately crude — it still does not check for a well-formed +// step, because the failure it guards is not subtle (a tool nobody calls) and a +// crude test that would have failed #28 beats a precise one nobody wrote. But +// "appears in a sentence" and "appears in a command" are not the same claim, and +// `tools/` is what every spelling of the invocation has in common: +// +// go run ./tools/upstreamlinks --root content/en +// go run github.com/go-embedded-ruby/docs/tools/conformancefigure@v0.6.0 +// +// while a prose comment does not. A workflow that invoked a tool some other way +// — through a variable, a matrix — would now read as unwired, which fails loudly +// rather than quietly, and that is the right direction for this guard to be +// wrong in. func Unwired(commands []string, workflows string) []string { var out []string for _, c := range commands { - if !strings.Contains(workflows, c) { + if !strings.Contains(workflows, "tools/"+c) { out = append(out, c) } } diff --git a/tools/wired/wired_test.go b/tools/wired/wired_test.go index 9688343..2e73848 100644 --- a/tools/wired/wired_test.go +++ b/tools/wired/wired_test.go @@ -38,14 +38,15 @@ func TestEveryToolIsWiredIntoAWorkflow(t *testing.T) { bad := wired.Unwired(cmds, wf) if len(bad) > 0 { - t.Errorf("no workflow mentions %v.\n"+ + t.Errorf("no workflow invokes %v (looked for `tools/`, not the bare name:\n"+ + "a tool named in a comment is documented, not called).\n"+ "A tool nothing calls is indistinguishable from a tool that passes: both are "+ "silent. Either wire it into .github/workflows or delete it; leaving it is the "+ "state that let the MkDocs-to-Hugo move (#28) drop both documentation guards "+ "without a red build.", bad) return } - t.Logf("%d tool(s) under tools/, all named by a workflow: %v", len(cmds), cmds) + t.Logf("%d tool(s) under tools/, all invoked by a workflow: %v", len(cmds), cmds) } // TestUnwiredNamesOnlyWhatIsMissing checks the rule itself against literals, so @@ -64,6 +65,43 @@ func TestUnwiredNamesOnlyWhatIsMissing(t *testing.T) { } } +// ⛔ A NAME IN A SENTENCE IS NOT AN INVOCATION, and this repository already +// contains the sentence: docs.yml:108 reads +// +// # docexample judges the examples that claim an answer -- 11 of them. The +// +// so against the bare name, deleting the `go run ./tools/docexample` step and +// leaving that comment kept the tool reported as wired — the exact blindness +// this package exists to remove, for the one tool whose step is documented. +// +// The two halves are asserted together: the comment alone must NOT count, and +// the invocation must still count, so a rule tightened into matching nothing +// cannot pass this. +func TestAMentionInProseIsNotAnInvocation(t *testing.T) { + const comment = " # docexample judges the examples that claim an answer -- 11 of them. The\n" + if got := wired.Unwired([]string{"docexample"}, comment); len(got) != 1 { + t.Errorf("a tool named only in a comment was reported as wired (Unwired = %v)", got) + } + + // The real spellings of an invocation, each of which must count. + for _, yaml := range []string{ + " run: go run ./tools/docexample --rbgo /tmp/rbgo --root content/en\n", + " run: go run github.com/go-embedded-ruby/docs/tools/docexample@v0.6.0\n", + " run: cd tools/docexample && go run .\n", + } { + if got := wired.Unwired([]string{"docexample"}, yaml); len(got) != 0 { + t.Errorf("an invoked tool was reported as unwired (%v) for:\n%s", got, yaml) + } + } + + // And the comment beside a real invocation is the repository's actual state: + // wired because of the step, not because of the prose. + both := " run: go run ./tools/docexample --root content/en\n" + comment + if got := wired.Unwired([]string{"docexample"}, both); len(got) != 0 { + t.Errorf("Unwired = %v, want the step to count", got) + } +} + // repoRoot walks up to the directory holding go.mod, so the test runs from // wherever `go test ./...` places it. func repoRoot(t *testing.T) string {