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

.
refactor(database): turn the slug converters into migrations
library:generate-slugs and downloads:generate-slugs were one-time commands an
administrator had to know about, find in the changelog and run by hand. They are
migrations of their modules now: the upgrade runs them along with everything else, and
the journal is what remembers they are done - not a flag in a local config file that a
restored dump knows nothing about.

They fill only what is empty, where the commands regenerated everything. A slug is the
address a page already has, so one that is set - by the old command or by hand - is
kept, and the ones generated around it reserve it and step out of its way.

forum:normalize-message-links stays a command. It asks which hosts count as internal,
falling back to the configured homeurl, and it works through two services of the module
and a model. A migration may depend on none of those: it has to run unchanged years
from now, when all three have moved on.

The parity test now runs the baseline of a source rather than everything in it. The
fixture is a record of what the old installer built, and a migration written after the
baseline is meant to take the schema somewhere that code never went.
.
feat(admin): say when the database is behind the code
An administrator who unpacked a new version had no way of learning that the database
still had to be brought up to it, short of reading the changelog and reaching for a
terminal. The admin panel now says so on every page, and offers the page that puts it
right.

The notice is shown only to whoever may open the maintenance page - to anybody else it
would be about something they cannot act on - and only while something is actually
waiting. What it counts is cached for a minute and dropped the moment a run finishes,
so it goes away with the migration rather than with the expiry, and a page of the panel
does not pay for a directory listing and a query.

A migration nobody can read is logged and counted as none: this is a notice on a page,
and taking down every page of the panel with it would hide the problem behind a worse
one.

migrate itself joins the maintenance page as a background task, so a site with no shell
can update its database from the browser. Background because on a large site the schema
takes longer than a web server waits, and a queued task cannot be started twice by a
reloaded page.

The language templates are regenerated, which also catches up with the line references
the schema move left behind.
.
refactor(database): move the schema into baseline migrations
The installer no longer builds the tables itself. A fresh installation and an
existing site upgrading to 10.0 now take the same path: the migrations of the core
and of every module. The four places the schema used to be described in - the
installer, AuthSchema, MailSchema and the install() of each module - are gone.

The baseline is a snapshot rather than a step, and the one place idempotence is
allowed: it creates what is missing and leaves the rest alone, so a site upgrading
from 9.9 arrives at the schema a fresh one is built with. Every migration after it is
a plain step forward.

BaselineSchemaParityTest holds the old code to account: the fixture is the exact MySQL
the installer and the module installers produced on a fresh database, recorded from
that code before it was deleted, and the migrations have to produce the same, table by
table. Verified against a real server as well - a database built from the migrations
and the one built by the old installer have the same 60 tables, with the same columns,
keys and options; five tables list their keys in a different order, which MySQL does
not care about.

The port grew two questions the baseline needs to be able to skip its own work:
hasForeignKey(), because a database may satisfy a foreign key with an index it already
has and then no index carries the name of the constraint, and fullText(), the word
index the forum and the library search with - left out on a database that has no such
thing.

AuthSchema and MailSchema also held the names of their tables, which models and queries
spell with them; those names live on in AuthTables and MailTables.

Tests build their schema by running migrations (Tests\Support\RunsMigrations) instead
of calling a second description of the same tables. A test may ask for single
migrations of a source, and for the core it has to: its index names come from 9.x,
where several tables name an index user_id, which MySQL keeps per table and SQLite per
database.
.
feat(database): add the migration engine
Migrations live in system/migrations and modules/<module>/migrations, one file per
step returning an anonymous class extending Migration. A migration is given a schema
and a connection and nothing else: it has to keep running unchanged on a site
upgrading years from now, and a model or a service it reached for will have moved on.

The journal is a table of the database it describes, so it travels with a restored
dump instead of being left behind in a config file. A row is identified by its source
and version rather than by a file name: two modules may well reach the same minute,
and the words after the timestamp are a description an author may correct.

Sources come from MigrationSourceProviderInterface, tagged by the container, so a
package that ships tables of its own joins the run without a line changed here. The
core is asked first and the modules after it - a module may point at the tables of the
core and the core points at nobody's.

A run takes a lock, applies one migration at a time and journals each before calling
it done. A failure stops the run and is not journalled, and the message says the step
may have carried out part of its work: MySQL does not roll back DDL.

Commands: migrate, migrate:status, migrate:rollback and make:migration. Rolling back
is a tool of development - it undoes a step the data has already been through, so a
migration that says nothing refuses instead, and a site not in debug mode has to pass
--force.
.
feat(database): add the connection and schema ports
Two contracts for the code that has to outlive the ORM: ConnectionInterface for
running a query, and SchemaInterface with a neutral table description for creating
and altering tables. Neither names the library underneath.

The description covers what the CMS actually uses - 19 column types, five modifiers,
four kinds of key - and deliberately leaves out platform-only idioms such as column
positioning, which no portable adapter could carry out.

Behind the ports, IlluminateSchema and BlueprintCompiler translate to the Eloquent
schema builder: the same builder that created the tables of every existing
installation, so a table built through the port is identical to the one there now.
MysqlDdlTest pins that DDL statement by statement, against a connection double that
cannot reach a server.

The description is excluded from the directory load of the container: its value
objects carry a column name and a length, and autowiring them breaks the
compilation.
.
fix(admin): autowire the config repositories writing local files
FileCaptchaConfigRepository and FileSystemAuthConfigRepository take a LocalConfigWriter, but both were registered with set() without autowire(), so the constructor argument was never injected and the captcha settings page ended in an ArgumentCountError.
.
Merge remote-tracking branch 'origin/10.x' into 10.x
# Conflicts:
# composer.lock
# docs
# system/src/Container/PSRContainerFactory.php
.
docs(captcha): bump the documentation submodule
.
feat(admin): add the captcha settings page
.
feat(captcha): add the recaptcha v3, smartcaptcha and hcaptcha providers