Writing the Documentation¶
Rules for the documentation itself: the reStructuredText under docs/source/
and the pcapkit docstrings the API reference renders from. Most were settled on
#719, the prose sweep. As on Running the Repository, every ruling is paraphrased
rather than quoted; the issue named beside a rule holds the original wording.
Concision¶
Every page, docstring and comment is as short as it can be while staying accurate. Cut restatement, hedging, narration, and any sentence that would survive deletion unnoticed; keep the specific fact, name and reference. Where the content is structure or a run of parallel items, a diagram (Mermaid for Flows) or a table says it in less. This reaches the Markdown documents and every code comment too. Ruled on #719 and reaffirmed by the owner in session, recorded on #2787.
Cut timed context – when a thing changed and what it used to do – everywhere but version-bounded prose: changelog entries, breaking-change and deprecation notes, and the version directives. Keep a design decision: where a reader would otherwise undo a deliberate choice, its reason is tightened rather than removed. Ruled on #719.
Heading Case and Shape¶
Title Case, and short. Sentence case is only for a heading that genuinely is a sentence, such as a how-to question, and that was ruled rare: a sentence should generally not be a title at all. Settled on #719.
Title Case is the conventional kind, not every-word capitalisation: a, an,
the, and, or, of, in, for and to stay lowercase unless they
lead. That reading was put to the owner on #719 and left standing; The
all Extra and Issue and Pull Request Labels on Running the Repository follow it.
Casing is the easy half. A correctly cased heading still breaks the rule if it is a clause: What a Failed Lookup Raises became Failed-Lookup Exceptions. A finite verb makes a heading a sentence and earns a rewrite; a gerund or infinitive phrase is a noun phrase and needs only re-casing, so Running the tests becomes Running the Tests.
Note
A heading must also describe its section; check that on any rewrite. One Approval, Whole Release became The Release Pipeline because the section opens on a Mermaid flowchart, and the approval prose it was named for sat well below it.
A module docstring’s = title underline runs six past the title. The title
is measured as written, inline markup included, as the crawler templates under
pcapkit.vendor compute it. Ruled on
#2718; tests/project/test_docs_module_title_underline_2718_unit.py
fails on an underline of any other length.
"""Some Module
=================
"""
Renaming a Heading¶
Each of these traps has been hit.
Re-measure the underline against the new text. A case flip usually keeps the length; a rewrite rarely does, and docutils only warns on a short underline, which the build does not fail on.
A heading is a link target, so a rename is a repository-wide sweep, not a
docs/source/ one: .rst prose, :ref: and :doc: link text, toctree
entries, implicit ` `Text`_ ` references, pcapkit docstrings and
tests/. One rename left stale references in
pcapkit/corekit/sentinels.py (a shipped docstring the API reference renders),
tests/corekit/test_sentinel_exports_unit.py and
tests/project/test_conventions_doc_claims.py.
Warning
Derive the old headings from the pre-change file, not from the diff, which drops any heading whose underline is outside the hunk. Search every tracked file, whitespace-flattened, for each pre-change heading string.
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 without -n or -W and a dead reference renders as plain text.
#934 found a batch that way.
No tool checks the prose around a renamed heading either: making one plural left a following “it” on The Registry Protocol without its antecedent.
Mermaid for Flows¶
Draw the structure, and keep the prose beside it short. A pipeline, a hierarchy, a dispatch chain, a lifecycle or a decision procedure gets a Mermaid graph rather than an ASCII diagram, even where its paragraph reads well, since a graph reads faster. #719 asked for graphs where necessary and helpful; the owner widened that in session, recorded on #2787, keeping #719’s bounds and adding the last of these:
A short sequence does not earn a graph. Two steps read well as a sentence.
A rationale stays prose. A diagram carries structure and sequence, not why a choice was made, which #719 protects rather than compresses.
Do not redraw a graph another page already has. Approving the navigation work on #719, the owner required that nothing duplicate information already shown.
Every node and edge traces to the code or to the page, so Accuracy applies to each.
Every diagram is vertical, because a horizontal one renders too small to read. Ruled by the owner in session and recorded on #2787:
A flowchart is
flowchart TDorflowchart TB, and a subgraph inside one stacks too, so it sets no horizontaldirectionof its own.A state diagram is
stateDiagram-v2withdirection TB.A sequence diagram is allowed only while its participants are few, since they lay out side by side;
wrap: true,widthandactorMarginin its front matter wrap its messages between narrow participants.
Vertical is judged on the render, which must come out taller than wide. A diagram is
TD or TB unless an LR layout measures both taller than wide and larger than
every TD attempt, as the workflow trigger map and the field and protocol
hierarchies do.
Where a vertical layout runs wide or piles up its labels, stack siblings with an
invisible link (A ~~~ B), group nodes in subgraphs, break a long label with
<br/>, lower nodeSpacing in the front matter, or split it – never turn it on
its side.
For the mechanics, follow the diagrams in the tree: each builds and is a bare
.. mermaid:: with no directive options. This page names the directive in its prose,
so the listing excludes it:
grep -rn 'mermaid::' docs/source --include='*.rst' \
--exclude='documentation.rst' | wc -l # directives
grep -rl 'mermaid::' docs/source --include='*.rst' \
--exclude='documentation.rst' | wc -l # pages carrying one
Quote a node label whose text Mermaid would otherwise parse, and leave a bare identifier bare. A graph whose labels are prose quotes all of them. A type hierarchy keeps the bare
A{{Meta}},B(Base),D([user customisation ...])andsubgraph name [Title]forms, and quotes only where the text forces it –h1["HTTP/1.*"]indocs/source/pcapkit/protocols/index.rst– and onclicktargets.Write a
clicktarget relative to its page –protocol.html#...,../../ext.html#.... The site is served under/PyPCAPKit/, so a target starting with/resolves outside it and 404s.Edge labels follow no rule. The exemplars quote some and leave others bare, such as
|workflow_run: completed|and|02:00|, which Mermaid accepts; match the graph being edited.Inside a quoted label,
<br/>breaks the line and a literal<or>is<or>. Mermaid reads label text as Markdown, so an underscore that opens or closes a name is\_–\_missing\_,pcapkit.\_\_version\_\_– or the name renders as emphasis. Mermaid’s#95;escape is no substitute: in a label with a<br/>it is decoded before the Markdown pass.Where a steep edge runs under a neighbouring edge label,
curve: monotoneXin the front matter keeps it clear, as the mint criterion’s decision graph does.Leave the sizing to
docs/source/conf.py. Itsmermaid_init_configturnsuseMaxWidthoff for flowchart, state and sequence diagrams, each of which reads its own copy, so a diagram renders at its own size; a new diagram type needs its own entry there.
The release pipeline is the example of a vertical pipeline, the engine hierarchy of a vertical type hierarchy, and Maintenance Workflow of the state and sequence diagrams.
Toctree Captions¶
A caption renders wherever its own toctree renders, and that differs by toctree. Measured on a built tree:
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.
So the per-package toctrees are deliberately bare: a caption on a package index would
show on that page alone. The root ones in docs/source/index.rst carry captions,
and Subpackages in docs/source/pcapkit/index.rst is the only nested one. A
built tree confirms it (this page names the captions, so it is excluded):
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 # the pcapkit index page alone
The root toctrees are :hidden: because otherwise each caption renders twice on the
root page, in the body and in the sidebar: the duplication the owner ruled out on
#719.
Note
For the same reason, add no inline .. contents::: furo already renders a
sticky page-local table of contents on every page. The one in
docs/source/index.rst is commented out and stays so.
toc_object_entries in docs/source/conf.py was set by measurement, and
the comment beside it records the figures so nobody reopens it blind. That is worth
copying for the next measured setting; it is a precedent, not a ruling.
Paraphrasing a Ruling¶
Write a ruling in your own words. Do not quote the owner verbatim – a standing instruction on #719, which every page here follows (#949 converted the earlier ones).
The cost is not style. A quotation is pinned to the moment it was said, so it cannot be corrected when its subject moves, and a test asserting on it fails when the wording is tidied rather than when the claim stops being true; such tests derive the claim from the tree instead. Running the Repository and Minting an Unrecognised Value show the form: say what was ruled and where, and leave the wording there.
Name the issue, not the pull request. A pull request describes the day it merged, and a later change can make the citation wrong without touching it. The issue, where the ruling was asked for and given, outlives the work. 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.
Cite a ruling given in a pull request’s review by the issue that pull request
closes, as ruled in review of <PR> (:issue:`N`). Where it closes none, cite the
pull request with :pr: and say that no issue holds the ruling. Ruled in review of
#2599 (#2557).
The changelog and tests/ are exempt. A changelog entry exists so a
reader can find the change, and the pull-request number is that pointer –
docs/source/changelog/1.5.0.rst cites pull requests by design. Many pull
requests cited under tests/ close no issue, and where one exists it often lacks
the cited fact, which lives in the pull request’s body or review. Ruled on
#719.
Nothing else is exempt: a sibling page here that cites a pull request is unconverted, not a third exemption.
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 fixed – #719’s own charter. Docstrings have outlived what they described here before.
State no count. A count of things in the codebase or the documentation drifts as soon as what it counts changes, so living prose words the claim without one – every registered option, the remaining entries – and drops a count that was the only content. Protocol and specification quantities, code constants, versions and issue numbers are not counts, and a measurement that justifies a design choice stays, stated as a measurement. Ruled on #1103; the released changelogs are a frozen record and keep theirs.
A keyword search is not a sweep. Take every cited repository path in each form it is written in – slash path, dotted module, bare filename, elided spellings – intersect them with the diff since the merge base, and re-measure 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 had that shape:
A sweep asserted every
.. module::target resolved. The RARP page declaredpcapkit.protocols.data.link.rarp, which never existed, since RARP and DRARP reuse ARP’s data class. Fixed in68fbccd90.#911’s ruling – export the sentinel objects, not their types – was read as describing every module then holding a sentinel.
pcapkit.corekit.fields.fieldexported neither, so applying it there meant adding a name.
Know which claims are pinned. tests/project/test_conventions_doc_claims.py
checks several pages against the tree by name rather than by figure – the
extension-header table against the classes it classifies, Running the Repository’s changelog
grouping against the entries, The Registry Protocol’s helpers against the code – so
a stale claim there goes red. A page’s entry in that module’s ANCHORS fixes only
its existence, anchor and toctree position; the rest is pinned only where a test reads
it. The same module checks what neither a reader nor Sphinx would notice: in a
` `#NNN <.../NNN>`__ ` link the displayed number must match its URL’s, since a
review round once corrupted a run of well-formed ones.
Resolvable Targets¶
A .. module:: target must name a file on disk. A dangling one is worse than
none: it adds a module-index entry for a module that does not exist and a
cross-reference target that resolves to nothing. Settled on #719; the check:
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
Under a .. module::, a hand-written .. data:: or .. function::
takes the bare name. The module directive supplies the prefix, so
.. data:: pcapkit.x._T under .. module:: pcapkit.x registers
pcapkit.x.pcapkit.x._T, which no reference resolves to exactly. An object in
another module takes a :module: option, as the LINKTYPE entry on
Link Layer does. #1775 found long-form directives
that left TypeVar references ambiguous;
tests/project/test_docs_ambiguous_xrefs_1775_unit.py fails on the next one.
Every importable shipped module is documented, with one deliberate exception: the
modules under pcapkit/protocols/*/NotImplemented/, placeholders for unwritten
dissectors with no class a reader could use. Packaging does not force this – measured
on 1.5.0b8, each is in the source distribution and the wheel and imports as a
namespace-package submodule. None of those directories has an __init__.py, so
find_packages omits them. tests/project/test_docs_module_pages_2559_unit.py
fails on a module with no module, currentmodule or automodule directive,
and on a placeholder that gains one.
When something is removed, its documentation entry goes too: with a deprecation note
where a user could have depended on it, deleted outright where it never shipped, as the
rarp entry was. Ruled on #719.
Version Directives¶
One versionadded per version in a docstring or prose block; same-version
directives merge. A body that only names the thing added and cites its issue or pull
request is dropped, leaving the bare tag, since the summary already names it. A body
with content of its own stays, such as a move (Moved here from …), a rename or a
takeover. Ruled on #2530.
Format and Mechanics¶
reStructuredText under
docs/source/, Markdown outside it, the READMEs included. Ruled by the owner on #719, correcting a blanket always.rst;CONTRIBUTING.md’s Documentation section records the same split. Theglobal-includeinMANIFEST.indoes not reach Markdown, so the READMEs underexamples/ship throughrecursive-include examples *.md, and any other Markdown file ships only if setuptools or anincludeline adds it.Cite an issue or a pull request by role wherever Sphinx renders the text. In an
.rstpage, a docstring or a#:doc-comment an issue is:issue:`NNN`and a pull request:pr:`NNN`, by what the number is rather than what a URL says: a bare#NNNrenders as plain text there, and a hand-written tracker link hides which of the two it names. A plain#comment, a workflow’s YAML comment,tests/and any other unrendered file keep#NNN, which GitHub links. A link into another repository or to a Discussion keeps its URL, andGH-NNNis a deliberate issue form. Ruled on #1103.tests/project/test_docs_issue_pr_roles_1103_unit.pyfails on a bare number, a hand-written link or a role of the wrong kind in rendered text, andtests/project/test_issue_refs_in_comments.pyon a role in a plain comment. Each number’s kind comes fromtests/project/issue_pr_roles_fixtures/kinds.json, whichpython util/issue_pr_kinds.pyregenerates from the tracker and the scheduled Issue and PR Kinds workflow proposes refreshing (GitHub Actions Workflows). The test skips a number above the file’s highest until a refresh, and fails on a lower one the file lacks, such as a Discussion’s. ItsALLOWEDset exempts the hand-written links and the bare#1962on GitHub Actions Workflows, which other tests pin in that form, so converting one changes its pin in the same commit.Prose names the real defining module, not a re-export, a strand of the consistency sweep on the roadmap. Every sentinel is defined in
pcapkit.corekit.sentinels, and Naming a Sentinel’s table names it rather than the modules that use them, which do not re-export them (#1744).A verbatim upstream port is exempt from these conventions. Naming a Sentinel gives the reason: a vendored copy’s value is that it can still be diffed against upstream, which a house-style rewrite destroys.