Documentation Style#

Estimated reading time: 49 minutes

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 — Read a Sounding From an Archive’s “Read a Sounding From an Archive”, Work With Units’s “Work With Units”, Add the tephpy Logo’s “On the Plot or Around It” — while keeping the short ones down, as in Frame the View’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 (#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 Work With 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 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.

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:

avoid:   ``calc.parcel_path(...)`` computes a parcel's ascent
prefer:  :func:`calc.parcel_path(...) <tephpy.calc.parcel_path>` computes a parcel's ascent

Third-party objects (matplotlib, metpy, numpy, pandas, pint, xarray) resolve the same way through intersphinx — see 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 Changelog Fragments.

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 metpy.calc.moist_lapse() or 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#

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

:ref:`spec §3.2 <logo-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 <title> 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#

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.

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: #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 — 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 Emphasise a Reference Isopleth 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: Emphasise a Reference Isopleth’s “Configure It Once” section shows the context-manager idiom and then a diagram opting out of it, and Add the tephpy 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 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 — Configure tephpy 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:

$ 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:

.. 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 [Bry19] [Car82] — 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:

.. readingtime:: 45

and a rate replaces the default for that page alone:

.. 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 getting started, 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 the first-mention 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:

: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 Browse by Topic 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 #66.

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 (Sounding, Profile, 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:

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 (#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#

A published source is cited with :cite: against a key in docs/src/refs.bib, and renders in 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 documentation link or an inline URL; another tephpy page is a cross-reference; a specification section is a citation.

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#

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 (#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 #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 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 #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 #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 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.