tephpy topic discovery — design specification#
Estimated reading time: 48 minutes
Living document. This specification is maintained alongside the code, not archived behind it. The taxonomy module, the extension, the gate and the report it describes cite it by section —
topics spec §3.4and the like — so these sections are the reasoning behind what that code does, and where the two ever diverge it is the specification that gets corrected. Read it as current.
Date: 2026-09-03 (originated; maintained since)
Status: living design specification
Citation prefix:
topics spec §…— nottags spec, because gallery spec §3.6 already owns “tags” for sphinx-gallery’s own mechanism, and a citation that reads as naming that rule while meaning the site-wide taxonomy is the collision plots spec avoided when it declinedfigures spec. The subject here is the reader’s topic, not the markup that records itScope: one taxonomy module, one Sphinx extension, one published page, one pytest gate, one scheduled report, and a
:tags:field list on 14 narrative pages; the five gallery examples keep the tags they already declare; no change tosrc/tephpy/. The amendment to.github/scripts/check_glossary_links.py, so the glossary gate skips a leading docinfo field list as metadata rather than reading it as prose (§3.2), is not a companion change but a prerequisite — a scope line omitting it would describe a change that could not have been mergedParent spec:
2026-08-20-examples-gallery-design.md— gallery spec §3.6 is the closed vocabulary this generalises, and gallery spec §7 is where the site-wide index was first deferredSibling spec:
2026-08-27-narrative-quadrants-design.md— narrative spec §3.8 rejected this feature on a premise §1 below re-examines, and narrative spec §3.1 describes the reader it is forRelated:
2026-08-31-reading-time-design.md— reading spec §3.1’s module split is the pattern §3.5 follows, and reading spec §3.7’s exemption list is one of the registration sites §4 enumerates
1. Purpose#
Diátaxis segregates documentation by the reader’s intent: learning, working, understanding, looking up. tephpy’s four quadrants are that framework made literal, and the clarity is real. But a reader does not always arrive with an intent. Often they arrive with a topic — “units”, “how do I get my data in”, “what is CAPE” — and a topic is orthogonal to intent. It lands in two, three or four quadrants at once, and the reader has no way to see that except by visiting each in turn and hoping.
Measured on the corpus of 2026-09-03, sounding appears in twelve of nineteen items across
three quadrants; diagram and isopleths each appear in six items across all four. A
reader following one of those topics currently has to know to look in the tutorials, the
how-tos and the gallery. Nothing on the site tells them so.
The alternative on offer is the Read the Docs search box, which is a full-text index over prose. It answers “which pages contain this word”, which is a different and weaker question than “which pages are about this subject”, and it cannot say which quadrant an answer came from.
1.1 The rejection this reopens#
This feature was rejected twice. gallery spec §7 rejected the sphinx-tags dependency on
2026-08-20 and left the site-wide index as a question for the plan that would own the
narrative quadrants. narrative spec §3.8 then closed it on 2026-08-27:
After this plan the narrative corpus is about eleven pages across three quadrants, each with a landing page and a toctree. A tag index is navigation for a corpus too large to browse, and eleven pages is not one.
That reasoning is sound and this document does not contradict it. It reopens the question for two reasons.
The premise has moved. Those three quadrants now hold fourteen pages — tutorials 3, how-tos 9, explanation 2 — and the tagged corpus including the gallery is nineteen items. Growth alone does not overturn a decision, but the number it rested on is no longer the number.
The question answered was a different one. narrative spec §3.8 asked whether the corpus is too large to browse. That is a question about volume, and its answer is still arguably no. This document asks whether the corpus is organised on an axis orthogonal to how a reader arrives, which is a question about structure and is unaffected by volume. A four-page corpus split across four quadrants has the same problem in miniature. §2 decision 1 is what falls out of taking the second question seriously while conceding the first.
2. Decisions#
The index is the deliverable; the filter is an enhancement on it. Nineteen items fit on one screen, so a filter over them is a convenience rather than the feature. What solves §1’s problem is a single page on which a topic’s pages sit together with their quadrants labelled, which no quadrant landing page can do. The filter earns its keep as the corpus grows, and costs almost nothing once the tags exist (§3.6).
One vocabulary, one declaration per item, two declaration sites. sphinx-gallery reads
# sphinx_gallery_tagsfrom an example’s source and nothing else will, so gallery tags stay where they are (gallery spec §3.6). Narrative pages declare in page metadata. Neither is copied into a registry, because a registry that restates a declaration is a second thing to keep true (§3.2).Promotion is not deletion. A term earns a button by spanning two or more quadrants and selecting under half the corpus. A term that earns no button still exists, still tags its pages, and still drives the gallery’s own filter. Deleting unpromoted terms would strip
barbsandoverlayfrom the gallery filter that needs them, and would blind the coverage report to exactly the terms whose promotion is the signal worth watching (§3.4).Both thresholds are relative, so the taxonomy tracks the corpus. Two quadrants is a count because there are only ever four; the breadth cap is a fraction. Neither is a number anyone edits as the documentation grows, which is what makes the rule survive the growth it is designed for.
The rule is gated; its output is not. Freezing the promoted set as a baseline would fail CI on every new page that legitimately changes it, which inverts decision 4. What the gate asserts is that the rule was applied correctly (§3.7).
An empty cell in the coverage matrix is a question, not a defect. Diátaxis does not require every topic in every quadrant. The matrix belongs in a report a human reads, and never in a gate, because a documentation gate that manufactures work makes the documentation worse (§3.8).
No new dependency.
sphinx-tagswas rejected on 2026-08-20 and stays rejected, for a reason that has outlived the premises of the original rejection (§5).
3. Architecture#
3.1 The corpus#
Nineteen items: the fourteen narrative pages of the tutorials, how-to and explanation quadrants, and the five gallery examples.
The reference quadrant is out. Its pages — the glossary, the CLI and configuration references, the bibliography, the changelog — are lookup surfaces a reader reaches by name, and the generated API is ninety-four objects that would dominate any filter built over the same buttons.
The developer section is out, and §8 records why the reason changed. It was first excluded because #66 was expected to change which pages existed there, and tagging a set about to be rewritten files the wrong set. That reason expired when the rewrite landed. What replaces it is durable and is three things at once. The vocabulary of §3.3 is about the diagram: measured 2026-09-11 against the eight pages the section now holds, seven of them — contributing, testing, changelog, docs-style, packaging, continuous integration, and the specification index — are described by no term in it, and the corpus being discovered rather than listed, they would fail the gate until it grew terms for them.
That measurement is of each page’s subject, checked one page at a time against §3.3’s
covers/not table, and it has to be: a scan for the words instead reports docs-style.rst
carrying twelve of the seventeen, because the page quotes example prose about soundings,
parcels and MetPy to illustrate what a review question catches. The other two collisions
are plain homonyms — ci.rst’s “analysis” is the CodeQL job and its “labels” are GitHub’s,
and the specification index’s “config” is the configfile spec prefix in a table of
prefixes. A term covers a page when it says what the page is about, which is the one
question a word count cannot answer. Those
terms would be process terms, confined to one section by construction, which §3.4 promotes
never and for the stated reason that a term confined to one quadrant describes something
browsing that section already solves — and since #306 that section has a landing
table doing exactly that. The eighth page, the plotting tour, is described by four
existing terms, and tagging it alone is the worst of the three: its terms do span quadrants,
so a reader filtering isopleths for a tutorial would be handed a contributor’s map of
module ownership from a page this index labels by quadrant.
The corpus is discovered, not listed. Every published page under the three narrative quadrants must carry tags, so a page added tomorrow fails the gate until it declares them. A hand-maintained list is one a new page silently misses, which is the failure mode reading spec §3.6 already reasons about for its own coverage gate.
Discovery is recursive, so an item is keyed by its docname — its whole path under the
source root, without the suffix — and its quadrant is that path’s top-level directory.
For a page sitting directly in a quadrant those are the same string, which is why the
distinction is easy to lose: taking the immediate parent instead would file
howtos/advanced/tuning under a quadrant called advanced, entering §3.4’s span count and
§3.8’s matrix as a fifth column that no vocabulary term has ever heard of. Keying by
docname is also what makes the three readers of §3.5 agree, the extension having no other
name for a page.
3.3 The vocabulary#
Seventeen terms, closed. Each is defined by what it covers and what it excludes, because the boundary is the part a contributor needs: a term with no stated edge is one two people apply differently, and the gate of §3.7 cannot see that — both spellings are legal. This table is the authority when they disagree.
term |
covers |
not |
|---|---|---|
|
deriving quantities from an ascent: lifting a parcel, computing stability |
the named numbers themselves ( |
|
the wind column drawn deliberately — an explicit |
a page whose sounding carries wind it never draws or discusses |
|
the tephpy logo and its placement |
general figure decoration ( |
|
the configuration file, its discovery, precedence and lifecycle |
styling done in a call rather than a file ( |
|
getting an ascent into tephpy: dataframes, datasets, archives, file formats, decoders |
what the resulting object is ( |
|
the canvas: axes, extent, framing, composition, output |
the isopleth families drawn on it ( |
|
named derived numbers — CAPE, CIN, LCL, LFC, EL — and their display |
the computation that produced them ( |
|
the five families as such: intervals, emphasis, which are drawn |
the coordinate system they live in ( |
|
axis titles, edge labels, annotation, legends |
the logo ( |
|
where MetPy is the subject — what is delegated to it and why |
a page that merely imports it |
|
drawing more than one ascent, or one ascent more than once, on shared axes |
composing separate axes side by side ( |
|
the parcel path itself: ascent, Normand’s point, the LCL construction |
the energies shaded from it ( |
|
the T–ln θ coordinate system and the rotation: why the axes sit as they do |
the families drawn in it ( |
|
filled regions between traces — CAPE, CIN — and how they are drawn |
the numbers those areas equal ( |
|
the observed ascent as an object: what it holds, how it is labelled and plotted |
how it was obtained ( |
|
appearance set through a call: colour, dashes, weight, emphasis tiers |
the same set through a file ( |
|
pint quantities at the API boundary: what goes in, what comes back, what is rejected |
the physical quantities themselves |
Nine of those are gallery spec §3.6’s, unchanged. Eight are new. They name subjects the
narrative quadrants cover; parcel is the one that also has gallery content
(plot_parcel_analysis), which carries the subject under analysis rather than under a
term of its own.
Closed, for gallery spec §3.6’s reason, which generalises without amendment: a barb
button beside a barbs one splits the very index the feature exists to build. The
vocabulary grows deliberately — a contributor writes a page, finds no term fits against
the table above, the gate of §3.7 rejects the unknown term, and a term is added with a
decision and a definition behind it. A term added without an entry in that table is the
drift the closure exists to prevent, so the two land together. That is
the growth mechanism, and it is why §3.8’s report does not attempt to infer candidate terms
from word frequency.
Each item declares two to four tags, gallery spec §3.6’s own bound: one tag files an item under a single button, and a full house files it under every one, either way telling the filter nothing.
3.4 Promotion#
A term earns a button on the discovery page when both hold:
it appears in two or more quadrants, and
it selects fewer than half the items in the corpus.
The first is the point of the feature: a term confined to one quadrant describes something browsing that quadrant already solves. The second is what stops a term so common it discriminates nothing from presenting itself as a filter.
Both conditions are needed, and the corpus of 2026-09-03 demonstrates each failing alone.
barbs appears in two gallery examples and no narrative page, so it fails the first while
passing the second. sounding appears in twelve of nineteen items across three quadrants,
so it passes the first and fails the second — a button returning sixty-three per cent of
the corpus is not a filter.
Measured against the tagging of §6.1, eight terms promote:
term |
items |
quadrants |
|---|---|---|
|
6 |
all four |
|
6 |
all four |
|
4 |
how-tos, tutorials |
|
3 |
explanation, gallery, tutorials |
|
3 |
explanation, gallery, tutorials |
|
2 |
gallery, tutorials |
|
2 |
explanation, gallery |
|
2 |
explanation, how-tos |
Nine are held back: sounding as too broad, and barbs, branding, config, labels,
overlay, projection, styling and units as single-quadrant.
That units and config are held back is a real cost and is recorded as one. “How do I
work with units” is a question a new reader genuinely arrives with, and the rule gives it
no button. The rule is a testable proxy for “is this a useful facet”, and on this corpus
the proxy is imperfect at both ends. It is kept because the alternative — an editorially
curated button list — is not testable at all, and because the failure is self-correcting:
units promotes itself the moment a tutorial or explanation page touches units, with no
one editing a list.
3.5 Two modules, split on the Sphinx boundary#
The taxonomy is data and a pure function; the extension is only the Sphinx adapter. The split follows reading spec §3.1, and here it is load-bearing for a second reason.
tests/test_readingtime_directive.py guards itself with pytest.importorskip("sphinx"),
so an extension test skips in the test-py3* environments the CI matrix runs. The
existing tag assertions do not: gallery spec §3.7 has them read the example source as text
precisely so they run everywhere. Putting the vocabulary inside a Sphinx extension would
therefore make an existing gate start skipping across the matrix — weakening a gate as a
side effect of extending it.
docs/src/_ext/tephpy_topics_data.py imports nothing outside the standard library. It
holds the vocabulary of §3.3, the sphinx_gallery_tags pattern, and the promotion rule of
§3.4 as a pure function over {item: (quadrant, tags)}. The vocabulary currently declared
in tests/examples/test_examples.py moves here rather than being copied, so this
removes a duplication rather than creating one.
docs/src/_ext/tephpy_topics.py is the adapter: it reads env.metadata for narrative
tags, reads the five example sources for gallery tags, computes the promoted set, and emits
the page of §3.6.
Tests import the data module by path, using the mechanism already in
tests/test_readingtime_directive.py but without the importorskip, so the gate of §3.7
runs on every supported Python. The promotion rule being pure is what allows both of its
boundaries — exactly two quadrants, exactly half the corpus — to be asserted from fixtures
with no documentation build.
3.6 The page#
A single published page, reached from the root toctree.
It gets no landing-page card. gallery spec §5 ruled that the landing grid is the four Diátaxis quadrants and that anything sitting in it reads as a fifth; that reasoning transfers unchanged, and this page is a way into all four rather than a peer of them.
It lists every item in the corpus with its quadrant labelled and its tags shown, above
two rows of filter buttons. Filtering is client-side over data-topics and
data-quadrant attributes — the shape sphinx-gallery’s own sg-tags.js already
demonstrates — so it needs no dependency and degrades to the full list with scripting off,
which is the list that decision 1 says is the actual feature.
The quadrant row offers all four groups of §3.1, unconditionally: each always holds pages, and a row whose membership moved with the corpus would shift under a returning reader. The topic row offers only the terms that earned a button under §3.4. The two are different kinds of control — one is the coarse cut a reader already knows the shape of, the other is earned by a rule — and they are coloured differently so that the difference is visible before either is clicked.
The two rows combine differently, and the data forces it. Topics are ANDed, as they are
in sg-tags.js: an item carries two to four, so a second term genuinely narrows. Quadrants
are ORed, because an item sits in exactly one — ANDing two quadrants would select nothing
every time, and a control whose second click always empties the page is broken rather than
strict. The rows are then ANDed with each other, which is what makes “how-tos about
parcels” expressible at all, and that query is the reason this page exists: §1’s reader
arrives with a topic, and the quadrant is how they say what kind of help they want with it.
Each quadrant has a colour, carried by both its button and the badge on every row belonging to it, so a filtered list shows at a glance which quadrant answered. The four are pydata-sphinx-theme’s own semantic colour families rather than invented ones, which is what makes them correct in both light and dark without a second palette — the same reasoning that has the reading-time banner name the theme’s properties instead of hard-coding a background. Colour is never the only channel: every button and badge also carries the quadrant’s name as text.
clear is the one control on the page that is not a filter, and it is styled as one. It
takes the danger family — the only one the four quadrants leave free, and the one this
page refuses for a quadrant precisely because red reads as an action rather than a
category — filled rather than outlined, set at the end of its row, and marked with a
leading multiplication sign so it is identifiable by shape as well as by hue. It cannot
take the accent, which is already the active topic button’s fill: the earlier styling gave
it the topic buttons’ own grey and a dashed border, and it read as a ninth term nobody had
selected. It appears only once something is selected, and clears both rows, since clearing
one and leaving the other would leave the list filtered with nothing obviously still set.
The selection is reflected in ?topics= and ?quadrants= query parameters, mirroring
sphinx-gallery’s ?sg-tags=, so a filtered view can be linked to from an issue or a reply.
A term or quadrant in the URL that no button offers is ignored rather than applied, so a
stale link cannot filter the list down to nothing with no control to undo it.
3.7 The gate#
tests/test_docs_topics.py, running on every supported Python:
Every page in the corpus of §3.1 declares two to four tags.
Every declared tag is in the vocabulary of §3.3.
Every vocabulary term is used by at least one item — a term nothing uses is a typo or the residue of a deleted page.
The corpus is discovered, so a new narrative page with no tags fails (§3.1).
The promotion rule is asserted at both boundaries against fixtures (§3.5).
What the gate deliberately does not assert is which terms are promoted. Recording that
set as a baseline would fail CI on every page that legitimately changes it — write a units
tutorial and the build breaks because units newly qualifies — which inverts decision 4.
That cuts against this project’s habit, and the difference is worth stating. plots spec §3.5 freezes the figure baselines so that a change is visible in review. Those freeze things that change unintentionally. A promotion change is always downstream of deliberate editorial work and is reported by §3.8 in any case, so freezing it would buy visibility that already exists at the price of a false failure.
3.8 The report#
.github/scripts/topics_issue.py, run by a scheduled workflow, modelled on
floors_issue.py and ci-floors.yml — including the MARKER dedupe, so a standing
finding updates its issue instead of filing another one each run.
It carries two things:
Promotion changes since the last run: terms newly promoted, terms newly held back. This is what makes §3.4’s relative thresholds observable rather than merely correct. It is said twice, deliberately: once as the comment, which is the notification and reaches a reader once; and once in the body, where a newly promoted term is marked
(new)and a departed one is named on a line of its own. The comment alone would age badly — this issue is a standing dashboard for a package under sustained maintenance, and a reader opening it in a year should learn what recently changed without scrolling a year of comments. The marker means “moved at the most recent run” and clears itself at the next. That is correct and it is not sufficient: a term promoted in October is not new in November, so the marker cannot simply be kept, and a body carrying only markers answers for one month before sending the reader back to the comments. So the body also carries a dated record — “Last change — 2026-10-01:unitspromoted” — which is stamped when the set moves and carried forward untouched across every quiet run until it moves again. The two say different things on purpose: the marker says this just happened, the record says this is when it last happened, and only the second is still true a year later. The record is state and takes the same round trip the promoted set takes. The run that creates the issue carries neither: there is no previous set, and calling the whole of it new would say nothing.The coverage matrix: for each term, which quadrants hold it and which do not, closing with a column total — how many of the vocabulary’s terms each quadrant covers at all. The totals read across the grain of the rows: a row says where one subject is covered, a total says how much of the subject matter a quadrant reaches. A quadrant well below its neighbours is breadth worth a look in the way an empty cell is depth worth a look, and it is the cheaper of the two to read, being one line rather than seventeen.
The terms that are too broad to filter on, when there are any: those selecting half the corpus or more, with their count, their share and the quadrants they hold. This is the breadth half of §3.4’s rule reported rather than merely applied. Such a term is not a defect and is not deleted — decision 3 keeps it tagging its pages and driving the gallery’s own filter — but a term crossing that line, in either direction, is editorial news. Span is deliberately not part of the test: a term selecting most of the corpus is too broad whether or not it also spans, and its quadrants are printed beside it so the two failures can be told apart. The section is omitted entirely when nothing qualifies, an always-present empty heading being the kind of thing a reader learns to skip.
The matrix is the report’s second job and arguably its better one. On the corpus of
2026-09-03 it already says something actionable: analysis and shading both appear in
tutorials, explanation and the gallery and in no how-to, which is conspicuous in a
project whose how-to quadrant is its largest at nine pages.
Two limits are recorded here so the report is not read for more than it says. An empty cell is a candidate for editorial judgement and not a defect (decision 6). And the matrix can only see gaps between subjects already written about: the vocabulary is closed, so a topic nobody has documented carries no term and appears nowhere. The instrument for that gap is user demand, not coverage — #261 tracks Read the Docs search analytics, whose zero-result queries are the complement to this matrix.
Monthly, not weekly. A documentation corpus moves on pull-request timescales, and a weekly report that says nothing most weeks is one nobody reads.
4. Companion changes#
The page is a new published page, and this project keeps one set in several hand-written places. The registration sites are enumerated here so the set is visible in one place:
File |
What it gains |
|---|---|
|
the toctree entry |
|
an |
the reading-time specification |
reading spec §3.7’s exemption table, and its carrying-page count |
|
the |
|
its |
|
the filter button and badge styling |
|
the extension in |
|
the filter, registered by the extension rather than by |
|
the “Topic Tags” section, the vocabulary’s new home in “Gallery Examples”, and the topic index in the “Reading Time” exemption sentence |
|
a leading docinfo field list is metadata, not prose (§3.2) |
|
the shared |
|
its |
The reading-time exemption set (reading spec §3.7) is written in four places, not
three: tests/test_docs_readingtime.py’s EXEMPT tuple, reading spec §3.7’s exemption
table, reading spec §3.7’s own carrying-page count — the “That leaves … pages carrying
the directive” sentence, which restates the same set as arithmetic and does not update
itself when the table does — and docs-style.rst’s “Reading Time” prose.
The fourteen narrative pages each gain a :tags: field list. The five example files are
untouched.
5. Alternatives considered#
sphinx-tags. Rejected on 2026-08-20 (gallery spec §7) on two premises that have since
changed: that it duplicated a feature already installed, and that a five-example gallery did
not need site-wide tag pages. There is now a genuine cross-quadrant corpus. It stays
rejected on a premise that has not changed: sphinx-gallery locks its tags into the example
source, so sphinx-tags would need a second declaration on the gallery pages, leaving two tag
mechanisms live at once with nothing to say which an item’s tags feed — the precise
objection gallery spec §3.6 raised. It also produces per-tag pages rather than one
filtered index, and the rule of §3.4 is not expressible in it. This was reasoned rather
than probed; §8 records that.
A script generating a static page. Simpler, with no Sphinx API surface. Rejected
because it writes source into the tree, which this project avoids deliberately — gallery/
is generated and untracked so that nobody hand-edits it — and because a generator the
build then reads is a build-ordering constraint in the Makefile. It saves less than it
appears to, since the filter needs client-side code either way.
Freezing the promoted set as a baseline. Rejected in §3.7.
Inferring candidate vocabulary terms from word frequency. Considered for §3.8 and cut. It needs stopword and stemming heuristics to be anything but noise, and it is redundant with the growth mechanism of §3.3, where the gate rejects an unknown term and a human adds it deliberately.
Curating the button list editorially. Considered when §3.4’s proxy was found imperfect at both ends. Rejected because it replaces a testable rule with a judgement nothing checks, and because the proxy’s failures are self-correcting as the corpus grows.
6. Testing#
6.1 The tagging this document is measured against#
The eight-term promoted set of §3.4 is computed from a tagging proposed on 2026-09-03 by
reading each page’s title, section headings, ax.* calls and glossary references, and
confirmed on 2026-09-03: the gallery’s five rows are the tags those files already
declare, and the fourteen narrative rows were checked against the covers/not table of
§3.3 as each page gained its :tags: field list, revising none of them. The promoted set
of §3.4 therefore now rests on a measurement of the tagged pages rather than on this
table’s original proposal.
quadrant |
item |
tags |
|---|---|---|
tutorials |
|
diagram, isopleths, sounding |
tutorials |
|
analysis, sounding, shading, indices |
tutorials |
|
sounding, data-input |
how-tos |
|
sounding, data-input |
how-tos |
|
config, isopleths |
how-tos |
|
isopleths, styling, config |
how-tos |
|
diagram, sounding, parcel |
how-tos |
|
diagram, labels, isopleths |
how-tos |
|
branding, diagram |
how-tos |
|
sounding, data-input |
how-tos |
|
sounding, data-input |
how-tos |
|
units, sounding |
explanation |
|
analysis, parcel, shading, metpy |
explanation |
|
diagram, isopleths, projection |
gallery |
|
diagram, isopleths |
gallery |
|
sounding, barbs |
gallery |
|
overlay, sounding |
gallery |
|
metpy, barbs, sounding |
gallery |
|
analysis, shading, indices, sounding |
Two judgements in that table are worth recording because they are contestable. metpy is
tagged only on parcel-ascent, where MetPy is the subject of a section; it is mentioned in
units and emphasis in passing and tagging on passing mentions would inflate every count
in §3.4. And no narrative page is tagged barbs, because the string does not appear in any
of them — which is what makes barbs fail promotion.
6.2 What the gate asserts#
The five assertions of §3.7, plus unit tests of the promotion function at both boundaries from fixtures rather than from the live corpus, so that a documentation change cannot make a rule test pass or fail for the wrong reason.
6.3 What is not gated#
The page’s rendering. Whether the filter buttons work is a browser question, and this
project gates correctness of content rather than of presentation. Nothing asserts that the
page carries one data-topics attribute per item — that is a property of how tephpy_topics.py
constructs the page, not something checked. What the build does assert, at doctree-resolved:
build_corpus raises if the corpus is empty, and — because the build runs
--fail-on-warning — it fails if a narrative page declares no tags, or if its source
declaration disagrees with what Sphinx read into env.metadata. The filter itself was
checked by hand on 2026-09-03, against docs/_build/html/topics.html loaded as a
file:// URL, in Chrome for Testing 151.0.7922.34 driven through Playwright. Checked,
and observed to hold:
The filter bar appeared with one button per promoted term (eight:
analysis,data-input,diagram,indices,isopleths,metpy,parcel,shading), in both themes.Clicking a button narrowed the nineteen-item list and marked the button active.
A second button narrowed the list further rather than widening it — AND, not OR.
analysisanddata-inputtogether, which share no item, showed the empty notice rather than a blank list.clearappeared on the first selection and restored the full list of nineteen items when clicked.Selecting a topic added
?topics=…to the URL, and reloading that URL restored the selection.A hand-written
?topics=nonsensewas ignored: every item stayed listed, no button showed as active, and the unrecognised parameter was stripped from the address bar —topics.jsrunsparams.delete(PARAM)whenever the selection is empty, which an unrecognised term leaves it.With JavaScript disabled (
browser.new_context(java_script_enabled=False)), the filter bar did not appear and every item was listed.
The quadrant row of §3.6 was added after that check and exercised the same way, on the same date, in the same browser. Checked, and observed to hold:
Both rows appeared: four quadrant buttons and eight topic buttons, over nineteen items.
Tutorialsalone left three items — the three the corpus holds.TutorialsandExplanationtogether left five, not zero. This is the one that matters: the rows combine differently, and ORing the quadrant row is what the data requires rather than a preference. Under the topic row’s AND semantics this click would have emptied the page.How-To Guideswithparcelleft exactly one item, Frame the View — the cross-axis query of §3.6, and the reason the feature exists.Tutorialswithmetpy, which share no item, showed the empty notice rather than a blank list.The selection reached the URL as
?quadrants=tutorials&topics=metpy;clearemptied both rows, restored all nineteen items, stripped the query string and hid itself.Reloading
?quadrants=gallery&topics=shadingrestored one item with both buttons active;?quadrants=nonsense&topics=nonsensewas ignored in both rows and stripped.In light and dark, each quadrant’s badge border resolved to exactly the same colour as its own button when active —
rgb(0, 132, 63)for Tutorials in light,rgb(95, 180, 136)in dark, and so on for the other three — and the four differed from each other in both. Colour is carried by the theme’s semantic families, so this is a consequence of §3.6’s choice rather than four pairs of values kept in step by hand.clearwas measured against the controls it has to be told apart from, after it was restyled for reading as one of them. Text on it contrasts at 4.82:1 in light and 7.08:1 in dark, both above the 4.5:1 WCAG AA floor for text at this size. Against the inactive topic grey and against the active topic fill it is clearly distinct in both themes — CIE ΔE76 of 63.6 and 85.9 in light, 40.1 and 62.9 in dark, where under about 15 is confusable and over 25 is clearly separate. The dark pairing was checked because both colours are light pinks there and the eye can be wrong about it; the measurement says they are not close. Its accessible name remainedclear, the glyph being CSS rather than markup.
The check also caught a defect the implementation fixed before this record was written:
Sphinx places an add_js_file script in <head>, undeferred — the same place it puts
sphinx-gallery’s own sg-tags.js — so the script runs before <body> is parsed. A bare
top-level filter, run at that point, finds #teph-topic-filter absent and returns
immediately, forever; sg-tags.js avoids this by deferring its own work to
DOMContentLoaded, and topics.js now does the same.
7. Scope#
In scope. The taxonomy module and its vocabulary; the Sphinx extension; the published
page and its filter; the :tags: field list on fourteen narrative pages; the pytest gate;
the scheduled report and its script; the companion changes of §4.
Out of scope. The reference quadrant and the generated API (§3.1); the developer section, which waits on #66; any change to sphinx-gallery’s own filter, which keeps all seventeen terms including the unpromoted ones; Read the Docs search analytics, tracked separately as #261; and the five example files, whose tags are already correct.
8. Open items#
Tagged per docs spec §3.5.
Closed (2026-09-03) — the field-list metadata mechanism (§3.2). Established by build: a
:tags:field list reachesenv.metadataand renders nothing, but only on the first line of the file, not under the title as this document first proposed. §3.2 carries the measurement and the correction. The fallback directive was not needed.Open —
sphinx-tagswas reasoned about rather than probed (§5). The rejection rests on sphinx-gallery locking its tags into the example source, which is documented behaviour, but no build was run to confirm sphinx-tags cannot read a generated gallery tree. Worth ten minutes if the decision is ever challenged.Open —
soundingreturns as a button if the corpus outgrows it. At twelve of nineteen it fails the breadth cap; at twelve of twenty-five it would pass. Whether a term that broad is wanted as a button at any corpus size is a judgement §3.8’s report will surface rather than settle.Closed (2026-09-11, §3.1) — the developer section. Deferred here because its pages were expected to change, and tagging a set about to be rewritten files the wrong set. The rewrite happened —
contributor specadded four pages andtour speca fifth, #66 closed — so the premise expired and the question was taken up rather than re-deferred. The answer is that the section stays out, on a reason that cannot expire: seven of its eight pages are described by no term in a vocabulary that is about the diagram, the process terms they would need are unpromotable by §3.4’s own rule, and tagging only theplottingtour would put a contributor-facing page inside buttons built for readers. §3.1 carries the measurement.Deferred (#261) — Read the Docs search analytics. The demand signal complementing §3.8’s coverage matrix, and perishable at ninety days on the Community plan, so it is triggered by the announcement rather than by this plan.
9. References#
gallery spec §3.6 — the closed vocabulary, the flag, and why a registry does not hold tags
gallery spec §5 — why a way into the quadrants gets no landing-page card
gallery spec §7 — the original
sphinx-tagsrejectionnarrative spec §3.8 — the rejection §1.1 re-examines
reading spec §3.1 — the module split of §3.5
reading spec §3.6, §3.7 — the discovered-corpus gate and the exemption list
plots spec §3.5 — the frozen baseline §3.7 declines to imitate
#66 — the developer and contributor guide
#261 — Read the Docs search analytics as an editorial signal