0.33.x¶
0.33.0¶
- Released:
27.08.2026
The story of this release, in four arcs:
sources became a declared, shareable surface —
[[source.mounts]] folds external trees into the document set,
[source] dir supersedes [project] srcdir,
and variant rules decide which files are in the build,
with sphinx-build reading the same ubproject.toml keys through
sphinx-mounts 0.2.0, released alongside;
the needs domain moved in step with Sphinx-Needs
(8.4.0, also released alongside) —
list2need creates its needs in both engines,
needflow gained a portable presentation vocabulary,
and a need records its parent and its enclosing sections;
markup coverage widened —
index entries, the :download: role, Markdown links that resolve,
code-block line numbers, CSS classes, and page <head> metadata;
and the lint surface grew —
around sixty-five new diagnostic codes ship, most of them on by default,
so an unchanged project can see new warnings (see Breaking Changes).
✨ Sources & mounts¶
[[source.mounts]]— fold an external tree into your document setA generated, vendored or independently-built tree of
.rst/.mdfiles becomes part of the project with no copying, symlinking or staging. Each entry declares adir(walked with its owninclude/exclude/gitignore) or an explicitfileslist, amount_atdocname prefix, and an optionalattach_tohost document whose table of contents gains the entry (toctree_index,entry_docandattach_eachtune the wiring):[[source.mounts]] dir = "../bazel-bin/docs/api" mount_at = "_generated/api" attach_to = "index"
Mounted files are confined to their root — a path escaping it is reported as
build.mount_path_escape— and a mounted document that would take a host document’s name loses, with a diagnostic.sphinx-buildreads the same table via sphinx-mounts 0.2.0. See Mounts: extra source trees.[source] dir— the project’s source root (supersedes[project] srcdir)The directory your documents are discovered from now lives in
[source], under the same key name a[[source.mounts]]entry uses for its own root. Setting it also anchors every discovery glob at each source root, so one written pattern means the same thing under your own tree and under every mounted one — and it may point outside the configuration folder (dir = "../sources"), whichsrcdircould spell but never made work. sphinx-mounts 0.2.0 reads the same key when anchoring variant rules. See dir, and the deprecation under Breaking Changes.Variant-gated source selection:
[[source.variant_sources]]Decide which files are part of the build, per build variant. Each rule pairs an
ifcondition with a set offilesglobs; every rule whose condition is false removes its files, and a file no false rule matches is unaffected. Rules only ever narrow, and their order does not matter. A removed file is never read — no page, no document name, no needs, and nothing in it reaches search,objects.inv, cross-references orneeds.json. Atoctreeentry naming a removed document reports the informationaltoctree.variant_excluded— naming the condition — instead of a missing-document warning, so a 150% index page stays green underubc check.sphinx-buildnarrows the same document set via sphinx-mounts 0.2.0 (0.1.x does not read the key), held to the same condition grammar by a shared, machine-checked conformance corpus. See 5. Selecting whole files with variant_sources.ifon a[[source.mounts]]entry — gate a whole bundle per variantWhere a
variant_sourcesrule narrows a file set by glob,ifon a mount removes the whole bundle: it contributes no documents, wires nothing into itsattach_tohost, and its pages leave every index. It needs no globs, so it can gate a tree outside the project, and it is the only way to gate afilesmount. Both gating keys share one condition grammar and one validator, so a condition string means the same thing wherever it is written. Reported once per gated mount asconfig.mount_gated(informational).sphinx-buildhonours the same key via sphinx-mounts 0.2.0 or newer. Neither tool can see the other’s version while it builds, so these release notes are the whole mechanism: an older sphinx-mounts does not gate the bundle — it reports an unknown key and publishes the mount anyway. Upgrade both together. See Gating a whole bundle with a mount if.std.file_outside_roots— a discovered file that no root claimsA source file that matches your patterns but lies outside the source root and outside every mount root is not discovered at all — no page, no document name, nothing a
:doc:ortoctreeentry can resolve. This warning is the only thing that will tell you it is there, and it names the ways to act: move it under a root, add it to[source] extend_exclude, or.. include::it from a document (a fragment some document already pulls in is exempt). It cannot arise in the default layout, where the configuration folder is the root. See Linting.
✨ Needs¶
list2needcreates its needsThe directive was recognised and then ignored: it rendered a placeholder, the needs it described did not exist, and every
:need:role pointing at one raised a falseneeds.link_ref. Each bullet is now a real need — filters,needtable,needs.json, schema validation,needextend, hover, goto-definition and find-references all work on it.:presentation:is applied:nested(the default) draws a child item’s card inside its parent’s,standalonekeeps every card a sibling; an item’s content is parsed as markup in the host document’s own coordinates. Sphinx-Needs 8.4.0 rebuilt the directive the same way upstream and gained Markdown support and true line numbers with it; what stays ubCode’s own is the editor integration — goto-definition lands on the(ID)token the author wrote. Auto-generated ids are byte-exact against Sphinx-Needs 8.3.1, which is one release behind 8.4.0’s id change for bracket-carrying items. Twelve inputs that abort a Sphinx build areneeds.list2need_*warnings naming the offending line or option; the item is skipped — the block, for a missing:types:or an empty body — and everything else still builds.A need written inside another need records it as its parent
parent_needsnow carries the id of the nearest enclosing need — the way a Sphinx build always has — andparent_needis that list’s first entry. It is derived for an authored nested directive and for alist2needitem under:presentation: nestedalike. Backlinks, the*_backfilter surface,needtablecolumns,needs.jsonand the card’s meta rows all pick it up with no new configuration. A:delete:d need never enters the chain, and intermediate containers — an admonition, a list item, anifbody, an.. include::splice — are transparent. See the derivation rule under Breaking Changes.A need records the sections it is written under:
sectionsandsection_namesectionscarries the enclosing section titles, innermost first;section_nameis the innermost one. Both mirror Sphinx-Needs, and both are filterable and projectable everywhere: the python:filter:dialect, a:cypher:query (and so the MCPquery_cyphertool),needtable:columns:and:sort:,ubc query filter, andneeds.json. Aneedtablenamingsection_namein:columns:used to render an empty column beside a “not a known column” warning; those columns now render and the warnings are gone. See The section-location node properties.Warning
Both fields are core fields, so the first
ubc diffacross the upgrade reports them as added on every need under a heading — and renaming a heading now reports a change on every need under it, which then enters impact propagation. Exclude them per run withubc diff --deny-core sections --deny-core section_name.A project-wide default card design:
[needs] default_layoutEvery need with no
:layout:of its own is drawn with the named design instead of the hardcodedclean. This is Sphinx-Needs’ canonical TOML spelling ofneeds_default_layout, so one sharedubproject.tomlnow means the same thing tosphinx-buildand to ubCode; previously ubCode did not read the key and the two engines disagreed in silence. Precedence, top wins: an authored:layout:, a[needs.fields.layout]predicatesrow, that field’sdefault, thendefault_layout, thenclean. An unknown name is reported once at configuration time. See default_layout.needflowspeaks a portable presentation vocabulary:direction:(down/up/right/left, plus theTB/TD/BT/LR/RLaliases) means the same thing on both diagram engines;:show_link_names:takes a value (none/outgoing/incoming/type); and:show_legend:can name a[needs.flow_legends]configuration — an ordered legend of the types and link types you choose, placed where you say. Four new[needs]keys carry the project-wide defaults:flow_direction,flow_show_links,flow_show_legendandflow_legends. The same options shipped in Sphinx-Needs 8.4.0, so one source means one diagram under both tools. A legacy:config:naming a direction that disagrees with:direction:is reported asneeds.option_conflict(the option wins). See Needs.A
needtable:cypher:query can name the table’s columnsA query returning more than the matched node is a projection: the
RETURNclause’s columns become the table’s columns,ASnames the headers, and each row still links to its need.collect(o)orcollect(o.id)in the finalRETURNrenders as a cell of linked chips, so one row can carry a whole neighbourhood. A single-nodeRETURNstays a selection and takes its columns from:columns:, as before. See Projections in a needtable.A Cypher query naming absent vocabulary is diagnosed
A
:cypher:filter that names a field, label or link type the project does not define —n.staus,:SPEC,[:implments]— now reportsneeds.cypher_vocabulary, with a did-you-mean, instead of silently matching nothing. It is a warning: on the defaultdeny = "warning"gate a project carrying such a typo starts failing where it previously exited 0 — silence it withignore = ["needs.cypher_vocabulary"]under[lint]while you fix the query. A known field merely unset on some needs is never reported.
✨ Markup & rendering¶
The
indexdirective and role render like Sphinx — and their entries are searchable.. index::(and MyST{index}) no longer prints a placeholder box into the page — it renders only its anchor, exactly as a Sphinx build does, and the:index:role renders its display text as ordinary prose. The editor’s Rendered preview keeps a small chip listing the entries while you author. Every entry is parsed the way Sphinx parses it —single:/pair:/double:/triple:/see:/seealso:, the leading!main flag and the comma shorthand — and filed against its anchor: a word you put in.. index::and nowhere else in your prose is findable in the search of a site built withubc build html(alpha), and answers thesearch_referencesMCP tool as anindex:entryhit. A malformed entry is reported (index.invalid_entry,index.legacy_prefix) instead of accepted in silence. There is still nogenindex.htmlpage.The
:download:role:download:`assets/report.pdf`publishes that file alongside your site and links it as a download, in the editor preview and inubc build html(alpha) alike. The published file keeps its own name, one file linked from many pages is copied once, and a target containing://is linked as an external URL and never copied. A target that names no readable file keeps its text and loses its link, as a Sphinx build does — reported while you type (download.not_readable) rather than only when the site is built. In the preview, clicking a download link opens the file with your operating system’s application for it. A:download:inside imported need content is refused with a diagnostic and never published. See Downloads.Markdown link destinations are cross-references
A Markdown link destination no longer has to be a web address:
[](#target-name)reaches a target or label,[](other.md#section)reaches another document and a place inside it,[](notes.txt)publishes a file for the reader, and[](inv:#target)searches another project’s intersphinx inventory. A link you gave no text takes the target’s title, and a destination that resolves to nothing keeps its text and is reported — rather than being silently deleted from the page as a Sphinx build does. Three new[parse.parsers.md]keys tune it:url_schemesdeclares custom schemes (optionally rewriting them from a template),links_external_new_tabopens external links in a new tab, andall_links_externalturns destination lookup off entirely — the escape hatch if the new resolution reports links you meant literally. See Cross-referencing with links and Markdown link cross-references.Line numbers and emphasised lines in code blocks
code-block,sourcecode,codeandliteralincludenow apply:linenos:,:lineno-start:,:number-lines:and:emphasize-lines:, andliteralincludeapplies:lineno-match:— in the editor preview and the built site. The numbers are drawn beside the code, so they can never be copied with it... highlight::’s:linenothreshold:numbers every long-enough literal block below it — including a plain::block and a Markdown fence — exactly as in a Sphinx build. A malformed option is reported instead of ignored, and the block still renders in every case, unlike a Sphinx build, which drops it.CSS classes: the
rst-classandcssclassdirectivesBoth attach CSS classes to your content rather than rendering a placeholder box. With a body, the body’s blocks carry the classes; without one, the class attaches to the block that follows — and, when the directive is the last thing in a section, to the next section, matching a Sphinx build exactly. An element that cannot carry a class is reported (
directive.class_unsupported_target) rather than silently dropping it. Bare.. class::is no longer an unknown directive: it is the Python domain’s object directive under Sphinx’s defaultprimary_domain— userst-classto attach a class. See CSS classes.The inline
rawrole, and fifteen more core constructs.. role:: raw-html(raw)with:format: htmldefines a working pass-through role;raw_enabled = falseunder[parse]now switches the role off too, with its own code, so the directive’s warning and the role’s can be silenced independently... acks::renders its bullet list; the:manpage:,:mailheader:and:newsgroup:roles render;sectionauthor,moduleauthor,codeauthor,header,footerandmetaare accepted and render nothing — which is what a default-configured Sphinx HTML build produces — instead of printing a placeholder box of your authoring metadata into the reader’s prose. Sphinx 9’s hyphenatedversion-added/version-changed/version-deprecated/version-removedmarkers work, anddescribe,objectandsection-numberingare recognised. See Raw content.
✨ The HTML build (alpha)¶
Page redirects:
[build.html.redirects]Maps the path of a page that moved to the page it became, and the build writes a small forwarding page where the old one used to be — keeping any query string and fragment the reader came with. A target may name a section of its page or an external URL; chains resolve to their end; removing an entry removes its forwarding page on the next build. A faulty entry is reported and skipped, leaving the rest of the table building. This is the
sphinxext-rediraffeport. See redirects.Page
<head>meta tags.. meta::(reStructuredText and MyST alike) and MySThtml_meta:front matter now write<meta>tags into the built page’s<head>, where a Sphinx build puts them —descriptionandkeywordsfinally reach the built HTML, and hand-writtenproperty=og:…tags work today. The directive’s body is validated: a malformed body is reported (meta.invalid_body,meta.invalid_attribute) and the fields that could be read are kept, where docutils drops the whole directive. A page that writes none is byte-identical to before. See Page <head> meta tags.A page can override its browser tab title
.. title:: Some Other Title(and the MyST{title}fence) now sets what the browser tab and the search results show — without renaming the page anywhere else. The heading stays the heading: the sidebar entry, the previous/next labels, the parent’s table-of-contents caption and theobjects.invexport all keep showing yourH1, which is Sphinx’s own two-channel split. The directive also stops printing a placeholder box into the page, in the editor preview as well as the built site. See A page can override its browser title.
✨ Querying¶
ubc query search— find what your project can cross-referenceA new
ubc querysubcommand that searches everything the project can cross-reference — its own:ref:labels, glossary terms and documents, its filed index entries, and every entry of every loaded intersphinx inventory — and returns ranked hits, each carrying the exact role to paste where one exists. Filter with--kind(label/term/doc/index, or adomain:objtypeglob) and--scope(local/external/ one intersphinx project), choose--format tableorjson(the same envelope thesearch_referencesMCP tool answers with), and script it on the exit code. Passing no query lists every reference by name, soubc query search --kind labelis “list this project’s labels”. See Finding what you can reference.
👌 Improvements¶
A
:cypher:may be paired with the python filter surfaces. A view directive carrying both no longer raisesneeds.filter_conflict— which, on the default gate, failed the build for exactly the portable idiom the docs recommend: ubCode evaluates the Cypher, a Sphinx build evaluates the python surfaces, and Sphinx-Needs 8.4.0 accepts and ignores:cypher:for exactly this purpose. The code is retired; a stale[lint] ignoreentry for it is inert.A need card labels its link rows with the link type’s configured titles. A row now reads the
outgoingtitle of its link type, and an incoming “links back” row reads theincomingtitle, instead of the raw type name followed by(incoming)— so the builtinparent_needsrows readparent needsandchild needs, the same words a Sphinx build writes.A field or link named
contentis no longer declared in silence.contentis a need’s own body and no declaration can shadow it, so filters, tables, variant conditions, report templates andneeds.jsonall answer the body for that name and the declared value is invisible to them. The declaration is kept — nothing that renders today stops rendering. It is now reported asconfig.field_shadowed_by_identityon all four declaration surfaces; silence it withignore = ["config.field_shadowed_by_identity"]under[lint]while you migrate.Report templates see the need’s context fields.
.html.j2templates now exposecontent,docname,lineno,sections,section_nameand the origin flags alongside the need’s declared fields, through the same projection every other need surface reads.Variant data is resolved while the configuration is loaded, so it is available to source discovery; the merged value and its diagnostics are unchanged. Sphinx-Needs 8.4.0 makes the same move upstream.
[source] extend_excludeandextend_includeaccumulate across anextendchain instead of a child’s list replacing its parent’s, matching ruff. See Sharing configuration.A missing source root is reported, not fatal. A root that is absent or not a directory is reported as
config.source_dir_missingand the project’s existing pages are retained rather than reading as wholesale deletions;ubc build list-documentsprints configuration diagnostics to stderr before the list, so an empty list arrives with its reason.An in-project
:ref:`search`links the built search page when the build writes one, matching a Sphinx build — and a project defining its ownsearchlabel wins, for the reference and theobjects.invrow alike. A no-title:index:role that looks like a typed entry gets the informationalindex.role_type_prefixadvisory, naming the rewrite.Navigation titles keep their inline markup. A document title carrying
code, bold or a:ref:reached the sidebar, the prev/next footer and an in-page.. toctree::as plain text — a:ref:simply vanished. All four surfaces now render the title’s markup. The sidebar logo renders centred at its intrinsic width, as furo’s does.The
search_referencesMCP tool answers an emptyquerywith a name-ordered listing of everything the project can cross-reference, instead of an empty result.
‼️ Breaking Changes¶
Discovery follows the declared roots
Discovery walked the whole configuration folder while document names were only derived for the source root and the mount roots. A file in that gap was walked, parsed and indexed — and then had no document name at all: no page, no toctree slot, and its needs sat in the index as unbuildable ghosts. The walk now covers exactly the declared roots, so a file outside every root is no longer discovered: it leaves
needs.jsonandubc querywith the page it never had.std.file_outside_roots(above) is what reports it. A project that sets neither[source] dirnor[project] srcdiris unaffected — the configuration folder is the root.[project] srcdiris deprecatedIt keeps working, unchanged and frozen: with
[source] dirunset, every code path behaves exactly as it did. Setting both is the legitimate mixed-version bridge —dirwins, andconfig.source_dir_supersededsays which key is ignored. A project onsrcdiralone getsconfig.srcdir_deprecated, which lists every discovery glob that migrating would require rewriting, with the rewritten spelling where it can be computed. Both codes can be silenced individually via [lint] ignore.New warnings may appear on an unchanged project
This release adds around sixty-five new diagnostic codes — about fifty-five of them warnings — and on the default gate (
deny = "warning") a warning failsubc checkandubc build html. The likeliest to fire on a project that was clean on 0.32.0:config.srcdir_deprecated(every project still on[project] srcdir),std.file_outside_roots,config.field_shadowed_by_identity,needs.cypher_vocabulary, the newly resolved Markdown link destinations (std.doc/std.refon links that used to pass through as text —all_links_external = truerestores that), and the newly applieddirective.code_block_*/directive.class_*/index.*/meta.*/role.raw_*families on previously-silent authoring mistakes. Silence any of them individually or by family under[lint]:ignore = ["directive.class_*"].parent_needsis derived, not authoredWriting
:parent_needs:on a need directive — or((parent_needs="…"))on alist2needitem — is now reported asneeds.option_derivedand the value discarded. (A Sphinx build accepts the option and discards the value with no diagnostic at all.) A project that declared[needs.links.parent_needs]itself keeps the option but loses authored values: they are derived from nesting instead. Declare[needs.fields.parent_needs]to keep an ordinary, authorable field of that name.Variant conditions and
[[copy()]]over need context fields now resolveA variant condition or
[[copy(...)]]namingsection_name,docname,content,lineno,sectionsor an origin flag previously resolved to nothing and silently took its fallback arm; it now resolves, as it does in Sphinx-Needs — resolved field values inneeds.jsonchange for such projects with no source edit. A condition written with then.prefix likewise starts matching. An unknown name is no longer silent: it is reported underneeds.variantwith a suggestion. A project that declares its own field namedcontentmoves in the opposite direction: the identity field cannot be shadowed, so these surfaces now read the need’s body where they previously read the declared field.needflow‘s:show_link_names:wins over the configurationThe option and
[needs] flow_show_linksused to be OR-ed, so configuration could only ever turn labels on; only an unset option consults the configuration now. This is what makes:show_link_names: noneable to unlabel one diagram in a labels-on project. The same change shipped in Sphinx-Needs 8.4.0.A
needtable:cypher:query’s order is honouredThe default
id_completesort was applied unconditionally, so a:cypher:carrying anORDER BYrendered in id order. With a:cypher:present and no:sort:, the query’s returned order is now the row order — which also means a query with noORDER BYrenders in returned order (case-sensitive, or match order for a traversing query) where the old sort was case-insensitive id order. Add:sort: id_completeto keep the previous order.A built site’s
objects.invno longer advertisesgenindex,modindexorpy-modindexThose labels named pages
ubc build html(alpha) does not build, so an intersphinx consumer that resolved one followed a link to a file that was never written — silently. The inventory now promises only the page set the build produces (thesearchlabel stays), and a downstream project referencing them gets an actionable unresolved-reference warning instead of a dead link. Referring to those names from inside your own project is unaffected.One-time full re-index and re-render on upgrade
The cache format moved (6.116 → 6.145) and the build manifest with it (73 → 84). The first run after upgrading rebuilds the index and re-renders every page once — about the cost of a cold build — then incremental behaviour resumes. Self-healing, nothing to delete by hand. Note that each build variant keeps its own cache directory, so per-variant CI pays a cold index per variant.
🐛 Fixes¶
An unresolvable
predicatesname no longer deletes the need. One bad name in one[needs.fields.*] predicatesexpression removed every need the predicate touched from the index, leaving a single warning and exit code 0 — so filters, tables, schema validation andneeds.jsonall saw an empty project. It is now reported (with a did-you-mean), the predicate is treated as non-matching, and the field takes its plaindefault. A project affected by this will see needs — and every dead link and schema violation those needs generate — that the previous build was hiding. (Sphinx-Needs 8.4.0 ships the same fix upstream.)The shorthand
:cypher:form is vocabulary-checked. A:cypher:written as a bare expression (n.staus = 'open') produced no vocabulary notes, because only the full-query form was ever parsed. Both accepted forms are checked now — and thequery_cypherMCP tool keeps its did-you-mean notes for a comment-prefixed query, which were previously dropped.A
[[source.mounts]]dirmount’s ownincludeis now honoured; it was computed and then never read, so a mount could neither narrow nor widen its own file selection.A mounted file selected by a path-prefixed parser
includenow routes to the same parser in the editor as in the index, instead of falling back to the editor’s language id.[[source.variant_sources]]rules whose globs name a path now reach mounted trees, not only rules whose globs name a bare file name.objects.invno longer emits twosearchrows for a project that defines its ownsearchlabel, and diagnostics raised about an:index:role now point at the role instead of at line 1 of the file.