4.5 The documentation and CLI gates¶
Several documents in this repository are generated and then committed: the
man page, USAGE.md, the rendered user guide, and the rendered developer guide
with its Graphviz figures. Committing generated output buys something real — a
fresh clone can read all of it with no toolchain installed — but it also
creates a silent lie waiting to happen, because nothing else in the build can
notice when a committed output stops matching the source it came from.
make docs-check is what closes that hole, and it is a prerequisite of both
make unit-test and make regression, so a stale document fails the test run
itself rather than waiting for CI or a reviewer to spot it.
The three parts¶
docs-check runs each part as its own target and aggregates the results, so
that one stale document never hides another and each one prints its own
remediation line:
docs-man-checkregeneratesdoc/man/jnext.1andUSAGE.mdfromdoc/man/jnext.1.mdwith pandoc, and diffs them. The fix ismake docs-man, then commit.docs-userguide-checkrebuilds the user guide fromsrc/doc/user-guidewithmkdocs build --strictand diffs the resulting tree againstdoc/user-guide. The fix ismake docs-userguide, then commit.docs-devguide-checkdoes the same forsrc/doc/developer-guideagainstdoc/developer-guide, and additionally re-renders everydiagrams/*.dotto SVG and diffs those too. Checking only the rendered site would let a hand-edited SVG through, because mkdocs copies it across unchanged. The fix ismake docs-devguide, then commit both.
On a machine that has no pandoc, mkdocs or graphviz the relevant part
skips; in CI the same part hard-fails, keyed on $CI, because a check
that skips silently reads as a pass.
The renderer fingerprint¶
pandoc, mkdocs-material and graphviz all emit byte-different output across their own versions for identical input. A byte-diff against outputs that were generated by a different toolchain therefore reports a version gap as staleness — which is exactly what broke CI once, when it ran a stock Ubuntu runner's pandoc 3.1.3 against outputs made with 3.7.0.2, with nothing in the failure message to hint that the version rather than an edit was the cause.
So each generator writes a fingerprint file next to its output, recording the
tool versions that produced it, and each check compares fingerprints before
anything else. When they do not match, the check skips the byte-diff and says
so, reporting that the guide builds cleanly but that staleness was not
compared, rather than raising a failure that is not real. CI sidesteps the whole
class by running in a Fedora container that matches the maintainer's own
distribution, instead of pinning tool versions one at a time.
SOURCE_DATE_EPOCH is pinned for a related reason: mkdocs stamps
sitemap.xml.gz with the build date, so an unchanged guide rendered tomorrow
would otherwise differ from the same guide rendered today.
What docs-check does NOT prove¶
It proves that the generated outputs match jnext.1.md. It proves nothing
about whether jnext.1.md describes the CLI that src/main.cpp actually
parses.
That seam was held together by developer diligence alone, and it failed twice with every gate green. The first time, five flags were entirely undocumented. The second time, a released man page confidently described a wrong scale range, two GUI menus that do not exist, and a status-bar indicator that does not exist. Both were found by reading the running product while writing the user guide, and not by any check.
make cli-check¶
The fix was to stop treating the flag set as control flow.
src/core/cli_options.h now holds it as a data table — one row per accepted
spelling, carrying its arity, a documentation class and its OptId — and
main.cpp dispatches from that table, which means a flag that is not in the
table cannot be parsed at all. cli_options_test then diffs the table against
the OPTIONS section of doc/man/jnext.1.md in both directions:
CLI-DOC-01catches implemented but undocumented: every documented-class spelling must have a man page entry.CLI-DOC-02catches documented but unimplemented: every man page OPTIONS entry must be a spelling the table accepts.CLI-DOC-03catches an arity disagreement: the value arguments the parser consumes must equal the metavars the man page shows.CLI-DOC-00guards the scrape itself, so that a broken parse of the man page cannot makeCLI-DOC-02vacuously true.CLI-SRC-01fails ifmain.cpp's parse loop reintroduces a hand-rolledarg == "--flag"comparison. That is the pattern that made the flag set unenumerable in the first place, and it would re-open the gap invisibly, because such a flag appears neither in the table nor in this check.CLI-BIN-01runs the real binary end to end, proving that the table drives the actual parser rather than only this test.
The suite runs both as make cli-check, a prerequisite of make regression,
and as a declared row of test/unit-tests.conf. It hangs off unit-test-build
rather than off docs-check, because the table is C++ and docs-check
deliberately needs no compiler.
Declaring a deliberate exception¶
Exceptions are declared in the table, never as a grep exclusion inside the
checker. Doc::UndocumentedAlias marks a flag that is intentionally absent
from the man page, with the reason written in a comment on its row; today the
only one is --sd-card, a back-compatibility alias for --sdcard that should
not be advertised. CLI-DOC-05 then asserts that such a flag really is absent,
because a deliberate exception that quietly became documented is drift too.
Doc::ShortAlias covers short forms that are documented inline on their long
form's entry.
Adding a flag is therefore four edits: an OptId, a table row, a case in
main.cpp — which the compiler enforces through -Wswitch — and an entry in
doc/man/jnext.1.md. Skip the last one and cli-check fails.
The residual gap, stated plainly¶
cli-check covers the flag set, not the prose. The wrong scale range
and the phantom status-bar indicator were both narrative text in the man page,
and nothing checks narrative text. The same is true of every page in this
guide: a stale paragraph here is the same class of defect as a stale man page,
with the difference that no gate can detect it. Keep reading the running
product.