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

.
feat(modules): describe every module with a manifest
A directory is a module because it carries module.php. The manifest holds
what the system has to know before anything of the module is loaded: the
key (vendor/name, the directory and the package name), the alias (the flat
name a Twig namespace, a gettext domain and a migration source are made of),
what to call it, its version and whether it may be switched off at all.

The loader refuses rather than skips. A key that does not match the
directory it lies in, a key that is not vendor/name, an alias holding a
slash, a field of the wrong type, a file returning something other than an
array — each is answered with the file name and the reason. A module
dropped because of a typo would be routes, tables and permissions that
silently stop existing, and the failure would surface somewhere else.

The repository is the opposite: a directory with no manifest is not a
module and is passed over, so an unpacked archive or a leftover of the
one-level layout cannot stop the boot.

Nothing reads any of this yet — the registry that will is the next step.
The fields it does not need yet are absent on purpose: requirements arrive
with the compatibility check, autoload with the module autoloader, assets
with their publishing. A field nobody reads is dead code repeated in 21
files.
.
refactor(modules): move the bundled modules under a vendor directory
modules/<module>/ becomes modules/johncms/<module>/. A module gets a vendor
of its own, so two authors may publish a module of the same name without
colliding — the ground the installable-module work needs before anything
else can be built on it.

Only paths move. The template namespace (@forum), the translation domain
(d__('forum', …)) and the migration source (--source=forum) are the name
alone and stay what they were: nothing in a template, a dictionary or the
migrations table is rewritten, and a site upgrading from 9.9 keeps the
journal it already has.

What that costs elsewhere:

* the glob patterns of the container, the router, the migration sources,
the language file manager and i18n:translate look two levels deep;
* TemplatePathRegistry is given module keys and takes the namespace as the
basename of one, so a theme still overrides templates/<name>;
* the _module attribute of a route now carries the key (johncms/forum).
ModuleContext resolves the directory from it and the domain from its
basename — the route has to say where the dictionaries lie, and the name
alone no longer does;
* paths in composer.json, phpstan-baseline.neon, translate.xml.dist,
crowdin.yml, phpunit.xml.dist, phpcs.xml.dist, modules.global.php and
ide-twig.json.

CHANGELOG and the documentation submodule carry the upgrade instruction:
the old one-level directories are not picked up, but have to be deleted by
hand after unpacking.
.
ci(tests): build the assets before the functional suite
The suite renders real pages, and every layout asks Vite for the entry point of its theme. A runner has no compiled bundle, so each of them failed on the missing manifest — 43 of 72 tests. The job builds the assets the way the Node one does.
.
test(functional): check a route's permission from an account that holds it
The suite could only drive requests as a guest, so a gate was pinned by the refusal alone; an account now says which permission it holds and the way in is covered too. Runs the functional suite in CI, and writes down how a test is written in .agents/testing.md.
.
test(functional): run the functional suite against SQLite instead of the local stand
The suite used to drive requests against the database of the local stand: without one every test skipped itself, tests looked for an account the installation happened to have, and each of them had to remove what it wrote. It now boots the application against SQLite in memory, builds the schema with the migrations of the core and of every module, seeds the system roles, and rolls back the transaction of every test.

FunctionalUserFactory writes the accounts, so a test says which permissions its visitor holds instead of borrowing a role from an installation, and Fixture fills in the NOT NULL columns the legacy tables are full of. The comments pages create what each of them asks for and are really rendered now, rather than skipping for want of a row.
.
feat(database): let the schema adapter write to a database with global index names
Index names come from 9.x, where a table names its key after the column it covers, so a dozen tables have an index called user_id. MySQL keeps such names per table; SQLite keeps them per database, so the baseline migrations could not be run on it at all. SchemaPlatform collects what differs between databases — the word index and the scope of an index name — and prefixes the table where the namespace is shared. MySQL is unaffected.
.
refactor(auth): split the upgrade to roles by what may be repeated
The four commands an administrator had to run in the right order become two, told apart
by whether running them again is safe.

auth:sync-roles creates the built-in roles this version declares and grants them the
default permissions they are missing. Both halves only add, so it is meant to be run
after every update - a release or a module that declares a new role or permission has no
other way of reaching a site that is already installed. Only a fresh installation used to
get this, from the installer.

auth:migrate-legacy-access does what has to happen exactly once: it seeds the roles,
gives every member of staff the one their access level stood for, and turns the mod_*
settings into permissions. The second half revokes as well as grants - it brings the
roles back to what those settings say - so repeating it would undo whatever was arranged
in the panel afterwards.

Whether there is anything left to do is read off the database: while users.rights is
there the conversion is unfinished, and once the column has gone it can never be needed
again. That replaces the flag in a local config file, so OneTimeTaskTracker and
one_time_tasks.local.php are gone, and the guard of auth:drop-legacy-columns is now the
thing it actually cares about - nobody left with an access level and no role.

This also restores something the schema move had dropped: auth:upgrade-schema used to
seed the built-in roles, and after it was deleted nothing did that for an upgrading site.
It would have failed on the first command of the bridge, telling the administrator to run
a command that no longer exists.
.
refactor(forum): drop the legacy link converter
forum:normalize-message-links converted the posts of a site moving from 9.8 to 9.9:
it unwrapped the /redirect/ interstitial around the site's own links and rewrote the
query-string links of the older forum. 10.0 can only be reached from 9.9, where that
work is behind everyone.

ForumMessageLinkNormalizer goes with it - it existed to serve that command and nothing
else called it. The static analysis baseline loses the entries of both, and the
documentation of the upgrade no longer promises that migrate fills in slugs.
.
refactor(database): drop the slug migrations, the conversion belongs to 9.9
They could never do anything. The installer of 9.9 already created the slug columns and
their unique keys, and a site that came to 9.9 from 9.8 had them filled by the very
commands this replaced - and 10.0 can only be reached from 9.9. On a fresh installation
the baseline creates the columns and there are no rows; on an upgrading one everything
is already there.

The commands stay deleted: they converted 9.8 to 9.9, which is a path 10.0 no longer
has, the same reason the pre-9.9 install scripts were dropped.

The parity test keeps running only the baseline of a source. The fixture is a record of
what the old installer built, and the first migration written after the baseline would
otherwise break it with a confusing failure.
.
docs(database): write down the migration rules and bump the documentation
A guide for whoever writes the next migration: where the files live, what a migration
is given and what it must never reach for, and why - it is a historical record that has
to run unchanged on a site upgrading years from now. The module scaffold gains its
migrations directory, and the documentation submodule follows.