Documentation CI gates#

The docs build runs three independent quality gates on every PR. This page documents what each gate enforces and how to invoke it locally — useful both for contributors writing new docstrings and for debugging a CI failure.

Numpydoc validation#

The driver lives at tools/numpydoc_validate.py. It walks every public callable reachable from femorph_solver.__all__ and runs numpydoc.validate.validate() on each, then renders a markdown summary table.

Where the report appears. In the docs GitHub Actions job, the script’s output is appended to $GITHUB_STEP_SUMMARY so the violation table is visible on every PR run without having to dig into the build log.

What’s enforced. The enforced violation classes live in [tool.numpydoc_validation] in pyproject.toml. The set is the target — the bar Phase 1 closes the project to. Initial classes:

  • GL08 — docstring missing entirely.

  • SS01 / SS06 — missing summary or summary on more than one line.

  • PR01 / PR02 — parameters listed in the docstring do not match the signature.

  • RT01 — function returns a value but no Returns section.

EX01 (no Examples block) is intentionally omitted from the initial set — Phase 1 turns it on once every public method has been touched.

How the gate is enforced. The CI step currently runs the script with --warn-only so the table is visible but the build does not fail on baseline violations. Once #585 closes the surface to bucket-A documentation, the --warn-only flag is removed and the gate becomes hard.

Local invocation:

pre-commit run numpydoc-validate --hook-stage manual
# or directly
python tools/numpydoc_validate.py
python tools/numpydoc_validate.py --json /tmp/report.json
python tools/numpydoc_validate.py --all  # fail on every class

Doctest gate#

Every docstring Examples block on an allowlisted module is re-run by the test suite via tests.test_doctest_modules. A failing doctest fails CI — the docstring is the spec of what the API claims to do, and a broken example is an outdated spec.

Why an allowlist. Phase 0 cannot run --doctest-modules src/femorph_solver/ end-to-end because dozens of pre-existing >>> blocks are illustrative (use undefined names like model or tip_node) rather than runnable. Promoting a module to the allowlist is a single-line change in tests/test_doctest_modules.py that lands in the same PR as the docstring rewrite — see the Phase 1 children of #585.

Local invocation:

pytest tests/test_doctest_modules.py --no-cov
pytest --doctest-modules src/femorph_solver/<module>.py --no-cov  # ad-hoc on one file

Linkcheck (nightly)#

A separate docs-linkcheck workflow runs Sphinx’s linkcheck builder against every external URL referenced from the docs tree. It runs on a 04:00 UTC cron rather than per-PR, because broken external URLs are usually not introduced by the PR under review — they break later, when an upstream site renames or removes a page. On non-zero exit, ci-failure-tracker.yml files (or updates) a ci-failure-main-labeled issue.

Configuration. The exclusion list is in conf.py under linkcheck_ignore — typically used for the version-switcher JSON (which is only valid against the deployed multi-version build, not against arbitrary checkouts).

Local invocation:

make -C doc linkcheck

Sphinx strict mode (planned)#

SPHINXOPTS="-W --keep-going" is the eventual end-state, with conf.py nitpicky=True enforcing that every cross-reference resolves. Today the docs build emits ~700 unresolved cross-refs because the API reference page (api/index.rst) is hand-curated and does not autodoc every public symbol — so :class:`UnitSystem and friends raise ref.class warnings.

The fix is the autosummary refactor tracked under #587 (Phase 3). Strict mode lands together with that refactor; turning it on first would indiscriminately fail every doc PR for warnings unrelated to the PR’s scope.