Documentation Style ==================== .. readingtime:: .. _title-style: Title Style ----------- Hand-authored page and section titles use Chicago Manual of Style headline style with one deliberate deviation: capitalize the first and last words, all major words, and prepositions of four letters or more; lowercase articles, coordinating conjunctions, the infinitive "to", and prepositions of three letters or fewer. The deviation is the length threshold. CMOS lowercases a preposition however long it is, and ``tephpy`` capitalizes the longer ones — :ref:`howto-read-a-sounding`'s "Read a Sounding From an Archive", :ref:`howto-units`'s "Work With Units", :ref:`howto-logo`'s "On the Plot or Around It" — while keeping the short ones down, as in :ref:`howto-framing`'s "Fix the View by Ranges". The rule is written this way because it is what the corpus already did. Every title carrying a medial preposition of four letters or more capitalized it bar one, and every title carrying a shorter one lowercased it bar an adverbial particle that is not a preposition at all (:issue:`217`). Nothing had ever enforced the stricter reading, so the rule is matched to the pages rather than imposed on them. A preposition doing another job keeps that job's case. In :ref:`howto-units`'s "Quantities Go In Directly", ``In`` is an adverbial particle of "go in" rather than a preposition, and CMOS capitalizes it whatever its length. Preserve literal case for: code and API identifiers, filenames, config keys, CLI commands, and paths; project and library names in their own casing (matplotlib, numpy, pint, metpy, pixi, tephpy); and acronyms and scientific symbols (CAPE, CIN, LCL, WMO, SPEC 0). The rule does not apply to autoapi-generated API pages, numpydoc section headers, changelog entries, or anything that is a full sentence (captions, admonition text, docstring summaries), which use sentence case. Bibliography entries reproduce the source's published title. A title is a claim like any other, and :ref:`reviewing-claims` is the review that reads it as one. Glossary -------- The glossary is written for software engineers, not meteorologists. Each entry gives the concept in one plain sentence, then how it appears in ``tephpy`` (the data, its units, the API type that carries it), and links deeper physics to the Explanation quadrant. .. _glossary-rule: Cross-reference the *first* mention of a glossary term per page with ``:term:``, in narrative prose only — never in titles, code blocks, API signatures, or admonition labels. Within a definition, link related terms but never the term itself. Keep one canonical spelling per concept. The documentation build is fail-on-warning, so a ``:term:`` whose entry does not exist yet breaks it: a pull request that reaches for a new term adds the entry in the same change. The first-mention half is checked by ``.github/scripts/check_glossary_links.py``, a pre-commit hook over the user quadrants. It reads the glossary into alias groups, so linking ``CAPE`` satisfies ``convective available potential energy`` too, and it reports the line rather than only the page. What it deliberately passes over is what this rule already excludes — titles, directive bodies and options, role targets, inline literals — and emphasis spans besides, because a page quoting another page's title in italics is citing it rather than using the concept. When a definition names a documented API, cross-reference it with the matching Sphinx domain role — ``:class:``, ``:func:``, ``:meth:``, ``:mod:``, or ``:obj:`` — so the reader can follow the link straight into the API documentation, rather than quoting the name as a plain double-backtick literal. Keep the accessor idiom the entry reads in as the link's display text, so ``calc.parcel_path`` and ``ax.shade_cape`` stay legible: .. code-block:: rst avoid: ``calc.parcel_path(...)`` computes a parcel's ascent prefer: :func:`calc.parcel_path(...) ` computes a parcel's ascent Third-party objects (matplotlib, metpy, numpy, pandas, pint, xarray) resolve the same way through intersphinx — see :ref:`cross-references` below. Reserve plain double-backtick literals for names with no documentation target: private members, dataclass fields already reachable through their linked owner, external tools without an intersphinx inventory (pixi), option strings, and keyword arguments. This mirrors the changelog fragment convention documented in :doc:`changelog`. .. _cross-references: Cross-References ---------------- Third-party APIs resolve through intersphinx. Every third-party package the documentation names — matplotlib, metpy, numpy, pandas, pint, xarray — has its inventory registered in ``intersphinx_mapping`` in ``docs/src/conf.py``, so a domain role such as :func:`metpy.calc.moist_lapse` or :class:`pint.Quantity` links straight into that project's documentation. When you first cite an API from a package that is not yet mapped, add its ``objects.inv`` location there in the same change, so the reference resolves rather than rendering as plain text. Parameter and return *types* are cross-referenced automatically. With ``numpydoc_xref_param_type`` enabled, numpydoc turns each type in a ``Parameters``/``Returns`` block into a link: fully-qualified names (``pint.Quantity``, ``numpy.ndarray``) resolve through intersphinx, tephpy's own short type names (``Sounding``, ``Profile``, the ``Tephpy*Error`` exceptions) are mapped to their targets in ``numpydoc_xref_aliases``, and descriptive connective words (``optional``, ``of``) are listed in ``numpydoc_xref_ignore``. Write the type as its plain qualified name — ``pint.Quantity``, not a hand-written role — and let the configuration link it. ``nitpicky`` is enabled, so an unresolved cross-reference is a warning and — via the docs Makefile's ``--fail-on-warning`` — fails the build. A reference that does not resolve is therefore caught automatically; a clean build is proof the links land. The only sanctioned exceptions live in ``nitpick_ignore`` in ``conf.py``: annotation types autoapi emits as ``py:class`` xrefs while numpy publishes them as ``py:data``/``py:attribute`` (``numpy.typing.ArrayLike``, ``numpy.typing.NDArray``, ``numpy.float64``), the ``Ellipsis`` in variadic tuples, and parameter defaults from the private ``_constants`` module. Do not extend that list to silence a reference you can instead make resolve — add a ``numpydoc_xref_aliases`` entry or write the full dotted name. .. _specification-citations: Specification Citations ----------------------- Cite a design specification as plain text — ``spec §3.2``, ``logo spec §1``, ``docs spec §3.6`` — and never as a hand-written role. The build turns each one into a link to the section it names, so writing the role yourself is not an improvement but a hazard: a role carries a second string that can disagree with its display text, and .. code-block:: rst :ref:`spec §3.2 ` has the right text against the wrong document while resolving perfectly cleanly, so neither the citation checker nor a nitpicky build has anything to object to. Writing the citation once means the text and the target cannot disagree. The prefix names the document and is load-bearing. A bare ``§N`` means *this* document's §N, which makes it safe inside a specification and an error anywhere else — a docstring owns no sections. Where several sections are cited together, the prefix carries across the run, so ``spec §3.3, §10`` and ``spec §3.1/§10`` each name two sections of the parent specification. The run continues across a comma or a solidus, and across nothing else: writing ``and`` in place of either separator ends it, leaving the second citation bare. A bare ``§N`` opening the next sentence falls back to the containing document rather than inheriting, for the same reason. A citation must also sit whole on one line, and so must a compound run — one wrapping after its comma or solidus strands the continuation, which falls back to the containing document instead of inheriting the prefix it was written under. Only horizontal whitespace joins a prefix to its section number, and the same holds of the gap after a separator, so a prefix stranded at the end of a line with its number wrapped onto the next is no longer part of the citation: what remains is a bare ``§N``, rejected outside a specification and read as a local reference inside one — either way, not the citation that was written. The rule is what keeps the displayed text and the link target from disagreeing, because the hook reads one line at a time while the build reads a whole paragraph, and a citation able to span the wrap is one they can read differently. Cite a section in body prose. Four other places will not carry a citation, and each fails the documentation build rather than rendering wrongly. A page title is linked like any other heading, but Sphinx copies the title into ```` with the markup stripped, and the theme repeats it in the breadcrumb without the anchor, so the citation reaches the reader as plain text in the browser tab and above the page. A toctree ``:caption:`` is a directive option rather than text the build can rewrite, and renders twice — once where the toctree sits and once in the sidebar. A ``.. raw:: html`` block and an API signature — a parameter's default value included — are left alone deliberately, because the build rewrites neither raw output nor code. Name the section in the surrounding prose instead. A section heading is worth avoiding for a second reason, and it is the one the build names. The theme rebuilds its "On this page" navigation out of the headings, keeping the text and dropping the anchor, and wraps the copy in the navigation's own link — so the citation *is* a link, to the section it sits in rather than to the section it names, and the check on the built HTML cannot tell one anchor from another. The build therefore warns about a citation inside a heading, naming the heading, and ``--fail-on-warning`` turns that into a failure. Writing the link yourself does not avoid it: the navigation strips an author's link the same way, so a heading citation is reported whether the build would link it or you already have. Cite the section in the prose below the heading instead. A heading is worth avoiding for a third reason, which fails differently again. A ``.. contents::`` directive links every heading it lists — in its own list and in the heading itself — and it does so after the citation has already become a link, so the page ends up with one anchor inside another. That is invalid HTML, which a browser restructures silently, and Sphinx reports nothing: only the check on the built HTML notices. Writing a citation inside a link yourself is the same collision from the other side, and in body prose it is *not* an error — the build leaves such a citation as plain text rather than nesting a link in a link, and your own link is the one the reader follows. In a heading it is reported, for the reason above. A pre-commit hook checks that every citation names an anchor that exists, and the documentation build checks that every rendered citation became a link. Both are specified in the published specifications design: docs spec §3.6 covers the hook, and docs spec §3.7 covers the build. .. _github-references: GitHub References ----------------- Refer to a tephpy issue or pull request with the matching extlink role, never as plain text and never by URL. Write ``:issue:`65``` and ``:pull:`73``` in reStructuredText and in docstrings; write ``{issue}`65``` and ``{pull}`73``` in the Markdown specifications. Each renders as a linked ``#65`` or ``#73``. This is the opposite instruction to the one above, and the same reasoning decides both. An extlink generates its caption from its value, so there is one string and the text cannot disagree with the target; a hand-written ``:ref:`` carries two. Writing the role is what keeps them together here, and what would pull them apart there. Keep the word that says which kind it is. ``PR :pull:`19``` renders ``PR #19``, and a reader who sees only ``#19`` cannot tell what the link opens, because the caption is the same for both roles. Two things follow. An issue in another project has no role — the two above are scoped to this repository — so write it as an ordinary link with its own URL. And a hexadecimal colour is not a reference: keep it in literal markup — ``#808080`` — or inside a string, which is where a colour belongs anyway. A pre-commit hook rejects both a bare ``#65`` and a hand-written ``https://github.com/bjlittle/tephpy/issues/65``; the documentation build rejects the second again through ``extlinks_detect_hardcoded_links``, naming the role to write instead. Neither can tell ``:issue:`` from ``:pull:`` — GitHub redirects between them, so the wrong one of the two still reaches the right page. The rule is specified in docs spec §3.8. .. _documentation-links: Documentation Links ------------------- A few tracked files link into the documentation by absolute URL, because they are outside the Sphinx project and have no role to write instead: ``README.md``, the repository's landing page, and a script that sends a contributor to the page explaining why it failed them. Such a link is invisible to everything that checks the rest — ``nitpicky`` sees only the references the build resolved. Write the URL as ``https://tephpy.readthedocs.io/en/latest/<page>.html``, optionally with a fragment. A per-pull-request preview host (``tephpy--<pr>.org.readthedocs.build``) is where a documentation change is verified rather than where a link belongs — Read the Docs deletes the preview when the pull request closes — and ``latest`` is the only version published, so ``en/stable`` and a path that drops the version alike resolve nowhere. A URL whose path never reaches ``.html`` names no page the gate can look up, so it is passed over rather than judged — as the Read the Docs badge at the top of the README is, pointing at the base with a query string and no path. In ``README.md``, write the link as a Markdown reference — ``[CAPE][cape]`` in the prose, with the target defined in the block at the foot of the file — so the prose stays readable and each URL is stated once: .. code-block:: markdown [cape]: https://tephpy.readthedocs.io/en/latest/reference/glossary.html#term-CAPE Link the *first* mention of a glossary term in the README and no more, as on a documentation page. Take the fragment from the built page rather than deriving it: a glossary anchor is ``term-`` followed by the term with its case preserved and each run of non-alphanumeric characters collapsed to a single hyphen, so ``CAPE`` gives ``term-CAPE`` and ``Normand's point`` gives ``term-Normand-s-point``. Label the reference in lower case — Markdown labels are case-insensitive, and a lowercase label is hard to mistake for the fragment, which is not. The documentation build checks these links. ``check_documentation_links.py`` reads each URL out of every file named in its ``SOURCES`` constant and looks it up in the HTML just built, failing when a URL naming a page is written some other way, when the page is absent, or when the fragment names no ``id``. Renaming a glossary term or moving a page therefore fails the build, rather than leaving a link pointing into a 404 that nobody notices. A new file that writes such a URL is checked only once it is added to ``SOURCES``; the gate reads that list and not the repository, so that a URL quoted in a test fixture or frozen into an implementation plan is left alone. A file that stops carrying a documentation link fails the gate rather than dropping out of it in silence, so removing the last link means removing the entry too — and emptying ``SOURCES`` entirely fails the same way, rather than passing on a search of nothing. Code Examples ------------- Every python block in the how-to, tutorial and explanation quadrants is executed by ``tests/test_docs_snippets.py``, as one script per page and in document order, ending with a draw of every figure the page leaves open. Four rules follow from that, and the gate itself is specified in docs spec §3.9. A page is a session, not a catalogue. A later block may rely on a name an earlier one bound — ``add_logo()`` with no argument brands the figure the block above it created — so the blocks of a page cannot be reordered freely, and a block that would not run after the ones above it is a page defect rather than a gate problem. There is no way to mark a block as not for execution. A block a reader is invited to copy and which cannot run is the defect; the answer is to fix the snippet, or to stop presenting it as one. A REPL transcript is code too — write it as a script in a ``python`` block rather than as ``pycon``, which the gate reports rather than skips. Snippets carry no linter directives. ``# noqa`` and ``# type: ignore`` suppress nothing in a ``.rst`` file, and they ask a reader pasting the line to satisfy tooling they are not running. Where an import looks unused, say why it is there instead — ``import tephpy # registers the "tephigram" projection``. Where a snippet's surrounding prose makes a behavioural promise, a test pins the promise. Execution and truth fail independently: :pull:`113` fixed a passage whose snippet ran perfectly and whose prose was wrong, and the gate would have passed it. Name the test in the pull request that adds the prose, so the connection is on the record. Where no test can pin the promise — a claim about an external tool, or one that covers a set rather than a case — :ref:`reviewing-claims` carries the questions that stand in for one. Published Figures ----------------- A user page either publishes figures or it does not, and the two forms never mix. On a page that does, every python block is a ``.. plot::``, which renders the block and shows its source. Leaving one plain ``code-block:: python`` behind is the defect the rule exists to stop: that block runs in the snippet gate and not in the documentation build, so the build's namespace silently loses whatever it bound. The rules below are specified in plots spec §3.2 and asserted by ``tests/test_docs_snippets.py``; the images themselves are pinned against ``docs/baseline`` by ``.github/scripts/check_docs_figures.py`` (plots spec §3.5). One picture per point the prose makes, not one per block. A page is a session in which a later block supersedes an earlier one — three blocks of :ref:`howto-emphasis` call ``ax.isotherms(...)`` on the same axes — so a picture after every block would sometimes show a state the surrounding prose has stopped describing. A section making two distinct points publishes two: :ref:`howto-emphasis`'s "Configure It Once" section shows the context-manager idiom and then a diagram opting out of it, and :ref:`howto-logo`'s "On the Plot or Around It" section shows the axes-and-figure targeting and then the no-argument call that brands whichever figure is current. Each block carries its options by five rules: - The first block on the page carries ``:context: reset``. Without it the page opens with whatever the previously built page left behind, and build order is not a property any page controls. - Every later block carries ``:context:`` or ``:context: close-figs``. A block with no ``:context:`` at all runs in a fresh namespace, where the page's imports never happened; ``close-figs`` is what opens a section that starts its own figure. The two values do not combine — the directive takes exactly one of nothing, ``reset`` or ``close-figs``. - A block whose picture would add nothing, or should not be published, carries ``:nofigs:``. It still runs, so the session is unbroken and the snippet gate still covers it. That is why a plain ``code-block:: python`` is not the answer for such a block. - Every figure-producing block carries a ``:filename-prefix:``, unique across the documentation. Unnamed, the image takes a per-document counter, so inserting a section renumbers every image after it and every baseline with them. A name and a ``:nofigs:`` on the same block is a figure declared and never built, which the figure gate reports. Spell the option in lowercase, indent it with spaces, and keep the value to one run of letters, digits, dots and dashes: Sphinx accepts more than that, and the gate reports anything else as a near miss rather than reading it as a declaration. - No file-argument form. ``.. plot:: script.py`` renders the figure from a file, and the code a reader is invited to copy has to be on the page. Nothing a published block does may outlive it. Every block on every page executes in the Sphinx process, with ``sys.modules`` shared across the whole build, and ``:context: reset`` clears the namespace the blocks run in without touching module state. So demonstrate configuration with :meth:`tephpy.config.context` rather than by assigning to ``tephpy.config``: a bare assignment applies to every axes created afterwards, on that page and on every page built after it. A page whose subject *is* global, persistent configuration publishes no figures — :ref:`configure-from-a-file` is that page (plots spec §3.3). When a figure is meant to change, re-bless it in the same change that caused it: .. code-block:: console $ pixi run docs-figures Read the diff before committing. That command approves whatever was rendered, a regression as readily as a correction. It refuses outright, without touching a file, when the gate cannot read a declaration — a near miss as above, or a page the gate expects to publish that has stopped declaring anything. Fix the declaration and run it again. The baseline that declaration named is a live pin, and to a scan that cannot read the declaration it looks exactly like the orphan of a renamed section, which this command removes. Reading Time ------------ Every page a reader reads start to finish opens with a reading-time banner, placed after the title and before the first section: .. code:: rst .. readingtime:: In a specification, which is Markdown, it is the equivalent fence. The estimate is counted from the *parsed* page rather than from its source, so directive options, comment blocks and front matter are not counted as words and a body another directive generated is. The rate is 150 words per minute :cite:`brysbaert2019` :cite:`carver1982` — below the 175 wpm floor Brysbaert reports for ordinary non-fiction prose, because these pages alternate argument with code the reader parses line by line. Two per-page overrides exist, and both are deliberate marks in the source rather than silent adjustments. A literal duration is quoted instead of counted: .. code:: rst .. readingtime:: 45 and a rate replaces the default for that page alone: .. code:: rst .. readingtime:: 200wpm Anything else is a build error. There is no way to spell an argument the directive half-understands. A page carries exactly one banner. Two is a copy-paste, not a decision, and ``test_no_page_carries_more_than_one_reading_time`` fails the build over it. A page that is *navigated* rather than read carries no banner: the four Diátaxis landing pages, the developer and specification indexes, the site root, the glossary, the topic index, the four reference pages whose body a directive generates, and a reference page whose body includes another page that already carries the banner. These are named in ``EXEMPT`` in ``tests/test_docs_readingtime.py``, with the reason beside each, and ``test_every_page_a_reader_reads_carries_a_reading_time`` fails for any other page that omits one. Adding a page means adding the banner or adding the reason. Landing Pages ------------- A section landing page is navigation rather than prose, which is the rule above read forwards: it carries no reading-time banner because nobody reads it through. Six sections take one of two shapes, and both end in a hidden ``toctree``. **A table**, in :doc:`getting started <../start/index>`, the tutorials, how-to and explanation quadrants, and this developer guide. The page carries, in order, an introduction, one two-column ``list-table``, and the ``toctree``. The introduction says what the section is for, who it assumes the reader is, what it guarantees of every page in it, and where to go if this is the wrong section. It says nothing about an individual page. A paragraph that summarises the section page by page is a list that has to track a directory, and the how-to page's grew from six clauses to nine by hand before this rule existed. Each row is a ``:doc:`` link against one sentence. The sentence is editorial rather than descriptive: it is there so a reader can tell this page from its siblings, and it is deliberately not the page's opening line, which a hover already shows. Write ``:widths: auto`` and no header row, which is the shape the API reference's own summary tables already take. **A grid of cards**, in the reference quadrant, whose pages are looked up by name rather than chosen between. The page carries, in order, a one-sentence introduction, a ``.. grid:: 1 2 2 2`` of cards, the guidance paragraphs, and the ``toctree``. The API card comes first and spans the row with ``:columns: 12``. One column below 576px is not a matter of taste: two columns at that width split words mid-word, which narrative spec §3.9 records measuring. Each card takes its page's title — the API card excepted, because that page is titled by the package name — a light and a dark icon from ``_static/cards/reference/``, and one sentence that tells it from the card beside it. A card raises no hover tooltip, so its sentence may say what the page opens with where that is clearest. Draw an icon in the root page's vocabulary; its dark file differs from the light one only in the navy, ``#8FB8E8`` for ``#1B3A6B``, and the knock-out halo, ``#14181e`` for ``#FFFFFF``. The index and the toctree carry the same pages in the same order, and it is the order a reader needs rather than the alphabet. Hiding a toctree hides it from the page body only: the sidebar, the breadcrumb and the previous and next footer all read its order. ``tests/test_docs_landing_pages.py`` fails when the two disagree, in membership or in order; when an entry links outside its section; when the index omits a page the section holds, which an ``:orphan:`` page would otherwise do silently, since the build's own toctree check never sees one; and when the toctree is not hidden, which would publish the same list twice. It reads a row's first cell and a card's ``:link:``, and derives the API card's page from ``conf.py``, since that page exists only while a build runs. Glossary terms stay out of the cells and the cards. Both are directives, and :ref:`the first-mention rule <glossary-rule>` already passes over a directive's body, so a ``:term:`` in either neither satisfies that rule nor breaks it. Write first mentions in the prose, and let a cell or a card take the plain word. Topic Tags ---------- Every page of the tutorials, how-to and explanation quadrants declares two to four topic tags, on the **first line of the file**: .. code:: rst :tags: units, sounding Above the ``.. _label:`` target, with a blank line under it. The position is not a style choice: Sphinx lifts a field list into page metadata, where nothing renders it, only when it precedes every other piece of markup. Under the title it renders at the reader as a stray definition list instead (topics spec §3.2). The terms are the closed vocabulary of topics spec §3.3, shared with the gallery, each defined there by what it covers *and* what it excludes. Two to four, for the reason a gallery example takes two to four: one tag files a page under a single button, and a full house files it under every one. ``tests/test_docs_topics.py`` discovers the quadrants rather than reading a list, so a new page fails until it declares tags, and an unknown term fails until it is added to ``VOCABULARY`` and to that table together. A term earns a filter button on :ref:`topics` by appearing in two or more quadrants and selecting fewer than half the corpus (topics spec §3.4). Both thresholds are relative, so nobody edits a number as the documentation grows, and a term that earns no button still tags its pages and still drives the gallery's own filter. The reference quadrant and the developer section carry no tags. Reference pages are lookup surfaces reached by name, and the developer section waits on :issue:`66`. Gallery Examples ---------------- The gallery is scraped from ``src/tephpy/examples``, which ships in the wheel: every entry is a module a reader can download, and also one an installed tephpy can run with ``tephpy examples run <name>``. The rules below are specified in gallery spec §3.2, §3.3, §3.5, §3.6. Every one a test can read off a file — the registry, the ``main()`` shape and its guard, the figure size, the tag vocabulary and how many tags — is asserted by ``tests/examples/test_examples.py``. What belongs in the gallery at all, and that an example reaches no network and writes no file, are left to review. The gallery shows what the package draws. Everything else is a how-to. An example whose subject is not a picture — getting data in, configuring the package, installing it — belongs in the how-to quadrant, however much code it carries (gallery spec §5). An example that happens to load data is fine; the subject is what is tested, not the API surface touched. Every module is named ``plot_*.py``, and the prefix is load-bearing. sphinx-gallery's ``filename_pattern`` defaults to ``/plot``, and only a matching file is *executed*: a file outside the pattern is still rendered, silently, with no figure and no error. Every module defines ``main()``, which builds the figure and returns it, and closes with the guard that shows it: .. code-block:: python def main() -> Figure: ... return fig if __name__ == "__main__": main() plt.show() One construction then serves four consumers — sphinx-gallery, which executes the file as ``__main__``; ``tephpy examples run``; ``pytest-mpl``, which decorates a function returning a figure; and the reader running the downloaded script. Showing inside ``main`` would cost the third of those, and the pinned figure would then be a claim about the test rather than about what was published. An example takes any data it needs from :mod:`tephpy.samples`, reaches no network, and writes no file. The documentation build executes it, so a ``savefig`` call would leave an artefact in the generated tree on every build; the vector-output line appears in an example's prose instead, shown and not run. Add a new example to ``REGISTRY`` in ``src/tephpy/examples/__init__.py``, in the position it should occupy. Registry order is gallery order is ``examples run --all`` order, and the tests read it: an unregistered ``plot_*.py`` fails them rather than disappearing quietly. Pass ``figsize=(8.0, 4.0)`` at the example's own ``subplots`` or ``figure`` call — sphinx-gallery calls ``plt.rcdefaults()`` before every example, so a configured default is discarded before the first line runs. Tags come from a closed vocabulary of seventeen terms, shared with the site-wide topic index, two to four per example, declared in the flag sphinx-gallery reads: .. code-block:: python # sphinx_gallery_tags = ["analysis", "shading", "indices", "sounding"] Each term is defined by what it covers *and* what it excludes in topics spec §3.3, and that table is the authority when two people would tag a page differently — the gate cannot see a disagreement, because both spellings are legal. They render on the page and drive the index's filter buttons, so a ``barb`` beside a ``barbs`` splits the very index the feature exists to build. Widening the vocabulary means adding the term to ``VOCABULARY`` in ``docs/src/_ext/tephpy_topics_data.py`` *and* its definition to that table, together. Spell the flag exactly: sphinx-gallery parses ``sphinx_gallery_tag`` into a differently-keyed entry and discards it in silence, with no warning to fail the build on — which is why the test reads the flag out of the source text rather than asking the parser. Write the flag flush under the imports, with no blank line above it — the removal preserves the blank lines around what it strips, so a blank line there is one the reader is left looking at, three before ``def main`` where PEP 8 wrote two. ``tests/examples`` reproduces the removal and asserts the spacing it leaves, so any flag written the other way fails there. The flag itself does not reach the page. ``remove_config_comments`` strips ``# sphinx_gallery_*`` lines from the rendered code once sphinx-gallery has read them, so the tags still render and still drive the index filter, and the script the reader downloads still carries the flag. Nothing else is hidden — the pattern matches sphinx-gallery's own flags and no other comment, which is why this replaced ``sphinx_gallery_start_ignore``, the mechanism that would have hidden source along with it (gallery spec §3.6). A module's docstring is not a docstring — it is the page's title and opening prose — so the :ref:`glossary rule <glossary-rule>` applies to it in full: cross-reference the first mention of a term per example with ``:term:``, and seed the entry in the same pull request when the term is new. The ``# %%`` block comments and ``main()``'s own numpydoc are code and documentation of code, and take no ``:term:``. Attribute Documentation ----------------------- The API reference is generated by ``sphinx-autoapi``, which parses the source *statically* and therefore never reads comments. A Sphinx ``#:`` doc-comment — whether on the line above an assignment or trailing it inline — is silently dropped from the rendered page; only ``sphinx.ext.autodoc`` (which imports the module) honours ``#:``. Document a rendered attribute one of two ways instead: - Prefer the numpydoc ``Attributes`` section of the owning class's docstring. This is the established pattern for tephpy's public dataclasses (:class:`~tephpy.sounding.Sounding`, :class:`~tephpy.calc.Profile`, :class:`~tephpy.calc.SoundingIndices`) and keeps every field's description in one place alongside its type. - When an attribute must carry its documentation at the point of definition, use a PEP 224 *attribute docstring*: a triple-quoted string on the line *below* the assignment. autoapi renders it; a ``#:`` comment in the same spot renders nothing. Reserve ``#:`` comments for annotations on private members — the private ``_constants`` and ``_config`` modules, and ``_``-prefixed module constants — which autoapi excludes from the reference regardless of comment style. There the choice is purely stylistic, and ``#:`` reads naturally above a constant. API Version Stamps ------------------ Every published API object records the version it arrived in, as a numpydoc ``Notes`` section carrying the Sphinx ``versionadded`` directive. It is the docstring's **last** section, because numpydoc's section order places ``Notes`` after ``Raises``. Sphinx would render the directive anywhere in the docstring; the gate accepts it only inside ``Notes``, so the form stays one form: .. code-block:: rst Notes ----- .. versionadded:: 0.1.0 "Published" means what the API reference publishes: every module under ``src/tephpy`` with no underscore-prefixed path component except the gallery, and the objects those modules define. Attributes are outside it — a dataclass field is documented in its class's ``Attributes`` section, so it has nowhere of its own to carry a directive — and so is the gallery, whose module docstring is sphinx-gallery's rendered title block rather than a numpydoc docstring. Write ``versionadded``, not Sphinx 9's ``version-added``. The two are the same directive and render identically, but the documentation floor is ``sphinx>=8.0``, where the hyphenated spelling does not exist; numpydoc's ``GL10`` two-colon check does not fire for it either, because it is not in numpydoc's directive list. ``.github/scripts/check_api_docstrings.py`` is the gate. Run it directly to see what is missing; ``tests/test_api_docstrings.py`` is what enforces it, and ``check_api_inventory.py`` holds its idea of the published surface to the inventory a real build writes. It is a test rather than a pre-commit hook because it imports ``tephpy``: the other local hooks are pure-stdlib text scanners and run in the isolated environment pre-commit builds, whereas declaring this one's ``additional_dependencies`` would restate ``requirements/pypi-core.txt`` and drift from it. The same gate checks that a published object documents what it **raises**. The rule is deliberately narrow: only an exception raised in the object's own body, with no propagation through the calls it makes. A propagating analysis cannot follow dynamic dispatch, cannot see an exception a third party raises — ``datetime.fromisoformat``'s ``ValueError`` is correctly documented and invisible to it — and reports internal guards no caller can reach. Each of those is a false positive, and a gate that cries wolf gets switched off. So *documented but not found* is never an error; only the missing direction is checked. A class is read through ``__post_init__`` or ``__init__`` rather than its own body, because that is where a dataclass validates and the class docstring is the only one the API reference shows. Documenting the raise on the private validator instead would put it where no reader looks. ``Raises`` entries are listed **alphabetically**, which the gate also checks. The rule earns its place where one failure is reachable from many entry points: thirteen docstrings across ``plotting`` restate the same configuration-driven raise set by hand, and nothing else keeps those copies in step (:issue:`226`). **numpydoc enforces none of this.** Its checks stop at ``RT05`` and ``SA04``, with no ``RS`` family and no rule for a version directive. Any statement to the contrary — including in the frozen implementation plans — is wrong. .. _bibliography: Bibliography ------------ A published source is cited with ``:cite:`` against a key in ``docs/src/refs.bib``, and renders in :ref:`references`, which lists the file with ``:all:``. That is the fifth referencing mechanism this page describes, and the narrowest: it is for sources outside this project that are *published* — a printed chart, a textbook, a standards document. A web page with no edition is a :ref:`documentation link <documentation-links>` or an inline URL; another tephpy page is a :ref:`cross-reference <cross-references>`; a specification section is a :ref:`citation <specification-citations>`. Reach for it where a convention needs an authority. ``tephpy`` emphasises the 0 °C isotherm because Met Office Factsheet 13 draws it distinctively, and a reader who doubts that is entitled to the edition and the page rather than to a sentence saying somebody checked. An entry reproduces the source's published title, which means bracing it — ``title = {{Upper air observations & the tephigram}}`` — because pybtex lowercases an unbraced one and would publish a title its source does not carry. Record the edition or year and the date the source was consulted, in ``note``: a citation is provenance, and provenance without a date ages into a claim about a document that may since have changed. ``:all:`` on the directive means an entry added and never cited still renders, so the file cannot quietly accumulate sources nothing uses. .. _reviewing-claims: Reviewing Claims ---------------- Four of this project's gates execute something and ask whether *an* answer came back: ``tests/test_docs_snippets.py`` runs the python, ``check_docs_figures.py`` compares the images, ``check_citations.py`` resolves the section anchors, and ``check_documentation_links.py`` resolves the URLs. None of the four asks whether the answer is the *intended* one, and a sentence that executes nothing is read by nobody but a reviewer. The four questions below are that review, and each is written to have a name for an answer, because "did you verify this?" does not (:issue:`193`). **Which member did you check?** A sentence covering a set is checked against every member of it, not against the first. *Each*, *both*, *all* and *every* announce such a sentence; the commonest form announces nothing and simply names two things. The review of :pull:`191` found four in one branch — a page proposed as "Decode TEMP and BUFR with ecCodes", where ``bufr_dump`` will not read a TTAA bulletin; a specification saying the recipe points at ecCodes, which is true of one of its two formats; "six entries, each with an onward pointer", where two of the six carry none; and "the page registers in ``docs/src/howtos/index.rst`` and in ``tests/test_docs_snippets.py::DOCUMENTED``", where four membership lists take the entry and the sentence named two. The set is routinely larger than the sentence admits, so count it before writing *each*. Spec §9 now reads "most of them naming what to reach for instead", which is what counting produced. Say in the pull request which member you counted. **Does the title survive that?** A page title is a claim, and the least-reviewed sentence on the page: the ecCodes title above named a format the page's own body then said the tool cannot read. Read a title against :ref:`title-style` once the page is finished, and against the question above whenever it names more than one thing. This is the review spec §8.6 requires of a title, and this section is the checklist it names. **What did you run?** A claim about a tool this project will never install cannot be pinned by a gate. ecCodes is the standing case: it is a non-goal, so it is never in the ``test`` feature, on any Python, in any tier (scope spec §3.2). What can be recorded is what was run — the tool, its version, the date, and what the run did *not* establish. The plan of :pull:`191` did this before the recipe was written, under the heading "What was verified": ecCodes 2.48.0 through ``pixi exec --spec eccodes``; the key names and the ``MISSING`` sentinel read off genuine ``bufr_dump -p`` output; and no transcript, because ecCodes ships no sample sounding message and encoding one failed on a delayed-replication array-size mismatch. That record is why the page shows an invocation and describes its output in prose rather than inventing a session — which is what the same branch nearly shipped. Plans freeze on merge and are not published (docs spec §3.4), so provenance left only in one is provenance a reader of the claim cannot reach. Where the claim is published, carry the tool, version and date into the living specification beside it. **Does it say how, and did you read that?** A sentence about what a function *does* is checked by reading it once. A sentence about *how it arrives at the answer* is a claim about an implementation that can change underneath the page, and has to be re-read whenever that implementation does. Both defects :issue:`206` records are of the second kind, and nothing here catches that shape: a false sentence about a function executes nothing, and the function it names resolves perfectly, so the four gates above have nothing to fail on. *Parcel Ascent and Normand's Point* said :func:`calc.normand_point <tephpy.calc.normand_point>` "takes the dry adiabat through the parcel's temperature and the mixing-ratio line through its dewpoint, and returns where they meet. Nothing is iterated and nothing is fitted". It intersects nothing the diagram draws; it delegates to MetPy, and this project's own test is named ``test_normand_point_is_the_metpy_lcl``. That second sentence is worse than unverified — it is a claim about a moving target, false against an older MetPy and true today by accident. *Why the Axes Are Rotated* said ``tephpy`` computes the isobars and pointed at the transform that runs the other way. So prefer the contract. Say what a function takes, returns and raises, and leave how it gets there to the function. Where the method *is* the point — the rotated-axes page cannot explain why isobars are computed rather than ruled without describing the computation — describe it, and name in the pull request the function you read to check it. A page that describes method carries a maintenance obligation the contract does not, which is the reason to take one on deliberately rather than in passing.