Enforce single top-level markdown heading per docs notebook#866
Open
drbenvincent wants to merge 3 commits into
Open
Enforce single top-level markdown heading per docs notebook#866drbenvincent wants to merge 3 commits into
drbenvincent wants to merge 3 commits into
Conversation
Closes #863. Sphinx promotes every top-level (#) markdown heading inside a notebook into its own toctree entry, so a notebook with N H1s appears N times on the rendered notebooks index page. The convention has always been exactly one H1 per docs notebook, but nothing enforced it. Extends the existing validate-notebooks pre-commit hook (scripts/validate_notebooks.py) with a single-H1 check scoped to notebooks under docs/source/notebooks/: - _count_h1_headings walks markdown cells only and tracks fenced code blocks (``` and ~~~) so Python comments inside fenced ```python``` and inside code cells cannot spuriously trigger the rule (covers the existing its_lift_test.ipynb pattern). - _is_docs_notebook scopes the check to docs/source/notebooks/, leaving scratch/dev notebooks elsewhere alone. - _format_h1_violation produces an actionable error naming each offending cell index and heading text. Tests cover the valid case, zero/two/three H1 violations, code-cell comments, fenced ``` and ~~~ blocks in markdown cells, the out-of-scope exemption, and a regression test that runs the validator across every shipped docs notebook. Made-with: Cursor
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #866 +/- ##
=======================================
Coverage 94.60% 94.61%
=======================================
Files 80 80
Lines 12764 12821 +57
Branches 770 770
=======================================
+ Hits 12076 12131 +55
- Misses 485 486 +1
- Partials 203 204 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
The two ``pytest.skip(...)`` guards in ``test_all_docs_notebooks_pass_h1_check`` only fire when ``docs/source/notebooks`` is missing or empty. Under CI both conditions are always false, so codecov flagged them as uncovered lines / partial branches and patch coverage dropped below the 94.6% target. These are intentional defensive guards for partial checkouts, not behaviour worth testing — exclude them via ``# pragma: no cover`` so patch coverage reflects real testable code. Made-with: Cursor
drbenvincent
added a commit
that referenced
this pull request
Apr 24, 2026
The validate-notebooks pre-commit hook (#866) deterministically enforces exactly one top-level H1 per docs notebook, so the pr-review skill no longer needs to flag it. Remove the dedicated section from docs-patterns.md and trim the now-stale examples from SKILL.md and the docs-patterns intro. Keep the how-to-extend.md bullet as the worked example of the "formalise into a hook → prune from the skill" maintenance rule. Made-with: Cursor
Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Enforce single top-level markdown heading per docs notebook
Fixes #863
Summary
Sphinx promotes every top-level (
#) markdown heading inside a notebookinto its own entry under the
toctreeindocs/source/notebooks/index.md. A notebook with N top-level headingstherefore appears N times on the rendered notebooks index page,
polluting the navigation. The convention has historically been "exactly
one
#heading per docs notebook", but nothing enforced it.This PR extends the existing
validate-notebookspre-commit hook(
scripts/validate_notebooks.py) with a single-H1 check scoped tonotebooks under
docs/source/notebooks/.Changes
scripts/validate_notebooks.py_count_h1_headings(notebook)helper that walks markdowncells only, tracks fenced code blocks (
```and~~~), andcollects top-level
#headings. Code cells and#-prefixedPython comments inside fenced blocks are skipped so they cannot
spuriously trigger the rule (this matters for e.g.
its_lift_test.ipynb, which today has apythonfenced blockinside a markdown cell containing
# Calculate ...comments)._is_docs_notebook(path)so the rule only applies to notebooksunder
docs/source/notebooks/. Scratch / dev notebooks elsewherein the repo are left alone, and the hook entry stays generic.
_format_h1_violation(...)to emit a clear, actionable errorthat names each offending cell index and heading text.
validate_notebook(...)after the existingnbformat schema validation.
causalpy/tests/test_notebook_validation.pythree-across-cells H1 cases — each asserts the failure message and
H1 count.
#-prefixed comments inside code cells do not count asH1s.
```fenced blocks within markdown cellsdo not count as H1s (replicates the
its_lift_test.ipynbpattern).~~~fenced blocks behave the same.docs/source/notebooks/.in
docs/source/notebooks/and assert it passes.Audit
Scanned the 31
.ipynbfiles currently underdocs/source/notebooks/on
main— 0 violations. The known offender(
its_place_in_time_analysis.ipynb, 5 H1s) lives on PR #826 and is notyet in
main; that PR is responsible for fixing its own notebook.Enabling the check now is therefore safe and prevents future
regressions.
Testing
prek run --all-files— all hooks pass, including the now-stricterValidate notebook schemahook running over every notebook in therepo.
python -m pytest causalpy/tests/test_notebook_validation.py -v—all 11 tests pass.
Manual smoke test: ran the validator against a synthetic
docs/source/notebooks/bad.ipynbwith two#headings; the scriptexits 1 and prints:
Checklist
scripts/validate_notebooks.pyenforces exactly one top-levelmarkdown heading per docs notebook.
```and~~~fenced blocks within markdowncells are skipped.
block edge case).
prek run --all-filespasses.depth) and broader markdown linting — left for a follow-up.