Re-anchoring the Plotting Section — Design Specification#
Estimated reading time: 22 minutes
This specification covers the parent specification’s spec §3.2, the plotting
section, and the 170 citations that land on it.
Living document. This specification is maintained alongside the code, not archived behind it. Where it and the tree diverge it is the specification that gets corrected. Read it as current.
Date: 2026-09-09 (originated; maintained since)
Status: living design specification
Citation prefix:
anchor spec §…— named for the work, re-anchoring one section, and not for the anchor machinery in general, which isdocs spec §3.6’sScope: giving the parent specification’s §3.2 an internal structure, and moving the 170 citations that land on it onto the subsection each means
Parent spec:
2026-07-22-tephpy-design.md— §3.2 is the section this restructuresSibling specs:
2026-08-03-published-specs-design.md—docs spec §3.6’s anchor/heading contract, which decides the shape available here;2026-09-08-contributor-guide-design.md—contributor spec §3.8’s practice of recording what a gate cannot hold
1. Purpose#
Nearly half the citations of the parent specification land on one section, and that
section has nowhere specific to land. Measured 2026-09-09 on main at e2bd346:
|
413 lines (126–538), 28% of a 1465-line document |
citations of it in |
170 of 401 — 42% |
files carrying them |
15, of which five carry 87% |
citations naming a subsection of it |
0 — there are no subsections to name |
of the 170, references to re-point |
164; six stay on the container (§3.3) |
internal structure |
15 top-level bullets and ~90 lines of discrete topics, under no subheadings at all |
So §3.2 absorbs 42% of the citations in 28% of the text, and each of the 170 resolves to the same 413-line target. A reader following one arrives at a section and must then find the paragraph themselves.
The counts exclude the other sixteen specifications’ own §3.2, whose prefixes are derived from each document’s first anchor slug rather than hand-listed. A first pass using a naive pattern under-reported by eight, discarding citations preceded by an ordinary word — “as described in spec §3.2”. The derivation is recorded because the number is the argument.
#293 carries the measurement; #66’s third part is what it was taken for.
2. Decisions#
Seven subsections, cut thematically rather than per bullet. §3.2’s fifteen bullets run from one line to 63; a heading per bullet would give a one-line bullet its own citable number and produce a set too fine to stay stable. The anchors are an interface 170 citations depend on, and fewer, larger, topic-shaped anchors survive editing (§3.1).
§3.2 gains headings, because anchors cannot exist without them.
check_citations.py’scheck_anchorsasserts in both directions that every anchor sits immediately above the numbered heading it is numbered for, and that every numbered heading carries an anchor. So this is not “sprinkle anchors on a wall of prose”; it is giving §3.2 the structure it has never had (§3.2).Every one of the 170 citations is considered, and 164 are re-pointed, in six batches by file. Adding anchors while leaving the citations on the container would make the section navigable and leave untouched the thing #293 was filed about (§3.3).
The tool is a worksheet, not an oracle. Measured: keyword matching over a citation’s surrounding prose classifies about half, and gets worse with more context — 49% at 120 characters, 41% at 400, 27% at 900 — because a wider window matches more topics. Clustering by enclosing symbol gives 125 groups, 105 of them singletons. Neither makes the work small, and a tool presenting confident proposals for half of them would invite rubber-stamping exactly where attention is needed (§3.4).
Meaning is reviewed, not gated, and the limit is written down. The gate checks that a citation resolves; nothing can check that it names the section meant (§3.5).
3. Architecture#
3.1 The seven cuts#
Verified as a partition: 413 of 413 lines, no gaps and no overlaps.
§3.2.1 The diagram, and what is drawn by default 126-153 28 lines
TephigramAxes, the grid, the five families, differences from tephi
§3.2.2 Isopleth labels: inline or on the edges 154-208 55 lines
placement, the declutter control, the inline box's tint and clearance
§3.2.3 Claimed edges, their ticks and the title 209-260 52 lines
claiming and releasing, stock ticks, the split axis title, edge_axis
§3.2.4 Emphasis 261-343 83 lines
emphasis= on every accessor, and emphasis forcing its member drawn
§3.2.5 The plotting accessors 344-429 86 lines
plot_profile, plot_sounding, plot_barbs, shade_cape/shade_cin,
annotate_indices, set_extent
§3.2.6 The cursor readout 430-451 22 lines
format_coord, the field registry, NaN rendering
§3.2.7 Extent, edge coverage and the panel layout 452-538 87 lines
DEFAULT_EXTENT, the dead corner, the per-family coverage table,
the side-of-axes contract
The listing is fenced deliberately, and the prose below names these subsections by
title rather than by number. They do not exist yet, so citing one would resolve to
nothing and fail check_citations.py — and read_lines skips fenced blocks, which is
the same allowance docs spec §3.3 relies on to show an anchor that is an example
rather than a reference. This specification cannot cite the sections it is proposing.
The cursor readout stands alone at 22 lines, which is smaller than the others and deliberate: nine of the citations carrying an unambiguous topic signal are about the cursor readout, and the field registry is exactly the kind of specific target this work exists to provide. Folding it into the accessors would put those citations back on an 108-line section.
Corrected 2026-09-09, while the plan was being written. This section first said the cuts
fall on existing paragraph boundaries and that no prose is rewritten. That is true of two
of the seven. Measured: the first cut and the last fall on clean boundaries; the other five land
inside a bullet list — the fifteen bullets are one list introduced by Differences from tephi:, and a heading between two of them splits that list into six, orphaning bullets from
their lead-in.
The granularity stands and the constraint narrows: no paragraph is rewritten, and each interrupted subsection gains a lead line. The five open with a one-line lead naming what they cover, so a reader arriving at a heading is not dropped into an unintroduced list. Nothing inside a bullet is edited.
Corrected 2026-09-09, at implementation. This paragraph first said that
Differences from tephi: is re-sited into the first subsection’s prose. It needs no move:
the first cut falls above it, so it already sits inside spec §3.2.1, introducing the
bullets that stay with it. Verified after the fact by diffing §3.2 against main with the
anchors and the seven headings removed: 22 lines added, none removed — the five leads
and their spacing, and no sentence deleted or duplicated.
The alternative was cutting only on clean boundaries — about three subsections, leaving citations on a ~300-line list, which is barely better than today and would not have justified the work.
A second correction. The cursor-readout cut was first sized at ~63 lines.
That figure came from a flat bullet scan that measured format_coord’s bullet to the next
- line, which lies beyond the section boundary — the true extent is 22. The partition
above is measured from the boundaries themselves.
3.2 Headings, and the collection’s first fourth level#
docs spec §3.6’s contract is enforced by check_anchors, which reads each document twice:
down from every numbered heading, catching one with no anchor or the wrong one; and down from
every anchor, catching one whose heading has been deleted from under it. An anchor without a
heading fails. So the seven anchors arrive as seven #### 3.2.N headings.
Every one of the collection’s 132 headings is ### today, so these are its first fourth-level
headings. HEADING in tephpy_citations.py already matches #{2,6}, and the citation
grammar’s \d+(?:-\d+)* already admits (spec-3-2-1)=, so no machinery changes. What is
untested is presentation — how a fourth level renders in the theme’s secondary sidebar, and
whether the specification’s own reading-time banner and page length change materially. §7
carries that.
The container anchor stays. (spec-3-2)= remains on the ### 3.2 heading, so
spec §3.2 goes on resolving. A claim that genuinely spans the section keeps somewhere to
point, and nothing outside this work breaks on the day the headings land.
3.3 Re-pointing, in six batches#
Five files carry 87% of the citations, so the work batches into reviewable units — one file, one commit, one review:
batch |
file |
citations |
cumulative |
|---|---|---|---|
1 |
|
46 |
27% |
2 |
|
38 |
50% |
3 |
|
30 |
67% |
4 |
|
17 |
78% |
5 |
|
17 |
87% |
6 |
the remaining ten files |
22 |
100% |
Six of the 170 are not re-pointed. One is a grammar fixture:
tests/test_citations.py carries Spec §3.2 inside an inline literal as test data
demonstrating a sentence that opens with a citation, not a reference to the design.
Re-pointing it would edit the input to a test about citation grammar. The other five make a
claim that genuinely spans the subsections — a module’s scope line, a test file’s section
heading, a Raises list, a note on the keyword-only signatures, and the teardown a figure
clear stands down from — and the container is what such a claim is for (§3.2). So 164 move
and six stay.
Corrected 2026-09-09, at implementation. This section first said 169 citations, of which
168 move and one stays. Both figures were wrong, for two independent reasons. The inventory
was one short because the scan that produced it required the word spec beside the section
number, so it missed src/tephpy/_constants.py’s BARB_INCREMENTS, which cites the section
second in a compound run. Counting is now done with the project’s own citation grammar — the
resolver check_citations.py itself uses — which no compound run, capitalisation or prefix
fallback can hide from; the same flaw undercounted §1’s denominator, 364 against a true 401,
and spec §6’s citations, 45 against 47. The second reason is not a measurement error at
all: “one stays” assumed the fixture would be the only exception, when §3.2 above had
already provided for claims that span the section. Five turned out to be of that kind, each
upheld under review.
Batch order is by size and not by dependency: the batches are independent, because a citation is a comment and re-pointing one cannot affect another. Ordering by size front-loads the attention while it is freshest, and gets the two hardest files reviewed first.
A batch is not split across commits. A file half re-pointed is a file where the reader cannot tell which citations have been considered.
3.4 The worksheet#
A throwaway script emits every citation with its file, line, enclosing symbol and surrounding context, ordered by batch. It exists for completeness — 170 sites is more than anyone holds in their head, and the failure to design against is a citation nobody looked at, not a citation looked at and got wrong.
It proposes nothing it cannot justify. Where a keyword signal is unambiguous the row may carry it as a clearly-labelled hint; where it is not, the row says so. Decision 4 records why: the measurement says such a signal is right about half the time at best, and a column of confident-looking guesses is worse than an empty one, because it converts review into assent.
It is scaffolding and is not committed. What is committed is the re-pointed citations and this specification.
3.5 What is checked, and what is not#
Checked. check_citations.py resolves every citation against the anchor registry, so a
a citation naming a subsection that does not exist fails, as does an anchor whose heading has gone. The
existing gates need no change.
Not checked, and this is the honest limit. Nothing verifies that a citation names the section it means. A citation of the accessors subsection resolves whether or not the claim beside it is about the accessors. This project has met the distinction repeatedly: #290 shipped a citation that resolved to a different document’s section and passed every gate until review caught it.
Recording the limit rather than implying coverage follows contributor spec §3.8. It is also
why decision 3 batches by file: a wrong anchor is most visible in the diff beside the code it
describes, which is where a reviewer is already looking.
4. Companion changes#
docs/src/developer/specs/index.rst— the prefix table gains ananchor spec §…row and its toctree the matching entry. Two hand-written lists over one set.docs/src/developer/specs/2026-07-22-tephpy-design.md— §3.2 gains seven#### 3.2.Nheadings and their anchors. No prose is rewritten; the cuts fall on existing paragraph boundaries.
5. Testing#
what lands |
what holds it |
|---|---|
every citation resolving, including the new sub-anchors |
|
every new anchor sitting above its heading, and every heading carrying one |
|
the specification rendering with a fourth heading level |
|
each citation naming the section it means |
review, per batch (§3.5) |
No new gate. The existing ones already cover everything mechanically checkable here, and §3.5 says plainly what they do not.
6. Scope#
In scope. The seven subsections of §3.1, their headings and anchors, the re-pointing of all 170 citations, and the companion changes of §4.
Out of scope. #66’s third part — the plotting tour in the developer guide — which
this unblocks rather than performs. Any rewrite of §3.2’s paragraphs: this is re-anchoring, not
re-drafting. Each subsection gains a lead line and Differences from tephi: moves, as §3.1
records; nothing inside a bullet or a paragraph is edited, and a cut that needed more than a
lead line to work would be the wrong cut. Any
change to the other sixteen specifications, whose §3.2 citations are a different document’s.
Tranches. The headings and anchors land first and alone, in one commit: until they exist there is nothing to re-point at, and landing them separately means the six batches each have a stable target. The batches then land in the order of §3.3.
7. Open items#
Tagged per docs spec §3.5.
Resolved (2026-09-09, at implementation) — how a fourth heading level renders. Both halves measured against the built HTML. The theme carries it: all seven subsections appear in the parent specification page’s secondary sidebar, nested under
3.2 plotting, at the existing depth — no theme option changes. The banner reads 107 minutes and is computed rather than literal; the branch added 137 words to a 16,072-word document, which at the extension’s 150 wpm is at most one minute. Not material.Resolved (2026-09-10, #294 via #298) —
plotting/axes.pymade a claim no section stated. Its docstring said the side panels are the diagram’s to remove on a directax.clear()and the figure’s on a figure clear, so the teardown stands down. No §3.2 subsection carried that rule, and the nearest statement wasspec §10item 16, where it was an aside about whyroot=is load-bearing rather than a specification of the ownership. The fix named here — state the rule inspec §3.2.7— is what happened, as parent-specification authorship taken up as its own change rather than smuggled into this one (§6). The citations that resolved without meaning now name the subsection that states the rule, and the four tests asserting it cite it too.Refined (2026-09-15, #325) — whether §3.2’s container citations should eventually be discouraged. Once the subsections exist, a bare
spec §3.2is sometimes right and sometimes laziness, and no gate can tell them apart, so none tries. What this section asked for instead — a way to notice the container accumulating citations again — is a census, since 2026-09-10 and #297:containers()incheck_citations.pyfinds every anchor another anchor’s slug extends, andtests/test_citations.pyrecords which citations land on one, outside the specifications themselves. The record is keyed by file and anchor, not by a total — a total lets a citation removed from one file pay for one arriving in another, and neither gets read (#297 review) — and by file rather than by line, so an edit above a citation does not churn it. A change fails the test and the message names the file and both counts, so the judgement is made once, by a reader, at the moment it arrives.Measuring it corrected this section’s premise. §3.2 is not the only subdivided section: 23 anchors have children, and 36 citations outside this collection land on one. §3.2 takes 14 of them, and so does
configfile spec §5over two subsections — recorded as #296 rather than assumed to want the same answer.What a release moves is outside it (#318):
CHANGELOG.rst, and every fragment towncrier consumes into it. Assembly deletes each fragment and writes its text into the one file, so the citations are conserved while every key they sat under changes — four rows at once, on the commit that gets tagged, and again at every release. Nor is there a judgement left to make: an entry records what a pull request did with the sections as they stood, and a section subdivided later leaves that record true. The corpus keeps both files, so every citation in them must still resolve under docs spec §3.6; only the census stops counting them. What that gives up is noticing a new fragment that cites a container, which the census had done once, for the two in #300 — and both were upheld.Deferred — whether the other heavily-cited sections deserve the same treatment.
spec §6takes 47 citations andspec §3.4takes 31; neither approaches §3.2’s 170, and the same measurement should be made before assuming the same answer.
8. References#
#293 — this work, with the measurements that motivated it
#66 — populate the Diátaxis quadrants and build out a developer guide; §3.2’s split is its third part
docs spec §3.6— the anchor and heading contract this works withincontributor spec §3.8— recording what a gate cannot hold, the practice §3.5 follows