Skip to content

🔧 Import sphinx-codelinks into the workspace as packages/sphinx-codelinks - #1871

Merged
chrisjsewell merged 133 commits into
masterfrom
import/sphinx-codelinks
Sep 6, 2026
Merged

chrisjsewell merged 133 commits into
masterfrom
import/sphinx-codelinks

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Sep 6, 2026 •

Copy link
Copy Markdown
Member

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 pull request's title with no hand-editing.
allow_merge_commit is false today and has to be flipped on immediately before the
merge and off immediately after.

useblocks/sphinx-codelinks moves into this workspace as packages/sphinx-codelinks/,
with its history, its seven release tags and its release path. It is the third
distribution here and the first that depends on another one at runtime.

What the import is

The whole of that repository's main — 110 commits, tip 63adfe50 — is merged in,
rewritten by git filter-repo so that every historical commit already places its files
under packages/sphinx-codelinks/. 207 files, +18 382 lines, and nothing outside the
package. Three things came with it:

  • Tags. Its seven releases arrive prefixed: sphinx-codelinks-v0.1.1 …
    sphinx-codelinks-v1.4.0. Here the prefix is load-bearing rather than hygienic:
    release_plan.py reads every tag without -v in it as a sphinx-needs release, and
    three of the seven bare names — 1.1.0, 1.2.0, 1.3.0 — are existing sphinx-needs
    releases
    . Without the rename the fetch would have moved them.
  • A scoped commit-message rewrite. Bare #N references became
    useblocks/sphinx-codelinks#N, 110 of them on main, bounded to the repository's own
    number range (#1–#106). The one already-qualified cross-repository reference,
    useblocks/ubcode-pub#75, is untouched — the rule's lookbehind is what leaves it alone.
    Every remaining # in the history is a Markdown heading or a quoted shell comment, and
    none is followed by a digit.
  • The commit map, committed as
    packages/sphinx-codelinks/design/import-commit-map.txt. The rewrite gives every commit
    a new hash, and this project has seven released versions whose GitHub Releases,
    changelog entries and PyPI page are full of the old ones. The old repository will be
    archived, not deleted or transferred, so those permalinks keep serving; this file is what
    carries them across.

How to read the history afterwards

git log packages/sphinx-codelinks/<path> works with no --follow — filter-repo
rewrote the paths, so from this repository's point of view those files were always there.
That is the opposite of the sphinx-needs files the 2026-09 restructure moved, which do need
it. git blame needs nothing either way, and git log --first-parent master shows the
whole import as one line.

If you have an open pull request in the old repository

Its commits move across with their authorship, and as cleanly as your branch merges
with main today
. The property that makes it work is that filter-repo's rewrite is
deterministic: filtering your branch with the same rules puts its commits on exactly the
commit this import merged.

Seven of the eight open pull requests conflict with main right now (only #100 does
not), so seven authors will hit a conflict during the cherry-pick — the conflict they
already had, not one the import creates. Rebase your branch onto main in the OLD
repository first
and the move is conflict-free.

git clone https://github.com/useblocks/sphinx-codelinks.git pr-move && cd pr-move
git fetch origin '<your branch>:<your branch>'
# for a fork: git fetch https://github.com/<you>/sphinx-codelinks.git '<branch>:<branch>'
printf '%s\n' 'regex:(?<![\w/])#([1-9]|[1-9][0-9]|10[0-6])\b==>useblocks/sphinx-codelinks#\1' > rules.txt
uvx git-filter-repo --to-subdirectory-filter packages/sphinx-codelinks \
    --tag-rename ':sphinx-codelinks-v' --replace-message rules.txt --force
# then, in a checkout of useblocks/sphinx-needs:
git remote add prmove ../pr-move
git fetch prmove '<branch>:refs/remotes/prmove/<branch>'
git checkout -b <branch> origin/master
git cherry-pick $(git merge-base prmove/main prmove/<branch>)..prmove/<branch>

Then resolve anything this branch's later commits touched, and push. The list, most
likely first:

  • the member's pyproject.toml — [dependency-groups], [tool.ruff*], [tool.ty*]
    and [tool.pytest.ini_options] no longer exist there; the root owns them. A branch that
    added a test dependency puts it in the ROOT test group instead, and a new extra stays
    in the member's [project.optional-dependencies]. This is the one a branch that touches
    the manifest at all will hit;
  • the four __init__.py under src/sphinx_codelinks/;
  • the changelog, which moved from docs/development/change_log.rst to docs/changelog.rst;
  • analyse/utils.py, if your branch touches the git helpers the #106 fix rewrote.

Proven end to end twice: on sphinx-codelinks#100 (11 commits, 153 files, zero conflicts —
the one branch that is current with main) and on #96, a one-commit fork branch four
commits behind, where exactly one file conflicted (pyproject.toml, for the reason above)
and the other 25 applied cleanly, including a rename cherry-pick followed on its own.

What changed beyond the move

The standalone-repository files retire. .github/ (five workflows), .python-version,
.pre-commit-config.yaml, tox.ini, .vscode/ and .gitignore — the root owns every one
of them, and while the member kept a .pre-commit-config.yaml prek treated it as a project
of its own. The three ignore entries the root lacked (invalid_objs.json, output/,
.ty_cache) moved up; uv.lock deliberately did not, because this workspace tracks its
lock. gh_pages.yml goes with the rest even though the Pages site is still live: the old
repository keeps serving it until the DNS moves, so a copy here would be dead weight. From
the manifest, the tables check_workspace.py check (7) refuses — the pre-import work had
already made them copies of the root's, so nothing about lint, types or collection changes
when they go. Added instead: [project.urls], which this distribution has never had.

The root wiring. The member is listed and sourced from the workspace; docs-codelinks
and codelinks-libclang re-export its extras. The shared test group gains toml (its
CLI tests write toml, and tomllib only reads) and furo (two test fixtures set
html_theme = "furo" and one is built by a test — a test dependency here, not a docs one).
The typing group gains types-Pygments and types-jsonschema, without which five of the
package's # ty: ignore comments suppress nothing and the error-on-warning ratchet
reports them as unused. uv lock adds 29 package names / 31 entries, removes nothing and
moves no existing package's version
.

testpaths is deliberately untouched: this package ships a tests/__init__.py like
sphinx-mounts', so adding its path makes the bare root pytest collect nothing rather
than more. The Lint job's "Check a bare root pytest still collects" step already guards it.

Bringing it under the root's ruff cost two import re-sorts, and they are structural.
The root's [tool.ruff] src names packages/sphinx-needs/src, so sphinx_needs is
first-party in this workspace
and isort moves it out of the third-party block. This is
the first member whose source imports it — nothing in packages/sphinx-mounts/src does —
so it is a class of change the pre-import convergence could not have anticipated from
outside. The member's one per-file-ignore entry moved to the root scoped to
packages/sphinx-codelinks/tests/*; as **/tests/* it would have loosened the other
three suites too.

Four empty __init__.py. analyse/, source_discover/, sphinx_extension/ and
sphinx_extension/directives/ were PEP 420 namespace directories, so
pkgutil.walk_packages never reported them and import_check.py — the release gate that
imports every module of the built wheel against its dependencies as published — saw
5 of the package's modules and exited 0. With the four files, and with the new
--extra option that gives the walk the optional engine, it reports 24, which is
every .py under src/sphinx_codelinks/ (the four new files included).

Four latent bugs the old repository's CI shape could not see

1. __version__ had read "0.1.0" since the first commit, through seven releases,
while the manifest went to 1.4.0 — so an installed 1.4.0 reported the wrong version. It is
exported in __all__, so it is public API. check_workspace.py check (5) is what caught
it on the first run here, and poe bump keeps the two in step from now on.

2. locate_git_root found no git root in a linked worktree. It required .git to be a
directory, and get_remote_url/get_current_rev then read <root>/.git/config and
<root>/.git/HEAD directly. In a worktree .git is a file holding gitdir: …, the
remotes live in the main repository's git directory named by commondir, and only
per-worktree state is local. The consequence is worse than the missing links the issue
reports: poe docs-codelinks runs -nW, and this repository is worked in worktrees as a
matter of routine
, so the documentation build failed for every developer while staying
green in CI and on Read the Docs, which use plain clones. Reverting the fix here gives five
[codelinks.git_root] errors and a red build. Two helpers do it, and a plain checkout takes
exactly the path it took before. The regression test builds a real git worktree in both
shapes git makes — detached, where HEAD is a SHA, and a branch, where it is a ref: into
the common directory. Closes sphinx-codelinks#106.

3. libclang was not actually optional for the suite. It is an optional extra at
runtime, and three test modules guarded it; a fourth imported
sphinx_codelinks.analyse.preproc unguarded, and that package imports the libclang loader
eagerly. So an environment without the wheel failed during collection rather than
skipping. The old CI never saw it because its test group always installed libclang. One
pytest.importorskip line, and the suite now degrades honestly: 359 passed with the
engine, 303 passed / 26 skipped without it
— 26 rather than 56, because a module-level
importorskip is one skip per module and never collects the tests inside; 56 is how many
test cases stop running.

And CI asserts the engine is there. extra-groups: codelinks-libclang is the only
executable place that wheel is named, and losing it would be silent: a cell without the
group is green at 303 passed, 26 skipped. So the Extensions cell imports clang.cindex
right after its sync and fails naming the group — the libclang half of the renderer check
the composite already carries, and the same rule the repository already applies to
graphviz and PlantUML.

4. get_current_rev returned None once the refs were packed. It resolved
ref: refs/heads/<x> by looking for a loose ref FILE, and there is none after git gc or
git pack-refs --all — which run unattended on any long-lived checkout. Same red -nW
docs build as #106, reached by a different route, and again invisible to CI and RTD, which
both leave a detached HEAD. It now falls back to packed-refs, with a fixture case that
packs the refs and asserts the sha.

A fifth, smaller one: test_form_https_url passed a filepath relative to the CWD, so
the suite only passed when pytest ran from the package directory. It is fixed rather than
worked around with a working-directory:, which is what lets CI run it from the repository
root like every other cell.

CI

tests-mounts becomes tests-extensions: one cell per sphinx series (ubuntu ×
sphinx-7/8/9, plus one windows-latest cell on sphinx-9) running both extension packages'
suites as steps, each with its own --cov-report=xml:<name>.xml and its own codecov
upload. The names of the checks change and the count does not. One cell for the two of
them rather than a second three-cell matrix because sphinx-codelinks' whole suite is under
20 seconds: as cells of its own it would spend more runner time starting jobs than testing.
The cost is that a failure in the first suite could hide the second, so the second pytest
step runs under !cancelled().

test-package.yaml cannot express two suites and should not learn to — a workflow_call
input is a scalar and a job's steps cannot be generated from data — so test-extensions.yaml
is a second reusable workflow. What would have made that a step backwards is the 57 lines
of prologue the two units share, which is the duplication #1864 existed to remove. So those
move to a composite action, .github/actions/prepare-cell: the renderer installs and
their guards, the Point PLANTUML_JAR at the vendored jar step (#1870's, verifying
vendor/plantuml/ against its pin), the cell sync, the version report and
check_sphinx_cell.py. test-package.yaml is refactored onto it with no change to any
caller's inputs and no change to what a cell does — and test-extensions.yaml needs no
PlantUML wiring of its own, because it already passes needs-plantuml: true and inherits
the verified jar through the composite.

The composite holds run: steps only. dependabot's github-actions ecosystem is what
keeps actions/checkout, astral-sh/setup-uv and codecov/codecov-action pinned and
bumped, and a pin moved inside a composite action stops being seen — so all three stay
written in the two workflows. The one consequence is that setup-uv now runs before the
renderer installs rather than after them; nothing reads across that boundary, and #1870
needed setup-uv first for its own reason anyway (its --verify step runs a python
script), so the two changes converge.

libclang is a dependency group, not part of test. The wheel is 23 MiB and 81 MB on
disk and only 56 of 359 tests use it, so every cell of every package would otherwise pay
for an engine it never calls. The Extensions cell adds --group codelinks-libclang; the
release workflow's compat cell gets it from
packages/sphinx-codelinks/compat-requirements.txt, through the per-member hook the
sphinx-mounts import already added. Without either, the affected tests skip rather than
fail — so both are load-bearing against a quieter green rather than a red.

A new Docs codelinks job builds them on every pull request — one uv sync
and one sphinx-build, mirroring docs.yaml's Docs-Linkcheck idiom rather than going
through poe, which would install the whole dev group as well just to have poe on the
path. The build itself is 7 seconds. Those docs are the one end-to-end exercise of the extension — src-trace runs
over four projects of the package's own source — and nothing else in CI builds them:
docs.yaml is a linkcheck scoped to sphinx-needs' docs, and the Read the Docs pull-request
build is neither a required context nor even run when the package did not change. It is
also what caught the worktree bug. An equivalent for sphinx-mounts is worth having and is
not this pull request's.

codecov.yml gains a codelinks flag with target: auto, as mounts has, until a real
number is known. check gains both new jobs. The bazel lane, tests-core, tests-js,
tests-no-mpl, smoke-needs and concurrency are untouched. Cost: roughly +2 runner
minutes per push
— four Extensions cells each about 20 s longer, plus a Docs codelinks
job of about a minute, of which the build itself is 7 s.

What is lost, and accepted

The old ci.yml ran eight pytest cells: py3.11/3.12/3.13/3.14 across sphinx 7/8/9, two
Windows cells, macOS, and ubuntu-24.04-arm. This repository runs one pinned interpreter
for the extension packages and one Windows cell. macOS, arm64 and the per-interpreter
matrix go
, and that is a real reduction rather than an oversight — it is the shape this
repository already runs for sphinx-mounts, and the alternative is eight more cells on every
push for a package whose suite is nine seconds. The Windows cell is kept deliberately:
this package does path arithmetic, and three of the five latent bugs above are path
bugs: the packed refs, the worktree gitfile and the CWD-relative test parameter.

And the newly lowered Python floor is exercised by nothing. This pull request ships a
changelog bullet announcing "Python 3.11 is now supported; the floor moves down from 3.12"
— and tests-extensions passes no python-version, so every cell runs on the root pin
(3.13). tests-core's 3.11 rows test sphinx-needs only, the release compat cell uses the
pin too, and check_workspace.py compares requires-python strings without executing
anything. That is this repository's existing shape for sphinx-mounts as well, which is why
it is accepted here rather than fixed in this pull request; #1865 is the workspace-level
issue for it.

Also gone: the old Documentation build job, replaced by the Docs codelinks job above,
and the standalone coverage.yaml, folded into the Extensions cell's flag.

Read the Docs

packages/sphinx-codelinks/.readthedocs.yaml is re-rooted — every path in it is relative
to the repository root, which is RTD's documented monorepo rule — and it carries the
post_checkout job that exits 183 when nothing under the package changed. sphinx-mounts'
import deferred that because RTD's commit-status behaviour for a cancelled build was
unverified; it has since been measured (build 34411602: cancelled, and a success status
on GitHub), so this one has it from the start.

docs/conf.py and docs/ubproject.toml named the old repository in five places — furo's
view/edit buttons and the four remote_url_pattern entries src-trace uses. All five now
name this repository, with source_directory = "packages/sphinx-codelinks/docs/". {path}
needed no change: it is computed relative to the git root, so a built page's blob link comes
out as
…/sphinx-needs/blob/<sha>/packages/sphinx-codelinks/tests/data/dcdc/charge/demo_1.cpp#L16.

After the merge

  1. Create the label pkg: sphinx-codelinks — before the MERGE, and before the issue
    transfers in step 3, which cannot apply a label that does not exist. It is not needed
    for this pull request's own CI: label-prs.yaml runs as pull_request_target, so it
    reads .github/labeler.yml from master, which has no pkg: sphinx-codelinks entry
    until this merges — the labeler never tries to apply it here and stays green (it applies
    pkg: workspace, which this pull request earns). By the same token the new
    labeler.yml entry is unproven until the first pull request after the merge. Match the
    existing convention (pkg: sphinx-mounts is #0E8A16, pkg: sphinx-needs #1D76DB,
    pkg: workspace #5319E7), each with a one-line description.

  2. Push the seven tags — git push origin 'refs/tags/sphinx-codelinks-v*'.
    Deliberately after, not before: pushed early they would make the codelinks commits
    reachable while the code is not on master, and release_plan.py would already report a
    third member.

  3. The issues. Nineteen of the old repository's 21 open issues transfer; transfer needs
    write access on both repositories, so it must happen before archiving, and
    pkg: sphinx-codelinks must exist first or it cannot be applied in the same pass. Do
    not pass createLabelsIfMissing — every label those issues carry already exists
    here, so they survive the transfer by name. Two do not transfer:

    Two open pull requests carry an issue number in their title (needimport: Sort the imported needs using natural sort order #82 says "issue needs_template and needs_template_collapse removed? #69",
    ID option does not support dyn. functions #99 says "(IDs are forced to uppercase. #43)"). A transfer does not rewrite PR titles, because the PRs do not move —
    fix those by hand when they are re-opened here.

  4. The eight open pull requests. None is mergeable as it stands: seven conflict with
    main and one is merely behind. Their authors move them with the recipe above, or close
    them with a pointer — the branches stay readable in the archived repository. Two
    decisions are worth taking at the freeze rather than later: Bug: dyn. func. copy() not working with tags #100 (test
    infrastructure, the only mergeable one) and the three overlapping marked-RST pull
    requests Wrong error message for missing link #44 / Need copy/mirror: Showing the same need at different places #66 / ID option does not support dyn. functions #99
    , which are three implementations of one issue.

  5. Read the Docs, in this order, because RTD's automation rules run when a version is
    created and both recommended actions build it:
    a. the seven tags are pushed first (step 2), so RTD's initial sync creates those seven
    versions before any rule exists — a rule added first would activate and build all
    seven, and all seven would fail, because their trees predate this
    .readthedocs.yaml;
    b. create the project against this repository; set Admin → Settings → Build
    configuration file
    to packages/sphinx-codelinks/.readthedocs.yaml. Do not touch the
    sphinx-needs project's setting — it applies to every version that project builds;
    c. let the default latest build go green once;
    d. add the two automation rules on ^sphinx-codelinks-v\d+\.\d+\.\d+$ — Activate
    version
    and Set version as default. RTD derives stable with
    packaging.version.Version(<tag>) and drops what it cannot parse; a prefixed tag is
    not PEP 440, so without the second rule the project never gets a default;
    e. docs/readthedocs.org:sphinx-codelinks must not become a required status context.
    The exit-183 job makes that build absent on most pull requests, and a required context
    that never reports blocks the merge. Same reason the sphinx-mounts one is not required.

  6. The redirects, and then the domain. Four external repositories link deep into
    codelinks.useblocks.com today (/basics/installation.html,
    /components/configuration.html, /components/analyse.html#marked-rst-blocks), and
    those paths gain an /en/stable/ prefix under RTD. Add the redirects before the DNS
    switch — /basics/$rest, /components/$rest, /development/$rest and
    /objects.inv → /en/stable/…; objects.inv matters because this project is an
    intersphinx target. Then add codelinks.useblocks.com as an RTD custom domain and move
    the DNS CNAME off useblocks.github.io. Until that last step the OLD repository's Pages
    deployment IS the live site, so there is no window with no site — and only after it is
    the old gh_pages.yml genuinely dead.

  7. Archive the old repository — never delete, never transfer. Seven real
    git+https://github.com/useblocks/sphinx-codelinks.git@… pins exist in the wild, in
    four repositories. An archived repository stays public and clonable read-only, so all
    seven keep resolving; deleting it breaks every one, along with every blob permalink to a
    pre-rewrite hash. Three of the four are useblocks' own (bazel-drives-sphinx,
    needs-large-scale, ubtrace) and should re-point at PyPI or at
    git+https://github.com/useblocks/sphinx-needs.git@…#subdirectory=packages/sphinx-codelinks;
    ubtrace's is an @main pin, so it is the one that silently stops receiving updates.

Refs #1803

danwos and others added 30 commits June 11, 2025 14:08
---------

Co-authored-by: Marco Heinemann <marco.heinemann@useblocks.com>
Co-authored-by: Marco Heinemann <marco.heinemann@useblocks.com>
- added minimum config project
- applied defaults when the config is not given
- adapted test cases
- simplified docs configs
Co-authored-by: Marco Heinemann <marco.heinemann@useblocks.com>
- use github-hosted runners
- not specified cache dir in macos
chrisjsewell added a commit that referenced this pull request Sep 6, 2026
… not 25

`ci.yaml`'s concurrency comment carries the same number the previous commit corrected
elsewhere, and it moved for the same reason: #1871 added `Docs codelinks` and widened
`tests-mounts` into `tests-extensions`, so a run is 26 jobs, not 25 (counted on run
34057129950, `ci.yaml` on `a3aebf1f`, success). The argument the comment makes -- a fix-up
push would otherwise leave every one of them running against 60 org-wide runners -- is
unchanged and only gets stronger.
chrisjsewell added a commit that referenced this pull request Sep 6, 2026
…ign left behind (#1897)

Follow-up to #1870 (the committed jar at `vendor/plantuml/`), from
`master` after #1871, closing
the four actionable findings of that pull request's round-2 review. **No
behaviour changes**: two
new tests, two strengthened assertions, four string literals and three
sentences — nothing under
any `src/`, so no changelog entry.

## The fence had no fence (review finding 5)

The committed jar rests on **one flag**. `--verify` hashes the jar
against `pin.toml` and never
touches the network; without it the same script is the *bump* tool,
which **downloads**. A site
that lost the flag would turn the fence into a self-healing download
that exits **0** on exactly
the mistake it exists to catch — a bumped pin with no committed jar —
repairing the runner's tree
and reporting green (measured in review: with the flag, `EXIT=1` naming
the missing jar; without
it, a 29 MB download and `EXIT=0`). Nothing here could see that:
`check_workspace.py` reads
manifests, `actionlint` cannot know what a flag means, and no test
executed a workflow.

`tools/tests/test_ci_plantuml_steps.py` parses every
`.github/workflows/*.y{a,}ml` and
`.github/actions/*/action.y{a,}ml`, finds each step whose `run:` invokes
the script, and asserts:

- **`--verify`** at all **8** sites; and, for the **7** that capture the
path, **`| tr -d '\r'`**,
**`shell: bash`**, and the **very next line** writing `PLANTUML_JAR=` to
`$GITHUB_ENV`;
- the eighth — CI's Lint job, a bare check with no pipe and no capture —
is exempt from the last
two, and that exemption is *asserted* rather than granted by being left
off a list;
- **a positive control**: the site count. Every other case is
parametrised over the walk, so
without it an empty walk or a renamed script would report a tidy row of
passes.

Both clauses are load-bearing invisibly. Without `shell: bash` the
runner's default `bash -e` has
no `pipefail`: the failing script inside `$( )` exits 0, the step goes
**green**, and
`PLANTUML_JAR` is exported **empty**, which every consumer reads as
"unset". Without the `tr`,
Windows exports a path with a trailing CR that prints identically in a
log and names no file. 31
cases, each named after the file and step; Lint already runs `pytest
tools/tests`.

## A zero-byte jar (finding 4)

`--verify` already refuses one — the hash of nothing is a real sha256 —
but nothing pinned it: a
`st_size == 0` early return that accepted the file left all 263 tests
green.
`test_verify_refuses_an_empty_jar` writes a 0-byte jar and asserts exit
1 and `e3b0c442…b855` in
the message — what a truncated checkout or a failed LFS smudge leaves
behind.

## "the jar `poe fetch-plantuml` puts in `vendor/plantuml/`" (finding 1)

A leftover from the fetched design #1870 dropped, in **four** places.
`poe fetch-plantuml` does
not put the jar there — the checkout does, and the task re-hashes it;
and the caller that reaches
this message in CI is `--verify`, which fetches nothing at all. All four
now read "…or unset it to
render with the jar **committed at** `vendor/plantuml/`.", word for
word, and both tests that
reach it assert that clause instead of the substring `"unset it"`.

## "22 jobs … (measured: 24 jobs, all but `Lint` and the smoke test)"
(finding 2)

22 was right; 24 was not, and the job that went missing was `check`, the
alls-green aggregator.
Re-counted on this head from run `34057129950` (`ci.yaml` on `a3aebf1f`,
success): **26** jobs, now
that #1871 has added `Docs codelinks` and widened `tests-mounts` into
`tests-extensions`. The four
that do not render are `Lint`, the smoke test, `Docs codelinks` (those
docs draw no diagrams) and
`check`. Corrected in the root `AGENTS.md`, `vendor/plantuml/README.md`
and the docstring; the
README also gains a paragraph naming the new test.

**Mutations.** `--verify` dropped at one site · `shell: bash` at one ·
the `tr` at one · capture
and export collapsed into one `echo` · `verify()` made to accept an
empty file — **5 of 5 red**,
each naming the file and step, everything else green.
chrisjsewell added a commit that referenced this pull request Sep 15, 2026
…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.
chrisjsewell added a commit that referenced this pull request Sep 16, 2026
… fails every uv job

Every rebase or recreate of #1940, and the run after #1944 reverted the
versioning strategy, errored with "Dependabot can't resolve your Python
dependency files". #1944 blamed the strategy; that was wrong, and this commit
says so where the claim was made. The failing probe is docutils: since
sphinx-codelinks arrived (#1871) with a bare `docutils` dependency, docutils is
a direct, unpinned dependency, so dependabot tries to lock 0.23 -- by writing
`docutils==0.23` on every requirement line it finds, the `typing` group's
`docutils~=0.20.0` included -- and uv refuses that beside `sphinx~=7.4`. The
lock already carries docutils at 0.20.1 in the typing split and 0.21.2
elsewhere, which is what `[tool.uv] conflicts` is for; dependabot has no way to
express that, and the uv updater treats the refusal as fatal to the whole job.

So docutils joins types-docutils under the same ignore: major and minor
updates ignored, patches inside the series still flow, and the default
environment's docutils moves by hand with `uv lock --upgrade-package docutils`.
The earlier rationale for not listing it -- keeping new series flowing to the
main environment -- never operated, because docutils was not a direct
dependency until #1871; the default environment sat on 0.21.2 with 0.23 out.

`increase-if-necessary` is therefore untested rather than unusable, and the
comment records it as a candidate to retry once this rule is seen to keep the
jobs green; the AGENTS.md paragraph is corrected to match.
chrisjsewell added a commit that referenced this pull request Sep 16, 2026
… fails every uv job (#1946)

Adds `docutils` to the uv ecosystem's ignore list for major and minor
updates, beside `types-docutils`, and corrects a wrong claim #1944 made.

### The error, and what it is not

Every rebase or recreate of #1940 errored with "Dependabot can't resolve
your Python dependency files", and so did the rebase job that ran after
#1944 reverted `versioning-strategy`, under the default:

```
Because sphinx>=7.4.0,<=7.4.7 depends on docutils>=0.20,<0.22 and
sphinx-needs-workspace:typing depends on docutils==0.23, …
```

#1944 attributed the error to `increase-if-necessary`. That was wrong:
the same job fails under the default strategy. The comment in
`dependabot.yml` and the paragraph in `AGENTS.md` now say so where the
claim was made.

### What it is

docutils 0.23 has been out since May. What changed is #1871:
sphinx-codelinks arrived with a bare `docutils` dependency, so docutils
became a *direct, unpinned* dependency in dependabot's eyes, and
dependabot now tries to lock 0.23. It does that by writing
`docutils==0.23` on every requirement line it finds, including the
`typing` group's `docutils~=0.20.0`, and uv correctly refuses that
beside `sphinx~=7.4`, which needs docutils below 0.22. The lock already
carries docutils at 0.20.1 in the typing split and 0.21.2 everywhere
else, which is exactly what `[tool.uv] conflicts` provides; dependabot
has no way to express "0.20.x here, newest elsewhere", and the uv
updater treats uv's refusal as fatal to the whole job. Group refreshes
before #1871 (the 5 September batch, #1851) rebased fine.

### The fix

docutils joins `types-docutils` under the same ignore: major and minor
updates ignored, patches inside the series still flow. The earlier
rationale for leaving docutils unlisted, keeping new series flowing to
the main environment, never operated: dependabot only moves a lock entry
for a direct dependency, and docutils was not one until #1871, so the
default environment sat on 0.21.2 with 0.23 out. It moves by hand now,
with `uv lock --upgrade-package docutils`, the way the typing series
itself does.

### Consequences for the strategy question

`increase-if-necessary` (#1942) was reverted for a reason that turns out
not to hold, so it is untested rather than unusable. The comment records
it as a candidate to retry once this rule has been seen to keep the jobs
green; that is a separate decision and a separate pull request.

### Proof after the merge

The dependabot run this config change triggers should complete green on
the Insights page, where the red "Rebase update" entries are. There is
no open dependabot PR to refresh right now, so that run is the evidence.

### Checked

`uv run poe lint` 8/8. No lock change.
ubmarco pushed a commit to useblocks/sphinx-codelinks that referenced this pull request Sep 30, 2026
A notice at the top of the README: the last change before this
repository is archived.

sphinx-codelinks now lives in useblocks/sphinx-needs under
[`packages/sphinx-codelinks`](https://github.com/useblocks/sphinx-needs/tree/master/packages/sphinx-codelinks),
imported with its full history in useblocks/sphinx-needs#1871. Its
issues moved at the import, and the eight pull requests still open here
were closed with a pointer to useblocks/sphinx-needs#1898, which carries
a measured plan for moving each one; their branches stay, so nothing is
lost. The pinned #107 stays as it is. The package keeps its name on PyPI
and its documentation URL, and its next release from the monorepo points
PyPI's links at the new repository.

After this merges, the repository is archived.
patdhlk pushed a commit to patdhlk/sphinx-needs that referenced this pull request Oct 7, 2026
The whole of useblocks/sphinx-ai-index' `main` -- 7 commits, tip `74c783b9` ("update
changelog", the tree tagged `v0.1.0` and published to PyPI as sphinx-ai-index 0.1.0) --
rewritten by `git filter-repo --to-subdirectory-filter packages/sphinx-ai-index` so that
every historical commit already places its files under `packages/sphinx-ai-index/`, and
merged here with `--allow-unrelated-histories`. It is the fourth such import, after
sphinx-mounts (useblocks#1850), sphinx-codelinks (useblocks#1871) and sphinx-test-reports (useblocks#1931).

* **The tree is the source, byte for byte.** `git archive` of the source clone at
  `74c783b9` and of `packages/sphinx-ai-index` here extract to the same 22 files
  (`diff -rq` exit 0), and the merge touches nothing outside the prefix. Nothing was
  stripped and no commit message was rewritten: none of the seven carries an issue
  reference, so there was nothing to qualify.
* **The tag.** Its one release arrives as `sphinx-ai-index-v0.1.0`
  (`--tag-rename 'v:sphinx-ai-index-v'`, which strips the source's own `v`; the bare
  `':sphinx-ai-index-v'` form gives `sphinx-ai-index-vv0.1.0`). It points at the imported
  tip, `6e4b36da`, outside master, as the siblings' prefixed tags do.
* **The commit map.** filter-repo's map from the seven old hashes to the seven new ones is
  recorded at `packages/sphinx-ai-index/design/import-commit-map.txt` by a following
  commit, which keeps this merge's tree exactly the source's.

The member's wiring follows in the next commit: the root manifest, its source entry and the
relock. Until then `uv lock --check` fails here -- sphinx-needs' `docs` extra already names
sphinx-ai-index, which the `packages/*` glob now finds as a workspace member with no
`[tool.uv.sources]` entry.

`git log packages/sphinx-ai-index/<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.
chrisjsewell added a commit that referenced this pull request Oct 8, 2026
…scope association (moved from sphinx-codelinks#100) (#2128)

## What

`packages/sphinx-codelinks`: moves useblocks/sphinx-codelinks#100 into
the monorepo with the
recipe from #1871 (11 commits, authorship kept), and addresses that PR's
review on top.

The declarative extraction fixtures (`tests/data/extraction/*.yaml`) now
snapshot **the real
production output**: exactly what `SourceAnalyse.dump_marked_content()`
writes, as the flat sorted
list of `Metadata.to_dict()` items. Before, they held a test-only
projection. Warnings are a separate
`[warnings].json` artefact per case. Three deviations from production
are documented in the harness
and the README:
- `filepath` is relative to the case root.
- One additive, test-only `tagged_scope_type` key is added.
- The warnings are sorted (production reports them in tree-sitter
capture order, which differs between runs).

New `scope.yaml` (25 cases) covers marker scope association: above a
function, inside a body,
no scope, the Python docstring path, YAML's structure finder, and C++
class and struct bodies.

## Why

Scope association (`find_associated_scope`) had no fixture coverage at
all. Snapshotting the real
shape also covers `filepath`, `source_map` columns and end positions,
`type` and `marker`, which the
projection dropped.

Review of useblocks/sphinx-codelinks#100, addressed here:

1. **Warnings snapshots were order-unstable.** `_build_warnings` sorts
the records again
(`file_path, lineno, type, sub_type, msg`). Production reports them in
tree-sitter capture order,
which differs between two runs of the same input once several extractors
are on, so it cannot be
   pinned.
2. **`SCOPE_NODE_TYPES[cpp]` named `class_definition`,** a kind
tree-sitter-cpp never emits, so a
marker in a class body had no scope. It now names `class_specifier` and
`struct_specifier`, pinned by
`scope.yaml`'s `cpp_class_body` / `cpp_struct_body` (🐛, changelog
entry).
3. **The field-count wording** was already fixed on master by #2092
("minimum is" / "maximum is").

Also from the review:
- `test_extraction_is_crlf_insensitive` was imperative. It becomes a
declarative `line_endings` key
on the case schema, plus `line_endings.yaml` (CRLF and a lone CR); such
a case also runs with LF
  endings and must match.
- The extraction corpus is shared with ubCode, which copies the YAML
files it also has (today 11 of the 14) byte for byte and takes these
snapshots as the expected output of its parity test; the README and the
harness header now say so, and useblocks/ubcode#3928 tracks how ubCode's
sync script takes up this production shape. The LF pin moves to the root
`.gitattributes` with that reason, and yamlfmt no longer formats the
corpus (closes #2133).

Production fixes the fixtures surfaced (physical columns, the need-ref
span, docstring tags, a block
comment's `*/` as an id) are in #2129, stacked on this one, so this
stays reviewable as a move.

## Review of this PR, addressed on the branch (taken over 2026-10-08
with Marco's go)

Four commits on top of Marco's, no source, corpus or snapshot change:
- The README and the harness header state that the corpus and snapshots
are shared with ubCode (the first bullet above).
- The LF pin for the corpus and snapshots lives in the root
`.gitattributes`, one commented block like the three conformance
corpora, with the real reason: ubCode copies the bytes. The tests do not
depend on it (YAML normalises line breaks, the harness writes each
case's source as exact bytes, syrupy reads snapshots in text mode). The
package-level `.gitattributes` is gone, and yamlfmt's exclude covers the
corpus (closes #2133).
- `_assert_portable_path` is removed: its input is
`relative_to(...).as_posix()`, which cannot fail its checks.
- The harness's contract comment and the README are cut to the facts;
the README lists the warnings sort among the deviations.

Found in that review, pre-existing and not changed here: production
orders two marked-content entries on one row by tree-sitter's capture
order, which differs between runs (#2150; no case here puts two entries
on one row), and the scope walk binds a marker above a `typedef struct`,
a union or a namespace to a struct further down the file now that
`struct_specifier` is a scope kind (#2151).

## Tests

The fixture harness and every `test_extraction_fixture` snapshot are
converted to the production
shape. Additions:
- `scope.yaml` (25 cases) and `line_endings.yaml` (2 cases).
- Re-captures after #2092 (warning wording, the moved `too_few_fields`
case) and #2021 (marked-rst
  rows); every one of those snapshot diffs is a master change.

`uv run poe test-codelinks`: **689 passed, 0 skipped**, stable across
reruns (also under xdist and
from the repository root); the same at the takeover tip, with lint,
typecheck, check-workspace, the CLI suite under `GITHUB_ACTIONS=1` and
`docs-codelinks` (0 warnings) green, and every corpus and snapshot byte
unchanged.

## Checklist

- [x] Every line of this change has been read: Marco's review of
useblocks/sphinx-codelinks#100, the review of 2026-10-08, and the
takeover's line-by-line pass; it was not generated automatically from an
issue.
- [x] I ran the package's tests (`uv run poe test-<package>`) and they
pass.
- [x] Documentation is updated where behaviour or options change.
- [x] The package's `docs/changelog.rst` has an entry under
*Unreleased*.
- [x] `uv run poe lint` and `uv run poe typecheck` pass.

Closes #2133

---------

Co-authored-by: Chris Sewell <chrisj_sewell@hotmail.com>
chrisjsewell added a commit that referenced this pull request Oct 9, 2026
…m sphinx-codelinks#82) (#2166)

## What

`packages/sphinx-codelinks`: moves useblocks/sphinx-codelinks#82
(TypeScript and JavaScript comment type, by @arnoox) into the monorepo
with the recipe from #1871, and addresses that PR's open review on top.

`comment_type = "ts"` analyses `.ts`, `.tsx`, `.mts`, `.cts`, `.js`,
`.jsx`, `.mjs` and `.cjs`. The grammar is chosen per file: TypeScript
for `.ts`/`.mts`/`.cts`, where a `<T>x` cast is legal, and TSX for every
other suffix, which also reads JSX. Legacy one-line `<!--` comments
(classic scripts only) are read too. For this comment type `exclude`
defaults to the usual build-output directories.

The move: the branch was 24 commits, 3 of them merges of `main`, and
#1898 measured five conflict stops on the old `main`. So it comes across
as two commits with their authors kept: Arnaud Riess's work up to the
2026-08-07 merge, and Marco Heinemann's follow-ups. Both were filtered
with the import's `git filter-repo` rule, which puts them on the commit
the import merged, and cherry-picked onto master. The full history stays
at useblocks/sphinx-codelinks#82. The conflicts against master were
resolved in place:
- The changelog entry moved to `docs/changelog.rst`, written for the
release.
- The docs hunks moved to `docs/components/`.
- The audited `AGENTS.md` and the `@AGENTS.md` `CLAUDE.md` are kept; the
PR's own `CLAUDE.md` is dropped.
- `test_analyse_oneline_needs` takes the PR's case dicts with master's
`get_multiline_needs`.
- `uv.lock` gains only the `tree-sitter-typescript` entries.

Then four commits (six, the takeover counted: a 📚 commit linking the
changelog entry to this pull request follows the third, and a 🧪 commit
pinning a `@need-ids:` line in a JSDoc block follows the fourth):
1. 🧪 The TypeScript cases are re-captured in the production-shape corpus
of #2128. Multi-line needs work in TypeScript with no code change, now
pinned by two cases.
2. 👌 Scope association covers TypeScript's declarations (the review
below).
3. 👌 The one-row block, JSX and `<!-- -->` markers.
4. 👌 A JSDoc tag line is a `jsdoc_tag` warning, not a need.

The merge of master: master moved by one commit since this branch was
cut, `132607bb` (#2168: `src-trace` renders multi-line needs). It is
merged in (`023dd70b`), not rebased, so the eight commits above are
unchanged. Two files conflicted. In `docs/changelog.rst` the multi-line
needs bullet takes master's text with this branch's language list, and
the TypeScript bullet follows master's new `src-trace` bullet. In
`tests/data/extraction/multiline_needs.yaml` master's four language
cases and this branch's two sit side by side after `bash_hash_run`.
Nothing else moved: against master the diff is this branch's own
(+2974/−149) plus the takeover's commits below. A `//` run and a JSDoc
block — the two shapes the corpus pins — now render through `src-trace`:
`test_typescript_blocks_render`.

The review round (two reviews of the takeover; every commit after the
merge): `export declare …` binds the declaration it holds
(`find_next_scope` looks one container deeper, pinned red first by
`typescript_export_declare`), and a `declare`-prefixed `const`, `let` or
`var` is a scope, as it holds no value (pinned red first by
`typescript_declare_const_binds`, which now holds one of each); JSDoc's
`@modifies` and TypeScript's JSX pragmas `jsx`, `jsxFrag`,
`jsxImportSource` and `jsxRuntime` join the shared tag list (112 names);
the grammar choice, the scope predicates, the C++ side of the
`jsdoc_tag` gate, `<!--` runs and `discover -e` get the pins they
lacked, each shown red under the wrong version it catches, but for the
`<!--` pin, which records today's behaviour; the narrowed rule's
question to the one-line parser (`jsdoc_tag_asks_the_one_line_parser`,
with `;` as the separator) and every JSX pragma of the tag list are
pinned too; the docs say what was measured (the `exclude` default's
width, the `discover.rst` example, `[[` rather than `@need` as the
suggested start sequence, where `<!--` is valid); and, in a commit of
its own, the `jsdoc_tag` rule is narrowed (item 15).

Closes #1890

## Why

The review of useblocks/sphinx-codelinks#82, open at the move. The first
round (five threads) was fixed on that branch before it moved.

1. **The TSX grammar is not a superset of TypeScript's.** Fixed on that
branch: the grammar is chosen per suffix. `oneline.yaml`
`type_assertion_oneliner_typescript` now pins it through production
wiring: under TSX, the marker after the cast is lost.
2. **Interfaces, enums, type aliases, abstract classes and generator
functions bound to the next unrelated function.** They are scopes now.
So are function signatures, modules, namespaces and interface/abstract
method signatures.
3. **`TS_FUNCTION_VALUE_TYPES` was too narrow.** Generator functions and
class expressions count, seen through `as`, `satisfies`, `!`, `<T>` and
parentheses.
4. **The demo marker field order.** Fixed on that branch.
5. **Nothing pinned the production grammar choice.** The test fixtures
now build their parsers through `init_tree_sitter`. The declarative
cases go through production wiring, and their `tagged_scope` differs
under the wrong grammar.
6. **JavaScript's assignment forms and anonymous default exports.**
`module.exports = function () {}`, `Foo.prototype.bar = …` and `export
default () => {}` bind.
7. **Class-field arrow functions bound to the class.** A class field
holding a function binds to the field.
8. **`.d.ts` declarations had no scope.** `declare …` is searched like
`export …`, so ambient declarations bind. `.d.ts` files stay discovered.
9. **TypeScript-only rules hardcoded in the shared traversal.** They now
live in two per-language tables, `SCOPE_CONTAINER_TYPES` and
`SCOPE_PREDICATES`. A comment type without a `SCOPE_NODE_TYPES` entry
has no scopes, not C++'s.
10. **Six copies of one query.** The five grammars (six, the takeover
counted: C/C++, C#, YAML, Go, JSONC and Bash) whose comments are one
`comment` node share `SIMPLE_COMMENT_QUERY`. TypeScript's query differs,
since it also captures `html_comment`.
11. **Five unsynchronised edits per language.**
`test_every_comment_type_is_wired` fails when a `CommentType` member
misses a table, rather than failing as a `KeyError` in discovery.
12. **The one-row `{/* @… */}` and `/* @… */` forms failed.** They work
since #2129 (the closing `*/` is no marker text) and are pinned now. The
docs no longer call them unsupported.
13. **The `CLAUDE.md` described an architecture that does not exist.**
Dropped in the move.
14. **The changelog heading.** The entry is under *Unreleased*.
15. **Every JSDoc tag line warned** (the takeover's change to the rule,
measured: one ordinary eight-line JSDoc comment with no comma anywhere
gave six `jsdoc_tag` warnings, enough to fail a `-W` build, while the
same text in `/* */` gave none). As `docstring_tag` was narrowed for
Python, a line of a JSDoc comment is now a `jsdoc_tag` warning, and
never a need, only when the one-line start sequence is `@`, nothing
alphanumeric precedes it, a listed tag directly follows it and ends at a
space, a tab, `{`, `}` or the line end, and the one-line parser would
otherwise have read the line as a marker or warned about it. `@param a
the augend` stays silent; `@param a the first, thing` still warns. Under
`[[`, `[[param a, b]]` is a need. The rule is written once, in
`jsdoc.yaml`'s header. It is its own commit, so it can be dropped.

Follow-up findings on that PR:
- `html_comment` markers were dropped. Fixed on that branch, and a
one-row `<!-- … -->` now drops its `-->` as a block comment drops `*/`.
- `@need-ids:` in a JSDoc block also produced a phantom need. Fixed on
master by #2049: a reference-marker line is no one-line need.
- Generated output was scanned as source. Fixed on that branch by the
per-comment-type `exclude` default.
- JSDoc tags with a comma became needs: `@param a First, second` gave a
need with the id `second`. Now a `jsdoc_tag` warning applies, following
the `docstring_tag` rule of #2129. A line of a JSDoc comment whose start
sequence is directly followed by a JSDoc tag warns and yields no need
(narrowed by the takeover to the lines the one-line parser would have
read, under `@`: item 15). A JSDoc comment is one opened by exactly
`/**`, under `comment_type = "ts"`. The tag must end at a space, a tab,
`{`, `}` or the line's end. The tag list covers JSDoc's block tags,
TSDoc's, TypeScript's and the inline tags; matching is case-sensitive.
The warning says to choose a start sequence JSDoc does not use. `/* */`,
`//` and `/*** */` comments are untouched, and so is C++ Doxygen.

## Tests

- `scope.yaml`: 23 TypeScript and JavaScript cases (30 since the
takeover's `typescript_export_declare`,
`typescript_declare_const_binds`, and one case per scope predicate and
for the abstract method signature), one association each, among them
every declaration kind above.
- `oneline.yaml`: 7 TypeScript-family cases: the three moved ones plus
the one-row block, JSX inline, `<!-- -->` and cast cases (8 with the
takeover's `jsx_text_oneliner_tsx`: JSX text is no comment, so `.tsx` →
TSX is pinned).
- `multiline_needs.yaml`: a `//` run and a JSDoc block (4 with the
takeover's two `<!--` pins: a run of `<!--` lines holds no block, and a
`<!--` row ends a `//` run).
- `jsdoc.yaml` (new, 6 cases; 7 with the `@need-ids:` case, the takeover
counted; 10 with its `jsdoc_more_tags`, `doxygen_in_cpp_is_no_jsdoc` and
`jsdoc_tag_asks_the_one_line_parser`): tag lines warn and the non-tag
marker is a need. It also covers where a tag ends, case sensitivity,
comments that are no JSDoc, JavaScript, and a start sequence JSDoc does
not use.
- `test_jsdoc_tag`: the tag shape, the same table as ubCode's.
- `test_every_comment_type_is_wired`.
- The PR's own unit, discovery and CLI tests.
- `test_multiline_needs_render.py::test_typescript_blocks_render` (the
takeover's): a `ts` project beside the fixture's `src`, a `//` run with
an RST body in a `.ts` file and a JSDoc block with a MyST body in a
`.tsx` file, each traced on an RST page. Each need is recorded at its
directive with its body's doctype and its links, its local URL is at the
block's open line, its body renders the role link and the list, and the
build warns nothing. With `get_multiline_needs` off for the project both
needs are absent and the test fails.
- The takeover's units: `test_ts_grammar_key` (`.ts`, `.mts`, `.cts`,
`.d.ts` in any case → TypeScript; the rest → TSX);
`test_a_ts_and_a_tsx_file_in_one_run_each_get_their_grammar` (one
analysis, both orders);
`test_discover_cli_excludes_replace_the_ts_default`; and the
default-exclude test asserts what discovery lists, `build/`, `out/`,
`coverage/` and a `.d.ts` included.
- At the tip: `poe test-codelinks` 1051 passed, 0 skipped, on Sphinx 9.1
and on Sphinx 7.4 / Python 3.11; `poe docs-codelinks` builds with no
warning; `poe import-check-codelinks` imports the wheel with
`tree-sitter` 0.25.2 and `tree-sitter-typescript` 0.23.2 resolved from
PyPI.

The corpus is shared with ubCode, whose parity test replays every case
above (useblocks/ubcode#2764, re-vendored from this branch the same day;
see *ubCode* below).

## ubCode

The twin is useblocks/ubcode#2764 (Marco's, taken over the same day and
merged onto `main`). It re-vendors the shared corpus (`SOURCE.md`) from
this branch while it is open and at this pull request's squash commit
once it merges, and its parity test replays the TypeScript cases of
`oneline.yaml`, `scope.yaml`, `jsdoc.yaml` and `multiline_needs.yaml`.
The `jsdoc_tag` rule (the tag list, the end class, the exactly-`/**`
rule, and since the review round the `@`-only and would-have-been-read
clauses) is written once, here, in `jsdoc.yaml`'s header, and ubCode
implements the same text. From the re-vendor the twin also mirrors the
two-level container walk (`export declare …`), the ambient-variable arm
of the declaration predicate (a `declare`-prefixed `const`, `let` or
`var` is a scope) and the 112-name tag list.
`js_html_row_breaks_a_line_run` in `multiline_needs.yaml` was written
for the reference by ubCode's review of the twin.

## Checklist

- [ ] I wrote this change myself and have read every line of it; it was
not generated automatically from an issue.
- [x] I ran the package's tests (`uv run poe test-codelinks`) and they
pass.
- [x] Documentation is updated where behaviour or options change.
- [x] The package's `docs/changelog.rst` has an entry under
*Unreleased*.
- [x] `uv run poe lint` and `uv run poe typecheck` pass.

---------

Co-authored-by: Arnaud Riess <arnaud.riesslgmail.com>
Co-authored-by: Chris Sewell <chrisj_sewell@hotmail.com>
chrisjsewell added a commit that referenced this pull request Oct 14, 2026
Six members of the workspace released together, in one pull request and
three tag waves: sphinx-needs 9.0.0 (the Sphinx half of the
derived-values lockstep with ubCode's next release) and the five
packages around it. Everything mechanical is `uv run poe bump`'s, one
commit per member; the prose, the gap bullets and the version-marker
sweep are by hand, each claim measured against the tree.

### What this releases

| member | from → to | level | why |
|---|---|---|---|
| sphinx-ai-index | 0.1.0 → **1.0.0** | major | the `ai_docs_index.json`
it writes has been the format's `"1.0"` since the first release, and
ubCode reads it in production — that is the stability 1.0 declares
(#2034); this is also its first release from this repository, on the
workspace's Python 3.11 / Sphinx 7.4–9 / docutils 0.21 floors |
| sphinx-mounts | 0.2.0 → **0.3.0** | minor (0.x) | variant data from
`[variants]` through ub-project, with new refusals and two new
`-W`-failing warnings; Python 3.11 again; docutils 0.21; first release
from this repository |
| sphinx-needs | 8.5.0 → **9.0.0** | major | computed values (derived
fields, dependency order), `needextend` filters and the needs.json
readers can change need data in certain corner cases; `[[…]]` in content
and `need_func` removed; Python 3.11; `[variants]`; `needstable.js` |
| ub-test-reports | 1.0.0 → **1.1.0** | minor | parser and converter
fixes that add `ReportReadError` and change which exceptions escape and
what a nested-suite report converts to |
| sphinx-codelinks | 1.4.0 → **2.0.0** | major | requires Sphinx-Needs
9; `get_rst` / `[analyse.marked_rst]` refused, the `@rst` blocks
replaced by multi-line needs; first release from this repository |
| sphinx-test-reports | 2.0.0 → **3.0.0** | major | the Sphinx-free half
is ub-test-reports; the import name moves to `sphinx_test_reports`;
requires Sphinx-Needs 9 and ub-test-reports 1.1 |

ub-project stays at 1.1.0: nothing to release.

The six versions, the `__version__` literals, the floors
(`sphinx-ai-index>=1.0.0,<2` in four `docs` extras;
`sphinx-needs>=9.0.0,<10` in codelinks and test-reports;
`ub-test-reports>=1.1.0,<2` in test-reports), the docker fallback
(`sphinx-needs-v9.0.0`), the lock and the six changelog headings are `uv
run poe bump`'s, one commit per member in the planner's order.

### What was added by hand

- **sphinx-needs 9.0.0**: bullets for the changes the entry had not
recorded — ‼️ `[[…]]` in need content is plain text and the `need_func`
role is removed (#1919); ✨ the needtable script (`needstable.js`,
`needs_table_page_size` / `needs_table_page_sizes`, Excel / PDF export
and column reordering removed; #1920); ✨ `[variants]` read through
ub-project, a new dependency, with two new warning types (#1996, #1998);
⬆️ docutils 0.21 (#1977); ⬆️ jsonschema-rs up to 0.58 (#1851, #1980); 👌
deterministic CSS order (#1914); 🔧 the uv workspace, the
`#subdirectory=packages/sphinx-needs` git install and the
`sphinx-needs-v<version>` tags (#1839, #1846), in a new "Internal
changes" section; 🔧 the sdist without `tests/` / `performance/` (#1905);
🐛 needgantt's integer completion (#1870); and the derived-fields bullet
cites #2204 and #2205.
- **sphinx-mounts 0.3.0**: the move bullet (#1850).
- **sphinx-codelinks 2.0.0**: 🐛 `get_current_rev` reads `packed-refs`
(#1871); the ‼️ floor bullet says Sphinx-Needs 9.0 / `>=9.0.0,<10`;
`codelinks write rst` "will be removed in 3.0.0"; the documentation is
served by Read the Docs; the `ingest_need_record` note trimmed.
- **sphinx-test-reports 3.0.0**: the floors it ships
(`sphinx-needs>=9.0.0,<10`, ub-test-reports 1.1.0).
- **ub-test-reports 1.1.0**: the UTF-8 bullet links #2024.
- **The six summary paragraphs**, one per entry (sphinx-ai-index's
replaces its old lead).
- **Pages outside the changelogs** (each states what this release
ships):
- sphinx-codelinks' `codelinks write rst` deprecation
(`WRITE_RST_DEPRECATED` in `cmd.py`, and `docs/components/write.rst`)
now promises removal in 3.0.0, not 2.0.0, and is dated 2.0.0, not an
uncut 1.5.0: 2.0.0 ships with the command.
- `docs/basics/introduction.rst` says `src-trace` renders multi-line
needs.
- sphinx-needs' `docs/directives/choose.rst` says `versionadded::
9.0.0`, not an uncut 8.6.0, and `docs/configuration.rst` says
sphinx-mounts reads `[variants]` "from 0.3.0", not "from its next
release".
- sphinx-test-reports' `docs/install.rst` names the Sphinx-Needs 9 floor
(`>=9.0.0,<10`).
- Comments and agent notes naming the old `>=8.5.0,<9` floor
(`compat-requirements.txt`, two `AGENTS.md`, `src_trace.py`) name 9.0.0.
- A sweep of all 216 version markers (`versionadded` / `versionchanged`
/ `deprecated`) in the six members' docs and sources, against each
member's released versions, found 13 more that named a release never
cut. Each now names the release that shipped it: mounts 0.2.0 ×2,
sphinx-needs 3.0.0 ×7, 4.0.0 ×1 and 7.0.0 ×1, test-reports 1.1.0 ×2.

### Merge and tag order

Merge RIGHT BEFORE tagging. **Re-stamp `:Released:` on the merge day
first** (it is stamped 2026-10-14, the earliest tag day); one line per
file, each in its own format:

| file | line | format |
|---|---|---|
| `packages/sphinx-ai-index/docs/changelog.rst` | l.11 `:Released:
2026-10-14` | ISO `YYYY-MM-DD` |
| `packages/sphinx-mounts/docs/changelog.rst` | l.11 `:Released:
2026-10-14` | ISO |
| `packages/sphinx-needs/docs/changelog.rst` | l.12 `:Released:
14.10.2026` | `DD.MM.YYYY` |
| `packages/ub-test-reports/docs/changelog.rst` | l.11 `:Released:
2026-10-14` | ISO |
| `packages/sphinx-codelinks/docs/changelog.rst` | l.11 `:Released:
14.10.2026` | `DD.MM.YYYY` |
| `packages/sphinx-test-reports/docs/changelog.rst` | l.11 `:Released:
14.10.2026` | `DD.MM.YYYY` |

Then tag the squash commit `<sha>` explicitly (so nothing merged later
can slip into a release), in three waves, each after the previous wave's
`publish` jobs are green — the plan job's check 4 refuses a member whose
workspace dependencies (extras included) are not on PyPI at the version
this tree builds:

```bash
# wave 1 — dependencies already on PyPI (ub-project 1.1.0) or none
git tag sphinx-ai-index-v1.0.0 <sha> && git push origin sphinx-ai-index-v1.0.0
git tag ub-test-reports-v1.1.0 <sha> && git push origin ub-test-reports-v1.1.0
# wave 2 — once sphinx-ai-index 1.0.0 is on PyPI
git tag sphinx-mounts-v0.3.0 <sha> && git push origin sphinx-mounts-v0.3.0
git tag sphinx-needs-v9.0.0 <sha> && git push origin sphinx-needs-v9.0.0
# wave 3 — once sphinx-needs 9.0.0 and ub-test-reports 1.1.0 are on PyPI
git tag sphinx-codelinks-v2.0.0 <sha> && git push origin sphinx-codelinks-v2.0.0
git tag sphinx-test-reports-v3.0.0 <sha> && git push origin sphinx-test-reports-v3.0.0
```

The bare `9.0.0` tag is pushed by `release.yaml`'s last step for
sphinx-needs ("Push the bare companion tag"); it is what moves Read the
Docs' `stable` and keeps `git+…@<version>` pins working. Only if that
step fails: `git tag 9.0.0 sphinx-needs-v9.0.0 && git push origin
refs/tags/9.0.0`. A tag pushed too early fails only its plan job, with
nothing published: re-run it once the dependency is up. After a green
`publish`, a red later job is re-run with **Re-run failed jobs**, never
*Re-run all jobs*.

### Before wave 1

Five of these PyPI projects release through `release.yaml` for the first
time — sphinx-ai-index, sphinx-mounts, sphinx-needs (8.5.0 went out
through the old token-based workflow), sphinx-codelinks and
sphinx-test-reports. Each must list the trusted publisher **owner
`useblocks`, repository `sphinx-needs`, workflow `release.yaml`,
environment `pypi-<dist>`**. The GitHub side is ready (the five
`pypi-<dist>` environments exist, each with the tag policy `<dist>-v*`);
the PyPI side can only be checked by a project owner. A wrong publisher
fails `publish` with nothing uploaded — fix it and re-run from the same
tag — but sphinx-ai-index's is the very first tag, so it would stall the
whole train.

### Rehearsal

Two rehearsals can pass before the merge, from this branch:

```bash
gh workflow run release.yaml --ref release/9.0.0-train -f tag=sphinx-ai-index-v1.0.0
gh workflow run release.yaml --ref release/9.0.0-train -f tag=ub-test-reports-v1.1.0
```

Dispatched from this branch at `9a2dd5ec`: run
[38479550037](https://github.com/useblocks/sphinx-needs/actions/runs/38479550037)
(`sphinx-ai-index-v1.0.0`) and run
[38479553168](https://github.com/useblocks/sphinx-needs/actions/runs/38479553168)
(`ub-test-reports-v1.1.0`). A dispatch runs `plan` and `build` (the
resolver gate and the compat cell) and never `publish`. It must name
this branch: at `master` the tag's version is not the tree's (check 2).
The other four cannot pass yet, because check 4 stays fatal in a
rehearsal: sphinx-mounts and sphinx-needs need sphinx-ai-index 1.0.0 on
PyPI (their `docs` extras), sphinx-codelinks and sphinx-test-reports
need sphinx-needs 9.0.0 (and test-reports ub-test-reports 1.1.0). They
can be rehearsed from `master` once the wave before them has published.

### What goes red and why

- **Docs-Linkcheck** (not a required check): the new
`8.5.0...sphinx-needs-v9.0.0` compare link 404s until the
`sphinx-needs-v9.0.0` tag exists.
- **`uv run poe import-check-codelinks` / `import-check-reports`**: red
until wave 2 publishes — "only sphinx-needs<=8.5.0 is available and
sphinx-codelinks==2.0.0 depends on sphinx-needs>=9.0.0,<10". Run them on
`master` between waves 2 and 3: red then would be a real missing symbol.
The other five import checks are green on this branch.
- **Docker**: the fallback ref becomes `sphinx-needs-v9.0.0`, so the
`docker/**` pull-request and manual-dispatch path, red since the move,
goes green once that tag exists. This PR does not run it (its trigger is
`docker/**` only), and the tag push builds the image from the tag
itself.

### For a human (outside the repository)

- **PyPI trusted publishers**: the five checks under "Before wave 1".
- **codelinks.useblocks.com deep links**: the old GitHub Pages site
served pages at the root (`…/components/analyse.html`). Under Read the
Docs they 404 and live under `/en/latest/`. A Read the Docs page
redirect (old path → `/en/latest/<path>`) on the `sphinx-codelinks`
project would keep bookmarks and inbound links working.
- **Read the Docs `stable` for sphinx-mounts and sphinx-codelinks**
serves the Sphinx-Needs 8.5.0 documentation today (both projects build
from this repository, whose highest PEP 440 tag is sphinx-needs' bare
`8.5.0`). After this train it follows the bare `9.0.0` tag, then every
later sphinx-needs release, and never a mounts- or codelinks-only one.
Consider deactivating `stable`, or making `latest` the default version,
on those two projects.

### ubCode

ubCode's next release names sphinx-needs 9.0.0 as the version its
derived-values reading pairs with. This changelog names no ubCode
version, by design: it keeps the entry's "phase-2 release" wording. The
ubCode release pull request is prepared separately.

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

Labels

pkg: sphinx-codelinks Concerns the sphinx-codelinks package (packages/sphinx-codelinks) pkg: sphinx-mounts Concerns the sphinx-mounts package (packages/sphinx-mounts) pkg: workspace The repository as a whole: workflows, CI, release, docker, tooling, the workspace root

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants