История коммитов

.
docs(agents): pin the behaviour of untested code before refactoring it
Most legacy modules have no tests, and a refactoring checked only by tests written alongside it says nothing about whether the behaviour was kept. The testing guide now asks for characterization tests committed ahead of the change, and the refactoring principles point there.
.
docs: require the database versions the system checks and bump the documentation submodule
README asked for MySQL 5.6.4, while the system check refuses anything below MySQL 5.7 or MariaDB 10.2, the oldest servers CI runs the suite against. The submodule corrects the same on the installation page, together with the version of PHP.
.
test(functional): run the functional suite against MySQL and MariaDB
The suite ran on SQLite alone while every site runs on MySQL or MariaDB, so a
migration, a query or a page that only those break on went unseen. CI now runs
the functional suite once more against MySQL 5.7 and 8.4 and MariaDB 10.2 and
11: the oldest servers the system check accepts and a current one of each.

TEST_DATABASE_URL points the harness at a server. The suite drops that database
and builds the schema anew with the migrations, so FunctionalDatabase refuses a
name that does not end in _test.

MySQL commits the open transaction before DDL and TRUNCATE, and what a test
wrote until then stayed for every test after it. The harness now fails the test
whose transaction was gone by the time it ended. The first run caught the system
check, which created a table on every visit.

The crawl opens the pages SQLite cannot serve when it runs against MySQL.
.
fix(admin): check the database driver without creating a table
The system check made sure the driver returns integers as integers by creating
a table, writing a row, reading it back and dropping the table, on every visit
to the page. A host that grants no right to create tables got a broken page,
and under MySQL the DDL committed whatever transaction was open.

A number selected without a table tells as much about the driver: mysqlnd
hands it back as an int, a driver that stringifies hands back a string.
.
fix(mail): build the file service with its storage
The services file of the module declared MailFileService on a line of its own,
after the load() that already registered it. That line replaced the autowired
definition with a bare one, which went unnoticed while the constructor took
nothing. Since the service got its storage, every page asking for it failed with
an ArgumentCountError: a conversation, sending a message, the list of files and
every delete.

The line is gone; load() registers the service as it does the rest.

The container test now holds every definition to the arguments its constructor
requires. Such a definition compiles fine and fails only on the page that needs
it, so nothing else in the gate saw this one.
.
test(functional): open every page as a guest, a member and the staff
RouteCrawlTest reads the routes from the router and opens every GET route that
needs no placeholder filled, three times over: 145 routes, 434 visits. A new page
is covered the moment it is declared. Every page has to answer without a server
error, finish the document it starts and put the CSRF token into each form that
posts; the panel has to let nobody in but the staff, and the staff has to be
refused nothing. The one page left out for the staff reads the version of MySQL,
which SQLite does not have.

When a page answers 500, the test asks again with catch: false, a new argument
of handleRequest(), and reports the exception the kernel would have logged, so
the cause shows in CI as well.

FunctionalUserFactory hashes the shared password once per process. bcrypt costs
a tenth of a second here, and the crawl alone signs in about three hundred
accounts: 46 seconds against 6.
.
docs: describe the full gate and bump the documentation submodule
composer check now runs the Twig checks as well, and the guides said less than
that. CONTRIBUTING lists what it runs and asks for the assets to be built first,
since the functional tests render real pages; AGENTS.md names the Twig checks in
the gate; the module guide says a command is tagged console.command and names
itself in #[AsCommand].

The submodule brings the page on console commands up to date: how a command is
registered, why it is built only when run, and the commands the list was missing.
.
ci: run the same gate as verify.sh
CI ran the coding standard and the two suites, while the local gate also ran
PHPStan, the Twig lint and the check of ide-twig.json. A change pushed without
the local gate could break static analysis or a template and still pass.

composer check is now the one list of checks: cs-check, phpstan, test,
twig-lint and twig-ide-check. verify.sh runs it inside the container and CI runs
it directly, so the two cannot drift apart. The three jobs become one that builds
the assets and runs that list; -v names every step before it runs.

Node CI is gone: the job builds the assets on every push and pull request.

PHPUnit runs with memory_limit=512M. Both suites now share one process in CI,
and with cold caches their peak sits just under the 128M most php.ini files
default to — a fresh checkout died where a warm one passed.
.
fix(database): exit with a failure status when the connection fails
The factory of the connection printed its error with die(), and a string given
to die() ends the process with status 0. A console command or a cron job that
lost its database reported success, and so did a PHPUnit run: a test reaching
for the connection where there is none cut the run short, the tests after it
never ran, and the pipeline stayed green.

The message is printed as before, and the process exits with status 1.
.
fix(console): build a command only when it is run
The console built every command on start, and every command built whatever it
depends on. One of them reaches the schema builder, which connects to the
database, so without a database no command worked at all: twig:lint, cache:clear
and even list answered with "Error connecting to the database server".

Commands are now collected by AddConsoleCommandPass of symfony/console. It reads
the name and the description of #[AsCommand] at compile time and hands the
application a loader of lazy commands, so listing them builds nothing and running
one builds that one alone. A command that names itself in configure() cannot be
listed unbuilt and is still built on every start.

The tag of commands becomes the one of Symfony, console.command, in place of
johncms.console_command. A module whose commands are autoconfigured has nothing
to change.