Skip to content

docs: variant-dependent documentation without Jinja, the same in Sphinx and ubCode - #4

Merged
ubmarco merged 63 commits into
developfrom
feat/docs-parity-sweep
Oct 1, 2026
Merged

ubmarco merged 63 commits into
developfrom
feat/docs-parity-sweep

Conversation

@ubmarco

@ubmarco ubmarco commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Variant-dependent documentation without Jinja, read the same way by Sphinx and by ubCode/ubc, for every variant.

This PR targets develop directly and carries the whole line of work: what #2 and #3 started, and this sweep. Those two PRs are not merged separately.

spl-core changes: useblocks/spl-core#5, pinned as a fork commit. clanguru fixes: useblocks/clanguru#1, proposed upstream as cuinixam/clanguru#9, also pinned as a fork commit. Both pins move to PyPI releases once upstream has released them.

What changes

Selecting a variant generates everything the readers need. CMake configure, or tools/variant_data.py on its own, writes these files. Nothing is decided in conf.py or on the command line, and nothing per component is maintained by hand.

File Holds
build/variants/<V>/<kit>/<target>.json the variant data, var.*: every KConfig symbol (promptless booleans included), the variant, kit, target and component list (from parts.cmake). There are cells for per-component reports too.
ubproject.variants.toml the document rules: one per documented component, gated on membership of the variant's component list, plus one gating the selected build's pages on its own variant and kit
build/selection.toml the cell's variant data file and, for a build's reports, that build's generated pages by their own paths
<build>/selection/[<component>/]<shape>.toml the same, for each documentation run spl-core starts in that build

ubCode reaches the selection through extend, Sphinx through conf.py, and spl-core's runs through -D spl_selection=. Adding a component means adding it to parts.cmake and writing its documentation.

No Jinja anywhere.

  • Whole documents are gated by the generated rules.
  • Blocks within a document are gated by Sphinx-Needs' {if} directive.
  • Report sections are gated on the target.
  • The global source-read pass, the Jinja raw tags in the listings and the per-variant JSON copies are all gone.

Generated pages are read where spl-core writes them, under upstream's names. That covers test specifications, test results, coverage and source listings.

  • There's no link, no junction, no copy and no mount.
  • Report toctrees glob /build/**/<component>/reports/..., which resolves to exactly one page because one build is read.
  • Two builds' reports can be built side by side, and ubc diff -c "$(cat <build>/selection/reports.toml)" compares them.

Traceability in both readers:

  • Implementation needs are one-line @need comments in the C sources, shown on each component's page. codelinks takes each variant's #ifdef branches from the selected build's compile database.
  • Test results are needs.json that both readers import. Test specifications link to their results through needextend. sphinx-test-reports and the sple_tr_link function are gone.
  • Per-component reports work in both readers.

One configuration file, ubproject.toml, with spl-core's needs model vendored into it. tools/config_checks.py runs on every Sphinx read and catches drift:

  • from spl-core's model;
  • between the two readers' document sets;
  • from the rule grammar.

A documentation gate in CI (test/test_docs_gate.py). For every variant and kit, and every per-component report, it builds with sphinx-build -W and with ubc and compares the two needs.json files need by need. It does this in two shapes:

  • the documents, which only need a configure;
  • the test kit's reports, after compiling and running the tests.

The only allowed differences are listed in test/docs_exceptions.toml: REQ_37 and REQ_58, which are untitled in the upstream ubConnect export.

Fixes found on the way:

  • SWDD_BC-203 was implemented outside its #ifdef; upstream has the same bug.
  • The CSV requirement images are now in the needs' content.
  • The light controller state machine diagram is fixed.
  • The source listings showed the wrong #ifdef branch and the system headers' functions. That's fixed in clanguru: Spa's report listed TS_BC-001, a manual-brightness test specification, instead of TS_BC-002/TS_BC-003, which Spa actually runs.

For people new to it:

  • VARIANTS.md is a step-by-step guide by use case.
  • tools/try_variants.py is a cross-OS guided tour, with setup, docs, build, component, compare and check cases. Each step says what it does and what to look at afterwards.
  • AGENTS.md describes the design.

Results

Sphinx ubc
Disco, test kit reports 134 needs 132: the two untitled imports apart, 0 field differences
light_controller component report 25 25
Documentation gate, locally 63 of 63

Backwards compatibility

  • Python 3.12 is the floor. Recreate an existing virtualenv.
  • ubproject.toml no longer extends spl-core's base configuration; it is vendored, and a test fails when it drifts.
  • Generated output: build/ and ubproject.variants.toml are generated and opened read-only in VS Code. A generated link from an earlier version of this work is removed on the next selection.
  • The documentation job compiles the test kit for the reports shape: about a minute per variant on a hosted runner.

Not in this PR

  • The Windows job fails while installing the toolchain, in scoop/pypeline. develop's nightly run fails the same way, so it doesn't come from this branch.
  • REQ_37 and REQ_58 need titles in the ubConnect export.

cuinixam and others added 30 commits August 17, 2026 08:59
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>
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>
`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>
Review finding 2. spl-core writes unit_test_spec.rst, unit_test_results.rst and
coverage.rst per component at configure time and lists them among its include
patterns for BOTH build shapes. conf.py forwarded every build/ pattern
unfiltered, so a docs build read all three per component -- and referenced none
of them, because the fences that would have linked them are false in a docs
build. Fifteen orphan warnings on Spa, for pages nobody asked to see.

Measured on a real CMake docs build: 19 toc.not_included before, 4 after, the
four being the genuine orphans both readers already agree on.

The build shape is now derived once. It was computed for the variant data file
and would have been computed again here; two derivations of one fact drift.

The review notes the compiler-free gate cannot see this, because a bare build
has no spl-core config.json. It can: the only thing CMake contributes is that
file, and it is three lines of JSON. Two tests fabricate it -- one asserts the
docs shape orphans nothing, the other that the reports shape still renders the
pages, because narrowing one shape must not starve the other.

The first version of that test passed with the fix reverted: it scanned stdout,
and Sphinx writes warnings to stderr. Caught by reverting the fix and watching
it stay green, which is the only way to find a vacuous test. It now fails with
three named warnings. The same blind spot made several failure messages quote
stdout alone; they include stderr now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding 1, taken as the in-repo option because the other one does not
exist yet: no released spl-core lets the flag be turned off. 8.8.0 is the newest
stable and 9.0.1rc4 the newest prerelease, and both hardcode --jinja-raw-tags.
So this is not a stopgap ahead of a pin bump -- it is load-bearing for an
unknown period, and it is written as a compatibility shim with its removal
condition stated, not as a TODO.

Every generated listing under __source_docs wraps its code-block in
`{% raw %}` / `{% endraw %}`. The only thing that ever consumed those markers
was the global Jinja pass this branch deletes, so in a reports build they
reached the reader as two literal paragraphs per listing.

The handler is a line filter, not a template render: nothing is evaluated, no
brace elsewhere is touched, and hand-written documents are never seen. The
markers become EMPTY LINES rather than disappearing, so the file keeps its line
count -- broken source mapping was one of the reasons the Jinja pass had to go,
and re-creating it in the replacement would have missed the point.

Two things that forbade any source-read handler are corrected rather than worked
around, because a rule the code visibly breaks is worse than no rule. The test
now asserts the shape -- exactly one handler, named, scoped to __source_docs, no
render_string anywhere -- and AGENTS.md states the exception and its expiry.

The fixture is generated by clanguru itself, so the test tracks what spl-core
actually emits rather than what it emitted once. Verified non-vacuous by
unregistering the handler and watching it fail on the marker.

A second markup trap caught in passing: asserting "int add" against raw HTML
fails because syntax highlighting splits it across spans. The test strips tags
before asserting, the same mistake that hid this bug from me a week ago.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings 3 and 4.

The Windows symlink fallback copied the whole CMake binary directory. Wrong
three times over: it ran at CONFIGURE time, when the reports the path exists for
have not been generated, so it only ever copied objects, binaries and
CMakeFiles; dirs_exist_ok meant a later configure never cleared an earlier
one's leftovers, so the copy went stale rather than obviously wrong; and
nothing reads generated/ yet, so the cost bought nothing. It writes a marker
explaining itself instead, and the fallback now has a test rather than leaving
it to the one platform nobody develops on.

ubc now runs in the documentation job, via useblocks/ubc-action, which puts the
binary on PATH where the tests already look for it. CI_REQUIRE_UBC turns a
missing binary into one clear failure instead of eleven skips: the parity tests
are the only check that the two readers agree, and a check that silently does
not run is worse than none, because the green tick claims it did. The other
tests still skip rather than erroring on a missing binary, so the reason is
stated once. Verified in all three modes -- optional and present, required and
present, required and missing.

Correcting the plan on one point: I recorded this as blocked on
UBCODE_LICENSE_KEY / UBCODE_LICENSE_USER secrets. It is not. `ubc license show`
reports no licence on this machine and there is no licence file, yet `ubc check`
and `ubc build html` both work -- every local run in this branch was unlicensed.
The action's licence inputs are wired anyway, so adding the secrets later
changes nothing, but nothing waits on them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings 5, 6, 7 and 8.

Sphinx walks with followlinks=True, so `generated` -- a symlink to the
configured variant's build directory -- was descended in full, under a second
set of paths that the `build/modules` and `build/deps` exclusions never match.
Excluding it prunes the walk: get_matching_files applies exclude_patterns to
directories and writes the result back to `dirs`, so the descent stops rather
than the files merely being filtered. Measured 0.09s -> 0.04s on a 301-file
build directory, and it scales with the tree.

That makes the forward-looking `generated/` configuration definitively inert
for Sphinx as well as for ubCode, which is the right state but a fragile one:
when spl-core writes the gcovr tree relative to the page and `generated/`
becomes real, the `build/` forwarding has to be dropped in the SAME change or
every report page is discovered under two docnames. That coupling was a comment,
which is read once. It is now a test that fails when only half the change is
done, verified by making the wrong half and watching it fire.

The variant-selection task no longer passes a target: the pointer is always the
docs cell. Choosing `reports` there opened every report fence in the IDE, each
globbing into build/**, which ubCode excludes -- an unmatched glob per
component. CMake writes the docs cell for the same reason. The target choice
stays on the build task, where it changes what is produced rather than what is
linted.

AGENTS.md claimed documents name `generated/` instead of globbing `/build/**`.
All seven still glob. It now describes the deferral, why it cannot be lifted
from this repository, and what has to move together when it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
avengineers/SPLed develop has two commits the useblocks fork lacks: spl-core
8.9 with a refreshed lock (f5ba89e), and the light controller state machine
fix (b8beee9). Conflicts, and how they were resolved:

- components/light_controller/doc/index.md: upstream nests the blink states
  in LIGHT_ON, inside a Jinja `{% if config.BLINKING %}` block. This branch
  turned that block into `{if} var.features.BLINKING` fences, so the fix goes
  into the BLINKING diagram, without the Jinja markers.
- pypeline.yaml: upstream only quoted `python_version: "3.11"`; the fork had
  already replaced that key with `python_executable: python3`. Kept the fork's.
- poetry.lock: upstream's refreshed lock, re-resolved against this branch's
  pyproject.toml. sphinx-codelinks stays at 1.4.0, the version this branch
  ran, with typer 0.26.7, because codelinks 1.4.0 caps typer below 0.26.8.
- uv.lock, which only the fork keeps: aligned to the same versions.

One conflict git could not see: the refreshed lock brings sphinx-needs 8.5.0,
which loads needs_from_toml at config-inited priority 10 and resolves the
variant data at 11. conf.py selected the variant data file at priority 20,
too late, so every build either failed on a missing `build/autoconf.json` or
silently read that pointer instead of the cell it was given. conf.py now
selects it at priority 10, between the two.
The documentation changes this project needs from spl-core are not in a
release yet: optional Jinja raw tags on generated listings, a variant data
file per Sphinx build shape, a stable path to the build directory for the
generated pages, and KConfig.declared_boolean_symbols(). They live on the
feat/configurable-docs-pipeline branch of useblocks/spl-core, five commits
on top of spl-core 8.9.0.

Pin that commit, so CI and every machine build the same code. In both lock
files only spl-core's source changes. Nothing uses the new settings yet;
the following commits do.
KConfig omits a promptless boolean from its output when it evaluates to n,
so the variant data defaults every declared boolean to false first. Finding
them meant reading the kconfiglib instance spl-core keeps privately, behind a
fallback for spl-core versions without an accessor. The pinned spl-core has
KConfig.declared_boolean_symbols(), so call it and drop the fallback.
…orkarounds

The pinned spl-core can now be told what this project's documentation
needs, so each workaround in conf.py and CMakeLists.txt gives way to one
setting.

Jinja markers. SPL_SOURCE_DOCS_JINJA_RAW_TAGS OFF: clanguru no longer wraps
the generated listings in `{% raw %}`, and conf.py's source-read handler that
blanked the markers again is deleted. conf.py registers no handler at all.

Variant data per build shape. SPL_VARIANT_DATA_FILE_DOCS and _REPORTS name
the cells, and spl-core passes the right one to every Sphinx build as
`-D needs_variant_data_file=`. The fixed-name copies CMake published and
conf.py's fallback that read them are deleted. Nothing in conf.py selects a
cell any more, which is also the only way that works with sphinx-needs 8.5:
it replaces conf.py's values with ubproject.toml's and resolves the variant
data before a config-inited handler at the old priority runs, so the old
selection was silently ignored there. The tests and the VS Code task select a
cell the same way, and it is the same key `ubc check -c` overrides.

Stable report paths. SPL_SPHINX_BINARY_DIR points spl-core at `generated`,
the link tools/variant_data.py maintains, so every generated page is named
`generated/...`. The report sections name their pages directly instead of
globbing `/build/**`, spl-core writes each coverage report next to its page,
and SplBuild finds the report artifacts there. conf.py prunes `build` from
the walk, so each page has exactly one name, and forwards only spl-core's
`generated/` patterns: the `generated/**` rule in ubproject.toml keeps them
out of a docs build, declaratively, for both readers. On Windows without
Developer Mode, `generated` becomes a junction instead of a marker directory,
because the report pages are now read through it.

The tests follow: they drive the fake report tree through `generated`,
check the settings CMake hands spl-core, and assert conf.py registers no
source-read handler. The strip's line-number test is gone with the strip.
VARIANTS.md walks a newcomer through the declarative variant handling in
18 use cases: switching the variant ubCode shows, previewing any variant
with Sphinx or ubc, comparing two variants, trying a change on a copy of
a variant data file, and writing, extending and checking variant-
dependent documentation. Every command in it was run against this branch.

It sits at the root, outside both readers' document sets, so neither
Sphinx nor ubCode indexes it. README.md and AGENTS.md link to it.
Nothing reads it. poetry.lock locks the project, and upstream deleted uv.lock in June 2025.
Pygments knows no jsonc lexer, so Sphinx warned twice. Its JSON lexer accepts // and /* */ comments, and ubc has no lexer check.
The three developer notes are orphan: true, which both readers honour. The requirements traceability matrix joins the start page's toctree, because it is product documentation. Four 'not in any toctree' warnings go in each reader.
tools/config_checks.py holds the checks the tests ran: the vendored needs model against spl-core's, ubCode's parser includes against conf.py's include_patterns, what ubproject.toml must leave to the generated files, and the generated rules against the grammar both engines share. conf.py runs them at config-inited and reports each finding as a warning, so a drift fails the build under -W instead of waiting for someone to run the tests.
The configuration tests read the generated rules and evaluate them, and the scope rules of a component report, with sphinx-mounts' own interpreter against the real cells; they check the one extend chain, the mount's condition and conf.py's source set by executing conf.py, and run the checks conf.py runs. The generator tests cover the component cells, the selection files with and without a build and for a component report, removing what earlier versions left, and hand-written rules. The documentation tests build through a selection file that mounts a fake build, where they used to re-point the link; ubCode reports a glob of the component toctree once, so the ubc comparison is made per glob.
ubc refuses an image in imported need content unless the import source is listed under [[needs.import_sources]] with allow_assets; since the image moved into the content, that is the CSV import. Sphinx needs no such permission and ignores the key.
sphinx-codelinks does not find the git root where .git is a file, and warns once per source (codelinks.git_ref). The gate tolerates exactly that warning, and only in a linked worktree; a clone, as on CI, has no such warning.
Two things let every component into a per-component report. A rule pattern without a slash matches at every depth, so the variant-wide rule's index.md also removed the component's own doc/index.md: the root index is no longer named, and is an orphan page, whose toctree entries a component report excludes anyway. And both readers match a rule's patterns against a mount's files relative to the mounted directory, so generated/<component>/** never narrowed the mounted build: each component's rule now names <component>/**, which covers its documentation in the tree and its pages in the build alike, and the variant's own pages are reports/**.\n\nA component report's links to the rest of the product dangle by design. Its selection file says so in ubc's terms (lint.ignore needs.dead_link), and conf.py suppresses sphinx-needs' needs.link_outgoing for that report. The rule tests now decide real paths with sphinx-mounts' own matcher, instead of comparing pattern strings, which is what let this through.
Every component report spl-core builds, in the docs shape, in both readers, strictly, with the needs compared: 24 of them next to the 10 variant cells. Each run selects its cell first, because codelinks reads the selected build's compile database.
@ubmarco ubmarco changed the title docs: make Sphinx and ubc agree, part 1 docs: make Sphinx and ubc agree Sep 29, 2026
tools/try_variants.py sets up .venv with uv and the locked dependencies and
walks through the cases -- one variant's documents in both readers, a CMake
build with its reports, one component's report, a comparison of two
variants, and the checks -- saying before each step what happens and after
it what to look at. Each case configures the variants it shows, so the
needs in the code follow their own #ifdef branches, and compare puts the
developer's selection back. VARIANTS.md points at it from Setup.
Ruff 0.16 with the project's settings. Behaviour is unchanged: the
subprocess calls that inspect their result say check=False, the logging
filter catches the errors a bad record raises rather than every
exception, the scripts with a shebang are executable, and noqa
directives no current rule needs are gone. try_variants' docs case also
says that it selects the variant it shows.
ubCode read 92 needs where Sphinx read 134 in Disco's reports: it does not
mount a directory inside the project, and the selection mounted the build
directory at `generated`. A mount injects sources from outside the project;
these pages are inside it. The selection now names the selected build's
pages by their own paths, as upstream spl-core names them, and both readers
read the same 132 needs (the two untitled imports apart), field for field.

- tools/variant_data.py: a reports selection declares the rst parser
  include of its build (one component's pages for a component report, none
  for the docs shape); the rules file gains one rule that shows a build's
  pages only for its own variant's and kit's reports, which the mount's
  condition used to do. build/selected_build.txt names the build for
  tools/compile_commands.py, since ubc refuses keys it does not know.
- conf.py adds the selection's rst include to include_patterns and prunes
  only build/ directories that never hold a document. ubproject.toml drops
  its rst parser and the build/** exclude, keeping build/modules/** out:
  googletest's docs/index.md matched `index.md`.
- Report toctrees glob /build/**/<component>/reports/...; the variant's
  coverage page gets a pattern per depth of the variant's name, since **
  would match every component's coverage page too.
- CMakeLists.txt no longer sets SPL_SPHINX_BINARY_DIR; spl-core is pinned
  at 5b98203, where it is reverted.
- The gate also builds the reports shape of every test-kit variant and of
  every component report. It fails for Spa: clanguru drops the -isystem
  feature-header directory, so the listings show the wrong #ifdef branch
  (TS_BC-001 instead of TS_BC-002/003). That is fixed in clanguru.
- The tests put back every file of the developer's selection.
The build jobs select the gate's tests by their quality-gate markers too,
without ubc, and the gate failed whenever CI was set: 63 failures in the
Linux job, 'ubc is required on CI'. It now fails only where CI_REQUIRE_UBC
is set, the switch the documentation job sets for the same purpose in
test_documentation.py, and skips elsewhere.
The source listings showed the #ifdef branch of "no feature defined":
clanguru took only -I and -D from the compile database, and the feature
header autoconf.h reaches the test sources through -isystem. Spa's report
listed TS_BC-001, a manual-brightness test specification, instead of
TS_BC-002/003, which Spa runs. clanguru also listed the declarations of the
headers a file includes, whose tabs ubc warned about.

Both are fixed in useblocks/clanguru (fix/docs-main-file-only, 07b8843),
proposed to cuinixam/clanguru; this pins that commit until a release has
them. The per-file ignore for the listings' tabs goes, and the gate passes
in full: 63 runs, the reports of every test-kit variant included.
@ubmarco ubmarco changed the title docs: make Sphinx and ubc agree docs: variant-dependent documentation without Jinja, the same in Sphinx and ubCode Oct 1, 2026
@ubmarco
ubmarco changed the base branch from feat/adopt-spl-core-docs-pipeline to develop October 1, 2026 13:39
@ubmarco
ubmarco marked this pull request as ready for review October 1, 2026 14:10

@Rubyfi Rubyfi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Reviewed in the Linux dev container on 2026-10-01 at commit 867d16b.

Findings

1. [Medium] The spl-core pin depends on temporary refs

pyproject.toml pins spl-core to
5b982030ab8e6bf192991a6613669d95a9e37af5. The commit is currently the tip of
only these remote refs:

  • refs/heads/feat/docs-parity-sweep
  • refs/pull/5/head

Deleting or rebasing the branch and eventually expiring the pull-request ref
would make fresh dependency installation fail. A durable tag or an upstream
release should protect the pinned revision.

2. [Low-medium] The parts.cmake parser accepts unsupported syntax

tools/variant_data.py recognizes the test-kit guard with a case-folded
substring check. Direct execution confirmed that it accepts all of these as the
plain test branch:

if(NOT BUILD_KIT STREQUAL test)
if(BUILD_KIT STREQUAL test OR FOO)
if(BUILD_KIT STREQUAL testing)
if(BUILD_KIT STREQUAL Test)

The first condition reverses the intended branch, while the others exceed the
documented grammar. The parser also returns "components/a" with its quotes
intact for a quoted component, so generated membership conditions do not match
the actual component. A multiline spl_add_component() call raises the bare
error ValueError: substring not found rather than a contextual grammar error.

Match the normalized condition exactly, handle quoted arguments deliberately,
and reject multiline calls with the file and line in the error message.

3. [Low] The VS Code coverage tasks open paths that are not generated

The Open variant coverage report and Open component coverage report tasks
expect standalone files such as:

build/Disco/test/Debug/reports/coverage/index.html

That path does not exist after a successful build. The report is copied into
the consolidated Sphinx output instead:

build/Disco/test/Debug/reports/html/build/Disco/test/Debug/reports/coverage/index.html

Update the task commands to use the consolidated report paths, or have the
build produce stable redirects for the task paths.

4. [Low] Tests do not protect the required CMake setting order

The spl-core documentation settings currently appear before the
parts.cmake include, which is required because spl-core computes its output
paths while adding components. Existing tests assert the setting values but not
their position. Moving them below the include could therefore break report
generation without failing the focused tests.

Add an assertion that each required setting precedes the parts.cmake include.

5. [Nit] Two comments describe obsolete configuration

  • .gitignore describes generated as the current-variant pointer and mentions
    a Windows copy fallback, although selection now uses TOML files.
  • doc/cross-platform-alignment-proposal.md says the project pins
    spl-core==8.6.0, while the installed and declared dependency is spl-core
    8.9.0 from the pinned fork commit.

Validation

Environment:

  • Python 3.12.3
  • spl-core 8.9.0
  • sphinx-needs 8.5.0
  • pytest 8.4.2

Focused review suite:

.venv/bin/python -m pytest \
	test/test_variant_data.py \
	test/test_ubproject_config.py \
	test/test_documentation.py -q

Result: 118 passed, 12 skipped in 37.87s. All skips require ubc, which is not
installed in the container.

Variant builds:

./build.sh --build --variant Disco --build-kit test --build-type Debug
./build.sh --build --variant Spa --build-kit test --build-type Debug
./build.sh --build --variant Sleep --build-kit test --build-type Debug
./build.sh --build --variant Disco --build-kit test --build-type Debug
Variant Tests Failures Errors Skipped
Disco 22 0 0 0
Spa 18 0 0 0
Sleep 37 0 0 0

Each build configured, compiled, ran its tests, and generated its consolidated
HTML report. Switching from Spa and Sleep back to Disco also succeeded. The
Disco Sphinx report completed schema validation with zero warnings.

Test Gaps

  • Windows was not tested.
  • ubc parity checks were not run because ubc is unavailable.
  • Parallel variant builds were not exercised.

The test-kit guard was found by a case-folded substring, so
if(NOT BUILD_KIT STREQUAL test) was read as the test branch, and a compound
condition, another kit name or STREQUAL Test passed as well. A quoted
component kept its quotes, so its rule never matched, and a call over two
lines raised a bare 'substring not found'.

Each statement is now matched whole: command names in any case, as CMake
reads them, arguments exactly. Everything else raises with the file and the
line. Found in the review of #4.
The coverage tasks opened build/<V>/test/Debug/reports/coverage/index.html,
which no build writes: gcovr's tree is part of the variant's HTML report, at
reports/html/<build-relative path>/coverage/. The same holds upstream.

The component report task opened the report's index.html, which is the
orphan variant start page since the component report has a root of its own,
doc/component_report. Found in the review of #4.
spl-core computes what it writes for a component while parts.cmake adds
it, so a setting moved below the include applies to none, and every test of
its value still passes. Found in the review of #4.
.gitignore still described the generated link as the current-variant
pointer, and the cross-platform proposal named spl-core 8.6.0 as the pin.
Found in the review of #4.
@ubmarco

ubmarco commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the thorough review, @Rubyfi. All findings confirmed and reproduced.

2. parts.cmake parser — fixed in 51a6231. Each statement is now matched whole: command names in any case (as CMake reads them), arguments exactly. if(NOT BUILD_KIT STREQUAL test), … OR FOO, testing and Test now raise, as do a multiline or two-argument spl_add_component() and else(X), each with the file and line. A quoted component loses its quotes. Every case has a test.

3. VS Code report tasks — fixed in ed69f22. Both coverage tasks open the consolidated report paths (the wrong path was there upstream too). While checking, I found that Open component test report opened the component report's index.html, which is the orphan variant start page since the component report has its own root; it now opens doc/component_report.html.

4. Settings order — 0717018 adds test_the_spl_core_settings_come_before_the_components, which fails when one of the four settings is moved below include(…/parts.cmake).

5. Obsolete comments — c452ecb fixes .gitignore. In the cross-platform proposal, the 8.6.0 pin note describes that branch's history, so it gets a dated update below it instead of a rewrite.

1. Temporary pins — agreed. The clanguru pin (useblocks/clanguru 07b8843) has the same exposure. The durable fix is the upstream releases (spl-core#5, cuinixam/clanguru#9). Until then I'd protect both commits with tags in the forks; I'll do that separately.

Also re-checked: Sphinx -W and ubc build html are clean apart from the known REQ_37/REQ_58 imports, and every relative link in the generated reports resolves, the coverage links included.

A bare 'ubc build html' writes _build/html under the project root, and
Sphinx's convention is _build/<builder>. SPLed's own commands write under
build/, but a bare run left an untracked _build/ behind.
ubc rendered `remote-url` as plain text: sphinx-codelinks registers the
field and adds its link rule only while Sphinx runs, in Python, so ubCode
never saw either. ubproject.toml now declares the field and a rule for the
URL ubCode stores there.

sphinx-needs applies only the first rule naming a field, matching or not,
and the TOML rule came before codelinks' own, which left Sphinx's value
unlinked; conf.py drops it for Sphinx between loading the TOML and
compiling the rules. The duplicate-field filter covers remote-url too.

The readers store different values in the field (the path in Sphinx, the
whole URL in ubCode), so the gate lists it with that reason in
docs_exceptions.toml instead of comparing it.
…branches

spl-core ce62088 is the merge of useblocks/spl-core#5 into the fork's
develop, which mirrors avengineers' develop; clanguru 50a25d7 the merge of
useblocks/clanguru#1 into the fork's main. Both contain the commits pinned
before (5b98203, 07b8843), which were reachable only from feature branches
and pull-request refs. Answers finding 1 of the review of #4.
@ubmarco
ubmarco merged commit e79a759 into develop Oct 1, 2026
10 of 12 checks passed
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.

5 participants