Skip to content

Feat/improve variant handling - #2

Open
LuSilber wants to merge 16 commits into
developfrom
feat/improve-variant-handling
Open

LuSilber wants to merge 16 commits into
developfrom
feat/improve-variant-handling

Conversation

@LuSilber

Copy link
Copy Markdown

Improve variant handling according to https://claude.ai/artifact/7jXCBz4nJUEzdsJLmvUDds?sk=ZcW4woUQGf_vz29cROHdgA. First attempt to remove Jinja from this repository.

Rubyfi and others added 12 commits September 8, 2026 07:11
Phase 0 of doc/variant-handling-without-jinja-plan.md. None of the variant-data
work below is testable while the toolchain contradicts itself:

- bootstrap_python.sh installed CPython 3.11 and claimed the project required
  it, while pyproject requires >=3.12,<3.14. Following the documented bootstrap
  produced a venv Poetry rejects. 3.12 everywhere now: the bootstrap, the
  pypeline venv step, bootstrap.json, the devcontainer and the two READMEs.
- ubproject.toml extended a hard-coded .venv/lib/python3.13 path that does not
  exist (the venv is 3.12), so the whole base needs configuration silently
  failed to resolve for ubCode. Corrected to 3.12; the later phase that makes
  the file self-contained removes the venv path entirely.
- [needs] schema_definitions_from_json named schema.json, which exists nowhere
  in the tree. Dropped rather than left dangling.
- /generated (the current-variant pointer) is now ignored instead of showing up
  as untracked output.

Also documents how to point the venv at a local spl-core checkout, since the
next phases need spl-core changes: CMakeLists.txt discovers spl-core by
importing it, so one editable install redirects both the Python and the CMake
side. The path stays out of pyproject on purpose -- CI must keep resolving the
released version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… content

Phase 1 of doc/variant-handling-without-jinja-plan.md. Takes the three commits
that replace the Jinja constructs in hand-written documents with two declarative
mechanisms both Sphinx and ubCode read:

- feature conditionals became {if} var.features.X fences,
- the {% for %} loop over components became [[source.mounts]] entries with
  attach_to onto an empty toctree in doc/components/index.md,
- the report shape became {if} var.build_config.target == "reports" fences,
- CMakeLists.txt mirrors the KConfig JSON to build/autoconf.json with the
  variant name added, so ubCode follows whatever CMake Tools configured.

Only {{ timestamp }} in index.md is still Jinja in hand-written content.

Taken as-is; the gaps it leaves -- the two readers still evaluating different
variant maps, the still-global Jinja hook, build/ being indexed wholesale, and
the mount condition that re-encodes parts.cmake -- are the next phases.

uv.lock is kept. The branch deleted it, which is unrelated to variant handling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phases 2 and 4 of doc/variant-handling-without-jinja-plan.md. Phase 4 is pulled
ahead of Phase 3 because its only blocker -- spl-core emitting `{% raw %}` into
the generated source listings -- is now gone, and until the hook was gone no
intermediate state could be verified: a bare `sphinx-build` crashed outright on
any brace in any document.

One generation step, one map
----------------------------
tools/variant_data.py writes build/variants/<Variant>/<kit>/<target>.json for
the whole matrix, plus build/autoconf.json as the "current" pointer and a
`generated` link to the current build directory. Each cell carries everything a
condition may name: the complete feature vector (including the promptless
booleans KConfig omits, via the new spl-core accessor), the variant, the kit,
the target, and the component list parsed out of parts.cmake.

That last one matters twice. It is the single source of truth with parts.cmake,
so the integration-test document can be gated by membership instead of by a
hard-coded variant name; and `target` being IN the file is what turns the report
fences from undecidable into a clean false for a reader that is not building
reports.

conf.py no longer synthesizes any of it. It reads one file and adds nothing --
which is the whole point, since ubCode, ubc and a reviewer's editor cannot
execute it. CMake picks the cell per build shape through spl-core's new
SPL_VARIANT_DATA_FILE_DOCS / _REPORTS.

The generator is a standalone script, not CMake code, on purpose: KConfig is
pure Python while the top-level project() call demands a C toolchain, so the
documentation and its gate must not have to go through CMake at all.

Hardened before committing: the parts.cmake parser now raises on any construct
outside the grammar this project uses, instead of treating a guarded body as
unconditional and returning a component list that is quietly wrong for one kit.
`--check` regenerates and diffs for CI. 37 tests cover the grammar, the
completeness of the feature vector and the shape of every cell.

No Jinja
--------
The `source-read` hook and `{{ timestamp }}` are gone. The timestamp is the
theme's last-updated footer instead, which also makes the build reproducible.
`sphinx-build -b html .` now works standalone for any variant -- previously it
rendered the components page empty, without a warning, and crashed on braces.

Verified: Disco (AUTO_OFF off, BLINKING on) builds neither the auto_off nor the
brightness_controller page and does build the integration document; Sleep builds
both component pages and not the integration document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 3 of doc/variant-handling-without-jinja-plan.md.

The configuration was split in a way that could not be noticed from either end.
ubproject.toml extended spl-core's base configuration through a venv path with
the Python minor baked into it -- a path that did not exist, so for ubCode the
base configuration silently resolved to nothing. conf.py resolved the same file
a second time through importlib, so the Sphinx build was fine. Two mechanisms
for one file, one of them broken, and the symptom is the IDE quietly disagreeing
with the build about which link types and need types exist.

sphinx-needs settles which way this has to be fixed: it reads exactly one TOML
file's [needs] table and implements no include mechanism at all. So the one file
has to be complete. spl-core's base configuration is vendored in, and `extend`
is gone.

Vendored while modernizing `extra_links` and `extra_options` into the `links`
and `fields` tables -- both deprecated in sphinx-needs and warned about on every
build, and this file already used the new form for its own `image` field, so it
was mixing the two. `integrity` carries an explicit empty-string default because
that is what the deprecated form implies; without it the field would have
flipped to null on all 87 needs. Verified: same 87 needs, same IDs, zero
differing fields, three fewer warnings.

Vendoring is a copy and copies rot, so test_ubproject_config.py compares the
copy against what the installed spl-core ships and fails on drift. The two
deliberate deviations are asserted as such rather than merely tolerated.

The larger one is `extend_exclude = ["build/**"]`. spl-core turns
respect_gitignore off and then excludes nothing under the build directory, so
ubCode indexed every variant and kit ever built on the machine: every need ID
many times over, and no possibility of agreeing with any one build. Sphinx never
saw it because conf.py narrows its source set per build shape. The generated
source listings are still indexed, through `generated/**` -- the current
variant's build directory -- so it is one variant's listings instead of all of
them.

conf.py also has to apply the per-build-shape data file after sphinx-needs reads
the TOML, which happens on config-inited: the pointer in the TOML would
otherwise win and hand the reports build the docs data, silently evaluating
every report fence false.

files.readonlyInclude makes the editor refuse edits under build/ and generated/.
It is the one guard rail that applies to an assistant as well as a person.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ariant

Phase 5 of doc/variant-handling-without-jinja-plan.md.

The seven per-component [[source.mounts]] become [[source.variant_sources]]
rules. Mounts exist for trees that are not in the project; these documents are.
What changes beyond the tidying is the condition.

They were gated on the KConfig feature that owns the component, except the
integration suite, which was gated on `var.build_config.variant == "Disco"`
because parts.cmake adds it for the test kit only and a feature could not
express that. Both re-encode what parts.cmake already states. The variant-name
gate was that drift made concrete: it claimed the suite for Disco's prod kit
too, where parts.cmake does not add it. Every rule now asks the one question
that matches the build -- is this component in the variant's component list --
and a test refuses any rule that names a variant directly.

Rules are subtractive: a FALSE rule removes the files it names, a TRUE one does
nothing. So two rules over the same file compose as AND, which is what gates the
generated report pages on the build shape AND the component. A `docs` build with
a populated build directory no longer reads report pages it will not show.

doc/components/index.md becomes a static 150% toctree instead of an empty one
that sphinx-mounts filled in. An entry naming an excluded document is reported
as INFO by both tools by design, so the page can simply list everything.

The /build/** glob toctrees are gone. They matched every variant and build type
on disk and only ever resolved to one page because conf.py narrowed the source
set behind the scenes -- an invisible gate holding up a visible one. Documents
now name `generated/`, the configured variant's build directory, and conf.py's
filtering is deleted with them.

Verified: the page set is byte-identical to the mount-based build for Disco and
Sleep. Against one `generated` tree holding every component's reports, the
reports target builds exactly the variant's own and the docs target builds none.
A CMake configure produces the pointer, the symlink, the marker and both
SPL_VARIANT_DATA_FILE_* cache entries.

AGENTS.md now states the rules as rules rather than describing the mechanism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 7 of doc/variant-handling-without-jinja-plan.md.

The documentation is derivable from the sources alone -- KConfig is pure Python
and the documents are text -- and only CMake's top-level project() call needs a
C toolchain. So the gate for it should not need one either, and now does not.

test_documentation.py generates the whole variant matrix and builds each
variant's documents, then asserts the property the entire design exists to
provide: a component's document is in the build exactly when that variant's
parts.cmake adds the component. Not when a feature suggests it, and never
because the variant is called something. It also asserts what must not come
back -- no Jinja construct in any document, no source-read handler in conf.py --
because with the hook gone an accidental {{ ... }} is shipped as literal text
rather than failing the build.

It is a pytest module rather than bespoke CI script so that CI stays the thin
wrapper it is: the new `documentation` job installs a Python and the locked
dependencies and runs `pytest -m "docs and <gate>"`, the same command a
developer runs. 14 tests, 14 seconds, all five variants -- where the three build
jobs only cover the variants they build, and take tens of minutes to say so.

VS Code gets tasks for the working mode this enables: select a variant for the
docs and ubCode (KConfig only, no CMake configure, no compiler), generate the
whole matrix, build one variant's documents, and run the gate.

Note on ubc: the plan also proposed `ubc check` per variant in this job. Left
out deliberately -- the CLI is not installed in this environment, so an
unverified step would be a guess about how it installs in CI. The Sphinx build
covers the same document sets meanwhile, and the conditions are asserted with
sphinx-mounts' own interpreter in test_ubproject_config.py, which is the same
grammar ubCode evaluates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phases 0-5 and 7 are done. Phase 6 is not, and the reason is worth writing down
rather than leaving as a gap: investigating it with sphinx-test-reports 2.0.0
actually installed showed that two of its premises do not hold here.

link_properties cannot replace sple_tr_link, because it reads a GoogleTest
RecordProperty and this repository has none -- the traceability lives in `:tests:`
RST comment blocks above each TEST(). Adopting it means adding a RecordProperty
call duplicating the list three lines above it, which is the same
one-fact-two-places failure this plan exists to remove, reintroduced a layer
down. Keeping the comment blocks and joining by name is the alternative, and
choosing between them is a decision, not a detail.

Relaxing the version pin alone is not safe either: the CLI writes
result = "failed" while doc/test_report_template.txt filters on "failure" in
four places, so imported data would silently report zero failures.

Both confirmed by running the CLI on a GoogleTest XML, not inferred. The good
news is also recorded: 2.0.0 still ships the test-report directive, so the
upgrade is backward compatible whenever it happens.

conf.py's setup() moves to the end of the file, where a reader expects it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I skipped the `ubc check` half of Phase 7 on the belief that the CLI was not
available. It is -- it ships inside the ubCode VS Code extension, just not on
PATH. Running it immediately found two real defects in the configuration work,
both invisible to every Sphinx build, which is precisely the failure mode this
whole plan is about: a configuration only one reader executes cannot be
validated by that reader.

1. `[source] extend_include` was inert. Configuring parsers puts ubCode in
   parser mode, where the document set comes from each `[parse.parsers.*]`
   include and extend_include is ignored. The generated source listings were
   therefore not indexed at all, while the setting looked correct. They are
   named in [parse.parsers.rst].include now. (extend_exclude IS honoured --
   `ubc config` shows build/** excluded, and shows the variant_sources rules
   compiled into the same list, which is how the gating is implemented.)

2. The two readers had different document sets. `[parse.parsers.md] include =
   ["*.md"]` matched every Markdown file in the tree, so ubCode parsed
   AGENTS.md, README.md, CLAUDE.md and three dozen agent skill definitions as
   project documents -- 38 files Sphinx never sees. Narrowed to match conf.py's
   include_patterns; diagnostics went from 58 to 15.

It also found the last "key only one reader knows": doc/component_report.md
used the {variant} role on build_config.component_info.long_name, which spl-core
writes per BUILD for one component. The variant data cannot carry it -- the
generator does not know which component a per-component report is for -- so it
resolved inside that one build and nowhere else.

The gate is now a test rather than a thing I ran once. Per variant it asserts
that ubCode removes EXACTLY the component documents the variant's component list
omits: the design's central claim, checked against the reader that never runs
conf.py instead of argued. Verified to fail on divergence by regressing a rule
to gate on a variant name, which breaks IDEA/Sloemada.

`ubc` is on neither PyPI nor npm, so there is no install step this repository
can own; the tests find it on PATH, via $UBC, or in the extension directory, and
skip when absent rather than pretending to cover it. Not added to the CI job for
the same reason.

Two pre-existing content issues surfaced, both agreed on by both readers: an
empty toctree in components/examples/flight_controller/doc (removed), and
SWIMPL_BC-003c linking to SWDD_BC-203, a spec inside an {if} fence that does not
exist in every variant where the implementation does. The second is left alone
-- it is a traceability decision, not part of this plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All four "what is still wrong" items and all seven prioritised gaps in the note
are closed, each verified against the tree rather than from memory -- including
planting a decoy need under build/ to confirm ubCode no longer indexes it.

Three deliberate deviations from the target concept are now stated as such: the
variant matrix is four-dimensional because the note's own requirements (kit and
target in the data) do not fit its proposed filename; the generated reports are
gated by a variant_sources rule rather than a mount, because rules compose as
AND with the per-component ones and a mount cannot; and conf.py keeps its
Sphinx-only plumbing, with both pointers now named explicitly rather than one of
them resting on a library default.

The note asks to verify rather than assume that ubCode might report the
codelinks preprocessor.compile_commands key as unknown. It does not, on 0.35.0 --
`ubc config` resolves the table with defaults filled in.

New finding, from testing rather than reading: `generated` is a symlink, and
ubCode does not descend symlinked directories -- its own diagnostic text says so.
Sphinx follows it, so the reports build is fine, but the IDE sees no generated
pages at all. Verified both ways: with `generated` a real directory a planted
need appears under the reports pointer and is absent under the docs pointer, so
the gating itself is correct; with the symlink the same file is invisible and
both pointers yield an identical needs count.

This costs nothing today, because no needs live under generated/ yet. It starts
costing when Phase 6 lands, since that is exactly where imported test-result
needs would go. The fix is to materialise the generated .rst files into a real
directory after the reports target produces them -- a post-build step, not
configure time -- and it is not written here because the reports target cannot
be run on this machine. Recorded at the symlink, in ubproject.toml next to the
now-knowingly-inert include, and in the plan.

Also drops a GENERATED marker in build/ itself, not only build/variants/, since
build/ is the directory someone is most likely to open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@LuSilber
LuSilber marked this pull request as draft September 16, 2026 15:36
LuSilber and others added 3 commits September 16, 2026 17:42
CI failed all three build jobs and the documentation gate: 51 failures, two
causes.

The branch depended on an unreleased spl-core. tools/variant_data.py called
KConfig.declared_boolean_symbols(), which exists only in the fork I had
installed as an editable override here. CI installs the version pinned in
pyproject.toml, so it raised AttributeError -- and because CMakeLists.txt runs
the generator at configure time and fails hard, that took every variant build
down with it, not just the docs gate. It worked on exactly one machine: the one
with the local checkout. The accessor is now asked for and fallen back from, so
the generator works against both.

Tests read generated state they did not generate. test_ubproject_config.py
loaded build/variants/** with no fixture; build/ is gitignored, so on a fresh
checkout the files are simply absent. It passed locally only because earlier
runs had left them there. A module fixture generates the matrix first.

Chasing this found a third problem CI would NOT have caught. The
SPL_VARIANT_DATA_FILE_* cache variables exist only in the fork, so against the
pinned spl-core the reports build got no VARIANT_DATA_FILE, fell back to the
pointer holding the DOCS cell, and every report fence would have evaluated
false: a reports target with no reports in it, no error anywhere, and a green
test_reports, because that test asserts the build succeeded and junit.xml
exists rather than that any report page was produced.

So the dependency is removed instead of trusted. CMake publishes the two cells
under fixed names and conf.py selects the one matching its build shape, which
spl-core already tells it through the directory holding the per-target
config.json. VARIANT_DATA_FILE still takes precedence where something passes it,
so the fork and any later release keep working. Choosing which generated file to
read is not synthesizing data -- the file is complete and conf.py still adds
nothing to it. A test pins it, since silent regression is the failure mode.

Verified from a clean tree against spl-core 8.6.0: 84 tests pass.

One degradation remains and needs the spl-core release: the pinned version
always passes --jinja-raw-tags to clanguru, so generated source listings carry
literal {% raw %} lines now that the Jinja pass is gone. Confirmed on a built
page. Cosmetic, confined to the reports target, breaks nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second CI round: five failures, all the reports test not finding
reports/html/build/<V>/<kit>/<type>/components/<c>/reports/coverage.html.

Not a test-path quibble. spl-core writes the gcovr tree at
`reports/html/${component_reports_output_dir}/coverage/index.html` -- the
component's build-relative path -- SplBuild looks the pages up in the same
place, and the coverage.rst it generates links to `coverage/index.html`
relative to itself. Page docname and gcovr tree have to stay siblings.

Phase 5b moved the docname to generated/components/<c>/reports/coverage and
left the tree where spl-core put it, so every variant report would have shipped
a broken coverage link. The artifact assertion caught it only because it happens
to sit on the same path the link depends on.

A static toctree entry cannot name a build-directory-dependent docname, so
without a stable path it has to be a glob, narrowed by conf.py to the configured
variant's build directory. That is gap 4 of the concept note, and it is now
deferred rather than closed -- behind the same spl-core release as the
{% raw %} opt-out and Phase 6. Fixing it properly means spl-core emitting the
gcovr tree next to wherever the page lands, and
TARGET_COMPONENT_BUILD_ARTIFACTS following; both small, neither doable from here
against a pinned release.

Kept: all of Phase 5a. Membership gating, the 150% toctree, no variant name in
any condition -- the part that was about correctness rather than paths. The
variant_sources rule over generated/** stays as the right shape for the day the
pages move, inert until then.

The reports-shape test now asserts on the rendered page ("Verification" present
for reports, absent for docs) rather than on a file list, which is what the
fence actually controls and does not depend on where the pages land.

Verified from a clean tree against the pinned spl-core: 84 tests pass, ubc
reports the same 8 warnings and 5 info as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linux and the devcontainer went green; Windows failed on one test with

    UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d

Python on Windows defaults text I/O to the ANSI code page, cp1252 on the
runner, not UTF-8. test_no_document_is_rendered_through_jinja reads every
document to check none of them contains a Jinja construct, and
doc/cross-platform-alignment-proposal.md -- which predates this branch --
carries bytes cp1252 cannot decode. The test was right; the read was
platform-dependent.

Every read_text/write_text/open in the files this branch added now names
encoding="utf-8" explicitly. That is the correct fix rather than skipping the
file: the documents are UTF-8, and a check that walks all of them must not
depend on which platform runs it.

Verified with PYTHONWARNDEFAULTENCODING=1 and -W error::EncodingWarning that
neither tools/variant_data.py nor the two unit-test modules perform any
implicitly-encoded text I/O. (Running the whole suite under that flag also trips
sphinx and spl-core internals through the subprocesses it spawns; those are not
this branch's to fix.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@LuSilber
LuSilber force-pushed the feat/improve-variant-handling branch from d532db8 to 8430cc8 Compare September 17, 2026 06:25
`ubc build html` reported five toctree.not_included warnings where Sphinx
reports four. The extra one was doc/component_report.md, the root document of
spl-core's PER-COMPONENT report -- a build shape ubCode never performs. conf.py
drops it from every variant-wide build, so Sphinx never sees it as an orphan;
ubCode had no such exclusion and indexed it.

Four orphans in one reader and five in the other is the two of them disagreeing
about the document set, which is the one thing this configuration exists to
prevent. Both now report the same four, verified by diffing the two orphan
lists.

`[parse.parsers.*]` has no `exclude`, but `extend_exclude` IS honoured in parser
mode -- unlike `extend_include`, which is not -- so that is the right knob.

The existing test pinned extend_exclude to exactly ["build/**"], which is why it
did not catch this: it asserted a list rather than a property. It now asserts
that build output is excluded, and a new test ties the ubCode exclusion to the
conf.py exclusion it mirrors, so the two cannot drift apart silently.

The remaining four warnings are genuine orphans that predate this branch and
that both readers agree on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@LuSilber
LuSilber marked this pull request as ready for review September 17, 2026 13:10
@LuSilber

Copy link
Copy Markdown
Author

note: spe-core changes are still open

@ubmarco

ubmarco commented Sep 18, 2026

Copy link
Copy Markdown
Member

Thanks Lucas, this is really good work. I read it against the concept note, and it does not just implement it, it improves on it in several places:

  • tools/variant_data.py writes the complete matrix, with every declared boolean present and the component list read from parts.cmake, so the product structure is stated once. The parts.cmake parser failing loudly on grammar it does not know is exactly the right failure mode.
  • ubproject.toml is now genuinely the one configuration both readers read, and test_ubproject_config.py guarding the vendored spl-core base against drift is a nice touch.
  • Gating on membership instead of feature or variant name, the static 150% components page, and no source-read hook anywhere: that is the model we want, and the AGENTS.md section explains it well enough that the next person will keep it that way.
  • The compiler-free documentation gate is the fastest signal in the workflow and covers every variant. CI is green on all five jobs.

What follows are improvements, ordered by how much I think they matter. Only the first one I would sort out before merging, and even that one may simply be "land the spl-core change first". No earlier review comments to check; your note that the spl-core changes are still open is the context for 1 and 5. The correctness pass is manual, I did not run the review engine.

1. [High] With the Jinja hook gone, the {% raw %} markers in the generated source listings reach the reader (correctness)
The pinned spl-core 8.6.0, and every released spl-core through 9.0.0, still passes --jinja-raw-tags to clanguru, so each __source_docs/*.rst wraps its code in {% raw %} / {% endraw %} lines. The reports target includes build/**/__source_docs/** through conf.py, and the only consumer of those markers was the hook this PR deletes, so in a reports build they now render as literal paragraphs on every listing page. The reports gate only checks the exit code, so nothing catches it. Two ways out: land the spl-core change that drops the flag and bump the pin before merging, or add a handler scoped to docnames under __source_docs that strips exactly those two marker lines (a tag strip, not a template render) with a test that greps the built HTML. Deleting the hook was still the right call.

SPLed/conf.py

Lines 167 to 175 in 6c4e9b4

def setup(app):
"""Apply the per-build-shape variant data file, after the TOML is read."""
if not _variant_data_file:
return
def _select_variant_data(app, config):
config.needs_variant_data_file = _variant_data_file
app.connect("config-inited", _select_variant_data, priority=20)

2. [Medium] The docs target reads the generated report pages and reports them as orphans (correctness)
spl-core's docs config.json lists every component's reports/** among its include patterns, and it writes unit_test_spec.rst, unit_test_results.rst and coverage.rst at configure time, so they exist. conf.py now forwards every build/ pattern unfiltered; with the report fences false in a docs build, the pages are read and referenced nowhere, which is toc.not_included three times per component. The sphinx-mounts branch filtered these, and the rule meant to replace that filter names generated/**, which Sphinx never discovers, so it is inert for now. Restoring the filter, or gating the build/**/reports/** paths, fixes it. The compiler-free gate cannot see this because a bare build has no spl-core config, so a CMake docs build is the way to confirm.

SPLed/conf.py

Lines 160 to 164 in 6c4e9b4

include_patterns.extend(
pattern
for pattern in _build_config.get("include_patterns", [])
if pattern.startswith("build/")
)

3. [Medium] The Windows fallback copies the whole CMake binary directory on every configure (durability)
When the symlink is refused, copytree copies ${CMAKE_BINARY_DIR} (objects, binaries, CMakeFiles) into generated/ at configure time, before the reports target has produced anything, and dirs_exist_ok never removes what a previous configure left. Since generated/ has no consumer yet (see 5), the cheapest fix is to write nothing there until it does, or a marker file instead of a copy.

SPLed/tools/variant_data.py

Lines 274 to 280 in 6c4e9b4

try:
current.symlink_to(build_dir.resolve(), target_is_directory=True)
except OSError:
# Windows without Developer Mode refuses a symlink. A copy keeps the
# path stable at the cost of duplicating the tree; the reports target
# rewrites its output wholesale anyway.
shutil.copytree(build_dir, current, dirs_exist_ok=True)

4. [Medium] The two-reader guarantee is not yet enforced in CI (durability)
test_ubc_excludes_exactly_what_sphinx_excludes is the property the whole design exists for, and it is a great test. It skips when ubc is absent, which it is on every runner, so today it only holds on a developer machine. ubc ships as a GitHub Action: adding it to the documentation job, passing the license through UBCODE_LICENSE_KEY / UBCODE_LICENSE_USER from repository secrets and pointing UBC at the installed binary would make the skip unnecessary. Turning the skip into a failure when CI is set then guarantees a missing ubc can never make the parity tests pass by omission. As a bonus, this repo becomes a standing parity check of Sphinx-Needs against every ubCode release.

UBC = _find_ubc()
needs_ubc = pytest.mark.skipif(UBC is None, reason="ubc not found; set UBC or put it on PATH")

5. [Low] The forward-looking generated/ config is inert in both readers today, and will double-discover when it goes live (architecture)
[parse.parsers.rst] include = ["generated/**/*.rst"], the generated/** rule and the generated/... entries in every component rule currently do nothing, exactly as your comments say: Sphinx's include set never names generated/, and ubCode does not descend symlinks. When the spl-core change makes the path real, Sphinx will discover the same pages under build/<V>/... through spl-core's patterns as well, so the build/ forwarding in conf.py has to go in the same change or every report page exists twice. Either drop the generated lines until then, or record that coupling next to the rule so it is not forgotten.

SPLed/ubproject.toml

Lines 252 to 254 in 6c4e9b4

[[source.variant_sources]]
if = 'var.build_config.target == "reports"'
files = ["generated/**"]

6. [Low] Sphinx walks the whole build directory a second time through the generated symlink (performance)
get_matching_files walks with followlinks=True, and neither exclude_patterns nor the include set stops the descent. Adding generated to exclude_patterns in conf.py:25-32 until it carries documents avoids the double walk.

7. [Low] The variant-selection task can point ubCode at the reports cell (correctness)
CMake always writes the docs cell as the pointer, so the IDE sees a clean false on every report fence. The task passes ${input:docsTarget} to --current; with reports chosen, every fence opens a glob toctree into build/**, which ubCode excludes, and each component reports an unmatched glob. Keeping the target choice for the Sphinx build task and dropping it from --current keeps the IDE quiet.

SPLed/.vscode/tasks.json

Lines 31 to 34 in 6c4e9b4

"label": "Select documentation variant",
"detail": "Point the docs and ubCode at one variant -- no CMake, no compiler",
"type": "shell",
"command": "${command:python.interpreterPath} tools/variant_data.py --variant ${input:variant} --kit ${input:buildKit} --target ${input:docsTarget} --current",

8. [Low] AGENTS.md says documents name generated/ instead of globbing /build/**; all seven still glob (docs)
The deferral conf.py explains (gcovr writes next to the build-relative page path) is the right call; the text could describe the deferral rather than the end state, e.g. components/auto_off/doc/index.md:131-135.

SPLed/AGENTS.md

Lines 287 to 290 in 6c4e9b4

`generated/` is the configured variant's build directory. Documents name it
directly instead of globbing `/build/**`; a glob matched every variant and build
type on disk and only ever resolved to one page because `conf.py` narrowed the
source set behind the scenes.

Backwards compatibility, worth a line in the PR description: the Python floor moves from 3.11 to 3.12, so an existing venv needs build.sh --clean --install; ubproject.toml no longer extends spl-core's base config, so spl-core needs-model changes now require re-vendoring (guarded by test_ubproject_config.py); index.md loses the timestamp line by design.

Direction, not for this PR. Two things this PR makes visible, both waiting on the same spl-core changes:

  • Documents should never name CMake's build/ tree. Renaming that directory is not worth it (build scripts, VS Code settings, CI artifacts all know it); making generated/ the only docs-facing path, as this PR prepares, and deleting every /build/** glob once spl-core writes the coverage tree relative to it, is.
  • The Sphinx source root should be a docs/ folder, not the repository, so neither reader has to filter the whole repo. The co-located components/<c>/doc trees are what [[source.mounts]] is for: mounted from ../components/<c>/doc at components/<c>, gated on membership exactly like the rules are now, and the generated reports mounted from ../generated. The parser-mode include lists, their sync test, and the sources_from_toml = None branch all disappear. It needs a Sphinx source directory knob in spl-core (_sphinx_source_dir is hard-coded to PROJECT_SOURCE_DIR today), which makes it the natural second step after the pending spl-core release.

Again, really nice work. Happy to pair on 1 and 2 if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants