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 itstitleorprefixis left out, a non-stringcolororstyletakes its default, an unknown key is ignored and a directive given twice uses its last entry, each aconfig.need_typewarning. An entry namingreq,spec,implortestwith neithertitlenorprefixis that built-in type, narrowed. See The table shape.A type declares its fields and link types. A
[needs.types.<type>]entry may carryfieldsandlinks, narrowing a declared field for that type’s needs (a stricterschema,nullable = false, its owndefault,predicatesorderive), andinheritsanother type’s declarations; defaults, required fields, validation,needextenddeletes and the editor’s checks and completions follow the need’s type. Afieldsorlinksentry 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 holdsnull([]for a link type), a value set there isneeds.undeclared_for_type,needs.jsonsaysDeclared by need types …, and Cypher’skeys(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 areqor a type inheriting from it; every other target is reported asneeds.link_target_type(needs.external_link_target_typefor a need fromexternal_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 everyneedextend, with its own back links; dead links and link conditions are reported at the member. It is the declarative form ofcopy = 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 alinksorcopyrule takes as candidates the needs whosefieldequals one of this need’sequalsvalues, 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_schemaandquery_cypherstart 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(default2;0reloads on every call) and--unload-after-idle-minutes N(default10) 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: trueon either tool answers from the index as it stands, carrying anindexstamp 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_schemaandubc query cypher --schemagainlabel_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:,needextendandlinks_from_filterand 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 … LIMITkeeps only the rows the limit needs and stops a scan ordered by id early;count(*)over a pattern counts without building rows; deepSKIPpages and filtered pages ordered by id stop at the page; grouping byn.typeand a single-hopcount { … }orexists { … }come off tables;length(p)of the first path keeps the early stop;DISTINCTaggregates 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 makesubc build htmlof 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 queryreturns 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 atc. 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},amsmathenvironments,: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, withbuild.pdf_math_unrenderednaming 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_requestneed 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.
assesshas 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, whichexecutereads before it edits that need (the Pharaoh Changes view’s Assess prompt asks for it too);approve --byrefuses until the rulings cover the reach (approval before the work, as Automotive SPICE SUP.10 asks);executeleaves the work as uncommitted edits;close --by(--dry-runto only report) checks that what was removed is gone.reassess,remove,showandlistcomplete the verbs;assess --print,execute --print --step Nandexecute --done --byserve an agent already in the session. See the verbs and the command reference.Unfinished work is a gap. An
approvedorexecutedchange request is achange_incompletegap until it is closed. A gap on a need of a type no stage produces, a change request’s among them, now makesubc agent nextanswerreason: "gaps"and list it inunattributed_gaps, where it answereddone. VS Code’s Pharaoh Changes view leads with the step each change request waits on, through the language server’sagent.changeList,agent.changeShow,agent.changeCloseandagent.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 checkreports 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 whoseroute.pathholds its file. The stage staysready,ubc agent promptputs aFIX (diagnostics):order first,ubc agent gapsandrelease-checkfail, and norelease-checkbaseline accepts a finding.Configured in
[workflow.diagnostics]:severityandschema_severity(both"warning"by default,"none"switches a bar off), andallowanddenylists of codes orfamily.*; schema-network findings hold a stage only whendenylists them. Onlyubc agentvalidates[workflow]now, so a key it does not know fails those commands alone, and a-con 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.j2template, 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 asqueries.<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 whenubproject.tomlor a.cypheror.j2file is saved. Two functions join every report template, insights or not:need_url(id)andtheme_style(). See Insights.
✨ Writing¶
sphinx-design badges. All 69
bdg,bdg-linkandbdg-refroles, 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 isstd.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 asappsordiffrenders instead of raisingdirective.octicon_unknown; sizes round as sphinx-design rounds them.
👌 Improvements¶
ubc build html,ubc build pdfandubc build linkcheckare 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”, “Readdocs/index.rst”, “Editedrequirements.rstand 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
ubccommand and option (Command-line reference), everyubproject.tomlkey, 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_minimumnames the key, the value, the minimum and the effect (needs.id_length = 0gives 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#:schemacomment, which taplo and Tombi both read.format_rst.transition_lengthnow 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_authorednames the type entry a rule is written in.Dependencies with security advisories are updated:
rustls0.23.46 (RUSTSEC-2026-0346 to 0349) and thewebpkicrates (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_schemaandquery_cyphercarry 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-stageoption is hidden from--helpand still works;ubc quickstart’svariantsandfullprojects 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_errorkeeps every other build failure.Back links’ names follow one rule with Sphinx-Needs: a link type whose
<name>_backwould be the name of a field or of another link type is refused, with oneconfigwarning naming both, and does not exist; a field or link type namedlinks_backorparent_needs_backis 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 htmlre-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 checkand the builders until ignored or fixed:config.value_below_minimum(a count below its minimum,format_rst.transition_length = 3among 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_typewhere atarget_typeskey was carried and ignored; aconfigwarning for adefaultorpredicatesset both on an entry and under[needs.global_options]; andstd.anyfor an unresolvedbdg-reftarget. See Configuration diagnostics (config and config.*).needs.build_errorisneeds.create_needfor 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 samedefaultorpredicates: 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 0restores 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
inused to be refused and is now evaluated, so aneeds.filter_invalid,needs.extend_filterorneeds.dynamic_functionsfinding 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’srequired,minLengthorpatternoncontentnow sees the need’s body (see Fixes); a report template that usedneed_urlortheme_styleas a variable of its own now gets the function; and a Cypher query with a variable-length step and noORDER BYmay return its rows in another order, so aLIMITwithout 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_unrenderednames the reason per formula and no longer counts among the unrendered constructs; alint.message-ignoresrule written on its old sentence stops matching.Lockstep with Sphinx-Needs 9.1.0: a project adopting need types as tables,
inherits,fieldsorlinkson a type,target_types,unionorjoinneeds 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, soubc agent gapsandubc agent release-checkfail until the finding is fixed, allowed under[workflow.diagnostics] allowor ignored under[lint]; norelease-checkbaseline accepts it.severity = "none"andschema_severity = "none"switch the gate off, for one run as two-coptions. Runubc agent updateso the installed skills fix a finding before anything else.Commit
[workflow.diagnostics]or[workflow.change_request]only once everyubcreading the project is on 0.38.0:ubc0.37.0 and earlier refuse the whole configuration over a[workflow]key they do not know, soubc check,ubc buildand 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
defaultorpredicateswin 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 globaldefaultno longer drops a field’s ownpredicates. See Global options.A widening redeclaration of a core field is not applied:
config.field_narrowingreports it and the core schema stands.A schema rule on
contentsees the need’s body:required: ["content"]fired on every need, body or not, andminLengthorpatternoncontentnever 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 … }andCOUNT { MATCH … }withoutRETURN: a full subquery inside the braces may omit itsRETURN, as in Neo4j 5; it used to fail withquery 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/discoverprobe. A client that opens with that probe beforeinitialize, such as terminal Claude Code, showed the server connected with no tools; the session now falls back toinitializeand 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 installreports the asset files that differ from this release asconflicts, and VS Code offers to replace them withubc agent update --overwriteor keep them, instead of looping on Drive.Cypher orders an integer and a float by their exact values, so
ORDER BY,DISTINCT, grouping andmin/maxagree beyond 2^53; aSKIPorLIMITthat drawsrand()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.0625emis1.062em.