Writing the Documentation¶
The pages before this one rule on library code and on running the repository. This
one rules on the documentation itself – how a page is titled, when a diagram beats a
paragraph, and what a sentence on it is allowed to claim. It governs the
reStructuredText under docs/source/ and the pcapkit docstrings the API
reference renders from, since the owner named both when settling the first of these.
Nearly all of it was settled on #719, the prose sweep, whose thread was the only place most of it lived. As on Running the Repository, every ruling here is paraphrased rather than quoted, on the owner’s standing instruction there; the issue named beside a rule is where the original wording is.
Heading Case and Shape¶
Title Case, and short. Sentence case belongs only to a heading that genuinely is a sentence – a how-to question is the example the owner gave – and that was ruled rare: a sentence should generally not be used as a title at all. Settled on #719.
Title Case here is the conventional kind rather than every-word capitalisation. The
short function words a, an, the, and, or, of, in, for
and to stay lowercase unless they lead, which was the reading put to the owner on
#719 and left standing. It is also what the tree does: The all Extra and
Issue and Pull Request Labels on Running the Repository are both in it.
The casing is the easy half. A heading can be in Title Case already and still
break the rule by being a clause rather than a title. The sweep of
docs/source/contributing/ under this ruling found, of 62 headings, 24 already
right, 16 needing only re-casing, and 22 to be rewritten. What a Failed Lookup
Raises was correctly cased and is now Failed-Lookup Exceptions.
The line that sweep drew, and the one to keep drawing: a finite verb makes a heading a sentence and earns a rewrite, while a gerund or infinitive phrase is a noun phrase and needs only re-casing – so Running the tests became Running the Tests rather than being reworded.
Note
A heading also has to describe the section beneath it, and a rewrite is the moment to check that. Two of that sweep’s own new titles were renamed again in review for failing it. One Approval, Whole Release became The Release Pipeline because the section opens on a Mermaid flowchart while the approval prose it had been named for sat well below, leaving the page’s most navigable artefact unfindable from the table of contents.
Renaming a Heading¶
Two mechanical traps, both of which bit on that sweep rather than being hypothetical.
Re-measure the underline against the new text. A case flip usually keeps the length; a rewrite almost never does, and docutils reports an underline shorter than its heading in a build that does not fail on warnings. The sweep re-checked all 62 and found none short, which is the standard to hold.
A heading is a link target, so a rename is a repository-wide sweep rather than a
docs/source/ one. In scope: .rst prose, :ref: and :doc: link text,
toctree entries, implicit ` `Text`_ ` references, and pcapkit docstrings
and the files under tests/. Applying that scope to the heading rename took two
passes: the first reported one surviving stale reference; review found five, spread
across pcapkit/corekit/sentinels.py,
tests/corekit/test_sentinel_exports_unit.py and
tests/project/test_conventions_doc_claims.py. The first of those is a shipped
module docstring that renders into the API reference, so a reader following it searches
the page for a string no longer on it.
Warning
Derive the list of old headings from the pre-change file, not from the diff. A diff-derived list silently drops any heading whose underline is not adjacent in the hunk, which is why the second pass undercounted as well as the first. What worked was a whitespace-flattened search for every pre-change heading string across every tracked file.
Nothing in CI catches a reference a rename left behind. docs/source/conf.py
sets no nitpicky and docs/Makefile leaves SPHINXOPTS empty, so the
build runs with neither -n nor -W: a dead reference renders as the plain text
it used to be, and the build still succeeds. #934 found sixteen of them at once
that way.
One thing a rename breaks that no tool checks at all is the prose around it. Turning a singular heading plural cost The Registry Protocol the antecedent of a following “it”, caught in the same review.
Mermaid for Flows¶
Where the subject is a flow, prefer a Mermaid graph to the paragraph or the ASCII diagram that would otherwise carry it – a graph is read faster than its own description. The owner ruled this on #719, asking for it where it is necessary and helpful, which bounds it in three directions:
A short sequence does not earn a graph. Two steps read perfectly well as a sentence, and a diagram of them costs a reader a context switch for nothing.
A rationale stays prose. A diagram carries structure and sequence; it cannot carry why a choice was made, and that reasoning is what #719 protects rather than compresses.
Do not redraw a graph another page already has. The owner’s condition when approving the navigation work on #719 was that nothing duplicate information already shown, and a second copy of a flow is exactly that.
The style model is the set already in the tree, every one of which builds. The sweep excludes this page, which writes the directive name three times in its own prose and would otherwise inflate both counts:
grep -rn 'mermaid::' docs/source --include='*.rst' \
--exclude='documentation.rst' | wc -l # 12 directives
grep -rl 'mermaid::' docs/source --include='*.rst' \
--exclude='documentation.rst' | wc -l # on 11 pages
All twelve are a bare .. mermaid:: carrying no directive options, and all twelve
are a flowchart: TD where the subject is a sequence or a decision, LR for a
type hierarchy. Quote a node label whose text Mermaid would otherwise try to parse,
and leave a bare identifier bare. That is what the exemplars do with node labels, and
it falls close to the TD/LR line without following it: every node label in the
six TD graphs is quoted, since every one of them is prose; the LR graphs quote
only where the text forces it, which today is h1["HTTP/1.*"] and h2["HTTP/2"] in
docs/source/pcapkit/protocols/index.rst and nowhere else. Everything else there
uses the bare A{{Meta}}, B(Base), D([user customisation ...]) and
subgraph name [Title] forms, and spends its quotes on click targets instead.
Edge labels follow no rule at all. The exemplars quote some and leave others bare,
including |workflow_run: completed| and |02:00|, which the reason above would
have quoted and which Mermaid accepts anyway – so match the graph being edited rather
than this paragraph. Inside a quoted label, <br/> breaks the text across lines and a
literal < or > is written < or >.
The release pipeline is the TD example and
the field hierarchy the LR one.
Toctree Captions¶
A caption renders wherever its own toctree renders, which is not the same place for every toctree. Measured against a built tree rather than assumed:
A toctree in the root document renders into the global sidebar, so its caption is visible from every page in the build.
A nested toctree renders only in its own page’s body, so its caption is visible on that page alone.
:hidden:suppresses the body rendering. On a root toctree the sidebar copy survives and the caption is still everywhere; on a nested one nothing is left, and the caption renders nowhere.
That is why most of the per-package toctrees are deliberately bare – a caption on a
package index would be invisible on every page but one. The three in
docs/source/index.rst carry captions, and Subpackages in
docs/source/pcapkit/index.rst is the single nested one. A built tree says so –
excluding this page, which names both captions in its own prose and would otherwise
count itself:
grep -rl 'API Reference' --include='*.html' --exclude='documentation.html' \
docs/build/html | wc -l # every other page
grep -rl 'Subpackages' --include='*.html' --exclude='documentation.html' \
docs/build/html | wc -l # exactly one
Those root toctrees are :hidden: because, without it, each of the three captions
rendered twice on the root page – once inline in the body, once in the sidebar – which
is the duplication the owner ruled out on #719.
Note
For the same reason, do not add an inline .. contents:: to a page. The furo
theme already renders a sticky page-local table of contents on every page, so an
inline directive is a second copy of it; the one in docs/source/index.rst
has been commented out since 2023 and should stay that way.
One setting there was decided by measurement rather than by preference:
toc_object_entries carries, in a comment beside it, the figures that settled it,
so that nobody reopens the question blind. That is worth copying the next time a
setting is chosen that way, but it is a single precedent and not yet a rule the owner
has ruled on.
Paraphrasing a Ruling¶
Write a ruling down in your own words. Do not quote the owner verbatim – a
standing instruction on #719, and the one every page in this directory follows.
#949 went back over the five pages that then existed and replaced their quoted
rulings with paraphrase.
What a quotation costs is not style. A quoted sentence is pinned to the moment it was said, so it cannot be corrected when the thing it describes moves; and a test that asserts on the quotation fails when the wording is tidied rather than when the claim stops being true. That change rewrote those tests alongside the prose, so that each derives its claim from the tree instead. Running the Repository and Minting an Unrecognised Value show the form: say what was ruled, name where it was ruled, and leave the wording there.
Name the issue, not the pull request. A pull request is a point in time: it describes what was true on the day it merged, and the next change past it can make the citation wrong without touching it. The issue is the durable half – where the ruling was asked for and given – and it survives the work that implemented it. So cite the issue a rule was settled on, and describe a change by what it did rather than by its number. Ruled on #719.
The changelog and tests/ are both exempt, for related reasons. A
changelog entry exists so a reader can find the change, and the pull-request number is
that pointer; converting it would delete the thing the entry is for –
docs/source/changelog/1.5.0.rst is dense with pull-request citations by design.
A substantial share of the pull requests cited under tests/ close no issue at
all – one credits a proposal to an external contributor and closes nothing – and where
an issue does exist beside a citation, it frequently lacks the fact being cited, which
lives in the pull request’s own body or review thread instead. Ruled on #719.
The rule reaches the rest of this directory as well: a sibling page that cites a pull
request is unconverted, not a third exemption. The changelog and tests/ are the
whole exempt set – nothing else is.
Accuracy¶
Verify a claim against the code it describes, never against another document. Where prose and code disagree the code wins and the prose is what gets fixed – #719’s own charter – and a docstring outliving the thing it described is a demonstrated failure mode here rather than a hypothetical one.
Re-derive a count; do not copy one. Better still, write down the command that produces it, as Running the Repository does, so the figure can be rechecked rather than trusted. A keyword search is not a sweep: the reliable method takes every cited repository path in each of the forms it gets written in – slash path, dotted module, bare filename, and the elided spellings – intersects them with the diff since the merge base, and re-measures the intersection. A tense-keyword grep misses a claim phrased as a fraction of a total.
Treat every, all, each and none as a claim about members, and
check the members one at a time. Several of #719’s findings were of exactly that
shape:
A sweep asserted that every
.. module::target in the documentation resolved. One did not:docs/source/pcapkit/protocols/link/rarp.rstdeclaredpcapkit.protocols.data.link.rarp, which has never existed, because RARP and DRARP reuse ARP’s data class. Fixed in68fbccd90.#911’s ruling – export the sentinel objects and leave their types out – was read as describing all three modules that then held a sentinel. One ran the other way:pcapkit.corekit.fields.fieldexported neither, so applying the rule there meant adding a name rather than removing one.
Some numbers on these pages are pinned, and knowing which is part of writing one.
tests/project/test_conventions_doc_claims.py re-derives figures for the three
pages that have a figure test of their own – extension-header-subclassing,
process and registry-protocol, the last most heavily of all, down to both halves
of an N of M – so a stale count there goes red instead of quiet. Every other figure,
this page’s included, is pinned by nothing: the entry it adds to that module’s
ANCHORS fixes the page’s existence, anchor and toctree position, not its arithmetic.
Re-derive before trusting, and do not read a number as load-bearing just because it is
written down. The same module checks something a reader cannot see and Sphinx cannot
warn about: in a ` `#NNN <.../NNN>`__ ` link the displayed number has to match the
number in its own URL. A review round once corrupted roughly thirty of them across two
pages and nothing caught it, because both halves were well-formed.
Resolvable Targets¶
A .. module:: target must name a file on disk. A dangling one is worse than
no directive at all: it registers a module-index entry for a module that does not
exist, and gives cross-references a target that resolves to nothing. The sweep that
settled this on #719 found exactly one, and repeating it is cheap:
grep -rhE '^\.\. +(py:)?(module|currentmodule):: ' docs/source --include='*.rst' \
--exclude='documentation.rst' |
awk '{print $NF}' | sort -u | tr '.' '/' |
while read -r path; do
[ -f "$path.py" ] || [ -f "$path/__init__.py" ] || echo "dangling: $path"
done
Every importable shipped module is documented, with one deliberate exception. The 33
modules under pcapkit/protocols/*/NotImplemented/ are the set with no entry:
placeholder sources for dissectors nobody has written yet, carrying no class a reader
could use. The exception is a choice rather than a consequence of packaging, which is
worth stating because the opposite is easy to assume – measured on 1.5.0b8, all 33
are in the source distribution and in the wheel, and each imports as a
namespace-package submodule. What they are not is a package: none of the four
directories carries an __init__.py, so find_packages returns 73 here and
omits all of them.
When something is removed, its documentation entry goes with it – carrying a
deprecation note where a user could have depended on the thing, and deleted outright
where it never shipped. The rarp entry above needed no note for that second reason.
Asked and ruled on #719.
Format and Mechanics¶
reStructuredText under
docs/source/, Markdown outside it. The owner ruled this on #719, correcting a blanket always.rstthat had been in circulation until then: the Sphinx documentation is reST, and the other documents – the READMEs included – are Markdown where that applies.CONTRIBUTING.md’s own Documentation section records the same split. One trap arrived with the ruling:MANIFEST.inreaches the two READMEs underexamples/throughglobal-include *.rstand has no*.mdequivalent, so renaming one without adding coverage drops it from every source distribution.Prose names the real defining module, not a re-export. One strand of the consistency sweep on the roadmap. The sentinels are the worked example: all four are defined in
pcapkit.corekit.sentinelsand re-exported by the modules that use them, and Naming a Sentinel’s table names the defining module rather than any of the re-exports.A verbatim upstream port is exempt from these conventions. Naming a Sentinel already carries the case that settled it, the
cached_propertybackport inpcapkit/utilities/compat.py: the value of a vendored copy is that it can still be diffed against upstream, and a house-style rewrite destroys that in exchange for nothing a reader ever sees.