Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,19 @@ jobs:
run: go test ./...

- name: The stated conformance totals match the gate
run: go run ./tools/conformancefigure -root content/en
run: go run ./tools/conformancefigure --root content/en

# The examples carry their answers in `# =>` comments; this runs them.
# rbgo is built from the ruby repository's main so they are judged against
# the interpreter these pages describe. An interpreter that cannot be
# produced exits 2 -- a REFUSAL, not a pass.
- name: The Ruby examples print what the pages say they print
run: |
go run ./tools/docexample -selftest
# GOBIN names the destination: `go install` otherwise writes to
# $(go env GOPATH)/bin, and an install that lands elsewhere looks
# exactly like one that worked.
GOBIN=/tmp go install github.com/go-embedded-ruby/ruby/cmd/rbgo@main
go run ./tools/docexample -rbgo /tmp/rbgo -root content/en
go run ./tools/docexample --rbgo /tmp/rbgo --root content/en

# docexample judges the examples that claim an answer -- 11 of them. The
# scientific-stack page also PRINTS around 150 method names in prose, and
Expand Down
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ after publication.
```bash
GOBIN=/tmp go install github.com/go-embedded-ruby/ruby/cmd/rbgo@main

go run ./tools/conformancefigure -root content/en
go run ./tools/docexample -rbgo /tmp/rbgo -root content/en
go run ./tools/numericsurface --rbgo /tmp/rbgo --root content/en
go run ./tools/upstreamlinks --root content/en
go run ./tools/conformancefigure --root content/en
go run ./tools/docexample --rbgo /tmp/rbgo --root content/en
go run ./tools/numericsurface --rbgo /tmp/rbgo --root content/en
go run ./tools/upstreamlinks --root content/en
```

The authority column is the point. None of these holds the right answer itself:
Expand All @@ -46,12 +46,10 @@ library that gets swapped out **disappears** from it, which is what made all
three stale attributions of 2026-10-20 detectable without anybody writing the
correct one down.

⚠ `numericsurface` and `upstreamlinks` use **cobra**, so their flags are POSIX
`--long`; the two older tools still use the standard `flag` package and take
`-root`. Converting them is
[#33](https://github.com/go-embedded-ruby/docs/issues/33) — a breaking change,
because `pflag` reads `-root` as a cluster of short flags rather than as
`--root`.
⚠ **All four use [cobra](https://github.com/spf13/cobra) since `v0.4.0`, so
every flag is POSIX `--long`.** `-root` is no longer accepted by any of them:
`pflag` reads it as the cluster `-r -o -o -t`. A caller pinned to `v0.3.0` or
earlier keeps working until it bumps, and must change the spelling when it does.

The section reader and the module table they share live in
[`internal/surface`](internal/surface), so the two guards cannot end up
Expand Down
125 changes: 94 additions & 31 deletions tools/conformancefigure/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ package main

import (
"bufio"
"flag"
"errors"
"fmt"
"io"
"io/fs"
Expand All @@ -34,6 +34,8 @@ import (
"strconv"
"strings"
"time"

"github.com/spf13/cobra"
)

// baselineURL is the gate's own file in the ruby repository. The figure is read
Expand Down Expand Up @@ -139,38 +141,80 @@ func allMatches(line string) []string {
return out
}

func main() {
var (
// content/en since the move to Hugo (#28); it was docs/ under MkDocs,
// and a default naming a directory that no longer exists meant every
// hand-run of this tool refused with "lstat docs: no such file".
root = flag.String("root", "content/en", "the directory of Markdown to check")
baseline = flag.String("baseline", "", "read the authoritative BASELINE from this path instead of fetching it")
timeout = flag.Duration("timeout", 30*time.Second, "how long to wait for the fetch")
)
flag.Parse()
// exitError carries the status the command means, so the documented 0/1/2 are
// not flattened into cobra's single "an error happened" exit of 1.
type exitError struct {
code int
msg string
}

// The control first: if the vocabulary is wrong, every verdict after it is
// worthless, and a wrong vocabulary that matches nothing is indistinguishable
// from a site with no stale figures.
func (e *exitError) Error() string { return e.msg }

// checkVocabulary runs the guard's own control.
//
// ⛔ It runs on EVERY invocation, not only under `go test`. This tool is used
// from another repository as `go run github.com/go-embedded-ruby/docs/tools/
// conformancefigure@vX`, and that consumer never runs this module's tests — so
// a control that lived only in a _test.go would silently stop protecting the
// landing page, which is the site whose HTML phrasing the vocabulary was
// extended for. A wrong vocabulary that matches nothing is indistinguishable
// from a site with no stale figures.
func checkVocabulary() error {
for _, c := range vocabularyCases {
n, got := match(c.line)
if got != c.want {
fmt.Fprintf(os.Stderr, "conformancefigure: the vocabulary itself is wrong: %q matched=%v, want %v\n", c.line, got, c.want)
os.Exit(2)
return &exitError{2, fmt.Sprintf("the vocabulary itself is wrong: %q matched=%v, want %v", c.line, got, c.want)}
}
if c.want && n != c.n {
fmt.Fprintf(os.Stderr, "conformancefigure: the vocabulary read %q as %q, want %q\n", c.line, n, c.n)
os.Exit(2)
return &exitError{2, fmt.Sprintf("the vocabulary read %q as %q, want %q", c.line, n, c.n)}
}
}
return nil
}

want, err := authoritative(*baseline, *timeout)
// RootCmd is the whole command, built so a test can drive it with SetArgs,
// SetOut and SetErr instead of a subprocess.
func RootCmd() *cobra.Command {
var (
root string
baseline string
timeout time.Duration
)
cmd := &cobra.Command{
Use: "conformancefigure",
Short: "Check that every stated ruby/spec total agrees with the gate's BASELINE",
Long: "conformancefigure reads the present-tense claims this site makes about\n" +
"rbgo's ruby/spec total and compares each with the sum of the gate's own\n" +
"per-file baseline. A dated sweep going stale is not an error and is\n" +
"exempt by its date.",
SilenceUsage: true,
SilenceErrors: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return check(cmd.OutOrStdout(), cmd.ErrOrStderr(), root, baseline, timeout)
},
}
// content/en since the move to Hugo (#28); it was docs/ under MkDocs, and a
// default naming a directory that no longer exists meant every hand-run of
// this tool refused with "lstat docs: no such file".
cmd.Flags().StringVar(&root, "root", "content/en", "the directory of Markdown and HTML to check")
cmd.Flags().StringVar(&baseline, "baseline", "", "read the authoritative BASELINE from this path instead of fetching it")
cmd.Flags().DurationVar(&timeout, "timeout", 30*time.Second, "how long to wait for the fetch")
return cmd
}

func check(stdout, stderr io.Writer, root, baseline string, timeout time.Duration) error {
// The control first: if the vocabulary is wrong, every verdict after it is
// worthless.
if err := checkVocabulary(); err != nil {
return err
}

want, err := authoritative(baseline, timeout)
if err != nil {
// A refusal, not a pass: an unreadable baseline finds no disagreement
// either, and that silence reads exactly like agreement.
fmt.Fprintf(os.Stderr, "conformancefigure: cannot read the gate's total: %v\n", err)
os.Exit(2)
return &exitError{2, fmt.Sprintf("cannot read the gate's total: %v", err)}
}

type finding struct {
Expand All @@ -181,7 +225,7 @@ func main() {
var wrong []finding
checked := 0

err = filepath.WalkDir(*root, func(path string, d fs.DirEntry, err error) error {
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
Expand Down Expand Up @@ -219,25 +263,44 @@ func main() {
return sc.Err()
})
if err != nil {
fmt.Fprintf(os.Stderr, "conformancefigure: walking %s: %v\n", *root, err)
os.Exit(2)
return &exitError{2, fmt.Sprintf("walking %s: %v", root, err)}
}

// "N of M, never N alone": a walk that matched nothing reports no
// disagreement, which is the same output as a correct site.
fmt.Printf("%d current claim(s) checked against the gate's %d\n", checked, want)
fmt.Fprintf(stdout, "%d current claim(s) checked against the gate's %d\n", checked, want)
if checked == 0 {
fmt.Fprintf(os.Stderr, "conformancefigure: found no statement of the total in %s; "+
"either the vocabulary no longer matches how the docs phrase it, or the figure was dropped\n", *root)
os.Exit(1)
return &exitError{1, fmt.Sprintf("found no statement of the total in %s; "+
"either the vocabulary no longer matches how the docs phrase it, or the figure was dropped", root)}
}
for _, w := range wrong {
fmt.Fprintf(os.Stderr, "%s: claims %d, the gate records %d\n %s\n", w.where, w.got, want, w.text)
fmt.Fprintf(stderr, "%s: claims %d, the gate records %d\n %s\n", w.where, w.got, want, w.text)
}
if len(wrong) > 0 {
fmt.Fprintf(os.Stderr, "\n%d claim(s) disagree with %s\n", len(wrong), baselineURL)
os.Exit(1)
return &exitError{1, fmt.Sprintf("%d claim(s) disagree with %s", len(wrong), baselineURL)}
}
return nil
}

// osExit is a variable so a test can observe the status without ending the
// test binary.
var osExit = os.Exit

func main() {
err := RootCmd().Execute()
if err == nil {
return
}
var ee *exitError
if errors.As(err, &ee) {
fmt.Fprintf(os.Stderr, "conformancefigure: %s\n", ee.msg)
osExit(ee.code)
return
}
// A cobra error -- an unknown flag, an argument where none is taken. It is
// a refusal to run, not a verdict on the pages, so it exits 2.
fmt.Fprintf(os.Stderr, "conformancefigure: %v\n", err)
osExit(2)
}

// authoritative sums the gate's per-file baseline: one `OK<TAB>path<TAB>passing`
Expand Down
Loading
Loading