Tracing source code with Codelinks¶
VS Code CLI MCP Beta
Added in version 0.29.0.
This guide walks you through embedding traceability markers in source code and bringing those markers into your documentation as Sphinx-Needs objects. For the full configuration reference, see Codelinks (source code tracing).
What is source code tracing?¶
In a Docs-as-Code workflow,
requirements, specifications, and test cases usually live in reStructuredText files.
But the code that implements those requirements lives somewhere else —
in .py, .cpp, .rs, or other source files.
When you need to prove that a requirement is covered by code
(or that a piece of code relates to a specific requirement),
you have to maintain that mapping by hand.
Source code tracing solves this by letting you embed traceability markers directly in code comments. ubCode’s Rust engine scans those comments, extracts the markers, and converts them into proper need objects — complete with IDs, types, links, and metadata. You get a live, bidirectional link between your documentation and your code without any manual bookkeeping.
Marker types¶
Codelinks supports three kinds of markers. Each one serves a different purpose.
One-line need definitions¶
A one-line marker creates a new need from a single comment line.
Fields are positional, separated by the field_split_char (comma by default).
# @Function Bar, IMPL_4, impl, [SPEC_1]
.. impl:: Function Bar
:id: IMPL_4
:links: SPEC_1
With the default configuration this produces a need with:
Title:
Function BarID:
IMPL_4Type:
implLinks:
SPEC_1
The field order, separator, and start/end sequences are all configurable
via [codelinks.projects.<name>.analyse.oneline_comment_style]
— see analyse.oneline_comment_style for the full reference.
Defaults and optional fields:
Fields with a default value can be omitted from the right-hand side.
For example, if type defaults to "impl" and links defaults to [],
then:
# @Function Bar, IMPL_4
is enough to create the same impl need, just without links.
Escaping:
If a field value contains the split character (,) or square brackets,
escape them with a backslash:
# @Title\, with comma, IMPL_5, impl, []
This produces a need titled Title, with comma.
Need ID references¶
A need-ID reference links a code location to existing needs without creating new ones. It records that this spot in the code is related to the listed requirements.
// @need-ids: REQ_001, REQ_002
The code location is linked to needs REQ_001 and REQ_002.
No new needs are created.
The marker keyword (@need-ids: by default)
is configurable in [codelinks.projects.<name>.analyse.need_id_refs]
— see analyse.need_id_refs.
The resolved code URL is written onto each referenced need
under the field named by ref_url_field (default code_url)
— see ref_url_field.
Supported languages¶
Codelinks recognises the comment syntax
of several programming languages.
Set the comment_type in
[codelinks.projects.<name>.source_discover]
to tell ubCode which language to expect.
Language |
Value |
Comment syntax |
Discovered file extensions |
|---|---|---|---|
C / C++ |
|
|
|
Python |
|
|
|
Rust |
|
|
|
C# |
|
|
|
YAML |
|
|
|
Go |
|
|
|
Bash / Shell |
|
|
|
TypeScript / JavaScript |
|
|
|
Note
For TypeScript / JavaScript, the default one-line start_sequence = "@" is JSDoc’s own sigil
(@param, @returns, @deprecated): a JSDoc tag line the one-line parser would read as a marker
or warn about (@param {number} b First, second — a comma, say) is a jsdoc_tag warning rather than a need,
and one it would ignore (@param a the augend) stays silent.
Set a start_sequence JSDoc does not use, such as "@@" or "[["
(not "@need": with get_multiline_needs on, that is the multi-line open word,
and the configuration is refused).
Under "[[", a JSDoc line naming ECMAScript’s internal-slot notation ([[Prototype]]) with a comma
is read as a marker; for C/C++, "@@" is the choice, since commented-out attribute syntax
(// [[nodiscard]] int add(int a, int b);) would be too.
Discovery also excludes common generated-output and dependency directories
(node_modules, dist, build, …) by default for this language — see source_discover.
The .. src-trace:: directive¶
Once a codelinks project is configured,
include the traced needs in your documentation
using the .. src-trace:: directive:
.. src-trace::
:project: my_app
This inserts all needs discovered by the my_app codelinks project.
Scope the results to a single file or a subdirectory:
.. src-trace::
:project: my_app
:file: main.cpp
.. src-trace::
:project: my_app
:directory: core/
Multiple .. src-trace:: directives can appear in the same document
to combine needs from different projects or scopes.
Note
The :file: and :directory: options are mutually exclusive —
use one or the other, not both.
What the directive renders¶
The directive draws one need card per traced need, at its own position,
in ascending id order.
A one-line need has no body — a one-line marker carries none —
so its card is its type, its title and its id in the header,
plus a metadata row for every other field the marker set
(a :links: marker field shows as that link type’s own row, links
outgoing, not under the field name)
and for the source URLs if you enabled them.
Traced needs are ordinary needs from that point on:
:need:`IMPL_HELLO`links to the card, on whichever page the.. src-trace::that traced it sits.A traced need appears as a link in other needs’
links incomingandlinks outgoingrows, like any need you wrote by hand.The cards are indexed for search, because a traced need’s card at its trace site is the only card the search index records for it. (A
needextractcan card it too, but an extract card carries no anchor of its own, so it is deliberately left out of the index.)
Three situations render the container with no cards inside it:
A scope that could not be read — a
:file:naming a file that is not there, for instance. The read failure is reported as aneeds.src_tracediagnostic, so the empty container is not the only thing you have to go on.A scope whose needs are all claimed by another site. If two
.. src-trace::directives cover the same marker, only one of them draws its card — the one on the earliest page by path, and earliest on that page — because publishing the same need id twice in one site would break the page’s own anchors. The duplicate is reported asneeds.duplicate, naming both directives.A need of the same id written, imported or declared external elsewhere. A hand-written, imported or external need always wins the id over a traced one, so the traced registration is demoted and draws no card. A site all of whose markers collide this way renders empty, and the collisions are reported as
needs.duplicate.
In the editor preview, a site shows at most needs.import_preview_max cards
(50 by default) and says so when it truncates,
so a directive tracing a large source tree stays responsive while you type.
ubc build html always renders every card.
Multi-line needs¶
Added in version 0.37.0.
With get_multiline_needs on (analyse),
a need can be written across the lines of a comment,
and the directive draws its card with its body, among the one-line needs.
A traced file such as
// @Login handler, IMPL_LOGIN, impl
int login();
/**
* @need req: Login must be rate limited
* :id: REQ_LOGIN_1
* :links: IMPL_LOGIN
* :status: open
*
* After **five** failed attempts the account is locked,
* see :need:`IMPL_LOGIN`.
* @endneed
*/
int rate_limit();
traced with
.. src-trace::
:project: my_app
:file: auth.cpp
draws two cards: IMPL_LOGIN, and REQ_LOGIN_1 with its status and links rows
and the body below them — “five” in bold, the :need: a link to IMPL_LOGIN’s card.
The options are a need directive’s. A key is read case-insensitively (
:Status:isstatus), links and tags are split, a typed field is converted, and a block without:id:gets the id a.. req::with that title would get — or, with no title, the one hashed from its body, as any untitled need’s is. An empty:id:refuses the block;delete,title_from_content,docname,lineno,partsandarchare warned about and ignored, the need kept; a key given twice (in any case) keeps its first value. The title is the open line’s, as a one-line need’s is:title_from_contentdoes not apply to a block.The body is parsed in the block’s markup, whatever the page’s:
@need[md]routes it to your Markdown parser and the default to reStructuredText, through[parse.parsers]as a file of themarkupssuffix would be. A markup no configured parser claims is parsed as the page’s markup, with one warning per markup per directive (also when no[parse.parsers]is configured and the markup is not reStructuredText). A heading in a body follows a need directive’s body: a reStructuredText section title is reported (block.title_disallowed) and left out, and a Markdown##heading is shown as a rubric.The need is located at the directive, as a one-line need is, and its
doctypeis the block’s markup. Itslocal-url/remote-urlname the block’s open line.A body is restricted: it cannot read files, its images and other assets are not resolved, a need directive (or a
needimport,list2need, nestedsrc-trace) in it is reported and not executed, and aneedumlin it is shown as its source rather than drawn. Nor does it see the page’s substitutions orrst_prolog.A label in a body is a target of the page that renders the card. An explicit target (
.. _label:), a:name:on a directive in the body, an inline target and a MyST(label)=register with that page, so a:ref:from the page, from another page or from a MyST page reaches it,objects.invlists it, and go-to-definition jumps to its line in the source file. The page’s own ids come first: a body name the page already holds — the same name, or a name whose anchor is already one of the page’s labels or sections — is reported asstd.duplicate_targetat the body line and does not register, and so is the second of two blocks on one page defining one name: the first in reading order keeps it — thesrc-tracedirectives in document order, the cards by need id within a directive. A body label is a:ref:/{ref}/#labeltarget; a docutils named reference (label_) from the page to it is still reported as unknown. The same name in blocks on two pages is reported at both source lines. A body’s footnotes are numbered on their own, from[1], without moving the page’s. A hidden block’s labels register too (its card is drawn, collapsed).A glossary term, a std object or a citation in a body registers the same way. A term, a
confvalorenvvarand a citation in a body register with the page that renders the card, under the same page-first and first-block rules, so:term:,:confval:,:envvar:and[X]_(and{term},{confval},{ref}in MyST) reach it from any page, andobjects.invlists it. A name the page already holds is reported with its own code (std.duplicate_term,std.duplicate_confvalorstd.duplicate_envvar,std.duplicate_citation), and so is a name whose anchor a page label, section, term or object already takes (std.duplicate_target).optionandprogramare not modelled, in a body or on a page.Every finding is at the source line — an option at its own line, an unknown type at the open line, a refused construct at its body line — with the
src-tracedirective as a second location. A dangling:need:or:ref:inside a body is reported at the block’s open line.A hidden block (
:hide:) draws its card collapsed, as a hidden need does anywhere.
See The .. src-trace:: directive for the full directive reference.
How it works under the hood¶
When ubCode encounters a .. src-trace:: directive, it runs the codelinks pipeline:
Discover — walk the configured
src_dir, applyinginclude/excludepatterns and.gitignorerules, to find source files.Parse — read each file and extract markers (one-line needs, need-ID references) according to the language’s comment syntax.
Cache — store results per file. On subsequent runs, only changed files are re-parsed (based on modification time), so incremental updates are fast.
Attach URLs — if
set_local_urlorset_remote_urlis enabled, compute file URLs using the configured patterns.Convert — transform extracted markers into proper need items and merge them into the needs index.
The entire pipeline runs in Rust and executes in parallel, so even large codebases are traced in milliseconds.
ubCode vs. Sphinx-Codelinks¶
The codelinks marker format was introduced by Sphinx-Codelinks, a Sphinx extension. ubCode reimplements the same analysis pipeline in Rust and integrates it directly into its engine.
Aspect |
Sphinx-Codelinks |
ubCode codelinks |
|---|---|---|
Installation |
|
Built into ubCode (VS Code extension, |
Configuration |
|
|
When it runs |
During |
In real time as you edit (VS Code), on demand (CLI), or via MCP |
Performance |
Python-based, runs during Sphinx build |
Rust-based, incremental, parallel; millisecond-scale updates |
Marker format |
Same (one-line needs, need-ID refs) |
Same (one-line needs, need-ID refs) |
Both can coexist. Use ubCode for fast feedback while editing, and Sphinx-Codelinks for the final Sphinx build. The marker format is identical, so source files do not need any changes.
Getting started¶
Follow these steps to set up codelinks in your project.
Define a codelinks project in your
ubproject.toml:[codelinks.projects.my_app] remote_url_pattern = "https://github.com/org/repo/blob/{commit}/{path}#L{line}"
Configure source discovery so ubCode knows where to look and what language to expect:
[codelinks.projects.my_app.source_discover] src_dir = "../src" comment_type = "python"
Configure the analysis to select the marker types you want:
[codelinks.projects.my_app.analyse] get_oneline_needs = true get_need_id_refs = true
Add markers in your source code:
# @Validate user input, IMPL_AUTH, impl, [REQ_AUTH]Include traced needs in your documentation:
.. src-trace:: :project: my_app
Enable remote URLs (optional) so each traced need links back to GitHub/GitLab:
[codelinks] set_remote_url = true
See Codelinks (source code tracing) for the full configuration reference and Complete example for a complete example.
Real-time diagnostics¶
Added in version 0.30.0.
When you open or edit a source file that belongs to a codelinks project,
ubCode validates one-line markers
(and, with get_multiline_needs on, multi-line needs) in real time
and reports problems as warnings directly in the editor.
Diagnostics appear inline and in the Problems panel,
just like RST lint rules.
The following warning codes are reported:
Code |
Description |
|---|---|
|
The marker has fewer fields than the configured field layout requires. |
|
The marker has more fields than expected. |
|
A list field (e.g. links) is missing its surrounding |
|
A list field has brackets on only one side. |
|
A field value contains a line break, which is not allowed in one-line markers. |
|
In a Python docstring, the start sequence is directly followed by a docstring tag
in the Epydoc field shape — |
|
In a TypeScript or JavaScript JSDoc comment (opened by exactly |
|
A multi-line need’s open line does not match |
|
The comment ends before a close line; the block is skipped. |
|
The open line names a markup that |
|
The open and close words are on one line (a one-line need is written with the one-line marker); the block is skipped. |
|
An option key repeats an earlier one, or names a key the need sets itself
( |
|
The open word starts a line of a block’s body; it stays body text (one need per block). |
All diagnostics use the source label ubcode-codelinks.
A build (ubc build html, ubc check) reports the same warnings for the files a src-trace reads,
at their source file and line with the directive as the second location,
each under codelinks.<code> (codelinks.too_many_fields, codelinks.multiline_need_unterminated, …).
A multi-line need’s own findings keep their own codes:
block.directive_unknown_option (an option a need does not take),
directive.invalid_option_value (an empty :id:),
codelinks.multiline_need_duplicate_option (a key repeated in another case),
needs.unknown_type, needs.build_error (a value that does not convert),
needs.create_need (a field its type requires has no value),
needs.import.denied_ingest and needs.import.nested_generative (a refused construct in a body),
codelinks.multiline_need_unclaimed_doctype and codelinks.multiline_need_ambiguous_doctype
(a markup no parser, or two parsers, claim),
and needs.link_ref / std.ref for a dangling reference in a body.
A label in a body that the page already holds is std.duplicate_target,
at the body line with the directive beside it,
reported with the project’s diagnostics (ubc check, ubc build) rather than on the file open in the editor:
Duplicate local target name "<label>", same as on line <N> of <file> when the page (or an earlier block) has the name,
and Target "<label>" would take the anchor "<anchor>", which <file> already uses when the page already uses its anchor.
needs.src_trace, at the directive, keeps what none of these codes covers:
a src-trace whose files could not be discovered or read,
a one-line marker that does not become a need (an unknown type, a field that cannot be set) or whose predicates warn,
and a codelinks project whose configuration does not validate.
lint.ignore = ["needs.src_trace"] silences none of the codes above — list the ones you mean.
Note
Diagnostics require the VS Code extension’s document selector
to include the language of the source file.
The default selector covers all languages listed in
Supported languages above.
If you have overridden ubcode.server.documentSelector in your settings,
make sure the relevant language IDs are included.
Source code outside the docs folder¶
In most projects, source code lives in a different directory tree
than the documentation.
ubCode normally discovers configuration by walking up parent directories
until it finds a ubproject.toml.
For files outside that tree (such as ../src/main.py),
the walk fails because no ubproject.toml exists above them.
To handle this, place a ubproject.redirect.toml in the source directory:
# src/ubproject.redirect.toml
path = "../docs"
This tells ubCode to look for the configuration in the docs/ directory.
See Configuration redirect with ubproject.redirect.toml for details.
Further reading¶
Codelinks (source code tracing) — the full
[codelinks]configuration reference.Complete example — a complete, annotated
ubproject.tomlexample.Key concepts — source code tracing concepts alongside needs, links, and RST.
Codelinks documentation — the standalone Sphinx-Codelinks reference, covering the Sphinx extension, CLI, and advanced topics.