Commit Graph
265 Commits
Author SHA1 Message Date
vhaudiquet bb719e7c80 apt/keyring: keep the keyring cache readable
A fresh chroot download failed with mmdebstrap unable to copy the
keyrings: its unshare-mode hooks run under an identity that cannot
read the invoking user's private directories, so the 0700 cache
directory the module created broke the trusted.gpg.d setup hook with
'Permission denied' (builds reusing an already-cached tarball never
hit the path).

Create the cache 0755 and chmod the keyrings 0644 instead. The
planting guard does not weaken: cached keyrings are trusted as-is,
and validate_keyring_dir keeps refusing directories that are not
owned by the current user or are writable by group or others — read
access for the bootstrap tool is not a planting vector. Legacy 0700
cache directories are validated as before, then widened.
2026-09-20 18:14:28 +02:00
vhaudiquet af9845c480 deb: install build-deps per dpkg cross semantics
Replace the apt-get build-dep passes with a native resolver: the
Build-* clauses are evaluated against the context's package state by
crate::debian::deps (the dpkg-checkbuilddeps equivalent), and the
unsatisfied ones install through explicitly architecture-qualified
names.

apt's --host-architecture build-dep resolution is coarser than the
checker it feeds: unqualified Multi-Arch: same libraries only ever
get the host-architecture variant, and there is no way to also
satisfy the build-architecture needs of a cross build without a
native re-resolution pass — which installs co-install partners that
break unpacking for -dev packages whose variants conflict on
arch-differing files (curl-config; the libcurl4-gnutls-dev
regression). The resolver now applies the same dpkg rules as the
checker, preferring the runnable build-architecture variant of
Multi-Arch: foreign tools, and passes virtual names through to apt.

Build-Conflicts are now checked before anything installs, which the
build-dep passes never did, and a failed dose3 diagnosis no longer
masks the resolver error (a latent flaw of the passes: binary-only
builds have no .dsc for dose-builddebcheck to read).

An end-to-end test pins the expected failure: a cross build whose
build-dependencies cannot satisfy dpkg's cross semantics aborts,
naming the dependency.
2026-09-20 18:04:13 +02:00
vhaudiquet 02cb5306f3 debian: test the cross build-dep lookup against dpkg
The Facts lookup already implemented Dpkg::Deps::KnownFacts
_find_package, but nothing pinned its cross-compilation behaviour:
with host != build, an unqualified dependency resolves against the
HOST architecture instance (or any instance of a Multi-Arch: foreign
package, or an Architecture: all one) and never against the
build-architecture instance of a Multi-Arch: no/same package.

Cover the matrix with unit tests and, like the source-build checker,
with a differential sweep against real dpkg-checkbuilddeps on a
synthetic admindir with -a <host>: the checker and the real tool
agree on every fixture, including the quirks (:native aborting on
Multi-Arch: foreign instances, first-match version binding).
2026-09-20 18:04:05 +02:00
vhaudiquet 5c026a7050 docs: add AGENTS.md with the tree conventions
CI / build (push) Successful in 3m12s
CI / test (push) Skipped
CI / snap (push) Successful in 4m32s
Codify what the history already does so agents and new contributors do
not have to reverse-engineer it: the fmt/clippy gates mirroring CI's
-Dwarnings, the <scope>: <summary> commit format with the per-module
scope table, and the library-side invariants (missing_docs, report
ports, data/*.yml embeds, README parity for user-facing changes).
2026-09-20 16:30:15 +02:00
vhaudiquet 9238aa961f chlog: fall back to the changelog history when no version tag exists
CI / build (push) Successful in 2m56s
CI / test (push) Skipped
CI / snap (push) Successful in 4m30s
get_commits_since_version silently returned an empty change list when
the previous version carried no tag. Walk the history back to the last
commit that modified the changelog itself and use it as the boundary
instead, so entries stay correct in repositories that commit their
changelogs without tagging them.

Tag detection remains the preferred path. Before falling back to the
changelog commit, the version recorded by the committed changelog is
probed for a tag: an uncommitted newer entry on top (e.g. UNRELEASED
from a previous run) does not hide the previous version's tag.
2026-09-20 00:38:15 +02:00
vhaudiquet d1056fbbbf ui: ellipsize fake-terminal pane lines wider than the terminal
Overflowing log lines in the pkh deb / pkh build rolling pane used to
wrap onto a second line, corrupting the pane layout. Truncate them to
the terminal width (minus the pane prefix) by display width and append
an ellipsis instead; lines are left whole when the terminal size is
unknown.
2026-09-20 00:15:37 +02:00
vhaudiquet a0e74073bf lint: add pkh lint, wrapping lintian for parity plus pkh-native checks
CI / build (push) Successful in 2m55s
CI / test (push) Skipped
CI / snap (push) Successful in 4m32s
pkh covered the package lifecycle but never validated the packaging
itself: broken control stanzas, unparsable changelog versions or
uncommitted debian/ edits only surfaced at build or upload time. pkh
lint lints a source tree with day-one lintian parity plus a native Rust
engine for the checks lintian cannot have.

The wrapper reuses the pkh build output next to the tree when it matches
the current changelog entry and no tree content is newer (mtime walk,
skipping .git/.pc), else packs fresh with dpkg-source -b using weak gzip
compression (the artifact is ephemeral; xz dominated the run at 9.8 s
versus 2.7 s on a 111 MB tree) and symlinks quilt orig tarballs from the
tree's parent, which dpkg-source searches in cwd. Findings are parsed
from the installed lintian into a unified report, deduplicated by tag
name against the native engine, and rendered lintian-shaped
(<L>: <pkg> <type>: <tag> <details>) as text or JSON, colorized at
render time (--color auto/always/never). Exit codes follow lintian's
contract (0 clean, 1 findings at/above --fail-on, 2 runtime error);
lintian's own exit code is ignored because it uses 2 both for findings
and for runtime errors. -d/--dist maps to lintian --profile so the
target distro's rules apply even on a foreign host.

The native engine hosts the first workflow check lintian cannot know:
pkh-debian-changes-not-committed flags debian/ content that is not
committed to git, since the pkh flow builds and uploads the tree as-is.
Checks register in a static registry validated by a unit test, and the
wrapper's parser is pinned by golden tests captured from lintian 2.129
output. Strategies for lintian's Ubuntu blind spots (its vendor data
there is one file plus 14 disabled tags) are specced in
plans/pkh-lint.md, deliberately not implemented yet.
2026-09-19 23:45:31 +02:00
vhaudiquet 4f5246ccd3 chlog: number Ubuntu backports with the per-release SRU scheme
CI / build (push) Successful in 2m56s
CI / test (push) Skipped
CI / snap (push) Successful in 4m35s
--backport was Debian-only: ~bpo is backports.debian.org's scheme and
its number the Debian release, so Ubuntu targets were rejected outright.
Ubuntu backports have their own documented scheme (Ubuntu version-
strings): the development release's version with a per-release ~YY.MM.1
appended, sorting before it (3.1-1ubuntu2 backported to 22.04 becomes
3.1-1ubuntu2~22.04.1; native 3.1 becomes 3.1~22.04.1) and independent of
the version the target release carries. The .N increments for
subsequent per-release SRU uploads.

backport_series_number becomes backport_suffix_for_series: the release
number comes from the new generic get_series_release_number (version
column of the target series' own distro-info data, leading token kept —
"12" for bookworm, "26.04" out of resolute's "26.04 LTS"; empty column
as on sid/experimental means None), and the suffix is picked per vendor:
~bpoNN+ for Debian (plain integer releases only), ~YY.MM. for Ubuntu.
Unnumbered series still error before anything is written.

Also serialize the changelog tests that mutate the process-global
DEBFULLNAME/DEBEMAIL variables behind a tokio Mutex: run in parallel
they raced each other's identity reads, which started failing
intermittently as generate_entry tests accumulated.
2026-09-19 21:32:02 +02:00
vhaudiquet dd2438a72c chlog: number regular uploads after the target series' vendor
A flagless entry (no --backport/--nmu/--rebuild) targeting an Ubuntu
series was numbered the Debian way: 1.0-1 became 1.0-2 with distribution
noble, and getting the conventional 1.0-1ubuntu1 required hand-editing
the version. The bump now derives from the vendor of the target series
(distro-info): Ubuntu series get the ubuntu suffix convention (1.0-1
becomes 1.0-1ubuntu1, and re-bumping an already-Ubuntu changelog
increments the counter instead of the revision), Debian series keep the
plain revision bump, and series that cannot be resolved to a vendor
(UNRELEASED, unknown) fall back to it too. There is no reverse sync, so
the Ubuntu-to-Debian direction needs no special casing.

Runs with an explicit --series now consult distro-info once, where they
previously queried it not at all; the interactive flow already did for
the series selector. EntryKind::Ubuntu remains the library-level way to
force the numbering regardless of the series.
2026-09-19 20:58:44 +02:00
vhaudiquet 012df20961 chlog: number entries as backport, NMU or no-change rebuild
The version flags were never reachable: --backport was declared but not
read, and compute_new_version's NMU/rebuild numbering sat behind a TODO
asking for CLI wiring (the old positional-bool signature always received
false). generate_entry now takes an EntryKind selected by three mutually
exclusive flags, and is async because the backport numbering derives the
Debian release number of the target series from distro-info:

- --backport: 1.0-1 becomes 1.0-1~bpo12+1 (12 = release number of the
  target series, backport suite names accepted too). Re-running on an
  already-numbered version bumps the counter; series without a numeric
  Debian release (sid, Ubuntu series, UNRELEASED) are rejected before
  anything is written.
- --nmu: 1.0-1 becomes 1.0-1.1 (native 1.0 becomes 1.0+nmu1).
- --rebuild: 1.0-1 becomes 1.0-1build1.

An explicit --version overrides all three. compute_new_version went
from four positional bools to a private Bump enum; backport numbering
reuses increment_suffix with a '~bpoNN+' suffix. The CLI prints the
computed new version before opening the editor.
2026-09-19 20:56:03 +02:00
vhaudiquet 47bb7c608e deb: resolve cross pkg-config against the target multiarch
CI / build (push) Successful in 2m54s
CI / test (push) Skipped
CI / snap (push) Successful in 4m32s
In-tree tools locate their libraries with the *host* pkg-config during
cross builds (the kernel's tools/build feature checks derive their
cflags/ldflags from 'pkg-config --cflags/--libs'), whose search path
only covers the build architecture's pkgconfig dirs. Cross builds of
linux-riscv died in rtla's Makefile.config: libtraceevent/libtracefs
were reported missing although the riscv64 -dev packages were
installed, because pkg-config never saw their .pc files.

The host-arch -dev packages that used to make those checks pass came
from the unscoped arch-indep build-dep pass, whose native
re-resolution b34e86d correctly scoped to the host arch — removing the
accidental co-install along with the bug it papered over. Export
PKG_CONFIG_LIBDIR for the target multiarch instead, so the checks
resolve target-arch libraries directly: no native build-dep bloat and
no wrong-arch linking.
2026-09-19 13:07:46 +02:00
vhaudiquet 6caedce61a report: reproduce the pre-refactor CLI output through the ports
Instead of carrying raw UI in core, the ports now represent everything
the CLI used to do inline:

- Prompter::present shows context outside of a question (the wizard
  summary screen, the vendoring notice spacing); TerminalPrompter
  prints it on stdout exactly like the println!s it replaces, server
  embeds forward it as a display event.
- generate_entry returns the generated entry (package, versions,
  series, path) instead of printing; the CLI renders the same lines.
- BuildTarget carries a flow-composed display line and a tee_log flag:
  the terminal adapter renders it verbatim ("Building source package
  ...", "Building ... for series/arch", "Uploading ... to ...") and
  uploads open no build log.
- The unmet build-dependency diagnostics are rendered by the CLI from
  the typed error, in the original order (details, then summary).
- --verbose constructs no live view at all (an idle widget used to
  linger), and the re-vendor offer only logs when it is actually
  asked, so headless runs print the error exactly once.
2026-09-19 00:52:31 +02:00
vhaudiquet bd8f814a53 Revert "package_info: drop the ANSI color from the not-found warning"
This reverts commit 4fae02bc85.
2026-09-19 00:39:29 +02:00
vhaudiquet 4fae02bc85 package_info: drop the ANSI color from the not-found warning
The location embedded in the log record was styled with crossterm: a
remote consumer of pkh's log records would receive ANSI codes inside
the message text. Plain text is the logger's business to style.
2026-09-18 20:55:56 +02:00
vhaudiquet c2dae4f3f9 lift main.rs business logic into the library
The chlog target-series resolution becomes changelog::series_candidates
(UNRELEASED pinning, development-series default and fallbacks modeled
by SeriesCandidates), PPA references get package_info::split_ppa
(shared by pull and deb, now also rejecting empty parts), and SSH
endpoints get context::ContextConfig::from_endpoint — so a library
consumer can resolve series, validate PPAs and build context
configurations without reimplementing the CLI's rules. All three carry
unit tests; main.rs keeps only parsing of flags and error handling.
2026-09-18 20:51:36 +02:00
vhaudiquet 0421a91e01 changelog: report through the log instead of printing to stdout
generate_entry's status messages become log lines, and the unmet
build-dependency diagnostics travel inside the UnmetBuildDependencies
error (its Display carries the full report) instead of being printed
to stderr by the library: the caller renders both like any other
outcome.
2026-09-18 20:44:05 +02:00
vhaudiquet 54cb04ba27 put: report through the view and ask the host-key question through the Prompter
put() loses its MultiProgress parameter: the summary, pre-flight and
connection spinners become view messages, the per-file SFTP transfer
reports determinate progress through view.progress (upload_file takes
a byte-count callback instead of an indicatif bar), and the display is
released through view.suspend on every exit path. The hardcoded
trust-on-first-use prompt in the SSH host-key verification becomes the
Prompter::accept_host_key port (fail-closed by default; the terminal
prompter prints the authenticity banner and confirms), so a remote
frontend can surface its own host-key dialog.
2026-09-18 20:42:17 +02:00
vhaudiquet bb76e41908 new: drive the wizard and verification offers through the Prompter port
The interactive half of pkh new no longer touches the terminal prompt
module directly: run() takes a Prompter, picks the wizard or the plain
resolve path through interactive(), and every select/text/confirm
question (including the verification offers) goes through the port.
Cancellations propagate as Err, preserving Ctrl+C-aborts; the summary
and vendoring-notice prints become log lines. A builder-server embed
can now drive the whole scaffold wizard over its own wire format by
implementing Prompter.
2026-09-18 20:38:05 +02:00
vhaudiquet d64e472845 report: grow the Prompter port and move display_path out of the ui module
Prompter gains interactive(), select() and text() (with the Validator
type), and confirm() now propagates cancellation as Err so flows abort
instead of silently taking a default when the user hits Ctrl+C. The
terminal prompter implements the full port; the port also re-exports
the path display helper, which is pure presentation formatting used by
events and messages rather than terminal code.
2026-09-18 20:34:33 +02:00
vhaudiquet 052c02cdc3 deb: drive build_binary_package through the BuildView port
The binary build joins the source build on the reporting ports:
build_binary_package takes a DebBuildOptions struct (replacing eleven
positional arguments), reports target, phases, progress and the
outcome through the environment-agnostic BuildView, and the Phase enum
with its default classifiers moves from the terminal widget into the
deb module (announced through the enter_phase helper). DebUi loses its
inherent event methods and only implements the port; tee logging and
the SIGINT behavior are unchanged.

No behavior change for the CLI; headless consumers pass report::Quiet.
2026-09-18 20:22:45 +02:00
vhaudiquet 27b1083b15 report: add BuildView/Prompter ports and drive pkh build through them
Core flows no longer reach into the terminal UI: build_source_package
takes a BuildSourceOptions struct (source tree, domain options, view,
prompter) and reports phases, messages and outcomes through the
environment-agnostic ports in the new report module. The classifiers
move from ui/logfmt to the core logfmt module, DebUi becomes a
BuildView adapter, the re-vendor retry asks the prompter instead of
checking for a TTY, and artifact/success printing moves to the CLI.

Headless consumers pass report::Quiet; an embedding (e.g. a builder
server forwarding events to a web frontend) implements BuildView and
maps the plain-data events onto its own wire format.
2026-09-18 20:02:56 +02:00
vhaudiquet a7d2cfdc6e ci: pin upload-artifact to v3
CI / build (push) Successful in 2m57s
CI / test (push) Skipped
CI / snap (push) Successful in 4m29s
v4's GHES check refuses to run on any non-github.com server, so the
snap artifact upload always failed on gitea; v3 uses the artifact API
gitea implements.
2026-09-18 18:07:54 +02:00
vhaudiquet ff635b305b ci: install nodejs in the snap job so JS-based actions can run
CI / build (push) Successful in 2m55s
CI / test (push) Skipped
CI / snap (push) Failing after 4m19s
2026-09-18 16:33:16 +02:00
vhaudiquet 781ed204c2 clippy: write the changelog body loop as while let (rust 1.98)
CI / build (push) Successful in 2m53s
CI / test (push) Skipped
CI / snap (push) Failing after 9s
2026-09-18 15:43:12 +02:00
vhaudiquet 0235ec6457 new: port the python template bodies to manifests
CI / build (push) Failing after 2m55s
CI / test (push) Skipped
CI / snap (push) Skipped
2026-09-18 15:20:22 +02:00
vhaudiquet fa399f64b2 new: port the meson, cmake and autotools template bodies to manifests 2026-09-18 15:08:27 +02:00
vhaudiquet e6f2012835 new: port the rust template bodies to manifests 2026-09-18 15:05:44 +02:00
vhaudiquet ffac4d6b57 new: port the shell, empty, makefile and go templates to manifests
Move the four templates' static file bodies into .tpl files under
data/templates/<id>/, referenced by their manifests' files: lists —
the shell skeleton script (executable, {command}-named) with its
skeleton-only debian/install mapping, the empty template's stub README,
the makefile hello.c/Makefile skeleton with its skeleton-only install
mapping, and go's go.mod/main.go skeleton (the go directive of go.mod
stays a literal: nothing about it is answer-derived).

The empty template ends up hookless — zero Rust, its registry entry
points at no hooks — and src/new/templates/empty.rs is deleted. The
shell and go hooks shrink to their probes (plus go's {go_import_path}
context value); the makefile hooks keep only the existing-tree hint
probing the packaged Makefile for a phony install: target, since that
heuristic reads the tree and cannot be data.
2026-09-18 14:58:13 +02:00
vhaudiquet 04a572cd77 new: template manifests and registry infrastructure
Split the Template trait into a data half and a logic half. Every
template is now declared by a manifest under data/templates/<id>/
(CLI id, wizard label, detection markers, Build-Depends, architecture,
rules dh line, rules-extra body, control source fields, gitignore
entries and static file bodies with {placeholder} substitution),
embedded through the TEMPLATE_SOURCES index and parsed once into the
registry; the order of the index is the wizard menu order and the
detection priority at once. The logic half is the slim TemplateHooks
trait (probe, post_write, file-body overrides merged over the manifest
bodies by path shadowing, Build-Depends/architecture amendments and
extra context values), registered per template as a HOOKS static: a
template without hooks needs zero Rust.

- TemplateId becomes a Copy wrapper of the stable CLI string; the
  enum, its all/as_str/display_name/from_label matches and the old
  statics array collapse into the registry accessors.
- rust's rules overrides move to data/templates/rust/rules.extra.tpl
  with {locked}/{artifact} hook context; python's backend table,
  meson/cmake's pkg-config opt-in, autotools' gettext and python's
  C-extension hints become hook amendments over the manifest baseline.
- detect.rs drops its hardcoded marker cascade: the manifests'
  detect.files drive detection in registry order, with the shell
  single-script heuristic and the never-detected empty template kept
  as the code special cases they are. License sniffing is untouched.
- The template tests port to manifest validation: registry coverage
  and stable order, placeholder presence in the rendering context,
  rules composition, the Build-Depends/architecture/dh-line table now
  asserted against the manifest data, and the hook shadowing merge.

The static skeleton bodies of the shell/empty/makefile/go templates
stay in their Rust hooks for now; the next commit moves them into
their manifests.
2026-09-18 14:46:28 +02:00
vhaudiquet fc0d2f247e build: resolve the default build profiles from the distro data 2026-09-18 13:58:57 +02:00
vhaudiquet ccd2e37385 deb: drive the cross-build repository setup from the distro data 2026-09-18 13:58:27 +02:00
vhaudiquet 52d5ad064f deb: match archive sources and the universe gate through the distro mirror data 2026-09-18 13:57:22 +02:00
vhaudiquet b8b2be5acf distro_info: model distro mirrors, components and build profiles in the yaml data 2026-09-18 13:56:36 +02:00
vhaudiquet dd1c70c91a new: drive the license menu, parsing and sniffing from data/licenses.yml 2026-09-18 13:52:24 +02:00
vhaudiquet 12407c8eac new: drive the forge hosts and tarball templates from data/forges.yml 2026-09-18 13:30:42 +02:00
vhaudiquet 7601524b7c apt: move the keyserver lookup URL to data/keyserver.yml 2026-09-18 13:28:05 +02:00
vhaudiquet 7af767898e launchpad: drive the endpoints from data/launchpad.yml 2026-09-18 13:27:20 +02:00
vhaudiquet ead97e1213 data: consolidate the YAML embed convention into an embed_data! macro 2026-09-18 13:22:13 +02:00
vhaudiquet 6e5ecd2f45 data: move the YAML data files into a top-level data/ directory 2026-09-18 13:15:29 +02:00
vhaudiquet 69f3c3954e new: build the target-distribution menu from supported_dists 2026-09-18 13:10:46 +02:00
vhaudiquet c106ebe315 apt: query the Launchpad PPA API through the shared HTTP client 2026-09-18 13:09:46 +02:00
vhaudiquet 941f91decf distro_info: read the YAML local series path, drop the dead dist_info stanza 2026-09-18 13:09:02 +02:00
vhaudiquet b489db2728 quirks: handle packages without quirks in deb extra dependencies 2026-09-18 13:08:23 +02:00
vhaudiquet c1f8893576 put: run the section pre-flight against the uploaded tree
CI / build (push) Failing after 2m57s
CI / test (push) Skipped
CI / snap (push) Skipped
The Section check always read debian/control from the current working
directory, so 'pkh put --changes ../other/pkg_changes' validated the
wrong tree. With an explicit --changes the check now runs against that
file's own directory when it holds debian/control, and is skipped with
a warning otherwise; tree uploads are unchanged.
2026-09-18 10:40:48 +02:00
vhaudiquet e640b153bd put: remove partial uploads when the transfer fails
A failed or interrupted upload left the already-uploaded payloads — or
a truncated .changes — in the PPA's incoming area. On failure the
already-uploaded files are now removed best-effort in reverse upload
order with the failed file first, so a .changes never outlives the
payloads it references; the original upload error keeps precedence over
cleanup failures, and record-after-success semantics are unchanged (a
failed upload must not count as uploaded).
2026-09-18 10:36:01 +02:00
vhaudiquet 231c478d0b put: surface known_hosts problems instead of silently degrading
An unreadable or unparsable known_hosts file was swallowed with
'let _', silently downgrading to prompt-and-accept without telling the
user why their configuration was ignored: warn naming the file, then
continue. And when the pinned Launchpad fingerprint matches, a
DIFFERENT key recorded for that host in known_hosts was silently
bypassed: warn about the stale entry (diagnostic only — the published
fingerprint stays authoritative).
2026-09-18 10:28:34 +02:00
vhaudiquet 1aa0ca3d2f clippy: collapse nested ifs into match guards (rust 1.98) 2026-09-18 10:19:16 +02:00
vhaudiquet b20acf3199 put: bound the SSH connect and session operations with timeouts
TcpStream::connect and the blocking libssh2 session had no timeouts: a
black-holed host hung pkh put forever, mid-resolution, mid-handshake or
mid-upload. Connect attempts now get a 15 s timeout per resolved
address, the session gets a 30 s API timeout for the handshake/auth
phase and a 300 s per-call timeout for SFTP operations (per low-level
libssh2 call, not per transfer — documented); failures name the
operation and host.
2026-09-18 02:03:20 +02:00
vhaudiquet 3feca504fc put: back up a corrupt uploads.json and write it atomically
A corrupt upload log was silently treated as 'never uploaded',
disabling the duplicate-upload guard without a diagnostic, and
record_upload truncated the file in place — a crash mid-write produced
exactly that corrupt state. Parse failures now log an error, back the
file up to uploads.json.bak (so a later successful upload cannot
destroy the recoverable history) and continue with an empty log; the
log itself is written to a temp file and renamed into place.
2026-09-18 01:53:52 +02:00
vhaudiquet 85f0d7d92f put: paginate the Launchpad published-sources lookup
The superseded check read only the first getPublishedSources page
(Launchpad defaults to 75 entries per page), so a source with a long
publication history could hide its true maximum published version and
let a superseded upload through, only to be rejected by the queue
hours later. Follow next_collection_link (ws.size=100, hard cap of 20
pages beyond which the check errors rather than risk a false 'not
superseded').
2026-09-18 01:49:50 +02:00