Differences from a Sphinx build

ubCode has two rendering surfaces of its own: the live preview in the editor, which re-renders your document on every keystroke, and ubc build html, which builds a static site from the whole project (in alpha). Neither is a full sphinx-build, and the two do not diverge from one in the same places: the preview deliberately skips the slower, whole-project transforms so that it can answer within a keystroke, while the build runs the project-wide index pass and so resolves much of what the preview only guesses at. This page is the honest register of the resulting user-visible differences, so you can trust what you see and know when to reach for a real build.

How to read this page

Each entry names the surfaces it applies to. An entry that names ubc build html says how the built site behaves — sometimes that it shares the difference, sometimes that it does not. One that speaks only of the preview has not been audited against the built site: expect the build to share the preview’s difference unless an entry says otherwise.

Rendering is the default scope. A few differences also reach indexing and diagnostics — what ubc check and the editor report — and those say so in their status line. Unless one does, the difference changes what you are shown and nothing else.

Preview entries describe the Rendered mode (in Structural mode every directive and role renders as a uniform placeholder; see Authoring).

The main entries follow the same shape — what you write, what the preview shows, what a Sphinx build does, and a short status note; smaller differences are grouped into bullet lists.

Group

What it covers

Rendering and transforms

Constructs still shown as placeholders, document topmatter and rst_prolog / rst_epilog, toctree navigation, substitutions, and the finer points of block and inline rendering.

Content inclusion

include and literalinclude, raw content and its trust controls, only / if conditional bodies, and two files claiming one document name.

Components and markup

sphinx-design components and tabs, tables, code blocks, images, diagrams, and directives or roles ubCode does not recognise.

Needs and filtering

Need cards and the view directives, external needs.json sources, the ubCode-only :cypher: option, and the filter expression forms ubCode does not accept.

Site output

Differences in what the built site contains rather than in how a page renders — today, page redirects.

Closing the gap

What is being closed, and when to run a real sphinx-build.

Rendering and transforms

Not yet rendered

These constructs are recognised and shown as self-describing placeholders, but the transform that gives them their final form does not run in the preview yet.

Math

You write a formula with the :math: role, a .. math:: block, or MyST dollar math ($x$ / $$x$$).

The preview shows the formula typeset (KaTeX), as does a built site — including multi-part blocks (blank-line-separated equations align, Sphinx-style), :no-wrap: raw environments, and :label: / :name: anchors with a ¶ permalink in built output. A formula KaTeX cannot parse stays as its raw source, with the error in the tooltip.

A Sphinx build additionally numbers labelled equations and resolves :eq: / :math:numref: cross-references.

Status — typeset now, numbering deferred: equation numbers ((1)), math_number_all, math_eqref_format and the :eq: role need the math-domain numbering machinery, which ubCode does not implement yet. Diagnostics are unaffected by that deferral. The deferral is widest for the MyST amsmath extension (bare \begin{align} … \end{align} environments): Sphinx numbers every environment written without a * and gives it an id plus a ¶ permalink, where ubCode numbers none of them and mints no anchor. That id is not stable output in a Sphinx build either — it is a fresh random UUID every time the page is parsed, so on every clean build and whenever the page is edited (an incremental rebuild that does not re-read the page reuses the cached one) — so nothing a reader could link to is lost by the deferral.

In the built site’s search index, raw LaTeX is excluded from the indexed terms — from page and section body text, and from the terms taken from a heading that contains math — so a formula’s control sequences (\frac, \partial) never become search words. The displayed strings still show the LaTeX source: a result whose title or breadcrumb contains math shows the formula source rather than nothing (Sphinx’s own search UI shows it too), and a title in the navigation sidebar, a rendered toctree entry (or a :doc: link) or the previous/next links shows that source rather than a typeset formula. Those surfaces do carry the rest of a title’s markup — an inline literal, **bold** and the like all render there — but math is deliberately left flat on them: the typesetter is loaded per page from the page’s own body, so a page that merely links to a formula-titled page never loads it, and half-rendered formulas would be worse than the source text. The On this page column beside the page (the drawer a ubc build html site draws from the page’s own headings) is not one of them: it keeps the heading’s markup, so its formulas are typeset like the body. A .. contents:: table of contents keeps it too.

Raw HTML in a heading

You write raw HTML inside a section title or heading — MyST’s inline <b>bold</b>, or reStructuredText’s :raw-html:`…` role.

A Sphinx build puts that markup into the page and into the navigation sidebar of every page in the project, live. A heading containing a <script> therefore runs on every page of the site, and an unclosed tag leaks into the rest of the navigation.

ubCode renders it in the page exactly as Sphinx does, but shows it as its own source text in the sidebar, the previous/next links and a rendered toctree — the surfaces a heading reaches from every other page. Styling markup such as <b> therefore reads as <b> in those places rather than emboldening the entry. Deliberate: those surfaces are the site’s chrome, and one heading should not be able to restyle — or run code in — every page that merely links to it.

Needs flow diagram (needflow)

You write a .. needflow:: traceability graph.

A Sphinx build draws it from the resolved need set via the configured Graphviz or PlantUML engine (an SVG/PNG image).

The preview and ubc build html draw the same graph as a Mermaid flowchart, rendered client-side by the bundled Mermaid runtime (the list, table, sequence and chart views render their real output too — the charts as inline SVG, with no client-side runtime at all; see below).

Status — implemented, with these deliberate differences from Sphinx-Needs (the output is self-consistent, not byte-identical to either upstream engine):

  • Portable presentation options — three options state an intent rather than one engine’s syntax, so the same document reads the same whatever draws the graph. All three behave identically in Sphinx-Needs; the two rows below them are the only places the two tools deliberately differ.

    • :direction: — where the graph grows: down (the default), up, right or left. The engine tokens TB/TD, BT, LR and RL are accepted as aliases. Mermaid draws all four natively, so nothing degrades. It supersedes the direction a :config: sets, which keeps working as the lower-precedence source; when a :direction: and the direction a :config: sets disagree, the :direction: option wins and a needs.option_conflict warning says so. Agreement is silent.

    • :show_link_names: — the existing flag, widened to take an optional value: none, outgoing, incoming or type. Written bare it still means outgoing, exactly as before, so no existing document changes. The value is what lets one diagram disagree with a project default: a flag could only ever turn labels on, so needs.flow_show_links = "outgoing" could not be switched off for a single diagram — :show_link_names: none now does that.

    • :show_legend: — the existing flag, widened to take an optional key: the name of a [needs.flow_legends] entry, which says what the legend lists (parts — an ordered list of types / links sections, where ["links", "types"] really does draw the link table first) and where it would like to be drawn (placement). Written bare it takes the project’s needs.flow_show_legend, and then the engine default. The value is a key and never an inline setting, deliberately: a project may name a legend types, and an option accepting both would need a rule to say which one :show_legend: types meant.

      Presence stays per-directive. needs.flow_show_legend selects which legend, never whether there is one, so no project setting can add a legend to a diagram that did not ask for one. An unknown key is reported against the directive and the default legend is drawn anyway, so a typo costs the styling and not the legend.

    Each has a project-wide default under [needs]: flow_direction, flow_show_links and flow_show_legend. A malformed value raises needs.option_invalid and is treated as unset, so the lower-precedence source (or the project default) still decides.

    Note

    :direction: is not yet accepted by Sphinx-Needs. A document using it builds under ubCode but a Sphinx build reports an unknown option and drops the whole directive, until the matching Sphinx-Needs release lands (acceptance is in flight, following the route ubCode’s earlier view options took into Sphinx-Needs 8.4.0: :cypher:, :width: and :height: are parsed there and ignored, while :max_items: went further and is genuinely implemented). Until then, keep a portable document on :config:.

    :show_legend: and :show_link_names: are the exceptions, and deliberately so: they are the options both projects already have, so a bare one of either is portable today. Sphinx-Needs is widening both in step — the same four label values under the same “bare means outgoing” rule, and the same legend keys — but until that release lands, either option with a value is refused by Sphinx the way :direction: is.

  • The legend is a table beside the diagram, not a block inside it. A bare :show_legend: draws Sphinx-Needs’ in-diagram legend there and ubCode’s external table here.

    Mermaid’s flowchart syntax has no legend construct at all, so there is nowhere inside the picture to put one; the external table carries the identical rows and differs only in where it sits.

  • placement = "internal" is honoured by Sphinx-Needs and silently drawn external by ubCode. No warning: the value is correct for the configuration it is shared with, and there would be nothing for an author to do about it.

    placement unset means the engine’s default placement, which is internal for Sphinx-Needs and external here — one axis each engine answers for itself, the same shape as the engine default for a legend nobody named. So a shared ubproject.toml that sets only parts lands differently in the two tools by design, and neither is overriding the other.

  • Engine — ubCode draws Mermaid by default, and can draw needflow with a real PlantUML instead. A project that configures a PlantUML executable and sets [build.plantuml] needflow = true has every needflow in ubc build html and ubc build pdf laid out by PlantUML itself, with each node a link to its need — see External PlantUML renderer. :engine: (graphviz/plantuml) overrides that per diagram, in either direction: plantuml draws this diagram with the executable even when the key is off, and graphviz keeps this diagram on ubCode’s own engine even when the key is on (ubCode has no Graphviz pathway, so graphviz means “not PlantUML” here). With no executable configured both values are accepted and ignored, so a source file shared with a Sphinx build raises nothing. [needs] flow_engine is read as the low-precedence default for the key, so one ubproject.toml need not say the same thing twice. The editor preview always converts to Mermaid, whatever the project configured, so a built page and the preview show different renderings of the same diagram — the same trade needuml already makes. It is a fidelity choice rather than a functional one, and four intents cannot be carried over exactly: :direction: up and :direction: left are drawn on their axis mate (down and right) with a needs.flow_direction_degraded warning, because PlantUML lays a diagram out on one axis per statement; a need type whose style is not one of PlantUML’s own node keywords is drawn as a rectangle (Sphinx-Needs passes the value through and loses the whole diagram to a syntax error); a link type’s style_start / style_end are accepted and ignored, as they are on the Mermaid arm — an edge is always drawn -->-headed; and a style word PlantUML does not know is dropped rather than passed through, so the edge draws as a plain line instead of costing the diagram. The legend stays a table beside the diagram, and parts and child needs stay un-nested, on both arms. A % in a need title, in a link type’s name or in the output of [needs] diagram_template is drawn as the literal % the author wrote. PlantUML runs a preprocessor before it parses a diagram, and that preprocessor expands %fn(…) built-ins — %version(), %filename(), %date("yyyy") and the rest — inside a quoted label; several other forms refuse the whole diagram outright. Sphinx-Needs hands the label straight to PlantUML, so a template that deliberately writes %version() expands there and draws literally here. The one place a built-in is honoured is a [needs.flow_configs] block, whose content is spliced verbatim exactly as Sphinx-Needs splices it — configuration is yours to write, a need’s title is data. An & is escaped for the same reason: PlantUML decodes numeric character references in a label, so an unescaped &#10; in a title would break the label up.

    A diagram the executable could not draw falls back to the Mermaid flowchart with a needs.flow_external_render_failed warning, so a failure is visible without being fatal. :debug: on such a diagram shows two blocks: the PlantUML that was attempted, labelled with the failure, and then the Mermaid that was drawn in its place — the warning’s line number refers to the first of them. A needflow reached through a needextract or needimport card is always the Mermaid flowchart, whatever the project configured: imported content is re-rendered as a self-contained document with no host page to resolve against, and the PlantUML pathway draws only the diagrams the index captured on a real page. So a diagram written inside a need’s body can appear drawn where the need itself is rendered and as Mermaid inside a card that extracts it — the same source, two renderings, on one page. :config: names entries of needs.flow_configs. Two of the names need no entry at all: lefttoright and toptobottom are read as the layout direction itself, on both arms, and neither warns when the project defines no block under them. Sphinx-Needs ships those two as needs_flow_configs entries; ubCode ships no entries at all, so the two spellings are built in instead. A project that does define a block under either name still gets its content inserted, as for any other name. Every other name is looked up, and a name with no entry is reported. The direction a :config: sets is the low-precedence source for :direction: above, and it is read from the entry’s CONTENT — a block containing left to right direction or top to bottom direction sets that direction, which is how Sphinx-Needs reads it too, so the common idiom of one project-wide style block carrying a direction statement gives the same axis in both tools. A block setting both statements resolves to top-to-bottom, again as in Sphinx-Needs, and so does a :config: naming two blocks that disagree — the search is over the blocks’ joined content, so the order they are named in does not change the answer. A block defined under one of the two direction names is read like any other block: its content decides, and the name does not override it. One case still differs deliberately. A direction statement that is commented out (' left to right direction) is inert to ubCode, which draws what it derived; Sphinx-Needs finds the text inside the comment, treats the direction as already set and suppresses its own statement, so it draws the project default instead. ubCode is the self-consistent one there, and that is the reason it emits its direction statement unconditionally. On the Mermaid arm the entry content is otherwise ignored: only the direction it sets is read. On the PlantUML arm each named entry’s content is inserted at the top of the generated diagram, as Sphinx-Needs does, and the resolved :direction: is written after it, so an explicit direction still wins. :alt: is accepted and ignored (it applies to a raster image, not an SVG). :scale: is accepted and warns, pointing at :width: / :height: below: it scales a raster image, which is not what ubCode draws.

  • Size — :width: and :height: size the rendered diagram, in the preview and in ubc build html alike. They take a CSS length or percentage (a bare number is pixels), and any CSS unit is accepted — 60vh and 30rem are ordinary here, because the diagram is a CSS-sized box rather than an image. A value that is not a size at all raises a warning and the diagram renders without that dimension. Sphinx-Needs has no size option on any of its diagram directives. Setting :height: also lifts the default height bound described under Diagrams below.

  • Link types — with no :link_types: option the diagram draws edges of every configured link type (except parent_needs, which is never drawn), matching Sphinx-Needs’ effective behaviour: its needflow directive defaults :link_types: to all link fields, so the needs_flow_link_types config (default ["links"]) is inert for needflow in Sphinx-Needs 8.4.0 and 8.5.0 — ubCode intentionally has no equivalent key.

  • Node cap — a default cap of 100 drawn nodes (with a shown-vs-total notice) keeps a large graph readable and within Mermaid’s edge limits; :max_items: 0 draws them all. The default is configurable project-wide with needs.views_max_items, and a build that caps a diagram also emits a build.need_view_truncated warning. Sphinx-Needs does not implement this option.

  • Flat, need-level graph — a need’s parts are not drawn as their own nodes: they are not matched as separate items (the project-wide convention the :need_count: role also follows), so an edge to a part collapses to that part’s base need. The parent/child hierarchy is likewise not drawn as nesting (Sphinx-Needs clusters child needs inside their parent).

  • Click-through — on the Mermaid arm nodes are not yet click-through links (the Mermaid runtime is initialised in its strict security mode, which strips link bindings); use the surrounding need cards / references to navigate. A diagram drawn by a PlantUML executable does carry them: each node is a real link to its need’s card, resolved the same way a :need: reference in prose is.

  • Caption — a :caption: renders as a caption paragraph rather than a docutils figure, and its inline markup is not parsed (as for diagram captions above).

  • No automatic anchor; use :name:. A diagram with a :name: carries a stable id you can :ref: in every body state, and a in its caption — but the caption is written only when the diagram is DRAWN, so a named view that is empty or failed keeps its id and shows neither. An unnamed one carries none of it, and ubCode never mints the positional id a Sphinx build does. See Anchors on views and diagrams for the whole rule, including the :name: values that normalise to nothing.

  • ``:root_depth:`` — the :root_id: walk bounds by shortest-path distance from the root (a breadth-first walk), rather than Sphinx-Needs’ order-dependent last-recorded distance; the two agree except at the exact depth boundary of a diamond-shaped graph.

  • ``:border_color:`` — applied as a literal colour (a bare 3- or 6-digit hex such as FF0000 is accepted, as Sphinx-Needs does; a per-need variant expression is not resolved yet).

Needs sequence diagram (needsequence)

You write a .. needsequence:: message flow, starting from one or more :start: needs.

A Sphinx build draws it via PlantUML (an SVG/PNG image); Sphinx-Needs offers no other engine for this directive.

The preview and ubc build html draw the same walk as a Mermaid sequenceDiagram, rendered client-side by the bundled Mermaid runtime — unless the project configured a PlantUML executable and set [build.plantuml] needsequence = true, in which case ubc build html and ubc build pdf have that draw it, from the same walk. See External PlantUML renderer. The key is separate from needflow’s, and [needs] flow_engine does not default it: Sphinx-Needs’ needsequence is PlantUML-only and has no engine option, so there is nothing for that key — or for a :engine: on the directive, which does not exist — to choose between. The editor preview always uses the Mermaid conversion, whatever the project configured. A diagram the executable could not draw falls back to the Mermaid one with a needs.sequence_external_render_failed warning.

The walk itself matches Sphinx-Needs: from each :start: need, the needs it links to are the messages, and each message need’s own links name the receivers. So a message need is never drawn as a lifeline — its title becomes the arrow label, which is what lets the diagram show a mediated relationship (specification → programme objective, labelled with the requirement that connects them).

Status — implemented, with these deliberate differences from Sphinx-Needs (the output is self-consistent, not byte-identical to the PlantUML engine):

  • Link types — with no :link_types: option the walk follows links only, matching Sphinx-Needs, whose needsequence hard-codes that default. Note this is not the needflow rule, which defaults to every configured link type. parent_needs may be walked here, where needflow never draws it.

  • One walk across all starts — several :start: ids share a single set of already-visited needs, so each lifeline is declared once and no subtree is walked twice; arrows between the starts’ subtrees are still drawn. Sphinx-Needs restarts its bookkeeping per :start: id, which redraws a shared subtree and declares its participants again.

  • Every drawn need is declared — including a need that only receives messages, which is shown with its title. Sphinx-Needs declares only senders and lets PlantUML auto-create the rest, so a receive-only lifeline appears under its raw ID while its neighbours show titles. A need whose messages exist but whose receivers are all filtered away or unresolvable keeps its lifeline, with no arrow attached — Sphinx-Needs does the same, and it is how the diagram says “this need was reached, and its messages went nowhere”. Truncation is the exception: the :max_items: cap never adds a lifeline the surviving messages do not touch.

  • Nothing to draw shows the empty-result paragraph — a :start: need with no links, an unknown :start: id, or a :filter: that admits no receiver each render the “no needs” wording (or your :filter_warning:) rather than a diagram. Sphinx-Needs renders an empty diagram frame for the first of those.

  • Errors are diagnostics, never a failed build — all three of the conditions below are fatal to a Sphinx build (a missing or blank :start: and an unknown :start: id raise NeedSequenceException; an unknown :link_types: name warns and then dies on a KeyError). None of them stops a ubCode build, but what you are told differs:

    • a missing or blank :start: warns (needs.option_invalid) and the directive renders the empty-result paragraph;

    • an unknown ``:link_types:`` name warns, with a did-you-mean over the project’s configured link types, and that type is dropped from the walk;

    • an unknown ``:start:`` entry is dropped SILENTLY — no diagnostic anywhere. If every entry drops, all you see is the empty-result paragraph. This is the one place the directive fails without telling you, so a mistyped :start: looks exactly like a filter that matched nothing; check the ID against the project when a sequence comes out empty.

  • Titles and IDs are escaped — a title containing #, ; or a Mermaid init directive, and an ID that collides with another after sanitisation or matches a Mermaid keyword, are all made safe. Sphinx-Needs escapes nothing, so such a title or ID produces broken PlantUML. The PlantUML arm escapes them too, for its own grammar: a real newline or a " in a title is a whole-diagram failure there, and two IDs that fold alike would otherwise overwrite one lifeline’s label in silence. A Mermaid init directive needs no counterpart guard on that arm — the form is inert to PlantUML — but a % does: PlantUML expands %version() and its other built-in calls inside a quoted title, so a % anywhere in a title or a message label is escaped on that arm as well, and renders as the % the author wrote. Without that, a title mentioning one of those names either lost the whole diagram or published text nobody wrote.

  • Legend — :show_legend: lists the types of the needs actually drawn, matching needflow. Sphinx-Needs lists every configured need type, including the service types it adds itself.

  • ``:show_filters:`` — echoes only :filter:, the one selection surface this directive actually honours. Sphinx-Needs also prints status, tags and types, which its own selection never consulted.

  • Accepted and ignored — :status:, :tags:, :types:, :sort_by:, :highlight: and :show_link_names: have no effect in Sphinx-Needs’ needsequence either (it never reads them); ubCode warns so the mismatch is visible rather than silent. :config: carries PlantUML configuration, which cannot apply to a Mermaid sequence diagram, and also warns — and it keeps warning even when the diagram is drawn by a PlantUML executable, because ubCode does not record which blocks the option named: a needsequence’s :config: is diagnosed and dropped rather than stored, where a needflow’s is stored. (A needflow drawn by PlantUML therefore splices its :config: blocks; a needsequence does not.) :scale: warns too, pointing at :width: / :height: below: it scales a raster image, which is not what ubCode draws.

  • Size — :width: and :height: size the rendered diagram, exactly as for needflow above (a CSS length or percentage, any CSS unit, a bare number is pixels), and :height: lifts the default height bound described under Diagrams below. Sphinx-Needs has no size option here either.

  • Message cap — a default cap of 100 drawn messages (with a shown-vs-total notice) keeps a deep walk readable; :max_items: 0 draws them all. The default is configurable project-wide with needs.views_max_items, and a build that caps a diagram also emits a build.need_view_truncated warning. Sphinx-Needs does not implement this option.

  • Caption — the directive argument renders as a caption paragraph rather than a docutils figure, and its inline markup is not parsed (as for needflow above).

  • No click-through — lifelines are not click-through links, for the same reason as needflow.

Needs chart views (needpie / needbar)

You write a .. needpie:: or .. needbar:: chart — one filter per content line for a pie, a separator-split grid of filters for a bar, each entry counted over the needs it matches (a bare non-negative integer is taken as a literal value instead, as upstream does).

A Sphinx build draws the chart with Matplotlib and writes it into the output as an image file referenced by an <img>.

The preview and ubc build html draw the same chart as an inline SVG written straight into the page, followed by a legend table carrying every number the picture shows. That table is always written, but it is shown only with :legend:: without the flag it is visually hidden — still read by assistive technology, still selectable and still indexed for search, simply not drawn. There is no image file, no client-side runtime and no extra request: the chart renders with JavaScript disabled and in a printed page.

Status — implemented, with these deliberate differences from Sphinx-Needs (the output is self-consistent, not pixel-identical):

  • Counts cover needs only — this is the difference most likely to change a number you are reading. A Sphinx-Needs build counts needs and their parts in every slice and cell; ubCode counts needs. A filter over the is_part context constant therefore renders 0 here (need parts are not first-class filter items — the parts themselves are modelled, and a needtable with :show_parts: renders a row for each), and any project using parts should expect its chart values to be lower than a Sphinx build’s. This matches the :need_count: role, which has always counted needs only — the same filter, evaluated by the same code path, cannot disagree with a chart cell.

  • Bad input warns and degrades; it never fails the build — a Sphinx-Needs build turns fifteen ordinary authoring mistakes into a hard abort with no HTML at all (most of them as raw Matplotlib messages). ubCode reports each one as an ordinary warning (needs.filter_invalid or needs.option_invalid, on the directive or the option that caused it) and still draws the chart: an unparseable filter counts 0 in place, keeping the slice’s position and its label; a short needbar row is padded with zero-valued cells, or with empty column names when the short row is the one :xlabels: FROM_DATA consumes; a :labels: / :xlabels: / :ylabels: / :explode: list that does not match the data is padded with numbered labels or truncated; an unusable :colors: value falls back to the theme palette, and an unusable :text_color: value to the theme’s text colour; a :colors: list shorter than the data continues from the theme palette for the rest (a Sphinx-Needs build cycles the author’s own colours around a pie instead, and pads a bar with its default cycle); an empty needbar data cell counts 0 and a blank line between needpie filters is skipped (a Sphinx-Needs build silently counts every need in both of those cases). The needbar half of that is a DATA-cell rule only: an empty cell in a row or column a FROM_DATA sentinel consumes is a label, so it is left as an unnamed series and never warned about, exactly as upstream leaves it.

  • Deterministic output — the same sources produce byte-identical chart markup on every build: fixed-precision geometry, ordered data, and element ids from a per-document counter, with no clock and no randomness anywhere in the path. Across platforms the guarantee is evidenced rather than asserted — the pinned chart fixtures are rendered and byte-compared on Linux, Windows and macOS in CI, so a platform difference would show up as a failing test. A Sphinx-Needs build’s chart images are not byte-reproducible — each carries the build’s wall-clock time, and a bar chart’s internal element ids are freshly random per build — so two builds of unchanged sources differ.

  • Correct on an incremental build — a page’s chart re-renders exactly when a number, label or colour it draws actually changes, including when the need that moved lives in a different document. A Sphinx-Needs build leaves such a chart stale until its own page is touched (as it does for needtable and :need_count:).

  • Accessible by construction — the SVG carries role="img" and is named and described by id. The accessible name is the chart’s title argument when there is one, and the generated summary when there is not, so a chart is never left unnamed. The description is that summary: for a pie it names each slice and its value, and for a bar it gives the shape (how many series over how many categories) and points at the table, because reading a whole grid aloud is worse than reaching it as a table. The table below the plot is the data: every label and value as ordinary selectable text, with a share column for a pie. A Sphinx-Needs build emits an <img> whose alt is the image file’s own path for needpie always, and for needbar whenever no title is given; the series is not reachable as text at all.

  • Option fidelity — :style: is accepted and ignored: it names a Matplotlib style-sheet preset, which describes nothing about ubCode’s markup (an unknown name ends a Sphinx build; here it does nothing). :shadow: is likewise accepted and ignored — a themed drop shadow costs markup weight for a purely cosmetic effect. :filter-func: (needpie only) is accepted, never executed, and warned at needs.filter_unsupported, as it is on every other view directive; the chart still renders from its content lines. :show_top_sum: labels each column with its total — on a grouped chart a Sphinx-Needs build instead labels every individual bar with its own value, which is what :show_sum: already does here, and on a stacked chart it labels only the last layer, with that layer’s own value rather than the stack’s total. :text_color: follows upstream’s reach, which differs per directive: on a needpie it recolours the in-wedge value labels only (the title keeps the theme colour), on a needbar it recolours every text in the chart. Note the hazard that upstream cannot have: its chart is a baked image with its own background, while ubCode paints on the page, whose background follows the theme — so a colour chosen against a light page can be unreadable in dark. For the value labels that hazard is measured away: a hex :text_color: is given a contrast halo computed from the colour itself (the near-black or near-white that contrasts more with it), painted under the glyphs, so the digits keep their shape whatever the theme or the bar underneath them does. A named colour is not resolved — naming a colour is not knowing its value — and keeps the page-coloured halo, exactly as a named :colors: entry does. The hazard remains for the texts that sit on the page and so carry no halo: a needbar’s title, tick and axis-title labels. Leave it unset to follow the theme.

  • Where ubCode is stricter or simply works — a rotation option takes any finite angle, including a negative one, clamped to one turn (a Sphinx-Needs build silently ignores anything that is not a non-negative integer, so -45 does nothing there); :ylabels: FROM_DATA on its own works here and always fails a Sphinx build; :separator: is taken literally in both, so quotes are part of the separator. An all-zero needpie renders the “no needs” paragraph (with :filter_warning: as its wording), matching upstream; an all-zero needbar still draws, with zero-height bars, also matching upstream — it has no all-zero empty state and no :filter_warning: option, and ubCode does not invent either. (A bar whose grid is empty rather than zero — stripped to nothing by :xlabels: FROM_DATA on a one-row body, say — does render the default paragraph: the empty arm itself is not chart-kind-specific, only the all-zero trigger is.)

  • Layout limits — the plot has a fixed aspect and cannot measure text, so a long label may overlap its neighbour in the plot area. The legend table below the chart always carries the full text, but it is only visible with :legend:; without the flag the full text is there for assistive technology and for search, and a reader looking at an overlapping label has to add the flag to see it. In-wedge values are omitted for slices under 5% and for empty slices, where they would collide.

  • The filter options are a SCOPE, not a selection — both charts take :filter:, :status:, :tags: and :types:, and a scope is not a slicer. This entry used to read “No filter options — as upstream, neither chart takes :filter:, :status:, :tags: or :types:: a chart’s selection lives in its content, one filter per cell.” The premise still holds — the selection does live in the content — which is exactly why a directive-level option cannot be a selection here and does something else instead: the needs it selects are the universe every content line is counted in, so a slice is that line’s filter intersected with the scope. The lines still do the slicing: add a line and you add a slice, exactly as without a scope. A bare integer line is unaffected (it is its own value, not a count of anything), and a scope on an empty body is still an empty chart — a scope is not a slice.

    The ubCode-only :max_items: is still not offered, and would be actively wrong — a chart renders counts rather than per-need items, so a cap would silently change the numbers rather than truncate a list. That is the useful contrast: a scope narrows the universe a number is counted over, a cap changes the number.

  • and both charts take a ubCode-only :cypher: SCOPE — the same concept in the other spelling, reading the same way. :cypher: takes precedence when both are written, so carrying both is the recommended portable form: ubCode evaluates the query, a Sphinx build reads the python surfaces, and the two engines then show the same numbers — subject to Counts cover needs only above, which applies to a paired scope exactly as it applies to a content line. Both sides of the intersection are counted needs-only here and needs-and-parts there, so a project using parts still sees lower ubCode values; what the pairing removes is the divergence caused by the scope being read by one engine and not the other, not the divergence in what a count includes. Written alone, a :cypher: is the one compatibility option whose no-op changes a rendered number: a Sphinx build ignores it and counts each line over the whole project. Some scopes have no python spelling at all — a link traversal, which is the reason to reach for the option in the first place — and for those the divergence stands, accepted and documented rather than warned about. needbar used to offer neither spelling; both are now read there, and a bar’s cells scope exactly as a pie’s slices do. Sphinx-Needs acceptance status: the accept-and-ignore change for :cypher: on both charts shipped in Sphinx-Needs 8.5.0 (sphinx-needs#1818), and the four python scope options are the chart’s scope there from that same release onwards (sphinx-needs#1831); on 8.4.0 and earlier a Sphinx build reports unknown option for any of the five and drops the directive, so the view disappears from the page (an ordinary build still reports success — only -W fails). The reading, with an example, is in A chart’s filter options SCOPE its slices.

Needs gantt chart (needgantt)

You write a .. needgantt:: selecting needs with the ordinary filter surfaces — each selected need becomes a task whose LENGTH comes from a need field (:duration_option:, default duration) and whose PROGRESS comes from another (:completion_option:, default completion); its POSITION comes from its links, through the three constraint families :starts_after_links: (default links), :starts_with_links: and :ends_with_links:. There is no date on a need: :start_date: is the only calendar in the whole feature, and it is optional.

A Sphinx build concatenates that into @startgantt text and hands it to PlantUML, which solves the schedule and bakes an image.

The preview and ubc build html render an inline SVG written straight into the page — ubCode solves the schedule itself, with a deterministic topological longest-path pass — with an elbow connector per dependency, a progress track on every task that has one, and (under :show_legend:) a task table carrying every task’s start, end, duration, completion and link as ordinary selectable text. There is no image file, no client-side runtime and no extra request, so a chart renders with JavaScript disabled and in print.

Status — implemented, with these deliberate differences from Sphinx-Needs:

  • Every task is drawn once. This is the largest visible difference, and it is a Sphinx-Needs defect rather than a design choice on either side. Its generated PlantUML declares each task as [title] as [id] but then addresses that task’s completion and colour lines by [title]; PlantUML binds a bracketed name it has not seen as an alias to a brand-new zero-length task, so each of those lines silently creates a phantom bar. Because type_color is populated for every configured need type by default, in practice every task in every Sphinx-Needs gantt is currently drawn twice. Two needs that share a title collide there for the same reason; here they are two independent tasks.

  • The two need fields must be declared, and are never invented. A Sphinx build adds duration and completion as integer fields to every need type of every project — whether or not a needgantt is ever written — from needs_duration_option and needs_completion_option. ubCode does not: declare them under [needs.fields], or point the project-wide [needs] duration_option / completion_option at fields you already have. A needgantt naming an undeclared or non-numeric field reports needs.option_invalid with a did-you-mean and still DRAWS — every task one day long, no progress — where a Sphinx build logs a config warning and deletes the directive from the page.

  • Warn and degrade, never abort. A malformed :start_date: and an unknown :timeline: each raise NeedGanttException in a Sphinx build — a BaseException subclass no except Exception catches — and take the whole build down with a traceback. Here each is a warning: the chart falls back to relative day numbering, or to the daily axis, and still draws.

  • A constraint cycle is reported, not drawn. PlantUML renders a cycle (including a need that links to itself) without complaint, so the picture a reader saw was silently meaningless. ubCode names the tasks involved and shows the view’s error block instead.

  • A dangling constraint target is reported, not fatal. A link naming a need that does not exist is an unguarded dictionary lookup in a Sphinx build — a KeyError that ends it. Here the constraint is dropped, the missing ids are named in a notice above the chart, and everything else draws. A target that is a real need but is filtered out of this chart is dropped silently, because there is no bar to anchor to — so that task is drawn at its unconstrained position. Be aware of what that means: a :filter: written slightly too narrowly (a status == 'open' that drops a completed predecessor, a :types: that drops one tier) turns a chained schedule into a set of tasks all starting on day one, and the chart carries no signal that it happened. It is deliberately not reported — an inline notice on every filtered chart would be noise, and a filter is an instruction rather than a mistake — but if a chart’s tasks all start together and you expected a chain, this is the first thing to check.

  • The month you wrote is the month you get. A Sphinx build indexes a zero-based month table with a one-based month number, so :start_date: 2020-03-25 reaches PlantUML as “the 25th of April 2020” — one month late — and any December start date raises IndexError and ends the build.

  • Task bars link to their needs. The Sphinx-Needs documentation says they do; its code emits no link anywhere, so no gantt it has ever produced had one. Here each bar is a plain SVG <a href> — no client runtime — and the task table’s name cell carries the same link.

  • A need type with no configured colour takes a theme palette slot, so it still reads as a distinct series and still re-themes with the page; a type with a colour uses it, as upstream does. :no_color: puts every bar on the neutral theme fill.

  • The task table is :show_legend: -gated, and IS the legend. By default a chart is just the chart, as a Sphinx build’s is. With :show_legend: a table follows it — one row per task, with a colour swatch, the need’s own link, its type, and its start, end, duration and completion as text — rather than the separate colour-per-type table needflow and needsequence emit. One table, because it already carries a per-task swatch: a second would be a second authority on the same colours, and could not name a palette slot at all, since that is a stylesheet token rather than a value. Whether or not the table is drawn, the chart’s <desc> names up to 25 tasks with their dates and then summarises the rest (…and N more; set :show_legend: for the full table.), so a chart of any size is readable as text and a chart beyond that size says where the whole of it is. A 500-task <desc> would be longer than the page it describes, which is why the cap exists; :show_legend: is the uncapped option.

  • Row order follows the document, not the id. With no :sort_by:, tasks are drawn in the order the needs were written — page by page, and within a page in directive order — which is the order a Sphinx build’s process_filters returns and the order a plan reads in. (ubCode’s other views default to id order, because a list has no authored cascade to preserve; a schedule does.) Pages are ordered by path rather than by the toctree, which is a deterministic stand-in rather than a claim of parity with Sphinx’s read order; needs with no need-directive origin — imported, external, src-trace — carry no page_order, so they sort after the authored ones, by id. (A traced need’s card does sit at its .. src-trace:: site — the schedule simply does not use that position, which is a deliberate limit rather than an absence.)

  • Dependencies are drawn. Every constraint the schedule honours gets an elbow connector from the anchor it pins to towards the anchor it pins, in a neutral colour — the Sphinx-Needs output does the same through PlantUML. A constraint the schedule could not honour (see the conflict rule below) gets no arrow, because the picture does not show that relationship; it is named in the :debug: dump instead.

  • Progress is a track, not a second shade. A task with a completion draws as an outline in its own colour with the done portion filled solid inside it, so “half done” reads at a glance in either theme. A task with no completion draws as a plain solid bar, as upstream does.

  • No automatic anchor; use :name:. A chart with a :name: carries a stable id you can :ref:, and a in its caption when it has one — a needgantt given no argument has no caption, and unlike needflow a captioned one draws its caption in every body state, so its is there even when the filter matched nothing. An unnamed chart carries neither, and ubCode never mints the positional id a Sphinx build does. See Anchors on views and diagrams for the whole rule — it is the same for every view and diagram, including the :name: values that normalise to nothing.

  • A completion value outside 0–100 is reported as written but clamped when drawn, so the fill cannot overflow its bar. Neither tool range-checks the field; a Sphinx build passes 500 to PlantUML verbatim. A completion of 0 draws no overlay, matching upstream, which emits its completion line only for a truthy value.

  • A missing, zero or negative duration becomes one day — upstream’s own substitution, kept so a project moved between the two tools draws the same bars. A fractional duration is rounded, also as upstream does.

  • :config: and :scale: are accepted and reported. :config: carries a raw PlantUML snippet (read, oddly, from needs_flow_configs — needflow’s own table) which cannot reach an SVG ubCode draws; :scale: sizes a raster image, and is redirected to the ubCode-only :width: / :height:. Both report needs.option_unsupported.

  • :link_types:, :show_link_names: and :highlight: have no effect in either tool — a Sphinx build declares them on this directive and never reads them (a gantt’s link vocabulary is the three *_links options). ubCode reports needs.option_unsupported so the no-op is not silent.

  • :debug: shows the solved schedule, one line per task, rather than generated diagram source — there is none, and the schedule is what a reader debugging a surprising chart needs.

  • No :max_items: and no :cypher:. Capping a schedule drops predecessors out from under the tasks that remain, so every surviving bar moves: the result is not a shortened chart but a wrong one.

  • parent_needs is not a constraint link type — the parent-child edge is containment, not precedence, and scheduling a child after its parent’s end is never what an author means. This follows needflow rather than needsequence.

  • Day one is the earliest BAR, not “the project”. The schedule is solved in relative offsets and then slid so that the earliest task sits on day one — and the slide applies to every task, including tasks that carry no links at all. So an :ends_with_links: chain that has to reach back before the start moves the whole chart, and a task you never linked to anything can change which day it is drawn on because of another task’s links. On a dated chart this means :start_date: anchors the earliest bar rather than the project: a Sphinx build’s Project starts the … line is a project anchor, and this is not. The alternative — clamping only the tasks that ran negative — would break the relative offsets the constraints encode, which is why the whole solution moves.

  • Where two constraints on one task conflict, the later position wins. A task’s start is the maximum over everything constraining it (that is what makes the schedule independent of the order the links happen to be walked in), so a need carrying links in two families — say :ends_with_links: to a short task and :starts_after_links: to a long one — takes the later of the two, and the :ends_with_links: it cannot also satisfy is simply not honoured. A dropped constraint of this kind is named in the :debug: schedule dump, which is the surface for it; the chart itself shows the position that won.

  • Layout limits — the plot has a fixed width and cannot measure text, so a long task title is ellipsised in the left gutter; the task table below always carries the full title, unabridged and linked. An in-bar completion percentage is omitted for bars too narrow to hold it.

  • No “today” marker, deliberately: it would move the page’s bytes every midnight, which would defeat the incremental build’s byte-comparison and make a rebuilt site differ from itself for no authored reason.

Needs report (needreport)

You write a .. needreport:: with any of :types:, :links:, :options: and :usage: — an introspection of the project’s own vocabulary rather than a view over its needs.

A Sphinx build renders a packaged Jinja template into reStructuredText and splices it back into the page, producing four .. dropdown:: sections; a :template: option (or needs_report_template) replaces that template with one of your own.

The preview and ubc build html do the same: with no template configured they compose those four sections themselves — the same .. dropdown:: construct, the same tables, the same columns — and with a :template: or a [needs] report_template they render your template against the same five names a Sphinx build supplies (types, links, options, usage and report_directive), splicing its reStructuredText back into the page. A section you did not ask for arrives as an empty value rather than as a missing name, so a template can always reference all five.

Status — implemented, with these deliberate differences from Sphinx-Needs:

  • The counts in usage are real — a Sphinx build’s are permanently zero, because its directive runs while documents are still being read and no correct count exists yet (useblocks/sphinx-needs#1110, which its own documentation states as a warning). ubCode evaluates the report from a finished index, so usage.needs_amount and usage["needs_types"] carry the numbers you would expect.

    If you want a template that renders identically in both systems, use :need_count: roles for the numbers, as the Sphinx-Needs packaged template does — they work the same way here, and ubCode tracks what they resolve to so a warm rebuild cannot serve a stale one.

  • A :template: may not leave the project’s source root — a path that climbs out of it is refused with needs.report_template_outside_project and the default report is rendered instead. A Sphinx build reads it. The reason is that ubCode resolves and reads this path in the editor, on every preview, where a Sphinx build only ever reads it inside a build you chose to run: unclamped, opening a checked-out repository would read a file that repository named, with no command run at all. [needs] report_template is not clamped — it is written by the project owner in the project’s own configuration file, the same trust boundary [variants] data_file (or [needs] variant_data_file) and [reports] directory already sit on.

  • A template that cannot be used falls back to the default report — and says why, under one of three codes: needs.report_template_not_found (no such file), needs.report_template_outside_project (the clamp above), or needs.report_template_failed (a Jinja syntax or evaluation error). The first two are reported while the document is indexed, so they appear in the editor — on the :template: option itself, or on the directive name when the template came from [needs] report_template and there is no option to point at; the third needs the whole project’s data and appears in the build output. A Sphinx build renders nothing at all in these cases, which is the honest answer when your only “default” is the packaged template — ubCode has a real one to fall back to, so it uses it rather than losing the section.

  • The path base for the configuration key differs — [needs] report_template takes a relative path from the directory holding ubproject.toml, and accepts an absolute one, which is what every other path-valued ubCode key does. A Sphinx build resolves needs_report_template against the source directory always, stripping a leading / rather than honouring it — so an absolute path is appended to the source directory instead of being read from where it points. The directive’s own :template: follows the ordinary Sphinx convention in both systems: relative to the document, with a leading / meaning source-root-relative.

  • There is no needs_render_context — a template sees the five names above and nothing else. In particular report_directive is fixed at dropdown: a Sphinx build recomputes it because no loaded extension may provide dropdown, and ubCode always does (see below).

  • needs_template_folder plays no part — it is the lookup root for need-content templates (the :template: option on a directive such as .. req::), a different feature ubCode does not implement, and it has never had a role in needreport resolution in either system.

  • Give the template file an extension your project does not build — .need or .txt, not .rst or .md. This is the same advice the Sphinx-Needs documentation gives, for the same reason: a template inside the documentation tree with a document extension is also read as a document of its own.

  • A template composes its own text, so ubCode stops escaping it for you — the default report escapes every value it interpolates, which is what makes it impossible for a need type titled .. raw:: html to open a directive inside the generated page. Your template decides that instead. What still holds either way is the fence: generated report content is parsed in its own arena with nested needimport / needreport and need-type directives refused and external content denied, so the blast radius is the page’s own rendering.

  • Counts cover needs only — as for the chart views and the :need_count: role, and for the same reason: one filter, one counting path, so a report row and a :need_count: written beside it cannot disagree. A Sphinx-Needs build counts needs and their parts here, and includes needs from external sources; ubCode counts needs, external ones included. A project using parts should expect a lower total.

  • Need types are listed in config declaration order, matching a Sphinx build: ubCode resolves [[needs.types]] into a name-keyed table, so the order you wrote them in is restored from a recorded ordinal rather than read off the table. Re-ordering the entries therefore changes the report — and is a configuration change, so it re-renders every page. The extra field names in the Need Fields section are the one list still shown alphabetically where a Sphinx build shows them in declaration order.

  • The vocabulary is ubCode’s — a Sphinx-Needs build lists three need types and eight extra fields its bundled services inject (issue/pr/commit; query, specific, url, avatar, closed_at, …) even in a project that uses none of them, plus the two needgantt fields duration and completion — ten extra names in all. ubCode ships no services, and — unlike a Sphinx build — never invents the two needgantt fields either (needgantt renders here; see its own entry above), so those rows are simply absent unless the project declared them itself. The classification is identical for every field a project does declare.

  • The dropdown provider is always there — a Sphinx-Needs build aborts if no loaded extension provides dropdown (useblocks/sphinx-needs#899), which is why its documentation tells you to enable sphinx_design. ubCode renders .. dropdown:: natively and unconditionally, so the failure cannot happen — and the report’s sections get the same ported sphinx-design presentation an authored .. dropdown:: already gets, in both the preview and a built site.

  • A report with no options warns and renders nothing — matching a Sphinx build’s behaviour, under needs.report_no_sections so [lint] ignore can silence it (a Sphinx build’s warning cannot be). A section you did ask for that has nothing to show is omitted rather than rendered empty, also matching.

  • Prefixes and titles render as plain text — a Sphinx-Needs build wraps the ID prefix in single backticks, which its default role renders as a citation reference. Every cell here is escaped text, so a need type titled *draft* reads as those characters rather than turning the row italic.

Needs diagrams (needuml / needarch)

You write a .. needuml:: whose body is a Jinja template that produces PlantUML, or a .. needarch:: — the same directive, written inside a need, which gains need() and import() for reading that need.

A Sphinx build renders the body against the project’s needs, hands the produced PlantUML to a PlantUML server through sphinxcontrib-plantuml, and embeds the returned image.

The preview and ubc build html render the same body with the same engine: the same callables (need, flow, filter, ref, uml and import), the same four-layer context precedence, the same :key:-based recursion guard, and the same [needs] diagram_template. Then they part company: ubCode has no PlantUML server, so the produced diagram is transpiled to Mermaid and the page draws it client-side.

Everything below follows from that one decision, or from the port’s standing warn-and-degrade rule.

  • The diagram is text in the page, not an image — no PlantUML server, no network call, no _images/ copy, and the same mermaid bundle every other diagram on the site uses. It is selectable, searchable and theme-aware, and it re-draws at the reader’s resolution. The cost is coverage: a PlantUML construct Mermaid has no equivalent for cannot be drawn. The whole diagram is then reported (needs.uml_unconvertible) and the produced PlantUML shown in its place, rather than left as a blank space — add :debug: to any diagram to see the same source. Diagram families outside Mermaid’s reach (gantt, mindmap, wbs, salt, ditaa, …) are the usual cause. A diagram that does draw but loses some of its lines on the way is reported too, under its own code (needs.uml_partially_converted), naming the lines that are missing — quoted as they appear in the message, so a [lint] message-ignores rule can match them. That code covers lines ubCode could not read. A construct it reads and then deliberately does not draw — a title on a component diagram, say, which Mermaid has no keyword for — is not reported and never will be: the diagram is drawn, and that part of it is a documented omission. Two codes rather than one, because they need different answers: the first means there is no picture, the second that the picture you can see is incomplete.

    Changed in version 0.37.0: needs.uml_partially_converted added. Before it, a diagram that lost part of its content built without a word.

  • :scale: sizes the diagram’s box, as a percentage width, where a Sphinx build scales the rendered bitmap. A value that is not a whole-number percentage is reported (needs.uml_scale_invalid) and the diagram drawn at 100%; a Sphinx build swallowed the error in silence until Sphinx-Needs 8.5.0, which reports it as well (scale value must be a number. "not-a-number" found).

  • The caption is a <figcaption> — the directive’s argument. A Sphinx build emits it as a docutils title, which renders as a heading in the middle of the page and, on a page whose first element is a needuml, is taken as the document title.

  • :config: resolves against [needs.flow_configs], and a name that table does not define is reported (needs.uml_config_unknown) and contributes nothing to the diagram; a Sphinx build passed it over in silence until Sphinx-Needs 8.5.0, which reports it as well (config name … is not defined in needs_flow_configs.). Because the diagram is transpiled, a block’s lines reach the produced PlantUML but only the ones Mermaid has an equivalent for change what is drawn — skinparam in particular is dropped. :debug: shows the block in place.

  • :extra: splits each pair on its first colon, so a value that contains one — a URL, a timestamp — survives. A Sphinx build splits on every colon and ends the build with an unpacking ValueError. A pair with no colon at all is reported (needs.uml_extra_invalid) and dropped, and the rest of the diagram renders.

  • :save: is accepted and not yet written. The option is read and its path validated — an absolute path is reported (needs.uml_save_path), and a \ separator or a .. segment reported as advice (needs.uml_save_path_unportable) — but no .puml file is produced. A Sphinx build writes one into the output directory.

  • A needarch outside a need is reported, not fatal (needs.arch_outside_need): the diagram renders without need() and import(), which have nothing to read. A Sphinx build raises NeedumlException and the build ends.

  • Every other failure degrades too. A template error, an unknown id, an import() naming something the need does not have, a diagram nested past the depth cap — each is reported under its own needs.uml_* code, which [lint] ignore can silence, and the diagram renders as far as it got. A Sphinx build raises on most of them. Two of these are reported by Sphinx-Needs as well from 8.5.0 onwards, where 8.4.0 accepted them in silence: a ref() given both or neither of option and text (needs.uml_ref_label) and an import() naming an option the need does not have (needs.uml_import_option_unknown). Both are warnings here.

  • A keyless self-referencing needarch renders once. A Sphinx build registers the directive’s own guard entry under its :key: — None when it has none — while uml() defaults to "diagram", so the two never match and the diagram renders twice (useblocks/sphinx-needs#1334). ubCode registers the key uml() would ask for.

  • filter() returns needs in id order. A Sphinx build returns them in its needs-view (insertion) order. ubCode’s needs index is a hash map whose iteration order is seeded per process, so any order but a sorted one would differ between two builds of identical sources — and the produced diagram is folded into the page fingerprint.

  • There is no Python-method surface. A Sphinx build’s template can call arbitrary methods on the objects it is given, and {{ need }} renders a Python repr. Templates here see the same need projection a ubc report template sees.

  • needs_uml_process_max_time has no equivalent, deliberately: it is a post-hoc warning about time already spent, and ubCode bounds the work instead — a fuel budget, a nesting cap, a produced-output cap, and a recursion guard.

  • needs_build_needumls, the whole-project .puml dump, is a separate Sphinx builder upstream. In ubCode an export is its own command rather than a side effect of building a site, so it is out of scope here.

Needs extract (needextract)

You write a .. needextract:: naming one need by id, or a filter selecting several, to re-present needs that are defined elsewhere at a place that suits the reader.

A Sphinx build copies each selected need’s already-parsed body out of the document that defines it and splices it into this page, then re-mints that need’s HTML anchor — and every anchor written inside its body — on this page as well.

The preview and ubc build html render a view: the same card a need gets on its own page, with its current metadata and its body, but no anchor of the need’s is repeated here.

The differences worth knowing:

  • An extract creates no need. This is not a difference from a Sphinx build — the copies are absent from its needs.json too, and invisible to every filter and count, including one written on the very page that shows them. It is stated because the Sphinx-Needs documentation calls them “copies of filtered needs”, which reads as though they were needs.

  • The card carries no copy of the need’s anchor, and neither does anything inside it: an explicit target, a :name: you gave a figure or a table, an index entry — all of them render, and none of them becomes a destination on the extract page. A Sphinx build re-mints every one of them, with no warning, so extracting one need twice on a page publishes the same id twice and extracting a need on its own page publishes it beside itself. Nothing links to those duplicates in either system — a reference to the need, or to a label inside it, resolves to the page that defines it — so what this removes is dead anchors, not live ones. What the site does get is one anchor of its own, ub-needextract-1, ub-needextract-2 and so on in the order the directives appear, so you can link to the extract itself. A Sphinx build reports a .. contents:: inside a need’s body as an error and renders no listing there. ubCode renders it, listing this page’s sections, with its entries anchored on the card rather than on the page, so it moves no anchor of the page’s own; a need’s body shown by needimport behaves the same. The card’s ID chip is a link back to the need’s own page — except for an external need, which has no rendered page in this project to point at, so its chip stays plain text.

  • Content that would create needs is refused. A need type, needimport, needreport, list2need or src-trace written inside a need’s body renders as a placeholder in the extract and creates nothing, with one needs.import.nested_generative warning per directive naming what was refused. A Sphinx build renders such content a second time, as a full card with the same anchor as the original — indistinguishable from a second definition.

    A needtable and a needextract are NOT in that refused set, because neither creates a need. A needtable inside extracted content renders live here, against the whole project, and re-renders when what it selects changes; a nested needextract renders as the plain directive placeholder with no warning, because an extract does not extract a second time. Both of them end a Sphinx build with a Python traceback.

  • Everything else in the body renders in full, including four things a Sphinx build cannot do here: a footnote (which ends its build with a KeyError), numbered figures (whose numbers vanish from the copy while a :numref: inside it prints the original’s), a substitution defined in the source document (which degrades to raw |markup|), and math on a different page from the need (useblocks/sphinx-needs#890). All four work here for one reason: the body is re-parsed and rendered through the same pipeline every other page goes through, rather than copied as an already-parsed tree that has seen neither document’s processing.

    Assets in extracted content follow the SOURCE need’s policy: a need authored in this project renders its images and figures, resolved against the need’s own document rather than against the page doing the extracting — so a need in a subdirectory shows the same image wherever it is viewed. A need imported from another project renders them only if you gave that source allow_assets = true; and an external need’s are refused, with a warning naming the need rather than a silent gap.

    A relative reference may reach anywhere inside the project: one written as ../shared/logo.png resolves against the need’s own document, exactly as it does on that document’s own page. Only a reference that leaves the project’s source directory is refused, with a warning naming the need; a Sphinx build copies such a file and renders it with no warning about the reference at all, so this is the one place ubCode is deliberately stricter here.

    One consequence is worth knowing about. You write the reference once, but a view resolves it to a project-relative spelling of its own while the need’s own page keeps the one you wrote — so the two pages ask for the same file under two different paths, and it is published twice, the second copy under the collision suffix (logo.png and logo1.png). Both images render and both hrefs resolve; only the bytes are duplicated.

    One limit applies to an authored need’s assets, and only inside an extract — on the need’s own page every reference renders normally. The directory holding the need’s document must have a name that is spellable as a mount base, which rules out whitespace, a backslash, or a #, ? or : anywhere in it; a need in such a directory has its assets refused in views of it, with a warning naming the need.

    A Markdown heading inside a need’s body is a rubric rather than a section, and it stays one in the extract: the copy does not become a heading in the extracting page’s structure, does not appear in that page’s contents, and carries no anchor of its own.

  • Hidden and external needs can be extracted. A Sphinx build refuses both — a :hide: need never stores a body for it to copy, and an external need has none at all — and warns once per need, so a broad filter in a project with external needs fills the log with warnings you cannot act on. ubCode has the stored content either way and renders it.

    Two limits apply to an EXTERNAL need specifically. Its card’s ID chip is plain text rather than a link, because the need has no rendered page in this project. And a :need: reference written inside its content is resolved as written, without the source’s id_prefix applied — so if the external project’s own ids are unprefixed and you gave the source a prefix, such a reference can resolve to a different need or to none. An extract is the first place ubCode renders external need content, so this is a limit of that content rather than a change to it; a need imported with needimport has its references re-pointed correctly.

  • One warning per directive, not per need. A selection containing needs with no content at all reports needs.extract_no_content once, naming how many there were and the first few ids.

  • Options that end a Sphinx build report a warning here. A mistyped id (needs.extract_missing_id), and an argument together with :filter: (needs.extract_argument_conflict, which the Sphinx-Needs documentation already tells you not to write) both warn and render the empty state, where a Sphinx build warns and then aborts on the next statement it reaches. needs_include_needs = false aborts a Sphinx build with no warning at all. It has no equivalent here at all: it is a conf.py setting, and ubCode does not read conf.py — see Supported toolchain, which lists it under the excluded configuration.

  • A filter matching nothing shows :filter_warning: if you gave one, and No needs passed the filters otherwise — the same empty state every other view directive uses. :show_filters: appends the used-filter line, matching a Sphinx build (which implements the option but does not document it).

  • The site’s own :layout: and :style: are applied to every card it draws. Until ubCode 0.33 they were read and reported instead (needs.option_unsupported), on the reasoning that a per-site override would need a second design resolver. That turned out not to be so — the resolver the need-level :layout: already goes through took the job — and the option is now honoured, in the editor preview and the built site alike. Precedence is a Sphinx-Needs build’s own: the site’s value beats the resolved value on the need (so it beats a needextend on another page, a [needs.fields.*] default, a variant, a dynamic function and an imported needs.json), an absent or empty option falls through to the need’s own, and a written :layout: clean is a real override rather than an absent one — visible once the project moves its default_layout. A site’s :style: replaces the need’s tokens for that site’s cards rather than adding to them. Two things diverge, both in the tolerant direction. An unknown design name is reported once for the SITE (build.need_layout_unknown, naming the directive rather than any need) and applies no override at all, where a Sphinx build raises and takes the whole build down. A rejected :style: token is reported once for the site (build.need_style_invalid) and the valid tokens still apply, where a Sphinx build injects the token unchecked. The editor preview applies both options and stays silent about both findings, exactly as it does for the need-level ones. An extract stays a view: the override changes how the cards are drawn and nothing about the needs, so the meta rows keep the need’s own stored values and needs.json is untouched. The one card element that follows the override rather than the need is the footer echo of a design that echoes them — complete’s layout: … / style: … line. ubCode’s footer names the design and tokens the card is actually drawn with, so inside an overriding extract it reads layout: complete where the need’s own value is clean; a Sphinx build’s footer echoes the need’s stored field values there instead. That is this arc’s one declared divergence, and it follows from a pre-existing one rather than being new: ubCode’s echo has always printed the resolved design name where a Sphinx build prints the raw stored field (which is empty for a need that set none), so the two already differ for every complete card with no :layout: of its own (a design with no footer, like the default clean, echoes nothing in either engine). :export_id: is deprecated in Sphinx-Needs and is reported and ignored, exactly as it is on the other view directives.

  • An extract’s cards are not indexed for search. They are a second view of needs already indexed at their own cards, and a card here has no anchor of its own to navigate to, so a search hit would lead nowhere. Prose around the directive is indexed normally.

Needs from a list (list2need)

You write a .. list2need:: whose body is an indented bullet list, two spaces per nesting level, with :types: naming one need type per level. Each bullet becomes a need.

A Sphinx build renders each item through a template into reStructuredText and splices the generated text back into the parser.

The preview and ubc build html parse the list directly and create the needs from it. They are ordinary needs from that point on: filters, needtable, needs.json, schema validation, needextend, hover, goto-definition and find-references all work on them, and a :need: role pointing at one resolves.

Status — implemented; these differences are deliberate:

  • Each need points at its own item. Goto-definition on REQ-1 selects the REQ-1 token on the line you wrote it on, and every diagnostic below lands on the offending item or option rather than on the block header. One diagnostic is the exception, and it is one you are likely to meet: needs.id_regex — raised when an id does not match needs_id_regex, which hand-written (REQ-1)-style ids do not under the default pattern — is anchored at the block’s list2need token rather than at the item, because it is raised by machinery shared with every need directive. Several failing ids in one block therefore give you several warnings at the same position — each one names the offending id, which is what tells you which bullet it is about.

    A Sphinx build cannot do this: the line number its needs carry comes from where the generated text was spliced in, which is why they routinely report a line past the end of the file (useblocks/sphinx-needs#1349), and why every need after a list2need in the same file is shifted too. ubCode’s line numbers are the true ones, for the generated needs and for everything after them.

  • It works in a Markdown document. A Sphinx build creates no needs at all there — its mechanism needs a parser API that MyST only mocks — so there is nothing to be incompatible with. Because that means such a document builds here and produces nothing there, each Markdown block reports one informational needs.list2need_myst_host note.

  • :presentation: lays the cards out, and a value it cannot use falls back to the default. nested — the default, here and in a Sphinx build — renders a deeper item’s card inside the card of the item above it, after that card’s own content; standalone keeps every card a sibling, in document order. A value that is neither is reported (needs.list2need_presentation) and then treated as unset, so the block renders nested rather than silently picking the other layout. A Sphinx build aborts on that value instead, so there is nothing to be incompatible with.

    The option is a data choice as well as a layout one, in both engines: a nested item records the item above it as its parent, and a standalone one records nothing — and cannot be made to, since an inline ((parent_needs="…")) is refused like the directive option below. ubCode derives parent_needs — the id of the nearest enclosing need — and projects parent_need from it, for a list2need item and for a need directive you write inside another need’s body alike, so a nested item’s needs.json entry carries both fields here as it does in a Sphinx build. The meta rows on the card read parent needs and child needs, the same words a Sphinx build writes. ubCode labels every link row by that type’s configured outgoing / incoming title, so this is general rather than special-cased for the builtin: name your own link type and the card shows the names you chose. The presentation still differs — a table row here against an inline parent needs: line there, so no trailing colon — and a link type declared with no titles keeps ubCode’s older labels, the bare type name and the name plus (incoming), where a Sphinx build would default to the name and <name> incoming. One detail remains worth knowing before you compare outputs: parent_needs_back is computed for :filter: expressions, needtable columns and the card, but is not written to the export file, the same as every other *_back field here. :links-down: remains the layout-independent alternative where you want an edge that does not depend on how the cards are drawn.

    You cannot write the field yourself. :parent_needs: on a need directive, and ((parent_needs="…")) on a list2need item, are both refused with needs.option_derived and the written value is dropped — the field is a fact about where the need sits, so an authored value could only contradict the document around it. That applies to the builtin link, which is what parent_needs is unless your project declares something else of that name: declaring [needs.fields.parent_needs] replaces it with an ordinary field of your own, and the builtin link is then never registered at all, so the name is yours to write and nothing here refuses it. A Sphinx build accepts the option, lets the value reach the need, and then overwrites it from the nesting with no message at all, so the stored data is the same either way and only the diagnostic is new. One upgrade note follows from it: a project that declared [needs.links.parent_needs] itself and authored values into it kept those values before, and loses them now — they are derived from nesting instead. Move such an edge to :links: or to a link type of your own.

  • A hidden item can still hold the items nested under it. A Sphinx build refuses to nest anything inside a ((hide="true")) item, because it removes a hidden need from the page entirely and a card placed inside one would be rendered nowhere. ubCode removes nothing — a hidden card is the whole card inside a collapsed disclosure — so the item you wrote underneath one stays where you wrote it, inside that disclosure, and its own anchor still reaches it.

  • An item’s content is markup, and it is parsed with *this* document’s grammar. **bold** is bold, a :need: role is a live cross-reference that counts towards traceability like any other, a :need_part: creates a real part, and a directive written under the item is a directive. Almost everything a need directive’s body can hold, an item’s content can hold. The one exception is reStructuredText-only: a directive you declared yourself under [parse.extend_directives], written inside an item’s content in a .rst document, shows its argument, options and body as text rather than parsing them. It is not reported — the names you declare under [parse.extend_directives] and [parse] ignore_directives silence the unknown-directive warning inside item content exactly as they do everywhere else — it simply is not parsed. In a Markdown document the same directive parses in full, options and all, exactly as it does in a need’s body. A Sphinx build parses it in a reStructuredText document, where the comparison exists, so this is a gap rather than a difference of taste.

    A directive nothing knows — a typo, say — is reported inside an item’s content exactly as it is in a need’s body, in both hosts.

    In a Markdown document the content is Markdown, not reStructuredText: write {need}`REQ-1` rather than :need:`REQ-1`, and a fenced directive inside an item needs an outer fence one backtick longer than the inner one. A Sphinx build parses item content with the document’s own parser too, for the same reason — but it creates no needs at all from a list2need in a Markdown document, so there is no Sphinx behaviour here to be compared with.

    Every position inside that content is a real position in your file: a diagnostic raised inside an item’s content — by ubCode or by the parser itself — names the line and column you wrote it on, so the squiggle lands where you are looking. Two lines report their whole width rather than an exact column: one containing a tab (whose width the two engines resolve differently, so the column cannot be trusted — source.tab_in_line reports the tab itself), and, in a Markdown document, a block-level report, because a Markdown block records only its line.

    One thing is refused rather than parsed: a need-creating directive inside an item’s content. A .. req::, a nested .. list2need::, a .. needimport:: — any directive that would create needs — is reported (needs.import.nested_generative) and not executed, because generating needs from generated content is the one shape that can recurse. Its own text stays on the page, shown as you wrote it, so the refusal is visible rather than a silent deletion; everything else in the same content still builds. A Sphinx build creates the need instead, so a document that relies on that will have one need fewer here.

  • A malformed list warns; it never aborts the build. Twelve inputs stop a Sphinx build dead, most with a bare Python traceback and no line number: a - or numbered bullet, a bare *, content before the first bullet, an indent that is not a multiple of two, an item deeper than :types: declares, too few :links-down: types, an unknown type or link name, an invalid :presentation:, a missing :types:, and an empty body. Here each is a needs.list2need_* warning naming the line or the option, and the item — or, for a missing :types: or an empty body, the block — is skipped. The rest of the list still builds. This matters most in the editor, where analysis re-runs on every keystroke: a half-typed list must degrade to a squiggle, not kill the file — and the empty body is precisely the half-typed case, an author who has written the directive and not yet the list.

    A thirteenth input is not an abort but has the same effect: an unknown option on the directive itself. docutils rejects the directive outright, so a Sphinx build creates no needs at all from that block — the whole list disappears, with an error where the block was. ubCode reports the option under block.directive_unknown_option, drops that one option, and builds the list.

  • A skipped indentation level does not fabricate a link. A Sphinx build’s downward-link walk stops only on an exactly equal level, so jumping from level 0 to level 2 lets it run past a de-indent and link an item to a need in a different subtree — while the real parent of that need gets no link at all. ubCode stops at the de-indent and reports the skipped level (needs.list2need_level). Your needs.json will therefore be missing that one link relative to a Sphinx build, and that is the point: it was never a real one.

  • A continuation line at column 0 keeps its first word. A Sphinx build silently deletes it, though its own documentation promises the line is added to the item above. The reach is wider than the missing word: a kept token can complete an ((…)) option region that a Sphinx build never sees, so the FIELD set can differ too, not just the content text.

  • Ids are identical to a Sphinx build’s, quirks included. Auto-generated ids are stable identity — they appear in needs.json, in every cross-reference and in every traceability link — so the id rules are reproduced exactly, including the ones that are traps:

    • :delimiter: defaults to ., so a title is everything up to the first full stop and v1.2 truncates it. There is no escape.

    • the (ID) scan is not anchored, so an ordinary parenthetical becomes the id — * Some (parenthetical) title gets the id parenthetical and loses those words from its title. ubCode reports this as needs.list2need_ambiguous_id where a Sphinx build is silent, but produces the same id.

    • only the first ((…)) option region is read, and it spans greedily to the last )) on the line, so a second region on the same item swallows the prose between them. Reported as needs.list2need_option_area; again the same result.

    • writing a literal () suppresses the id and switches the need to a different hash function — one that honours [needs] id_from_title where the ordinary list2need one ignores it. Both are reproduced, so a project with that key set gets the same two id schemes in one list that a Sphinx build gives it.

    The two informational reports above are graded below warning: there is nothing in your document to fix, and a Sphinx build accepts the same markup in silence.

  • Five generation artifacts are not reproduced, because they are accidents of building text and re-parsing it rather than behaviour anyone relies on:

    • a ()-suppressed id together with inline options turns those options into body text — here the options parse;

    • a directive written inside a nested item’s content is mangled by the re-indentation — here the content is content;

    • :links-down: together with any inline option glues the first option onto the generated links line, so a Sphinx build ends up with a link named after your option’s text and without the option — here the link and the field are both set, separately;

    • an empty option name (((="open"))) generates a :: line that docutils rejects, and a Sphinx build drops the whole need — here the need is created and the nameless option reported;

    • a continuation line starting with : is stored with three extra spaces of indentation. That last one is not purely an artifact: a Sphinx build strips the indentation off every continuation line, and those three spaces are what put the options of a directive written across an item’s continuation lines back under that directive. ubCode keeps the indentation you wrote instead, measured from the item’s own column, which serves that case and the ones the three spaces cannot reach — an indented directive body, a nested list, an option that does not start with :. The two engines agree on the directive’s options and part company on its body: a Sphinx build strips that body to column 0, so a .. code-block:: written under an item renders there as an empty block followed by a stray paragraph, where ubCode renders the block you wrote. They differ in the STORED string too, and in the other direction — a Sphinx build’s needs.json KEEPS the three spaces ('.. image:: logo.png\n   :align: center'), while ubCode’s stores a plain line join without them.

  • A tab in an item’s indent is refused in a Markdown document, with needs.list2need_indent naming the tab, and the item is skipped. Markdown keeps the raw bytes, and the two markup languages resolve a tab to different widths — 4 columns in Markdown against reStructuredText’s 8 — so honouring it would put the item on a level you never wrote and on which the two hosts disagree. A Sphinx build creates no needs in a Markdown document at all, so there is nothing to be incompatible with.

    In reStructuredText there is no tab to refuse, because neither engine ever sees one: docutils expands tabs before the directive’s content is read and ubCode expands them the same way. A tab-indented item therefore lands on whatever column it expands to and is then handled like any other indentation — which may still skip it, for the ordinary reason that the column is odd. `` <tab>* item`` becomes an indent of 5, so needs.list2need_indent reports “must be a multiple of 2 (found 5)” and skips the item, alongside ubCode’s pre-existing source.tab_in_line lint. Both engines read the same level out of the same bytes, so this is parity rather than a divergence — at docutils’ default tab width of 8. A project that sets tab_width: 4 in a docutils.conf changes what a Sphinx build reads and not what ubCode reads; ubCode’s expansion is fixed at 8, so a tab-indented item can land on a different level in the two engines under that setting.

Traced needs (src-trace)

You write a .. src-trace:: naming a codelinks project, optionally scoped to one :file: or :directory:, to bring the needs written as one-line comment markers in your source files into the documentation. This is a Sphinx-Codelinks directive rather than a Sphinx-Needs one, so the comparison below is against that extension.

A Sphinx-Codelinks build runs the analysis when it reaches the directive and adds one need per marker at that position.

The preview and ubc build html run the analysis when they index the project and draw one need card per traced need at the directive’s position. What a reader sees is the same shape; the differences worth knowing:

  • The cards are ordered by need id, ascending. A Sphinx-Codelinks build orders them by the marker’s line number across all files, so a marker on line 1 of zebra.py comes before one on line 40 of alpha.py, and adding a file re-interleaves every other file’s cards. Ordering by id is stable under adding a file and moving a marker, and it is the order a reader scanning for an id wants; it is also the one rule ubCode applies to every set of generated cards, needimport’s included.

  • The source URL rows are plain text. With set_local_url or set_remote_url on, both engines put the URL in a field and show it as a metadata row. Sphinx-Codelinks also registers a needs_string_links rule for that field itself, so its row is clickable with no configuration; ubCode does not yet, so its row is the URL as text. For remote-url you can add the rule yourself — Making the source URLs clickable has it, an identity template because ubCode stores the fully resolved URL in the field. For local-url there is no rule that works: it is a file:// URL, and ubCode’s string links render only https:, http: and mailto: targets. The next bullet is why that gap is not closeable by configuration.

  • local-url points at the file on your disk. ubCode writes a file:// URL to the real source file. A Sphinx-Codelinks build copies each traced source file into the output tree, generates a syntax-highlighted page for it, and points local-url at that page with a line anchor — so its link works for a reader who has only the published site, and ubCode’s works for a reader who has the checkout.

  • A marker’s git commit is read when the file is analysed, not when the site is built. remote_url_pattern’s {commit} is substituted into the stored field during the codelinks pass, and that pass re-runs only for files whose modification time changed — so committing without editing a traced file leaves the previous commit’s SHA in the URL until something in that file changes or you build with --no-cache.

  • Only one directive draws a card for a given need. If two .. src-trace:: sites cover the same marker, the one on the earliest page by path (and earliest on that page) draws the card and the other renders an empty container, with a needs.duplicate diagnostic naming both. A Sphinx-Codelinks build keeps the site read FIRST, and for the second warns codelinks.duplicate_need at the marker’s line, naming the document that already created the need (it is not created again).

  • Two markers with one id: the earlier line wins in both tools, one-line and multi-line needs alike. ubCode reports needs.duplicate naming both sites; a Sphinx-Codelinks build reports codelinks.duplicate_need at the skipped marker, naming its kind. A need directive with the same id always wins in ubCode, where in a Sphinx build the first one read wins, so a multi-line need beats a .. req:: written later on the same page.

With get_multiline_needs on, both engines turn a multi-line need into a need with its body; the differences:

  • A body is restricted in ubCode. It cannot read files (include, literalinclude, csv-table with :file:, raw), its images and other assets are not resolved, and a need directive written in it is reported and not executed. A Sphinx-Codelinks build refuses nothing yet: a page-relative include in a body is read, and a .. req:: in a body creates a second need, recorded against the page.

  • A body does not see the page. ubCode parses it on its own — no page substitutions, no rst_prolog — so a block reads the same on every page that traces it. A Sphinx-Codelinks build parses a reStructuredText body on a reStructuredText page inside that page.

  • A label in a body is a page label in both, but ubCode numbers the page first. A Sphinx-Codelinks build parses the body inside the page, in document order, so a body before a section can take the section’s anchor and a body’s footnotes shift the page’s own ids and footnote numbers. ubCode gives the page’s own ids first and the body’s after them: a body label whose name or anchor the page already has is reported (std.duplicate_target) and does not register. Sphinx reports a duplicate NAME and drops both (Duplicate explicit target name); a body label whose anchor a later section would take keeps the anchor there, and the section is renumbered (id4), with no warning. Two blocks on one page with one label: ubCode keeps the first in reading order — the src-trace directives in document order, the cards by need id within a directive — and Sphinx neither.

  • A docutils named reference (label_) from the page to a body label is still reported as unknown in ubCode, whose :ref:, {ref} and #label links reach it; in a Sphinx build it resolves.

  • A body’s footnotes are numbered on their own in ubCode, from [1] in each card; in a Sphinx build they continue the page’s numbering.

  • A hidden block’s labels register in ubCode, whose hidden card is drawn collapsed; a Sphinx build drops a hidden need, so a :ref: to its label does not resolve there.

  • A glossary term, std object or citation in a body registers in both, but when the page defines the same name, ubCode keeps the page’s and reports the body’s, where a Sphinx build keeps whichever comes later on the page; and a body term or object whose anchor a label, a section or a page term or object already takes is reported in ubCode, where a Sphinx build gives it a numbered anchor.

  • Every finding is at the source line, the directive beside it. A Sphinx build reports docutils’ and Sphinx’s own messages there too, but myst-parser’s name the page with the body’s line, and a value that does not convert is reported at the open line rather than at its option’s line.

  • A markup no parser claims falls back to the page’s markup in both, with a warning at the directive: once per markup per directive in both, matched against the suffix your [parse.parsers] include globs imply (case-insensitively, longest first) rather than Sphinx’s exact source_suffix key.

  • A diagram in a body is not drawn. A needuml or needarch written in a multi-line need’s body shows on the card as its source, with a build.directive_unhandled warning at its line, and is not stored in the need’s arch; written in an ordinary need directive it is drawn and stored. A Sphinx-Codelinks build stores it for the block too (an unkeyed diagram under diagram), and a needuml keyed diagram in the body stops the Sphinx build, where ubCode reports nothing about the key.

  • :template:, :pre_template:, :post_template: and :jinja_content: are accepted and do nothing, as on any need.

  • A hidden block draws its card collapsed, as a hidden need does in ubCode; a Sphinx build draws none.

Anchors on views and diagrams

One rule for the whole family, stated once here because it is identical for every directive it applies to.

A Sphinx-Needs build mints a positional id for each view — needgantt-<docname>-3, needflow-<docname>-1, and so on — and hangs a permalink off it. The number renumbers as soon as a chart is added or moved above it, so the deep link the page advertises silently retargets.

ubCode mints none. No positional id, no serial, and no you did not ask for. Instead, :name: is the anchor mechanism: give a view or a diagram a :name: and it gets a stable cross-reference target you can :ref:, on the same standard-target mechanism every :name:-bearing directive uses.

In a built site (ubc build html) a named directive renders

  • an id on its outermost element, normalised the same way every other :name: is (My Plan becomes my-plan) — which, for a name that normalises to a non-empty slug, is exactly the anchor ubCode publishes for it in objects.inv, so an intersphinx reference from another project lands on it too. See the empty-normalisation note below for the names that do not;

  • a permalink in its caption, when it renders one — a diagram with :caption:, a needgantt given an argument (its caption is drawn in every state, including the empty one), or a needflow given an argument that draws a diagram. A needflow renders its caption only in its drawn state, so a named one that is empty, failed or not yet hydrated keeps its id and shows no caption and no . A view with no argument has no caption at all, so it gets the id and no — which is what .. note:: and .. container:: do as well.

Exactly 44 directives accept :name: and register a target for it:

admonition, attention, caution, code, code-block, compound, container, csv-table, danger, digraph, div, dropdown, error, figure, graph, graphviz, hint, image, important, include, index, line-block, list-table, literalinclude, math, mermaid, needflow, needgantt, note, parsed-literal, plantuml, rubric, seealso, sidebar, sourcecode, tab-item, table, target-notes, tip, toctree, todo, topic, uml, warning.

Two of them deliberately publish nothing: a plain .. include:: and a :code: one. docutils only names the block its :literal: and :code: branches produce — the plain branch splices text and creates no element — so a :ref: to a plain include is an undefined label in a Sphinx build too, and ubCode matches that. The :literal: branch anchors, and so does literalinclude. :code: is not implemented in ubCode — it is reported as an unsupported option and the file is spliced as plain text — so there is no code block to name, and ubCode publishes no anchor for it where a Sphinx build does. toctree anchors its wrapper, as a Sphinx build does. target-notes accepts :name: here and anchors the block it renders; docutils’ own target-notes cannot carry the option at all. The index directive is the opposite case — its anchor is the only thing it renders at all. Extending :name: to the remaining views (needsequence, needtable, needpie, needbar, needlist) is tracked in useblocks/ubcode#2975. card and grid are not on the list, in either engine: sphinx-design declares no name option on them, and sphinx-build reports unknown option: "name".

Four further points belong to the same rule:

  • A :name: that normalises to nothing still gets an anchor — a numbered one. Under [parse] id_style = "docutils" the slug is built by transliterating what it can and then dropping every character that is not an ASCII letter or digit, so the rule is whether the slug comes out empty — not whether the name had ASCII in it. A purely numeric name (:name: 123) empties, because a leading run of digits is stripped as well; so, in practice, does every name written in a non-Latin script: Chinese, Japanese, Korean, Cyrillic, Greek, Arabic, Hebrew, Thai and so on. Some ASCII-free names do NOT empty, because the transliteration reaches them: :name: fi slugs to fi and :name: ß to sz. There is no readable slug to be had, so the element takes the page’s shared id1 / id2 / … counter instead, exactly as a Sphinx build does. The anchor is real: the page emits it, objects.inv publishes it, and :ref: reaches it, from this project and from another one through intersphinx. What you lose is the readable URL — #id1 says nothing about what it points at, and it moves if you add another such element earlier on the page. So an ASCII :name: with your own display text (:ref:`交付计划 <delivery-plan>`) is still the form you want — see the :ref: note below. By default (id_style = "github", see Parsing) a name in any script keeps its letters (:name: 交付计划 anchors at #交付计划), and only a name that loses every character — punctuation, or emoji without a variation selector — falls back to the numbered id1; an emoji written with U+FE0F (:name: ✔️) keeps that selector as its id — an invisible U+FE0F (#%EF%B8%8F in a URL), or a -1 / -2 serial of it.

  • A :ref: to a named view or diagram shows the label, not the caption. A Sphinx build substitutes the target’s caption text into a bare :ref:; ubCode never does, for any :name:-bearing directive — write :ref:`See the plan <my-plan>` for your own wording.

  • The editor preview emits no anchors at all — not for these directives, not for headings, not for figures. Anchors are a built-site surface; the preview has nothing to deep-link to.

  • An unnamed directive is unchanged: no id, no , nothing.

Smart quotes

ubCode educates prose the way a Sphinx build does — docutils’ own algorithm, for reStructuredText and Markdown alike, on by default, with Sphinx’s key names (see Smart quotes ([build] smartquotes)). The differences:

  • The conf.py is not read. A project that sets smartquotes = False (or smartquotes_action, or language) in conf.py gets ubCode’s default until it sets [build] smartquotes / smartquotes_action / [project] language in ubproject.toml as well.

  • A backslash escape never turns a quote. An escaped character is kept as written, and for the quotes beside it, it is simply the character it is: ("\*args") shows (“*args”) and a "\ *x*\ " b shows a “x” b, where a Sphinx build of a reStructuredText page shows (”*args”) and a “x“ b, because docutils lets its own escape markers decide the quotes next to them. An escaped quote, apostrophe, hyphen, full stop, backtick or backslash stays as written in text, in both syntaxes (a role’s explicit title loses its backslashes before quotes are decided, in a Sphinx build too, so :ref:`a \"b\" c <lbl>` shows a “b” c): \"x\" stays "x", which is CommonMark’s meaning of the escape; a Sphinx build curls it in a MyST page, because MyST-Parser drops the backslash before the transform runs. The reverse holds for an escaped backslash before a quote in a MyST page: c\\"x" keeps its backslash and curls the quote (c\”x”), as both builds do in reStructuredText, where a Sphinx build of a MyST page drops the backslash and keeps that quote straight (c"x”). In reStructuredText, text that spells one of docutils’ escape entities stays as written too: a &#34; b shows a &#34; b, where a Sphinx build shows a " b. A role’s content follows the rules of its syntax’s text. In reStructuredText it is unescaped once, as docutils does, so :menuselection:`C:\\Users --> Settings` shows C:\Users ‣ Settings in both builds, and an escaped quote, apostrophe, hyphen, full stop, backtick or backslash stays as written in every role, :guilabel:, :menuselection: and :abbr: included, where a Sphinx build curls an escaped quote in those three (:math: and :raw: keep their backslashes in both builds). In a MyST page the content of a prose role ({emphasis}, {strong}, {guilabel}, …) is unescaped as MyST text, so {strong}`p\*q` shows p*q, where a Sphinx build shows p\*q: MyST-Parser hands a role its content as written, and a Sphinx build drops only the backslash of an escaped quote, apostrophe, hyphen, full stop, backtick or backslash, in the roles whose quotes it educates, with smart quotes on. A code role’s content ({code}, {kbd}, {samp}, {command}, …) stays as written in a MyST page, as in a Sphinx build.

  • Quotes look past inline markup, line ends and paragraph breaks. ubCode reads a block of text as the text it renders as, so a quote beside emphasis, a reference or inline code sees the character on its other side: ("*x*") shows (“x”), where a Sphinx build shows (”x”), because docutils decides each piece of text without looking past its end. Inline code itself is never changed. A line break counts as the space it renders as, so a quote that ends a line after a space opens (He said " with hello" there. on the next line shows He said “ and hello” there.) in both syntaxes; a Sphinx build closes it in reStructuredText. The break between two paragraphs of one version note (versionadded, versionchanged and the like, which docutils reads as one block of text) counts as a space as well: a second paragraph that begins with a quote opens it, where a Sphinx build usually closes it.

  • MyST’s smartquotes extension uses docutils’ algorithm. markdown-it’s own rule pairs quotes, leaves an unmatched one straight and always uses English quote characters; ubCode educates a document whose parser lists the extension with the same rule every document gets, in the quote characters of [project] language (English ones for a language smart quotes do not support).

  • The target’s title in an implicit reference is educated on its own. An implicit :ref:, :doc: or :term: shows the target’s title educated as a block of its own, with a space before it, from the title as it is stored — its text, without its escapes or its markup; Sphinx shows the title as it was educated where it was written. The two differ when the title starts or ends with a quote that its surrounding text would have decided differently, holds an inline literal with quotes, dashes or ellipses in it, or holds an escaped quote, apostrophe, hyphen, full stop, backtick or backslash (a title Esc \"x\" title shows Esc “x” title in the link, Esc "x" title in Sphinx). This is the one place a backslash does not keep a character as written: the title itself keeps it, a link’s text to that title does not.

  • Two kinds of reference text are educated where Sphinx keeps them raw. An implicit :any: that resolves to a glossary term shows the term educated (“quoted” term), where Sphinx keeps the role’s own text, raw and in code style; and a Markdown link without text to a heading’s slug, [](#a-heading) or [](other.md#a-heading), shows the heading educated, where Sphinx shows the heading text as MyST-Parser stored it when parsing, raw. An :any: to a label or a document, and a link to a document or a label, show the educated title in both.

  • Inventory display names stay as written. Sphinx’s objects.inv carries the educated section and document titles; ubCode’s carries the text as written.

  • An unsupported language, or an unknown action letter, is reported. For a [project] language with no quote characters of its own, both engines educate nothing; ubCode also says so, as config.smartquotes_unsupported_language, where Sphinx is silent. Likewise both ignore a letter smartquotes_action does not know and apply the others, and ubCode reports it as config.invalid_smartquotes_action.

  • An empty smartquotes_action means the default. smartquotes_action = "" reads as unset, so quotes, dashes and ellipses are educated ("qDe"); Sphinx’s smartquotes_action = '' educates nothing. "0" (or smartquotes = false) is the way to say nothing in ubCode.

  • An extracted need’s own text is raw, as sphinx-needs renders it. sphinx-needs renders a needextract copy from one taken before any transform ran, so the copy’s text stays raw there even though the same content is educated in the need’s own box; a :ref: or :doc: inside it still shows the educated title, because sphinx-needs resolves the copy’s references afterwards into the target’s title, while a :term: keeps the copy’s own text, raw. ubCode follows all three, and would drop the rule if sphinx-needs changed it.

  • The plain-text form of a French title drops its no-break spaces. fr pads its guillemets with a no-break space (« x »). The page itself keeps it, but the page <title>, the search index’s titles and the PDF’s cover and running header are derived from the title’s plain-text form, which folds every kind of space into an ordinary one; Sphinx keeps the no-break space there.

  • Word characters are Python 3.12’s. Which characters count as letters and digits beside a quote follows Python 3.12’s regular expressions (Unicode 15.0), as docutils’ do under that Python; a character added to Unicode later is not counted, where a Sphinx build on a newer Python may count it.

  • Some Sphinx and docutils knobs are not supported: per-block language-xx classes, a docutils.conf smart_quotes setting, the alt quote sets and smartquotes_excludes. ubCode has no man or text builder to exclude, and the language half of smartquotes_excludes is [project] language.

Document structure

Document topmatter

You write a document that opens with a field list — bibliographic fields such as :Author:, :Version: or :Date:, or arbitrary ones such as :orphan:, :tocdepth: 2 or :sd_hide_title: — or, in Markdown, a YAML front-matter block.

The preview shows nothing for it: the block is consumed into that document’s metadata, exactly as a Sphinx build does. A :dedication: or :abstract: field is the exception — each renders as a titled topic block, again matching Sphinx, in both reStructuredText and Markdown. A field list anywhere else — after the document title, or mid-document — is ordinary markup and still renders as a field list, also as in Sphinx, and so is a field list at the start of an imported need’s content (that content is nested into the importing document, never read as a document of its own).

A Sphinx build does the same. It also consumes some of that metadata: :orphan: is honoured (a document marked orphan is exempt from the “not included in any toctree” warning), while tocdepth, the search-exclusion keys, and theme keys such as sd_hide_title and hide-toc shape the built page.

Status — topmatter extraction is implemented for both reStructuredText and Markdown, :orphan: is wired through to the toctree check, and ubc build html consumes the other four keys: sd_hide_title hides the page’s first title (it stays in the page, and its anchor, the browser title and the search index are all unchanged), hide-toc omits the “on this page” panel, tocdepth limits how deep that panel goes, and nosearch (or no-search) keeps the page’s prose out of the search index. All four test for the presence of the key, so hide-toc: false in Markdown front matter still hides the panel — as it does in Sphinx. A need defined on a nosearch page is still searchable as a need: needs are data objects, and Sphinx keeps a page’s domain objects in its own search index for exactly the same reason — only the prose goes. Two consumption divergences are known: tocdepth shapes the page’s own panel only, not the site navigation, which lists document titles and so has no section depth to cut; and tocdepth: 1 on a page with several top-level headings lists them all here, where Sphinx’s furo theme hides the panel entirely. The live preview hides an sd_hide_title title just as the build does. Two more follow from how Markdown pages that skip a heading level are modelled (a page whose first heading is not #): sd_hide_title does nothing there, and tocdepth counts one level deeper than Sphinx does. Three smaller extraction divergences are known: field names are matched against the English bibliographic names only; RCS keyword substitution ($Date: … $) is not applied; an escaped separator in :authors: (Doe\, Jane) still splits, because escapes are resolved before the metadata is extracted. In Markdown, a dedication/abstract value is shown as plain text — Sphinx re-renders it as inline Markdown, so emphasis inside one is not formatted.

rst_prolog / rst_epilog

You write content in rst_prolog or rst_epilog (in your parsing configuration).

The preview shows the prologue and epilogue spliced as their own blocks at the very start and end of each RST document, at the document root — not merged into the document’s section structure. A prologue that begins with a heading never swallows the rest of the document. State set there, such as a default role, still applies to the whole document. A malformed prolog is silently not shown.

A Sphinx build appends the epilogue text inside the document’s trailing section, and a prologue that opens a section can absorb the whole document.

Status — the splice is structural rather than a raw text concatenation. Indexing and diagnostics are unaffected by where the splice lands; the background index splices the prologue and epilogue too, so state and substitutions defined there apply to both the preview and the index.

toctree navigation

.. toctree:: renders as a nested list of links to the listed documents, resolved from the toctree model of the last background indexing pass — like need cards, it refreshes after each pass completes, not on every keystroke. Display options (:maxdepth:, :titlesonly:, :includehidden:) shape the rendered list, and explicit Title <doc> entries keep their explicit titles. A Sphinx build additionally weaves every toctree into site-wide navigation (the sidebar and prev/next links of your theme); the preview shows only the in-page list.

An entry naming a document with no title of its own is not listed at all — neither here nor in a built site’s sidebar — and the project reports toctree.no_title, as a Sphinx build does for the same page. A document has no title when it has no heading — a .. title:: sets the browser-tab title only and does not count (see A page can override its browser title), which is why a page carrying one is still reported and still unlisted, on both engines. The exception is a document whose only content is a toctree: it is reported by neither engine and still keeps its link here, though a Sphinx build renders no entry for it at all and lists its children in its place. A document with no heading and no .. title:: is still built, still reachable, and still sits in the previous/next chain, where it reads <no title> — the same placeholder a Sphinx build shows for it, and the same one its own browser-tab title, its :doc: link text, its entry in the site search and its objects.inv entry now carry. A page that does carry a .. title:: keeps that title in its browser tab and its search entry on both engines; its other surfaces differ between the engines, as A page can override its browser title explains. That applies to a root document with no heading too: nothing is dropped or reported for it (a root is never a toctree entry), but its browser tab, its neighbours’ previous/next labels, its search entry and its inventory row all read <no title>. Silencing the warning with lint configuration does not put the link back: it is the entry that is dropped, not the message.

One shape is reported here and not by a Sphinx build: a page whose only content is an object description, such as a lone .. confval::. A Sphinx build counts that description as the page’s table-of-contents entry and stays silent; ubCode has no equivalent at that stage and reports it. So a project that builds warning-free under sphinx-build -W can still meet this warning here.

The toctree of a page no toctree reaches, such as an :orphan: hub, is still read, so the pages it lists are not reported toctree.not_included, as in a Sphinx build; but a mistake in its entries — a document that does not exist, a duplicate entry, a :glob: that matches nothing — or a circular reference between such pages is reported by a Sphinx build (the circular reference by its html build) and not here.

Page-local table of contents (contents)

.. contents:: ({contents} in Markdown) renders the page’s own headings as a nested list, as a Sphinx build does: a bare directive lists the page’s H1 with everything beneath it, :local: roots the list at the section it is written in, :depth: cuts it, :class: adds classes to the <nav>, and :backlinks: — which defaults to entry in a Sphinx build too — turns every listed heading into a link back to its entry. A table of contents that finds no headings to list renders nothing at all, which is also what a Sphinx build does. In the editor preview the same list is drawn without links, because headings carry no anchors there.

Three differences from a Sphinx build:

  • An invalid option value is accepted silently and the option is treated as unset, where a Sphinx build stops with a directive error: :depth: notanumber means no depth limit, :backlinks: bogus means the entry default, a :name: is ignored (the directive has no such option upstream), and body content — which the directive does not take — is dropped. The same applies to a contents written inside a body element such as a .. note::: a Sphinx build errors, and ubCode renders the list where you wrote it.

  • A heading that contains a cross-reference is listed with the reference flattened to its text — as a Sphinx build lists it — but keeps a plain heading, where a Sphinx build also links it back to the table of contents. The reason is that linking it would nest one link inside another; a Sphinx build only avoids that by accident of timing (its reference is still an unresolved placeholder when the table of contents is generated, so the guard against nesting does not see it).

  • The anchors a page mints are assigned in document order as the page is written out, so a .. contents:: that precedes a section of the same name takes that name and, under id_style = "docutils", the section falls through to an idN; a Sphinx build assigns section anchors first and the table of contents afterwards, so it is the section that keeps the name. (The exact idN a displaced section lands on depends on how many anchors the page has already minted.) It only shows up when a heading’s slug collides with the table of contents’ own — or, on a page with two headings of the same text, in which id number the duplicate ends up with. By default (id_style = "github") the displaced section takes the -1 serial of its own slug rather than an idN.

Footnotes and citations

Footnotes are numbered, linked and back-linked on both surfaces — the built site and the live preview alike.

Auto footnotes ([#]_) take their numbers in definition order, each the next integer that is not already a footnote or citation label on the page, so a document mixing [1]_ and [3]_ with three [#]_ definitions numbers those 2, 4 and 5. (An earlier version of this page said 2, 5 and 6, which counted a fourth, named definition that is not in the example; a Sphinx build numbers this one 2, 4, 5, and so does ubCode.) A named auto ([#name]_) resolves by name however many times it is referenced; an unnamed one is paired with the autonumbered references in reference order, not with whichever definition happens to be nearest. An unnamed auto can also be referenced by the number it received — write [2]_ and it links to whichever [#] ended up as 2 — though that reference adds no back-link, so a definition reached only that way is still reported as unreferenced. (A named auto’s number is not referenceable this way; it keeps its own name. A named auto whose name is a digit string, .. [#1], is the exception: a plain [1]_ does reach it, and does add a back-link.) Symbol footnotes ([*]_) walk * † ‡ § # ♠ ♥ ♦ ♣ and then repeat the symbol, so the eleventh is **. Every reference links to its definition and every definition links back to each of its references, whichever of the two the page happens to write first.

A footnote written inside content that a needextract, needimport or needreport directive brings onto the page is numbered within that card, starting again at 1 as it does on the page it was written on, and its link stays inside the card — so a page showing one need twice shows that need’s footnote as [1] in both copies, and the page’s own footnotes are numbered as if the cards were not there.

Where two definitions claim the same name, neither is usable: both are still numbered and rendered, but nothing resolves to either, a reference to that name reports duplicate target name, and neither definition is reported unreferenced. That is docutils’ own rule, and it applies to reStructuredText. Markdown never reaches it — MyST catches a repeated [^label]: in its own pass and discards the second definition, so the reference resolves to the first. ubCode resolves the reference to the first one too, but keeps and renders the second (see the differences below).

A citation reference ([CIT2002]_) resolves across documents: a citation label is a standard-domain label, so a bibliography page’s definitions are reachable from every page that cites them, with goto-definition and find-references in the editor. A citation defined on the same page as the reference also gets a backlink, which is Sphinx’s own asymmetry — a cross-page reference never produces one. Inside an extracted or imported card the same rule holds within the card: a reference in the card back-links a definition in the card’s own content, may reach a definition in the page’s own prose without back-linking it (the card shows another document’s text, so that link would not be the page author’s), and is never reached from the page’s prose.

Seven diagnostics come with the numbering:

  • std.footnote_unreferenced — a footnote definition nothing references.

  • std.citation_unreferenced — a citation definition no page in the project references. Reported once every page has been read, at the definition’s own line: a citation label is a project-wide name, so a definition on a bibliography page may be answered only by a reference on another page, and the question cannot be settled while reading one document.

  • ref.unknown_footnote — a [1]_ or [#name]_ matching no definition and no auto-assigned number.

  • ref.duplicate_footnote — a reference to a name two definitions claim.

  • ref.too_many_autonumbered / ref.too_many_symbol_refs — more references than definitions.

  • std.ref — a citation reference no page in the project defines.

Each reports the same situation a Sphinx build reports. Two differ in their text: std.ref’s wording, and the label std.citation_unreferenced quotes — both noted among the differences below. All seven are warnings. A Sphinx build grades the four docutils-derived ones (unknown target name, duplicate target name, and the two too many … references) as errors; ubCode has no error grade for reference diagnostics, so they arrive in the warning count and, under --deny warning, still fail the build.

In Markdown, footnote definitions are collected to the foot of the page and re-sorted by number, behind a horizontal rule — MyST’s myst_footnote_sort and myst_footnote_transition, spelled footnote_sort and footnote_transition under [parse.parsers.md] and both on by default. footnote_sort = false turns off the collection and the first-reference numbering, exactly as MyST’s does; footnote_transition = false keeps the collection and drops the rule. The rule ubCode writes is <hr class="footnotes" />; a Sphinx build writes <hr class="footnotes docutils" />, the extra class being one docutils adds to every transition and ubCode adds to none. The rule is omitted on a page that holds nothing but footnote definitions; ubCode also writes it when the page’s only other content is an included fragment that itself holds nothing but footnote definitions, or a front matter MyST consumes entirely (one whose only key is myst:), where a Sphinx build does not. A definition written inside a body excluded by a falsy only is dropped with that body here, where a Sphinx build still renders it at the foot of the page — it moves the definition out of the body before deciding to discard it — so the reference is left as a plain marker with no link. See Footnotes for the keys in full.

Ten differences from a Sphinx build remain:

  • In Markdown, a repeated [^label]: definition is kept and rendered, numbered after the first, and it draws two warnings — that the name is a duplicate, and that the second definition is unreferenced. MyST discards the second definition and warns once. The reference resolves to the first definition on both sides, so only the extra footnote and the extra warning differ. The collected block makes it more visible than it was: both definitions land in it, so the page shows two entries where a Sphinx build shows one.

  • A reference that resolves to nothing keeps its raw label and simply carries no link. A Sphinx build replaces it with the source text marked as problematic, inside a link to a system message it then deletes — a dead link in its own output. The diagnostic says what is wrong either way. The label is kept in the syntax it was written in, so in Markdown the marker still reads [^label] and in reStructuredText [#label]. In Markdown the visible text is a Sphinx build’s, except that a zero-padded label loses its padding here as it does everywhere else ([^07] shows [^7]); only the wrapper differs otherwise; in reStructuredText a Sphinx build shows the full source of the reference, trailing underscore included ([#label]_), so the wrapper and one character differ.

  • A zero-padded label loses its padding: [^01] in Markdown and .. [01] in reStructuredText both render 1, where a Sphinx build keeps the authored 01 on either syntax. The consequence is larger than a rendering loss, and an earlier version of this page described only the loss: 01 and 1 become the same label, so [01]_ silently resolves to a .. [1] definition, adds a second back-link to it, and suppresses the unknown target name finding a Sphinx build reports for it. It also shifts the numbering of every later auto footnote, the same way the non-ASCII-digit bullet below describes: a Sphinx build reserves the label string 007, so an auto may still take 7, where ubCode reserves the number 7 and its autos step over it. On [^007] followed by seven autos the last one is 7 upstream and 8 here.

  • A Markdown label written with non-ASCII digits ([^٣]) stays a name rather than becoming a manual number, and a name takes an auto number — so it consumes one, and every later auto footnote on the page shifts up by one. On [^٣], [^a], [^b] a Sphinx build numbers ٣, 1, 2 and ubCode numbers 1, 2, 3.

  • A citation reference the project cannot resolve reports std.ref (undefined label), where a Sphinx build words the same finding citation not found. Same severity, same suppression family. Both that message and std.citation_unreferenced name the label normalised — lower-cased, with runs of whitespace collapsed — so a .. [CIT2002] is reported as cit2002, where a Sphinx build echoes the case you wrote.

  • A :ref: role pointing at a citation counts as a reference to it, so that citation is never reported unreferenced. A Sphinx build counts bracket references only: it reports the citation as unreferenced even on a page carrying the role, and then fails to create the cross-reference at all. Counting it follows from a citation label being an ordinary standard-domain label here, which is what makes the role resolve in the first place. A hyperlink reference (cited_) counts on neither side — it is a different family of reference on both.

  • A citation defined in a file spliced in by .. include:: is reported at that file, on the definition’s own line. A Sphinx build names the including page instead, with a line number that indexes the fragment while the file name indexes the page, so the pair points at whatever the host happens to have on that line — or past its end. The finding fires either way; only the file named differs, and ubCode names the one goto-definition opens.

  • A citation reference written inside a body excluded by a falsy .. only:: expression does not count as a reference, so a citation referenced only from there is reported unreferenced. A Sphinx build registers the reference while reading the page and discards the body afterwards, so it stays silent. The mirror case diverges the other way: a definition inside an excluded body is invisible here, so a citation defined only there is never reported, where a Sphinx build registers the definition while reading the page and reports it.

  • A footnote reference written in a heading is dropped from every plain-text rendering of that heading — the page <title>, a parent’s toctree entry, the sidebar and prev/next labels, the std:doc display text in the published inventory, and the page title the search index stores — while the label is defined, which is what a Sphinx build does too. On an undefined label both builds keep the marker in all of them. The one exception is a heading whose whole text is a defined marker: ubCode falls back to the raw source and shows [#]_, where a Sphinx build shows the empty string, writes an empty std:doc display name and leaves the page out of its search index altogether. The page’s own .. contents:: listing now agrees with a Sphinx build’s on every shape: a defined reference is dropped, an undefined autonumbered [#]_ or symbol [*]_ past its supply is kept, and an undefined named or numbered one is dropped. The “On this page” panel keeps every undefined marker, as it always has. A citation reference is kept by both listings on both sides, but ubCode writes the label as a bare [LABEL] span where a Sphinx build wraps it in a link to the citation and gives it an id — which is also what stops the page emitting one id twice, whether or not the label resolves. The heading itself still shows the marker, and so does the heading text stored against each search result, where a Sphinx build keeps the label too. For a Markdown [^label] that matches a Sphinx build exactly; for a reStructuredText one it matches but for the trailing underscore, which ubCode never renders. The heading’s own anchor moves toward a Sphinx build’s for the same reason: the marker stops contributing text to the slug, so title-with-ref-x becomes title-with-ref, which is what a Sphinx build has always written.

  • A footnote finding whose reference or definition sits inside a file spliced in by .. include:: names that fragment and the site’s own line and column, with the host page carried alongside it as the included-from location — which is where a Sphinx build reports it too, and matches the citation family described above. ubCode names the line the text is written on under every clip — :start-line:, :start-after:, the two together, and with or without :end-line: / :end-before:. A Sphinx build does not, and the two engines differ in opposite directions. For reStructuredText docutils reports the line counted from the start of the slice: a construct on line 18 of a fragment included with :start-line: 10 is reported at line 8, a defect docutils carries in its own source as # TODO: if startline != 0, line numbers are wrong. For MyST a Sphinx build names one line below the line the text is on, whatever :start-line: says — measured constant for :start-line: 0, 1, 2 and 10 — so on a sliced {include} the two engines differ by exactly one, the same constant an unsliced one already differs by. Under :start-after: a Sphinx build’s MyST number is unusable: myst-parser adds the character position and length of the search string to a line number, so a reference on line 18 of one measured fragment is reported at line 62. None of this is specific to footnotes — it is the position every finding reported from spliced content carries.

Substitutions

|name| substitution references expand in the preview: your own .. |name| replace:: definitions, and the Sphinx specials |version|, |release|, and |today| (fed by the version, release, today, and today_fmt keys of the [project] table in ubproject.toml). A few differences from a Sphinx build remain:

  • An undefined or circular substitution keeps its literal |name| chip and raises a diagnostic; a Sphinx build reports these as errors in the build log instead.

  • Definitions whose expansion would grow without bound are cut off with an oversized diagnostic — a guard with no Sphinx equivalent (a Sphinx build can take unboundedly long on such input).

Glossaries

A reStructuredText .. glossary:: renders as a definition list of terms, each carrying a term-… anchor and a permalink, with :sorted: applied in the same way a Sphinx build applies it (case-insensitively, on each entry’s first term, keeping a shared definition together). An entry written without a definition is kept and its term registered, rendered with an empty definition, exactly as in a Sphinx build. Two differences from a Sphinx build remain, both about the anchor and the classifier:

  • A term’s classifier is kept, but nothing renders it — and there is no general index for it to sort. In Term : classifier — spaces required on both sides — the part after the separator is a classifier, and a Sphinx build uses it for one thing only: the sort key of that term’s entry in the general index. ubCode records it as the index key of the term’s entry, so Term : b and Term : z are distinguishable to ubCode, but ubCode builds no general index, so the key has nowhere to be shown. Not rendering it matches a Sphinx build, and neither engine puts it in the built site’s search (both drop the key from the search index). The classifier stays out of the term’s anchor and out of the :sorted: ordering on both engines. Only the first classifier is kept, exactly as a Sphinx build keeps only the first. (A classifier in an ordinary, non-glossary definition list is unaffected.) One consequence worth knowing: markup written inside a classifier — an inline target, or a cross-reference — is still parsed and indexed even though it renders nowhere, so an anchor defined there is unreachable and a broken reference there is still reported. A Sphinx build splits the classifier off before parsing and never sees it.

  • Term anchors are lowercased. ubCode derives a term’s anchor with the standard docutils identifier rules, so Zebra becomes #term-zebra; a Sphinx build uses its own variant of those rules for glossary terms and emits #term-Zebra, preserving capitals, dots and underscores. Consequences: Foo_bar and Foo.bar share one ubCode anchor (a Sphinx build keeps them apart), a term beginning with digits loses them (123abc → #term-abc), and two same-named terms on a page get -1, -2 suffixes where a Sphinx build numbers them term-0, term-1. This affects only the emitted identifier and the objects.inv URI: :term: references resolve case-insensitively on both sides, so a reference that resolves in a Sphinx build resolves in ubCode too.

One further, cosmetic difference:

  • The list carries no ``simple`` class. A Sphinx build emits <dl class="simple glossary">; ubCode emits <dl class="glossary">. The simple class is a general docutils behaviour for compact definition lists, which ubCode does not emit on any definition list.

A Markdown (MyST) {glossary} is read with the same body grammar, which is not Markdown’s definition-list syntax: a term goes on its own unindented line and its definition on the indented lines below it, with a blank line between entries, exactly as in reStructuredText. That matches a Sphinx build, which hands a {glossary} fence’s raw lines to the same directive whatever the file’s format, so the deflist extension makes no difference to a {glossary} whether it is enabled or not. The definition’s content is parsed as Markdown. Because the definition-list spelling is a plausible thing to type there, ubCode reports it — glossary.definition_as_term, on the : … line. That is a finding a Sphinx build does not raise, not a difference in what is registered: the line becomes a term on both engines, and both publish it in objects.inv.

One thing that does not carry across the two flavours: an indented glossary body. reStructuredText de-indents a directive’s content before the grammar sees it, so an extra level of indentation is harmless there; Markdown strips only the fence’s own indent, so an indented {glossary} body reads as definitions with no term and produces an empty glossary. ubCode matches each engine’s own behaviour here — a Sphinx build does the same on both sides — so this is a difference between the two flavours, not from Sphinx.

Index entries

.. index:: and the :index: role render the way a Sphinx build renders them — for the directive, nothing you can see: a Sphinx build skips the index node in its HTML writer, so all the directive leaves behind is an anchor (:name: when you give one, index-0, index-1, … otherwise — one sequence shared with the role, in document order). Where that anchor sits differs in a way no link notices: ubCode emits an empty <span> at the directive, while a Sphinx build usually attaches the same id to the next paragraph instead. Either way #index-0 lands on a real element, so references resolve on both. The :index: role renders its display text as plain prose behind the same kind of anchor: :index:`Widgets <pair: widget; hardware>` shows Widgets, and :index:`!widget` shows widget.

Four things follow that are worth knowing:

  • The editor preview shows the directive as a small chip listing the entries you wrote (and the :name:, when it changes the anchor), so an otherwise invisible construct is visible while you author it. The built site does not — matching a Sphinx build.

  • The entries are collected and checked, but there is still no general index page. ubCode parses each entry the way a Sphinx build parses it — the type prefixes (single:, pair:, double:, triple:, see:, seealso:), the leading ! that marks a main entry, and the comma shorthand — and files it against the anchor its construct minted. What a Sphinx build then does and ubCode does not is build a genindex.html page, so there is still no index page to link to. A malformed entry is reported: the wrong number of ;-separated parts, and one of the long-removed prefixes — module:, keyword:, operator:, object:, exception:, statement:, builtin:. A Sphinx build only warns for the first; the second raises an uncaught error that aborts the whole build. ubCode reports both as ordinary diagnostics and keeps building, which is a divergence in the forgiving direction. One quirk is upstream’s and is reproduced faithfully: the ! is read at the very start of an entry, before any type prefix, so ! single: widget is a main entry while single: !widget keeps the ! as part of the value and is not one.

  • A term that appears only in an index entry is findable by search. A Sphinx site’s search has a dedicated index-entry channel (indexentries in its searchindex.js, since Sphinx 5.2), and a ubCode-built site now carries the same one, matched the way a Sphinx site matches it: the whole query has to be a substring of the whole entry and at least half its length, scored by how much of the entry you typed. So a word you put in .. index:: and nowhere else in your prose is findable — which is much of what the directive is for. Main entries (a leading !, and every glossary term) rank with the ordinary results; the rest are listed after them. A term’s classifier is not searchable on either engine. In the editor, the same entries are searchable through the search_references MCP tool, as index:entry hits that tell you where a phrase is already indexed.

  • A ubCode-built objects.inv does not advertise the index pages ubCode does not build. The inventory a built site publishes for other projects to point intersphinx_mapping at carries a search label — search.html is generated — and no genindex, modindex or py-modindex label, because no such page is written. A Sphinx build advertises all four unconditionally: measured, turning html_use_index off removes genindex.html and changes its inventory not at all, and a prose-only project with no Python still advertises modindex and py-modindex. So in another project, :external:std:label:`genindex` (or a :ref: through an intersphinx mapping) that would have silently produced a link to a missing page against a Sphinx-built inventory instead reports an unresolved reference against a ubCode-built one. That is the intended trade: a warning you can act on rather than a 404 nobody detects. From inside your own project the same rule decides what links, and it is the rule rather than a fixed list: :ref:`search` renders a real link to the generated search page (with Search Page as its text, or your own title in the :ref:`Find things <search>` form), exactly as a Sphinx build does — while that page is being built. Set enabled = false under [build.html.search] and there is no search.html, so ubCode seeds nothing: the reference goes back to an inert placeholder chip and the search row leaves objects.inv with it. That is the same principle applied consistently, not a special case — ubCode advertises and links exactly the pages it writes. :ref:`genindex` renders a placeholder chip in every configuration, because there is no such page to link to at all. None of these warn, in any configuration. If your project defines its own search label, yours wins — for the reference and for the inventory row alike, as in a Sphinx build. ubc build targets follows the same rule, so the two commands’ inventories agree row for row.

One quirk is copied deliberately, because a Sphinx build has it too: in the :index: role’s no-title form the entry prefixes are not interpreted, so :index:`pair: a; b` puts the literal words pair: a; b into your prose and files them as a single single entry. Write the explicit-title form when you mean a typed entry (:index:`a and b <pair: a; b>`).

  • ubCode adds an advisory here that a Sphinx build does not have. Because the no-title form almost never means what it looks like, a no-title :index: whose text opens with one of the six entry-type keywords and a colon (single:, pair:, double:, triple:, see:, seealso:) reports index.role_type_prefix, naming the rewrite. It is info-level, so it never fails ubc check: a project a Sphinx build accepts must stay a project ubc check accepts, and Sphinx is silent on this construct. Silence it entirely with ignore = ["index.role_type_prefix"] under [lint]. The rendering and the filed entry are unchanged either way — it is advice about a construct, not a change to it.

The :any: role

The :any: role (and its MyST {any} twin) resolves a target without naming what kind of thing it is. ubCode walks the same ladder a Sphinx build does, in the same order — a document, then a project-wide label, then a glossary term, then any loaded intersphinx inventory — and where more than one of the first three matches it warns and links the first, exactly as Sphinx does:

more than one target found for 'any' cross-reference 'guide': could be :doc:`The Guide` (guide) or :std:ref:`A Guide Section` (handbook#guide)

A target nothing matches reports 'any' reference target not found: <target>. Both messages share one code, std.any, which mirrors Sphinx giving both one suppression key: silence them together with ignore = ["std.any"] under [lint].

default_role = "any" works, and is worth knowing about: under it every bare-backtick span in the project becomes an :any: reference, and behaves identically to writing the role out.

The same ladder serves sphinx-design’s cross-reference directives. A button-ref whose :ref-type: is any — which is the default, so this is every button that does not say otherwise — and a card whose :link-type: is any walk exactly the rungs above, exactly as a Sphinx build does. One rung is sensitive to whether you wrote your own link text: a label with no section title after it can only be reached by a link that supplies its own text, because there is no title to display. For a button that means giving it content; for a card it means giving it :link-alt:. Without one, the link reports std.any and stays a placeholder — the same answer a Sphinx build gives.

The ladder serves imported need content too. An :any: role or a sphinx-design any link written inside the content of a need brought in by needimport walks the same four rungs. The document rung is the one that depends on where the reference is written, and an imported need has no file of its own — so it is resolved against the page whose needimport produced the card, which is also the page the link is rendered on. The other three rungs are project-wide and answer the same wherever the need is imported. A reference nothing matches reports std.any like any other, anchored at the needimport directive and naming the need whose content it is in:

'any' reference target not found: nosuchtarget (in content of imported need 'REQ_1')

If your imported data carries any references you cannot fix at the source, ignore = ["std.any"] under [lint] silences the code project-wide, and per-file-ignores narrows it to the page that imports them.

Six differences from a Sphinx build:

  • A mixed-case glossary term resolves here, and does not in Sphinx. :any:`Zebra` links to a Zebra glossary entry in ubCode. A Sphinx build reports it as not found, because its :any: ladder looks the term up lowercased in a store that kept the case you wrote — its own :term: role has a second lookup that recovers from this, and its :any: ladder does not use it. ubCode’s term lookup normalises both sides, so the reference simply resolves. Nothing that resolves in a Sphinx build fails here as a result; you may see a reference work in ubCode that a Sphinx build warns about.

  • The ladder reaches fewer kinds of target, because fewer kinds exist. A Sphinx build’s :any: can also reach a command-line option, a grammar token, and every object of the Python, C, C++, JavaScript and reStructuredText domains. ubCode does not index those, so :any: cannot reach them — the same reason the :option: and :token: roles are not yet supported. Where a target space arrives, its :any: rung arrives with it, and there are two worked examples now: configuration values and environment variables are both indexed, so :any: reaches both — after the glossary-term rung and before the inventories. Where one name is both, the configuration value wins and the ambiguity warning lists it first, which is the order a Sphinx build uses too.

  • The generated-page labels are a chip, not a link — for the same names :ref: already chips. A Sphinx build links :any:`genindex`, :any:`modindex` and :any:`py-modindex` to the pages it generates for them. ubCode generates none of those pages, so there is nothing to link to and the reference renders its placeholder instead. It does not warn — the names are exempt from the dangling check on both roles, so a project using the sphinx-quickstart boilerplate still builds clean. :any:`search` is the exception and it does link, exactly as :ref:`search` does, because ubCode really does build that page (unless you set [build.html.search] enabled = false, in which case it is a silent placeholder too).

  • An ambiguous sphinx-design link that supplies its own text names its candidates more usefully here. When a button-ref with content, or a card link with :link-alt:, finds more than one target, ubCode lists each candidate by the title that candidate resolved to, exactly as it does for an :any: role written without its own text. A Sphinx build lists them by the link’s own text instead, so every candidate in the message reads the same and none of them names what actually collided. This applies only to a link that supplies its own text: for one that does not, a Sphinx build captions from the resolved titles too, and the two messages agree. The link itself is identical in both: same winner, same target, and one suppression key per engine — std.any here, ref.any in Sphinx.

  • Each candidate in the ambiguity message says where it resolved to. The parenthesis after each candidate — (guide), (handbook#guide) in the message above — is the page the link would go to, and, after the #, the target as you wrote it. That second part is deliberately the name from your own sources rather than the fragment the page actually emits, so you can search for it: the emitted id is the [parse] id_style slug of the name for a label (#my-target for my target) when that slug is still free — on a collision it is a serial, #my-target-1 under the default (github) and an opaque #id1 under docutils — and it always carries a term- prefix for a glossary term (#term-my-target). So do not read the parenthesis as a URL. A Sphinx build prints the candidate’s title alone. That is enough when the titles differ, and useless when they do not: if you wrote your own link text (:any:`The Guide <guide>`), a Sphinx build captions every candidate with that text, so the message reads could be :doc:`The Guide` or :std:ref:`The Guide` and names nothing that collided. The role prefix and the location together tell the candidates apart. A whole document and an anchored target differ in the location — including when they live on the same page, which a page name alone could not show. A label and a glossary term that share one name on one page print the same location, and there the role prefix (:std:ref: against :std:term:) is what separates them. A titleless document is captioned <no title>, which is what a Sphinx build calls it too — and so is its :doc: link text, everywhere else the document is named.

  • An ambiguous match across two intersphinx inventories is reported here, and a Sphinx build is silent about it. If a target exists in nothing local but in more than one loaded inventory, ubCode links the same one a Sphinx build does and adds an intersphinx.ambiguous note saying which project won and which also matched:

    multiple inventory matches for 'shared': took 'zulu' (std:label), also matched by 'alpha' (std:label)
    

    It is info-level, so it never fails a build: a project a Sphinx build accepts stays a project ubc build html and ubc check accept. It is worth saying because of how the winner is chosen: the inventories are merged in project-name order and the alphabetically last one wins, so adding an inventory whose key sorts after an existing one silently moves every colliding link — and renaming a project key does the same. The order the projects appear in ubproject.toml has no effect at all, on either engine. To pin a target to one project, qualify it: :ref:`zulu:shared` or :external+zulu:ref:`shared`. See the lint reference for how to silence it. Four names are exempt on :ref: and :any: sites and carry no notice there — genindex, modindex, py-modindex and search — because every generated objects.inv ships them, so any two inventories collide on them before you write a line. A Sphinx build resolves those four against its own generated pages; ubCode builds none of them except search, so such a reference does follow the name-order winner out to somebody else’s site, silently. The notice is therefore not exhaustive. An explicit external form is not exempt: :external:ref:`genindex` asks for an inventory answer by name, and reports the collision like any other. The same collision reached through a Markdown link is reported at warning level instead — see Markdown link cross-references for why the two halves differ.

Configuration values

.. confval:: documents a configuration value, and :confval: links to one. Both work, on reStructuredText and Markdown pages alike, and what a page renders matches what a Sphinx build renders. Seven things below are worth knowing: four genuine differences (one of them in ubCode’s favour), and three places where the behaviour matches a Sphinx build in a way you might not expect it to.

  • The lookup is exact — and that is parity, not strictness. :confval:`Needs_Types` finds a value defined as Needs_Types and nothing else. A differently-cased spelling does not resolve, and neither does one carrying a leading ~: the tilde is part of the name you asked for. This is worth stating because :term: is not like this — a glossary term resolves case-insensitively — and a Sphinx build draws the same distinction between the two roles for the same reason.

  • A value defined twice is reported, and the last definition wins. Two .. confval:: directives for one name raise std.duplicate_confval at the second one, naming the first, and the link goes to the second — matching a Sphinx build, which warns and then overwrites. This holds whether the two definitions are in different files or in one file, and every surface agrees on it: the link’s address, the entry in objects.inv, and where “go to definition” takes you in the editor. Silence it with ignore = ["std.duplicate_confval"] under [lint]; a Sphinx build’s own copy of this warning carries no category, so its suppress_warnings cannot reach it.

  • A cross-reference inside :type: or :default: renders as a link. A Sphinx build crashes on this — it aborts with an AssertionError from its HTML writer — so there is no output to match and ubCode renders the sensible thing. Emphasis, literals and plain text behave identically in both.

  • The value is not listed in the page’s contents, and :no-contents-entry: therefore does nothing. A Sphinx build adds each configuration value to its own navigation tree (though not to a .. contents:: directive, which lists neither engine’s values). ubCode’s navigation lists sections only, so the flag has nothing to suppress. It is still accepted, so a page written for a Sphinx build needs no edits.

  • A link to a value is styled with ubCode’s own class. A resolved :confval: renders the name in monospace inside a link, as a Sphinx build does, but the link carries class="ub-xref" — the single class every ubCode cross-reference carries — where a Sphinx build writes class="reference internal" and gives the inner <code> the classes xref std std-confval docutils literal notranslate. Only a stylesheet written against Sphinx’s class names is affected; the address, the text and the monospace are the same.

  • A repeated signature line still appears — and that is parity, not a leak. Writing the same name twice in one .. confval:: argument renders both lines, and only the first gets an anchor and a permalink. The same holds for two spellings of one name (a  b and a b): both lines appear, each with its own spacing, and one of them is the target. A Sphinx build does exactly this — it de-duplicates the target, never the signature — so a page written for one renders the same here.

  • A duplicated anchor is numbered differently. Two values with one name on one page get #confval-name and #confval-name-1 here, where a Sphinx build writes #confval-name and #confval-0. A reference links to the second definition in both engines — the one the duplicate warning is about — so what differs is only that anchor’s spelling: #confval-name-1 here, #confval-0 there.

:no-index: behaves as a Sphinx build’s does, and so does :no-index-entry:, which is accepted and has no effect on a configuration value in either engine. :no-typesetting: also behaves as a Sphinx build’s does in the way that matters — the value renders nothing, its anchor survives, and :confval: still links to it — but the anchor sits on a different element: an empty <span> of its own here, and the element that follows the directive in a Sphinx build. No link’s address changes either way.

Environment variables

.. envvar:: documents an environment variable, and :envvar: links to one. Both work, on reStructuredText and Markdown pages alike, and what a page renders matches what a Sphinx build renders. Almost everything true of configuration values above is true here too — the lookup is exact, a value defined twice is reported and the last definition wins, the anchor keeps capitals and dots, a repeated signature line still appears, and a duplicated anchor is numbered #envvar-name-1 where a Sphinx build writes #envvar-0. Four things are worth knowing that are not shared with configuration values.

  • A broken :envvar: link is not reported — and that is a match, not an oversight. Writing :envvar:`HOEM` for a variable called HOME produces no warning at all, at any level. A Sphinx build does the same: its :envvar: role is the one member of this family registered without warn-on-dangling, so a missing environment variable is silent there too unless you run it in nitpicky mode, which ubCode has no equivalent of. :confval: is the contrast — a broken one does raise std.confval — so the two roles genuinely differ here, in both engines. What you do get is a visible marker: the reference renders as a small bordered chip carrying the role name and the text you wrote, where a Sphinx build renders it as ordinary monospace text. So a mistyped variable is visible here and invisible there, and neither engine writes a warning about it. The chip shows your text exactly as you typed it, which is also visible on the deliberately unlinked form: :envvar:`!HOME` shows !HOME here, where a Sphinx build strips the ! and shows HOME.

  • A link to a variable adds an entry to the index — twice. Every :envvar: reference (including a broken one) contributes an invisible anchor at its own position and two index entries: the variable’s name on its own, and the name nested under environment variable. A definition contributes only the nested one. This is a Sphinx build’s behaviour exactly, and it is the reason a reference is searchable and not only a link. Two consequences are worth knowing. The anchor is numbered from the same counter the :index: role and the .. index:: directive use, so adding or removing an :envvar: reference changes the anchor numbers of the index markers after it on the same page — again matching a Sphinx build. And the ! form is exempt: :envvar:`!HOME` contributes no anchor and no entries, so it does not disturb the numbering.

  • The value indexed is the target, never your link text. :envvar:`the home directory <HOME>` files HOME, not the home directory, and a run of spaces inside a name is collapsed to one before it is filed. A Sphinx build files the same string.

  • :type: and :default: are not accepted on .. envvar:: — and ubCode ignores them where a Sphinx build refuses the whole directive. A Sphinx build rejects an option the directive does not declare: it reports ERROR: Error in "envvar" directive: and nothing at all is produced for that block — no entry on the page, no index entry, and no target for :envvar: to link to. ubCode drops the unrecognised option in silence and then builds the block as though it had not been written, so the variable is documented, indexed, linkable and exported as usual. Neither engine renders it as a field of the variable, so nothing is misrendered — but the same source gives you a documented variable here and a missing one there, which matters if you build with both. This leniency is how ubCode treats an unrecognised option on most directives; the Sphinx-Needs ones, .. need:: and .. list2need:: among them, report it instead.

A duplicate definition raises std.duplicate_envvar, its own code rather than the configuration-value one, so silencing one does not silence the other: put ignore = ["std.duplicate_envvar"] under [lint]. A Sphinx build’s own copy of this warning carries no category, so its suppress_warnings cannot reach it.

A target stacked above an external target

An empty explicit target written directly above an external or indirect one inherits that target’s destination:

.. _upper:

.. _lower: https://example.com/lower

upper_ and lower_ both link to https://example.com/lower, on both engines — the upper target has been consumed, and neither engine gives it an anchor of its own.

So it is not a cross-reference target either. ubc build html publishes no objects.inv entry for upper and :ref:`upper` is reported as undefined label (std.ref) and rendered as plain text, which is what sphinx-build does with the same source.

Note

This changed in ubCode 0.37.0. Earlier versions registered upper as an ordinary target: objects.inv carried page.html#upper, and :ref:`upper` rendered as a link to an anchor the page has never emitted. If a project relied on that link, the build now reports std.ref for it — give the element its own target below the external one, or silence the code with ignore = ["std.ref"] under [lint] while you migrate.

One sub-case is stricter here than in a Sphinx build. Where the lower target sits inside a directive Sphinx failed to parse, a Sphinx build still publishes an inventory entry for the upper name — an anchor its own page does not emit — while ubCode publishes nothing.

Cross-references in titles

A cross-reference written inside a title carries its own advisory. A title is not shown only in the page’s own heading: it is re-used by the sidebar navigation, the prev/next footer, an in-page toctree entry and the page’s contents list, and every one of those is already a link. An anchor cannot nest inside an anchor, so on those surfaces the reference is shown as plain text — the same shape a Sphinx build with a standard theme produces. In the bare form (Using :ref:`target` here) a document title’s navigation entry also shows the raw target name instead of the target’s title, because the display text is not resolved at that point.

  • ubCode adds an advisory here that a Sphinx build does not have. A cross-reference role (:ref:, :doc:, :term:, :external:…) or a MyST link written in a page or section title reports title.cross_reference, naming the surfaces it affects and the rewrite: move the reference into the body, or — for a role — use the explicit-text form (:ref:`text <target>`). A MyST link is told only to move the reference: once the link is parsed its destination as written is no longer recoverable, so a spelled-out form could point somewhere other than the author wrote. It is info-level, so it never fails ubc check: a project a Sphinx build accepts must stay a project ubc check accepts, and Sphinx is silent on this construct. Silence it with ignore = ["title.cross_reference"] — or the whole family, ignore = ["title.*"] — under [lint]. The rendering is unchanged either way; it is advice about a construct, not a change to it. Need roles (:need: and :need_part:) are deliberately excluded: sphinx-needs supports a need reference in a heading and keeps a regression test for it, so ubCode does not second-guess it.

Directives that render nothing

Six directives are recognised, accepted, and deliberately contribute nothing to the page body. That is not a gap in every case — for most of them it is what a Sphinx HTML build produces too:

sectionauthor / moduleauthor / codeauthor

A Sphinx build renders these only when show_authors is switched on, and it defaults to off, so a default-configured build shows nothing either. ubCode has no show_authors equivalent yet, so the author line cannot be turned on — the one difference, and the reason to keep writing them is the metadata, not the output.

header / footer

A Sphinx HTML build drops these entirely: docutils moves their content out of the page body into separate writer parts, and Sphinx’s HTML builder only ever emits the body. ubCode matches that exactly. Other docutils writers (LaTeX, manpage) do render them, so this is specific to HTML output rather than a general limitation.

meta

Nothing in the body — which is what a Sphinx build produces too — and the tags themselves go where they belong, in the page’s <head>. See Page <head> meta tags for what is written and how it is checked.

Before this, all six printed a framed placeholder box into the page, so authoring metadata appeared in the reader’s prose. That is what changed.

A page can override its browser title

.. title:: Some Other Title (and the MyST {title} fence) 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 page’s table-of-contents caption and the objects.inv export all keep showing your H1. That two-channel split is Sphinx’s own, and ubCode reproduces it.

The directive renders nothing into the page — before this it printed a framed placeholder box, which is markup a Sphinx build never produces.

Details worth knowing, all matching a Sphinx build:

  • The last one wins. Two .. title:: directives on a page leave the second in the tab, with no warning.

  • Position does not matter. It can sit at the bottom of the file.

  • The text is taken literally. .. title:: *not emphasis* puts the asterisks in the tab — nothing about the argument is parsed as markup.

  • An included file’s ``.. title::`` sets the including page’s title.

  • Writing it with no argument reports block.directive_no_argument and the page keeps its heading — which is also what a Sphinx build does.

Three things it is not, or does differently:

  • MyST front matter title: is a different feature — upstream turns it into an H1 (only when myst_title_to_header is on), so it renames the page everywhere rather than only in the tab. ubCode does not implement it.

  • Giving the directive a body reports block.directive_content_not_permitted and the title still applies. A Sphinx build is harsher: its content gate replaces the whole directive with an error before the title is ever read, so the override does not apply there. ubCode keeps the argument — the forgiving direction.

  • On a page with no heading at all, a Sphinx build lets the override leak into the heading channel: objects.inv and the neighbouring pages’ <link rel="prev"> / <link rel="next"> titles carry it. ubCode keeps the channels separate there too, so those surfaces read <no title> — the page really has no heading, and the tab title is a different channel saying so. Such a page gets no table-of-contents link on either side, and both report it (toctree.no_title here), because the warning is about the missing heading and not about the tab title; the previous/next chain still links to it.

Page <head> meta tags

.. meta:: (reStructuredText and MyST alike) and MyST’s html_meta: front matter both write <meta> tags into the built page’s <head>, the way a Sphinx build does: each tag on its own line, after <meta name="generator"> and before <title>, with its attributes in alphabetical order and a lang= doubled into xml:lang. A page that writes none is byte-identical to one built before the feature existed.

Two behaviours are worth knowing because they look like bugs and are not:

  • Nothing is de-duplicated. Two description tags on one page stay two tags, and a viewport you write yourself joins the one ubCode already emits. That is what a Sphinx build does with the same source.

  • Order can surprise you. .. meta:: blocks written above everything else on the page come out in reverse order, while a block written below some content appends in the order you wrote it. That is a docutils quirk, faithfully reproduced — merge the blocks into one directive if the order matters to you, because within a single directive the fields keep their order.

One byte in the written tag differs from a Sphinx build on purpose: if you give an attribute a name containing ", <, > or &, ubCode escapes it, where docutils writes the name through unchanged. Since any attribute name is accepted, an unescaped one could close the tag and put arbitrary markup into your page’s <head>. Attribute values are escaped by both.

A ``{meta}`` fence on a MyST page does more than upstream, not less. myst-parser 5.1.0 cannot run the directive at all — a Sphinx build of the same page reports Directive 'meta' cannot be mocked and emits none of its tags — so ubCode supporting it is a deliberate superset, not a port. That also means its ordering against html_meta: front matter is ubCode’s own choice rather than a reproduction: front matter comes first and a fence appends after it, so the page’s tags come out in the order the file reads. The html_meta: front matter itself is ordinary parity.

Where ubCode differs from docutils: the severity, not what survives. A malformed field is reported at warning level here and at error level by docutils, but both keep exactly the same tags — the one place that is not true is noted on meta.unexpected_argument below. The body must be a field list (:name: content), and the field name follows the same grammar upstream uses: whitespace-separated tokens where the first is the tag name or an attribute=value pair, and every later token is such a pair. Any attribute name is accepted, so :property=og:title:, :http-equiv=X-UA-Compatible: and :og:image: all work. When something does not parse, ubCode reports it and keeps going:

meta.invalid_body (warning)

The body is not a field list. Everything from that line on is ignored; fields already read are kept. A Sphinx build reports Invalid meta directive. as an error and keeps exactly the same fields.

meta.invalid_attribute (warning)

One field’s name has a token that is not attribute=value. That field is skipped and its siblings — on both sides of it — are kept. A Sphinx build reports the same single field as an error and likewise keeps the rest.

meta.empty_field (informational)

A field with no content writes no tag. docutils reports this below Sphinx’s own reporting level, so a Sphinx build says nothing at all.

meta.unexpected_argument (informational)

Text written on the .. meta:: line itself, or after a {meta} fence’s opening. The directive takes no argument, so the text is ignored — and here ubCode really does keep more: the field list below it still reads, where a Sphinx build stops at that first line and so loses the whole directive.

MyST front matter adds three of its own: topmatter.html_meta_deprecated for the top-level html_meta: spelling (place it under myst: instead — the top-level one replaces any nested block rather than merging with it, which is upstream’s behaviour too), topmatter.html_meta_not_a_mapping, and topmatter.html_meta_value for a value that is not a string.

``:keywords:`` feeds the site search, as it does in a Sphinx build: each comma-separated item becomes searchable on that page, and nothing else about the meta channel is indexed — :description: is not searchable on either side, and a keyword never shows up in a result’s snippet or in the page itself. A :nosearch: page contributes no keywords, and :keywords lang=xx: is indexed only when xx is en (ubCode’s search stack is English-only, so there is no language to configure). That last gate is where a project with a non-English Sphinx language would see a difference in both directions: Sphinx accepts a lang matching that language and rejects lang=en, while ubCode always does the reverse. Keywords with no lang at all — the common case — index identically everywhere.

Two small differences from Sphinx here, both in your favour:

  • A multi-word item such as :keywords: alpha beta is searchable as alpha and as beta, at full score. Sphinx stores the pair as a single term with a space in it, which its own search box only ever reaches as a lower-ranked partial match (a substring scan for query words longer than two characters), never as an exact hit.

  • Empty items from a doubled or trailing comma (:keywords: a,,b,) are dropped. Sphinx stores an empty term for each, which nothing can match.

Not yet implemented, and named so you can plan around it: there is no project-wide default-meta setting (Sphinx’s myst_html_meta, which upstream applies to MyST pages only).

Acknowledgements (acks)

.. acks:: renders its bullet list on its own — no wrapper element and no injected “Acknowledgements” heading, which is exactly what a Sphinx build renders. The body must be a single bullet list; anything else falls back to the generic placeholder, where a Sphinx build warns and drops the directive. As with every list in ubCode, the <ul> carries no docutils simple class.

Version markers

The four hyphenated spellings Sphinx 9 added — version-added, version-changed, version-deprecated, version-removed — work and are indistinguishable from versionadded, versionchanged, deprecated and versionremoved, down to the CSS class on the rendered block. Sphinx collapses the alias before anything downstream sees it, and so does ubCode.

Inline roles

:manpage: renders the shape a Sphinx build renders with no manpages_url configured — <em class="manpage">ls(1)</em>, the whole reference as emphasised text, section number included. Sphinx turns it into a link when you set manpages_url in conf.py; ubCode has no equivalent key, so it always renders the unlinked form. One small difference in the same row: Sphinx reads a leading ! as “do not link this one” and strips it, while ubCode renders the ! — it only exists to switch off a link that is never emitted here.

:mailheader: and :newsgroup: match a Sphinx build exactly.

Structure and inline detail

Some finer points of block and inline rendering differ from a Sphinx build:

  • Broken inline markup inside a directive argument or option value is not reported: an unterminated ** in an admonition title renders without the “Inline strong start-string without end-string” warnings a Sphinx build emits (the text itself is never swallowed).

  • Nested line blocks render flattened rather than progressively indented.

  • .. container:: ins and .. container:: del render as plain containers — insertion and deletion semantics are not applied.

  • Attribution lines (-- author) in epigraph, highlights, and pull-quote stay plain paragraphs rather than becoming attributions.

  • An unsupported option on a structural block makes the whole directive fall back to the generic placeholder.

In a Markdown (MyST) document, a directive’s argument and its inline-typed option values are parsed with the MyST inline grammar, matching a Sphinx build: *emphasis*, ` `code` `, links, and {ref} and its sibling roles all work in a {admonition} title, a {rubric} argument, a {code-block} :caption:, a {topic} argument, a table caption or a sphinx-design card title, and a dangling reference in one is reported. $maths$ works there too when the dollarmath extension is enabled — a slot sees exactly the extensions the surrounding prose sees, so maths in a title is on the same footing as maths in a paragraph. Backslash escapes work as they do in prose: \{ref}`x` in a title is the literal text, not a reference. Link and footnote definitions are document-scoped, so a [label] in a directive title resolves against a [label]: … line anywhere in the file, including below the directive.

Two details differ from a reStructuredText document, and neither is a choice:

  • reStructuredText role syntax does not work in a Markdown slot. ` :ref:`x` ` in a {card} title is the literal text :ref: followed by an inline code span, exactly as a Sphinx build renders it. Write ` {ref}`x` ` instead.

  • A dangling reference is reported one line below the fence. A Sphinx build anchors a Markdown slot’s reference at the fence line plus one, where the reStructuredText equivalent anchors at the directive line itself; ubCode matches the build for a top-level directive. A version marker’s trailing text adds one more line, again matching. Inside a nested directive (a :::{grid-item-card} in a ::::{grid}, say) ubCode reports the line you wrote the reference on plus one, where a Sphinx build reports one line further down — the same one-line offset it applies to ordinary prose inside a nested directive, so ubCode’s number is the one to edit at.

Note

Writing a role inside a backtick-fenced directive’s argument does not work in MyST at all, whatever the renderer: CommonMark forbids a backtick in a fence’s info string, so ` ```{list-table} title {ref}`x` ` is not a fence and the whole block degrades to a paragraph. Use a ~~~ or ::: fence if you need one.

Note

A few references in a Markdown slot render but are not resolved or reported. A {csv-table}’s cell content is re-parsed as reStructuredText whatever the document’s format, so a reference written in a cell neither renders nor warns; an {external}-family reference in any slot renders but is not registered, so it never reports when it dangles; and a Markdown link to another page ([text](other.md)) is not checked inside a slot — in ordinary prose it is (see Markdown link cross-references).

One scoping rule to know: inside a file you pull in with {include}, the host document’s link definitions apply to that file’s own directive titles, not the partial’s. A [label] defined in the partial itself stays literal there. Footnotes are not affected: a [^label] is a reference wherever it is written, and it resolves against every definition on the finished page — the partial’s own included.

The Markdown block break (+++)

You write +++ on a line of its own in a Markdown (MyST) document — the MyST block break, which marks a boundary between blocks of text (notebook tooling uses it as a cell separator, and reads the text after it as that block’s metadata).

The preview shows a dashed horizontal rule, so the break is visible while you write and distinguishable from a plain thematic break (---), which draws a solid one.

A Sphinx build renders nothing at all for it, and neither does ubc build html. MyST treats a block break as a comment, and a Sphinx build drops comments from the page.

Status — the dashed rule is a deliberate ubCode-only authoring affordance, on the same footing as the chips for .. index::, the state-changing directives, block hyperlink targets and .. needextend::.

A plain thematic break is unaffected: --- still renders a real <hr>, in the preview and in a built page.

Markdown attributes

Added in version 0.35.0.

The attrs_inline and attrs_block extensions read {#id .class key=value} groups — see the authoring guide for the syntax and the per-element keys. Eight points differ from a sphinx-build with myst-parser, and each is deliberate:

  • An unterminated group keeps your text. You write ` `a`{.a ` (no closing brace). The preview shows <code>a</code>{.a — the characters you typed. A Sphinx build shows <code>a</code>: its attribute parser succeeds on a group that runs off the end, so the braces are deleted. Content loss is not a formatting quirk, so ubCode declines the group.

  • Classes accumulate on spans and links. You write [a]{.x}{.y} or [a](u){.x}{.y}. The preview shows class="x y". A Sphinx build shows class="y": it reads the existing class off the closing token, which carries none, and replaces it. Its own documentation promises the opposite (“if multiple classes are given, they are combined”), and so does djot, the syntax’s stated source. Code spans and images already accumulate upstream, so this also makes the four element kinds agree.

  • A span inside a link label keeps the link. You write [a [b]{.c} d](u). The preview shows a link whose text contains a styled span. A Sphinx build shows the whole thing as literal text — an artefact of how its label scanner meets the span rule, which ubCode’s structural scanner does not have.

  • A group before a directive fence becomes directive options. You write {#note-a .lead} above a {note} fence. The preview shows the admonition carrying that id and class: #id is the directive’s :name:, .class is appended to its :class:, and every other key becomes an option. An option the author wrote out wins over the group’s. A Sphinx build drops the group silently — although myst-parser already ships this exact mapping for its fence_as_directive mode. {eval-rst} fences are excluded: their body is another parser’s, so there is no option set to map onto, and the group is reported instead.

  • Mistakes are reported. attrs.invalid_value, attrs.invalid_choice, attrs.no_target, attrs.unsupported_target and the informational attrs.unsupported_key (see Linting) each cover a case a Sphinx build passes over in silence — or, for an image w=/h=/a= with a bad value, crashes on. Each is positioned on the value you wrote, not on the element’s line.

  • A link’s title renders on a document link. You write [a](other.md){title="Team handbook"}. The preview shows <a … title="Team handbook">. A Sphinx build shows no title: the doctree carries it, but the writer Sphinx uses for a document reference drops the attribute — while the writer it uses for an in-page anchor link keeps it, so [a](#anchor){title="…"} agrees on both. ubCode renders what you wrote in both places.

  • A compact paragraph keeps its wrapper when you attribute it. You write a list with no blank lines between its items, and put a group inside one of them:

    - {#step-one}
      Unpack the archive.
    - Run the installer.
    

    The preview shows <li><p id="step-one">Unpack the archive.</p></li> for the attributed item and the compact <li>Run the installer.</li> for the other. ubCode normally unwraps a tight item’s paragraph, following markdown-it; an authored id or class outranks that, because the id is a cross-reference target and dropping the element would leave every [](#step-one) pointing at nothing. A Sphinx build wraps every list item’s content in a <p>, tight or not, so it never faces the choice — the attributed items agree, and only the plain one differs.

    The same rule covers the other places ubCode compacts a paragraph away: a tight definition list’s <dd>, and the first body paragraph of deprecated / versionadded / versionchanged, which is normally merged into the version label’s own paragraph. Attribute any of them and the paragraph keeps its element.

    For the version directives that is a difference in your favour. A Sphinx build rebuilds the first body paragraph from scratch when it merges the version label into it, and the new paragraph carries neither the id nor the classes you wrote — so a reference to that id does not resolve upstream at all. ubCode keeps your paragraph, with its id and classes, as its own paragraph after the label’s.

  • A duplicate id keeps the first. You write {#dup} twice in one file. The preview shows the first as the target and reports std.duplicate_target — exactly what (label)= already does. A Sphinx build unregisters the name entirely, so :ref:`dup` then fails to resolve at all — and on two headings it does not warn at all: it aborts, with ValueError inside docutils, and writes no HTML.

  • An empty or comment-only group still marks the span in the source, but renders no element. You write [a]{} or [a]{%c%}. The preview shows a — an inline wrapper carrying neither a class nor an id is transparent in ubCode’s renderer, which predates attributes and is pinned elsewhere. A Sphinx build shows <span>a</span>. The group is still consumed on both engines; only the empty element differs.

  • An inline code span’s language uses ubCode’s own class. You write ` `x`{l=python} `. The preview shows <code class="code language-python">, the same language-… convention the code role already uses, which the highlighter picks up. A Sphinx build shows <code class="code docutils literal highlight highlight-python"> with Pygments spans inside. ubCode’s <code> has never carried the docutils literal classes, so emitting them only for an attributed span would disagree with every other code span on the page.

  • An explicit ``rel`` or ``target`` outranks the config default. You write [a](https://x){target=_self} in a project with links_external_new_tab. The preview shows target="_self". A Sphinx build overwrites it with _blank: the config is applied to the token before the group is read.

Content inclusion

Included files

.. include:: expands inline in the Rendered preview. A few things differ from how a Sphinx build splices the same file textually:

  • State set inside an included file — a .. default-role::, for example — is scoped to that file in the preview. A Sphinx build lets that state leak into the rest of the host document.

  • A section heading in an included file that uses the same underline level as the host nests one level deeper in the preview than in a Sphinx build.

  • Clicking inside spliced content does not jump to the included file; sync-scroll maps spliced content back to the .. include:: line in the host document.

  • Include options the preview does not support are flagged with a diagnostic and ignored; the file is still spliced in.

Markdown documents splice too. A MyST {include}, {literalinclude} or {csv-table} with :file: reads its file on every surface — the Rendered preview, the background index, ubc check and ubc build html — exactly as the reStructuredText forms do. Three things are specific to a Markdown host:

  • The included file is parsed as Markdown, whatever its extension. A fragment is parsed in the syntax of the document that includes it, which is what both Sphinx and myst-parser do: including a .rst file into a Markdown document yields paragraphs and plain text, not directives. (The reverse also holds and is unchanged: a .. include:: of a .md file is parsed as reStructuredText.)

  • An included file’s headings nest inside the include, not into the page. They keep their relative structure — a ## under a # stays nested — but they never close the host’s sections, and an included heading never becomes the page’s title. They do appear in the page’s “on this page” contents, and a target under an included heading keeps that heading as its title. A Sphinx build integrates them into the page at their literal levels, so an included # can open a section beside the page title; it also means an included file that starts at ## renders one heading level deeper here than it does there. :heading-offset: therefore has no effect and raises a warning, as do :relative-docs: and :relative-images:.

  • An included file’s front matter is ignored, as in a Sphinx build.

A file that includes itself, directly or through a chain, reports directive.include_circular and shows an error message in place of that one include; the rest of the page renders normally. The message names the file the include would re-enter, relative to the project source directory, so it reads the same on every machine that builds the project. This applies to both formats and to every surface — the preview, ubc check, the background index and ubc build html. A Sphinx build with myst-parser has no cycle detection at all and crashes instead.

.. literalinclude:: renders the named file as a code block — in the Rendered preview, in ubc build html, and validated by ubc check. Its selection options — :lines:, :start-after: / :start-at:, :end-before: / :end-at:, :dedent:, :prepend: / :append: and :tab-width: — apply exactly as in a Sphinx build, in the same order. Three options are not applied; each raises a warning and the file still renders with the remaining options applied:

  • :pyobject: — selecting a Python class or function by name.

  • :diff: — rendering a unified diff against a second file.

  • :encoding: — files are always read as UTF-8. utf-8 and utf-8-sig are honoured (they are what a Sphinx build uses by default); any other value warns and the file is still read as UTF-8.

A leading byte-order mark is stripped, as in a Sphinx build — unless you ask for :encoding: utf-8 explicitly, which keeps it. A :tab-width: above 1000 is clamped.

A missing file, a marker that is not found, a line spec that selects nothing, and a conflicting option pair each show a visible error message in place of the block — the equivalent of the error a Sphinx build renders into the page. A file that exists but is not valid UTF-8 is reported as an encoding problem, not as a missing file. Line-number options behave exactly as they do for code blocks (see below), including :lineno-match:, which numbers the included slice with the numbers its lines carry in the file. It is mutually exclusive with :lineno-start:, :prepend: and :append:, as in a Sphinx build.

The background index now splices .. include:: too: a target, need, or glossary term defined in an included file is registered in the host document, so a :ref: or :term: to it resolves from anywhere in the project — including from another page, and whether or not the included file is itself one of your documents. In ubc build html such a reference links the host page, because that is the page the target’s anchor is rendered on, and objects.inv advertises the same page. Goto-definition and the hydrated title still point at the included file where the target was written. A :doc: written inside an included file is resolved relative to the page it is included into, as in a Sphinx build. A default-role (or similar state) set inside an included file therefore applies within that file’s spliced content on both the preview and the index.

A :need: to a need defined in an included file behaves the same way: in ubc build html it links the host page, which is where the need’s card — and so its anchor — is rendered. The need’s docname (the value a :filter: or a Cypher n.docname sees) is likewise the host document, as in a Sphinx build.

If the same file is included by more than one page, its targets and need cards are rendered on every one of them, but a reference resolves to a single page — the first host in path order, chosen the same way for the page link and, for targets, for objects.inv so the two always agree.

Needs carry one extra caveat, because a need id may only be defined once. A need defined in a file that is included by more than one page, or in a file that is BOTH included and one of your own documents (an included .rst under your source directory rather than, say, an .inc partial), is defined more than once — which is reported as a duplicate need, and a :need: to it links whichever of those pages comes first in path order. That page does not drift as you edit: it is decided by your sources alone, so an incremental rebuild and a --no-cache build always name the same one. Keeping a shared need in a partial that your source directory does not pick up and that only one page includes avoids the duplicate entirely, and is the recommended shape.

Deleting an .. include:: line — or deleting a page that had one — also removes the needs and targets that include had contributed, so nothing is left linking to a card the page no longer renders.

A problem inside an included file is reported against that file, at its own line and column, as sphinx-build reports it — so the position always indexes into the file you are asked to open, even when the including page is shorter than the file it includes. Because the same content is checked once per including page, ubc check adds a second included-from line naming that page, and a report you see more than once is the same problem reached from more than one host. Either path can be used to silence it: a per-file-ignores entry matches whether it names the included file or the page that includes it.

Note

Both the preview and the background index splice rst_prolog / rst_epilog, so they agree on spliced content. Between indexing passes, the per-keystroke checks hold back undefined substitution diagnostics — a reference might be defined in your prologue — until the next background pass catches up.

Raw content

.. raw:: html passes its body through to the output verbatim, in a block position and inside a substitution definition (.. |br| raw:: html) — which is what makes an rst_prolog-defined |br| render as a real line break on every page that references it. As in every Sphinx build, raw HTML is enabled by default and is emitted unescaped, so treat a raw block as content you are responsible for. Two controls exist for when that assumption does not hold; both are described under Trust controls below.

  • The format argument is a list: .. raw:: html latex emits in an HTML build, and .. raw:: latex is silently dropped, exactly as docutils does. ubc build pdf draws no raw block either (a placeholder, when [parse] raw_enabled = false), with one exception: a .. raw:: latex block whose whole content is \newpage, \clearpage, \pagebreak or \cleardoublepage breaks the page there (see Page breaks you write).

  • :class: wraps the payload in a <div> (block) or <span> (substitution) carrying those classes; without the option there is no wrapper markup at all.

  • :file: reads the named file and records it as a dependency, so editing that file re-renders the page that embeds it.

  • :url: is not supported — it would need a network fetch at build time. It raises a warning and the directive renders nothing.

  • :encoding: is accepted but files are always read as UTF-8 (a non-UTF-8 value warns); a leading byte-order mark is stripped.

  • Where docutils would insert a red error block into the page — a missing :file:, a file and body content, neither of them — ubCode raises a warning and renders nothing, rather than injecting an error into the byte stream you are controlling.

  • The inline raw role works, in its docutils form — a role derived from it with a :format::

    .. role:: raw-html(raw)
       :format: html
    
    A line break: :raw-html:`<br>` and a bold span: :raw-html:`<b>x</b>`.
    

    The payload passes through verbatim, wrapped in a <span> carrying the role’s classes (the role name unless you give it a :class:), and the same format-list rule applies: a role whose :format: does not name html renders nothing in an HTML build. The :format: option is only meaningful on a raw-derived role; on any other base role it is ignored.

    Two differences from docutils: the bare :raw:`text` form warns (role.raw_no_format) and renders nothing, where docutils raises an error and prints the unusable role into the page — the bare form cannot work in either, because a raw payload with no output format named is addressed to no writer. And a :format: value is matched case-insensitively, so :format: HTML emits where docutils’ writer would not. A :format: written with no value at all renders nothing in both; ubCode additionally warns once per use site, where docutils is silent.

Trust controls

Raw content assumes the author of a page is a party you trust to write HTML into your output. Two independent controls apply when that is not true.

Disabling the directive — set raw_enabled to false (see Parsing):

[parse]
raw_enabled = false

Every .. raw::, and its MyST {raw} twin, then passes nothing through: it raises a directive.raw_disabled warning and renders a placeholder showing its own source, escaped, instead — a framed block, or an inline chip where the directive defines a substitution (so a |br|-style definition stays valid inside the paragraphs that use it). The :file: form is not read at all, so no dependency on it is recorded. This is the docutils setting of the same name (which Sphinx never changes), and it applies wherever a document is processed — ubc build, ubc check, and the preview alike. It is meant for pipelines that build contributions you do not control, such as an auto-deployed preview of an external pull request.

The raw role is gated by the same switch, because docutils’ own check there reads “raw (and derived) roles disabled”: a raw-derived role then renders nothing and raises role.raw_disabled, its own code, so you can silence the directive’s warning and keep the role’s, or the reverse.

Beyond those two, the switch changes nothing. HTML written directly in a Markdown document (<div>…</div> in a paragraph, or an HTML block) is neither a directive nor a role, so raw_enabled does not affect it — this is the docutils setting’s own boundary, which Sphinx never widens.

Escaping in an untrusted workspace — the preview panel can render raw HTML escaped rather than injected, showing the markup as text. Unlike raw_enabled this is not project configuration (a repository you do not trust also supplies its own ubproject.toml): it is driven by the editor’s own trust state, per session, and it covers both syntaxes — the raw directive and Markdown’s native raw HTML.

Note

In VS Code this control is currently redundant: the ubCode extension does not declare support for untrusted workspaces, so VS Code disables the extension entirely in one — a stronger guarantee than escaping, with no preview at all. The escaping path exists in the language server, and the editor client already reports its trust state on every render, so the two stay in step if that posture is ever relaxed.

Conditional content

.. only:: evaluates its tag expression (and / or / not, parentheses, and the a if b else c form) against the build’s tags: html, format_html and builder_html, plus everything in the top-level build_tags key (see Configuring a project with ubproject.toml). A truthy expression includes the body transparently; a falsy one excludes it from the built output entirely, while the preview shows it as a collapsed, greyed block so you can see it exists. An expression that cannot be parsed raises only.invalid_expression and the body is kept — the same fail-open direction a Sphinx build takes (it evaluates only in a post-transform that keeps the children on any error), so a typo can never silently delete a page’s content.

.. if:: (sphinx-needs) works the same way against variant data instead of build tags.

Section headings are allowed in the body of either directive, as they are in a Sphinx build.

In reStructuredText they behave exactly as upstream: the body’s adornment styles are levelled from scratch, so the first style used inside the body is its level 1 whatever that style means outside. only additionally follows Sphinx in raising the block: if the body’s first style is one the document already uses, the whole only is re-parented to the level that style implies, so the section becomes a sibling of the surrounding one rather than a child. if does no such raise — sphinx-needs does not either — so a heading there always stays where it is written. Headings inside any other directive body (note, container, a list item, a table cell, …) remain invalid and are reported as block.title_disallowed, matching docutils.

In Markdown a heading is a token rather than an adornment, so the levelling has no counterpart and two things differ. A heading at the top level of a {only} or {if} body opens a real section, and it stays inside the enclosing one — myst-parser raises it to the absolute level its # count implies, which reorders the surrounding content; ubCode does not. The body keeps its own relative nesting, so ## followed by ### is still a parent and a child. And a heading in any other container — {note}, {card}, a block quote, a list item, a need body — is not an error at all: it renders as a rubric, an informal heading that keeps the heading’s anchor but does not open a section and does not appear in the page contents. That is myst-parser’s own rule. Plain CommonMark and GFM documents are unaffected — they have no rubric, and a heading nested in a block quote or list item stays an ordinary heading.

Warning

Content inside an excluded only block is not indexed. A Sphinx build parses the body at read time and removes it only when writing, so a :ref: into a target defined inside a falsy only still resolves there — at an anchor that is not on the page. ubCode decides earlier and discards the body entirely, so targets, sections, needs and glossary terms written inside an excluded block are not registered, and an inbound reference to one renders as an unresolved reference chip. That reference is silent by design: ubCode remembers the names an excluded body would have defined and suppresses the std.ref / needs.ref warning for exactly those names, rather than reporting a target the author did write. A reference to a name that is nowhere in the project still warns as usual. Define anything you reference from elsewhere outside the only block.

An only block is an element of its own to what surrounds it, where an if is not. A content-less rst-class written just before an only block reports directive.class_unsupported_target and the class is dropped; a Sphinx build puts the class on the first element inside a true only and drops it silently before a false one. A tab-set that holds an only block warns at the set (“All children of a ‘tab-set’ should be a ‘tab-item’”), as a Sphinx build does, and leaves out the tab-item directives inside it, which a Sphinx build shows when the only is true. An if or a choose behaves as in Sphinx-Needs in both places: the class lands on the first element it shows, or on the next element after one that shows nothing, and its tabs show in the set, with nothing reported at the set. Two exceptions remain for the class. When the if or the branch taken opens with an rst-class of its own for that same first element, both classes land, but in the other order — class="inner outer" where a Sphinx-Needs build writes class="outer inner" — a recorded difference. And when it opens with an .. include::, the class is refused, as it is before an .. include:: anywhere (see CSS classes).

Two files, one document

A document’s name comes from its path with the suffix removed, so guide.rst and guide.md both claim the document guide. Only one of them can be that document, and ubCode picks the same one a Sphinx build picks: the first in path order — here guide.md, because m sorts before r. (It is path order, not the order your parsers are declared in.)

The other file is then not a document at all. It is not parsed as one, and it contributes nothing: no page, no :ref: targets, no glossary terms, no needs, no toctree entries. A :ref: to a label written in it is reported as an undefined label, exactly as in a Sphinx build, rather than linking an anchor the served page does not have.

The file is still a file, though — a third document that includes it with .. include:: still splices its content. Only “is this a document of its own” changes.

Both engines warn. ubCode reports std.duplicate_docname against the ignored file, naming every claimant and the one it is using, with the winning file as a second location you can jump to. One difference is deliberate: a Sphinx build’s multiple files found for the document warning carries no warning type, so suppress_warnings cannot reach it, while ubCode’s is an ordinary code — lint.ignore and lint.per-file-ignores both silence it.

Rename one of the two files if you meant to keep both.

Components and markup

sphinx-design components

The :octicon: role renders natively — in the Rendered preview, in ubc build html, and validated by ubc check. Write :octicon:`name`, :octicon:`name;height` or :octicon:`name;height;classes`, as in a sphinx-design build; it emits the same inline <svg class="sd-octicon sd-octicon-{name} …">, so a stylesheet written against those classes keeps working.

  • Only a subset of the GitHub octicon set is embedded. The full set is 328 icons, an unreasonable weight to carry for a decorative role. A name outside the embedded subset raises directive.octicon_unknown and the role keeps its self-describing chip, so your source stays visible.

  • A card / dropdown / tab / button title is parsed as inline content (an :octicon: role in a grid-item-card title is the common shape), matching sphinx-design, which runs the same inline parser on it. A title that looks like a block (.. card:: - dash title *em*) keeps its dash literal and renders its emphasis, rather than becoming a list inside the title element.

  • A height that is not a number followed by px, em or rem raises directive.octicon_invalid_height, and a class name containing anything but letters, digits, _ and - raises directive.octicon_invalid_class — the rendered SVG is emitted verbatim, so an unsafe class name is refused rather than escaped. Both keep the chip.

The sphinx-design directives render natively too — grid / grid-item / grid-item-card, card (with its card-header / card-footer sub-directives), dropdown, div, tab-set / tab-item, and button-link / button-ref. They emit the same sd-* classes and element nesting sphinx-design does, so a stylesheet written against those classes keeps working.

ubc build html ships its own minimal design stylesheet (_static/ubc-design.css) covering the 12-column responsive grid, the card, dropdown, tab and button chrome, and the spacing, sizing, display, border and text utility families. A site with no sphinx-design markup neither links nor writes it. Its colours follow your project’s theme: the accent, and a card’s border, text and header/footer colours, are defined in terms of the --ubc-* custom properties (see Theming with extra_css), so a project that sets --ubc-brand gets branded buttons, and cards that follow your surface, border and text tokens, in light and dark alike, with no --sd-* override of its own. A resting card is not brand-coloured — --ubc-brand reaches it only through the hover border of a sd-card-hover card — and a card’s background is left transparent, so it shows the page. Setting a --sd-* property yourself still works — extra_css is loaded last.

The live preview loads the same stylesheet: the language server inlines it into any preview that contains sphinx-design markup, with those tokens mapped onto your editor’s theme colours, so a grid lays out in columns there too.

What differs from a sphinx-design build:

  • The accent colour is your project’s, not sphinx-design’s. Components take --ubc-brand — #2563c9 in a project that sets no theme of its own — where a stock sphinx-design build shows #0071bc, and they follow the light/dark palette, which a stock build does not.

  • A card’s header and footer carry the recessed surface colour (--ubc-bg-soft), where a stock sphinx-design build leaves them transparent. This is a parity change rather than a departure: the Sphinx theme these pages are modelled on colours the same marginals #f8f9fb in light and #1e2124 in dark, against ubCode’s #f2f4f7 and #1e2023. A card’s border, by contrast, is deliberately more visible than that theme’s (see the note above about backgrounds).

  • A card’s header and footer come from sub-directives. Write .. card-header:: and .. card-footer:: inside a card or grid-item-card; they are hoisted into their slots wherever you put them. The legacy ^^^ header separator is not supported — use card-header. A card-header or card-footer outside a card raises a warning and renders its content in place.

  • The legacy +++ footer separator is honoured, but write exactly three. ubCode splits the parsed body where sphinx-design splits the raw lines, which cuts both ways. Better: a +++ inside a code block, a literal block or a nested card is not treated as a separator here, where sphinx-design’s line scan claims it. Worse: a run of four or more + is a transition character in reStructuredText, so the parser claims it first — a standalone ++++ still splits, but a ++++ written directly under a line of text is read as a section underline and that line is dropped. That shape raises directive.card_separator_invalid telling you to write +++ or use card-footer.

  • A grid-item or grid-item-card outside a grid raises a warning (as it does in a sphinx-design build) and still renders, as does a grid child that is neither.

  • A dropdown’s default icon is not read from conf.py. sphinx-design lets a project set per-directive option defaults through the sd_custom_directives configuration value, which ubCode has no equivalent for, so dropdowns relying on it render without their configured icon. A :icon: naming an icon outside the embedded subset raises directive.octicon_unknown and the dropdown renders icon-less.

  • A card or dropdown title is parsed as reStructuredText, on both host formats — so an :octicon: role in a grid-item-card title renders as an icon. A button-link with no content falls back to showing its URL verbatim; a button-ref with no content shows the resolved title of its target, as in a sphinx-design build.

  • A button’s content is inline-only. It becomes the link text, and a link may only contain inline markup, so a single wrapping paragraph is unwrapped. Block content (a list, an admonition, a nested directive) produces invalid nesting — sphinx-design has the same limitation, since it inline-parses the content lines. Keep button content to one line of inline markup.

  • A link’s text has no wrapping <span>. sphinx-design wraps a button’s and a card link’s text in one because docutils requires a reference to hold an element child; no stylesheet rule targets it, so ubCode emits the text directly.

  • A component container carries no trailing docutils class. docutils’ writer appends that token to every container it writes; no sphinx-design selector uses it, so it is not reproduced. For the same reason the sd-card-text class is not stamped onto a card’s or dropdown’s body paragraphs — the ubCode stylesheet reproduces its one effect with a .sd-card-body > p:last-child rule instead.

  • ``:img-background:`` is ignored (with a warning), and card-carousel, article-info, tab-set-code and the badge roles are not implemented — they still show the generic placeholder.

Tabs

tab-set and tab-item render as a real tab strip, and switching tabs needs no JavaScript. Each tab is a hidden radio button with a visible label, which is how sphinx-design does it too: the browser’s own radio-group behaviour does the switching, so tabs work with scripting disabled, and in the live preview.

.. tab-set::

   .. tab-item:: Linux
      :sync: nix

      Install with ``apt``.

   .. tab-item:: Windows
      :sync: win
      :selected:

      Install with ``winget``.

:selected: chooses the tab shown first — the first item if you set none, and a second :selected: in one set warns and loses. :sync: gives a tab a key: every tab on the page sharing that key (within the set’s :sync-group:, tab unless you name one) selects together, so a reader who picks Linux once sees it everywhere. :name: puts an anchor on the tab’s label, so #your-name targets it. :class-label: and :class-content: add classes to the tab’s label and to its content panel. :class-container: adds classes to a wrapper that a tab set dissolves, so inside a set it reaches nothing in the HTML — exactly as in a sphinx-design build. It has an effect only on a tab-item written outside a set (see below).

:sync:, along with two related conveniences, comes from a small script (_static/ubc-tabs.js) that ubc build html links only on pages that contain a tab set:

  • tabs sharing a sync key switch together;

  • a link to an anchor inside a closed tab opens that tab — and every tab enclosing it, for nested sets — before scrolling to it;

  • the reader’s choice per sync group is remembered in browser storage, so it survives the next page and the next visit. The storage keys are prefixed with ubc-tab-id-; build.html.sd_tabs_storage_prefix changes that prefix, and setting it to an empty string turns the remembering off entirely.

What differs from a sphinx-design build:

  • The storage-key prefix differs by default. sphinx-design uses sphinx-design-tab-id-; ubCode uses ubc-tab-id- so that two builds served from one domain do not overwrite each other’s remembered choices. Set build.html.sd_tabs_storage_prefix to sphinx-design-tab-id- if you want them shared.

  • Hovering an unselected tab shows a muted form of your brand colour; a sphinx-design build shows the full selected-tab colour. Upstream gives the hover and selected states the same value, so hovering makes a tab look selected and the set briefly reads as having two selections. Here hover is softened, and it follows your theme: a project that sets --ubc-brand gets a muted version of it, in light and dark alike. Proposed upstream as sphinx-design#301; if that lands, the two converge again.

  • A tab label is parsed as reStructuredText on both host formats, as a card or dropdown title is.

  • A ``tab-item`` outside a ``tab-set`` warns and renders its label as a rubric above its content — the same shape sphinx-design produces for non-HTML output, and the one place :class-container: lands. Its content is visible here; a sphinx-design build emits the same markup but hides it, because its stylesheet has no rule for a set-less item. We show it: the directive already warns, and losing the text you wrote is the worse outcome. A tab-set child that is not a tab-item warns and is dropped, except for a hyperlink target, which is kept so references to it still resolve. Both raise directive.tab_structure_invalid (one warning per set, where sphinx-design warns once per stray child).

  • An invalid option *value* warns instead of dropping your tab. A :sync: or :sync-group: written with nothing after it, or a :selected: given a value (it is a flag and takes none), raises directive.design_invalid_option and the tab still renders — unsynced, in the default tab group, or selected, respectively. A sphinx-design build treats each as a hard error and drops the whole tab item (or the whole directive). Deleting what you wrote over a one-character slip is the worse trade, especially in the live preview, which re-renders as you type.

  • In the live preview the tabs switch, but the script’s three additions do not run. The preview renders inside the editor’s own webview, whose content-security policy admits no script the editor did not sign — so :sync:, anchor-opening and the remembered choice are ubc build html only.

  • ``tab-set-code`` is not implemented and shows the generic placeholder.

Tables

.. tabularcolumns:: is a LaTeX-only marker and correctly renders nothing, matching an HTML sphinx-build.

A table title (the directive argument on table, list-table and csv-table) is parsed as inline content, so markup and references in it render and resolve — matching a Sphinx build, whose make_title runs the same inline parser. That includes a title that looks like a block (.. list-table:: - dash title *em*): the dash stays literal, because an inline parse knows nothing about bullets, and the emphasis renders.

:widths: on table, list-table and csv-table takes one positive whole number per column — the same relative shares, read by the same code, as a needtable’s :colwidths: below. A value ubCode cannot apply now raises a warning and renders the table without column widths, where before it was silently ignored: an entry that is not a positive whole number (including 0 and a negative, which Docutils rejects too), one above 18446744073709551615, a grid keyword on list-table or csv-table (only table accepts it), or a count that does not match the columns. A Sphinx build makes every one of those a directive error that replaces the whole table, so a table that used to vanish now renders with a warning.

Two details are worth knowing because they are easy to trip over and both match Docutils exactly. The separator is comma or whitespace, never both in one value: if the value contains a comma it is split on commas only, so :widths: 1, 2 3 is two entries and the second one (2 3) is not a number. And the keywords are matched exactly, so AUTO is not auto.

One divergence runs the other way, on table only. :widths: grid asks for the widths the source grid table itself declares, which ubCode does not carry through its document model, so it renders no column group where a Sphinx build sizes the columns from the === rulers. Give explicit numbers if you need those widths.

:width: (singular, the whole table’s width) is now validated where it used to be passed through untouched. It takes a number, optionally followed by em, ex, px, in, cm, mm, pt, pc or % — Docutils’ list, matched case-sensitively, so 3PT is not 3pt. Anything else raises a warning and the table renders without a width: auto, 100vw, a negative value, or a value with anything after the unit such as 75%; position: fixed. A space between the number and the unit is allowed and removed, so :width: 3 pt renders width: 3pt — this one is a fix, because width: 3 pt is not valid CSS and a browser discards it.

Code blocks

code-block (with its aliases sourcecode and code), :: literal blocks, literalinclude and .. highlight:: render with syntax highlighting, with a few differences from a Sphinx build:

  • ubc build html and ubc build pdf both highlight during the build, from a bundled collection of Sublime Text syntax definitions rather than from Pygments — about two hundred and twenty languages, plus ubCode’s own console, pycon, cypher and myst grammars, and nothing to install. The two agree on everything: the same engine, the same language table (see Languages) and the same names for the colours, so a construct gets the same class on the site and on paper. Colours differ from your Sphinx theme, and token boundaries differ from a Pygments lexer’s. A language name nothing answers to is reported by both (build.highlight_language_unknown), and Sphinx’s default is drawn as Python — see Code blocks are syntax highlighted and its paper twin.

  • The VS Code preview colours with the same engine, during the same render: the language server highlights the document and the preview draws the result, so a construct is coloured the same way in the editor, on the site and on paper. A language it does not know is drawn plain (never mangled), and the preview reports nothing about it — run a build to see the finding.

  • One listing is coloured more than a Sphinx build colours it, in every surface including the editor: a rst or restructuredtext block. ubCode’s own reStructuredText grammar paints about 66 % of a listing’s non-whitespace characters (rst) and about 59 % (restructuredtext), measured over this project’s own documentation, where Pygments’ own reStructuredText lexer paints about 33 %. The extra is mostly two things Sphinx leaves plain: a directive’s ARGUMENT, and a directive OPTION’s VALUE — a quarter of the characters of this project’s own Sphinx-Needs listings, which no other highlighter colours at all. The body of a .. code-block:: is also lexed in the language its argument names, which Sphinx’s own reStructuredText lexer (Pygments) does only when the directive carries no options. This is a deliberate over-paint: the number to match was the colour the editor’s own preview used to show, not the colour a Sphinx page shows.

  • Some reStructuredText constructs read as a different KIND of thing than they did before ubCode’s grammar replaced the bundled one. A field or option marker such as :id: reads as an attribute rather than as a tag name; a footnote label, a hyperlink target, a substitution and a simple reference all read as built-ins; a doctest >>> reads as a prompt, the same way it does in a pycon listing. Nothing is mangled, and — measured over this project’s own documentation, 48 characters against 6,054 the other way — nothing that the bundled grammar coloured is now plain except two constructs it read as something else: a substitution definition, which it could not parse and took for verbatim text, and a citation’s body, which its footnote rule took for a footnote’s text.

  • A section title’s TEXT is coloured only where the highlighter can see that it is one, which means from an OVERLINE that begins a block: the highlighter reads one line at a time, so the text above a bare underline stays plain, and so does an over-and-underlined title placed straight after a directive, a comment or a literal block, because the block before it runs through the blank line. The adornment lines themselves are always coloured. This is the one place the colouring is thinner than the editor’s previous highlighter, and it is six characters of this project’s documentation.

  • :linenos:, :lineno-start:, :number-lines: and :emphasize-lines: are applied, as is literalinclude’s :lineno-match:. :lineno-start:, :number-lines: and :lineno-match: each imply :linenos:, exactly as in a Sphinx build, and :emphasize-lines: counts the lines displayed, from 1, whatever numbers those lines carry.

  • The line numbers are drawn beside the code rather than inside it, as CSS generated content. A Sphinx build interleaves them with the Pygments token spans as real text; ubCode keeps them outside the <code> element so that the copy button copies the code and nothing else, and so that your browser’s find-in-page searches the code and not the gutter. Three consequences, all of them deliberate: the numbers can never be copied, selected or dragged into your clipboard (a Sphinx build ships user-select: none for the same reason, and can go no further because its numbers are text); they are hidden from screen readers, which read a Sphinx build’s aloud; and your browser’s find-in-page will not find a line number.

  • :dedent: is applied on a code block as well as on literalinclude. reStructuredText already removes a directive’s own content indentation, so :dedent: only has something to remove when the block’s body is indented further than the directive’s options are — which is exactly the case a Sphinx build applies it to. A Markdown fence keeps its body verbatim, so it dedents as written.

  • A :dedent: value that is not a whole number of columns (:dedent: two, :dedent: -1, and Python’s :dedent: 1_0) is reported and the block renders with its indentation unchanged. A Sphinx build fails the directive, so the code disappears from the page. This is the one option here that changes the TEXT you are shown, which is why it is reported rather than applied quietly.

  • :force: is not applicable, rather than pending. It exists upstream to silence a Pygments lexer-failure warning; ubCode’s engine reports no lexing failure at all — a grammar that cannot make sense of a line simply leaves it uncoloured — so there is nothing for the option to suppress. It changes no output byte in a Sphinx build either.

  • .. highlight::’s :linenothreshold: N is applied: every literal block below it that is long enough is numbered from 1, unless the block asks for numbering itself. The threshold counts the lines you are shown — for a literalinclude that is the selected slice, not the whole file. At :linenothreshold: 5 a five-line code block is numbered and a four-line one is not, while a literalinclude, an .. include:: with :literal: or a plain Markdown fence is numbered one line earlier — from four (a {code-block} fence is not: it behaves as a code block). That one-line difference is what a Sphinx build does too: its test counts line endings rather than lines, and the trailing newline those sources carry reaches it one line sooner. (An included file whose last line has no trailing newline is numbered from five, like a code block.) An explicit language argument does not exempt a block.

  • The threshold is how the blocks that have no options at all get line numbers: a plain :: literal block, a quoted literal block, a Markdown fence, an .. include:: with :literal:. Nothing else can number those.

  • Every .. highlight:: sets the threshold, so a later one without :linenothreshold: clears it — the same reset a Sphinx build performs. A value that is not a positive whole number is reported, and the threshold already in force keeps applying; a Sphinx build fails the directive there.

  • A parsed-literal is not numbered by a threshold, where a Sphinx build numbers it. Its body renders as parsed markup rather than as source lines, so there are no lines for the numbers to belong to.

  • docutils’ .. include:: also carries a :number-lines:, which is reported as unsupported wherever it is used; that one is still outstanding.

  • A line-number option a Sphinx build rejects is reported and the block still renders, without that one decoration: an unparsable :emphasize-lines:/:lineno-start:/:number-lines:, and :lineno-match: over a disjoint :lines: selection. A Sphinx build replaces the whole block with an error, so the code disappears from the page; a mistyped decoration should not cost you the code. An :emphasize-lines: naming lines past the end of the block behaves exactly as upstream: it warns, and the lines that do exist are emphasised.

  • .. code:: accepts :linenos: and :caption: here. A Sphinx build patches docutils’ code directive but keeps its narrower option set and errors on both.

  • :caption: renders above the block, with its inline markup parsed — including references, which resolve and warn when dangling. This applies to Markdown as well as reStructuredText, each with its own grammar (see Markdown documents). A dangling reference in a caption is reported at the directive line; a Sphinx build reports it at the first line of the block’s content — three lines further down for a one-option block with a blank line after it, more with more options, and past the directive entirely when a literalinclude has no content. Every other inline slot anchors at its host directive, which is where a Sphinx build reports it — including a version marker’s trailing text. The exception in the other direction is a line-block directive’s lines, where a Sphinx build’s own anchor is unusable (it reports an unrelated line, or none at all) and ours is the directive line.

  • A caption that looks like a block rather than a phrase (:caption: - dash caption, :caption: 1. numbered *em*) renders as literal text, markup and all. A Sphinx build keeps the dash literal but still parses the markup — inside a <li> with no surrounding list, which no browser renders as written. There is no valid output to match: emitting the list ourselves would put a <ul> inside the caption’s <p>, invalid in the other direction. The same applies to a wrapped caption whose continuation line is indented, which is read as a definition list. Unlike table and card titles, which are at parity here, this one is a deliberate divergence.

  • An argument-less code-block (or a plain :: block) with no .. highlight:: language and no configured highlight_language stays unhighlighted; a Sphinx build falls back to highlighting it as Python (its default).

Custom CSS targeting .hljs-* no longer applies

Until 0.35.x, ubc build html coloured code in the reader’s browser, and the spans it coloured carried highlight.js class names (.hljs-keyword, .hljs-string, and the rest). The colouring now happens during the build, and a span carries one hl- class instead — hl-keyword, hl-type, hl-function, hl-name, hl-built-in, hl-label, hl-attr, hl-variable, hl-number, hl-literal, hl-operator, hl-string, hl-escape, hl-comment, hl-section, hl-bullet, hl-link, hl-addition or hl-deletion, plus hl-strong and hl-emphasis for a bold or italic run that takes no colour of its own. A stylesheet in extra_css that targets the old names selects nothing and fails silently, which is why this note exists rather than a warning.

If you recoloured tokens by redeclaring the --ubc-hl-* custom properties, the values are unchanged and they are still what ubc build pdf paints from — but there are twenty-one of them now rather than thirteen, and a property whose reach narrowed no longer paints everything it used to. See the stable-surface note for which ones moved. The data-ubc-highlighted attribute the browser used to stamp on each element is gone with the client highlighter, as is the hljs class the old palette hung on.

Highlighted inline code — a role derived from :code: with a :language: option — follows the same rules as the blocks above, and ubc build html, ubc build pdf and the VS Code preview all colour it, with the same engine they run over a block. It is not a reStructuredText construct only: in Markdown, a role derived from code with a {role} directive works the same way, and with attrs_inline a code span takes its language from an {l=…}, {language=…} or {lexer=…} attribute. It has four differences of its own:

  • On paper, a diff token keeps its colour and not its tinted row. ubc build pdf draws each token in its colour inside the chip’s own background, and an inserted or deleted token of a diff in hl-addition-fg or hl-deletion-fg alone: the hl-addition-bg / hl-deletion-bg ground a code block paints behind the row would break the chip’s background at the token. ubc build html paints that ground inside the chip.

  • A Sphinx build writes Pygments token spans into the HTML (<span class="mi">1</span>) and ships a pygments.css to colour them, and standalone docutils writes them too but with its own longer class names (<span class="literal number integer">) — the two upstreams already disagree here. ubCode writes its own nineteen-class vocabulary instead — hl-keyword, hl-type, hl-function, hl-name, hl-built-in, hl-label, hl-attr, hl-variable, hl-number, hl-literal, hl-operator, hl-string, hl-escape, hl-comment, hl-section, hl-bullet, hl-link, hl-addition, hl-deletion, plus hl-strong and hl-emphasis for bold and italic runs — and colours it from the twenty-one --ubc-hl-* custom properties, so a pygments.css file or a pygments_style setting has no effect here. Redeclare those properties in an extra_css file instead; on paper the same roles are set under [build.pdf.theme.colors].

  • The derived role’s own name and any :class: tokens are dropped; both upstreams keep them alongside the language. (This is the same limitation custom classes have on other derived roles.)

  • .. role:: python(code) with no :language: renders plain inline code: the role’s name is never used as the language. This matches Sphinx. Current docutils development builds do fall back to the role name.

An unrecognised language is reported as build.highlight_language_unknown, naming the token, counting the places it is written and offering the nearest name the build does know; the code is drawn in full, without colours. Both builders report it on a block and on an inline :code: role, counted together under the one name, a chip in a section title once. A Sphinx build logs a highlighting_failure warning and falls back to unhighlighted output, which is the same behaviour; standalone docutils also reports a warning, then renders the role’s source text as a problematic node in place of the span. The VS Code preview resolves the same names from the same table, so it colours exactly what ubc build html colours; it draws an unrecognised language plain without a finding of its own, because the build is where the finding is reported.

A plain :code: role with no language is byte-identical to a docutils build (a bare <code> element, no classes); a Sphinx build adds its own code docutils literal notranslate classes and wraps each word in a <span class="pre">.

Further differences apply to .. role:: definitions generally, not only to :code::

  • Options must follow the signature without a blank line. docutils re-reads a role definition’s options out of its own content, skipping a leading blank line as it goes, so a :class: or :language: separated from the signature by a blank line still applies there. ubCode parses the option block the way it does for every other directive, which ends at the first blank line, so those lines are content and the option is not applied. Write the options directly under the signature.

  • A rejected body does not decline the definition. No docutils role accepts content, so a role definition with a body raises Error in "role" directive: no content permitted upstream and the role is left undefined. ubCode still defines it, so later uses of the role render rather than being flagged a second time.

  • A signature continued on the next line is joined into the signature. ubCode treats the signature as the directive argument, and a directive argument folds in its continuation lines, so .. role:: custom followed by an indented (code) defines custom with the code base — silently. docutils declares no argument for role at all and reads the signature back out of its own content, so a continuation line is content there: it raises Error in "role" directive: no content permitted and the role is left undefined. Keep the whole signature on one line.

  • A valueless :class: or :language: is ignored, not an error. :class: with no value falls back to the role name. docutils passes the missing value to its option parser, which raises argument required but none supplied and leaves the role undefined. The same best-effort stance applies to a repeated option, where ubCode keeps the first.

  • A cycle between role definitions renders a placeholder chip. Redefining a role in terms of one that already inherits from it — .. role:: a(emphasis), .. role:: b(a), then .. role:: a(b) — leaves a cycle, because ubcode resolves a base by name at the point of use. Such a chain is followed for at most 32 hops and then renders as a placeholder chip. Both upstreams bind the base role object when the definition is read, so the redefinition is harmless there and the role still renders (docutils renders :b: as the inherited emphasis).

  • An option the base role does not recognise is accepted and ignored. Both upstreams validate a role definition’s options against its base role, report an error at the definition, and then do not define the role at all — so every later use of that role errors too, as an unknown interpreted text role. ubcode records the option, defines the role, and the option simply never takes effect. :language: is only meaningful on a code base and :format: only on a raw base (each does its real work there — see Raw content for :format:, which is the whole mechanism of the inline raw role). On any other base, docutils reports unknown option: "language" / unknown option: "format" and myst-parser reports Unknown option keys: ['format'] (allowed: ['class']), where ubCode accepts the definition and ignores the option.

Both differences apply in a MyST fence as well, and both are now reported there: role accepts no content, so anything that lands in the fence body is flagged directive.content_not_permitted (Linting), the Markdown twin of reStructuredText’s block.directive_content_not_permitted. What differs between the two syntaxes is only the outcome.

A blank line still ends the option block, so an option written after one becomes body content and is dropped — here :class: red is reported but never applies:

```{role} custom

:class: red
```

A body written below the options is reported and dropped in the same way, and the :class: above it still applies:

```{role} custom
:class: applied

this body is reported and dropped
```

In both examples ubCode reports the rejected body and keeps the role defined, so later uses of :custom: still render. myst-parser refuses both outright — Error in "role" directive — and leaves the role undefined, so every later use warns Unknown interpreted text role. That is the real difference; the report itself is new in Markdown, and the dropped option is unchanged.

One case is still not reported, alongside the blank-line option above. When a directive declares no argument at all (todolist, target-notes, sectnum and the like), text written on the fence line becomes the directive’s argument in ubCode and is silently discarded, where myst-parser folds that line into the body and so reports it:

```{todolist} inline text on the fence line
```

The same todolist written with the text as a real body is reported by both. Only the fence-line route is silent.

One further difference is MyST-only: the signature must be the directive argument.

```{role} custom(code)
:language: python
```

The docutils-style form that puts the signature in the fence body is reported as a missing argument and defines no role, where myst-parser accepts it by handing the fence body straight to the docutils implementation.

Images

  • A local image whose file lives outside your workspace folder does not load in the preview; the preview can only read files within the workspace.

  • Remote images referenced by an https:// URL load directly.

  • An image written inside a substitution definition is accepted. A Sphinx build rejects the construct outright when the image is named (ERROR: Substitution definition contains illegal element <image>): docutils refuses a named element inside a substitution definition. An unnamed substitution image is fine in both engines. ubCode accepts either silently, which is the difference worth knowing — and its :name: does render as an anchor, so :ref: to it, and the entry ubCode publishes in objects.inv, reach the image on the page.

:width:, :height: and a figure’s :figwidth: are validated where they used to be split apart and passed through. Each takes a number, optionally followed by em, ex, px, in, cm, mm, pt or pc — Docutils’ list, matched case-sensitively, so 3PT is not 3pt. :width: and :figwidth: also take %; :height: does not. That asymmetry is Docutils’, not ubCode’s: its image directive reads the height with length_or_unitless and the width with length_or_percentage_or_unitless, so :height: 50% is a directive error in a Sphinx build while :width: 50% is a length.

Anything the grammar refuses raises a warning and the image renders without that dimension — auto, 100vw, 10rem, a negative value, or a value with anything after the unit such as 75%; position: fixed. A Sphinx build refuses the same values and drops the whole image: the directive becomes an error node, which Sphinx’s default keep_warnings = False then strips from the page, so the picture simply is not there and the reason is only in the build log. (Bare Docutils leaves the error visible in the output.) ubCode keeps the picture and drops the size. The last of those is the one worth knowing: before this, everything after the unit was carried into the style attribute, so :width: 75%; position: fixed really did add a position: fixed declaration to the image.

Raw <img> tags, with the MyST html_image extension enabled (see Parsing), behave like an {image} directive with the same options — so everything on this page about images applies to them too. Four differences are specific to the tag form:

  • A missing or empty ``src`` leaves the markup on the page. ubCode warns (md.html_image_no_src) and renders the HTML block exactly as written — every tag in it, whether or not that one was the problem. A Sphinx build raises an error and deletes the markup: the tag is replaced by the error message, so nothing image-shaped survives. ubCode’s rule is that a diagnostic never comes at the cost of your content.

  • A valueless attribute is empty, not the word “None”. <img src="a.png" alt> renders alt="", which is what HTML says a valueless attribute means. A Sphinx build renders the literal text alt="None" there.

  • A newline inside an attribute value stays inside it. <img src="a.png" alt="x ⏎ :width: 99"> keeps both lines as the alt text. A Sphinx build re-serialises the attributes as directive option lines before parsing them, so the second line becomes a real :width: option — the alt text silently resizes the image.

  • A stray end tag leaves the block raw. <img src="a.png"> followed by </div> in the same HTML block renders as written here. A Sphinx build discards the </div> and converts the image, so markup you wrote disappears.

Two more follow from the tag behaving like the directive, and are the same differences the directive has: a src containing a space is kept as written (a Sphinx build deletes the space, so a b.png names ab.png), and an align value outside the allowed set warns and is still applied as a CSS class (a Sphinx build warns and drops it).

Character references in an attribute value are decoded — src="a&amp;b.png" names a&b.png — but the ; is required. src="a&ampb.png" names a file spelled exactly that way here, where a Sphinx build decodes the legacy no-semicolon spelling too; the same rule means a&notb.png stays itself rather than becoming a¬b.png.

A figure’s :figwidth: image is accepted and sizes nothing. Docutils answers it by reading the image file’s intrinsic width, which ubCode does not do while indexing — and neither does Docutils itself without Pillow installed, where it also emits no width.

A space between the number and the unit is allowed and removed (:width: 3 pt is 3pt), as in a Sphinx build — the value is normalised once, when the document is read, so both the preview and ubc build html get the corrected form.

The number itself reaches the page exactly as you wrote it — 1.50cm stays 1.50cm and .5em stays .5em, as in a Sphinx build. One consequence of that faithfulness: a width with no unit also reaches the page as written, and width: 300 is not a length any browser applies, where a Sphinx build appends the px (#2827). (ubCode’s Docutils-conformance renderer, which is not the mode described here, re-formats them to 1.5cm and 0.5em.)

:scale: is the exception, and it is a layout difference rather than a spelling one. A Sphinx build multiplies the size and bakes the product into the length, so :width: 10px with :scale: 66 becomes width: 6.6px (written out in full as 6.6000000000000005px). ubCode keeps the authored width and applies a CSS transform, which scales what you see without changing the box the page lays out around — so text flows around a 10px image here and a 6.6px one there.

One gap worth knowing: a .. figure:: :figwidth: currently sets no width at all in the preview or in ubc build html (#2826); it is honoured only by that conformance renderer.

A * wildcard extension (.. image:: logo.*) selects the best available file, exactly as a Sphinx html build does: the pattern is globbed, candidates are classified by file extension, the html priority svg > png > gif > jpeg picks the winner, and within one type the shortest file name wins. webp is never selectable for html output — as in Sphinx. ubc build html copies and references the selected file, and hovering the image argument in the editor previews that same file. If nothing matches — or only unsupported types match — the build emits the pattern as written (minus any ?query/#fragment, which every asset reference is stripped of) with a warning, and ubc check reports image.not_found on the directive (a Sphinx build only warns at build time; the background index here also re-checks the glob when files appear or disappear beside it).

The build warning comes from rendering the page, but it is reported on every build: a page’s render warnings are stored with its rendered body, so a build that reuses the page re-reports them and --show-warnings shows the same list warm as it does under --fresh. ubc check reports the unmatched glob independently, from the index, on every run. A Sphinx build repeats neither (its image post-processing runs during the write phase, and an unchanged document is not written) — one of the few places ubCode is deliberately noisier.

Two deliberate differences from a Sphinx build:

  • candidates are classified by file extension only; Sphinx additionally sniffs the bytes of files with unknown extensions, so a real JPEG named photo.jpeg is selectable there but not here — name it photo.jpg;

  • language-specific candidates (figure_language_filename) are not searched. This is not limited to translated projects: Sphinx’s language defaults to "en" and figure_language_filename to "{root}.{language}{ext}", so in a project with no internationalisation set up at all, a Sphinx build looking for logo.* searches logo.en.* first and prefers a logo.en.svg sitting beside your logo.svg. ubCode has no i18n machinery, so it picks logo.svg. Both files are in the candidate set either way — only the winner within one image type differs — so this bites you only if you keep <name>.en.<ext> files beside <name>.<ext> ones.

A wildcard extension is an image feature: candidates are ranked by image type, which cannot choose a video file. So a .. video:: source must name its file exactly. A source written as clip.* is emitted as written (minus any ?query/#fragment) and reported as build.video_glob_unsupported; nothing is copied for it. A :poster: is a real image and resolves normally, wildcard included.

Two things to know about that report:

  • it is only <stem>.* that counts as a wildcard extension. Any other * is not glob syntax here and never was: .. video:: media/*.mp4 is treated as the literal filename *.mp4, so the build tries to copy it, fails, and reports build.image_read_failed instead — with a _images/*.mp4 reference that loads nothing. The same is true of an image: only the final .* triggers selection.

  • build.video_glob_unsupported comes from rendering the page, and ubc build html is its only surface: ubc check does not report it (the background index does not resolve video sources at all). It is nonetheless reported on every build, warm or fresh — a page’s render warnings are stored with its rendered body, so reusing the page re-reports them.

Selection is pinned to the html builder’s priority list (other output formats would re-open the choice), and the Rendered preview does not yet resolve the wildcard — the image does not load there, while the hover preview and the build resolve it.

Downloads

Added in version 0.33.0.

The :download: role publishes a file for the reader to save. A local target is copied into _downloads/<hash>/<name>, where <hash> is derived from the file’s project-relative path — the same scheme a Sphinx build uses, so an href written by either engine points at the same place. The file name is kept as it is: two files with the same name in different directories get different <hash> directories rather than a numbered name, because the name is what the reader’s browser saves. One file inside the source directory linked from many pages is copied once, however each page spelled the path. assets/data.txt from the root, ../assets/data.txt from a subdirectory, and even ../<source-dir>/assets/data.txt — which climbs out of the source directory and back in — are one download, not several: the path is normalised before it is hashed, as a Sphinx build normalises it.

That normalisation is done by reading the path, not by following the filesystem. So there are two cases it does not cover. If a .. climbs through a symbolic link, ubCode and a Sphinx build can name that file differently and publish it under different hashes; and two genuinely different files can end up sharing one published name, in which case one of them is published and a page linking the other gets those bytes. Both directions of that case need a .. crossing a link to happen at all. The other case needs no symbolic link: a target that points outside the source directory altogether is hashed as each page wrote it, so one outside file referenced from pages at different depths is published once per spelling, where a Sphinx build publishes it once.

A target containing :// is treated as an external URL: it is linked and never copied. That test is narrower than the one images use — a mailto: or //host/x target is read as a path here, exactly as a Sphinx build reads it, which means it names no file and degrades as below.

Three differences worth knowing:

  • A target that names no readable file loses its link, keeping its text. There is no placeholder and no empty link; the file name renders as inline code and nothing more. A Sphinx build does the same, but reports it only when the site is built — ubCode reports it while you type, as download.not_readable, on the line the role is written. A separate build.download_read_failed covers the narrower case of a file that disappears during a build, after the page linking it was written.

  • The rendered element is <code class="xref download">, where a Sphinx build adds docutils literal notranslate and wraps the text in <span class="pre">. ubCode drops that markup for every inline literal, not just this one; a stylesheet keyed on xref or download still selects it.

  • A :download: inside imported need content is never published, even with needs.import_allow_assets switched on — where an imported .. image:: is. The reference is refused with a diagnostic naming the key, and renders as its own text. Imported content comes from outside your project, and a download hands whatever it names to the reader as a file to keep, so this surface starts closed; see What works inside imported need content.

A leading ! switches the reference off — :download:`!notes.txt` renders the text alone — and whitespace inside a target collapses to single spaces, both as in a Sphinx build.

In the preview, a download link resolves to the real file on disk, and clicking it opens that file with the application your operating system associates with it. That is a deliberate difference from a built site, where the download attribute makes the browser save the file instead: in the editor the file is already on disk beside your sources, so the preview opens it rather than saving another copy. The link is never followed as a web address — the file is opened directly from where it sits on disk, before any _downloads/ copy exists.

Diagrams

Mermaid and PlantUML diagram directives render, with a few option differences:

  • :width: and :height: size the diagram container, in the preview and in ubc build html alike (a bare number is treated as pixels). The same two options are available on the needflow and needsequence views above, where they are a pure ubCode extension. sphinxcontrib-mermaid declares neither option, so a Sphinx build does not merely ignore them — it raises unknown option: "width" and drops the whole diagram.

    Any CSS unit is accepted here — 100vw and 10rem size the container correctly — which is more than an image or figure accepts, and deliberately so: the diagram is a CSS-sized box rather than an <img>, and a PlantUML build reads these options with Docutils’ image grammar only because its output is an image. A value that is not a size at all (bogus, -5px, 1.5.5px) now raises a warning and the diagram renders unsized; before, it was dropped in silence.

  • A very large diagram no longer grows the page without limit. A rendered diagram is at most 60% of the viewport height and scrolls past that, in the preview and in ubc build html alike. A diagram that already fits is unaffected, an explicit :height: wins over the bound, and printing releases it so the whole diagram is printed. The same bound applies to every rendered diagram, including the needflow and needsequence views. Sphinx has no equivalent default: sphinxcontrib-mermaid sizes every diagram from two global config values (mermaid_width, mermaid_height) instead.

  • :alt:, :config:, :title:, and :zoom: are parsed but not applied.

  • :scale: is applied only when a real PlantUML drew the diagram (External PlantUML renderer), where it becomes a percentage width on the diagram’s box. Without that it is parsed and not applied, because a diagram the browser has not drawn yet has no size to scale. A Sphinx build scales the rendered bitmap instead, so the two are visually similar rather than identical, and an explicit :width: wins over it here.

  • :html_format: and :latex_format: are accepted and ignored, including when a real PlantUML draws the diagram. ubCode inlines the SVG into the page rather than linking an image: png would throw away the point of the pathway, svg_img (an <img>) kills the links inside the diagram, and svg_obj (an <object>) resolves relative links against the SVG’s own URL rather than the page’s, breaking page-relative traceability links. There is no LaTeX builder for :latex_format: to serve. They are ignored rather than warned about, so a project that also builds with Sphinx is not warned on every diagram for an option that is correct there.

  • :name: gives the diagram a stable cross-reference target, rendered as an id in a built site (and a in the caption, when the diagram has a :caption:). A name whose slug is empty — punctuation or emoji only, and, under [parse] id_style = "docutils", most non-Latin-script names and any purely numeric one — still anchors, on the page’s numbered id1 / id2 counter rather than on a readable slug. See Anchors on views and diagrams.

  • Caption text is rendered as plain text; inline markup inside a caption is not parsed (unlike a code-block :caption:, which is).

  • A diagram supplied by file path (.. uml:: flow.puml) renders the file’s content, resolved like an image path (a leading / against the source directory, anything else against the document’s own directory). PlantUML is converted to Mermaid, approximately; a diagram the conversion cannot express falls back to showing the directive source, and ubc build html reports it (Unrendered directives, roles and diagram files). With a real PlantUML configured the file is drawn by the executable instead, exactly as an inline diagram is.

  • A diagram copied onto a page by needextract keeps the Mermaid conversion, even with a real PlantUML configured. The copy is rendered from the source need’s stored content rather than from the page’s own captured diagram sites, so there is nothing for the external renderer to have drawn it from. The diagram on its own page is drawn normally.

  • A local !include behaves differently once a real PlantUML draws your diagrams, and it is the one case that gets worse rather than better. The Mermaid conversion’s preprocessor skips an include it cannot resolve and draws the rest of the diagram; PlantUML, in the sandbox ubCode runs it under, fails the diagram — so it falls back to the Mermaid conversion with a warning, and ubc build html fails on any warning by default. Stdlib includes (!include <C4/C4_Context>, !theme cerulean, sprites) are compiled into PlantUML and work under the sandbox. A Sphinx build resolves local includes, because it does not sandbox PlantUML at all.

CSS classes

The rst-class and cssclass directives attach CSS classes to your content. Both are the same directive under two names, exactly as in Sphinx.

Where the class lands

You write .. rst-class:: highlighted with no content, followed by a paragraph, a list, a table or a section heading.

The preview shows the class on the element that FOLLOWS — and when the directive is the last thing in a section, on the NEXT SECTION, not on anything inside the one it was written in. Comments, hyperlink targets, substitution definitions, .. index:: entries and the state-changing directives (.. highlight::, .. role::, .. default-role::, .. default-domain::) are skipped on the way there.

A Sphinx build does the same. A class written inside a directive body stays inside that body, attaching to the body’s own first block rather than escaping past it.

Status — parity for the attachment rule itself. What a class can then land ON is narrower than a Sphinx build’s — see “Where the class CANNOT land” below; that is the remaining gap in this section, and it is reported rather than silent. Two of its members are not element-vocabulary gaps but differences in what the walk MEETS: before a directive ubCode does not recognise, Docutils skips its own invisible error node where ubCode’s visible placeholder stops the class, and an .. include:: is textual upstream where ubCode’s wrapper is a boundary — both are listed below with the rest.

Where the class CANNOT land

You write a content-less rst-class before an element ubCode cannot put a class attribute on — a transition (----), a footnote or citation definition, a field list, a sphinx-design component, an .. include::, or a directive ubCode does not recognise.

The preview shows the element unchanged, and reports directive.class_unsupported_target at the rst-class line.

A Sphinx build renders the class on any of them: Docutils writes every element’s classes through one shared code path, so the question never arises upstream.

Status — a known gap, and a deliberately LOUD one. ubCode’s HTML writer reads classes per element type, so a class stored on an element whose rendering cannot express it would disappear with no sign at all — which is worse than being told. The elements a class CAN land on are paragraphs, sections, bullet / numbered / field / definition lists, list items, tables, admonitions, images and figures, literal and math and raw blocks, block quotes, containers, compounds, topics, sidebars, rubrics and line blocks. (Field lists and list items joined that set with the MyST attribute family in 0.35.0, which needed the same declaration — two spellings of “put a class on this element” must not disagree.) Widening that list is additive work; report the element you need.

Two of those shapes are worth calling out, because a Sphinx build does something you might not expect: before an unrecognised directive, Docutils skips the error node it puts there and the class reaches the next real element, so upstream classes the paragraph AFTER the unknown directive where ubCode reports and drops.

With content

You write .. rst-class:: framed with a body.

The preview shows the body’s blocks with the class on each of them, and no wrapper element around them.

A Sphinx build does the same.

Status — full parity. Only the body’s TOP-LEVEL blocks are classed; a paragraph nested inside a body block quote is not, which is also what a Sphinx build does. If any top-level block of the body is one of the unsupported elements above, the whole directive falls back to the generic placeholder (which still renders the body) rather than classing some blocks and not others.

Classes on a section in the preview

You write an rst-class that lands on a section.

The preview shows nothing different.

A Sphinx build, and ubc build html, render <section class="…" id="…">.

Status — inherited from a wider gap: the preview does not emit a <section> element at all (it renders the heading and the content, without the wrapper), so there is nothing for the class to live on. The class IS applied in ubc build html.

A class on a list

You write .. rst-class:: on-a-list before a bullet list.

The preview shows <ul class="on-a-list">.

A Sphinx build renders <ul class="on-a-list simple">.

Status — the missing simple is the general list gap already noted for hlist and glossary, not something specific to classes.

Invalid class names

You write .. rst-class:: 123 — a name that normalises to nothing.

The preview shows the generic directive placeholder and reports directive.class_invalid_argument.

A Sphinx build reports an error and drops the directive.

Status — the same outcome, reported rather than errored. Class names are normalised the same way Sphinx normalises them: split on whitespace, lowercased, and punctuation folded to -, so :class: Foo_Bar BAZ becomes class="foo-bar baz".

Nothing to attach to

You write a content-less rst-class with nothing after it.

The preview shows nothing, and reports directive.class_no_target.

A Sphinx build renders a red error block quoting your directive’s source.

Status — reported, not printed into the page.

Bare .. class::

You write .. class:: Widget.

The preview shows the generic directive placeholder.

A Sphinx build documents a Python class — because primary_domain defaults to py, .. class:: is the Python domain’s object directive, not the class-attaching one. Use rst-class (or cssclass) to attach a class; that is exactly why Sphinx ships those names.

Status — recognised but not rendered, like every other Python-domain object directive. ubCode records the default domain but does not yet dispatch unprefixed names to it.

Unknown directives and roles

Unknown directives

You write a directive ubCode does not recognise, such as .. wibble::.

The preview shows a generic directive placeholder.

A Sphinx build reports an error and typically drops or marks the construct.

Status — unknown directives also raise a configurable lint diagnostic. Register the directive with extend_directives, or silence it with ignore_directives, in your parsing configuration; see Linting.

A need type named like a built-in directive

You write a [[needs.types]] entry whose directive is a built-in’s name — note, todo, tab-set, needtable, src-trace.

ubCode gives the name to the need type, title and all, and reports it (config.conflicting_directive_registration).

A Sphinx build does the same for docutils’, Sphinx’s and sphinx-needs’ directives and the ports (sphinx-needs registers each type’s directive after every extension’s setup()), usually with an [app.add_directive] warning — but NOT for a domain directive (the std domain’s envvar, option, glossary, …, or class, which resolves to the primary py domain’s): there the domain keeps the name and the need cannot be written.

Status — ubCode keeps the need for a domain name too. For a domain directive ubCode knows — the std domain’s and class — its finding says that Sphinx does not; a name ubCode does not model (function, module, …) is simply the type’s here, with no finding, where Sphinx’s py domain keeps it.

A need type and a [parse.extend_directives] entry of one name

You write both.

ubCode gives the name to the need type and does not use the declaration, and reports it (config.conflicting_directive_registration).

A Sphinx build has no extend_directives.

Unknown roles

You write text marked up with an inline role ubCode does not recognise, such as a :wibble: role.

The preview shows a placeholder chip naming the role and its content.

A Sphinx build reports an error and typically drops or marks the construct.

Status — the chip is self-describing, so you can spot the unrecognised role at a glance.

Needs and filtering

Needs and variants

Need directives render as need cards. How a card looks compared with a Sphinx-Needs build:

  • A hydrated card lists the fields you authored plus any added by needextend, and not values that exist only as a [needs.fields.*] default. This is ubCode’s own divergence, and it is in the narrowing direction: a Sphinx-Needs layout iterates the fully-populated need, so its clean shows configuration-defaulted values as ordinary rows. :layout: complete and :layout: debug restore them, per need or per project; see the :layout: entry below.

  • Cards and their links refresh after each background indexing pass completes — a bounded refresh, not one per keystroke.

  • A card’s fields, links and back-links sit in a collapsible disclosure, and :collapse: decides whether it starts closed. ubCode honours the option more broadly than a Sphinx-Needs build does:

    • It works in the editor preview and the HTML build alike, and in every card ubCode renders — including the cards a needimport renders and needs whose collapse was set from somewhere else entirely (a needextend on another page, a [needs.fields.collapse] default or predicate, a variant, a dynamic function, or the value stored in an imported project’s needs.json). In a Sphinx-Needs build the option does nothing at all unless the need’s active layout happens to call collapse_button, and it is always inert on non-HTML builders such as LaTeX.

    • The control is the browser’s own <details> element, so no JavaScript is involved and none is needed: it works in a saved page and with scripting off. A Sphinx-Needs build ships a jQuery toggle and a pair of show/hide images.

    • Before the background index has seen the need, the card renders with the disclosure open whatever :collapse: says. ubCode reads the resolved value, not the text in front of it, and the resolved value is not knowable until the index exists; the card corrects itself on the next render, exactly as its field and link rows do.

  • Each card carries a hover permalink in built output, the last thing on the header line, after the id — the same affordance a heading or a named caption has. It is the built site only — the belongs to the static build’s anchor output, which the editor preview does not render. A preview card still carries its own id, so a need stays addressable there; what the preview does not draw is the permalink. Three card shapes carry none, each for its own reason: a needextract view card, because it holds no copy of the need’s anchor to point at (the Needs extract (needextract) entry earlier on this page states that rule); a header-less design, which has no header line to hang one on and keeps whatever permalink its own regions draw — focus’s slim id footer, focus_l / focus_r’s vertical id strip; and a :hide: need’s collapsed summary line, which ubCode declines deliberately: a <summary> maps to a disclosure button, whose subtree assistive technology commonly flattens into the button’s name, so a permalink there could be visible and focusable and still go unannounced. The need’s own anchor sits outside the disclosure, so a hidden need stays linkable either way. A Sphinx-Needs build has no on a card at all. Its layouts link the id chip itself, which on the need’s own card is a self-link, and its <<permalink()>> layout function — which no built-in upstream layout calls — points at a permalink.html redirector page ubCode does not ship (needs_permalink_file and needs_permalink_data are excluded configuration).

  • A need keeps its id on its own page. When a need’s id equals the anchor a section, a :name: or a target on the same page would take, ubCode keeps the id on the need and gives the other element a de-duplicated anchor (its own anchor with a -1 serial by default; under id_style = "docutils", id1, or name-1 for a :name:), the one a second section with the same title would get. A :ref: or a named hyperlink reference to that element follows it, and nothing is reported; a Sphinx-Needs build publishes both elements under the same id, silently. The exception is a Markdown [text](#slug) link spelled as a displaced heading’s former anchor: it no longer reaches the heading and is reported as std.ref, so give the heading a (label)= target and link to that instead. This is only reachable with a need id that contains lower-case letters — valid under an id_regex that admits them; under the default regex such an id is itself reported by needs.id_regex. A card rendered by a needimport or a src-trace is not covered and can still share its id with a section.

  • :style: is honoured: each comma-separated token becomes a ub-need-style-<token> class on the card, in the order you wrote them. ubCode ships styling — in the preview and the built site alike — for the twenty style names below, which are the set a Sphinx-Needs build’s own needs_style_* stylesheet defines, and they work as it documents them:

    • fills: green, red, yellow, blue, gray, discreet, plus implemented / open / in_progress as aliases of green / red / yellow;

    • frames: green_border, red_border, yellow_border, blue_border, discreet_border;

    • trailing stripes: green_bar, red_bar, orange_bar, yellow_bar, blue_bar (note orange appears only here, and there is no gray_bar or discreet_bar);

    • clean, which strips the card’s chrome.

    It also tints the five tr_<result> styles a test-report need carries (tr_passed, tr_failed, tr_error, tr_skipped, tr_disabled), as sphinx-test-reports’ own stylesheet does (Results tables).

    A token outside that set is still a valid token if it is a legal CSS identifier: it becomes a class and is available to your own CSS, but ubCode ships no styling for it, so on its own it does nothing visible. Five differences are worth knowing:

    • The class prefix is ub-need-style-, not needs_style_. ubCode’s card is a <div> tree, not the <table> of layout regions a Sphinx-Needs build emits, so upstream card CSS could not be reused whatever the prefix were called; ub-* is ubCode’s documented, stable class vocabulary, and custom CSS you write against it keeps working.

    • Invalid tokens are rejected, not injected. A token must look like a CSS identifier (an ASCII letter or underscore, then ASCII letters, digits, underscores or hyphens — so :style: café is rejected). Anything else becomes no class at all, and ubc build html reports it as build.need_style_invalid, naming the need and quoting each rejected token. Valid tokens on the same need still apply. A Sphinx-Needs build performs no validation here — :style: a b silently adds a bare b class to the need’s table.

    • The editor preview stays silent about a rejected token. It applies the same style classes the build does (each surface uses its own theme tokens, so the two are visually equivalent rather than pixel-identical), but it has no surface for render-time findings, so run ubc build html (or read your CI log) to see the warning.

    • Order is kept and duplicates collapse. :style: blue, gray, blue yields ub-need-style-blue ub-need-style-gray — first occurrence wins its position, and the repeat is dropped.

    • Like :collapse:, the value is the resolved one, so a needextend on another page, a [needs.fields.style] default or predicate, a variant, a dynamic function, or a value stored in an imported project’s needs.json all reach the card — and, also like :collapse:, a card carries no style classes until the background index has seen the need.

  • :layout: is honoured, as a set of declarative card designs — ten built in, plus any your project defines with [needs.card_layouts] — rather than the layout DSL a Sphinx-Needs build ships. Layouts render in the editor preview and the built site alike — the two share the card markup, so that parity is free — and, like :collapse: and :style:, the value is the resolved one, so a needextend on another page, a [needs.fields.layout] default or predicate (Choosing a card design with [needs.fields.layout]), a variant, a dynamic function, or a value stored in an imported project’s needs.json all reach the card. A need with no value at all takes the project’s [needs] default_layout — the ubCode spelling of needs_default_layout, and the same key in the same table needs_from_toml maps onto it, so one ubproject.toml cannot mean complete to sphinx-build and clean to ubc build html. There is one tier ABOVE the resolved value: a .. needextract:: may write its own :layout: (and :style:), which draws every card that site renders, ahead of whatever each need resolves to for itself. The Needs extract (needextract) entry earlier on this page states the whole chain.

    A card renders as clean — not as the project’s configured default — until the background index has seen the need. This is the same “plain until hydrated” contract :collapse:, :style: and :hide: already follow, and it is where ubCode visibly differs from a Sphinx build, which has no un-hydrated state to render: in a project whose default_layout is focus, a card you have just typed flashes as an ordinary clean card for the moment before the index catches up. The card markup is produced by one writer shared with the built site, and that writer deliberately holds no project configuration — which is what makes the preview and the built site the same renderer rather than two.

    clean

    The default: header, a collapsible meta section listing the fields the need actually carries, then the content. This is the card ubCode has always rendered, and it carries no layout class at all — so writing :layout: clean produces the same card as writing nothing, as long as the project’s default_layout is clean. That condition is upstream’s too, not a ubCode qualification: a Sphinx-Needs build resolves the option with a lookup chain that treats an empty value as unset and falls through to needs_default_layout, while a written clean does not fall through. So once the project moves its default, the card with no :layout: draws that design and :layout: clean is a deliberate override — in both tools. What :layout: clean does not do is vanish: layout is a layout-visible field, so the value you wrote still shows as a meta row, the same way an unknown name’s does.

    complete

    clean’s chrome over the full effective field set — including values that come only from a [needs.fields.*] default, which clean hides as configuration padding — plus a footer echoing the resolved layout and style, as upstream’s complete layout does. Empty values are still skipped. layout and style move OUT of the meta section here, because the footer is now their home — upstream’s complete is the only built-in layout that names fields to exclude (an explicit exclude=["layout", "style"]), for the same reason.

    focus

    Content only: no header, no meta section. The card keeps its id anchor and data-* attributes and gains a slim footer carrying its permalink, so a focused need is still linkable and still appears in the built site’s search index under its own id and title. This is exactly upstream’s focus_f, and ubCode says so rather than pretending otherwise: upstream’s bare focus drops even the id, which would leave a card with nothing to link to. So focus_f resolves to this design by design — it is an identity, not an approximation — and it is upstream’s bare focus that has no ubCode equivalent. :collapse: is inert here — there is no meta section to collapse.

    debug

    clean’s chrome over every field the index can resolve for the need — internal fields, empty values and configuration-defaulted values alike — with the meta section forced open, so :collapse: is ignored. The “what can I filter on” view, and the configuration-defaulted values are what make that true: a filter resolves a field through the same default tier, so priority == "medium" selects a need whose priority was never written on it. The three designs with a meta section are therefore a chain, bar one relocation: every row survives into the next design up, except layout/style, which complete moves to its footer (clean and debug show them as ordinary rows).

    A Sphinx-Needs build does the reverse on the collapse question: its debug layout ships collapse_button(..., initial=True), which renders the card collapsed unconditionally. ubCode inverts that deliberately — a debug view that opens closed shows nothing.

    The six sphinx-needs names that add a side column to one of those designs are implemented as designs of their own, image and all:

    Name

    What it renders

    clean_l, clean_r

    clean plus a column drawing the need’s image field, on the left or the right, beside the whole card.

    clean_lp, clean_rp

    The same column, but beside the header and meta section only — the content takes the card’s full width below it. (That is what upstream’s _p grids do too.)

    focus_l, focus_r

    focus’s content-only card with the need’s id in a narrow vertical strip instead of a footer.

    Two upstream names remain documented identities rather than designs: test renders clean (upstream’s test layout is that minimal card), and focus_f renders focus (see above). Both are stated in the editor’s completion list, so nothing about the mapping is silent.

    A need with no value for the field a column draws renders byte-identical to the design the column was added to — no column, no wrapper, and not even a layout class to catch a stylesheet on. That is deliberate: it is what upstream renders for such a need too.

    Several further differences are worth knowing:

    • An unknown name is a warning, not a build failure. ubCode renders the project’s default design and reports build.need_layout_unknown, naming the rejected name, the design the card actually got, the valid set, and the needs affected. A Sphinx-Needs build raises SphinxNeedLayoutException — which derives from BaseException, so one mistyped layout ends the whole build. The lookup is case-sensitive in both: :layout: Clean is not clean. As with :style:, the editor preview stays silent about a rejected name — it renders the same default design the build does, but it has no surface for render-time findings, so run ubc build html (or read your CI log) to see the warning.

    • An empty value means unset, not “unknown”: :layout: with nothing after it renders the project’s default design and warns about nothing. That matches upstream, whose resolution is an or chain over a value an empty string is falsy in.

      Which default, exactly, is worth being precise about, because an empty :layout: is still a value: it is written on the need, so it suppresses a [needs.fields.layout] default or predicates row (those fill a field that is ABSENT), and then resolves as unset straight to [needs] default_layout, then clean. So writing :layout: on one need is how you opt that need out of a project-wide field default — not how you ask for it.

    • An empty [needs] default_layout is unset here and fatal there. default_layout = "" means “use clean” in ubCode — every optional string key in this table treats an empty value as unset, and so does :layout: itself. In a Sphinx build the same line is a build-killer: the value is the last link of the resolution chain, so there is nothing left to fall through to, and every need in the project raises SphinxNeedLayoutException. The divergence is deliberate — a configuration key whose empty value destroys the build is not a behaviour worth porting — but if you share ubproject.toml with sphinx-build, omit the key rather than emptying it.

    • An unknown [needs] default_layout is reported once, at configuration time. ubCode names it under needs.default_layout and falls back to clean, so a typo in one line costs one warning however many needs the project has — where a Sphinx build discovers it per need, at render time, and aborts on the first one.

    • A defaulted card’s layout_echo shows the resolved design. complete’s footer prints layout: complete for a card that got complete from default_layout, where a Sphinx build prints layout: and nothing after it — upstream’s footer cell reads the need’s stored field, which an unset :layout: leaves empty, while ubCode’s echoes the design the card was actually drawn with. ubCode’s is the more useful of the two, and it is what makes the echo worth having on precisely the cards that never named a design; the difference only became visible when a project could move its default.

    • You can define your own designs, with [needs.card_layouts] — but not with upstream’s DSL. needs_layouts is deliberately not implemented: its layout definition is a grid of docutils table cells filled by <<meta_all(...)>>-style callables, which describes a <table> card ubCode does not emit, and it is the mechanism behind the build-ending failure above. What ubCode ships instead is a declarative card spec — a named set of regions (header, meta, content, footer, side) and the typed elements each holds — which the ten built-in designs are written in too, so a design you define and complete are the same kind of thing, interpreted by the same code, in the preview and the built site alike. A Sphinx-Needs project can take the same vocabulary with that project’s needs_card_layouts, which compiles it down to needs_layouts.

    The layout classes are ub-need-layout-<design> (the design named clean emits none, and neither does a side-only design whose column drew nothing), and they compose freely with the :style: classes and the per-type accent. The exclusion follows the name, not the project’s default: under default_layout = "complete" a card with no :layout: carries ub-need-layout-complete and one with :layout: clean carries no class. It has to work that way — ubCode’s stylesheet keys real rules on those classes (complete’s footer alignment, the whole geometry of focus_l / focus_r’s vertical id strip), so a defaulted card that suppressed its class would silently lose them. A Sphinx-Needs build makes no exclusion at all: it emits needs_layout_clean on every clean card.

  • needs_string_links is honoured, as [needs.string_links] — a field value that matches a rule’s pattern renders as a link, in the need’s card, in a needtable, and in the editor preview and the built site alike. The matching rules are ported as they stand — the ,/; splitting, the first-declared-rule-wins precedence (with no fallthrough when its pattern does not match), the unanchored search that replaces the whole value, and the silence when nothing matches. Some behaviours deliberately differ, because the upstream one is a defect rather than a contract:

    • No trailing separator. A Sphinx-Needs meta area up to release 8.3 emits a ; after the last item as well as between items; its own needtable does not. ubCode emits separators between items only, on both surfaces — and Sphinx-Needs agrees from its next release (fixed upstream in sphinx-needs PR 1718).

    • A list field links on both surfaces. Upstream applies string links to a list field’s elements in a needtable but not in the need’s own meta area — the same field, two answers, on one page. ubCode links them in both.

    • A failed template keeps the value. When a template raises, upstream logs a warning without a location and the field’s value disappears from the page. ubCode renders the plain text and reports build.need_string_link_invalid.

    • Whitespace-only items are dropped. AB-1, , AB-2 is two items, not three (upstream tests emptiness before stripping, and renders a phantom).

    • A non-participating optional group renders empty, where upstream prints the literal None: a Python None reaches its template through the object bridge and is formatted as a string. Only optional groups — (?P<a>[A-Z]+)(?:-(?P<b>\d+))? on AB — can tell the difference.

    Further differences are consequences of the engines rather than choices about rendering:

    • A dangerous link target is refused, not emitted. A rendered link_url must be an https:// / http:// URL, a mailto: address, or a relative reference; anything else — javascript:, data:, a protocol-relative //host — draws no link and reports. Upstream emits whatever the template rendered. The check is on the template’s output, because a need field can arrive from an imported needs.json.

    • Rules are validated when the configuration loads. A missing key, an uncompilable pattern or an unparseable template is a warning against the rule, and the rest of the project keeps building; upstream raises out of the first need it renders, which ends the build even when no need carries the field. Two capabilities are missing on ubCode’s side, both stated by the diagnostic that reports them: patterns cannot use look-ahead, look-behind or backreferences — which are refused by name — nor a handful of rarer constructs (conditionals, atomic groups, \Z, (?#...)), which are refused as ordinary parse errors; the engine trades them for a linear-time guarantee. And there is no needs_render_context, so a template sees the pattern’s named groups and nothing else — which also removes the upstream footgun where a global silently shadows a group named value.

  • :hide: is honoured, as a collapsed card rather than as nothing at all. A hidden need renders its whole card — meta section, content, footer, nested needs and all — inside a closed disclosure whose summary is the card’s own header line. One compact line, expandable in place. Nothing is withheld; it is collapsed. It works in the editor preview and the built site alike, and, like :collapse:, :style: and :layout:, the value is the resolved one, so an authored :hide:, a needextend on another page, a [needs.fields.hide] default or predicate (Hiding cards conditionally with [needs.fields.hide]), or a value stored in an imported project’s needs.json all reach the card. A card renders expanded until the background index has seen the need.

    Note

    A dynamic function or a variant written as the value of :hide: does not currently reach the card: ubCode resolves neither back to a boolean, so the need falls through to the field’s default and renders uncollapsed. This is a limitation of boolean fields in general rather than of :hide: — :collapse: behaves the same way — and the string-valued fields beside them (:layout:, :style:) are unaffected. A Sphinx-Needs build does resolve both on hide (the field carries allow_df and allow_variants), so this is a real parity gap on ubCode’s side rather than a shared limitation. Tracked as issue #2910.

    Everything composes with it. :hide: beats nothing: a hidden complete card is a complete card, its :style: still paints, its type accent still paints (unless a *_border style claims that edge, exactly as on a visible card), and its :collapse: still decides whether its meta section starts closed — a second disclosure, nested inside the first, each with its own meaning. A mistyped :layout: or an invalid :style: token on a hidden need is reported exactly as it is on a visible one; hiding a need is not a way to silence it.

    The need stays linkable and findable, which a Sphinx-Needs build cannot manage:

    • the card’s id is on its root element, outside the disclosure, so a :need: reference or a link row targeting the need itself lands on it with nothing to expand;

    • anchors inside the card — a nested need’s id, a label written in the body — are reached by the browser opening the disclosure as it scrolls to the fragment. This is standard behaviour, verified in current Chromium, Firefox and WebKit; ubCode’s own shell script covers engines old enough to lack it. In the editor preview the cards render, but jumping the cursor into a collapsed card does not yet open it — the preview scrolls rather than navigating to a fragment, so nothing triggers the reveal;

    • the need keeps a full search-index record, its content included, since the body is published rather than withheld;

    • its needlist entry is now a hyperlink. This is the one change to output that a project upgrading may notice: those entries used to render as plain text, because before this a hidden need had no anchor worth pointing at. Entries in a needtable, and the nodes of a needflow or needsequence, are unaffected — they never unlinked hidden needs.

    A Sphinx-Needs build can do none of that, and the reason is structural rather than a matter of taste: its :hide: returns from need construction before the nodes.target that would carry the anchor is created (sphinx_needs/api/need.py:774-780 against :796-798), and the need node it does keep is then removed from the doctree outright (sphinx_needs/directives/need.py:325-332). So a hidden need has no anchor there at all, and a :need: reference to one resolves — without a warning — to a target that does not exist (sphinx_needs/roles/need_ref.py:271-282 builds the reference from the need’s docname, consulting nothing about hide). Its needlist special-cases hidden needs to plain text for exactly that reason (sphinx_needs/directives/needlist.py:117-118).

    Two further differences are worth knowing:

    • The body is parsed, and everything in it renders and is indexed. A Sphinx-Needs build returns before parsing a hidden need’s content, so — as its own source comment records — parts and arch are never populated and child needs are never found. ubCode parses every need unconditionally, so a hidden need’s parts and nested needs are all in the index — and, since the card renders in full, a need nested in a hidden need has its own card and its own anchor. Note that :hide: applies to the need it is written on and never to the needs inside it: nest a visible need in a hidden one and it renders as an ordinary card, inside a collapsed one. Hide the children too if that is what you want.

    • ``hide`` is exported to needs.json. A Sphinx-Needs build cannot: the field carries exclude_json: True (sphinx_needs/data.py:150-159), so a downstream consumer of its needs.json cannot tell which needs were hidden. ubCode exports the value like any other field.

    Printing a page renders hidden needs in their collapsed state — the summary lines are the compact view, which is the point of the design — so a printed page lists them without their bodies.

    :hide: is not a confidentiality boundary and never was: a hidden need’s content is published — collapsed — and is searchable, quotable and present in the page source. Nor is it the “produce no output at all” mechanism; :delete: is (see Needs). A deleted need is dropped from the index entirely; a hidden one is a full member of it that simply starts collapsed.

  • A need type’s color ([[needs.types]]) tints its cards, not just its diagram nodes: the card exposes it as a --ub-need-accent CSS custom property, and ubCode’s stylesheets use it to outline the type badge and paint the card’s leading edge (an explicit *_border style frames all four edges and takes that one; a visible card’s type badge, where its layout shows one, keeps the accent as its outline). It applies only when you have chosen a colour: a type declared without one is not accented, and neither is a built-in type still carrying its shipped colour (those exist to fill diagram nodes, and every project gets them whether or not it asked — a Sphinx-Needs build does not colour cards from them either). Change a built-in’s color and its cards accent normally. When a type colour accents a card lists the four shipped values, spells out which spellings of black and transparent count as unset, and says what to do when a colour of yours is being ignored — worth reading if you are migrating a [[needs.types]] block across, because those four values are exactly what you will be carrying.

    Only #RGB, #RRGGBB and #RRGGBBAA are used for the card; any other value is ignored there, and diagrams are unaffected throughout — they keep every colour, including the built-in ones. A Sphinx-Needs build uses color for diagrams only, and documents needs_type_<Name> classes as the way to colour cards by type.

  • :template:, :pre_template:, :post_template: and :jinja_content: are accepted and then ignored. Writing one on a need produces no diagnostic — it is a recognised option, not an unknown one — but nothing about the need or its card changes, and the value reaches neither needs.json nor a filter. A Sphinx-Needs build renders the need’s content through the named template. [needs.global_options] entries for template, pre_template and post_template are silently accepted and ignored the same way; a jinja_content entry there is instead reported as an unknown global option.

  • A falsy .. if:: block, and each branch a .. choose:: does not take, stays visible but collapsed and greyed out, labelled with its condition; a Sphinx-Needs build omits it entirely.

  • A need referenced only inside a non-indexed include fragment (for example an extensionless or excluded file) keeps its placeholder chip, because the background index does not look inside such fragments.

  • A needlist with no :sort_by: lists its needs in id order; a Sphinx-Needs build lists them in document order. ubCode sorts by id so the output is reproducible build-to-build — the incremental index has no stable document-processing order to preserve. Giving :sort_by: a field name orders by that field in both.

  • :need_count: — and every needpie slice, needbar cell and needreport metrics row, which all count through the same code path — counts needs only; a Sphinx-Needs build counts needs and their parts in the filtered case. (The plain total counts needs in both.) In particular, a count filtered on the is_part context constant renders 0 (need parts are not first-class filter items), where a Sphinx-Needs build counts the parts. The parts themselves are modelled — they resolve in a :need: role, they are validated, they are written to needs.json, and a needtable with :show_parts: renders a row for each — they are simply not things a filter expression selects.

  • :filter-func: is accepted but never executed on every view directive that offers it — needlist, needtable, needflow, needsequence and needpie. It names a Python callable for a Sphinx build to import and run; ubCode does not execute project Python, so the option is reported as needs.filter_unsupported and the directive still renders from its remaining filter surfaces (a chart, from its content lines). Use the portable :filter: expression, :status: / :tags: / :types:, or :cypher: where the directive offers them.

  • :export_id: is deprecated in Sphinx-Needs, which warns about it too and points at its needs_debug_filters configuration instead. ubCode ignores the option and reports needs.option_deprecated — its own code, deliberately not the needs.filter_unsupported above, so that silencing “ubCode cannot run project Python” does not also silence “stop using this everywhere”. It is reported the same way on every view directive that handles the option: needlist, needtable, needflow, needsequence and needgantt. Delete the option.

  • A needtable and a needlist cap their rendered rows/items at 100 by default and append an honest “Showing the first 100 of N needs” notice when the cap bites; a Sphinx-Needs build renders every matched need. The cap keeps an unfiltered view on a large project from dominating render (and live-preview) times. Set :max_items: 0 for no cap, or :max_items: N for a specific limit; the cap applies after the filter and the sort. The 100 is the default of needs.views_max_items, which sets the cap project-wide for all four view directives. Whenever a cap bites, ubc build html also emits a build.need_view_truncated warning naming the directive and the shown-vs-total, so a truncation is visible in the build log too.

  • A needtable is interactive by default, as it is in a Sphinx-Needs build: the reader sorts by column, filters the rows, pages through them, hides columns, and copies or downloads the table as CSV. That is what :style: datatables — the Sphinx-Needs default, and ubCode’s through [needs] table_style — now means. It is not DataTables: the two tools ship the same hand-written asset against the same markup contract, whose design record (design/needstable-contract.md in the Sphinx-Needs repository) is the reference for both. So a table’s rows, its :style_row: classes and the links inside its cells are the same nodes before and after — the widget enhances the table in place and never re-renders a cell. :style: table is the opt-out and keeps the static table inside a scroll frame with a sticky header row; set [needs] table_style = "table" to make that the project-wide default. The rows per page come from :page_size: or [needs] table_page_size, and the sizes offered from [needs] table_page_sizes. The widget and its stylesheet (_static/needstable.js and _static/needstable.css) are linked only on pages that hold an interactive table, so a page of prose — or one whose tables are all :style: table — downloads neither.

  • The interactive table pages the rows the cap allowed. :max_items: (and [needs] views_max_items) still truncate server-side, before the page is written, so a table over more needs than the cap allows shows the notice and pages within what it shows. Raise :max_items: — or set it to 0 — to page the whole selection.

  • :style_row: works, and every row carries the Sphinx-Needs row classes — need on a need’s row and need_part on a :show_parts: part row, with the :style_row: value appended after it, so the worked tr.needs_implemented { … } examples in the Sphinx-Needs documentation and any stylesheet you already have keep working. The value runs through the dynamic-function evaluator per need, so :style_row: needs_[[copy('status')]] colours each row by its own status, and whitespace in the result becomes _. Two limits, the second shared with a Sphinx build: ubCode resolves all seven Sphinx-Needs built-in dynamic functions (echo, test, copy, links_from_content, check_linked_values, calc_sum and links_from_filter) and renders ?? for anything else — where a Sphinx build also resolves the functions a project registers through needs_functions; and only the first function call in the value is resolved, which is Sphinx-Needs’ own behaviour, so a second, different call is left standing as literal text in both tools. Part rows do not get the :style_row: class, also matching a Sphinx build.

    When a function does not resolve, whether you are told depends on why. A Sphinx build warns on every failure; ubCode splits them, because one half is per-table and the other is per-row:

    • A call that cannot work for any need — it does not parse, it names a function ubCode has no built-in for, or its arguments are a shape that built-in cannot take — reports needs.dynamic_functions once, on the directive, naming the call and saying that ?? rendered in its place. A typo’d function name is this case.

    • A call that is well formed but fails for a particular need — most often [[copy('status')]] on a need that has no status — renders ?? for that row and says nothing. A Sphinx build warns once per affected row; doing the same on a table of two hundred needs is the un-deduplicated warning spam ubCode set out to remove, so the ?? in the class is the only signal here.

  • :show_parts: works: each need’s parts render as extra rows directly under it, with ID showing <need>.<part> and TITLE the part’s content, both prefixed with needs.part_prefix (default "→ ", an arrow and a non-breaking space — the same default as Sphinx-Needs’ needs_part_prefix). A part row’s outgoing-link columns are empty, its incoming-link columns show that part’s own incoming links, CONTENT shows the part’s content, and IS_PART / IS_NEED are true / false — all as in a Sphinx build, whose part item is an overlay on the parent need. Every column the overlay does not touch shows the parent need’s value in both tools: STATUS, TYPE, TAGS, your own fields, and docname / lineno / the is_* origin fields. (If you declare your own field called is_part or is_need, yours wins on a part row and on every need; a part tested by an include_parts rule answers its own is_part, is_need, id_parent and id_complete. Sphinx-Needs refuses such a declaration outright — Field 'is_part' already used internally at startup — so no Sphinx build that runs can observe the difference.)

    A Sphinx build also lists the parts a :filter: passes as items of their own, in a needtable and a needlist alike, and so shows each part twice under :show_parts:; ubCode’s table rows are needs, and a part appears once, as a :show_parts: row.

    Three differences from a Sphinx build:

    • The part id links to the parent need’s anchor, not the part’s own — ubCode does not yet give a part its own anchor in a built page, so there is nothing narrower to point at. A Sphinx build links #REQ_1.p1.

    • The part rows are ordered by part id, where a Sphinx build orders them by position in the document (id order is reproducible for parts read from a needs.json as well as from a page, the same reason :sort: breaks ties by id).

    • :max_items: counts needs, so a need that survives the cap keeps all of its part rows — which is what a Sphinx build does too.

    id_complete and id_parent are columns: the need’s own id on a need row, and on a part row <need>.<part> and the owning need’s id. parts is a column ubCode does not answer at all; it renders empty on a need row and on a part row alike, which is the same unsupported-column gap listed further down rather than anything specific to parts. parent_need used to be in that list and no longer is: it is answered from need nesting, as the first entry of parent_needs in the sorted order the column beside it shows — so the two never disagree about which need comes first. On a part row it is still empty — a part has no nesting parent of its own; the owning need’s id is id_parent.

  • :style_col: does nothing — in a Sphinx build either. Sphinx-Needs declares the option and stores it, but never reads it back, and it is absent from its own documentation. ubCode reports needs.option_unsupported saying so — the same stance it takes for needgantt’s no-op options above — so the dead option is not silent; delete it.

  • A needtable with no :sort: sorts by id (case-insensitively), matching a Sphinx-Needs build’s default; where two needs would tie on the sort key, ubCode breaks the tie by id rather than by document order (the same reproducibility reason as needlist above). The :columns: and :sort: options accept the same names as Sphinx-Needs; an unknown name raises a warning with a “did you mean” suggestion and renders an empty column (:columns:) or falls back to id order (:sort:).

  • :columns: also accepts several of the Sphinx-Needs core fields — docname, lineno, is_external, is_import, is_need, is_part, id_parent, id_complete, is_modified and modifications — plus ubCode’s own origin-kind fields is_directive and is_src_trace. (content was accepted before too, but rendered an empty cell; it now renders the need’s body text.) A Sphinx-Needs build renders every core field as a column; the ones ubCode does not answer yet — among them external_url, type_name and lineno_content — still warn and render empty. Service-injected fields such as service and user stay unknown unless you declare them as fields in ubproject.toml.

  • The same names are now readable in a :filter: expression, which has two knock-on effects if you were already using those spellings. lineno previously fell through to a [needs.filter_data] key of that name; it no longer does. And a project that declares its own field or link called content now gets the need’s body in a :columns: content cell, matching what a :filter: on content has always read — id, type and content are the need’s identity and nothing you declare overrides them. Your own is_need, docname and the like do still win, as before.

  • :sort: accepts the core fields above, so :sort: lineno puts a table in source order and :sort: is_external groups the needs read from an external source last. (Needs pulled in by needimport are a different origin — order those with :sort: is_import.) :sort_by: reads the same names, though it still ignores an unrecognised key without warning, and — unlike :sort: — it matches the key case-sensitively, so :sort_by: LINENO silently leaves the order alone. The two option vocabularies are otherwise the same, with two exceptions. id_complete sorts (it is the default) and is now a column too. And a link’s presentation spellings are columns but not sort keys — <link>_back, the outgoing / incoming aliases, and any custom incoming / outgoing label you declared for it. Sort on the link’s own name instead, which orders by the same values its column shows.

  • Sorting is now by value, not by text. A numeric field orders 2, 9, 10; previously every sort key was stringified, which ordered them 10, 2, 9. This affects :sort: and :sort_by: on any numeric field you already use, so an existing table may legitimately change order. Where a key has no value at all those needs sort first, and where one field mixes types the order is booleans, then numbers, then text — a Sphinx-Needs build raises TypeError on that mixture rather than choosing, so ubCode defines an order instead of failing the build.

  • :colwidths: sets the column widths, given as one positive whole number per column, separated by , or ;. The numbers are relative shares of their total, so 50,40,10 and 5,4,1 lay out identically — the same normalisation a list-table :widths: gets, from the same code. (As with any table on the site, the widths are what the browser is asked for; an unbreakable cell can still push a column wider. A Sphinx-Needs build is in the same position.) Anything else raises a warning and renders the table without column widths: a value that is not a number at all (including the 50% form the Sphinx-Needs documentation invites), a zero or negative one, one above 18446744073709551615, or a count that does not match the number of columns — with :columns: unset, that count is the six default columns. A Sphinx-Needs build fails on most of those, so a table that used to stop your build now renders with a warning. Some refusals run the other way, because ubCode reads a stricter grammar than Python’s int — ASCII digits with an optional +, no _ separators, and that upper bound. A Sphinx-Needs build accepts a zero or negative width, rendering a zero or negative percentage, and has no upper bound at all, so those are refused here and not there. Refusing a zero is Docutils’ rule rather than Sphinx-Needs’, which validates nothing but the count: on Docutils 0.22 a zero-width column aborts the build when the page is written, so the widths Sphinx-Needs lets through are ones a current Docutils will not render at all. With no :colwidths: at all, ubCode leaves the widths to the browser; a Sphinx-Needs build always writes a column group, giving a title column three times the width of each of its neighbours (and equal widths when no column is title).

  • Two of those columns read differently from a Sphinx-Needs build. A boolean cell shows true/false rather than True/False — ubCode renders every boolean the same way wherever it comes from, including your own boolean fields and the built-in collapse / hide. And for a need written in an included fragment, docname names the page the card is rendered on while lineno is the line in the fragment — the position your editor can actually open. A Sphinx-Needs build instead numbers lines in the spliced document, so its lineno is shifted for every need at or after an include.

See also

RST preview for how need cards hydrate, Variants for the variant authoring constructs, and the if directive for variant conditions.

Dynamic functions resolve all seven built-ins, and their filters

ubCode evaluates every one of Sphinx-Needs’ built-in dynamic functions — echo, test, copy, links_from_content, check_linked_values and calc_sum, and links_from_filter of Sphinx-Needs 9.0.0 — with their filter arguments (filter_string on check_linked_values), wherever a [[…]] may be written: a need’s option and link values, a needtable’s :style_row:, and the :ndf: and :need_func: roles. Arguments may be literals or need.<attr> references. Released versions of Sphinx-Needs accept need.<attr> only in an :ndf: role inside a need and in a needtable’s :style_row:; Sphinx-Needs 9.0.0 accepts it in option values too (see “A need.<attr> in an option value” below).

What a Sphinx build still does and ubCode does not:

A filter that reads beyond the candidate need

A filter naming needs or current_need, or reading a c. attribute such as c.this_doc(), and a filter given as a need.<attr>, are not evaluated here: the call is reported once, at the site you wrote it, as an informational needs.dynamic_functions finding, and the value is left at its placeholder. A Sphinx build evaluates them in some calls, and not always as the reader: needs in calc_sum, copy and links_from_filter, a c. attribute in copy and links_from_filter, and current_need in every call — though in calc_sum and check_linked_values it is the need tested, not the reader. For a sum over the needs passing a test of other needs, the portable form is a boolean derived field the sum filters on.

Functions your project registers

A function added through needs_functions is not resolved. ubCode cannot run project Python. The call is reported once at its site and renders ??, or keeps the role’s chip where a role wrote it.

Where the two tools resolve the same call to different values, they do so in these places:

A call that reads a value another call computes

ubCode resolves the calls in dependency order: a call reads the value the other call computes, a value on a cycle of reads keeps its empty value with a needs.derive_cycle warning, and a read that cannot be ordered is needs.derive_scope (see When a call reads a value another call computes). The answer is the same on every build. Released versions of Sphinx-Needs have no defined answer here: their single resolution pass mutates needs in place in the order they were added, so the value depends on document order — and on whether the build was incremental — and where the pass reaches the reading need first, a text field stores the string None; neither finding is reported. Sphinx-Needs adopts the same dependency order, and the same two findings, in its 9.0.0 release; until then, for the ordinary case — a call reading fields that are not themselves calls — the two tools agree.

A sum over every need

calc_sum('hours') without links_only adds its summands in id order in ubCode, the ids compared character by character, and in the order the needs were added in a Sphinx build. Floating-point addition depends on that order, so a total can differ between the two tools in its last digits: 0.1, 0.2 and 0.3 total 0.6000000000000001 added in that order, and 0.6 added in reverse. ubCode’s total is the same on every build; a Sphinx build’s depends on document order, and on whether the build was incremental. Adding in that id order is planned for Sphinx-Needs too.

An unknown need_id

copy('x', need_id='NOPE') and links_from_content(need_id='NOPE') both fail the call, reported as needs.dynamic_function naming the id, and the field keeps its empty value — as a Sphinx build reports both (a KeyError for one, “no stored node” for the other). A need_id — or a content_links rule’s from — naming an external need, which has no content, fails the same way in both. In a role, both render ?? here, where a Sphinx build renders ?? for copy and [] for links_from_content, which it does not raise.

A filter a need cannot be evaluated on

A need lacking a field the filter compares (hours > 3 on a need without hours) is treated alike by both tools: calc_sum still counts it, check_linked_values still tests it, copy drops it, links_from_content fails the call, and links_from_filter and a derived link type’s where let it not pass, silently. The finding differs in number only: a Sphinx build logs needs.filter for every such need under calc_sum and check_linked_values (once per call under copy), ubCode once per call, naming the needs.

is_modified after a dynamic function

A need whose own [[…]] computed a value counts as modified in ubCode — is_modified is true in a :filter:, in ubc query and in a :columns: cell — where a Sphinx build marks only the needs a needextend changed. A derived link type’s result marks nothing in either tool.

:need: references of a hidden need

links_from_content and a content_links derived link type read every :need: reference of a need’s content in ubCode, on a :hide: need too — as the carrier, or named by a from / need_id from a visible need. Sphinx-Needs 9.0.0 reads none of a hidden need’s: the call and the rule give [] there, with a needs.dynamic_function warning (links_from_content: no stored node for need 'HIDE_ONE', at the hidden need), until a later release decides how a hidden need’s content is processed. On any other need both tools read every reference, inside an only block or a figure caption included.

A link list in an array field

A non-empty [[links_from_filter(…)]] or [[links_from_content()]] written in a field declared as an array of strings stores the need ids in ubCode. Sphinx-Needs 9.0.0 refuses the value (dynamic function value <class 'list'> is not of type 'array' or item type 'string') and stores nothing — a defect in how it types the list, to be fixed there. The empty list passes in both.

A need.<attr> outside any need

An :ndf: role written outside a need body cannot read need.status, and renders ??. A Sphinx build renders the same ?? and additionally warns.

A need.<attr> in an option value

:total: [[calc_sum(need.which)]] reads the need’s own which in ubCode and sums the field it names; when which is itself a call, what the call reads is not known in time, so it is needs.derive_scope and not run. Released versions of Sphinx-Needs refuse the value when they read the directive (Need could not be created: … Unsupported arg 0 value type), so the need does not exist there at all; written in a needextend option, the option is skipped with a needs.needextend warning and the extend’s other options still apply. Sphinx-Needs 9.0.0 accepts need.<attr> in option values, as ubCode does. To build with both tools until then, name the field in the call: [[calc_sum('hours')]].

A need.<attr> that holds a list

ubCode joins a list-valued selector into one name with , and a space, so [[copy('title', need.links_back)]] on a need with one backlink copies that need’s title, where a Sphinx build fails the call. With two or more backlinks the call fails in both tools. With none it fails in ubCode, where a Sphinx build takes the empty list for no need_id and copies the need’s own title. [[links_from_content(need.links_back)]] gives an empty list in ubCode whatever the list holds, where a Sphinx build fails the call for a non-empty list and reads the need’s own content links for an empty one.

The dead-link flags

has_dead_links (a link names a need that does not exist, or a part its need does not have) and has_forbidden_dead_links (such a link is in a link type that does not allow dead links) are set once every link is final, with the backlinks, in both tools: a field’s call or variant condition reads them ([[copy('has_dead_links')]], <<[has_dead_links]: …>>), as does calc_sum, and a link’s is needs.derive_scope. A view directive’s :filter: reads them and a needtable shows them as a column, where a <link>_back field is read. The differences that remain:

  • They are not available in a needextend filter: ubCode reports such a filter and applies nothing, where Sphinx-Needs evaluates it against False with no warning, so not has_dead_links extends every need there and has_dead_links == True none.

  • ubCode does not write them to needs.json, where Sphinx-Needs writes both on every need.

  • A needtable cell reads true / false, as every boolean cell does, where Sphinx-Needs prints True / False.

  • A field the project declares with one of the two names is its own in ubCode, everywhere, where Sphinx-Needs rejects the directive option and reads its flag; a link type of the name is its own in ubCode too, where Sphinx-Needs accepts it and fails the build as soon as that flag is true on any need (any dead link for has_dead_links, a forbidden one for has_forbidden_dead_links; the flag is written over the link type’s value). Rename such a field or link type to build alike in both.

  • An imported need whose parent_needs names a need that does not exist has a dead link in ubCode, which keeps the imported value, so both its flags are set; Sphinx-Needs recomputes parent_needs from the nesting of the needs, so the need has no parent there, and no dead link.

A link field on a need that has none

copy('links') on a need with no :links: option copies an empty value in both tools — an empty list, which an array field holds and a text field refuses — check_linked_values finds no match on such a need in both, and a variant condition reads an empty list there too: <<['T_ONE' in refs]: yes, no>> gives no. The stored needs.json still differs: Sphinx-Needs gives every configured link type a key on every need, where ubCode writes only the link types a need actually uses.

check_linked_values and part links

Released versions of Sphinx-Needs look a linked need up by its full link string, so a link to a need part (ID.PART) — or to no need at all — fails the whole call, as it does a calc_sum with links_only. ubCode resolves the parent need and compares against that instead, and skips a link to no need (the dead link is reported on its own), as Sphinx-Needs 9.0.0 does. It also compares values type-strictly, where Python’s in treats True and 1 as equal — so a mixed list such as ['a', 1] may match in a Sphinx build and not here.

test() echoes a need.<attr> as you wrote it

[[test(need.id)]] on need REQ_1 prints args: (need.id) in ubCode and args: (REQ_1) in a Sphinx build, which substitutes the attribute before test ever formats it. For the same reason test(need.nope) — an attribute the need does not carry — renders ?? in a Sphinx build and prints need.nope here. test is a debug helper, so this affects nothing but its own output.

Numbers and booleans a function returns

In an :ndf: role or a :style_row:, calc_sum’s float renders 22.0 rather than 22 — matching Python’s own str(), which is what a Sphinx build prints — and a boolean renders True / False for the same reason. In the rendered text, very large and very small floats are the one place the two differ: Python switches to exponent form (1e+20 for 1e20, 1e-05 for 1e-5) where ubCode writes the digits out (100000000000000000000.0 and 0.00001). In a need’s field a result is not rendered at all: as in a Sphinx build, it must have the field’s type, so a number, a boolean or a list in a string field is a needs.dynamic_function warning and the field stays empty (see When ubCode cannot resolve a call), as is a list in an integer, number or boolean field and an item of another type in an array field. A boolean is not a number either: true in an integer or number field, or as an item of an integer or number array, is the same warning here, where a Sphinx build stores it, because Python counts a boolean as an integer, and then reports the stored value as a schema violation. An integer is accepted in a number field and stored as a float (77.0), where a Sphinx build keeps the integer (77).

A function that returns nothing

copy('x') of a field the need does not set and that has no default returns None. In a field that may not be empty — an extra_options entry without a schema, or a field declared nullable = false — both tools warn (needs.dynamic_function) and leave the field empty. In a field that may be empty, ubCode leaves it empty (null) and reports nothing, where a Sphinx build stores the text None in a string field and [None] in an array field, which its schema validation then reports as an error; a planned Sphinx-Needs release leaves it empty too.

Derived fields need sphinx-needs 9.0.0

A derived field — a derive table on a [needs.fields.<name>] or [needs.links.<name>] entry — is read by sphinx-needs from its 9.0.0 release, with the same kinds, roles and findings.

A sphinx-needs older than 9.0.0

ignores the derive key without a warning, so a Sphinx build leaves the field authorable and unset where ubCode closes it: an option an author writes on it is stored there, and reported (needs.derive_authored) and ignored here. ubCode cannot see which sphinx-needs a project builds with, so it reports config.derive_sphinx_minimum (info) once on every project that declares derive; silence it once the project builds with sphinx-needs 9.0.0 or later.

Once both tools compute a rule

ubCode computes every kind with sphinx-needs 9.0.0’s values and findings, checked by the conformance corpus both tools run. Both order the reads the same way: a where or a links_from_filter naming a field a value kind computes is needs.derive_scope and links nothing, since the field is computed after the links; and a call or a rule without after = "derived" reading a hash field, or one with after = "derived", is needs.derive_scope and keeps its empty value. What still differs:

  • A hidden need’s :need: references are read by a content_links rule in ubCode and by none in sphinx-needs (see the entry on a hidden need’s references above).

  • A hidden need’s parts are candidates of include_parts in ubCode, which reads a hidden need’s content, and are not in sphinx-needs, which collects no part of a hidden need.

  • In needs.json, ubCode writes no <link>_back key, and a link type a need does not carry is absent from that need, where sphinx-needs writes [] — a derived link type whose rule computed nothing included (#4035 writes every declared link type). The values a need carries are the same in both.

  • A derived field or link type does not make its need is_modified in either tool; a need’s own [[…]] does in ubCode (see “is_modified after a dynamic function” above).

  • A derive_by_type entry keyed issue, pr or commit, or need in a project that declares no [[needs.types]], is accepted by sphinx-needs and refused in ubCode (config.derive_invalid, “not a need type”). sphinx-needs registers the GitHub service’s need types in every project, and its default need types are ubCode’s four plus a legacy need type, which ubCode does not have; ubCode knows only the project’s own need types (declare the type under [[needs.types]] and ubCode reads the entry too), and creates no service needs, so the rule has no need to compute on here. The values on the need types both tools know are the same. A cycle finding that lists need types names those extra types too in sphinx-needs. A derive that cycles on every need type ubCode knows is reported as cycling everywhere in ubCode (“The field keeps its empty value”) and per type in sphinx-needs (”… on every other need type the rule applies”) when sphinx-needs still has a type the cycle leaves out: a derive_by_type entry on a field of the cycle naming a type only sphinx-needs registers, or entries naming every one of ubCode’s types. A plain cycle, with no derive_by_type entry on the cycle’s fields, reads as cycling everywhere in both.

The configuration finding

A rule that cannot be read is config.derive_invalid in ubCode and needs.derive_invalid in sphinx-needs: each tool keeps its own family for configuration findings, so one ignore entry does not cover both.

A field named parent_needs

A [needs.fields.parent_needs] entry replaces the built-in parent_needs link type in ubCode; sphinx-needs reports and ignores it. A field named after any other link type is refused by both tools.

needextend renders nothing in a built page

You write a .. needextend:: (or the MyST {needextend} fence), naming a need id or a filter, with one option per field you want changed.

The preview shows the directive as a small chip listing the id or filter you named and the changes you asked for, so an otherwise invisible construct is visible while you author it.

A Sphinx build renders nothing at all for it, and neither does ubc build html — matching a Sphinx-Needs build exactly.

Status — the chip is a deliberate ubCode-only authoring affordance, on the same footing as the chips for .. index::, the state-changing directives and block hyperlink targets.

The directive’s effect is untouched by any of this. The extend is applied while your project is indexed, before any page is rendered, so every card on every page already shows its post-extend values — including cards written above the needextend in the same file, and cards on entirely different pages. What a built page no longer carries is the directive’s own source: the id or filter being extended, and the raw option rows such as +tags — addressed to an author, not to a reader.

Three things follow that are worth knowing:

  • A dead extend renders nothing either. A needextend naming an id that does not exist, or a filter that matches nothing, renders nothing in a built page just as a working one does. The dead id is still reported, as needs.extend_missing_id, while you type; a filter that matches nothing stays silent, which is what a Sphinx-Needs build does too.

  • No anchor is lost, because there was never one. needextend mints no cross-reference target in ubCode, so nothing could have linked to the site. A Sphinx-Needs build does mint one in its document tree, but it does not survive to the page either: docutils moves the id onto the node Sphinx-Needs then deletes, so a real Sphinx build publishes no anchor for a needextend at all.

  • Site search is unaffected. The chip’s text was already excluded from ubCode’s search index, so removing it from built pages changes no search result.

Before this, a built page printed a framed placeholder box for every needextend, so the id being extended and the operators you wrote appeared in the reader’s prose. That is what changed.

External need sources

An [[needs.external]] source reads another project’s needs.json, the same job Sphinx-Needs gives needs_external_needs. Where the two differ, they differ in one consistent direction: a Sphinx build raises and stops, while ubCode reports and carries on with as much of the source as it can use — a source that is momentarily wrong should not cost you the whole project’s index.

These differences reach indexing and diagnostics, so they change what ubc check and the editor report, not only what a page looks like.

A source with no base_url

You write a [[needs.external]] entry with a path but no base_url.

ubCode reads the source’s needs and reports one warning for the source saying they have no external_url, so every reference to them — tables, lists, the :need: role — renders as plain text instead of a link. A source that contributes no needs stays silent: there are no references to be unlinked.

A Sphinx build refuses the source outright and fails the build with “base_url must be configured in external_needs item <n>”.

Status — deliberate. URL-less external sources have always been allowed here, so refusing them now would break projects that rely on it. The warning exists because “external needs render as plain text” is the symptom nobody attributes to a missing configuration key. Set base_url for linked references in both.

A target_url that fails to render

You write a target_url template that parses but cannot be rendered against some need’s data — an unknown filter, or an operator applied to a field that is not the type it expects.

ubCode ingests every need anyway; the ones whose URL failed simply have no external_url, and their references render unlinked. The failures are reported once per distinct error message rather than once per need, each naming how many of the source’s needs it cost, the smallest affected id, and the error itself. Read the count as the diagnosis: all of them means the template is wrong, a few of them means those needs’ data is not what it expects.

A Sphinx build raises the Jinja error and fails the build on the first need that hits it.

Status — deliberate, on the same “report, don’t stop” principle. A template that does not parse is a separate case, reported once for the source before any need is read.

A needs_schema field_type of the wrong JSON type

You write (or export) a needs.json whose needs_schema gives some property a field_type that is not a string — a number, null, or a list.

ubCode cannot parse the file, so the source contributes no needs at all; the message names the JSON path of the offending property.

A Sphinx build never reads field_type — it takes only each property’s default — so the value is inert whatever type it has.

Status — quote the value ("extra") to fix it. A property that simply omits field_type, or states a value ubCode does not recognise, is no longer a divergence in what gets read: it is reported once, naming the JSON path and the accepted values, and the source’s needs are read as before, that property’s default applied exactly as a Sphinx build would apply it. Only the wrong JSON type still stops the read.

The [test_reports] section

ubCode reads the [test_reports] section sphinx-test-reports reads, and adds the same need types and fields. Where the two read it differently:

The section is the switch

You write extensions = ["sphinx_test_reports"] in conf.py, and no [test_reports] table and no [parse] extensions entry in ubproject.toml.

ubCode adds no test-report need type or field: it does not read conf.py, so importing the converter’s needs.json reports Invalid need type: testcase.

A Sphinx build registers the types and fields because the extension is loaded.

Status — deliberate: add a [test_reports] table, an empty one will do, or mirror the conf.py list into [parse] extensions; both tools then read the same settings.

Switched off with the section kept

You write "sphinx_test_reports" = false under [parse.extensions] beside a [test_reports] section (or under a section inherited through extend).

ubCode keeps the section’s settings and withdraws the extension: no need types or fields, and its directives render as labelled placeholders, unless a need type of the same name owns them. Switched off, the extension ranks below everything else that claims a name: a [[needs.types]] entry or a [parse.extend_directives] entry of the same name owns it. An explicit false under either of its two names beats an explicit true under the other.

A Sphinx build has no such switch: the extension is loaded or it is not, and an unloaded one leaves its names to whatever else registers them.

Status — a ubCode-only switch, so one ubproject.toml can carry the settings for a build that does not load the extension.

The directive names belong to sphinx-test-reports

You write a [[needs.types]] entry whose directive is test-case (or test-file, test-suite) while sphinx-test-reports is switched on.

ubCode gives the name to sphinx-test-reports: .. test-case:: reads a test report, the type is kept, and config.conflicting_directive_registration says so.

A Sphinx build gives the name to whichever registers it last, which for two config-inited registrars is the order of extensions: listing sphinx_test_reports after sphinx_needs (the order sphinx-test-reports documents) gives it the name, the other order gives it to the type.

Status — deliberate: ubCode reads no conf.py, so it fixes the documented order.

rootdir left unset

You write a [test_reports] section without rootdir.

ubCode reads relative report paths from the source directory, [source] dir.

A Sphinx build reads them from its configuration directory, the one holding conf.py.

Status — deliberate: ubCode has no conf.py. The two agree whenever conf.py sits in the source directory, the usual layout; otherwise set rootdir, which both tools read from the directory of the ubproject.toml.

No conf.py layer

You write tr_* values in conf.py, or pass them with sphinx-build -D.

ubCode reads only the section, with a -c override in place of -D: -c wins over the ubproject.toml, which wins over the default.

A Sphinx build reads -D, then the section, then conf.py, then the default.

Status — move tr_* values into the section, where both tools read them.

A need type the project declares itself

You write a [[needs.types]] entry named testcase (or testfile, testsuite) beside a [test_reports] section.

ubCode uses your declaration unchanged, and reports config.test_reports_type_declared when its title, prefix, color or style differs from the section’s.

A Sphinx build stops with “already exists as need type”.

A link target that is not a link type

You write property_link_types = { verifies = "verifies_link" } without declaring verifies_link under [needs.links].

ubCode reports config.test_reports_link_target_undeclared, and the property’s values are not linked.

A Sphinx build’s test-report directives stop at the first test case carrying the property, with InvalidNeedException; a Sphinx import of a needs.json warns and drops the key.

A need-type table that leaves keys out

You write [test_reports.case] with only some of its six keys.

ubCode takes each key left out from its default.

sphinx-test-reports refuses the configuration, naming the missing keys.

Status — write all six keys while the project is also built with Sphinx.

A renamed directive

You write a directive other than test-file, test-suite or test-case.

ubCode keeps the setting, keeps the default name, and reports config.test_reports_rename_unsupported.

A Sphinx build registers the directive under the new name.

Status — renaming is deferred, tracked in #3845.

The test-report directives

ubCode supports sphinx-test-reports’ test-file, test-suite and test-case directives (Test reports: the directives), one need each, and a test-file’s :auto_suites: / :auto_cases: expansion into a need per suite and case. Where the two differ:

A mistake in a directive

You write a test-case without :suite:, a :suite: or :case: the report does not have, a :collapse: that is not true or false, no :file:, or a report that is not well-formed XML.

ubCode reports it on the directive (a needs.test_report_* warning), creates no need for that directive, and carries on; the directive keeps the generic directive box on the page. A directive that loses a duplicate-id contest keeps the directive box too: no second card and no duplicate HTML id, unlike two need directives sharing an id, which both render a card.

A Sphinx build stops with an exception (TestReportInvalidOptionError, TestReportFileNotSetError, an lxml syntax error, a duplicate id, …).

Status — deliberate: one bad directive does not stop a build.

One difference runs the other way: an extra_options name is matched as written, so :TestType: unit sets the TestType field, where a Sphinx build lowercases the option name and refuses it as unknown (a mixed-case extra option reaches a Sphinx need only through a report property).

An option docutils refuses

You write an option the directive does not take, an option with no value, a value on the :auto_suites: or :auto_cases: flag, or no title.

ubCode reports it (block.directive_unknown_option, needs.test_report_option_invalid or block.directive_no_argument) and creates no need for that directive.

A Sphinx build reports a docutils error and drops the directive, creating no need either. Both tools treat an unknown option on a need directive differently: the need is kept and the option dropped.

Status — the same in both.

A report that does not exist

You write a :file: naming no file, or naming something that is not a readable file.

ubCode reports needs.test_report_missing and renders the directive as the generic directive box.

A Sphinx build warns (an untyped warning, so it cannot be suppressed by type); a test-file renders “Test file not found”, and a test-suite or test-case then stops the build.

Status — deliberate.

Counters for an attribute the report leaves out

You write a test-file or test-suite over a report whose suites have no skipped attribute (googletest writes none).

ubCode counts the skipped cases (skipped: 1) and deducts them from passed.

A Sphinx build adds -1 for each missing attribute (skipped: -2) and leaves passed as the report states it.

Status — deliberate: a counter is never negative. The same rule is proposed for sphinx-test-reports (sphinx-needs#2054).

A test case’s evidence

You write a test-case for a case with several failures, or with system-err output.

ubCode appends every failure, error and skip — each message left out when its text already says it, the text’s own indentation kept — then System-out and System-err: the rendering the test-reports build needs converter writes.

A Sphinx build appends the first part only, as **Text** and **Message**, each line stripped of its indentation, then System-out, and no System-err.

Status — deliberate: one rendering for both routes, the converter’s (sphinx-needs#2055). ubCode keeps every byte of report text literal: whatever line terminator a line of it carries, it stays inside its literal block and is never read as markup.

Generated ids and id_regex

You write a directive without :id:, in a project with the default [needs] id_regex.

ubCode gives it the generated id (testfile86527, or testcase__MathTest__Subtraction_srmht) and does not check it against id_regex. An :id: you write is checked.

A Sphinx build checks the generated id too, and stops: both forms contain lower-case letters.

Status — deliberate.

A :file: outside rootdir

You write :file: ../../secret.xml, or an absolute path, leaving [test_reports] rootdir.

ubCode reports needs.test_report_path_outside_root and does not read the file.

A Sphinx build reads it.

Status — deliberate: the editor indexes a project as soon as it is opened, so a path written in a document may not read files outside the root its configuration names (the needreport :template: rule). Point rootdir at the reports instead.

Nested suites in an expansion

The two tools agree since sphinx-needs#2149 (unreleased at the time of writing): a need for every suite at every depth, with the same ids and links; a release before it stops with KeyError: 'testsuites' at the first nested suite.

Two needs of one expansion with one id

You write a test-file with :auto_suites: over a report whose two suite names hash alike at suite_id_length (the vendored many_testsuites.xml does at the default 3), or that holds one suite, or one case, twice.

ubCode creates the first need, leaves the later one out — a suite with its cases and nested suites — and reports needs.test_report_duplicate_id with what would separate the two.

A Sphinx build stops with Suite ID … already exists or Case ID exists — or, for one case in two suites under tr_deterministic_case_ids, with sphinx-needs’ A need with ID … already exists. [duplicate_id].

Status — deliberate: the same outcome is proposed for sphinx-test-reports (sphinx-needs#2052).

A :suite: that names a nested suite

The two tools agree since sphinx-needs#2149 (unreleased at the time of writing): a top-level suite of that name first, then the first at any depth; a release before it looks at the top-level suites only and stops with Suite … not found.

A section titled like a test-report need’s id

You write a section titled exactly like the id of a need a test-report directive creates — one of the lower-case ids, testcase__usage_test__usage_test_sxckt — on the page of that directive, with the default [parse] id_style = "github".

ubCode gives the id to the need card and <id>-1 to the section in the built page, while :ref: links to the section and objects.inv name <id>, which lands on the card; it reports needs.test_report_id_shadows_section.

A Sphinx build gives the section an id of docutils’ form (testcase-usage-test-usage-test-sxckt), which a need id never equals, and reports nothing.

Status — a gap, until the index reserves these ids as the build does (#3903).

An expansion whose test-file loses its id

You write a test-file with :auto_suites: whose :id: another need also has (a req).

ubCode reports needs.duplicate; the req keeps the id and the test-file renders as the generic directive box, followed by the cards of its expansion. Those suites and cases link to the id as their parent — so to the req; and a suite whose id another need takes leaves its cases linking to that need.

A Sphinx build stops when the test-file comes second (InvalidNeedException … [duplicate_id]); when the other need comes second, it drops that need with a warning (Need could not be created … [needs.create_need]) and keeps the test-file and its expansion — the opposite winner from ubCode, which lets the req win in either order.

Status — a gap (#3904).

Order in a needgantt

You write a needgantt over needs from a page that mixes need directives and test-report directives.

ubCode draws the test-report needs after the page’s directive needs, by id.

A Sphinx build orders them in document order.

Status — a gap (#3878).

Need parts in the body

You write a need part (the :np: role) in the body of a test-case.

ubCode records no part on the need, so a :need: reference to the part (TC_SUB.p1) does not resolve, and reports the role as needs.need_part (“Need part role not in need directive”): false here, since the directive does create a need, and a failure under --deny warning.

A Sphinx build collects the part, as from any need’s content.

Status — a gap (#3877).

A JSON report

You write a :file: naming a JSON report (results.json).

ubCode reports needs.test_report_json_unsupported and creates no need: JSON reports are not read yet ([test_reports] json_mapping is accepted but not applied).

A Sphinx build reads it with its JSON parser, under tr_json_mapping, and creates the need.

Status — a gap, until JSON reports are read.

A test case’s evidence on a MyST page

You write a {test-case} in a MyST document.

ubCode renders the evidence as reStructuredText (literal blocks), as the converter writes it, after the body written in MyST — on the page and in a needextract of the need alike.

A Sphinx build appends its own reStructuredText evidence (the first part only, see above) to the MyST body and parses the whole as MyST, so the evidence’s literal blocks do not render as such.

Status — by design: the evidence is reStructuredText whatever the page’s syntax.

The test-report tables

ubCode renders sphinx-test-reports’ test-results and test-env directives (Results tables). Where the two differ:

The suites are real sections

The two tools agree since sphinx-needs#2148 (unreleased at the time of writing): a section per suite (per environment), with the same ids, in the table of contents; a release before it renders each title as a heading at the enclosing section’s level, and stops inside a list item (AssertionError).

A :ref: to a suite’s section

You write a :ref: to a suite’s name (:ref:`parent_testsuite`) with sphinx.ext.autosectionlabel on.

ubCode labels no section test-results or test-env generates: the reference reaches an authored section of that name, or is std.ref.

A Sphinx build with sphinx-needs#2148 (unreleased at the time of writing) labels the suite’s section, and the reference reaches it.

Status — a gap.

Nested suites

The two tools agree since sphinx-needs#2149 (unreleased at the time of writing): every suite at every depth, each suite’s own cases before its nested suites; a release before it shows the outer suite with an empty table and drops everything below it.

Counters and Time:

You write test-results over a report whose suites leave out an attribute (failures, skipped, time).

ubCode counts the missing counter from the suite’s cases, its nested suites’ included (Failures: 1), and leaves the Time: line out.

A Sphinx build prints -1 for each missing counter and Time: -1.0.

Status — deliberate: a counter is never negative, as on the test-report needs (sphinx-needs#2054).

The reason cell

You write test-results over a case that failed with a message and a traceback.

ubCode renders the message as a paragraph and the traceback as a literal block, its lines kept; a skip or error whose message is absent shows only its text.

A Sphinx build joins the two into one paragraph, where the browser runs the traceback’s lines together.

Status — deliberate.

The table’s frame

You write either directive.

ubCode gives the test-results table its 20 / 20 / 20 / 40 % column widths (test-env 50 / 50), ub-test-results / ub-test-env on the table, and no row-odd / row-even striping; the row, cell and paragraph classes (tr_<result>, the column names) are the oracle’s. A JSON value is highlighted by ubCode’s own highlighter.

A Sphinx build writes no column widths, docutils align-default on the table, and Pygments markup.

Status — ubCode’s own table writer; a stylesheet keyed on the tr_* classes works in both.

:data: order and the “not present” warnings

You write :data: platform, host, nope.

ubCode shows the rows in the option’s order, and reports nope once for the directive; a name listed twice counts once, and an empty entry is skipped.

A Sphinx build shows the rows in the file’s order, reports nope once per environment, and reports a repeated name and an empty entry (option '') as not present.

Status — deliberate (sphinx-needs#1969).

Values in a test-env table

You write a variable whose value is false, null, 0, "" or true, or text with non-ASCII characters inside a list.

ubCode writes each scalar as JSON writes it (false, null, 0, true), and non-ASCII text as it is.

A Sphinx build leaves every false-ish value’s cell empty, writes True, and escapes non-ASCII text in a JSON block (\u00f6).

Status — deliberate.

What stops a Sphinx build

You write a report that does not exist, is not well-formed XML, or has no suite; a .json argument to test-results; an environment file that is not valid JSON, is not an object of objects, or starts with a byte-order mark; a non-numeric counter or time in a report.

ubCode reports each one as a positioned warning (a note for a report with no suite or a value read past) and renders what it can; a byte-order mark is read past.

A Sphinx build stops with an exception (#2052’s list, and TypeError / IndexError / ValueError).

Status — deliberate (sphinx-needs#2052).

An option under test-results

You write :class: foo under .. test-results::.

ubCode reports block.directive_unknown_option and does not render the directive.

A Sphinx build reads the option line as part of the report’s path, and stops on a missing file.

Status — deliberate.

Row colours in the PDF

You write test-results in a project built with ubc build pdf.

ubCode draws the tables without the tr_<result> row colours.

A Sphinx PDF build (LaTeX) draws none either; the html builds colour the rows.

Status — ubCode’s PDF has no stylesheet to carry them.

The test-report directive

ubCode renders sphinx-test-reports’ test-report directive through its report template (A whole report: test-report). Where the two differ:

The template’s language

You write a template with a format spec ({id:>8}), a conversion ({id!r}), an index or an attribute ({content[0]}, {id.upper}).

ubCode reports needs.test_report_template_invalid with the construct, its line and column, and renders nothing of the template: a template is {key} for its ten keys and {{ / }}.

A Sphinx build formats the template with Python’s str.format and renders all four — an attribute as a memory address (<built-in method upper of str object at 0x…>).

Status — deliberate; a template language with conditions and loops is proposed upstream (sphinx-needs#2142).

What stops a Sphinx build

You write a template with an unknown key, a lone brace or a numbered field; a report_template that does not exist, or is not valid in import_encoding; a test-report without :id: or :file:, with a :collapse: that is not true or false, or with the :id: of another report or test-file.

ubCode reports each one on the directive and carries on (a duplicate id is needs.duplicate, the template’s generated test-file being an ordinary one).

A Sphinx build stops with an exception at the first one (KeyError, ValueError, IndexError, InvalidConfigurationError, UnicodeDecodeError, SphinxError for no :id:, TestReportFileNotSetError for no :file:, a bare Exception for :collapse:, duplicate_id).

Status — deliberate.

In a Markdown page

You write a {test-report} fence in a MyST page.

ubCode reports needs.test_report_markdown_host and renders nothing.

A Sphinx build reports Directive 'test-report' cannot be mocked and renders nothing.

Status — the same outcome, as a positioned finding.

A section in the template

You write a template with a section title.

ubCode nests the section one level under the section the test-report stands in, as it does for an .. include::d file’s section.

A Sphinx build reads the title in the page’s own title styles: a section underlined like the page’s title becomes a sibling of it, and the page’s next sections move under it.

Status — the include rule (a registered divergence of .. include::).

Where a finding in the template points

You write a template with a reStructuredText mistake, or a body with one.

ubCode reports a mistake in text the template wrote — on its own lines, or before or after {content} on a body’s line — at the directive, naming the template and its line, and one in the body at the body’s own line. Inside a body line that holds a tab, columns right of the tab are tab-expanded.

A Sphinx build names the page with the template’s line, or with a line past the page’s end.

Status — deliberate: every position is a real line.

{template_path}

You write {template_path} in a template, or use the shipped one, which does.

ubCode writes the template’s path relative to the ubproject.toml that set it, test_report_template.txt for the shipped one.

A Sphinx build writes the absolute path, on the machine that built it.

Status — deliberate: a page is the same wherever it is built.

The shipped template’s tables

You write a test-report over a report of more than 100 cases, with the shipped template.

ubCode lists every case: its copy of the template sets :max_items: 0 on its three tables.

A Sphinx build lists every case: its tables have no cap.

Status — the same outcome; a template of your own follows [needs] views_max_items.

A report inside a file the template includes

The two tools agree: with a template holding .. include:: inner.txt and a test-report in inner.txt, both render the inner report once and report the inner template’s inclusion of inner.txt as circular (directive.include_circular in ubCode). A report’s template should not include a report.

{content}’s indentation and the template’s encoding

You write {content} twice, or once after an escaped {{content}}; a template starting with a byte-order mark; import_encoding = "latin-1".

ubCode indents each {content} like its own line, drops the byte-order mark, and reads the encoding names of the WHATWG standard (latin1, not latin-1).

A Sphinx build indents every {content} like the first line holding {content}, the escaped one included, renders the byte-order mark into the page, and reads Python’s codec names.

Status — deliberate.

ubCode-only view filters

The needlist, needtable and needflow view directives accept a ubCode-only :cypher: option — a read-only Cypher selection (a bare WHERE-style expression such as n.status = 'open', or a full single-node-column query MATCH (n) ... RETURN n) — in place of the portable python filter surfaces.

Two of the three directives read a richer query as more than a selection: a needtable’s can name its columns, and a needflow’s can name its edges. Both are covered in their own sub-sections below.

A fourth directive, needpie, takes the same option with a different meaning. It has no selection for a query to replace — a chart’s selection is its content, one filter per line — so the query scopes the chart instead. That reading has its own sub-section below too.

You write a needlist, needtable or needflow with a :cypher: option.

The preview shows the needs the Cypher query selects, exactly as a :filter: selection would.

A Sphinx build reports an error on Sphinx-Needs 8.3.1 and earlier: those releases have no :cypher: option, so the unknown option is a directive error. Sphinx-Needs 8.4.0 accepts the option as a no-op (sphinx-needs#1760), so from that release on a Sphinx build simply ignores it — needlist, needtable and needflow each declare cypher in their option spec and nothing reads it.

Status — :cypher: is a deliberate ubCode extension; Sphinx-Needs will not evaluate it, and the interoperability cost is accepted.

:cypher: takes precedence over the python filter surfaces (:filter: / :status: / :tags: / :types:) rather than conflicting with them. When both are present ubCode evaluates the Cypher selection and ignores the python surfaces.

Carrying both is therefore the recommended portable form, not a mistake: ubCode reads the Cypher, a Sphinx build reads the python surfaces, and one source builds under both tools.

.. needtable::
   :cypher: MATCH (n:spec) WHERE n.status = 'open' RETURN n
   :types: spec
   :filter: status == 'open'

The block above is shown as source only. For the same pairing rendered live — by this Sphinx build and by ubc build html alike, on the tutorial’s own model — see Selecting with Cypher.

Keeping the two selections in sync is the author’s job. ubCode does not check that a Cypher query and a python expression select the same needs — proving that in general is not possible, so nothing warns about the pairing (the needs.filter_conflict warning that used to greet it was retired). A divergence shows up honestly, as the two builders rendering different needs.

A needtable’s :cypher: can also name its COLUMNS

On a needtable, a :cypher: that returns more than the bare node defines the table’s columns and headers (see Projections in a needtable), so a :cypher: table’s presentation is ubCode-only too, not only its selection.

You write a needtable whose :cypher: projects several columns.

The preview shows one column per RETURN item, headed by its AS alias or its expression text.

A Sphinx build renders the table from :columns: (or the configured default column set), ignoring the query entirely.

Status — deliberate, and the reason :columns: alongside a projection draws no warning: it is the Sphinx half of the same dual-builder form.

How close the two builders stay depends on the shape of the projection:

Projection

Dual-builder story

single anchor, per-need property columns only, from a MATCH that binds one node (no traversal) — no UNWIND, no computed columns (MATCH (n) RETURN n, n.status AS State; MATCH (n) RETURN n.id, n.status AS State)

Portable. One row per need in both builders, and every projected column has a :columns: equivalent, so only cell CONTENT can differ.

The n.id form is the strongest case: a projected n.id renders the same linked id cell that :columns: id renders, so RETURN n.id, n.status AS State and :columns: id, status are cell-for-cell equivalent — every cell identical, links included. The headers come from the query (alias with AS to match), and the row order follows the cypher rule above (add :sort: id_complete to match the parity order).

single anchor, but row-multiplying or computed (UNWIND n.tags AS t RETURN n, t; RETURN n, toUpper(n.status) AS Shouty)

ubCode-only, for the same two reasons as the row below, and worth its own row because “one anchor” is not what makes a projection portable. UNWIND multiplies rows without any traversal — four rows for two needs, where a Sphinx build renders two — and :columns: has no vocabulary for a function call or a literal.

multi-variable (RETURN n, m.title)

ubCode-only. A traversal repeats its anchor once per match, so a Sphinx build renders FEWER rows — and :columns: has no vocabulary for a value reached across a link, so the column itself is inexpressible there.

The discriminator is therefore does the query multiply rows, and can every column be named in :columns: — not “how many variables does it mention”.

Three smaller differences inside a projected table:

A need’s LINKS are relationships, not node properties. n.links and n.satisfies are therefore not properties at all: they project as null — a blank cell — and the query is reported as needs.cypher_vocabulary, on the :cypher: option, with a note that names the link type and shows the traversal. Traverse instead: MATCH (n)-[:satisfies]->(m) RETURN n, m renders linked chips. (A need’s own FIELDS are properties and project normally, so n.tags is correct and renders its entries joined by a semicolon and a space, exactly as a :columns: cell does.)

That is also the contrast with n.id, which is a property and the node’s own: its value is a need id by construction, which is why it renders as a linked id cell while a list of link targets cannot.

On a :show_parts: part row, a projected column shows the PARENT’s value — so a projected column can disagree with the parity column of the same name in the same table. The parity path OVERRIDES ID, TITLE, CONTENT, IS_PART, IS_NEED and the link columns on a part row; a projection overrides none of them. A projected n.title therefore shows the parent’s title where :columns: TITLE shows the part’s own content.

A null anchor fails the table. An OPTIONAL MATCH that matches nothing yields an all-null row, and a needtable row with no need behind it has no id cell and nothing for :style_row: to evaluate against. ubCode reports the failure rather than dropping the rows silently.

A needflow’s :cypher: can also name its EDGES

On a needflow, a :cypher: that returns paths or relationships supplies the diagram’s edges as well as its nodes (see Edges in a needflow), so a :cypher: diagram’s shape is ubCode-only too, not only its selection.

You write a needflow whose :cypher: returns paths or relationships — RETURN p, RETURN n, r, m, or a bare RETURN r.

The preview shows exactly the edges the query matched, between the needs those edges touch.

A Sphinx build ignores the query and draws its own diagram from :root_id: / :root_direction: / :root_depth: / :link_types: and the python filter surfaces.

Status — deliberate, and there is no python dual form: a diagram whose edges come from a query cannot be expressed in the option model at all, so this is the one :cypher: extension whose two builders draw different diagrams rather than the same one. That is also why the walk options draw no warning beside such a query: they are the Sphinx half of the pairing, ignored by ubCode and read by Sphinx-Needs, exactly as the python surfaces are beside any :cypher:.

Why the two diagrams differ at all is the point of the feature. Without a graph-returning query — in ubCode and in Sphinx-Needs alike — a needflow draws every allowed link between the needs it selected, so the edge set is a function of the node set and :link_types: is the only lever on it. Moving that lever moves the node set with it, because one option gates both the walk and the draw. A query that returns paths gives the edges directly, so the same needs can be drawn with different edges. Sphinx-Needs has no equivalent, and will not gain one.

Carrying the walk options alongside is therefore the recommended form, for the same reason carrying the python surfaces alongside a :cypher: selection is: it gives a Sphinx build something to draw. Expect the two pictures to differ — that is what the feature is for — and keep them recognisably about the same needs.

.. needflow::
   :cypher: MATCH p = (r {id: "T_CAR"})<-[:specifies]-()<-[:tests]-()
            RETURN p
   :root_id: T_CAR
   :root_direction: incoming
   :link_types: specifies, tests

Two smaller differences inside a query-drawn diagram:

A part edge loses its dotted styling. Parts are not nodes in the needs graph and a matched relationship carries only its type, so a query-given edge cannot know it crossed a part: it draws as a plain edge to the part’s parent need, where the option model draws it dotted. And a link to the need’s OWN part is not drawn — in the graph the two spellings are one relationship, so it arrives as a self-loop, and the default model suppresses that loop too.

An unbounded * pattern is a build risk. A variable-length pattern enumerates paths rather than testing reachability, so its cost grows exponentially with the bound. A view directive runs under a short build budget, and an unbounded pattern over a couple of hundred densely linked needs will exceed it — reported as build.need_view_failed, with an error block on the page in place of the diagram. Bound the pattern (*1..3).

A chart’s filter options SCOPE its slices

On a needpie or a needbar, a directive-level filter option is not a selection at all — there is nothing for it to select, because a chart’s selection is its content, one filter per line (needpie) or per separator-split cell (needbar). It is a scope: the needs it selects are the universe those cells are counted in, so each slice is that cell’s filter intersected with the scope.

One concept, two spellings. The portable one is the four python surfaces :filter:, :status:, :tags: and :types:; the ubCode-only one is :cypher:, which takes precedence when both are written. Writing both is the recommended form, exactly as it is on needlist, needtable and needflow: ubCode evaluates the query, a Sphinx build reads the python surfaces, and one source then counts the same numbers under either engine — subject to the needs-only counting recorded in Needs chart views (needpie / needbar) (ubCode counts needs, a Sphinx build counts needs and their parts), which a paired scope neither causes nor cures.

You write a chart with a scope — :cypher:, or the four python surfaces, or both — and one filter per content cell, as usual.

The preview shows each cell counting only the needs that are BOTH in the scope and matched by that cell’s filter.

A Sphinx build reads the python surfaces and scopes by them, so a chart whose scope has a python spelling shows the same numbers in both engines — as far as the SCOPE goes; a project using need parts still counts more there than here (Needs chart views (needpie / needbar)). Given a bare :cypher: and nothing else, it counts each cell over the whole project instead, so every slice is larger (or equal).

On Sphinx-Needs 8.4.0 and earlier none of the five options is known on a chart: the build reports unknown option and drops the directive, so the view disappears from the page. The accept-and-ignore change for :cypher: on both charts shipped in Sphinx-Needs 8.5.0 (sphinx-needs#1818), and the four python surfaces are the chart’s scope there from that same release (sphinx-needs#1831).

Status — deliberate. A paired scope is portable and its numbers agree. A bare :cypher: is the one :cypher: extension whose no-op changes a rendered number rather than a selection or a shape; that divergence is accepted and documented, and nothing warns about it.

Two things follow from “scope, not slicer”, and both are what makes the option usable rather than a second filter language:

  • the lines keep slicing. Add a content line and you add a slice, remove one and you remove a slice, exactly as without a scope. A bare non-negative integer line is untouched — it is its own value, not a count of anything — so a fixed reference slice stays fixed inside a scope.

  • a scope alone draws nothing. A needpie whose body is empty renders the empty-result paragraph whether or not it carries a :cypher:.

The reason to reach for it is the reason :cypher: exists everywhere else: a question about the links, which ubCode’s python filter dialect cannot ask. Here the scope is the requirements that some specification realises, and the two content lines then split that set by status:

.. needpie:: Realised requirements by status
   :cypher: MATCH (n:req)<-[:links]-(:spec) RETURN n
   :labels: open, closed
   :legend:

   status == 'open'
   status == 'closed'

The block above is shown as source only, and no longer because a Sphinx build would refuse it: this build pins Sphinx-Needs 8.5.0, which reads all five scope options. It stays source because its scope is a traversal, which has no python twin, so rendering it would publish two different sets of numbers — the very divergence this sub-section exists to record — and because the register’s examples are written against a generic req / spec model that this project does not define. For a scope that does pair, rendered live by this Sphinx build and by ubc build html alike, see Selecting with Cypher.

There is a portable python dual form for a scope, and it is the same four options every other view uses. This paragraph used to open “there is no portable python dual form for a scope yet”, and gave the reason: “a chart takes none in either tool today: the only place a Sphinx build could read an equivalent restriction is inside each content line, which means writing the scope into every line by hand.” That is no longer the only place. Both charts now read :filter:, :status:, :tags: and :types: as a scope, and a Sphinx-Needs release carrying those options reads them the same way, so the pairing that works on needlist, needtable and needflow works here too:

.. needpie:: Open requirements by kind
   :cypher: n.type = 'req' AND n.status = 'open'
   :types: req
   :status: open
   :labels: safety, other

   "safety" in tags
   "safety" not in tags

The one thing the pairing cannot carry is a traversal. ubCode’s python dialect judges each need on its own fields and cannot follow a link, so a scope like MATCH (n:req)<-[:links]-(:spec) RETURN n has no python spelling — in a content line or in a scope option — and there the two engines’ numbers still differ. Folding a plain field test into every content line by hand remains possible and is now pointless: write it once, in the scope option.

A Sphinx-Needs filter string can reach other needs, through the needs view its filter context binds. That form is named here only so it is not mistaken for a route: ubCode does not accept it, and even in a Sphinx build it does not do what a scoped slice needs. Sphinx-Needs narrows the needs view by an id / type / status / tags test anded beside the traversal before binding it, so the natural one-line spelling — status == 'open' and <traversal over needs> — is evaluated against a narrowed universe and, whenever that narrowing drops the needs the traversal walks through (the usual case: a specification carries no status), counts nothing. The bare traversal alone works. So expect the numbers to differ, and keep the two engines’ charts recognisably about the same needs.

A mistyped label or property in a :cypher: scope is caught before it quietly changes every slice: it is reported as needs.cypher_vocabulary, positioned on the :cypher: option, exactly as on the other three directives. An unparseable :filter: scope is reported as needs.filter_invalid on the :filter: option, and is silent whenever a :cypher: is written beside it — the option that loses the precedence is not evaluated, so it is not complained about either. A query that does not parse, or one that returns anything other than the matched node itself, is reported as needs.cypher_invalid at edit time and again as build.need_view_failed when the page renders — and on the page the chart itself is replaced by an error block: the failed scope is a recorded failure, which the writer shows in place of the chart, and every filter slice would have counted 0 in any case. Note that this replaces the whole chart: the scope is resolved before any content line is looked at, so even an all-literal pie (whose numbers no scope could change) shows the error block rather than its literals. That is the standing “nothing silent” posture — a chart is never left looking like data when its selection failed — and not an accident.

A failing scope has four postures, and what separates them is which LAYER can see the defect — the handler while you type, the index-time vocabulary sweep, or only the evaluation itself — not which spelling you wrote it in:

the defect

at edit time

at build time

on the page

:cypher: does not parse, or returns the wrong shape

needs.cypher_invalid

build.need_view_failed

error block, no chart

:filter: does not parse

needs.filter_invalid

nothing

the chart, every filter cell 0, literal cells kept

either spelling parses but cannot be executed — an unknown field, a type error, a :cypher: that exceeds its time budget

nothing

build.need_view_failed

error block, no chart

a :cypher: names vocabulary the project does not have (a mistyped label or property)

needs.cypher_vocabulary

needs.cypher_vocabulary

the chart, every filter cell 0, literal cells kept

Row two is the one whose defect is reported once: the complaint is already in your editor, so a second build-side copy would be a second report about one mistake, and suppressing it is the posture needlist has had all along.

Row three is the one to know about, because it is the only row with no warning of any kind before the build. Both spellings reach it: the check behind needs.filter_invalid is a parse check, so :filter: bogus_field == 'x' is accepted while you write it; and the only edit-time gate on a :cypher: reads the query’s form, so a query that parses and returns a single node column — which is every well-formed query, a link traversal included — passes it and can still fail against the index. Either way the failure surfaces only when the build resolves the scope.

Row four is the one that looks like row one and behaves like row two, and it is why “which spelling” is the wrong question to ask. A mistyped label is not an execution failure at all: the query engine follows openCypher null semantics, reads the missing name as null and returns an empty result rather than raising — so the chart draws its zeros with the literal cells kept, exactly as an unparseable :filter: does. What saves you is the separate index-time vocabulary check described above, which reports needs.cypher_vocabulary on the option in the editor and on the build’s warning stream.

needbar reads a scope too, in both spellings. This section used to end “needbar does not offer the option. Nothing about the reading is pie-shaped — a bar’s cells would scope identically — so this is a deliberate hold, not a limitation of the idea.” The hold is over, on that same reasoning.

One difference between the two charts is worth knowing before you scope a bar. A needpie whose slices sum to zero renders the empty-result paragraph (with the :filter_warning: override, if set), so a scope that matched nothing says so. A needbar has no all-zero empty state and no :filter_warning: — upstream parity — so it draws, with zero-height bars and no message. The empty paragraph itself is not chart-kind-specific: a bar whose grid is empty (rather than zero) renders it like a pie does. What a bar lacks is the all-zero trigger, which is exactly the case a scope that selects nothing produces. On the page itself a mistyped bar scope is therefore the quietest mistake in the family, which is why the vocabulary check above matters most here: in the editor it is still a squiggle on the option.

Filter expression syntax

ubCode evaluates :filter: options and :need_count: targets with its own filter engine, which accepts the whole portable subset Sphinx-Needs documents (comparisons in either operand order, in / not in, is None, and / or / not, var.* variant fields, search(), and a bare True / False). A few forms Sphinx-Needs accepts are deliberately not accepted here; each raises a needs.filter_invalid warning (with the source column) and renders an empty result, and each has a workaround.

Raw-string prefix

You write a filter using a raw-string regex, such as search(r"[\w\s]+", title).

A Sphinx build accepts it (Python treats r"..." and "..." alike).

Status — drop the r prefix and write search("[\w\s]+", title). Regex escapes that are not also Python string escapes — \w, \d, \s — are preserved in an ordinary string, so the pattern is unchanged. But \b (word boundary), \n and \t do change meaning without the r prefix (Python reads them as backspace, newline, tab): keep those as an explicit character class or double the backslash (\\b). Note also that search() patterns must be valid for ubCode’s regex engine, which does not support lookaround ((?=...), (?<=...)) or backreferences (\1) — such patterns raise needs.filter_invalid.

Tuple or set containers

You write a membership test against a tuple or set literal, such as id in ("a", "b") or id in {"a", "b"}.

A Sphinx build accepts the tuple/set form.

Status — use a list literal instead: id in ["a", "b"].

Ordering comparisons against non-numbers

You write an ordering comparison (< / <= / > / >=) whose other operand is a string, boolean or None — in either operand order, such as deadline < "2024-06-01" or "beta" < status.

A Sphinx build accepts it: Python applies the operator to the values at runtime, and only raises if the types are not orderable.

Status — the ordering operators support numeric literals only (priority > 3, 5 <= level); equality (== / !=) and membership (in) work for strings. Use a numeric field for ordering. Date-string ordering has no direct portable equivalent — model the date as a sortable numeric field, or select with equality / in.

Unrecognised function calls

You write a filter calling a function other than the recognised helpers, such as bool(arch).

A Sphinx build accepts it (its eval has the Python builtins available).

Status — the only calls the engine recognises are search(), len(), .lower(), .upper(), .startswith() and .endswith(). For bool(arch), write the bare field arch — truthiness is supported (0, "" and [] are falsy, as in Python).

Site output

Page redirects (sphinxext-rediraffe)

You write [build.html.redirects] in ubproject.toml, a table mapping the path of a page that moved to the page it became.

A Sphinx build does the same job through sphinxext-rediraffe: rediraffe_redirects names either a dict in conf.py or a whitespace-separated redirects.txt, and the extension writes a small forwarding page at each old location.

The preview has nothing to show — a redirect is a property of the built site, not of a document. ubc build html writes a forwarding page at each old location, carrying the reader on with their ?query, and with their #fragment unless the entry names a section of its own.

Status — implemented, with these deliberate differences from rediraffe:

  • A deleted entry deletes its page. rediraffe records the stubs it has written in _rediraffe_redirected.json and only ever adds to that record, so a redirect you remove from the configuration leaves its forwarding page on the site forever. ubCode’s forwarding pages are ordinary managed output: remove the entry and the next build prunes the page, and any directory it emptied — in an output directory the build owns, which is the projection’s standing rule for every file it manages (a directory ubCode did not create is never pruned).

  • An existing forwarding page is rewritten, not skipped. rediraffe takes a fast path for any stub already in its record, so a change to the template never reaches a site that was built once. ubCode recomputes every forwarding page’s bytes on every build and writes only what differs — so a fix reaches an existing site, while an unchanged redirect keeps its file and its timestamp.

  • A loop warns; it does not abort the build. rediraffe raises an ExtensionError on a circular redirect and the whole Sphinx build dies. ubCode reports build.redirect_cycle, skips the entries in the loop, and builds the rest of the site — which still fails the default quality bar, so nothing is quietly accepted.

  • A faulty table fails loudly. Every entry ubCode cannot use names itself in a warning — an unreadable key or target, a refused URL scheme and two spellings of one page are all build.redirect_invalid. rediraffe’s file parser has silent failure modes — a file saved with a byte-order mark, for instance, writes a forwarding page under a mangled name and reports success.

  • A key that names a real page never overwrites it, whatever the target. The author’s page keeps its own content and build.redirect_source_exists says so.

  • Targets are validated. A redirect whose chain ends at a document the build renders no page for raises build.redirect_target_missing and writes nothing, rather than a page that forwards to a 404.

  • The forwarding page replaces the history entry rather than pushing one, so Back from the new page returns where the reader came from instead of bouncing them forward again.

  • The reader’s URL is reassembled, not concatenated. A target may carry its own ?query and #fragment; each component the target sets wins, and only an absent one is taken from the reader’s URL. Without JavaScript the fallback refresh reaches the target page but can carry neither.

  • Fragments are understood, on both sides. A target may name a section ("guide/new#install"), and so may a KEY — rediraffe has neither. A section-level entry cannot work through a forwarding page (a fragment never reaches the server), so it works in the reader’s browser instead: on a page that still exists it rides the site’s own script, and on a page that is itself redirected it travels in that page’s forwarding page. A section that is still there always wins, so an entry that has not really moved is inert rather than surprising — and says so (build.redirect_anchor_shadowed). The liveness check honours <a name> as well as id, matching what a browser resolves; rediraffe’s own anchormap proposal checks getElementById alone.

  • A reader is forwarded once, across both layers. If the page moved and the section moved, the forwarding page already carries the section’s new destination — its map pre-merges the map of the page it forwards to. Composing the two layers naively (which is what rediraffe’s anchormap proposal does) costs the reader two navigations.

  • Target sections are checked against the ids the target page actually renders — section slugs, the ids generated for repeated or empty titles, anchors written by hand in a raw block, and all — and reported as build.redirect_anchor_unknown when the page has no such anchor. Both levels are checked: an entry naming a section of its target is checked whether or not its KEY names one.

  • The reader’s query rides every hop. Their ?highlight= or ?q= is theirs whichever layer moved them — the forwarding page’s or the site script’s — unless the destination names a query of its own, which wins. rediraffe concatenates instead, which turns an external target’s own query into ?a=1?b=2. Their fragment behaves differently, and deliberately: it rides a page redirect (nothing consumed it, so it is still theirs), while a section redirect replaces it with the destination the entry names — or with nothing, which means the top of the new page. Re-attaching it there would send the reader to the one anchor the entry exists to say has gone.

Not ported, deliberately:

  • no redirects.txt file form — the table lives in ubproject.toml with the rest of the project’s configuration;

  • no rediraffecheckdiff / rediraffewritediff builders;

  • no rediraffe_template — the forwarding page is fixed;

  • no anchormap directive — rediraffe’s open proposal authors section redirects in the moved page’s own source; ubCode keeps them in the same configuration table as the page redirects, so one place answers “where did this go”;

  • no wildcard keys (which is a sphinx-reredirects feature rather than a rediraffe one).

Listing "sphinxext.rediraffe" in [parse].extensions is harmless and does nothing: it is not one of the extension ports, and [build.html.redirects] is not gated on it.

Closing the gap

The preview grows release by release: ubCode actively closes the gap with Sphinx and Sphinx-Needs — substitutions, toctree rendering, resolved footnotes and citations, and named hyperlink references have all moved off this page’s “not yet rendered” list recently — and the constructs still shown as placeholders are tracked on the Roadmap. When you need the final, fully-transformed output, run a full sphinx-build.