Skip to content

[pull] master from useblocks:master - #221

Merged
pull[bot] merged 175 commits into
boschglobal:masterfrom
useblocks:master
Sep 15, 2026
Merged

pull[bot] merged 175 commits into
boschglobal:masterfrom
useblocks:master

Conversation

@pull

@pull pull Bot commented Sep 15, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

danwos and others added 30 commits April 26, 2018 09:20
…o test-files and tests for pytest and nosetest output xml files
* added directive test-env and its supporting doc

* added test-env directive

* added test-env directive and supporting docs

* test cases and bug fix in directive

* test case for Warning

* warning test case, py27-sphinx15 fix
…TEST_51

skip testsuites element if present
ubmarco and others added 26 commits September 4, 2026 23:39
`test-reports convert FILE... --output needs.json` converts test-result XML
into schema-conform needs.json outside Sphinx, as a cacheable build action:
- no Sphinx import anywhere in the CLI chain (asserted by a test)
- byte-stable output: sorted keys, fixed indent, no wall clock
- deterministic case IDs, source location and synthesized source URLs
- XML properties become need fields; --link-property promotes them to
  link fields
- schema test skips on sphinx-needs versions without check_needs_data
…locks/sphinx-test-reports#146)

Reverts 6e594a5 (useblocks/sphinx-test-reports#144).

useblocks/sphinx-test-reports#144 was merged ahead of the declarative-configuration work
unintentionally. The merge itself was not the problem — the ordering
was. The intended stack is:

1. **this revert** — take the CLI back off `master`
2. **useblocks/sphinx-test-reports#145, reshaped to config-only** — the `[test_reports]` section of
`ubproject.toml` and the Sphinx bridge, with no CLI surface
3. **a new CLI PR** — the converter rebased on top, reading its
conversion settings from the declarative config from the start, instead
of shipping flags first and growing `--config` afterwards

## What this removes

The five files useblocks/sphinx-test-reports#144 added — `sphinxcontrib/test_reports/cli.py`,
`needs_export.py`, `remote.py`, `docs/cli.rst` and
`tests/test_cli_convert.py` — plus the `test-reports` console script,
the `cli` toctree entry and the `cli.py` mypy exclude.

It also takes back the PEP 562 lazy `setup` resolution in `__init__.py`,
which existed so the CLI could import package submodules without pulling
in Sphinx. It has no consumer without the CLI and returns with it.

## What is unaffected

Nothing on the Sphinx side depends on the reverted files. `identity.py`
came from useblocks/sphinx-test-reports#143, not useblocks/sphinx-test-reports#144, so `directives/test_common.py` and the
deterministic-ID feature are untouched. 110 tests pass on this branch.

The work is not lost: it is preserved locally on `str-needs-json-cli` at
35f26dc and comes back as step 3.
Python 3.10 reached the end of upstream support.

The floor matters beyond housekeeping: `tomllib` landed in 3.11, so a
3.10 floor means carrying `tomli` as a dependency purely for older
interpreters. Split out of useblocks/sphinx-test-reports#145, which needs a stdlib TOML parser, so
the version bump can be reviewed and merged on its own.

## Changes

- `requires-python` `>=3.10` → `>=3.11`
- `noxfile.py` and the CI test matrix run 3.11/3.12
- the `3.9` and `3.10` classifiers go (3.9 was already stale —
`requires-python` has excluded it for some time)
- the `pre-commit` and `linkcheck` jobs pinned Python 3.9 purely to
drive nox; they move to 3.12, since 3.9 is two releases past end of life
and the sessions they start pick their own interpreter anyway

No source change — nothing in the package uses a 3.11 feature yet. 110
tests pass.

For reference, the sibling repos are already ahead of this:
sphinx-codelinks and sphinx-mounts both require `>=3.12`.
…st-reports#150)

Follows useblocks/sphinx-test-reports#147 (Python 3.11, merged). Split out so the version floors can
be reviewed on their own;the declarative-config, plugin and converter
PRs are rebased onto it and lose their remaining compatibility branches.

## Why

The package now requires Python 3.11. sphinx-needs raised its own floor
to ​3.10 with ​6.0 (#1468 dropped 3.9) and requires Sphinx 7.4 from that
release on. The versions this package still carried compatibility
branches for — sphinx-needs 2.x to ​5.x, Sphinx below ​6.1 — are ones it
no longer shares an interpreter range with in practice,and​ ​6.x is
where field registration with a schema begins, which is what the
extension relies on.



The exact floor is **6.0.1**:that is the first release whose
`add_extra_option` takes `schema=` (sphinx-needs #1527). On 6.0.0 the
call raised `TypeError`, which `_register_field` swallowed under a
catch-all meant for "already registered" — so no field was registered
and every directive failed with `InvalidNeedException`. The first CI run
of this PR caught exactly that on the 6.0.0 cells.



## Changes

- `sphinx-needs>=6.0.1` and `sphinx>=7.4` (the floor sphinx-needs 6
implies anyway)
- nox and CI matrices: sphinx-needs 6.0.1 /​ ​6.3.0 /​ ​7.0.0 /​ ​8.0.0
× Sphinx ​7.4.7 /​ ​8.1.3 (the Sphinx 5.0 cells could not have installed
sphinx-needs 6)
- `sphinx_needs_update` registers fields with a schema
unconditionally;the no-schema branch is gone
- `_register_field` catches only `NeedsApiConfigWarning` (a duplicate
registration) instead of every exception,and no longer inspects
`add_extra_option`'s signature
- the Sphinx version switches around `logging`, `status_iterator` and
the test fixtures' path type are gone
- the `needs_schema_definitions` tests no longer skip
- the test fixtures' confs now switch to `needs_fields` at sphinx-needs
7.0.0 instead of 8..0.0:7.0.0 is where `needs_fields` was introduced and
`needs_extra_options` deprecated (sphinx-needs #1611), so on ​7.x the
old option emits a `[needs.deprecated]` warning, which
`test_test_file_needs_extra_options_no_warning` caught. Schema-based
registration works since ​6.0.1, so this only changes the 7.x path — no
behaviour change for ​6.x or​ ​ 8.x.



No behaviour change for any supported version. 110 tests pass on each
of: sphinx-needs ​6.​0.1 / Sphinx ​7.4.7,​ ​7.0.0 / Sphinx ​7.4.7,and​
​7.0.0 / Sphinx ​8.1.3;also verified on ​8.5.0 / Sphinx ​9.1.
…seblocks/sphinx-test-reports#145)

Stacked on useblocks/sphinx-test-reports#150 (sphinx-needs 6.0.1 / Sphinx 7.4 floor) and, through it,
on the Python 3.11 floor of useblocks/sphinx-test-reports#147 — reading the section needs `tomllib`.

Describe the project once, in the `[test_reports]` section of
`ubproject.toml` — the declarative file sphinx-needs, sphinx-codelinks,
sphinx-mounts and ubCode already read — instead of restating it in
`conf.py`.

**Rescoped.** This PR previously also carried the `test-reports convert`
CLI integration and the conversion settings themselves. useblocks/sphinx-test-reports#144 has been
reverted (useblocks/sphinx-test-reports#146) so the declarative configuration lands first; the
converter returns as a follow-up PR stacked on this one, bringing its
own settings under `[test_reports.build.needs]` and reading them from
the start rather than shipping flags and growing `--config` afterwards.
What is left here is exactly what the Sphinx extension consumes.

## What

- New Sphinx-free module `projectconfig.py` parses, validates and
normalises the section. It imports no Sphinx, and neither does the
package `__init__` it runs through: `setup` is resolved lazily (PEP
562), so a build action can read the section without the documentation
toolchain installed. The section describes the *project*, not this
extension.
- **Bridge**: a `config-inited` handler at `priority=100` applies the
Sphinx-facing keys to their `tr_*` config values, ahead of the handlers
that read them. Precedence is **`-D` > `ubproject.toml` > `conf.py` >
built-in default** — the file is the source of truth for the project,
the command line stays the per-invocation escape hatch. Both are
announced in the log (`Applied … from …`, `Kept the -D value of …`).
- **New confval `tr_config_from_toml`** (default `ubproject.toml`;
`None` disables, and `NoneType` is an accepted type so the documented
opt-out does not trip Sphinx's own confval check). With the default name
the file is searched for upwards from the `confdir` to the repository
root — the directory holding `.git`, and nothing else bounds the search:
a `pyproject.toml` on the way up marks a distribution, not the project,
so `docs/pyproject.toml` beside `conf.py` and a uv-workspace member
(`packages/<dist>/docs/conf.py` with one `ubproject.toml` at the
monorepo root) both find the file. A fruitless search says where it
ended (`sphinx-build -v`). An explicitly named file is used as-is and
warns if missing (`test_reports.missing_config`).
- **Spellings**: `file`/`suite`/`case` accept both the positional
`conf.py` list and a named table
(`directive`/`type`/`name`/`prefix`/`color`/`style`). Relative
`rootdir`/`report_template` anchor at the TOML file's directory; the
loader joins without resolving, so the form of the path it is handed
(Sphinx's already-resolved `confdir`, a converter's working directory)
is kept as is.
- **Error policy**: a *known* key with the wrong type is an error — that
is the typo class this validation exists for — and table *values* are
checked, not just the outer table, including the values inside a
`file`/`suite`/`case` table. An *unknown* key is warned about and
ignored (suppressible via `test_reports.unknown_key`): the file is
shared with tools on independent release cadences, so a key this version
does not model must not take a build down. Same posture sphinx-mounts
documents for `[[source.mounts]]`.
- **Foreign sub-table**: `[test_reports.build]` — and only that one — is
recognised and passed through untouched. It holds the settings of the
`test-reports build` command line, one sub-table per artifact it
produces (`[test_reports.build.needs]` for turning test reports into a
`needs.json` outside Sphinx), none of which this extension does, so it
must draw neither an unknown-key warning nor a `tr_*` value. Any other
sub-table is an unknown key like any other. The converter PR defines and
validates the contents of `build.needs`.
- `tr_rootdir` declares `types=(str, os.PathLike)`: its default is
Sphinx's `confdir`, a `_StrPath`, so a plain string — the only thing
TOML or a string literal in `conf.py` can supply — used to fail Sphinx's
confval type check and take a `-W` build down. Pre-existing, but the
documented example newly advertised it.

## Notes

- The section is spelled `test_reports`, matching every other section of
`ubproject.schema.json` (`build_tags`, `format_rst`, `needs_json`, …)
and leaving the existing `[reports]` — report *templates* — unambiguous.

## Tests

54 tests in `tests/test_project_config.py`: loader validation,
normalisation and path anchoring; upward discovery, its repository-root
boundary and the layouts a `pyproject.toml` must not end
(`docs/pyproject.toml`, workspace member); the foreign-sub-table
passthrough; real Sphinx builds asserting the precedence chain (`-D`
beats TOML, TOML beats `conf.py`), the warning paths, where a fruitless
search ended, and that the documented example — read from
`configuration.rst` verbatim — applies without a warning; and that
`projectconfig` imports in a process where Sphinx is blocked.

## Review

Reviewed in
useblocks/sphinx-test-reports#145 (comment),
addressed in
useblocks/sphinx-test-reports#145 (comment).
The CLI-side fixes from that round move with the converter to its own
PR.

Second round:
useblocks/sphinx-test-reports#145 (review)
— the three fixes, the text corrections and both unconfirmed claims are
addressed in 809bd9a; the discovery-rule question across packages stays
open for the monorepo.

Two findings from the first review move with the conversion settings and
are re-applied in the converter PR: the `need_type` / `case.type`
agreement check (both name the need type of a test case, so they must
not disagree), and the `link_properties` value-type validation.
…t-reports#154)

Closes useblocks/sphinx-test-reports#152. Follow-up from @chrisjsewell's [review of
useblocks/sphinx-test-reports#150](useblocks/sphinx-test-reports#150 (review)).

## What

`packaging` was declared as a runtime dependency, but PR useblocks/sphinx-test-reports#150 removed
the last import of it under `sphinxcontrib/`. It is still genuinely used
outside the package, so it moves to the extras that cover those
consumers rather than being deleted outright:

| Consumer | Installed via | Extra it now lives in |
| --- | --- | --- |
| `docs/conf.py` | RTD (`.readthedocs.yaml`) and the `linkcheck` nox
session | `docs` |
| 9 × `tests/doc_test/*/conf.py` | the `tests` nox session | `test` |

All of them use it for the same thing — the
`Version(sphinx_needs.__version__) >= Version("7.0.0")` config gate.

The `>=20.0` floor is dropped rather than carried over:
`packaging.version.Version` predates it by years, Sphinx already pins a
much higher floor, and the surrounding extras entries only carry bounds
where one actually matters.

## Verification

- `grep -r packaging sphinxcontrib/` → no matches (the issue's stated
criterion).
- AST scan over all 18 modules under `sphinxcontrib/` for `import
packaging` / `from packaging …` → **zero** hits, so this is not a grep
artifact.
- Installed metadata after the change lists `packaging` only as
`packaging ; extra == "docs"` and `packaging ; extra == "test"` — no
longer a runtime requirement.
- `pytest -n auto tests/` → **169 passed**, unchanged from baseline
(Python 3.12, Sphinx 8.1.3, sphinx-needs 8.0.0, from a fresh `.[test]`
install).
- `sphinx -b html docs docs/_build/html` from a fresh `.[docs]` install
→ `build succeeded.`, no warnings.
- `pre-commit run --all-files` → all 7 hooks pass, `taplo-format`
included.
- `uv sync --group dev && uv run mypy` → `Success: no issues found in 6
source files`.

No lockfile in the repo, so nothing to regenerate.
Closes useblocks/sphinx-test-reports#153.

## What

Adds `8.5.0` as a fifth `sphinx_needs-version` cell to the CI matrix and
to
the `SPHINX_NEEDS_VERSIONS` list in `noxfile.py` that mirrors it. Four
new
jobs (python 3.11/3.12 × sphinx 7.4.7/8.1.3), taking the `tests` matrix
from
16 to 20.

## Why — and a correction to the issue's premise

The issue asks for this on the grounds that the `add_field` registration
branch in `sphinx_needs_update()` is reachable only from sphinx-needs ≥
8.5
and therefore untested. **That premise does not hold.** Checking the API
across the matrix range:

| sphinx-needs | `sphinx_needs.api.add_field` |
|---|---|
| 6.0.1 | absent |
| 6.3.0 | absent |
| 7.0.0 | **present** |
| 8.0.0 | present |
| 8.5.0 | present |

`add_field` was introduced in **7.0.0**, alongside `needs_fields` and
the
deprecation of `needs_extra_options` — the same 7.0.0 change the [PR
useblocks/sphinx-test-reports#150

review](useblocks/sphinx-test-reports#150 (review))
identified as the correct threshold for the test fixtures' switch. The
existing 7.0.0 and 8.0.0 cells were already exercising that branch; the
`# sphinx-needs >= 8.5` comment above the import said otherwise and is
the
likely source of the mix-up. This PR corrects it to `>= 7.0`.

The cell is still worth adding, for a different reason: the matrix
topped out
at 8.0.0, while 8.5.0 is the current release and the one a fresh
`pip install sphinx-test-reports` resolves to today. That version was
untested. It now is.

## Changes

- `.github/workflows/ci.yaml`, `noxfile.py`: add `8.5.0` to the
sphinx-needs
  matrix
- `sphinxcontrib/test_reports/test_reports.py`: correct the `>= 8.5`
comment
  to `>= 7.0` (comment only, no behaviour change)
- `docs/changelog.rst`: `Testing:` entry under Unreleased

## Verification

All four new cells run locally against sphinx-needs 8.5.0:

```
[py3.11 sphinx=7.4.7 sn=8.5.0] -> 169 passed
[py3.11 sphinx=8.1.3 sn=8.5.0] -> 169 passed
[py3.12 sphinx=7.4.7 sn=8.5.0] -> 169 passed
[py3.12 sphinx=8.1.3 sn=8.5.0] -> 169 passed
```

`pre-commit run --all-files` and `mypy` are clean. CI on this PR is the
real
check.

8.5.0 fits the rest of the matrix without constraint changes: it
requires
`python >=3.10` and `sphinx >=7.4,<10`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…locks/sphinx-test-reports#151)

Based on master. Independent of the needs.json converter (useblocks/sphinx-test-reports#148, merged);
rebased on top of it.

## Why

A stock pytest run writes a JUnit XML this extension can only partly
use. Under pytest's default `junit_family = xunit2`, no `<testcase>`
carries the `file`/`line` attributes that give a test case its source
location (`tr_source_file_option`/`tr_source_line_option`, and the
deterministic ID of `tr_deterministic_case_ids`). Under `xunit1` pytest
writes them, but counts the line from 0, keeps Bazel's runfiles prefix,
and has no way to point a case at the file that drove it. And nothing
records the requirements a test verifies.

## What

`-p sphinxcontrib.test_reports.pytest_plugin`, with `junit_family =
xunit1` (warned about otherwise), does two things to every `<testcase>`,
through hooks on the test reports rather than fixtures, so a case
skipped or erroring during setup gets the same treatment and
pytest-xdist works:

- the source location becomes the one an editor shows: line counted from
1, path relative to the rootdir with Bazel's `_main/` runfiles prefix
cut at a whole path component;
- the properties of an `add_test_properties(...)` decorator (or, for
file-driven parameterised tests, the `apply_test_metadata(...)` runtime
helper, which can also point the case at the driving file) are written
as `<properties>`.

Which properties exist is configuration, not code. The
`test_reports_properties` ini option declares them, one `keyword [=
XmlName] [, list]` per line: the keyword a test writes, the `<property>`
name it is written under, and whether it takes a list (joined with `",
"`, which `tr_property_link_types` splits again). A keyword not declared
is written under its own name with a single value; a list under it, a
list item that is not a string, or bytes, is a `TypeError` naming the
option, so nothing is lost to a Python `repr` silently. The plugin ships
no metamodel of its own.

S-CORE's model is the worked example in the docs:

```ini
[pytest]
addopts = -p sphinxcontrib.test_reports.pytest_plugin
junit_family = xunit1
test_reports_properties =
    partially_verifies = PartiallyVerifies, list
    fully_verifies = FullyVerifies, list
    test_type = TestType
    derivation_technique = DerivationTechnique
```

Ported from S-CORE docs-as-code's `score_pytest` attribute plugin: same
two entry-point names, and with that example the XML comes out the same,
so tests written against that plugin keep working when they import from
here. Four deliberate differences in the XML: a case skipped or erroring
at setup carries its location and properties here and pytest's stock
values there; the properties keep the order of the keywords; the Bazel
prefix is cut at a whole `_main` component only; and a `file` handed to
`apply_test_metadata` is cut the same way. Two of that plugin's rules
are not ported, as process rules of that project rather than properties
of the data: the `TestType`/`DerivationTechnique` vocabularies are
documented, not enforced, and a decorated test needs no docstring.

The package `__init__` resolves `setup` lazily (PEP 562, useblocks/sphinx-test-reports#145), so
loading the plugin into a pytest process imports neither Sphinx nor
docutils; a test asserts it. A per-module mypy override lifts only the
two `Any` checks the pytest API needs. A `pytest` extra (`pytest>=7.0`,
where everything the plugin uses exists) pulls pytest in; it is not a
dependency of the extension. pytest before 7.3.2 does not run on Python
3.12 by itself, so a `plugin_floor` nox session, wired into CI, runs the
plugin's tests on the oldest pytest of each Python: 7.0.1 on 3.11, 7.3.2
on 3.12.

## Tests

`tests/test_pytest_plugin.py`, 63 tests through `pytester`, each inner
pytest a fresh subprocess: locations (a decorated function is located at
its first decorator, as pytest does; skipped and setup-erroring cases
included), the properties written from ini and pyproject declarations,
the option grammar and its errors, the value rules, marker merging
across class and function, the runtime helper's location override,
nested in-process sessions (one failing to configure), a decorator
applied before `pytest_configure`, a bad shape next to a fixture error,
`--strict-markers`, the `xunit2` start-up notice under strict warning
policies, `-p no:junitxml`, `legacy`, an xdist run, the Bazel path cut,
and that importing the plugin imports no Sphinx. Verified on pytest
7.0.1 and 7.4.4 (Python 3.11), 7.3.2, 7.4.4 and 8.4.2 (3.12) and 9.1.1
(3.14).
… and `[pytest]` install without the toolchain (useblocks/sphinx-test-reports#159)

Closes useblocks/sphinx-test-reports#155.

## What changes

`pip install sphinx-test-reports` now installs `lxml` alone. Sphinx and
sphinx-needs move to a `sphinx` extra:

```toml
dependencies = ["lxml"]

[project.optional-dependencies]
sphinx = ["sphinx>=7.4", "sphinx-needs>=6.0.1"]
pytest = ["pytest>=7.0"]   # from useblocks/sphinx-test-reports#151, unchanged
```

So the install line of a documentation project becomes `pip install
"sphinx-test-reports[sphinx]"`, and a test runner or build action that
only wants `test-reports build needs` no longer pulls the toolchain. The
same goes for the pytest plugin useblocks/sphinx-test-reports#151 added as the `pytest` extra: `pip
install "sphinx-test-reports[pytest]"` now yields the plugin, pytest and
`lxml`, nothing of the toolchain. `[test]` gains the two floors it used
to inherit from the core list; `[docs]` already had them; the nox matrix
pins both explicitly and is unaffected.

The install page lists the three install lines side by side (`sphinx`
extra for a documentation project, `pytest` extra for a test runner,
bare package for a build action running the command), and the plugin's
page, which said how to enable the plugin but not how to install it,
names the extra.

## The version-constraint gap

An extra is opt-in, so a project that keeps installing the bare package
next to an already-installed older toolchain never shows pip the floors.
The lazy `setup` in `__init__.py` now enforces them at load time, before
the extension is imported (an outdated sphinx-needs may well import and
only fail later, inside a directive):

```
Extension error:
Could not load extension sphinxcontrib.test_reports: sphinx-needs 5.1.0 is installed, but sphinx-needs>=6.0.1 is required. Install the Sphinx extension's dependencies with: pip install "sphinx-test-reports[sphinx]"
```

A toolchain that is missing altogether keeps the existing "Could not
import extension" path, which now names the extra too:

```
Extension error:
Could not import extension sphinxcontrib.test_reports; the Sphinx extension's dependencies are an extra, install them with: pip install "sphinx-test-reports[sphinx]" (exception: No module named 'sphinx_needs')
```

One deviation from the sketch in the issue: instead of
`app.require_sphinx((7, 4))` plus a hard-coded sphinx-needs floor, the
new `toolchain` module reads the `sphinx` extra's requirements from the
package's own metadata (`importlib.metadata.requires`) and compares each
installed version against its specifier. The floors then live in
`pyproject.toml` alone -- useblocks/sphinx-test-reports#150 just moved them, and a copy in code would
need a lockstep test -- and a test guards that the extra the check reads
is the one `pyproject.toml` declares. `packaging` does the parsing; it
is imported lazily and the check stands down without it, since it
arrives with Sphinx (and pytest) wherever the extension can be loaded. A
distribution that is *not installed* is deliberately left to the import:
the import states it more precisely, and a toolchain importable from a
source tree without metadata must not be refused.

On Sphinx 9 the error appears inside Sphinx's crash report, as the
existing "Known" changelog entry already notes for the configuration
errors; the note now covers this case too.

## Guaranteeing the property in CI

A `toolchain_free` nox session (Python 3.11 and 3.12, new CI job, wired
into `all_good`) installs the package with the dependencies it declares
-- `pip install ".[pytest]"`, no `--no-deps`, so the declared lists
themselves are what is tested -- asserts that neither `sphinx`,
`sphinx_needs` nor `docutils` is importable, and runs the converter's
and the pytest plugin's test modules with `-m "not toolchain"`. The
tests that need a build carry the new `toolchain` mark (the issue
suggested `needs_sphinx`; `sphinx` itself is taken by Sphinx's own
testing fixtures). `tests/conftest.py` loads `sphinx.testing.fixtures`
only where Sphinx is importable, and one top-level import of the
package's Sphinx-dependent `exceptions` module in
`test_project_config.py` became local to the marked test that uses it.

useblocks/sphinx-test-reports#151's `plugin_floor` session (the plugin's tests on the oldest pytest
per Python) installed `.[test]`, which after this change brings the
toolchain back in. It now installs `.[pytest]` plus pytest-xdist and
runs the same toolchain-absent guard, so the floor run doubles as the
isolation proof on old pytest. It also forwards nox's posargs, which CI
passes (`--full-trace`) and it ignored.

The mypy CI job installs with `uv sync --group dev --extra sphinx`;
without the extra it would no longer see Sphinx and docutils.

## Open questions from the issue, as resolved here

- **Extra name:** `sphinx`, as proposed. It matches the
`[docs]`/`[test]` style and the `[pytest]` extra from useblocks/sphinx-test-reports#151.
- **1.5.0 or 2.0:** not decided here. The changelog carries a
"Breaking:" entry spelling out the install-line change; `version` is
untouched, so the release can decide.
- **Two-distribution split:** not done, per the suggested path.
- **`[pytest]` extra:** on master since useblocks/sphinx-test-reports#151; this PR is what makes it
toolchain-free, and the second commit proves and documents that.

## Verification

- `pytest -n auto tests/`: 407 passed (13 new in
`tests/test_toolchain.py`)
- `nox -s toolchain_free-3.11 toolchain_free-3.12`: 308 passed, 3
skipped (the pytest-xdist tests; xdist is not installed there), 24
`toolchain`-marked deselected, in a venv holding the package, `lxml` and
pytest only; the toolchain-absent guard passes
- `nox -s "plugin_floor(python='3.11', pytest_version='7.0.1')"
"plugin_floor(python='3.12', pytest_version='7.3.2')"`: 63 passed each,
toolchain absent
- `mypy`: clean (the new module is strict-checked, not excluded)
- `pre-commit run --all-files`: clean
- `sphinx-build -W` of the docs: clean

## Notes for the reviewer

- Rebased onto useblocks/sphinx-test-reports#151 (`pyproject.toml`, `noxfile.py`, `ci.yaml`,
`tests/conftest.py` conflicted; all resolved as a union of both sides).
The first commit is the original change; the second is the
plugin-isolation follow-up and can be reviewed, or dropped, on its own.
- `docs/install.rst` also fixes two pre-existing typos on the page it
rewrites ("must to be", "can be find").
…esult vocabulary for both parsers (useblocks/sphinx-test-reports#161)

## Why

S-CORE cannot adopt the `result` field as it is spelled today.

A test case's `result` was never a chosen name — it was whatever the
input called it. The JUnit parser passed the name of the result-carrying
`<testcase>` child through verbatim, so a failure came out as `failure`,
after the `<failure>` element:

```python
RESULT_PART_KINDS = ("skipped", "failure", "error")   # XML element names
...
tc_dict["result"] = first_part["kind"]                # the element name, verbatim
```

The two states with no element of their own were invented in the `else`
branch, and whoever wrote it reached for participles — `passed`, and
`disabled` for googletest's `status="notrun"`. The JSON parser passed
its report's own value through untouched.

Three consequences:

* the same product said `failure` for one case's result and `failed` for
the count of them on its suite (`fields.FIELDS`), and `errors` next to
`error`;
* the same logical outcome could arrive under two spellings depending on
the input format, although `jsonparser`'s API is documented as being in
sync with `junitparser`'s;
* `docs/directives/test_case.rst` documented the value as **`failed`** —
which it never was. The page that documents the directive was simply
wrong.

S-CORE's own colouring hook keys on pytest's vocabulary
(`passed`/`failed`/`skipped`/`disabled`), so STR-converted needs would
render uncoloured against it.

## What changed

Every state is now a participle: **`passed`, `failed`, `error`,
`skipped`, `disabled`**.

`error` is deliberately *not* renamed. It agrees with the `errors` count
beside it and with pytest, which spells that outcome `error` too. The
pair that disagreed was `failure`/`failed`, and `failed` is the half
that already appeared in a field name.

The substance is not the rename but the **normalisation step that did
not exist**: the vocabulary is declared once, in a new `results` module,
and both parsers map their input onto it. A JSON report written against
the JUnit dialect no longer produces a different `result` than the XML
it mirrors. A state this package does not know is still passed through
untouched, so a `tr_json_mapping` pointing at a report with a vocabulary
of its own keeps working.

The part-level `kind` stays the XML element name — it reports which
element the evidence came from, and the converter capitalises it into
the evidence heading ("Failure: …"). A test pins that divergence, and it
is red-green verified: normalising `kind` too makes it fail.

The declared `result` description now names the states, so a consumer
reading the declarations out of a produced `needs.json` — a schema
check, a metamodel validator, S-CORE's tooling — learns the field's
domain from the file:

```json
"description": "Test result status: passed, failed, error, skipped, disabled"
```

## Breaking — what a project has to update

1. **A filter naming the value**: `'failure' == result` becomes
`'failed' == result`.
2. **A custom `tr_report_template`**: it carries *two* such filters from
the shipped template it was copied from. This is the dangerous one — see
below.
3. **Custom CSS** targeting the generated `tr_failure` class, now
`tr_failed`. The stylesheet keeps rules for both, so the colours survive
either way.

The value is also what `test-reports build needs` writes into
`needs.json`, so a consumer of that file has to be updated with it. All
of this is in the changelog entry.

## The untested coupling this surfaced

The shipped `test_report_template.txt` filters on the result value
twice, and it had **no test coverage at all**: every fixture exercising
`test-report` pointed `tr_report_template` at a template of its own. A
rename could have emptied its "Failed test cases" table and count in
every project using the default, silently.

A fixture now builds with the default template, and its count caught
exactly that during this change — the parser started saying `failed`
while the template still filtered `failure`, and the count went to zero.

## Verification

* `pytest -n auto tests/` — **421 passed**, 0 failures (baseline before
the change: 407)
* `mypy` (strict) — **Success: no issues found in 14 source files**. The
new module is type-checked rather than parked on the `useblocks/sphinx-test-reports#114` exclude list
that both parsers sit on.
* `nox -s toolchain_free` — 3.11 and 3.12 both successful; the new tests
run with Sphinx, sphinx-needs and docutils all absent, so the
normalisation stays toolchain-free.
* `pre-commit run --all-files` — all hooks pass. The KOI8-encoded
fixture template survived the edit (its test asserts on the decoded
text).
* `sphinx-build docs` — clean. The documentation's own pytest example,
filtering `'failed' == result` against a real report, renders **"Failed
test cases: 4"** and emits `tr_failed` 12 times, so the filter and the
CSS class are verified end to end rather than by inspection.

## Follow-ups, deliberately not in this PR

* `common.css` has no `tr_disabled` rule although the parser emits
`disabled`, and no dark-theme variants at all.
* Annotating a *reference* to a test-case need with its result (the
S-CORE feature this came from) — that belongs in sphinx-needs'
`need_ref`/`meta_links`, with STR supplying the `tr_<result>` palette.

---

Targets `master`, adding to the `Unreleased` section; useblocks/sphinx-test-reports#160 gets rebased
on top, which folds the entry into the 2.0.0 section.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Release v2.0.0

Bumps the version, retitles the version directives that were written
against
the planned 1.5.0, and turns the `Unreleased` changelog into the 2.0.0
section.

### Why 2.0.0 and not 1.5.0

Four changes in this cycle are breaking, so the major digit moves. The
first
three break an existing install; the fourth breaks project content
instead:

| Change | PR | What breaks |
| --- | --- | --- |
| Sphinx and Sphinx-Needs became the `sphinx` extra | useblocks/sphinx-test-reports#159 | A
documentation project has to install `sphinx-test-reports[sphinx]`; the
bare package no longer brings a toolchain |
| Python 3.10 dropped | useblocks/sphinx-test-reports#147 | 3.11 is the floor |
| Sphinx-Needs 6.0.1 / Sphinx 7.4 floors | useblocks/sphinx-test-reports#150 | The compatibility
branches for older releases are gone |
| A failed test case's `result` is `failed`, not `failure` | useblocks/sphinx-test-reports#161 | A
filter naming the value (`'failure' == result`), a custom
`tr_report_template` — which carries two such filters from the shipped
template it was copied from — and custom CSS on the generated
`tr_failure` class, now `tr_failed` |

The docs were already written for 1.5.0, so every `versionadded` /
`versionchanged` directive naming it is retitled here.

### Changes

- **Version** → `2.0.0` in `pyproject.toml`, `docs/conf.py` (`version` =
`2.0`, `release` = `2.0.0`) and
`sphinxcontrib/test_reports/test_reports.py`
  (`VERSION`), matching the three places 1.4.0 touched.
- **Version directives** → `2.0.0` in `docs/cli.rst`, `docs/pytest.rst`,
  `docs/install.rst` and the five in `docs/configuration.rst`.
- **Changelog** → new `2.0.0` section, released 11.09.2026, with the
  `Unreleased` heading kept empty on top.

### Changelog notes

The entry prose is unchanged; it is regrouped by category the way the
1.4.0
section is, and each entry now carries its PR link. Two deliberate
edits:

- The Python 3.10 drop and the Sphinx-Needs 6.0.1 / Sphinx 7.4 floors
were
labelled `Support:`. They are relabelled `Breaking:`, since they are
what
  the major bump is for.
- useblocks/sphinx-test-reports#154 (`packaging` dropped as a runtime dependency, moved into the
`docs` and
  `test` extras) had no changelog line. Added as a `Support:` entry — it
  changes the installed dependency set, which is this release's theme.

All 16 `Unreleased` entries survive; the new count is 17. Rebasing onto
master
after useblocks/sphinx-test-reports#161 merged folded in its two entries — the `Breaking:` rename and
the
`Improvement:` parser normalisation — bringing the section to 19. They
are
placed by the same category grouping, and the section's opening prose
gained
the rename as a fourth breaking change; its "Nothing else in an existing
project has to change" sentence had to be inverted, since the rename
makes it
false.

### Verification

- `uv run pytest -n auto tests/` → **421 passed** (Python 3.14, from a
fresh
`uv sync --all-groups --all-extras`), re-run on the rebased branch; 407
  before useblocks/sphinx-test-reports#161, which added a test module.
- `uv run sphinx-build -b html docs docs/_build/html` → `build
succeeded.`,
  no warnings, so the rewritten changelog is valid reStructuredText.
- `uv run pre-commit run --files <the 8 changed files>` → all hooks
pass.
- `grep -rn "1\.5\.0" docs/ sphinxcontrib/ pyproject.toml` → no matches
left.
- No test asserts the package version, so the bump needed no test
changes.

### After merge

Push a `2.0.0` tag. `.github/workflows/release.yaml` triggers on tags
matching
`[0-9].[0-9]+.[0-9]+`, which `2.0.0` does, and publishes to PyPI via
trusted
publishing.
…st-reports

The whole of useblocks/sphinx-test-reports' `master` -- 161 commits, tip `ca74b80`
(🚀 Release v2.0.0, useblocks/sphinx-test-reports#160) -- rewritten by `git filter-repo` so
that every historical commit already places its files under `packages/sphinx-test-reports/`,
and merged here with `--allow-unrelated-histories`. It is the third such import, after
sphinx-mounts (#1850) and sphinx-codelinks (#1871).

Note the qualification above, because this message is the one place the rule below could
not reach: it was written by hand AFTER filtering, so nothing rewrote its references. An
unqualified number here would autolink to the sphinx-needs issue that happens to carry it,
which is exactly the error the cap exists to prevent -- so every sphinx-test-reports
reference in this message is written out in full. `#1850` and `#1871` ARE sphinx-needs
numbers and are correctly bare.

Three things came with it:

* **Tags.** Its ten releases arrive prefixed, `sphinx-test-reports-v0.3.6` ...
  `sphinx-test-reports-v2.0.0`. The prefix is load-bearing rather than hygienic: six of
  the ten bare names -- `1.0.1`, `1.0.2`, `1.1.0`, `1.2.0`, `1.3.0` and `2.0.0` -- are
  existing sphinx-needs releases, and `release_plan.py` reads every tag without `-v` in
  it as a sphinx-needs release.
* **A scoped commit-message rewrite.** Bare `#N` became `useblocks/sphinx-test-reports#N`,
  150 of them, bounded to that repository's own number range, 1 to 161. Exactly three
  references are deliberately left bare, all in one commit ("build: require sphinx-needs
  6.0.1 and Sphinx 7.4"): `#1468`, `#1527` and `#1611` are **sphinx-needs** issues, and
  left bare they autolink here to the issues they actually mean. A lazy `#([0-9]+)` rule
  would have mangled all three into references that do not exist.
* **The PlantUML jars, removed.** `--path-glob '*.jar' --invert-paths` strips three jar
  blobs -- 19.97 MB, 94 % of this import's permanent weight -- from every historical tree.
  The import adds 1.07 MiB of objects instead of 18.75 MiB. This workspace already vendors
  one PlantUML jar, once, at `vendor/plantuml/`, under a `verify-plantuml` gate. The
  imported tree is otherwise byte-for-byte the source clone at `ca74b80`; the commit map
  records the strip and the blob hashes.

`git log packages/sphinx-test-reports/<path>` reads with no `--follow`: filter-repo
rewrote the paths in every commit, so from this repository's point of view those files
were always there.
**This is the arc's ONE `--no-verify` commit, and it has to be.** The merge before it made
`uv sync --frozen` fail instantly -- `[tool.uv.workspace] members = ["packages/*"]`
discovers the directory the moment it lands, so the lock is a member short and the
`uv-lock` prek hook, the `check-workspace` hook and the `ty` hook all fail together until
this commit relocks. Every commit after this one passes the hooks normally.

* **Layout.** `git mv sphinxcontrib src/sphinxcontrib`, the shape every other member has.
  `sphinxcontrib/` stays a PEP 420 namespace with no `__init__.py`, which is why the
  manifest has to declare `[tool.flit.module] name` at all.
* **The member manifest is truncated to what a member may own.**
  `check_workspace.py` check (7) refuses `[dependency-groups]`, `[tool.ruff.lint]` and
  `[tool.pytest.ini_options]` here -- the root owns all three -- and `[tool.mypy]` goes
  because this workspace type-checks with ty, not because a fence says so. The `test` extra
  goes with them: its contents are the root's shared `test` group, minus `nox` and
  `packaging`, which the noxfile took with it. `requires-python` becomes `>=3.11,<4`
  (check (3) is a string EQUALITY compare against the root, and a bare `>=3.11` fails it),
  and both surviving `sphinx-needs` specifiers become `>=8.5.0,<9` (check (4)'s tight
  tracking on an intra-workspace edge, `propagate_floors.py`'s to move at each release).
  `[project.urls]` is repointed at this repository, and the dead lowercase `download-url`
  dropped.
* **Root wiring, all eight edits**: `[project] dependencies`, the `docs-reports`
  pass-through extra, `[tool.uv.sources]`, both `[tool.ruff] src` roots (isort decides
  first-party by finding a module under one of them), `[tool.ty.src] include`, **the
  `typecheck` task's explicit path list** -- ty intersects the CLI path with the include,
  so the include alone would leave the member checked by nothing with every gate green --
  the `toolchain` pytest marker relocated from the member, and `sphinx-design` into the
  shared `test` group, without which three tests error at setup in every cell.
  `dependabot.yml` gains the fourth `ignore` entry.
* **Poe tasks**, short name `reports`: `test-reports`, `test-reports-sphinx{7,8,9}`,
  `docs-reports`, `docs-reports-clean`, `build-reports`, `import-check-reports`.
* **The lock gains one name, one entry, zero third-party distributions and zero bytes to
  download**: 144 -> 145 packages, `+ sphinx-test-reports`, nothing removed and no version
  moved. Both of this package's non-sibling needs were already here -- `lxml` through the
  root `test` group, `sphinx-immaterial` through sphinx-needs' `theme-im` extra.

**This commit also adds `design/import-commit-map.txt`**, all 258 commits the rewrite
parsed. 161 of the right-hand hashes exist here (`master`); the other 97 lived only on the
29 branches that were not imported and resolve in the archived old repository through the
left-hand column. Its header also records what makes this import unfaithful in exactly one
respect -- the three stripped PlantUML jar blobs, by hash and byte count -- so that a
reader of the map does not have to find that out from a pull request body.

`check_workspace.py` now exits 0 with every check green.
Every file that existed because the package was its own repository.

**Retired**, each replaced by something this repository already has:

* `.github/workflows/{ci,coverage,release}.yaml` -- the four Extensions cells, the
  Extensions cell's codecov upload, and this repository's one `release.yaml`. The
  20-cell sphinx-needs axis that `ci.yaml` ran is the one real loss, and the changelog
  entry names it rather than letting the CI diff say it.
* `noxfile.py` -- 36 sessions. Its `toolchain_free` lane does NOT retire with it: it is
  the fence that keeps the converter and the pytest plugin importable with no
  documentation toolchain installed, and it comes back as a scratch-venv CI job, because
  no `uv sync` at this root can express it (every workspace environment installs every
  member, and sphinx-needs declares sphinx at runtime). `plugin_floor` is deferred.
* `Makefile` and `docs/{Makefile,make.bat}` -- the poe tasks.
* `.pre-commit-config.yaml` -- the root's is the one config, and its `ruff` hook id is
  even spelled differently here (`ruff-check`).
* `.yamlfmt` -- identical intent to the root's.

**Two things retire with NO replacement, and they are named here because nothing else in
the diff says so:**

* **`linkcheck`.** The retired `ci.yaml` ran one. The workspace's `docs.yaml` is scoped
  `paths: [packages/sphinx-needs/docs/**]` and runs in that directory, so **this package's
  documentation is link-checked nowhere** -- in a pull request that repointed three stale
  links. Widening that job is a follow-up, not this import.
* **Three prek hooks**: `end-of-file-fixer`, `trailing-whitespace` and
  `pretty-format-json`. The root's hook set is taplo-format, ruff-check, ruff-format,
  yamlfmt, check-workspace, ty, uv-lock and check-readme -- there is no `pre-commit-hooks`
  repository in it at all.
* `.gitignore` -- the root's covers all of it. Nothing was merged upward: its only
  entries the root lacks are tox and editor leftovers (`.tox/`, `{envlogdir}`,
  `.directory`, `.VSCodeCounter`), and one that would be actively wrong here, `uv.lock`.
* `CONTRIBUTING.md` -- this repository's contributing guide is
  `packages/sphinx-needs/docs/contributing.rst`, which the root README links. Every
  command in the retired file (`nox`, `make`, `uv sync --all-groups --all-extras`) is
  wrong in a workspace; the last of those three is documented in `AGENTS.md` as a thing
  that ALWAYS fails.

**Kept**, deliberately: `AUTHORS` (three named contributors -- nothing else in this
workspace records them, and a credits file deleted in an import is the kind of thing
nobody notices until someone does), `LICENSE` and `README.rst`, which every member has.
Two halves of one commit, and they cannot be separated: the `ty` prek hook fires on any
commit touching `packages/*/src/`, so a lint-only commit would be gated by a type check
that had not been done yet.

**ruff** -- the member's own `[tool.ruff.lint]` left with the manifest (check (7) refuses it,
because a member table REPLACES the root's configuration rather than extending it), so the
root's set applies here for the first time. 56 findings: 33 fixed by `--fix` (import sorting,
which appears only now that the root's isort roots name this package, plus
`UP006`/`UP035`/`UP037`), and 23 by hand:

* **`UP031` x3, deleted rather than rewritten** -- all three were
  `plantuml = "java -jar %s" % os.path.join(cwd, "utils/plantuml.jar")` in the `custom_tr_*`
  test projects, naming the jar the import removed. They were dead BEFORE the move: `cwd` is
  `os.getcwd()`, which under pytest is the rootdir, where no `utils/` has ever existed.
* **`RUF043` x9** -- `pytest.raises(match=...)` patterns containing metacharacters, now raw
  strings; every pattern was already being read as a regex and none changed.
* `SIM118` x5, `RUF015` x2, `UP007`, `RUF059`, `RUF028` (a `# fmt: skip` inside an `if (...)`
  expression, which suppressed nothing), and `RUF001` on the deliberate Cyrillic in the
  koi8-r fixture -- whose reason had to move OUT of the comment, or `RUF003` flags the
  comment for containing the same character.

**No root per-file-ignore was needed**, unlike sphinx-codelinks. What the workspace set does
not carry that this package's did -- `FURB`, `PERF`, `PGH`, `PIE`, `SLF` -- is at zero
violations today, which is exactly why that loss is recorded in the changelog and the
package's `AGENTS.md` rather than left to a reader of the manifest diff.

**ty** -- 62 diagnostics over 9 files, now zero, and almost none of it by suppression:

* **~45 of them had ONE cause.** `TestCommonDirective.__init__` set eleven attributes to a
  bare `None`, so every `self.results[0]`, `for x in self.results`, `self.extra_options[k]`
  and `add_need(title=self.test_name, ...)` in the three directive subclasses was being
  checked against `None`. They are now declared with the type each actually holds.
  `test_file` and `test_id` start empty rather than `None`, and the two guards that enforced
  their presence became falsiness checks. For `test_id` that is identical behaviour; for
  `test_file` it is a fix, described below, and
  `test_file_given = self.test_file[:]` stops being a crash for a directive written with no
  `:file:`.
* **27 more were the three `add_need(**...)` splats**, whose keys come from configuration
  (the report-path field is renameable). Each site now builds one `dict[str, Any]` first,
  which is what a mapping splatted into individually-typed keyword parameters has to be.
* **The sphinx-needs < 4 branch is gone.** The import narrowed this member to
  `sphinx-needs>=8.5.0,<9`, so the `make_hashed_id` fallback could no longer run -- and no
  longer resolves either, the name having left `sphinx_needs.api` entirely (measured on
  8.5.0). `_make_hashed_id` is the only spelling left.
* **TWO latent crashes fixed.** In `test_env.py` the `:raw:` + `:env:` branch iterated
  `self.data_option_list` outside the `is not None` guard its sibling branch keeps it
  inside, so a `test-env` written without `:data:` raised `TypeError`. And giving
  `test_file` an empty default turns a second one into a readable error: the guard that was
  meant to catch a directive written without `:file:` was DEAD CODE, because
  `prepare_basic_options` runs before `load_test_file` in all four directives and
  `test_file_given = self.test_file[:]` raised `TypeError` on `None` two lines before the
  guard could be reached. The slice is now a no-op and the guard is live, so that mistake
  raises `TestReportFileNotSetError`. The source comment says which of the two guards is
  identical behaviour (`test_id`) and which is a fix (`test_file`).
* **Both come with a regression test**, in a new `tests/test_directive_regressions.py`
  with three new fixture projects. Neither crash had a fixture that could reach it: no
  `test-env` fixture combined `:raw:` and `:env:` without `:data:` (they carried `:data:`
  with `:env:`, the same with `:raw:`, or no options at all), and every `test-file`
  fixture carried a `:file:` -- which is precisely why nothing caught either. Each of the
  two tests was mutation-proved against the code it covers: red on the published form,
  green on the fix. The same module covers the missing-file error path, which no fixture
  reached, and fences the `nodes.Text()` change below -- mutation-proved the same way --
  with a warning filter scoped to docutils' exact deprecation AND to this package's module
  -- without the filter an assertion on the rendered node passes on the old call too,
  which was measured -- plus a positive control on the same pattern constant, so a
  docutils rewording fails loudly instead of disarming the fence.
* `nodes.Text(text, text)` -> `nodes.Text(text)`. A deprecation cleanup, not a bug fix:
  docutils ignores the second (`rawsource`) argument, warns that it is deprecated, and
  removes it only in Docutils 2.0, so the old call works on every supported version. ty
  reported it because the typing floor's docutils stubs already omit the parameter.
* **Four `# type: ignore` comments deleted** -- ty reports nothing at any of them and their
  codes were mypy's. **FIVE narrow `# ty: ignore` remain**, each with its reason: two on
  `schema={"type": ...}`, where sphinx-needs types the parameter as a union of TypedDicts
  (a real API-shape mismatch, worth its own issue); one on the `option_spec` assignment
  docutils types as optional; one on `app.tr_types`, an attribute attached to the
  application object; and one on the `lxml` import, which ships no stubs and which
  `allowed-unresolved-imports` cannot cover because the diagnostic is a "has no member"
  rather than an unresolved module.
* The root gains `pytest`, `pluggy` and `_pytest.**` in `[tool.ty.analysis]
  allowed-unresolved-imports`, exactly as `clang.**` is there: the `typing` environment
  deliberately has no pytest, so this package's pytest plugin cannot resolve the runner it
  plugs into. The plugin itself is still checked.

`error-on-warning = true` means every one of those suppressions must still be suppressing
something -- a stale one fails Lint, which is how the first placement of the
`add_extra_option` ignore was caught: ty reports that error on the argument, not the call.

The suite is unchanged throughout: **420 passed, 1 failed** before and after, the failure
being the pytest-playwright collision the next commit fixes.
**R7.** Three tests here spawned the build command resolved on `PATH`
(`test_env.py`, `test_test_file.py`, `test_test_ctest_file.py`). The bare word is not the
environment under test: from an unactivated shell, or any shell whose `PATH` carries
another Sphinx first, the child process is a different Sphinx and a different sphinx-needs
-- or none, and the test dies in `subprocess` with `FileNotFoundError` rather than
reporting anything about the build. All three failed that way from a bare shell, which is
useblocks/sphinx-test-reports#145.

The fix is the workspace's, not this suite's: `sphinx_needs_testkit.sphinx_build_command`
builds `[sys.executable, "-m", "sphinx", ...]`, the same entry point the console script
wraps. `tests/test_subprocess_fence.py` is the one-line fence, reading SOURCE because there
is no result to read -- a site that spawns the bare word passes in every environment where
`PATH` happens to be right, which is every CI job here. sphinx-mounts carries the identical
file; the helper's own docstring already named this package as its third consumer.

**And the workspace-only failure, which is worth its own paragraph.**
`test_an_inner_pytest_session_leaves_the_model_intact` drives an in-process `pytest.main()`
inside a `pytester` session. In a developer's default `.venv` that process has every
installed plugin loaded, and pytest-playwright -- from the root `js` group, which the
default `dev` group includes -- keeps a module-global soft-assertion scope in its
`pytest_runtest_call` wrapper, so the nested session dies with *nested soft assertion scopes
are not supported*. Every CI cell syncs `--no-default-groups --group test --group sphinx-N`,
where the plugin is absent. **So this suite would have been green in CI and red for anyone
running `poe test-reports`** -- the shape the brief's own gate list warns about. The
innermost session now passes `-p no:playwright`.

That line has to stay a single line, and there is a comment saying so:
`test_an_inner_session_that_fails_to_configure_leaves_it_intact` derives its own fixture
from this one by replacing the literal `str(inner)]) == 0`, which a wrapped call breaks --
measured, it turned that sibling red.

Default `.venv`: **422 passed**, from 420 passed + 1 failed (the collision) + the new fence.
`uv run poe docs-reports` is `-nW --keep-going`, like every docs task here. Five warnings,
each with its own cause.

**The renderer (3).** `docs/conf.py` pointed `plantuml` at `docs/utils/plantuml.jar`, a
second copy of the tool at a second version, which the import removed. It now resolves the
renderer through the chain the whole workspace agrees on -- `PLANTUML_JAR`, then the jar
committed at `vendor/plantuml/` that `vendor/plantuml/pin.toml` names, then a `plantuml`
executable on `PATH` -- written out here rather than imported, exactly as
`packages/sphinx-needs/docs/conf.py` writes it out and for the same reason: a docs build
must not import a test-only member. Of the three EXTENSION packages these are the only docs that render
(13 `needflow` directives across three pages -- sphinx-needs' own docs render far more), so
this is not decorative.

**The shipped template (1), and it is a user-facing bug.**
`src/sphinxcontrib/test_reports/directives/test_report_template.txt` ends with a
`literalinclude` of *itself* by a path with no `src/` in it, so the `src/` move broke it.
That file is not a docs page -- it is the DEFAULT `tr_report_template`, and it ships in the
wheel, so **every project using the default template already emits a literalinclude of a
path that exists only in this package's own checkout**. Verified in the published 2.0.0
wheel. The path is fixed here; the released bug is an issue of its own, because it affects
users and is not this pull request's to fix.

**A nitpick target (1).** `docs/pytest.rst` cross-referenced `apply_test_metadata` with
`:func:`, and these docs run no autodoc, so there is no target to resolve. It was invisible
upstream: Read the Docs' `fail_on_warning` is `-W` without `-n`, and the retired Makefile
passed neither.

Beside those, three links that the move made wrong: the issue tracker in `docs/support.rst`
(now this repository's, with a note to set the `pkg:` label), the theme's `repo_url`, and
the `README.rst` logo, which pointed at `raw.githubusercontent.com/.../sphinx-test-reports`
and would have gone stale the moment that repository was archived.

Finally, `docs/changelog.rst`'s `Unreleased` section loses `:Released: under development`.
That line is not cosmetic: `poe bump` refuses to run at all while the newest `:Released:` is
neither `DD.MM.YYYY` nor `YYYY-MM-DD`, so until it went, this member could not have been
released from this workspace.

    build succeeded.   (0 warnings, -nW --keep-going)
**R11.** The file keeps its name and moves nowhere -- it arrived at
`packages/sphinx-test-reports/.readthedocs.yaml` with the import -- but every path inside it
was relative to the old repository root and is now relative to THIS one, which is what Read
the Docs' monorepo guide requires and what both sibling packages already do.

Three things differ from the siblings, each for a reason:

* **`apt_packages: [default-jdk, graphviz]` stays.** Both SIBLING extensions' configs
  deliberately install none, because those docs draw nothing; these render 13 `needflow`
  directives across three pages. (sphinx-needs' own documentation renders too, and far more
  -- 48 of them -- and its config installs the same two packages. The contrast is with
  sphinx-mounts and sphinx-codelinks, not with every docs build here.) The jar is not
  installed -- `docs/conf.py` resolves the one committed at `vendor/plantuml/`, which is
  outside this package but inside the checkout RTD makes, so the build reaches the network
  for nothing beyond its own pip install.
* **Two `python.install` entries, the sibling first**, as sphinx-codelinks has: a member
  installed alone would resolve `sphinx-needs` from PyPI rather than from the checkout
  beside it, and the build could then pass or fail on code this workspace does not have.
* **The `exit 183` `post_checkout` block**, so that a pull request touching nothing under
  this package cancels the build instead of spending a runner on it. The comment records
  the one input outside the package that this filter deliberately ignores --
  `vendor/plantuml/` -- and why that is safe.

`os` and `tools.python` move to `ubuntu-24.04` / `3.13`, matching both siblings and the
root `.python-version`, from `ubuntu-22.04` / `3.12`. The old `pre_create_environment` job
(`echo "DOT details"; which dot`) was a debugging aid and does not survive.

**The RTD project's own settings are not in this pull request and cannot be**: the
repository URL has to be repointed at `useblocks/sphinx-needs` and `readthedocs_yaml_path`
set to this file, both in the project's admin page, by someone with access (the project's
users are danwos and ub_marco). The pull request body carries that checklist.
**R12.** One more pytest step and one more Codecov upload in the Extensions cell, in the
sphinx-codelinks shape, plus the fourth `codecov.yml` project status at `target: auto` --
this package arrived with an import and its coverage number has never been measured here.
`reports.xml` joins `mounts.xml` and `codelinks.xml` in `.gitignore`: the cell runs three
suites in one working directory, so each names its own report or the uploads all carry the
first one's numbers under different flags.

**No new tool installs.** Nothing in this suite renders a diagram, so the graphviz and
PlantUML the cell already installs for sphinx-mounts are not for it, and it needs no
libclang and no browser. The marginal cost is the ~14 s the suite takes, on a 1-3 minute
job, times four cells.

`--cov=sphinxcontrib.test_reports` is the IMPORT name, which for this member is not the
distribution name with `-` -> `_`: it installs into the `sphinxcontrib` PEP 420 namespace.

**A NEW job, `toolchain-free`.** The converter and the pytest plugin are meant to run where
the documentation toolchain is not installed -- that is why Sphinx and sphinx-needs are an
extra of this package rather than dependencies -- and the retired noxfile's `toolchain_free`
session was the only thing keeping that true. It cannot become a matrix cell, and the reason
is structural rather than a configuration gap: the root's `[project] dependencies` name every
member, those are installed in EVERY environment, and sphinx-needs declares sphinx at
runtime, so every environment a `uv sync` at this root can produce has Sphinx in it. The job
therefore builds its environment outside the project with `uv pip install --no-sources`,
exactly as `release.yaml`'s compat cell does, **asserts that sphinx, sphinx_needs and
docutils are all absent** -- without which the job would pass while testing nothing it claims
to -- and runs the ten toolchain-free modules with `-m "not toolchain"`. ~20 lines, ~40 s.
`plugin_floor`, the noxfile's other lane, is deliberately not here; it belongs with the
release that makes the plugin a shipped surface of its own.

**A `Docs reports` job**, in the `docs-codelinks` shape plus the renderer these docs need
(graphviz, and `PLANTUML_JAR` pointed at the vendored jar). R9 and R11 took this build from
five warnings to zero, and without a job nothing holds that: the Read the Docs build is not
a required check, it is `-W` without `-n`, and that project's settings do not point at this
repository yet. sphinx-mounts has no docs job either, so this is a deliberate departure from
that precedent rather than an oversight -- the difference is that these docs RENDER, which
is the failure mode a build catches and review does not.

The job's PlantUML step is the workspace's one capture shape -- `shell: bash`, `tr -d` on
the capture, the export on the next line -- and **`tools/tests/test_ci_plantuml_steps.py`
counts it**: that fence hard-codes how many CI steps run `fetch_plantuml.py` and says adding
a rendering job means raising the numbers deliberately, "so that the addition is a decision
someone made rather than a step nobody checked". This is that decision: eight sites, seven
captures. (Both counts' prose was already one ahead of the constants -- the comment said
seven captures while asserting six, and a docstring already called the Lint check "the
eighth site" -- so after the bump the text and the numbers agree for the first time.)

**And a fix to a hole that predates this pull request.** The Lint job's type-gate canary --
the step that plants a deliberate type error per package and demands the failure name the
probe, because a stale `[tool.ty.src] include` makes ty check zero files and exit 0 --
probed only `sphinx-needs` and `sphinx-mounts`. **`sphinx-codelinks` was already missing
from it**, so the gap was one package wide before this import and would have been two after.
Both are added. It is three tokens on one line, and it is the difference between the gate
being checked and being believed.
…t cell its extras

**R13, two defects with one fix each. Both are invisible until a release job runs**, which
is why they are fixed now rather than found at the worst possible moment.

**1. `module=${DIST//-/_}` derives a module that does not exist.** For this member that is
`sphinx_test_reports`, and the package installs into the `sphinxcontrib` PEP 420 namespace:
the real import name is `sphinxcontrib.test_reports`, declared in `[tool.flit.module]`. The
`import_check` walk would have failed with `ModuleNotFoundError` on every release of it. The
step now reads that key and falls back to the derivation for the three members that declare
no such table -- `check_workspace.Member.module` and `import_check.py` have both honoured it
from the start, so this brings the third reader into line. The comment it replaces predicted
this member by name. Verified against all four manifests: `sphinx_needs`, `sphinx_mounts`,
`sphinx_codelinks`, `sphinxcontrib.test_reports`.

**2. The compat cell installs the wheel with no extras, and 2.0.0 put Sphinx in one.**
Measured on the built wheel: `Requires-Dist: lxml`, full stop -- both Sphinx and sphinx-needs
sit behind the `sphinx` extra, so that the converter and the pytest plugin install without a
documentation toolchain. Run by hand, the cell fails the `import_check` walk
`6 of 25 modules ... No module named 'sphinx_needs'`.

The workflow already has exactly the right mechanism and needed no change for this half: a
per-member `compat-requirements.txt`, installed with `-r` before the wheel and passed again
as a `-c` constraint. Both siblings use it, each with a comment explaining why theirs exists;
this one explains what is different about this member -- it is the first here whose RUNTIME
dependencies are optional, so the file covers the package's own requirements rather than a
test-only need.
**R15.** Four lists, one new file and one shim.

* **`.github/labeler.yml`** and **`.github/issue-labeler.yml`**, plus the "Package" dropdown
  of both issue forms. These go together and cannot be split: the issue labeler runs with
  `sync-labels: 1`, so an option offered by a form with no pattern behind it means the label
  is SYNCED AWAY rather than merely not added. The comment about no two names containing
  another as a `\b`-delimited word is restated for four names, since it is an invariant
  someone has to re-check when a fifth arrives.
* **The root `README.md` table gains TWO rows**, not one: **sphinx-codelinks was already
  missing from it**, so the table has listed two of three packages since that import. It now
  lists four of four.
* **`packages/sphinx-test-reports/AGENTS.md`**, the package delta, with `CLAUDE.md` as the
  usual one-line shim. It carries the paragraph **R14 requires**: `check_workspace.py` check
  (5) prints no line at all for this member, for two independent reasons — a dotted module
  name that `module_version()` can never resolve, and no `__version__` literal anywhere,
  which the check treats as "not an error" by design. Silence that nobody wrote down is the
  failure mode; a documented gap is not. It also records what the move COST -- the
  sphinx-needs axis, five ruff families, `plugin_floor`, and the theme still being
  sphinx_immaterial -- none of which is visible in a diff.
* **The root `AGENTS.md`** gains the member's row, its two commands, its RTD path, the
  renderer facts (the suite needs none; the docs need both -- of the three extension
  packages, the only docs that render, though sphinx-needs' own render far more), the
  `pkg:` label, and the correction from "all three packages ship a `tests/__init__.py`" to
  four. The history section now names three imports rather than two, and says that this
  one's commit map records the stripped jars.
**R16.** The entry says what the import COSTS, not merely that the package moved, because
none of it is visible in a diff and the CI diff is a poor place to learn it.

* **The Sphinx-Needs axis goes from five tested versions to one.** The retired noxfile ran
  6.0.1, 6.3.0, 7.0.0, 8.0.0 and 8.5.0; the workspace tests against the sibling in the tree,
  varying the Sphinx series instead. The declared floor narrows with it, from
  `sphinx-needs>=6.0.1` to `sphinx-needs>=8.5.0,<9`. Users on an older Sphinx-Needs keep
  2.0.0; they do not silently get a package that no longer supports them.
* Five ruff families, plus four more rules the shared set ignores (`B904`, `ICN001`,
  `ISC004`, `N818`); `plugin_floor`; **`linkcheck`, which retires with no replacement**
  because the workspace's link-check job is scoped to Sphinx-Needs' own docs; three
  formatting hooks; and mypy's fifteen-module `exclude`.

**The shipped-template bullet does not claim a user-facing fix, because there is not
one.** `literalinclude` resolves against the document doing the including, which for a
user is somewhere in their own docs tree -- so no path written in the shipped template can
be right for both that user and this repository. What this pull request fixed is this
package's own documentation build. The bullet says exactly that, says the self-include is
tracked separately, and points at the workaround: copy the template, which is what
`tr_report_template` is for, and remove the `literalinclude` directive at its end -- both
of its lines, or the whole Template section, since removing only the first leaves a stray
`language: rst` field list. An earlier draft of this entry said "the path is fixed", which
would have told users a bug was closed that is still open for them.

The entry also carries the three bug fixes this pull request does make -- the `test-env`
branch that raised `TypeError` without `:data:`, a directive written without `:file:` that
raised `TypeError` instead of a readable error, and this repository's own template path --
and one deprecation cleanup, the second argument to docutils' `Text()`, which is
deliberately NOT filed as a bug: docutils ignores that argument and removes it only in
Docutils 2.0, so on every supported version the old call works. The entry also gives the
four facts a user of the old repository needs: the new repository, where issues go, the
prefixed tags, and that **the published package does not change** (same distribution, same
import name, same command).

`poe docs-reports` still builds clean with it.
The Extensions windows-latest cell, this suite's first run on Windows, failed two tests in
`tests/test_project_config.py::TestLoader`. Both are POSIX assumptions in tests added with
useblocks/sphinx-test-reports#145, whose CI only ever ran on ubuntu-latest. Neither is a product
bug:

* `test_unreadable_file_is_a_config_error` makes its file unreadable with `chmod(0o000)`. On
  Windows `os.chmod` only sets the read-only attribute, so the file stays readable and nothing
  raises ("DID NOT RAISE TomlConfigError"). Its existing skip covered only root on POSIX; it now
  also skips on win32, with that reason. The behaviour it covers, wrapping an `OSError` from the
  open as a `TomlConfigError`, is platform-independent code.
* `test_absolute_paths_stay_untouched` wrote `tmp_path` into a TOML basic string. A Windows path's
  backslashes are escape sequences there -- `\U` starts a unicode escape -- so the file was invalid
  TOML ("Invalid hex value (at line 2, column 16)"), which the loader correctly reported. The value is
  now a TOML literal string, which processes no escapes, so the test checks the same thing on every
  platform.

Measured locally: a Windows-shaped path in a basic string reproduces CI's exact `TOMLDecodeError`,
and the same path in a literal string parses equal to itself. The module passes 78 and the package
suite 426 on macOS; the Windows cell is the proof for both changes.
…st-reports (#1931)

> **Merge with "Create a merge commit"** — not squash. The whole point
is the preserved history,
> and `merge_commit_title` is already *Pull request title*, so master's
first-parent line reads
> this title with no hand-editing. `allow_merge_commit` is `false`
today; the steps are under
> *Before merging* below.

`useblocks/sphinx-test-reports` moves into this workspace as
`packages/sphinx-test-reports/`,
with its history, its ten release tags and its release path. It is the
fourth distribution here
— and the first whose runtime dependency on a sibling is **optional**,
which is where most of the
interesting parts below come from.

### What the import is

The whole of that repository's `master` — **161 commits, tip `ca74b80`**
(🚀 Release v2.0.0) —
rewritten by `git filter-repo` so that every historical commit already
places its files under
`packages/sphinx-test-reports/`. 227 files in the merge, and nothing
outside the package.

- **Tags.** Its ten releases arrive prefixed,
`sphinx-test-reports-v0.3.6` …
`sphinx-test-reports-v2.0.0`. The prefix is load-bearing: **six of the
ten bare names — `1.0.1`,
`1.0.2`, `1.1.0`, `1.2.0`, `1.3.0`, `2.0.0` — are existing sphinx-needs
releases**, and
`release_plan.py` reads every tag without `-v` in it as a sphinx-needs
release.
- **A scoped commit-message rewrite.** Bare issue numbers became
`useblocks/sphinx-test-reports#N`,
150 of them, bounded to that repository's own number range, 1 to 161.
**Exactly three references
are deliberately left bare**: #1468, #1527 and #1611, all in one commit
(*build: require
sphinx-needs 6.0.1 and Sphinx 7.4*), are **sphinx-needs** issues — ⬆️
Drop Python 3.9, 👌 Add
schema to `add_extra_option`, ✨ Add `needs_fields`. Left bare they
autolink here to exactly the
issues they mean; a lazy rule would have mangled all three into
references that do not exist,
which was measured by running it. The merge commit's own message was
written by hand after
filtering, so the rule never reached it: every sphinx-test-reports
reference in it is written out
  in full for the same reason.
- **The commit map**, at
`packages/sphinx-test-reports/design/import-commit-map.txt`. Ten
released
versions' GitHub Releases, changelog entries and PyPI page are full of
the old hashes; the old
repository is archived rather than deleted, and this file carries them
across.
- **One deliberate departure from a byte-for-byte copy — the PlantUML
jars.** The filter also
stripped **three jar blobs, 19.97 MB**, from every historical tree.
Unstripped the import adds
**+18.75 MiB** of objects to a repository whose whole object set is
~101.6 MiB; stripped,
**+1.07 MiB**. This workspace already vendors one PlantUML jar, at
`vendor/plantuml/`, under a
  `verify-plantuml` gate.

**The imported tree differs from the source by exactly those jars**, and
that is checkable. Run it
against the **import commit**, `918f3cd3`, which is what the claim is
about:

  ```bash
git clone https://github.com/useblocks/sphinx-test-reports.git /tmp/str
  mkdir -p /tmp/a /tmp/b
  git -C /tmp/str archive ca74b80 | tar -x -C /tmp/a
  git archive 918f3cd:packages/sphinx-test-reports | tar -x -C /tmp/b
  diff -rq /tmp/a /tmp/b
# Only in /tmp/a/docs: utils <- contained plantuml.jar, nothing else
  # Only in /tmp/a/tests/doc_test/utils: plantuml.jar
  ```

At HEAD the same command prints about sixty lines, because the ten
commits after the merge change
the package — that is the point of them, and each is described below. (A
reviewer of this branch
also compared **all 161 imported trees** against the source by mode and
blob sha, not just the
  tip: zero mismatches.)

### How to read the history afterwards

`git log packages/sphinx-test-reports/<path>` works **with no
`--follow`** — filter-repo rewrote the
paths. `git blame` needs nothing, and `git log --first-parent master`
shows the import as one line.

### If you have an open pull request in the old repository

**Once this pull request is merged**, its commits move across with their
authorship. Not before: until then `master` here has no
`packages/sphinx-test-reports/`, and the cherry-pick below stops with
modify/delete conflicts — measured, on useblocks/sphinx-test-reports#68.
The property that makes it work is that
filter-repo's rewrite is deterministic: filtering your branch with
**exactly** the rules below puts
its history on the very commit this import merged, so only your own
commits need applying.

```bash
# clone pr-move NEXT TO your checkout of useblocks/sphinx-needs: the second half reaches it as ../pr-move
git clone https://github.com/useblocks/sphinx-test-reports.git pr-move && cd pr-move
git fetch origin '<your-branch>:pr-branch'
# for a fork: git fetch https://github.com/<you>/sphinx-test-reports.git '<your-branch>:pr-branch'
printf '%s\n' 'regex:(?<![\w/])#([1-9]|[1-9][0-9]|1[0-5][0-9]|16[01])\b==>useblocks/sphinx-test-reports#\1' > ../rules.txt
uvx git-filter-repo --to-subdirectory-filter packages/sphinx-test-reports \
    --tag-rename ':sphinx-test-reports-v' --replace-message ../rules.txt \
    --path-glob '*.jar' --invert-paths --force
# then, in your checkout of useblocks/sphinx-needs, beside pr-move:
git remote add prmove ../pr-move
git fetch prmove master pr-branch
git checkout -b <new-branch-name> origin/master   # a name not already used here, e.g. str-pr-68
git rev-list --reverse --no-merges "$(git merge-base prmove/master prmove/pr-branch)..prmove/pr-branch" \
    | git cherry-pick --stdin
```

Three details differ from the sphinx-codelinks import's recipe, and each
was learned by running it:

- **`--path-glob '*.jar' --invert-paths` is not optional.** This import
stripped the jars, so a
filter without the strip produces a different history: measured,
`master` comes out as
`206c6494` instead of `ffdf962e`, and nothing you cherry-pick lands on
the imported commits.
- **The old repository's default branch is `master`**, and the branch is
fetched as `pr-branch`,
so that a fork whose branch is itself called `master` does not collide
with the clone's own. For
the same reason the branch you create here takes a **new** name: every
sphinx-needs checkout already
has a local `master`, and `git checkout -b master` there stops with
`fatal: a branch named 'master'
  already exists`.
- **`--no-merges`.** A plain `git cherry-pick <base>..<branch>` stops at
the first merge commit in
your branch. Dropping merges is safe only when they resolved nothing:
check with
`git show --cc <merge>`, which prints a diff only for hunks resolved by
hand. If any merge in your
branch did resolve a conflict, **rebase your branch onto `master` in the
OLD repository first**,
  which linearises it, and then move it.

**Proven end to end on useblocks/sphinx-test-reports#68**, the one open
pull request from an
outside contributor — a fork branch named `master`, one commit plus one
merge of the upstream
branch, and behind `master`. The merge was trivial (`git show --cc` is
empty); the move applied
**one commit with zero conflicts**, following the file's rename into
`src/` on its own; **the
original author is preserved** on it (the committer becomes whoever runs
the recipe); and the net
change of each of its three files is byte-identical to the pull
request's. On the moved branch
`ruff check`, ty, its own seven tests, the whole package suite and the
subprocess fence are all
green. The one thing it needs is a `ruff format` pass over
`tests/test_tr_link.py`.

useblocks/sphinx-test-reports#68 is still mergeable in the old
repository, but that repository's `master` has not moved from
`ca74b80` since this import was cut, so **merging it there now would
leave it out of this import**.
Moving it with the recipe is the route.

Then resolve anything this import changed under your branch, and push.
The list, most likely
first:

- **formatting and lint** — the workspace's `ruff format` and `ruff
check` now apply to this
package (on useblocks/sphinx-test-reports#68, one file needed formatting
and nothing else);
- **the member's `pyproject.toml`** — `[dependency-groups]`,
`[tool.ruff.lint]`,
`[tool.pytest.ini_options]` and `[tool.mypy]` no longer exist there, and
there is no `test` extra;
  a new test dependency goes in the ROOT `test` group;
- **files this import retired** — `noxfile.py`, `Makefile`, `.github/`,
`.pre-commit-config.yaml`,
`.yamlfmt`, `CONTRIBUTING.md` — which a branch that edits them hits as a
modify/delete conflict;
- **the directive modules the type-checking port rewrote**, above all
`directives/test_common.py`;
- **a test that spawns `sphinx-build`** must build its argv with
`sphinx_needs_testkit.sphinx_build_command`, or the subprocess fence
fails.

A file that moved under `src/` is not on this list: the cherry-pick
follows the rename.

Two further pull requests there come from maintainers and **already
conflict with `master`** in the
old repository: useblocks/sphinx-test-reports#117 (mypy fixes, 29
commits, 21 files — much of what
it touches is retired or re-done here with ty) and
useblocks/sphinx-test-reports#63 (the JUnit
`error` attribute). Whether to move or close them is left to their
authors.

### A warning for anyone grepping the diff

**`tests/doc_test/utils/*.xml` contain the literal string
`sphinxcontrib/`** — for example
`file="sphinxcontrib/test_reports/junitparser.py"`. They are **fixture
payloads** describing a
historical pytest run: test data, not paths anything opens. A
`src/`-move `sed` would corrupt them
silently. They are untouched, and their 23 files are byte-identical to
the import.

### What changed beyond the move

One commit each.

1. **Wiring and the relock** — the one `--no-verify` commit, and it has
to be: `members =
["packages/*"]` discovers the directory the moment the merge lands, so
`uv sync --frozen` and three
hooks fail together until the lock catches up. The manifest is truncated
to what a member may own;
`requires-python` becomes `>=3.11,<4`; both `sphinx-needs` specifiers
become `>=8.5.0,<9`. **The
lock gains one name, one entry, zero third-party distributions and zero
bytes to download.** This
commit also adds the commit map, and deletes 22 `sys.path.insert(…
"../../sphinxcontrib")` lines
from fixture `conf.py` files: the `src/` move is what made that path
false.
2. **Retirements.** The noxfile, Makefiles, standalone CI, pre-commit
and yamlfmt configs,
`.gitignore`, `CONTRIBUTING.md`. `AUTHORS` is kept. **Two things retire
with no replacement**, and
are named because nothing else in the diff says so: `linkcheck` (the
workspace's `docs.yaml` is
scoped to sphinx-needs' docs) and three prek hooks (`end-of-file-fixer`,
`trailing-whitespace`,
   `pretty-format-json`).
3. **ruff + ty.** 56 ruff findings and **62 ty diagnostics, now zero**,
ported here rather than
excluded, because an unchecked member is invisible. ~45 of the 62 had
one cause: eleven directive
attributes initialised to a bare `None`. **Five** narrow `# ty: ignore`
remain, each with its
reason. The regression tests below land here, beside the fixes they
cover.
4. **The subprocess fence.** Three tests spawned `sphinx-build` resolved
on `PATH`; they now go
through `sphinx_needs_testkit.sphinx_build_command`, with the one-line
fence sphinx-mounts carries.
**And a failure that would have been green in CI and red for every
developer**: an in-process
`pytest.main()` collides with pytest-playwright in the default `.venv`,
which no CI cell has.
   Fixed with `-p no:playwright` on the inner session.
5. **Docs** — clean under `-nW --keep-going`, from five warnings.
6. **Read the Docs config**, re-rooted at the repository.
7. **CI** — one pytest step and one Codecov upload in the Extensions
cell (flag `reports`); a new
**`toolchain-free`** job, the fence that keeps the converter and the
pytest plugin importable with
no documentation toolchain (it cannot be a matrix cell, because every
workspace environment
installs every member and sphinx-needs declares sphinx at runtime); and
a new **`Docs reports`**
job, since nothing else would hold the zero-warning build. That job's
PlantUML step is counted by
`tools/tests/test_ci_plantuml_steps.py`, whose site counts go 7 → 8 and
6 → 7 — the fence asks for
   exactly that decision to be made explicitly.
8. **The release workflow.** The compat cell installs a bare wheel whose
`Requires-Dist` is `lxml`
alone, so this member gets a `compat-requirements.txt`; and
`module=${DIST//-/_}` derived a module
that does not exist, so the step now reads `[tool.flit.module] name`.
**Exercised by hand**:
without the file the import walk fails 6 of 25 modules; with it, 25 of
25 and the suite passes
   against published dependencies.
9. **The lists that name the packages**: labeler, issue labeler, both
issue forms, the root README
(which was **already missing sphinx-codelinks** — it now lists four of
four), and `AGENTS.md`.
10. **The changelog.**
11. **Two loader tests made portable to Windows**, after CI's first
Windows run of this suite (see below).

### Three bug fixes, one deprecation cleanup, and one thing that is NOT
fixed

- 🐛 **A `test-env` with `:raw:` and `:env:` but no `:data:` raised
`TypeError`** — one branch
iterated outside the guard its sibling keeps it inside. No fixture had
that shape.
- 🐛 **A `test-file` written without `:file:` raised an unhandled
`TypeError`.** The guard meant to
catch it was dead code, reached only after a slice of the missing value.
It now raises
  `TestReportFileNotSetError`. No fixture had that shape either.
- 🐛 **This repository's own docs build** could not resolve the report
template's `literalinclude`
  after the `src/` move.
- 👌 **`nodes.Text(text, text)` → `nodes.Text(text)`.** Not a bug:
docutils ignores the second
  argument, deprecates it, and removes it only in Docutils 2.0.
- ⚠️ **The shipped default report template's self-include is still
broken for users, and this pull
request does not fix it.** `literalinclude` resolves against the
*including* document — for a
user, somewhere in their own docs — so no path written in the shipped
file can be right for both
them and this repository. Filed as #1932. Until it is fixed a project
can copy the template and remove the
`literalinclude` directive at its end — **both** lines, or the whole
*Template* section; removing
  only the first leaves a stray `language: rst` field list.

**Each of the three code changes above — the two crashes and the
`Text()` cleanup — comes with a
mutation-proved regression test**, in a new
`tests/test_directive_regressions.py` with three new
fixture projects: red on the published code, green on the fix. The
fourth item, the docs build's own
template path, is not a pytest; the `Docs reports` job holds it, and it
fails on the unfixed path.
The `Text()` test needed a warning filter scoped to docutils' exact
deprecation message **and** to
this package's module — without it an assertion on the rendered error
node passes on the old call
too, which was measured — and a positive control on the same pattern
constant, so that a docutils
rewording fails loudly instead of disarming the fence.

### Two pre-existing gaps fixed on the way past

- **The Lint job's type-gate canary probed only two of the packages.**
`sphinx-codelinks` was already
missing, so the hole was one package wide before this import. Both are
now probed.
- **The root README table was missing `sphinx-codelinks`.**

### What this costs, stated rather than left to the CI diff

- **The sphinx-needs axis goes from five tested versions to one**, and
the declared floor narrows
  with it, from `sphinx-needs>=6.0.1` to `>=8.5.0,<9`. Accepted.
- **Five ruff families** (`FURB`, `PERF`, `PGH`, `PIE`, `SLF`) are no
longer enforced, and four more
rules this package enabled are ignored by the shared set (`B904`,
`ICN001`, `ISC004`, `N818`). All
  nine are at zero violations today.
- **`linkcheck`** and three formatting hooks retire with no replacement.
- **`plugin_floor`** has no replacement yet.
- The docs keep `sphinx_immaterial`; converging on furo has its own
issue.

### One effect on a sibling suite, which pass counts alone would hide

Putting `sphinx-design` in the shared `test` group moves one
**sphinx-needs** test from **skipped to
passing**:
`test_needreport.py::test_sphinx_design_dropdown_output_is_unchanged`,
guarded by
`skipif(not SPHINX_DESIGN_INSTALLED)`. The direction is good; it is
stated because a pass count
cannot show a skip becoming a pass.

| | before this branch | this branch |
|---|---|---|
| `poe test-needs` | 1804 passed, **12 skipped** | 1805 passed, **11
skipped** |
| `poe test-mounts` | 944 passed, 2 deselected | 944 passed, 2
deselected |

### Windows

Everything was measured locally on macOS and three Linux-shaped cells,
so the Extensions
`windows-latest × sphinx-9` cell was this suite's first run on Windows.
The old repository's CI ran on
`ubuntu-latest` only. That first run failed two tests in
`tests/test_project_config.py::TestLoader`,
both added with useblocks/sphinx-test-reports#145, and **both test
defects rather than product bugs**:

- `test_unreadable_file_is_a_config_error` makes its file unreadable
with `chmod(0o000)`. On Windows
`os.chmod` only sets the read-only attribute, so the file stays readable
and nothing raised. It now
  skips on Windows, as it already did for root on POSIX.
- `test_absolute_paths_stay_untouched` wrote `tmp_path` into a TOML
basic string, where a Windows path's
backslashes are escape sequences, so the file was invalid TOML — which
the loader correctly reported.
It now uses a TOML literal string and checks the same thing on every
platform.

Fixed in `d68d10df`. On it the Windows cell's sphinx-test-reports step
reports:

| run | passed | skipped | failed |
|---|---|---|---|
| `f4075034` | 423 | 1 | **2** |
| `d68d10df` | 424 | 2 | 0 |

The three converted subprocess sites, the part expected to be
Windows-sensitive, passed on the first run.

### Gates

Every figure below was measured locally on `f4075034`. The one commit
after it, `d68d10df`, changes two
tests for Windows and nothing else (see *Windows* above), and CI runs
the full matrix on it.

```
poe lint              8/8 hooks             poe test-reports        426 passed
poe typecheck         All checks passed     cells sphinx-7 / 8 / 9  426 passed each
poe docs-reports      0 warnings            pytest tools/tests      313 passed
pytest --collect-only 2175, exit 0          check-workspace         all green
import_check --walk   25 modules            compat cell (by hand)   sphinx-needs 8.5.0 from PyPI,
                                                                    25 modules, 426 passed
mutation proofs       each proof red when its fix is reverted, green restored; the positive
                      control red when its pattern is broken
```

`check_workspace.py` check (5) prints **no line** for this member, by
design and for two
independent reasons (a dotted module name, and no `__version__`
literal); the package's `AGENTS.md`
says so, and the release that renames the module dissolves both.

### Follow-up candidates, not in this pull request

- the shipped template's self-include — #1932;
- converging the docs theme on furo;
- widening `docs.yaml`'s linkcheck to this package;
- `schema={"type": …}` against sphinx-needs' TypedDict union — a real
API-shape mismatch, suppressed
  here with a reason;
- `plugin_floor`;
- three things no gate currently fences: a typo in the labeler glob
(which is what applies
`pkg: sphinx-test-reports` to every future pull request), the
`[project.urls]` values, and the
`toolchain` marker line — removing any of them leaves every gate green.

### Before merging

- [x] Create the label **`pkg: sphinx-test-reports`**, and **apply it to
this pull request by hand**. The
`pull_request_target` labeler reads the default branch's configuration,
which does not know this
package until this merges, so it cannot apply the label here however
early the label exists. It
      must exist before the merge and before any issue is transferred.
- [x] File the follow-up issue for the shipped template's self-include,
carrying that label — #1932.
- [ ] Flip `allow_merge_commit` **on** immediately before the merge,
merge with **Create a merge
      commit**, and flip it **off** immediately after.

### After the merge

- [ ] Push the ten `sphinx-test-reports-v*` tags, **before** creating
any Read the Docs automation
      rule (rules run on version creation).
- [ ] Read the Docs: repoint the project's repository URL at
`useblocks/sphinx-needs` and set
`readthedocs_yaml_path` to
`packages/sphinx-test-reports/.readthedocs.yaml`. **Needs danwos or
      ub_marco.**
- [ ] Transfer the open issues, each with the new label; close
useblocks/sphinx-test-reports#149 with
a pointer here; point the useblocks/sphinx-test-reports#68 contributor
at the recipe above; pin a "this repository has moved" issue; archive
rather than delete the old repository.
- [ ] Delete the superseded `import/sphinx-test-reports` branch.

`Closes` nothing — useblocks/sphinx-test-reports#149 is closed by hand
in the other repository.

---------

Co-authored-by: Daniel Woste <daniel.woste@useblocks.com>
Co-authored-by: saifsayyad <44859469+saifsayyad@users.noreply.github.com>
Co-authored-by: Peter Tillemans <pti@melexis.com>
Co-authored-by: Lasse Benn Nørregaard <lassenorregaard@gmail.com>
Co-authored-by: James Barr <James.Matthew.Barr@gmail.com>
Co-authored-by: Bence Bognar <beni20180722@gmail.com>
Co-authored-by: Martin <54633070+mawieland@users.noreply.github.com>
Co-authored-by: Martin <martin.wieland@gmx.de>
Co-authored-by: Duodu Randy <duodurandy19@gmail.com>
Co-authored-by: haiyangToAI <haiyang.zhang@tutanota.com>
Co-authored-by: Joerg Kreuzberger <j.kreuzberger@procitec.de>
Co-authored-by: Maximilian Sören Pollak <maximilian.pollak@expleogroup.com>
Co-authored-by: Gabriel Fougeron <gabriel.fougeron@hotmail.fr>
Co-authored-by: Nick Touran <ntouran@terrapower.com>
Co-authored-by: Marco Heinemann <marco.heinemann@useblocks.com>
Co-authored-by: korbi-web-215 <149482188+korbi-web-215@users.noreply.github.com>
Co-authored-by: Patrick Dahlke <patrick.dahlke@pichu.io>
Co-authored-by: Christian Polzer <119361+cpolzer@users.noreply.github.com>
Co-authored-by: ~chrstian polzer <~c.polzer@hai-fai.de>
Co-authored-by: Christian Polzer <Christian.Polzer@gmail.com>
Co-authored-by: christian polzer <c.polzer@hai-fai.de>
Co-authored-by: Miro <natsubeloud@gmail.com>
Co-authored-by: Miro <200482516+Mirochill@users.noreply.github.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from
10.0.1 to 10.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/astral-sh/setup-uv/releases">astral-sh/setup-uv's
releases</a>.</em></p>
<blockquote>
<h2>v10.1.0 🌈 New output <code>python-runtime-id</code>and respect
NO_PROXY</h2>
<h2>Changes</h2>
<p>This release adds more bheind the scene security improvements and
also 2 small improvements.</p>
<h3>NO_PROXY</h3>
<p>This action now respects <code>no_proxy/NO_PROXY</code> environment
variables which were previously ignored.</p>
<h3>New output <code>python-runtime-id</code></h3>
<p>The new output <code>python-runtime-id</code> can be used to know
which python version exactly was installed if you use
<code>activate-environment</code>. See <a
href="https://redirect.github.com/pyca/cryptography/pull/15572#discussion_r3913508686">pyca/cryptography#15572</a>
for details on why this can be useful.</p>
<h2>🐛 Bug fixes</h2>
<ul>
<li>fix: respect no proxy directive <a
href="https://github.com/mj0nez"><code>@​mj0nez</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1037">#1037</a>)</li>
<li>Use JSON + a typed wrapper instead of TS codegen <a
href="https://github.com/woodruffw"><code>@​woodruffw</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1025">#1025</a>)</li>
</ul>
<h2>🚀 Enhancements</h2>
<ul>
<li>Expose a Python &quot;identity&quot; output <a
href="https://github.com/woodruffw"><code>@​woodruffw</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1036">#1036</a>)</li>
<li>Verify downloads with astral-sh/versions checksums <a
href="https://github.com/zaniebot"><code>@​zaniebot</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1033">#1033</a>)</li>
</ul>
<h2>🧰 Maintenance</h2>
<ul>
<li>chore: update known checksums for 0.12.12 @<a
href="https://github.com/apps/github-actions">github-actions[bot]</a>
(<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1041">#1041</a>)</li>
<li>chore: update known checksums for 0.12.10/0.12.11 @<a
href="https://github.com/apps/github-actions">github-actions[bot]</a>
(<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1038">#1038</a>)</li>
<li>chore: update known checksums for 0.12.9 @<a
href="https://github.com/apps/github-actions">github-actions[bot]</a>
(<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1035">#1035</a>)</li>
<li>chore: update known checksums for 0.12.7/0.12.8 @<a
href="https://github.com/apps/github-actions">github-actions[bot]</a>
(<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1031">#1031</a>)</li>
<li>chore: update known checksums for 0.12.6 @<a
href="https://github.com/apps/github-actions">github-actions[bot]</a>
(<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1030">#1030</a>)</li>
<li>chore: update known checksums for 0.12.5 @<a
href="https://github.com/apps/github-actions">github-actions[bot]</a>
(<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1020">#1020</a>)</li>
<li>Use self-repo syntax for all in-repo actions/reusable workflows <a
href="https://github.com/woodruffw"><code>@​woodruffw</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1024">#1024</a>)</li>
<li>Pin one-shot tools <a
href="https://github.com/woodruffw"><code>@​woodruffw</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1022">#1022</a>)</li>
<li>ci: remove obsolete direct push attempts <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1019">#1019</a>)</li>
</ul>
<h2>📚 Documentation</h2>
<ul>
<li>docs: update version references to v10.0.1 @<a
href="https://github.com/apps/github-actions">github-actions[bot]</a>
(<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1018">#1018</a>)</li>
</ul>
<h2>⬆️ Dependency updates</h2>
<ul>
<li>chore(deps-dev): roll up Dependabot updates <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1043">#1043</a>)</li>
<li>Harden npm install defaults <a
href="https://github.com/zaniebot"><code>@​zaniebot</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1026">#1026</a>)</li>
<li>Add dependency cooldowns <a
href="https://github.com/woodruffw"><code>@​woodruffw</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1021">#1021</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/bec219d24cd3e171d82865faccec33120bb574f4"><code>bec219d</code></a>
chore(deps-dev): roll up Dependabot updates (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1043">#1043</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/b90ec40d15bfa44c33c6700196eb6efcdddb4373"><code>b90ec40</code></a>
fix: respect no proxy directive (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1037">#1037</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/421feb646df5262e7dd93bc54161edfa30372417"><code>421feb6</code></a>
chore: update known checksums for 0.12.12 (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1041">#1041</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/f634bf473ad85bf3e23a613f52c5fa9f363874fc"><code>f634bf4</code></a>
Expose a Python &quot;identity&quot; output (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1036">#1036</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/a6772c8f0a09dc9e3582c70a994b0c55af921803"><code>a6772c8</code></a>
chore: update known checksums for 0.12.10/0.12.11 (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1038">#1038</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/e105c8fb1d7b13074b851babdaef4185243c6a07"><code>e105c8f</code></a>
chore: update known checksums for 0.12.9 (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1035">#1035</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/cd13f9217092d43a771cf9ba7b09bdd3da8d7c4d"><code>cd13f92</code></a>
Verify downloads with astral-sh/versions checksums (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1033">#1033</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/3aef7b92c52cec135792ea1e95f4c77683d39e61"><code>3aef7b9</code></a>
chore: update known checksums for 0.12.7/0.12.8 (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1031">#1031</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/d08d816a1ea176d61a318eff45abd3dffef415b1"><code>d08d816</code></a>
chore: update known checksums for 0.12.6 (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1030">#1030</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/19b4d1e990bec64818914c40230bde93a0de300b"><code>19b4d1e</code></a>
Harden npm install defaults (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/1026">#1026</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/astral-sh/setup-uv/compare/v10.0.1...v10.1.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astral-sh/setup-uv&package-manager=github_actions&previous-version=10.0.1&new-version=10.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@pull pull Bot locked and limited conversation to collaborators Sep 15, 2026
@pull pull Bot added the ⤵️ pull label Sep 15, 2026
@pull
pull Bot merged commit 687b38f into boschglobal:master Sep 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.