0.38.x

0.38.0

Released:

22.10.2026

The story of this release, in five arcs: fields per need type, in lockstep with Sphinx-Needs 9.1.0 — a need type declares its own fields and link types, inherits another, narrows a shared field and says which types a link may point to; a link type may be the union of others, and needs link by matching values; Cypher and the MCP server at scale — the MCP server keeps a project’s index in memory between queries, answers a burst of queries without re-scanning on request, lists which properties each label carries, and the engine orders, counts and groups large projects ten to fifty times faster, thousands of times for a burst of queries, with the same answers; a PDF that typesets maths — ubc build pdf (alpha) sets display and inline formulas in KaTeX’s own faces, breaks lines as a browser does, and places scripts, markers and table cells where the site puts them; change requests — ubc agent change turns an edit to needs that already exist into a record of its own: the engine computes what the edit reaches, your AI agent rules on each of those needs, a person approves, and the change stays a gap until it is closed out; and documentation rebuilt around what you want to do — generated references for every command, key, setting, tool and diagnostic code, a live Writing reference, reader paths, and pages for teams, for any editor and for coming from Sphinx-Needs. Also: ubc build html, ubc build pdf and ubc build linkcheck are now Beta; report insights (alpha) render a page for a need from declared Cypher queries; ubc agent holds a stage while a need it produces carries a reported diagnostic; a filter may test one field against another with in; a bounded variable-length Cypher pattern is expanded from its bound end; every sphinx-design badge role and octicon renders; a count below its minimum is reported; and the published configuration schema is a third smaller. New warnings can fire on an unchanged project, and the diagnostics gate is on by default (see Breaking Changes).

This release is the ubCode half of a lockstep with Sphinx-Needs 9.1.0: need types as tables, inherits, fields and links on a type, target_types, the union kind and the join role are read by both tools from this release and 9.1.0 onward, with the same values and findings on a shared conformance corpus. A Sphinx build with an older Sphinx-Needs does not read the per-type keys and reports a join or union rule as unreadable; ubCode names 9.1.0 once per project for those two (config.derive_sphinx_minimum).

✨ Fields per need type

  • Need types as tables, validated. [needs.types.<directive>] tables may stand beside the [[needs.types]] array, in the order given. An entry that cannot be used no longer stops the configuration: an entry missing its title or prefix is left out, a non-string color or style takes its default, an unknown key is ignored and a directive given twice uses its last entry, each a config.need_type warning. An entry naming req, spec, impl or test with neither title nor prefix is that built-in type, narrowed. See The table shape.

  • A type declares its fields and link types. A [needs.types.<type>] entry may carry fields and links, narrowing a declared field for that type’s needs (a stricter schema, nullable = false, its own default, predicates or derive), and inherits another type’s declarations; defaults, required fields, validation, needextend deletes and the editor’s checks and completions follow the need’s type. A fields or links entry naming nothing declared elsewhere declares a field or link type of those types alone, under one rule, “one name, one shape”: a need of another type holds null ([] for a link type), a value set there is needs.undeclared_for_type, needs.json says Declared by need types …, and Cypher’s keys(n) leaves it out. What cannot be used is ignored as a whole and reported: config.type_inherits, config.type_conflict, config.type_widening. See Fields per need type.

  • Which need types a link may point to. target_types = ["req"] on a link type, or on a need type’s narrowing of it, accepts a req or a type inheriting from it; every other target is reported as needs.link_target_type (needs.external_link_target_type for a need from external_needs) and kept, and the editor’s link completion lists the accepted needs first. Until now the key was ignored silently. See Link types.

  • A derived union of link types. derive = { kind = "union", fields = ["links", "blocks"] } on a link type holds the need’s links of the members it names, each once, in member order, computed after every needextend, with its own back links; dead links and link conditions are reported at the member. It is the declarative form of copy = true, which Sphinx-Needs 9.1.0 deprecates and ubCode keeps reading. See union.

  • Needs linked by matching values. join = { field = "case_name", equals = "test_case" } on a links or copy rule takes as candidates the needs whose field equals one of this need’s equals values, each once, in id order; links_from_match(field, equals) is the inline spelling, the eighth built-in dynamic function and the nearest form of sphinx-test-reports’ tr_link. See Matching a field: the join role.

✨ Querying at scale

  • The MCP server keeps the index in memory. get_graph_schema and query_cypher start from the index the previous call left in memory and re-index only what changed: 0.37.0 reloaded the whole project on every call, about 1.7 s at 200 000 needs, where a call now first checks for changed files, about 80 ms. ubc serve mcp --projects-in-memory N (default 2; 0 reloads on every call) and --unload-after-idle-minutes N (default 10) bound it; the VS Code extension launches the server with one project resident. See Keeping the index in memory.

  • A burst of queries without re-scanning. assume_unchanged: true on either tool answers from the index as it stands, carrying an index stamp saying when it was indexed and how old that is; the configuration and the license gate still run. A small query at 200k needs drops from tens or hundreds of milliseconds to a few. See Skipping the re-scan: assume_unchanged.

  • The schema says which properties each label carries. get_graph_schema and ubc query cypher --schema gain label_properties, so a field declared on need types alone is listed only for the types that carry it, and a query written from the schema alone runs. See get_graph_schema.

  • A field on both sides of in. A filter testing one field against another — owner in tags, platform in var.archs, owner.lower() not in tags — used to be refused; it is evaluated as Python does (substring when the right-hand field holds a string, membership when it holds a list), in every :filter:, needextend and links_from_filter and in a <<[...]>> variant value; it stays refused in a variant condition, which now names the form. See Python syntax.

  • The engine is much faster on large projects, with the same answers. Measured over MCP on a synthetic project of 200 000 needs and 620 000 links: queries that took 1.7 to 8.3 s in 0.37.0 take about 90 to 190 ms, or 1.6 to 16 ms with assume_unchanged (an ordered four-hop traversal: 8.3 s, 157 ms, 2.8 ms), and the server’s peak memory on them falls from 0.6 to 1.3 GB to 370 to 560 MB; every answer is byte-identical, checked against the openCypher TCK and the engine’s reference mode. ORDER BY … LIMIT keeps only the rows the limit needs and stops a scan ordered by id early; count(*) over a pattern counts without building rows; deep SKIP pages and filtered pages ordered by id stop at the page; grouping by n.type and a single-hop count { … } or exists { … } come off tables; length(p) of the first path keeps the early stop; DISTINCT aggregates de-duplicate by hash; “did you mean” notes check the declared names first. The needs graph builds about 40 % faster in half the memory, resolves links on up to four threads from 8 192 needs, and is built once per snapshot and shared by every :cypher: view of a build or preview, which makes ubc build html of such a project with twelve :cypher: views about 22 % faster. The needs index takes about 30 % less memory once loaded and loads 13 to 26 % faster on projects of 100 000 to 200 000 needs, ubc query returns about 30 % sooner, and the language server tells the editor about a new index about 0.35 s sooner at 100 000 needs and 0.7 s at 200 000, with no request stalled while the old one is freed.

  • A bounded variable-length pattern is expanded from its bound end. (d)-[*1..5]->(c {id: $current_id}) walked every path of up to five links from every need, to keep those ending at c. Such a chain now starts from a node an earlier clause bound or, when its first node is bare (no label, no inline property), from the node with the most of a label and an inline property, as long as its longest path is under 1 000 links and every inline property is a literal or a given parameter. On 200 000 needs, an insight written that way that timed out at 10 s renders in about 1.1 s, and the same query over MCP takes 73 ms where it took 12.9 s. See Queries and the template context.

✨ PDF build (beta)

  • Maths is typeset. .. math::, MyST $$ and {math}, amsmath environments, :math: and $…$ are set from the same TeX the site hands KaTeX, in KaTeX’s own faces, numbered per document, at 1.21 times the text ([build.pdf.typography.math] size) with one em around a display formula; a formula too wide for its page or cell is drawn smaller, and one the typesetter refuses is shown as its TeX, with build.pdf_math_unrendered naming why. See Maths.

  • Lines, scripts, markers and cells as the site has them. Text is laid out with parley 0.12: line breaks follow UAX #14 (no break before a /, ) or . after a space), a no-break space never hangs past the margin, and a narrow cell of emoji sequences no longer aborts the build. Every paragraph is set on the site’s left-to-right base, so one beginning in Hebrew or Arabic sits where the browser puts it. :sub: and :sup: shift as a browser shifts them and grow their line when they must, and footnote marks are raised the same way. A chip takes its own font’s box, a paragraph of one class role keeps its block’s spacing, a list marker sits on its item’s baseline, and the text cells of a row align on their first baselines.

  • A hidden need as its header line. [build.pdf] hidden_needs = "collapsed" prints a :hide: need as one line of type, title and id, as the site’s closed card; the default "full" prints the whole card as before, and labels inside the unprinted body stay link destinations. See Options.

✨ Change requests (experimental)

  • A change to existing needs, recorded as a need. A change_request need holds the needs it is aimed at (cr_anchor), what its impact analysis reached, the assessment’s ruling on each (cr_impact_modify, cr_impact_remove, cr_impact_excluded), the decision and an append-only status log, cr_status; it is written where [workflow.change_request.route] says, never at a guessed path. ubc agent change plan --id (repeatable; --cr-id, --purpose) walks up from each anchor and all the way down, never down from what it reached going up: a mean of 4.2 to 5.7 needs per anchor on the three projects measured. See Changing what is already specified and its configuration.

  • Assess, approve, execute, close. assess has your AI agent rule on every reached need and always write a Planned changes section, one bullet per modified or removed need saying what changes in it, which execute reads before it edits that need (the Pharaoh Changes view’s Assess prompt asks for it too); approve --by refuses until the rulings cover the reach (approval before the work, as Automotive SPICE SUP.10 asks); execute leaves the work as uncommitted edits; close --by (--dry-run to only report) checks that what was removed is gone. reassess, remove, show and list complete the verbs; assess --print, execute --print --step N and execute --done --by serve an agent already in the session. See the verbs and the command reference.

  • Unfinished work is a gap. An approved or executed change request is a change_incomplete gap until it is closed. A gap on a need of a type no stage produces, a change request’s among them, now makes ubc agent next answer reason: "gaps" and list it in unattributed_gaps, where it answered done. VS Code’s Pharaoh Changes view leads with the step each change request waits on, through the language server’s agent.changeList, agent.changeShow, agent.changeClose and agent.changeApprove. See Unfinished work shows up in gaps.

✨ The diagnostics gate (ubc agent)

  • A diagnostic on a workflow need holds its stage. A finding ubc check reports after [lint] on an active need of a type a stage produces holds that stage; one that means a need failed to exist (codelinks.*, needs.build_error, needs.create_need, needs.type_resolution) is held by the stage whose route.path holds its file. The stage stays ready, ubc agent prompt puts a FIX (diagnostics): order first, ubc agent gaps and release-check fail, and no release-check baseline accepts a finding.

  • Configured in [workflow.diagnostics]: severity and schema_severity (both "warning" by default, "none" switches a bar off), and allow and deny lists of codes or family.*; schema-network findings hold a stage only when deny lists them. Only ubc agent validates [workflow] now, so a key it does not know fails those commands alone, and a -c on it merges key by key where it replaced the whole section. See Diagnostics gate.

✨ Report insights (alpha)

  • A report rendered for a need, from declared Cypher queries. A [reports.insights.<name>] table names a .html.j2 template, the needs it takes (input: none, one or several), the need types it is offered for and named Cypher queries, run once per render over one needs graph, whose rows the template reads as queries.<name>; the input needs reach a query as $current_id / $current_ids. ubc report <insight> --need <ID> renders it; in VS Code an insight lens on every need of an accepted type renders it beside the editor, and the Reports view lists insights. Every declaration is checked when ubproject.toml or a .cypher or .j2 file is saved. Two functions join every report template, insights or not: need_url(id) and theme_style(). See Insights.

✨ Writing

  • sphinx-design badges. All 69 bdg, bdg-link and bdg-ref roles, in every colour and outlined, render in the site, the preview and the PDF with sphinx-design’s classes; bdg-ref* resolves like :any:, an unresolved target is std.any, and completion offers them. See Badges.

  • Every octicon. All 328 icons sphinx-design ships are embedded, where 101 were, so an :octicon: name such as apps or diff renders instead of raising directive.octicon_unknown; sizes round as sphinx-design rounds them.

👌 Improvements

  • ubc build html, ubc build pdf and ubc build linkcheck are Beta. No behaviour changes: the help, the pages and the feature matrix say so, and what may still change between minor releases is stated on each page (an option or the emitted markup, the typeset output, a verdict class or the report format). Out of scope or not yet: autodoc, Graphviz, numbering of sections, figures and equations; the PDF guide gains a table of which diagrams draw on a default install. See Beta.

  • The Pharaoh Agent panel says what each tool call did: “Ran git status --short”, “Read docs/index.rst”, “Edited requirements.rst and 2 more”, filled in as the agent streams; a repeated call updates its row; an Expand tools toggle beside Auto-approve opens every tool row and is remembered.

  • The documentation, rebuilt around what you want to do. A hub, AI agents, checked by the engine (AI agents, checked by the engine), says what the engine checks and when, and why each stage of an agent’s work is checked before the next; the requirements route follows the lifecycle, define, implement, verify, iterate, prove (Manage requirements and traceability). Four reader paths; ubCode for teams (licensing modes, CI and air-gapped use, every network request ubCode makes); maturity levels with a feature matrix and a maturity strip on each feature page; generated references for every ubc command and option (Command-line reference), every ubproject.toml key, every VS Code command, setting and view (VS Code reference), every MCP tool (MCP tools reference) and every diagnostic code (Diagnostic codes), kept in step with the product by tests; a live Writing reference of every construct ubCode renders, in reStructuredText and MyST, opening with an essentials tour and closing with what is not rendered (Writing content); and new pages on what works without a license, on using ubCode from any editor (Use ubCode from any editor) and on coming from Sphinx-Needs (Coming from Sphinx-Needs). The docs and the Marketplace listing call ubCode an X-as-Code engineering toolchain.

  • A count below its minimum is reported. config.value_below_minimum names the key, the value, the minimum and the effect (needs.id_length = 0 gives every generated id of a type the same id); the value is still used. The published schema carries the minimums, so an editor flags them as you type, declares the "$schema" key, shows each key’s own description on hover, and is a third smaller with its meaning unchanged; the docs recommend a #:schema comment, which taplo and Tombi both read. format_rst.transition_length now has a minimum of 4. See Configuration diagnostics (config and config.*).

  • Derived fields on a page of their own, every example built with ubc, with diagrams of fields per need type and of the order a need’s values are evaluated in (Derived fields); needs.derive_authored names the type entry a rule is written in.

  • Dependencies with security advisories are updated: rustls 0.23.46 (RUSTSEC-2026-0346 to 0349) and the webpki crates (RUSTSEC-2026-0381 to 0383, withdrawing trust in intermediates whose private keys are known), which secure licence activation, remote sources and link checking.

  • Small surfaces. get_graph_schema and query_cypher carry the read-only and idempotent MCP annotations the other tools have; ubc format --preview’s help says the flag is required while the command is in Beta; the internal --license-stage option is hidden from --help and still works; ubc quickstart’s variants and full projects keep their variant data in [variants] and require Sphinx-Needs 9.0 or later.

♻️ Changes

  • A need missing a required field is needs.create_need, as Sphinx-Needs codes it, naming the first such field; needs.build_error keeps every other build failure.

  • Back links’ names follow one rule with Sphinx-Needs: a link type whose <name>_back would be the name of a field or of another link type is refused, with one config warning naming both, and does not exist; a field or link type named links_back or parent_needs_back is refused instead. See the back-links rule.

‼️ Breaking Changes

  • One-time full re-index and re-render on upgrade. The cache format moved (6.240 → 6.256) and the build manifest with it (145 → 147): the first run after upgrading re-indexes every project and ubc build html re-renders every page; nothing is lost and nothing needs doing. The link-check result cache and fetched intersphinx inventories are not discarded.

  • New warnings can fire on an unchanged project, and fail the default gate of ubc check and the builders until ignored or fixed: config.value_below_minimum (a count below its minimum, format_rst.transition_length = 3 among them); config.need_type (an unknown key or a duplicated directive in [[needs.types]], dropped silently before); config.field_narrowing (a [needs.fields.<core>] entry that widens the core field, which is reported and no longer applied); needs.link_target_type where a target_types key was carried and ignored; a config warning for a default or predicates set both on an entry and under [needs.global_options]; and std.any for an unresolved bdg-ref target. See Configuration diagnostics (config and config.*).

  • needs.build_error is needs.create_need for a need missing a required field: a [lint] ignore = ["needs.build_error"] no longer covers it.

  • [needs.global_options] no longer wins where an entry sets the same default or predicates: needs take the entry’s value (see Fixes).

  • .. a:: is a directive. A reStructuredText directive whose name begins with a one-character segment was read as a comment; it is a directive now, so a one-letter need type creates its need and an unknown one is reported; .. *x:: is a comment, as in docutils.

  • The MCP server holds up to two projects in memory by default, about 0.8 GB with one 200k-need project resident; --projects-in-memory 0 restores per-call loading. A remote source that failed to download is retried after five minutes rather than on every call.

  • Answers change on an unchanged project: a filter with a field on both sides of in used to be refused and is now evaluated, so a needs.filter_invalid, needs.extend_filter or needs.dynamic_functions finding disappears and the rows, modifications or links it stood for appear, and a <<[a in b]: x, y>> variant value is picked per need where it silently gave the fallback; a schema rule’s required, minLength or pattern on content now sees the need’s body (see Fixes); a report template that used need_url or theme_style as a variable of its own now gets the function; and a Cypher query with a variable-length step and no ORDER BY may return its rows in another order, so a LIMIT without one may return other rows of the same result.

  • An unchanged project’s PDF comes out differently (beta): maths is typeset, lines break as a browser breaks them, scripts, footnote marks, list markers and table cells move, right-to-left paragraphs take the site’s direction, and badges render as pills. build.pdf_math_unrendered names the reason per formula and no longer counts among the unrendered constructs; a lint.message-ignores rule written on its old sentence stops matching.

  • Lockstep with Sphinx-Needs 9.1.0: a project adopting need types as tables, inherits, fields or links on a type, target_types, union or join needs Sphinx-Needs 9.1.0 on the Sphinx side.

  • The diagnostics gate is on by default: in a project with a [workflow] section, a warning-or-worse finding on a workflow need now holds its stage, so ubc agent gaps and ubc agent release-check fail until the finding is fixed, allowed under [workflow.diagnostics] allow or ignored under [lint]; no release-check baseline accepts it. severity = "none" and schema_severity = "none" switch the gate off, for one run as two -c options. Run ubc agent update so the installed skills fix a finding before anything else.

  • Commit [workflow.diagnostics] or [workflow.change_request] only once every ubc reading the project is on 0.38.0: ubc 0.37.0 and earlier refuse the whole configuration over a [workflow] key they do not know, so ubc check, ubc build and the language server stop on it. A project onboarded for change requests needs 0.38.0 on every machine, in CI and in the VS Code extension. See Setting up a project.

🐛 Fixes

  • An entry’s own default or predicates win over [needs.global_options], where the deprecated table silently replaced them; the global value is reported as ignored once per field or link type, and a global default no longer drops a field’s own predicates. See Global options.

  • A widening redeclaration of a core field is not applied: config.field_narrowing reports it and the core schema stands.

  • A schema rule on content sees the need’s body: required: ["content"] fired on every need, body or not, and minLength or pattern on content never saw it; the body is now a member of the validated object, present when it is not empty, as in Sphinx-Needs. A schema message quoting a string longer than 120 characters now quotes its first 120, an ellipsis and its length.

  • EXISTS { MATCH … } and COUNT { MATCH … } without RETURN: a full subquery inside the braces may omit its RETURN, as in Neo4j 5; it used to fail with query must end with RETURN.

  • ubc build pdf: a MyST pipe-table cell sets its inlines on one line, as the site does; an inline <br> is a hard line break where it was dropped; an :index: role no longer prints a placeholder chip; and a listing whose text ends in a newline no longer draws one blank row too many, numbered under :linenos:.

  • MCP: ubCode’s tools are listed after a client’s server/discover probe. A client that opens with that probe before initialize, such as terminal Claude Code, showed the server connected with no tools; the session now falls back to initialize and lists all six (rmcp 3.5.1).

  • Shared agent assets another project put there can be taken over: in a repository holding several ubCode projects, ubc agent install reports the asset files that differ from this release as conflicts, and VS Code offers to replace them with ubc agent update --overwrite or keep them, instead of looping on Drive.

  • Cypher orders an integer and a float by their exact values, so ORDER BY, DISTINCT, grouping and min / max agree beyond 2^53; a SKIP or LIMIT that draws rand() is evaluated once.

  • A narrow table cell holding several emoji sequences no longer aborts ubc build pdf.

  • Octicon sizes round as sphinx-design rounds them: 1.0625em is 1.062em.