0.31.x

0.31.2b1

Released:

12.08.2026

pre-release:

Five arcs this release: the HTML build goes publicubc build html steps out of hiding as an alpha, with a user guide, offline math, sphinx-design tabs and furo-parity page chrome; PlantUML diagrams get a preprocessor — variables, macros, loops and a bundled C4 library expand before rendering, always on, with no filesystem or network access; imports grow up — external needs get real links, imported need content renders as sandboxed cards, and a re-ordered needimport can no longer silently lose its needs; the index speaks Sphinx — dangling :ref: / :doc: / :term: references now warn as sphinx-build does, sphinx.ext.autosectionlabel and sphinx.ext.todo are ported, and every image, figure, table and diagram dimension is validated; and MCP grows agent-grade reference tools — search and resolution over everything the project can cross-reference, structured answers on every tool, and one-command registration with terminal Claude Code.

✨ The HTML build (alpha)

  • ``ubc build html`` is now public (alpha)

    The static-site builder is no longer hidden from ubc build --help. It is marked (alpha): the command works and is under active development, but the emitted markup, the theming surface and the available options may still change between releases, so it is not yet recommended for production builds.

    A new guide — Building a static HTML site — covers running it: the quickstart and principal options, where the site lands and how it works from a file:// URL (search included), incremental rebuilds and --fresh, and how the build reports its warnings. The [build.html] configuration page remains the option reference.

  • Math typesets, offline

    The math directive, the :math: role and MyST dollar math render through a bundled KaTeX — generated entirely at build time, no CDN, works offline — in the built site and the Rendered preview alike. A :label: gets an anchor and a permalink; raw LaTeX stays out of the search index; a page with no math ships none of it. See Math typesetting.

  • sphinx-design tabs render, and the family follows your theme

    tab-set / tab-item render in the oracle’s own markup and switch with pure CSS (JavaScript-disabled readers included), with :sync: groups, URL-hash deep-linking and persisted selection on top. The new [build.html] key sd_tabs_storage_prefix controls (or disables) the remembering (its default deliberately differs from sphinx-design’s). The whole sphinx-design family now takes its palette from the site’s --ubc-* tokens, so branding the shell brands the components — and the editor preview renders them styled too.

  • furo-parity page chrome

    Section headings and named table / code captions carry a hover permalink; a followed fragment link tints its target; admonitions take furo’s tinted-title-bar model over the documented accent tokens; wide tables scroll inside their own box instead of stretching the page.

  • Document topmatter becomes metadata

    An RST leading field list or MyST YAML front matter is consumed into per-document metadata instead of rendering as visible content, and four keys now shape the page: sd_hide_title, hide-toc, tocdepth and nosearch. Front pages that used to show their option block as text stop doing so.

  • The build tells you what it could not render

    A role with no renderer is aggregated once per name under build.role_unhandled, and the diagram file forms — .. uml:: flow.puml, .. mermaid:: flow.mmd — now render the file, re-render the pages that use it when it changes, and report declines instead of silently showing a placeholder. Build warnings are durable across incremental rebuilds and name their page. See Unrendered directives, roles and diagram files.

  • ``objects.inv`` is complete

    Markdown pages (and their (target)= labels) now appear in the published inventory — previously a .md page was not intersphinx-linkable at all — and a :ref: to a target defined inside an .. include::-d fragment links the page that renders it, matching what the inventory advertises.

  • The differences register covers both surfaces

    Differences from a Sphinx build — the honest register of where ubCode’s rendering differs from a full sphinx-build — is reframed around the two public surfaces (preview and ubc build html), grouped into four thematic sections with a jump table, and explicit about what has and has not been audited against the build.

✨ Diagrams

  • PlantUML preprocessing, always on

    A diagram’s body is preprocessed before it is translated, in the preview and in ubc build html: variables (!$name), !define / !definelong, !function / !procedure, conditionals, loops and the string / list / arithmetic builtins all expand, so a diagram can be generated rather than written out in full. The C4 standard library is bundled — !include <C4/C4_Container.puml> resolves offline and the Person() / Container() / Rel() macros expand — with no filesystem access and no network, ever: your own file includes, URLs and themes are recognised and skipped rather than fetched. This needs no configuration and cannot be turned off, matching the Sphinx plantuml extension. See Diagrams for the full supported / not-yet list.

  • PlantUML fidelity: labels stop leaking their markup

    Creole display text (**bold**, //italic//, size / colour / font tags) renders instead of appearing literally; C4 sprite references become glyphs; left to right direction is honoured; a per-stereotype skinparam paints only its stereotype; inline element colours reach the output — so the flagship C4 samples now look like PlantUML’s own rendering rather than markup soup.

  • ``needsequence`` renders as a Mermaid sequence diagram

    The needsequence directive is now drawn in the Rendered preview and by ubc build html, instead of showing a placeholder frame. From each :start: need the walk treats the needs it links to as the messages and each message need’s own links as the receivers, so an intermediate need becomes the arrow label — which is what makes the view good at showing a mediated relationship. Where Sphinx-Needs fails the build — a missing or unknown :start:, an unknown :link_types: name — ubCode warns and renders what it can.

  • ``:width:`` and ``:height:`` on ``needflow`` and ``needsequence``

    The two diagram views can now be sized, with the same options and units the mermaid / uml / plantuml directives take. A value that is not a size raises a warning and the diagram renders without that dimension. Sphinx-Needs has no size option on any of its diagram directives, so these are a ubCode extension. :scale: on the two views now warns and points at :width: / :height: instead of being ignored in silence.

  • A very large diagram no longer grows the page without limit: a rendered diagram is bounded to 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: lifts the bound, and printing releases it. (A 200-node needflow previously produced a 45,000-pixel page.)

✨ Needs: imports and external needs

  • External needs are linked

    An external need’s external_url is now computed when its source is read, from the source’s base_url and target_url, and stored on the need — as sphinx-needs does. needtable ID and link cells, needlist items and the :need: role all render real links to the site the need lives on, and external_url becomes an ordinary field: a :columns: entry, a :sort: key, a :filter: term, a Cypher property, and a key in the needs.json export. A source that cannot produce URLs now says so instead of leaving its needs silently unlinked.

  • Imported need content renders as sandboxed cards

    Each needimport renders one card per imported need, and imported needs become clickable everywhere a need renders. Content is sandboxed deny-by-default: images and file-reading directives inside imported content are refused with a visible diagnostic naming its config key, and a project can opt in per source ([[needs.import_sources]] with allow_assets / assets_root). Every need also gains a derived doctype core field. See What works inside imported need content.

  • Silent ``needimport`` outcomes now say so

    A :filter: that selects nothing reports its counts (needs.import.filter_empty, Info — it never fails ubc check); an :ids: entry naming no need in the source warns; an ambiguous doctype names every tied parser and the winner; and unresolvable references inside imported content are diagnosed at the hosting directive.

  • Python-dialect :filter: expressions in needtable, needlist, needflow and the :need_count: role can now read the incoming half of every link type as a <link>_back pseudo-field (implements_back, …), pinned to sphinx-needs’ own values.

✨ Sphinx parity in the index

  • Dangling references warn, as ``sphinx-build`` does

    A :ref: / {ref} to no label, a :doc: to no document and a :term: outside the glossary now warn — Sphinx’s exact message shapes under the codes std.ref, std.doc and std.term — in captions, table cells, substitution definitions and sphinx-design titles included. Targets defined inside excluded .. only:: blocks are exempt, so multi-format projects that are Sphinx-clean stay clean.

  • ``sphinx.ext.autosectionlabel`` and ``sphinx.ext.todo`` are ported

    Both are opt-in via [parse] extensions, with their upstream confvals (autosectionlabel_prefix_document, autosectionlabel_maxdepth; todo_include_todos, todo_link_only). todo renders as a first-class admonition and todolist as a live cross-page aggregation. One deliberate divergence: todo_include_todos defaults to true.

  • Every dimension is validated

    :width: / :height: / :figwidth: on images, figures, tables, diagrams and videos are read by docutils’ own grammar family; a value docutils rejects raises directive.measure_invalid (or directive.table_widths_invalid for :widths: lists) and the dimension is dropped instead of leaking into the page’s CSS.

  • Directive arguments and options parse their inline markup

    Markup in a directive’s argument or an inline-typed option value — admonition and topic titles, rubric, code captions, changeset text, card and tab titles and more — now renders, and references inside them resolve and warn when dangling. In RST and in MyST.

  • A plain :doc: now consults intersphinx inventories (Sphinx’s named-inventory phase), and std:ref is accepted as an alias of std:label in :external: references.

✨ MCP and the editor

  • MCP: ``search_references`` and ``resolve_reference``

    Two new tools let an AI agent see everything the project can cross-reference — local labels, glossary terms and documents plus every loaded intersphinx inventory. search_references is ranked keyword / symbol search returning paste-ready role snippets; resolve_reference answers how the build would resolve a given role and target — local, external, or not-found with near-miss suggestions — and can list every usage site that resolves to the same target, so a rename’s blast radius is visible before it happens. See Model Context Protocol (MCP) server.

  • MCP: structured content on every tool

    Every tool now declares an outputSchema and carries structuredContent against it, so a client can rely on an answer’s shape instead of parsing prose.

  • Terminal Claude Code, registered in one step

    A new ubcode.agent.id value — Claude Code (terminal), claude-code-cli — drives Pharaoh by handing the prompt to your own terminal session, so it works on a Claude subscription. The editor command ubCode: Register MCP server with Claude Code (or ubc agent install --register-mcp) registers the ubCode MCP server for the project after an explicit confirmation dialog.

  • ``ubc intersphinx list``, and inventories in the editor

    ubc intersphinx list renders every configured [intersphinx.projects.<name>] inventory (inspect only ever looked at one file), and the Std Referencing view gains an intersphinx node — project → domain:objtype → entries, fetched lazily, with click-to-open documentation. The :external: role family also completes segment by segment now, including after :external+ (+ is a new completion trigger).

  • The Diagnostics view gains a Group By toggle — by diagnostic code (the default) or by file path — persisted per workspace.

🤖 Pharaoh (experimental)

Still experimental (ubc agent remains hidden); see the Pharaoh documentation.

  • Drive runs on the new ``ubc agent prompt``: the verb prints, as plain text, the resolved instruction for the one next workflow action — the read-only twin of ubc agent run — and the drive starter now hands it to the agent, so each Drive click resolves one arm; the direct prompt output works much better with weaker LLMs.

  • Installed agent assets can be updated: a new ubCode: Update agent skills command runs ubc agent update, and Drive offers the update once per project per session when the installed preset is behind the one ubc ships (local edits are never replaced without a “Replace / Keep mine” confirmation). The drive contract’s teaching surfaces were also verified against the engine and trimmed by a third.

  • Writing a review verdict now refreshes every view that folds review state — the Gaps overview and the need graph’s overlay included — without resetting the graph viewport.

👌 Improvements

  • The cache is byte-reproducible: two cold builds of an unchanged project leave identical cache files behind, making a cache directory comparable and checksummable. (A project with a remote needimport still writes one clock-derived stamp.)

  • Link and backlink value lists render and export in sphinx-needs’ exact order and membership — deduplicated and naturally sorted — and an incoming chip no longer prints the target’s part id against the source’s id.

  • needtable: the sphinx-needs core/system fields (docname, lineno, is_external, …) are real :columns: entries and :filter: terms, :sort: orders by value rather than text (2, 9, 10, not 10, 2, 9), and :colwidths: renders a real <colgroup> with docutils’ relative-share semantics.

  • The PlantUML preprocessor’s include budgets are tightened to values measured against real diagrams (at most 64 included files and 8 MiB of included text, both far above what any legitimate diagram reaches). Diagram output is unchanged.

  • Site search stops matching on markup artefacts: state-directive chips, hyperlink target labels, permalinks and raw LaTeX are all out of the index.

  • An unknown directive option’s “did you mean” suggestion is capped by edit distance, so :priority: is no longer “corrected” to layout.

  • [lint] code lists accept trailing-.* family wildcards everywhere codes are configured (ignore, lsp_select, per-file and message ignores) — "std.*" suppresses the whole standard-domain channel in one entry. A [parse] extensions entry close to a known port name now gets a near-miss typo warning (config.unknown_extension) instead of silence.

‼️ Breaking Changes

  • One-time full re-index and re-render on upgrade

    The cache format moved (6.40 → 6.99) and the build manifest with it (15 → 49). The first run after upgrading rebuilds the index and re-renders every page once; this is self-healing and needs no action.

  • New warnings may appear on an unchanged project

    This release adds 28 new diagnostics, all reported by default — notably dangling standard references (std.ref, std.doc, std.term — the largest new surface for projects that have never run sphinx-build cleanly), duplicate labels and docnames (std.duplicate_label, std.duplicate_docname), dimension and option validation (directive.measure_invalid, directive.table_widths_invalid), needimport outcomes (needs.import.*), needs.extend_missing_id, and the ubc build html render radars (build.role_unhandled, build.diagram_file_unresolved, build.diagram_unconvertible). Every one is suppressible by code through the ordinary lint configuration, and whole families in one entry with the new trailing-.* wildcards ("needs.import.*", "std.*").

  • A bare ``”config”`` ignore no longer silences everything

    Six configuration checks that reported under the single config code now report under fine-grained config.<subcode> codes. A lint.ignore entry of "config" matches only the code-less configuration warnings from now on — replace it with "config.*" to keep suppressing the whole channel.

  • ``needs.json`` / Parquet / SQLite exports change shape

    Exports gain external_url and doctype (as schema properties, and as per-need keys when defaults are not elided), and link / backlink lists change order and membership (deduplicated, naturally sorted). A pipeline diffing exports byte-for-byte across the upgrade will see churn once.

  • ``ubcode.agent.cliPath`` defaults to the bundled binary

    The default changed from "ubc" (whatever is on PATH) to "", meaning the version-matched ubc bundled with the extension. Set it back to ubc explicitly if you rely on a PATH install for the agent commands.

  • Previously-accepted dimension values are dropped

    A :width: / :height: / :figwidth: value docutils rejects (:height: 50% on an image, :width: auto) no longer reaches the page’s CSS — it warns and the dimension is dropped.

  • MCP output shapes: search_docs’s text block is now compact JSON (was pretty-printed), and fetch_doc_page’s structured half carries provenance only — read the page body from the text block.

  • A failed cache write now stops the build on ubc build html, ubc build targets and ubc check, which previously continued in silence.

  • Warm builds converge to cold: a project where two imports pull the same need id may see a different copy win than its last incremental build produced — the winner is now a function of the sources, identical to --no-cache.

🐛 Fixes

  • A ``needimport`` that was re-ordered or re-configured no longer silently loses its needs — one of two directives reading the same remote source could contribute nothing, with no warning, until the remote re-check window expired. Re-ordering or adding needimport directives is also free now (no re-reads), and honours [needs.remote] cache_days.

  • The project cache commits as one group. A build interrupted between index writes could leave the cache describing two generations at once — a page silently gone from the site, a need gone from needs.json, a real warning suppressed for ever. Builds now write a commit record, verify it on load, and rebuild from scratch when it does not match.

  • A deleted source no longer poisons the cache: the deleted page was purged in memory but resurrected from disk on every later build — ubc check went green on a genuinely broken project and ubc build html failed permanently. Already-poisoned caches heal on the first build after upgrading.

  • Warm rebuilds converge on the cold needs state: deleting an include no longer leaves ghost needs, re-indexing no longer duplicates a fragment’s needs, and an edit inside a partial no longer leaves the stale copy as the winner.

  • needs.json read diagnostics say where the fault is — a needs_schema property missing field_type or type no longer rejects the whole source: it is reported once, naming the JSON path and the accepted values, and the source’s needs are read. Read failures name the JSON path, needimport diagnostics name the JSON source file, and Rust debug output no longer reaches these messages.

  • A ``:need:`` to a need defined in an included fragment links the page that renders the card, not a dead chip or the fragment’s non-page.

  • A ``:name:``-d figure or image has an anchor at all — every :ref: to one (and every published objects.inv URI) was a link to a fragment that existed nowhere; figure captions also gain the permalink.

  • PlantUML sequence and state diagrams that previously rendered as nothing now render — four emitter defects produced Mermaid the renderer refuses; measured over a 646-diagram corpus, 20 diagrams that emitted invalid Mermaid now emit valid Mermaid, with none newly broken.

  • Three inputs that could crash the preview while parsing a PlantUML diagram — a malformed skinparam scope, an extreme %darken argument, and a note on link written with the Kelvin sign — no longer do.

  • A PlantUML !return value is now held to the same size budget an assigned value is; a deliberately hostile diagram could previously exhaust memory (measured 217 MiB → 10 MiB on the probe).

  • A section heading inside a truthy only / if body is a real section again — the page keeps its title, and ubc format --preview no longer deletes the heading from the source file. In MyST, a heading inside a directive body renders as a rubric instead of emptying its section.

  • Built pages stop rendering chips Sphinx renders nothing for: hyperlink-target definitions and the role / default-role / highlight / default-domain state directives (their labels are out of the search index too; the preview keeps its chips).

  • ubc query filter resolves the docname field, and a needextend whose argument names no need warns (needs.extend_missing_id) instead of applying nothing in silence.

  • image.* wildcards select the builder-best file exactly as a Sphinx html build (glob + mimetype priority), a query / fragment suffix on an image URI no longer raises a false image.not_found, a todo whose body is a list no longer renders run together in the todolist, and end-of-line completions work in CRLF documents.

  • Need graph: link names scale with the graph instead of dwarfing it at low zoom, edges survive a theme change (labels lost their cutout and read as struck through), and rows leave room for the link name between them.

  • Diagnostic anchors tell the truth: positions are 1-based everywhere, a diagnostic raised inside an included fragment is anchored in that fragment rather than its host, and a circular-include message names the file project-relative instead of by its absolute path.

  • ubc format no longer panics on a bare enumerator line, and a deeply-nested declining table no longer hangs the parser (directive.nesting_too_deep bounds it).

0.31.1b1

Released:

30.07.2026

pre-release:

Five arcs this release: openCypher becomes the needs query language (CLI, MCP and the IDE filter boxes); Intersphinx brings cross-project references to the IDE and preview; the preview renders the sphinx-needs view directives (needlist / needtable / needflow) plus substitutions, toctrees, tables and much more markup; ubc check becomes the canonical project linter for CI; and Pharaoh gains an in-editor Agent panel that drives Claude Code, Mistral Vibe or OpenCode. Also: codelinks learns preprocessor-aware C/C++, Go and Bash, and remote needs.json sources are cached and served stale instead of vanishing on a failed fetch.

✨ openCypher queries

  • A real openCypher engine replaces the previous pseudo-cypher filter dialect. The project is queried as a property graph — each need a node labelled by its type, each typed link a relationship — supporting MATCH / OPTIONAL MATCH, WHERE, WITH, UNWIND, RETURN, UNION, ORDER BY / SKIP / LIMIT, aggregation, variable-length paths, EXISTS / COUNT subqueries and the openCypher function library (read-only, with query limits). It powers the new ubc query cypher subcommand, the Needs Index / Needs JSON filter boxes in VS Code (a bare expression over n still works), and the ubCode-only :cypher: option on the view directives below. See the query guide — and the migration notes under Breaking Changes.

  • MCP: ``query_cypher`` and ``get_graph_schema`` — AI agents query the needs graph with real openCypher and discover the schema (labels, link types, properties and available parameters, Neo4j-style), replacing the previous three needs tools (see Breaking Changes). Result tables are capped, with honest truncation notes and paging guidance.

  • Project context as query parameters — the reserved parameters $var (variant data, e.g. $var.deploy.region), $build_tags and $docname bridge project context into queries, and every node carries a computed docname property. An unknown parameter errors, naming the available ones.

  • Typo guards — a label, relationship type or property absent from the whole schema warns with a “did you mean” suggestion (on stderr in the CLI; as note: lines in MCP); the new ubc query cypher --strict flag turns these warnings into a failure.

  • Python-dialect parity and friendlier errors — bare True / False literals (the upstream total-count idiom for :need_count:), literal-on-the-left comparisons ("spec" == type) and the is_need / is_part constants now parse, so filters written for sphinx-needs stop failing whole views (18 → 4 view failures on the sphinx-needs docs corpus). Filter parse errors now name the offending position, unknown fields get “did you mean” suggestions, and the deliberately unsupported forms are documented with workarounds (see Differences from a Sphinx build).

✨ Intersphinx — cross-project references

  • :ref: and :term: references that miss locally now resolve against external Sphinx inventories configured in the new [intersphinx] section — in the IDE and in the preview, where they hydrate to real links carrying the target project’s titles and an ↗ marker. The explicit :external:role: / :external+inv:domain:role: family is ported too, for both reStructuredText and MyST.

  • IDE support is offline-capable:ref: completion includes external labels, hover shows the resolved title and URL, and every externally resolved site becomes a ctrl+clickable link. Inventories are cached per user with conditional revalidation; fetch problems surface as intersphinx.* warnings and never fail a build; offline = true pins the cache for network-restricted CI.

  • New ubc intersphinx CLIinspect <file-or-url> prints any inventory (Sphinx- or JSON-formatted, filterable), and cache dir | list | clean manages the cache.

✨ Preview

  • The sphinx-needs view directives renderneedlist and needtable (sticky header, projected columns, :columns: / :sort: validated with suggestions) render live filtered views hydrated from the index, :need_count: counts inline (including the a ? b percentage form), and needflow draws a real traceability graph — typed coloured nodes, per-link-type edges, legend — as a client-side Mermaid flowchart. Needs are selected via the shared Python filter surface or the ubCode-only :cypher: option, and a failed view renders an error block naming the reason. See the new needs tutorial.

  • Substitutions expand|name| references resolve from your replace / unicode / date definitions plus the Sphinx specials |version| / |release| / |today| (new [project] keys release, today, today_fmt); document titles and :ref: labels use the expanded text everywhere (hover, Site Map, needs.json); undefined, circular and runaway definitions are diagnosed (substitution.*).

  • Toctrees and the Site Map.. toctree:: renders as a nested list of links (a :hidden: toctree as a collapsed block); the new Site Map view shows the project’s navigation tree from the configured [project] root_doc; new toctree.* diagnostics report structural problems (missing documents, cycles, documents not included anywhere — :orphan: exempts); and ubc build list-documents gains --order <toctree|path>.

  • Tables buildlist-table, csv-table (including :file:, a tracked dependency) and table render as real tables.

  • literalinclude renders the included file as a highlighted code block with Sphinx’s filter pipeline (:lines:, :start-after: / :end-before:, :dedent:, :prepend: / :append:, …), as a tracked dependency, with honest diagnostics for the unsupported rest.

  • The raw directive passes through, with trust controls.. raw:: html emits its payload verbatim, in block position and inside substitution definitions (an rst_prolog-defined |br| finally works). The new [parse] key raw_enabled = false makes every raw directive inert (directive.raw_disabled plus an escaped placeholder) for pipelines that build unreviewed contributions, and the preview escapes raw HTML in untrusted workspaces. See Raw content.

  • The only directive renders conditionally — its tag expression is evaluated against build_tags; a falsy body shows as a collapsed greyed block in the preview and is excluded from the index (its needs and targets do not resolve, matching Sphinx).

  • Markdown hosts splice files too — MyST {include}, {literalinclude}, {csv-table} and {raw} with :file: now read their files on every surface, with cycle detection (which a Sphinx build lacks entirely).

  • sphinx-design components and :octicon:grid, card, dropdown, div and the button directives render with sphinx-design’s own markup (structure only in the preview for now — styling follows), button-ref and card :link: options participate in reference resolution, and :octicon: renders real inline icons.

  • Built-in extension ports — the new [parse] key extensions enables ported Sphinx extensions under their conf.py names: sphinxcontrib.video (video renders a real HTML5 player; playback inside the VS Code webview is still limited), sphinx.ext.extlinks (link-shorthand roles declared in [parse.extlinks]), and sphinx_syntax_example — a new syntax-example directive (its Sphinx twin published to PyPI as sphinx-syntax-example) that shows markup as highlighted source plus rendered result, with opt-in per-document numbering (syntax_example_numbering). Ported directives join autocomplete and hover. See Parsing.

  • Highlighting and pinningrst, myst and cypher code blocks now highlight, plus jinja, powershell, bat, cmake and latex; and a new 📌 pin toggle holds the preview on its document instead of following the active editor.

✨ Indexing & needs data

  • Included content reaches cross-file features.. include:: and rst_prolog / rst_epilog are now spliced on the index path too, so targets, needs and glossary terms defined there resolve project-wide, and references bound via default-role or custom roles are collected like an explicit :ref:.

  • Dangling hyperlink references are reported — an unresolved, duplicated or circular named hyperlink reference (target_, and the backtick-quoted phrase form) is diagnosed (ref.* codes) in the editor and ubc check.

  • Remote needs sources: cached, revalidated, stale-served — the new [needs.remote] section (cache_days, timeout, offline) governs every needs.json fetched over HTTP(S); a failed refetch now keeps the previously loaded needs and reports the failure (previously the needs silently vanished for 24 hours), revalidation is conditional (ETag / Last-Modified), and file:// URLs work.

✨ ubc check — the canonical project linter

  • Whole-project by defaultubc check (no arguments) now indexes and lints the entire project, Markdown included, reporting everything the editor reports (needs, reference, toctree and schema diagnostics included); ubc check <paths> scopes the findings to the given paths; --per-file restores the old isolated single-file behaviour. See linting on the command line.

  • Severity-aware exit policy — findings are summarised by severity (Found 1 error(s), 2 warning(s).), and the new --deny <info|warning|error> and --max-warnings <N> flags (on both ubc check and ubc build index) control what fails the run.

  • --output-format json — machine-readable diagnostics on stdout (code, severity, message, source locations), with the human progress chatter demoted to stderr.

🤖 Pharaoh (experimental)

Still experimental (ubc agent remains hidden); see the Pharaoh documentation.

  • The Pharaoh Agent panel — a new sidebar chat that runs an external coding agent in your workspace over the Agent Client Protocol: Claude Code, Mistral Vibe or OpenCode (GitHub Copilot keeps its native chat). The agent sees the project’s installed skills, asks inline before touching files or running commands (with a per-session auto-approve), and gets the ubc MCP tools. Provider API keys come from the environment — Claude Code needs an Anthropic Console ANTHROPIC_API_KEY (subscription logins are not permitted over ACP); keyless setups degrade to copyable prompts.

  • Harness-aware Drive and Run QA — the ubcode.agent.id setting now routes Drive / Capture intent to Copilot Chat, the Pharaoh panel, or plain clipboard copy (other), with always-available copy-prompt buttons; Run QA scores through the selected agent too (pin a different one with ubcode.qualityAnalysis.agentId to avoid self-review). Prompts name the bundled, version-matched ubc.

  • Documentation grounding — new MCP tools search_docs and fetch_doc_page ground any agent on the published ubCode and sphinx-needs documentation (symbol-aware: an exact needs_types hit outranks keyword matches), and the @pharaoh participant now uses the same engine-side retrieval, degrading gracefully when the docs are unreachable. The Clear documentation cache command is gone (the cache is now session-scoped).

  • Portable skills, better onboardingubc agent install additionally writes skills to the harness-neutral .agents/skills/ location (read by Mistral Vibe and OpenCode), and install --detect now derives per-type skills and a [quality] section, so Run QA works right after onboarding an existing project.

👌 Improvements

  • Directive diagnostics now underline the offending option name (or the directive name) instead of the whole directive block.

  • ubc build targets derives every objects.inv anchor and display name through the exact docutils / Sphinx rules, so external projects linking into yours land on anchors that exist.

  • The PlantUML-to-Mermaid preview conversion decodes display-text escapes (\n line breaks in notes, labels and member names) and renders multiple stereotypes.

  • The Pharaoh chat transcript renders full Markdown (tables, task lists, nested lists) with bounded parsing, so a pathological reply degrades to plain text instead of freezing the panel.

  • The ubCode sidebar is reordered (everyday views first), and the Site Map gained a pinnable Project row showing the followed file and its ubproject.toml.

‼️ Breaking Changes

  • MCP needs tools replacedget_data_for_single_need, get_schema_for_need_filter and query_needs are removed in favour of query_cypher and get_graph_schema. Update saved prompts, agent configurations and allowlists.

  • Cypher filter semantics — the VS Code needs-view filter boxes (and anywhere the previous pseudo-cypher dialect was used) now evaluate real openCypher: a missing field reads as null with three-valued logic (NOT (n.priority > 7) now drops needs without a priority), =~ is anchored full-string regex (was substring), upper() / lower() become toUpper() / toLower(), and the implicit one-hop l / o namespaces are removed (the error suggests the explicit MATCH (n)-[l:type]->(o) rewrite). The Python dialect and free-text search are unchanged. See the migration notes.

  • New warnings may appear — whole-project ubc check surfaces diagnostic families the old per-file mode never ran, and the new reference, toctree and substitution diagnostics also reach the editor — previously-clean CI runs may newly fail. Scope with --per-file, or suppress individual codes via the standard lint configuration. Info-only runs now exit 0 (use --deny info to fail on any finding).

  • View output is cappedneedlist / needtable / needflow default to :max_items: 100 with an explicit truncation notice, and default ordering is id-sorted for build-to-build reproducibility (upstream: document order).

  • ubc build list-documents orders by toctree by default and fails when the index cannot be built; --order path restores the previous pure-discovery listing.

  • One-time full re-index on upgrade — the cache format moved 6.15 → 6.40 across this release’s features and the config hash gained the new keys, so every project cold-rebuilds its index once. Self-healing, no action needed.

  • Driving Claude Code is ACP-only — selecting claude-code no longer opens a terminal with the claude CLI pre-typed; it runs in the Pharaoh Agent panel and requires ANTHROPIC_API_KEY. Declining the agent-skills install offer now genuinely cancels the action it gated (previously it carried on regardless).

🐛 Fixes

  • Crash fixes: a needextend argument of a lone " aborted the CLI; a malformed list-table / table could crash the language server and duplicate index entries; and pathological filter strings could overflow the stack — filter expressions are now bounded (16 KiB, 64 nesting levels).

  • :term: case-insensitive fallback is now deterministic when two glossary terms case-fold to the same string — resolved links no longer flip between builds.

  • Directives from enabled extension ports appear in autocomplete and hover (previously the parser knew video but completions did not).

  • ubc agent doctor and config-validate now reject a config that fails to load, in agreement with agent next, naming the offending line; doctor newly honours -c/--config and UBCODE_CONFIG_OVERRIDE.

  • Workflow editor: clicking a stage without a review skill no longer crashes the inspector, the graph follows the panel size, uses a proper layered layout (no overlapping labels), and no longer opens blank.

  • Pharaoh Gaps: a project whose ubproject.toml lives in a subfolder is no longer treated as greenfield, Set up project no longer tries to write to the filesystem root, and a need awaiting review reads “Missing review” instead of its own stage.

  • Onboarding hand-off prompts name the bundled, version-matched ubc (not whatever ubc is on PATH), and the detect-answers file is written outside the project and cleaned up afterwards.

  • Site Map rows no longer attract other extensions’ context buttons, and a collapsed hidden-toctree block in the preview can be expanded even with click-to-source enabled.

  • Codelinks: one-line markers terminated by a newline anchor to the comment start, so prose mentioning @author mid-comment no longer produces a bogus need.

  • Patched high- and moderate-severity advisories in the VS Code extension’s production dependencies (brace-expansion DoS, dompurify XSS, uuid bounds check).

0.31.0b1

Released:

16.07.2026

pre-release:

Three arcs this release: the Rendered preview hydrates cross-file links and need cards live from the index (and renders far more markup); the engine picks up saves, includes and imported content, with a wider diagnostic net; and Pharaoh — ubCode’s experimental agentic engineer — makes its debut.

✨ Preview

  • Cross-file hyperlinks (reference “hydration”)

    :ref: / {ref}, the :need: role family (directional and title <target> forms included), :doc: and :term: now render as working links in the Rendered preview once the background index has resolved the target: click-through opens the target document at the exact line, and placeholder chips upgrade to links as indexing completes. Resolved links show their target’s real title (explicit link text wins, then the resolved title, then the bare target; :need: references render as “Title (ID)”, matching sphinx-needs), and relative :doc: targets resolve Sphinx-faithfully from subdirectories. Unresolved (or !disabled) references keep the placeholder chip, and typing latency is unaffected — the preview only looks up what the index has already published; it never resolves.

  • Need cards

    Need directives (every type from needs.types) render as proper cards — type badge, title, ID, meta table and body — hydrated live from the index: field values include needextend modifications, outgoing links are clickable, and incoming “links back” rows show data absent from the source file. Cards show authored and extended state only, following the sphinx-needs show_in_layout taxonomy (config-default padding is skipped, so cards stay clean on hundred-field projects), and upgrade as indexing completes; Structural mode keeps the placeholder.

  • .. include:: is expanded in place

    Nested includes, live unsaved edits of the included file, the docutils slicing options (literal, start-line / end-line, start-after / end-before), section re-basing, and cycle / depth protection; a failed include shows an error chip carrying the docutils-shaped message. The included file becomes a tracked dependency of the index — editing it re-indexes every document that includes it — and missing files or failed slicing matches are diagnosed (directive.include_* codes). Not yet expanded: MyST {include} and the remaining docutils options (parser, code, …), each diagnosed as unsupported; targets and needs in an included file still attribute to the fragment file.

  • Document state: default role, custom roles and highlight language

    .. default-role::, doc-local .. role:: definitions and .. highlight:: now take effect instead of being inert: bare interpreted text (`` text ) renders as docutils' ``title-reference — following an active default-role — instead of raising a bogus warning, custom roles resolve within the document, and highlight sets the language of subsequent literal blocks (a muted chip marks each state change). New [parse] keys — Sphinx confval equivalents — seed the initial state: default_role and highlight_language (an unknown configured role is diagnosed as config.unknown_default_role), plus rst_prolog / rst_epilog to prepend / append source to every RST document — as in Sphinx, prologue state applies to the whole document, and an .. include:: inside it works (substitution definitions do not yet expand). See Parsing.

  • Code blocks render with syntax highlighting

    The code-block, sourcecode and code directives render as real literal blocks (previously generic placeholders) with :caption:, :name: and :class: support, coloured by bundled client-side highlighting that follows the editor’s light / dark theme. The language comes from the directive argument, falling back to the current highlight state or [parse] highlight_language.

  • Much wider markup coverage

    Newly rendered: the docutils text roles (:title-reference: and friends), deterministic :pep: / :rfc: link roles, the Sphinx UI and styled-literal roles (:kbd:, :menuselection:, :file:, …), the sphinx-needs :variant: role, version-change directives (versionadded / versionchanged / deprecated), structural block directives (topic, sidebar, rubric, …), centered and hlist, diagram directives with file arguments, image / figure parity, admonitions including seealso and :collapsible:, and generic need-type rendering. Unhandled roles now at least visualise their name and content. The new Authoring reference documents exactly what renders — and where the preview deliberately differs from Sphinx.

  • Diagram captions, alignment and sizing

    The mermaid, uml and plantuml directives’ :caption: and :align: options are now respected — a caption wraps the diagram in a figure — and :width: / :height: size it (a bare number means pixels; a small deliberate extension over upstream sphinxcontrib-mermaid so all three directives size consistently). Diagrams without these options render exactly as before.

✨ Indexing & diagnostics

  • Saving now refreshes cross-file features by default

    [server] index_on_save now defaults to true, made safe by a new trailing debounce: burst saves coalesce into one indexing pass after a quiet window ([server] index_debounce_ms, default 500; 0 = immediate). Diagnostics, preview hydration and need cards heal on save out of the box; set index_on_save = false to restore the old behaviour.

  • if directive for conditional content (Sphinx-Needs 8.2 parity)

    The Sphinx-Needs .. if:: / {if} directive is now supported: its argument is a filter expression evaluated against the build’s variant data (the var.* namespace from needs_variant_data). A truthy condition includes the body — needs inside are collected and indexed as if unwrapped; a falsy condition excludes it entirely — needs are not collected and targets do not resolve, matching Sphinx-Needs. An expression that cannot be evaluated is reported as if.invalid_expression and the body is excluded.

    In the editor, a falsy if block renders in the preview as a collapsed, greyed block labelled with its condition, and the inactive source region is faded (like disabled code behind an #if in a C editor) — conditional content stays visible without cluttering the active view. See the if directive.

  • Real-time validation of need option values as you edit

    Need option values are now checked against their schema while you type, publishing editor squiggles for the open document — no full project build required — in both reStructuredText and MyST Markdown (previously these issues surfaced only after a full index build, in the diagnostics panel). Three codes are published, each suppressible via the lint configuration: needs.invalid_field_value (enum / const, string length, numeric bounds, array constraints), needs.invalid_link_value (link-ID length), and needs.dynamic_functions (a [[...]] value that does not look like a dynamic-function call). The checks are deliberately lightweight — regex pattern / format validation and dynamic-function values are left to the full build — so they stay cheap on every keystroke.

  • Imported need content is now parsed

    The content of needs pulled in via needimport is markup-parsed in the importing document’s format: need parts become addressable, :need: references join the link graph (including backlinks), and markup problems surface as diagnostics anchored at the needimport directive, prefixed with the need’s ID. Imported content parses context-free — without the host document’s substitutions or relative paths — a deliberate, documented divergence from sphinx-needs’ accidental host-context leakage; nested generative directives are diagnosed (needs.import.nested_generative), never executed.

  • Markdown (MyST) documents contribute to the project index

    Markdown / MyST documents now contribute their structure (titles, headings, targets, footnote definitions) to the project index, as reStructuredText always has — cross-references into MyST documents resolve (including from reStructuredText, and in the preview links above), and Markdown titles surface in language-server features.

  • New markup diagnostics

    All surfacing as editor squiggles (ubcode-syntax source), in ubc check and in the build index log, governed by the standard lint configuration:

    • invalid directive option values and choices, diagram content-plus-file conflicts, and non-consecutive Markdown heading levels;

    • directives missing a required argument (reStructuredText and MyST);

    • invalid :variant: / :pep: / :rfc: role content;

    • directive-name collisions between needs.types and extend_directives in the configuration.

🤖 Pharaoh — the agentic engineer (experimental)

This release debuts Pharaoh, ubCode’s agentic engineer: describe your engineering process once — the stages a requirement passes through, and the evidence each stage needs — and from then on AI does the writing while the engine does the judging: what is done, what is missing, what to do next. Experimental (ubc agent is still hidden from ubc --help) and may change between releases; see the Pharaoh documentation for the full guide.

  • Describe your process; the engine judges

    A new [workflow] configuration section (Workflow) declares your process as stages that produce need types, depend on each other, and carry gates (trace links, code coverage, lifecycle, review). On top of it: ubc agent next names the single next thing to build (make for your requirements), gaps lists every unmet obligation, status reports each stage done / ready / blocked, and release-check is the combined final gate — all projections of one deterministic evaluation, so they can never disagree.

  • Onboarding, greenfield and brownfield

    ubc agent install scaffolds a workflow from a profile (default: a V-model) plus its AI-asset bundle of skills and agent definitions; install --detect onboards an existing project by deriving a drivable [workflow] from the observed link graph and schemas, raising only what it cannot infer as a few evidence-backed questions (--plan previews, --answers applies); ubc agent doctor pre-flights the setup.

  • Quality Analysis — independent review of need substance

    The graph judges structure; Quality Analysis judges substance — and the AI never grades its own homework. A new [quality] section (Quality Analysis) defines criteria packs (per-axis rubrics as TOML data) and per-axis floors: an LLM measures each need, the engine judges pass / fail deterministically at read time, and the IDE explains the result. Verdicts live under .pharaoh/verdicts/ and fingerprint both the need’s one-hop content and the criteria, reading as outdated when either drifts. The review-brief, verdict-submit and verdict-check verbs gate substance fail-closed (also folded into release-check --with-verdicts); in VS Code, Run QA drives the review through the editor’s language-model access (ubCode never talks to an LLM provider itself), with a results panel and live CodeLens badges.

  • The VS Code cockpit

    Pharaoh Workflows and Gaps Overview panels, a workflow-aware needs graph with gap rings and a parent / child depth control, a read-only workflow-editor graph, per-need CodeLenses with gap diagnostics in the Problems panel, and the Drive / Accept / Tailor commands — hand the next step to your AI agent, commit only the authored artefacts, or derive a [workflow] proposal from existing types and links. Without a [workflow] section none of this appears — the agent surface stays silent until you opt in.

  • The chat participant is now @pharaoh

    Renamed from @ubcode (kept as an alias; chat history survives), with high-level intents captured via /pharaoh-intent. The participant — and every driving skill — now runs the extension’s bundled, version-matched ubc binary: no PATH setup, no mismatched installs.

👌 Improvements

  • Language-server request latency

    Requests no longer deserialize the needs / domain / diagnostics indexes from disk — they reuse the in-memory snapshot the background indexer already published (roughly 66 ms → sub-millisecond per request at 10,000 needs), and the indexer reuses its own indexes across passes, so incremental network_back validation now engages between background runs. External CLI builds are still adopted automatically.

  • Faster schema validation: needs-network schema validators are now precompiled once per pass, with a hub-graph fallback for highly connected projects.

  • Faster index builds: the config-derived directive-specification table is built once per pass and shared, instead of rebuilt per file — roughly 0.6–0.8 s of cold-build wall time on a 5,000-file corpus.

  • Markdown (MyST) inline constructs now carry precise source ranges, improving diagnostic and navigation targets.

  • New “Authoring” documentation section: a reference for what the preview renders — directives, roles, needs and diagrams, Markdown — with an explicit differences-from-Sphinx page (see Authoring).

  • The editor-title preview icon now carries a small magnifier badge, so it is distinguishable from the built-in Markdown preview icon.

  • Hovering a :variant: role value in reStructuredText now shows its resolved variant data, and the docutils generic text roles are offered in role-name completion.

  • The PlantUML-to-Mermaid preview conversion handles multi-line and rectangle / hexagon sequence notes, activation-shortcut colours and group end variants, with hardened diagram detection.

♻️ Changes

  • Preview always renders via the unified AST; new Rendered / Structural modes

    The preview now always lowers the parsed document to the unified AST and renders from it — the per-format concrete-syntax-tree path is retired, along with the internal server.use_ast_preview flag (never part of a release). The old rich/plain toggle becomes two modes, switchable live from the preview toolbar:

    • Rendered (the default) — every construct renders as its semantic HTML shape (headings, admonitions, images, diagrams, tables, …).

    • Structural — “show me what was parsed”: every directive and role renders as a uniform placeholder (name, arguments, options, body), identical for reStructuredText and MyST.

    The VS Code setting ubcode.preview.experimentalRendering (boolean) is renamed to ubcode.preview.mode (rendered | structural), which seeds the panel’s initial mode.

  • Need extraction is sourced from the unified document AST

    Need directives (fields, links, parts, content links, diagnostics) are now extracted from the same unified AST the preview renders from, rather than a separate concrete-syntax-tree walk — an internal unification with no intended behaviour change, apart from the MyST body-role fix below.

‼️ Breaking Changes

  • One-time full re-index on upgrade

    The cache format version moved 6.10 → 6.15 (imported-content parsing, glossary display text, :doc: / :term: site collection, the state-directive diagnostics, include dependency edges) and the config hash gained new inputs ([server] index_on_save / index_debounce_ms, [parse] default_role / highlight_language / rst_prolog / rst_epilog), so every project cold-rebuilds its index once after upgrading. Self-healing — no action needed.

  • New warnings may appear

    ubc check / ubc build index (and the editor) now report the markup-semantics, required-argument and role-resolution diagnostics above, the needs.import.* codes, the interpreted-text role codes below, and config.unknown_default_role — CI gating on warning counts may newly fail. Suppress individual codes via the standard lint configuration.

  • New include warnings may appear

    Projects with broken .. include:: paths — or jinja-templated ones (e.g. {{ build_root }}/…), which ubCode cannot expand yet — now get directive.include_not_found warnings (~150 on one large real-world corpus, from templated paths alone). Suppress the code until jinja support lands.

  • Diagnostic changes for interpreted text

    The inline.role_no_name warning is retired — bare `` text `` is legal docutils — so suppressions keyed on it are dead configuration. In exchange, directive.unknown_role, directive.invalid_role and directive.role_redefinition are diagnosed (docutils reports these as errors; on by default).

  • Anchors unified to exact docutils rules

    Rendered previews and the objects inventory derive every anchor through one docutils-faithful make_id (differentially fuzzed against real docutils). Anchors that diverged from sphinx-build output move to the Sphinx-identical form — a parity upgrade, but external deep links to the old anchors need updating.

  • needextend application order now matches sphinx-needs

    When several needextend directives modify the same field of the same need, they apply sorted by (docname, lineno) — the sphinx-needs contract, verified against a real sphinx-needs 8.2.0 build — instead of file-path order. Projects whose path order and docname order differ (e.g. x.rst vs x-y.rst) may see different final values; the new values are the ones sphinx-needs produces.

  • Background indexing now runs on save by default — set [server] index_on_save = false to restore the previous behaviour (see the feature entry above).

  • Editor diagnostic source renamed: ubcode-rst-lintubcode-syntax — the source now covers Markdown too. Any client-side filters keyed on the old source string must be updated.

  • Needs inside a falsy .. if:: block are now excluded from the index: previously the (unknown) directive was a no-op, so such needs were still collected.

  • Pharaoh state directory: agent state now lives in .pharaoh/ (a .ubc/ from earlier development builds is ignored) — verdicts re-derive on the next review, the install manifest on the next ubc agent install.

🐛 Fixes

  • The CommonMark parser no longer panics on a no-break space (U+00A0) near heading / paragraph boundaries.

  • Image and figure src paths now resolve correctly in the Markdown (MyST) preview.

  • MyST need bodies now collect their {need} and {need_part} roles

    A {need} / {need_part} role inside the body of a MyST need directive is now collected (as a content link / need part), matching reStructuredText need bodies. Previously such roles were silently ignored, so content links and parts were missing from the extracted need (and part-dependent diagnostics such as duplicate-part warnings could not fire).

  • MyST diagram directives now render in the Markdown preview.

  • The MCP stdio server no longer drops connections intermittently on Linux.

  • Language-server edge-case fixes: float enum values in need-field validation, hover ordering, and schema key handling.