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 :func:`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 :mod:`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/.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.