JNEXT
Skip to content

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-check regenerates doc/man/jnext.1 and USAGE.md from doc/man/jnext.1.md with pandoc, and diffs them. The fix is make docs-man, then commit.
  • docs-userguide-check rebuilds the user guide from src/doc/user-guide with mkdocs build --strict and diffs the resulting tree against doc/user-guide. The fix is make docs-userguide, then commit.
  • docs-devguide-check does the same for src/doc/developer-guide against doc/developer-guide, and additionally re-renders every diagrams/*.dot to 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 is make 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-01 catches implemented but undocumented: every documented-class spelling must have a man page entry.
  • CLI-DOC-02 catches documented but unimplemented: every man page OPTIONS entry must be a spelling the table accepts.
  • CLI-DOC-03 catches an arity disagreement: the value arguments the parser consumes must equal the metavars the man page shows.
  • CLI-DOC-00 guards the scrape itself, so that a broken parse of the man page cannot make CLI-DOC-02 vacuously true.
  • CLI-SRC-01 fails if main.cpp's parse loop reintroduces a hand-rolled arg == "--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-01 runs 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.